-
Notifications
You must be signed in to change notification settings - Fork 287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ControlLoop for controller manager #728
Conversation
ac4d425
to
e129d3f
Compare
Please refer to the diagram and discussion in the related issue for reference to understanding this change. |
85bf7ab
to
6afb3eb
Compare
0910a39
to
a19eb9e
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked through the tests and made some suggestions around naming.
This pull request is in conflict. Could you fix it @tylerjw? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We actually never planned for this control loop to grow so much, but this makes sense for sure! Thanks for the good explanations and for adding docs diagrams. Could you put those into the docs folder? And reference it so it does not get lost.
In general, I find the changes too complex only for the sake of testing. Is it possible to do this simpler?
*/ | ||
template < | ||
typename NowInvocable, typename OkInvocable, typename WorkInvocable, typename SleepInvocable> | ||
void ControlLoop( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH, I am not certain that this code deserves it own file. Why not add it as a method into the ".cpp" file. This should also be testable then.
Furthermore, do we really need methods as arguments? IMHO, for this simple functionality this looks very overengineered.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would not be testable if put in the .cpp
file because I need to create a new version of it in the translation unit that is the test. This is the reason that templates always have to go in a header file if you want to use them in two separate translation units.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added static_assert statements to this to make the compile errors nicer if a user tries to use this function and makes a mistake in the sort of function-like objects they pass into this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Furthermore, do we really need methods as arguments? IMHO, for this simple functionality this looks very overengineered.
Do you have a suggestion for how to make this behavior testable without methods as arguments? IMHO, code that is not designed to be tested is almost always broken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code that is not designed to be tested is almost always broken.
bit heavy-handed, isn't it?
I agree testability is good, but you cannot generalise like you're doing here. It isn't very constructive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry for being sarcastic and mean here. Please let me know what feels overly complex or over engineered. I'm happy to make changes or add documentation to make this code more readable or better in any way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no need to apologise Tyler.
Obviously introducing tests is better than not doing it, and adapting code structure to allow for testing is better than not doing it.
There are different ways of testing though, and unit tests are only one of them. Not all of them require dependency injection or mocking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried testing this loop externally with an integration test and was unable to figure out how to do it robustly or simply. My current robot driver I'm working on is effectively a test of this but I do think it is way too much code for a test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're misunderstanding me: I'm not telling you this is not a good approach, or you should use a different testing strategy.
I'm saying asserting that only "testable/tested code is fault-free" is not a claim you can make, and that using a different approach than dependency injection/mocking could also be used in some cases.
I'm not saying in this case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"testable/tested code is fault-free" is an exaggeration for sure. As a person who makes a lot of mistakes, and finds writing code that does what I want hard, I find writing tests useful.
Theoretically, tests also help you in the future when you come back to something and want to make a change but not break what already exists.
// wait until we hit the end of the period | ||
end_period += period; | ||
std::this_thread::sleep_for(std::chrono::nanoseconds((end_period - cm->now()).nanoseconds())); | ||
auto const period = std::chrono::nanoseconds(1'000'000'000 / cm->get_update_rate()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice readability detail! Thank you!
std::chrono::system_clock::time_point(std::chrono::nanoseconds(time.nanoseconds()))); | ||
}; | ||
auto const ok = []() { return rclcpp::ok(); }; | ||
auto const do_work = [&cm](rclcpp::Time current_time, rclcpp::Duration period) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although better, I would also not consider this 100% correct. If we do changes here, we should then measure times separately between, “read”, “update” and “write” calls. Maybe calculating different periods is not necessary (but good to have), but we should have at least “current_time_read”, “current_time_update”, “current_time_write”.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could add these separately inside the controller manager by having each of these functions take the time when they are called if you need that for something. Do you already have tooling or reasoning for wanting to expose those timing values?
Let us not add things because they sound good to have but we don't actually have a known use for them yet. This will just make the code have confusing extra variables. I would argue based on the response I got in the WG meeting when asking about the justification for the new time and period parameters passed to read/write that the change to add those is lacking a clear use-case.
}; | ||
auto const ok = []() { return rclcpp::ok(); }; | ||
auto const do_work = [&cm](rclcpp::Time current_time, rclcpp::Duration period) { | ||
// Write is called first as the consistent rate of writing to hardware |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am uncertain if this is, in general, a good idea. This will cause first “write” always with wrong values. I would leave this as it is for now, but we should cover probably the three cases in the future:
- the standard one when hardware read-update-write are executed in sequence
- with sleep before write
- without sleep where communication with the robot dictates the rate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will cause first “write” always with wrong values.
In my experience this is already a problem, this won't change the current behavior where write gets called with invalid values. At least on Galactic with my current driver, I have had to write code in the system interface to guard against this with the current implementation. Nothing about this change fixes that part of the broken behavior.
This is taking a generic programming approach to solving this problem. I generally think correctness and robust testing to be much more valuable than reducing the number of things the programmer has to learn to read the code. I can find you references if you'd like for why I am using template arguments over using I do think this function is complex enough and separate enough from the main function to justify it being in its own file. If this was trivially easy to write in a correct way why was the main loop itself so broken? In my experience, code that is not designed to be tested is almost always trivially broken because it is too hard to program without errors. |
Signed-off-by: Tyler Weaver <tylerjw@gmail.com>
Co-authored-by: G.A. vd. Hoorn <g.a.vanderhoorn@tudelft.nl> Signed-off-by: Tyler Weaver <tylerjw@gmail.com>
Co-authored-by: Nathan Brooks <nbbrooks@gmail.com>
Signed-off-by: Tyler Weaver <tyler@picknik.ai>
Co-authored-by: Denis Štogl <denis@stogl.de>
Signed-off-by: Tyler Weaver <tyler@picknik.ai>
Signed-off-by: Tyler Weaver <tylerjw@gmail.com>
5c05b73
to
aa678d0
Compare
I added an explanation of the control loop to the userdoc.rst and included the diagram.
Is there something specific you think is too complex? I do not understand what about this change is more complex than what is normal in this project. |
0ec8659
to
9c43043
Compare
If you are curious about why I chose to use template arguments for the function parameters here are a couple of links. The basic reason is that because I don't need to store the functions in a runtime collection I use templates. In general, you should always use templates when passing functions if you can because if the type is known at compile time your compiler can do a much better job of optimizing it. If you are curious how std::function works and allows us to store function objects at runtime without inheritance, it uses a technique called type-errasure. The downside to type-erasure is that it has to do a heap allocation and does use virtual function dispatch via inheritance of an internal model template. This trick is a way of having a duck-typed interface in C++ without inheritance in your interface. The best explanation of type-errasure I know of is this Sean Parent talk: I also just updated the function to have static_asserts that will cause compilation failures when the invocable template arguments have the wrong function interface. This should improve the compile errors generated by this code if a user creates their ros2_control_node and makes a mistake when creating the various functions used by the ControlLoop function. |
Signed-off-by: Tyler Weaver <tylerjw@gmail.com>
9c43043
to
2760308
Compare
…face Signed-off-by: Tyler Weaver <tylerjw@gmail.com>
b8b87ab
to
2c11be7
Compare
This pull request is in conflict. Could you fix it @tylerjw? |
This pull request is in conflict. Could you fix it @tylerjw? |
This pull request is in conflict. Could you fix it @tylerjw? |
1 similar comment
This pull request is in conflict. Could you fix it @tylerjw? |
Closing as this is a dead PR. |
Signed-off-by: Tyler Weaver tylerjw@gmail.com
Fixes #726