Skip to content

Implicit contracts make ros2_control hard to use #712

Open
@tylerjw

Description

@tylerjw

Background

I really appreciate all the work that has gone into ros2_control. Recently I've been implementing the various plugins I need to make a robot arm work with ros2_control. Here I will try to summarize my experience and pain points around what you have to do to implement robot control with ros2_control.

Much of this can be summarized as ros2_control is not easy to use correctly because there are many implicit contracts. In order to use it correctly, I had to learn a ton about how it works internally. If we can find ways to reduce the amount people have to know about how the inside of ros2_control is implemented to implement against it their robot drivers will be more robust, the ros2_control team will have more freedom to change the internals, and robot drivers will be easier to write.

Much of this seems to stem from the shared memory for communicating between plugins. Here are things that were surprised to me about the shared memory:

  1. Write starts being called after on_init finishes, not after on_activate. This means that the values in the command vectors need to be valid after on_init because if they are not and invalid values in the command cause the write to fail, the system interface is stopped in a bad state.
  2. It wasn't clear to me who is responsible for initializing the shared memory. I now know it is the responsibility of the system interface (who owns the lifetime of the shared memory) to initialize it.
  3. I didn't expect write to be called with invalid/uninitialized values in the command. This means that in write I need to validate the values in the command before sending them, there is no part of the interface that makes this clear.
  4. When write is called there is no way for me to know what values are new without building tools to detect changes in values or something else.

This whole communication out of band method of sending data down to the system interface by my understanding is done because we want the flexibility to assign any hardware interfaces to any controller and track each one independently. I imagine another reason for the shared memory model is that there is a goal of this project to allocate as much of the memory used by the program up front and avoid all dynamic allocation at runtime to make the execution time of the loop much more deterministic.

There is a misconception that is commonly accepted without testing and that is that copying is slow, so we should design architectures to avoid copying. The reality of what is slow and what is fast in computers is much harder to understand because of how modern CPUs work. In my experience memory locality is often much more important than copying. I am not suggesting that we should introduce dynamic memory allocations into the main loop, but instead replace this shared memory implicit contract with functions that take doubles as arguments, copying the value from one part of the code to the other in a much more explicit way.

Functions for sending new values to system interface

My proposal is we make the sending of data from one layer to the next explicit by replacing the model of keeping track of pointers to doubles with functions. This is how I imagine it would work.

During initialization, the system interface would call export_command_interfaces which would populate a vector of function objects instead of a vector of pointers to double. Those functions would have a signature that looks like this: CallbackReturn(double). Normally the author of the system interface would create lambdas here that will set the internal state variables of the system interface. The primary benefit to this change is that now the system interface has a block of code that executes when the values change, this makes it much more straightforward to track when those values get updated.

Data structures for enforcing different policies with not-updated data

The next thing to do is to create a data structure that represents a set of values to be used by the system interface. The reason for this is to make the policy of when values get written to hardware explicit. When these functions are called they would call an update function on this data structure that would be used in any case where writing to hardware is an atomic operation but always involves a certain set of values (a good example is just joint position values that in my case have to be written together in one message). There is a question of what do you in the case where only some of the values are new when write is called. I forsee there being several separate policies you might want to use when some of the values have not been updated:

  1. do nothing
  2. replace not-updated with what we previously sent and write
  3. replace not-updated with values from the latest read state and write

I think we should provide a data structure that implements these policies and would make the code in write much more trivial to write correctly. Here is an example of how I imagine it could work:

if (position_command.all_valid()) {
  robot->Write(position_command.values());
}

Async Service interface

Secondly, I think we should provide an async service call interface for things like requesting the system interface to clear a protective stop. The way I think this should work is that the hardware interface should register a lambda for these calls that the controller can call that has the same interface that a callback for a service would have.

Trajectory execution succeeding when the robot hardware interface is in a bad state

This may seem like more of an annoyance than a fundamental design decision but I think there should be a way of a controller knowing when what they are commanding is not happening because the system interface they are commanding is in a bad state so it can fail.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions