|
| 1 | +--- |
| 2 | +title: 'Learn ros2_control: from Definition to Implementation' |
| 3 | +publishDate: '2025-05-31' |
| 4 | +updatedDate: '2025-05-31' |
| 5 | +description: 'ros2_control can be understood by everyone, even if you are new to ROS2.' |
| 6 | +tags: |
| 7 | + - ros2 |
| 8 | + - ros2_control |
| 9 | + - control |
| 10 | + - robotics |
| 11 | + - robot |
| 12 | + - c++ |
| 13 | + - cpp |
| 14 | + - hardware |
| 15 | +language: 'English' |
| 16 | +draft: false |
| 17 | +--- |
| 18 | + |
| 19 | +# ros2_control for everyone |
| 20 | + |
| 21 | + |
| 22 | + |
| 23 | +**ros2_control** is a framework system developed in ROS 2 ecosystem for elevating production speed and abstraction to any hardware system. The key concept is **abstraction**. From a single framework, you can empowering any robot project to life. |
| 24 | + |
| 25 | +## 1. The design of ros2_control |
| 26 | + |
| 27 | +1. **Controller Manager (CM)** |
| 28 | +Controller Manager, or in short CM, is the main component in ros2_control and a layer for connecting hardwares to the controllers. It manages the lifecyle of controllers, start and stop of controllers, and initialize the hardware. CM have access to both controllers and Resource Manager. We are configuring CM with yaml file config and manage or interfacing it via ROS service. |
| 29 | + |
| 30 | +2. **Resource Manager (RM)** |
| 31 | +Resource Manager, RM in short, is an abstraction for hardware and the driver. We call driver or program that drive the real hardware as hardware component in ros2_control. We are not coding this RM as it is part of ros2_control system. RM loads hardware component and manages its lifecyle: `on_init()`, `on_configure()`, `on_activate()`, `read()`, `write()`. Node lifecyle is a new concept in ROS2, not available in ROS1, in short it is a node with steroids. Don’t worry to much on lifecyle, we will see it section 3. |
| 32 | + |
| 33 | +3. **Controller Componenents (controller_interface)** |
| 34 | +Controllers is the program that we make to manipulate the hardware_interface. We can make many controllers for different control purpose like, for example, walking, jumping, or kick will use different controller. We seperate each control for easier management. |
| 35 | +We can refer controllers as the same in control theory. Given input values from sensor, it will output the calculated result in the actuator. This controller_interface also use ROS2 lifecyle and a loop function method: `update()`, that access latest hardware state, write to hardware component, and execute at configured frequency in CM yaml file. |
| 36 | + |
| 37 | +4. **Hardware Components (hardware_interface)** |
| 38 | +The hardware component is the driver program that we make to drive, communicate with real, pyhsical hardware, on our robot. hardware_interface represent abstraction in ros2_control. It purpose is to access the real hardware, abstract it, so that we can access the hardware state and hardware command from controller_interface. hardware_interface export hardware state to controller and receive hardware command from controller. There are three types of hardware component: |
| 39 | + |
| 40 | + - `Sensor`: can only read hardware state, e.g.: temperature sensor (read temperature) |
| 41 | + - `Actuator`: can only write command to hardware, e.g.: motor (write pwm signal) |
| 42 | + - `System`: capable of both sensors and actuator, e.g.: servo(read and write position) |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## 2. ROS2 Lifecycle |
| 47 | + |
| 48 | +Node lifecyle, introduced in ROS2, is designed as a system for managing the states of ROS2 node in ordered and predictable way. It is usefull in robotics application where we need full control for the node initialization, startup, shutdown, and the program loop. Used in a system where we need to make sure we need to run the initialization process first and shutdown the program properly so that the robot have the expected behavior. |
| 49 | + |
| 50 | +The lifecycle includes these states: |
| 51 | +- **Unconfigured**: Initial state |
| 52 | +- **Inactive**: Configured but not operational |
| 53 | +- **Active**: Fully operational |
| 54 | +- **Finalized**: Shutting down |
| 55 | + |
| 56 | +Transitions between states are triggered by these functions: |
| 57 | +- `configure()`: Initialize parameters, set up publishers/subscribers |
| 58 | +- `activate()`: Start program execution (e.g., timers) |
| 59 | +- `deactivate()`: Stop execution while retaining configuration |
| 60 | +- `cleanup()`: Release resources and clear data |
| 61 | +- `shutdown()`: Gracefully terminate the program |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | +--- |
| 66 | + |
| 67 | +## 3. Adaptation of ROS2 Lifecyle in hardware_interface and controller_interface |
| 68 | + |
| 69 | +ros2_control use the same principle as the lifecycle. Both controller_interface and hardware_interface adapted the design of node lifecyle with more fine control of the program. |
| 70 | + |
| 71 | +- `on_init()` \ |
| 72 | +Called once when the controller is first instantiated. |
| 73 | +Used for basic initialization tasks like member variable setup. |
| 74 | +**Should not access ROS interfaces** (publishers, subscribers, parameters). |
| 75 | +Failures here prevent the controller from being used. |
| 76 | + |
| 77 | +- `on_configure()` \ |
| 78 | +Transitions the controller from "Unconfigured" to "Inactive" state. |
| 79 | +**Used to load parameters, initialize ROS interfaces, and allocate resources.** |
| 80 | +Sets up communication channels without actively using them. |
| 81 | +Common tasks: creating publishers/subscribers, loading configuration |
| 82 | + |
| 83 | +- `on_activate()` \ |
| 84 | +Transitions the controller from "Inactive" to "Active" state. |
| 85 | +Enables the controller to **start operating and controlling hardware**. |
| 86 | +Typically starts publishing control commands and processing sensor data |
| 87 | + |
| 88 | +- `on_deactivate()`\ |
| 89 | +Transitions the controller from "Active" to "Inactive" state. |
| 90 | +S**tops active control while maintaining configuration.** |
| 91 | +Ensures the controller stops sending commands to hardware. |
| 92 | +Allows for quick reactivation later without reconfiguring. |
| 93 | + |
| 94 | +- `on_error()`\ |
| 95 | +**Called when the controller encounters an error during operation**. |
| 96 | +Provides opportunity for error handling and graceful degradation. |
| 97 | +May attempt recovery or prepare for cleanup/shutdown. |
| 98 | + |
| 99 | +- `on_cleanup()`\ |
| 100 | +Transitions from "Inactive" to "Unconfigured" state. |
| 101 | +**Releases resources acquired during configuration.** |
| 102 | +Undoes what was done in on_configure(). |
| 103 | +Returns the controller to a state where it can be reconfigured. |
| 104 | + |
| 105 | +- `on_shutdown()`\ |
| 106 | +Called when the controller is being terminated. |
| 107 | +Performs final cleanup before destruction. |
| 108 | +Releases any remaining resources. |
| 109 | + |
| 110 | +### 3.1 Main Loop |
| 111 | + |
| 112 | +Both **controller_interface** and **hardware_interface** have main loop that run at the specified frequency. This main loop can only be executed if the state is active, which is after `on_activate()` successfully executed without error. |
| 113 | + |
| 114 | +- `read()`, `write()` \ |
| 115 | +The main loop for **hardware_interface** with type **sensor** only capable of `read()`, **actuator** can only `write()`, and **system** capable of both. |
| 116 | + |
| 117 | +- `update()` \ |
| 118 | +The main loop for **controller_interface**. |
| 119 | + |
| 120 | +### 3.2 Hardware Abstraction Management by **Export and Claim** |
| 121 | + |
| 122 | +Every hardware state or command that we accessed from `hardware_interface` needs to be exported or advertised so that the controller_interface can access it. Based on the type of hardware_interface (Sensor, Actuator, or System), we use `export_state_interface` and `export_command_interface`. For *sensor type* hardware, we can only `export_state_interface`. For *actuator type* hardware, we can only `export_command_interface`. *System type* hardware capable of both. |
| 123 | + |
| 124 | +The Resource Manager handles these exported interfaces and manages which controller can access which hardware resource. Controllers must explicitly claim the interfaces they need with `claim_interfaces()` during activation. This claiming mechanism prevents conflicts where multiple controllers might try to command the same hardware simultaneously. |
| 125 | + |
| 126 | +The flow works as follows: |
| 127 | +1. Hardware components export their interfaces during initialization |
| 128 | +2. Controllers declare which interfaces they want to claim |
| 129 | +3. Resource Manager validates and manages these claims |
| 130 | +4. When activated, controllers gain exclusive access to their claimed command interfaces |
| 131 | +5. Multiple controllers can read state interfaces, but only one controller can write to a command interface |
| 132 | + |
| 133 | +This export-and-claim pattern is central to how ros2_control safely manages hardware resources across different controllers. |
| 134 | + |
| 135 | +#### 3.2.1. **Implementation of state export in `hardware_interface`** |
| 136 | + |
| 137 | +```c++ |
| 138 | +std::vector<hardware_interface::StateInterface> FooHardware::export_state_interfaces() |
| 139 | +{ |
| 140 | + std::vector<hardware_interface::StateInterface> state_interfaces; |
| 141 | + for (unsigned int i = 0; i < info_.joints.size(); i++) { |
| 142 | + state_interfaces.emplace_back(hardware_interface::StateInterface( |
| 143 | + info_.joints[i].name, |
| 144 | + hardware_interface::HW_IF_POSITION, |
| 145 | + &joints_[i].state.position)); |
| 146 | + state_interfaces.emplace_back(hardware_interface::StateInterface( |
| 147 | + info_.joints[i].name, |
| 148 | + hardware_interface::HW_IF_VELOCITY, |
| 149 | + &joints_[i].state.velocity)); |
| 150 | + } |
| 151 | + |
| 152 | + return state_interfaces; |
| 153 | +} |
| 154 | +``` |
| 155 | + |
| 156 | +From the code above, we are exporting the state interface by `export_state_interface()`. The example is from servo which is capable of position and velocity state. Here we have setup servo/ joint name in `info_.joints_` array. Then we push every state interface to `state_interfaces` vector. `hardware_interface::StateInterface(...)` takes prefix name (hardware name), state type (position and velocity), and variable pointer to handle the value we get from `read()` process. |
| 157 | + |
| 158 | +#### 3.2.2. **Implementation of hardware command export in `hardware_interface`** |
| 159 | + |
| 160 | +```cpp |
| 161 | +std::vector<hardware_interface::StateInterface> FooHardware::export_state_interfaces() |
| 162 | +{ |
| 163 | + std::vector<hardware_interface::CommandInterface> command_interfaces; |
| 164 | + for (uint i = 0; i < info_.joints.size(); i++) { |
| 165 | + command_interfaces.emplace_back( |
| 166 | + hardware_interface::CommandInterface( |
| 167 | + info_.joints[i].name, |
| 168 | + hardware_interface::HW_IF_POSITION, |
| 169 | + &joints_[i].command.position)); |
| 170 | + } |
| 171 | + |
| 172 | +return command_interfaces; |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +Similiar to the previous state export, we export the command interface so that the hardware can be commanded from `controller_interface`. The command interfaces takes prefix name (hardware name), command type (position), and variable pointer to handle the value we pass trough to `write()` process. |
| 177 | + |
| 178 | +#### 3.2.3. **Implementation of hardware claim-ing in `controller_interface`** |
| 179 | + |
| 180 | +```cpp |
| 181 | +controller_interface::InterfaceConfiguration |
| 182 | +FooController::command_interface_configuration() const |
| 183 | +{ |
| 184 | + controller_interface::InterfaceConfiguration config; |
| 185 | + config.type = controller_interface::interface_configuration_type::INDIVIDUAL; |
| 186 | + for (const auto& joint_name : joint_names_) |
| 187 | + { |
| 188 | + config.names.push_back(joint_name + "/" + |
| 189 | + hardware_interface::HW_IF_POSITION); |
| 190 | + } |
| 191 | + return config; |
| 192 | +} |
| 193 | + |
| 194 | +controller_interface::InterfaceConfiguration |
| 195 | +FooController::state_interface_configuration() const |
| 196 | +{ |
| 197 | + controller_interface::InterfaceConfiguration config; |
| 198 | + config.type = controller_interface::interface_configuration_type::ALL; |
| 199 | + return config; |
| 200 | +} |
| 201 | +``` |
| 202 | + |
| 203 | +Take a look at `command_interface_configuration()` first. We set the InterfaceConfiguration to INDIVIDUAL. It means we only claim the hardware individually, choosing the hardware what we want to command. The format to claim the hardware is a string of "hardware_name/command_type", e.g.: "head_pan/position" then we push it to the array. |
| 204 | + |
| 205 | +Claiming the hardware state in `state_interface_configuration()` is the same as in command_interface_configuration(). In the example, we set the type to claim to ALL, we claim all available state exported from `hardware_interface`. |
| 206 | + |
| 207 | +> [!IMPORTANT] |
| 208 | +> **Hardware command** can **only** be claimed **once** in any controller_interface. Claiming the hardware command in multiple controller_interface can result in hardware conflict and raise error from the Resource Manager. |
0 commit comments