Writing a Hardware Component
In ros2_control hardware system components are libraries, dynamically loaded by the controller manager using the pluginlib interface. The following is a step-by-step guide to create source files, basic tests, and compile rules for a new hardware interface.
Preparing package
If the package for the hardware interface does not exist, then create it first. The package should have
ament_cmakeas a build type. The easiest way is to search online for the most recent manual. A helpful command to support this process isros2 pkg create. Use the--helpflag for more information on how to use it. There is also an option to create library source files and compile rules to help you in the following steps.Preparing source files
After creating the package, you should have at least
CMakeLists.txtandpackage.xmlfiles in it. Create alsoinclude/<PACKAGE_NAME>/andsrcfolders if they do not already exist. Ininclude/<PACKAGE_NAME>/folder add<robot_hardware_interface_name>.hppand<robot_hardware_interface_name>.cppin thesrcfolder.Adding declarations into header file (.hpp)
Take care that you use header guards. ROS2-style is using
#ifndefand#definepreprocessor directives. (For more information on this, a search engine is your friend :) ).Include
"hardware_interface/$interface_type$_interface.hpp".$interface_type$can beActuator,SensororSystemdepending on the type of hardware you are using. for more details about each type check Hardware Components description.Define a unique namespace for your hardware_interface. This is usually the package name written in
snake_case.Define the class of the hardware_interface, extending
$InterfaceType$Interface, e.g., .. code:: c++ class HardwareInterfaceName : public hardware_interface::$InterfaceType$InterfaceAdd a constructor without parameters and the following public methods implementing
LifecycleNodeInterface:on_configure,on_cleanup,on_shutdown,on_activate,on_deactivate,on_error; and overriding$InterfaceType$Interfacedefinition:on_init,export_state_interfaces,export_command_interfaces,prepare_command_mode_switch(optional),perform_command_mode_switch(optional),read,write.
For further explanation of hardware-lifecycle check the pull request and for exact definitions of methods check the
"hardware_interface/$interface_type$_interface.hpp"header or doxygen documentation for Actuator, Sensor or System.Adding definitions into source file (.cpp)
Include the header file of your hardware interface and add a namespace definition to simplify further development.
Implement
on_initmethod. Here, you should initialize all member variables and process the parameters from theinfoargument. In the first line usually the parentson_initis called to process standard values, like name. This is done using:hardware_interface::(Actuator|Sensor|System)Interface::on_init(info). If all required parameters are set and valid and everything works fine returnCallbackReturn::SUCCESSorreturn CallbackReturn::ERRORotherwise.(Optional) Adding Publisher, Services, etc.
A common requirement for a hardware component is to publish status or diagnostic information without interfering with the real-time control loop.
This allows you to add any standard ROS 2 component (publishers, subscribers, services, timers) to your hardware interface without compromising real-time performance. There are three primary ways to achieve this.
Method 1: Using the Framework-Managed Publisher (Recommended & Simplest for HardwareStatus Messages)
Method 2: Using the Framework-Managed Node (Recommended & Simplest for Custom Messages)
The framework internally creates a dedicated ROS 2 node for each hardware component. Your hardware plugin can then get a handle to this node and use it.
Access and using the Default Node: You can get a
shared_ptrto the node by calling theget_node()method and use it just like any otherrclcpp::Node::SharedPtrto create publishers, timers, etc.// Continuing inside on_configure() if (get_node()) { my_publisher_ = get_node()->create_publisher<std_msgs::msg::String>("~/status", 10); using namespace std::chrono_literals; my_timer_ = get_node()->create_wall_timer(1s, [this]() { std_msgs::msg::String msg; msg.data = "Hardware status update!"; my_publisher_->publish(msg); }); }
Method 3: Using the Executor from `HardwareComponentInterfaceParams`
For more advanced use cases where you need direct control over node creation, the
on_initmethod can be configured to receive aHardwareComponentInterfaceParamsstruct. This struct contains aweak_ptrto theControllerManager’s executor.Update ``on_init`` Signature: First, your hardware interface must override the
on_initversion that takesHardwareComponentInterfaceParams.// In your <robot_hardware_interface_name>.hpp hardware_interface::CallbackReturn on_init( const hardware_interface::HardwareComponentInterfaceParams & params) override;
Lock and Use the Executor: Inside
on_init, you must safely “lock” theweak_ptrto get a usableshared_ptr. You can then create your own node and add it to the executor.// In your <robot_hardware_interface_name>.cpp, inside on_init(params) if (auto locked_executor = params.executor.lock()) { my_custom_node_ = std::make_shared<rclcpp::Node>("my_custom_node"); locked_executor->add_node(my_custom_node_->get_node_base_interface()); // ... create publishers/timers on my_custom_node_ ... }
For a complete, working implementation that uses the framework-managed node to publish diagnostic messages, see the demo in Example 17.
Write the
on_configuremethod where you usually setup the communication to the hardware and set everything up so that the hardware can be activated.Implement
on_cleanupmethod, which does the opposite ofon_configure.Command-/StateInterfacesare now created and exported automatically by the framework via theon_export_command_interfaces()oron_export_state_interfaces()methods based on the interfaces defined in theros2_controlXML-tag, which gets parsed and theInterfaceDescriptionis created accordingly (check the hardware_info.hpp).To access the automatically created
Command-/StateInterfaceswe provide thestd::unordered_map<std::string, InterfaceDescription>, where the string is the fully qualified name of the interface and theInterfaceDescriptionis the configuration of the interface. Thestd::unordered_map<>are divided intotype_state_interfaces_andtype_command_interfaces_where type can be:joint,sensor,gpioandunlisted. E.g. theCommandInterfacesfor all joints can be found in thejoint_command_interfaces_map. Theunlistedincludes all interfaces not listed in theros2_controlXML-tag but were created by overriding theexport_unlisted_command_interface_descriptions()orexport_unlisted_state_interface_descriptions()function by creating some customCommand-/StateInterfaces.For the
Sensor-type hardware interface there is noexport_command_interfacesmethod.As a reminder, the full interface names have structure
<joint_name>/<interface_type>.
(optional) If you want some unlisted
Command-/StateInterfacesnot included in theros2_controlXML-tag you can follow those steps:Override the
virtual std::vector<hardware_interface::InterfaceDescription> export_unlisted_command_interface_descriptions()orvirtual std::vector<hardware_interface::InterfaceDescription> export_unlisted_state_interface_descriptions()Create the InterfaceDescription for each of the interfaces you want to create in the override
export_unlisted_command_interface_descriptions()orexport_unlisted_state_interface_descriptions()function, add it to a vector and return the vector:std::vector<hardware_interface::InterfaceDescription> my_unlisted_interfaces; InterfaceInfo unlisted_interface; unlisted_interface.name = "some_unlisted_interface"; unlisted_interface.min = "-5.0"; unlisted_interface.data_type = "double"; my_unlisted_interfaces.push_back(InterfaceDescription(info_.name, unlisted_interface)); return my_unlisted_interfaces;
The unlisted interface will then be stored in either the
unlisted_command_interfaces_orunlisted_state_interfaces_map depending in which function they are created.You can access it like any other interface with the
get_state(name),set_state(name, value),get_command(name)orset_command(name, value). E.g.get_state("some_unlisted_interface").
(optional) In case the default implementation (
on_export_command_interfaces()oron_export_state_interfaces()) for exporting theCommand-/StateInterfacesis not enough you can override them. You should however consider the following things:If you want to have unlisted interfaces available you need to call the
export_unlisted_command_interface_descriptions()orexport_unlisted_state_interface_descriptions()and add them to theunlisted_command_interfaces_orunlisted_state_interfaces_.Don’t forget to store the created
Command-/StateInterfacesinternally as you only return shared_ptrs and the resource manager will not provide access to the createdCommand-/StateInterfacesfor the hardware. So you must take care of storing them yourself.Names must be unique!
(optional) For Actuator and System types of hardware interface implement
prepare_command_mode_switchandperform_command_mode_switchif your hardware accepts multiple control modes.Implement the
on_activatemethod where hardware “power” is enabled.Implement the
on_deactivatemethod, which does the opposite ofon_activate.Implement
on_shutdownmethod where hardware is shutdown gracefully.Implement
on_errormethod where different errors from all states are handled.Implement the
readmethod getting the states from the hardware and storing them to internal variables defined inexport_state_interfaces.Implement
writemethod that commands the hardware based on the values stored in internal variables defined inexport_command_interfaces.(optional) Framework Managed Publisher
Implement
init_hardware_status_messageandupdate_hardware_status_messagemethods to publish the framework-supported hardware status reporting throughcontrol_msgs/msg/HardwareStatusmessages:`init_hardware_status_message`: This non-realtime method is called once during initialization. You must override it to define the static structure of your status message. This includes setting the
hardware_id, resizing thehardware_device_statesvector, and for each device, resizing its specific status vectors (e.g.,generic_hardware_status,canopen_states) and populating static fields likedevice_idand interfacename. Pre-allocating the message structure here is crucial for real-time safety.
// In your <robot_hardware_interface_name>.hpp hardware_interface::CallbackReturn init_hardware_status_message( control_msgs::msg::HardwareStatus & msg_template) override; // In your <robot_hardware_interface_name>.cpp hardware_interface::CallbackReturn MyHardware::init_hardware_status_message( control_msgs::msg::HardwareStatus & msg) { msg.hardware_id = get_hardware_info().name; msg.hardware_device_states.resize(get_hardware_info().joints.size()); for (size_t i = 0; i < get_hardware_info().joints.size(); ++i) { msg.hardware_device_states[i].device_id = get_hardware_info().joints[i].name; // This example uses one generic status per joint msg.hardware_device_states[i].generic_hardware_status.resize(1); } return hardware_interface::CallbackReturn::SUCCESS; }
`update_hardware_status_message`: This real-time safe method is called from the framework’s timer callback. You must override it to fill in the dynamic values of the pre-structured message. This typically involves copying your internal state variables (updated in your read() method) into the fields of the message. This method must be fast and non-allocating.
// In your <robot_hardware_interface_name>.hpp hardware_interface::return_type update_hardware_status_message( control_msgs::msg::HardwareStatus & msg) override; // In your <robot_hardware_interface_name>.cpp hardware_interface::return_type MyHardware::update_hardware_status_message( control_msgs::msg::HardwareStatus & msg) { for (size_t i = 0; i < get_hardware_info().joints.size(); ++i) { auto & generic_status = msg.hardware_device_states[i].generic_hardware_status; // Example: Map internal state to a standard status field if (std::abs(hw_positions_[i]) > joint_limits_[i].max_position) { generic_status.health_status = control_msgs::msg::GenericState::HEALTH_ERROR; } else { generic_status.health_status = control_msgs::msg::GenericState::HEALTH_OK; } } return hardware_interface::return_type::OK; }
Enable in URDF: Finally, to activate the publisher, add the
status_publish_rateparameter to your<hardware>tag in the URDF. Setting it to 0.0 disables the feature.
<ros2_control name="MyHardware" type="system"> <hardware> <plugin>my_package/MyHardware</plugin> <param name="status_publish_rate">20.0</param> </hardware> ... </ros2_control>
For a complete, working implementation that uses the framework-managed node to publish diagnostic messages, see the demo in Example 17.
IMPORTANT: At the end of your file after the namespace is closed, add the
PLUGINLIB_EXPORT_CLASSmacro.
For this you will need to include the
"pluginlib/class_list_macros.hpp"header. As first parameters you should provide exact hardware interface class, e.g.,<my_hardware_interface_package>::<RobotHardwareInterfaceName>, and as second the base class, i.e.,hardware_interface::(Actuator|Sensor|System)Interface.Writing export definition for pluginlib
Create the
<my_hardware_interface_package>.xmlfile in the package and add a definition of the library and hardware interface’s class which has to be visible for the pluginlib. The easiest way to do that is to check definition for mock components in the hardware_interface mock_components section.Usually, the plugin name is defined by the package (namespace) and the class name, e.g.,
<my_hardware_interface_package>/<RobotHardwareInterfaceName>. This name defines the hardware interface’s type when the resource manager searches for it. The other two parameters have to correspond to the definition done in the macro at the bottom of the<robot_hardware_interface_name>.cppfile.
Writing a simple test to check if the controller can be found and loaded
Create the folder
testin your package, if it does not exist already, and add a file namedtest_load_<robot_hardware_interface_name>.cpp.You can copy the
load_generic_system_2dofcontent defined in the test_generic_system.cpp package.Change the name of the copied test and in the last line, where hardware interface type is specified put the name defined in
<my_hardware_interface_package>.xmlfile, e.g.,<my_hardware_interface_package>/<RobotHardwareInterfaceName>.
Add compile directives into ``CMakeLists.txt`` file
Under the line
find_package(ament_cmake REQUIRED)add further dependencies. Those are at least:hardware_interface,pluginlib,rclcppandrclcpp_lifecycle.Add a compile directive for a shared library providing the
<robot_hardware_interface_name>.cppfile as the source.Add targeted include directories for the library. This is usually only
include.Add ament dependencies needed by the library. You should add at least those listed under 1.
Export for pluginlib description file using the following command: .. code:: cmake
pluginlib_export_plugin_description_file(hardware_interface <my_hardware_interface_package>.xml)
Add install directives for targets and include directory.
In the test section add the following dependencies:
ament_cmake_gmock,hardware_interface.Add compile definitions for the tests using the
ament_add_gmockdirective. For details, see how it is done for mock hardware in the ros2_control package.(optional) Add your hardware interface`s library into
ament_export_librariesbeforeament_package().
Add dependencies into ``package.xml`` file
Add at least the following packages into
<depend>tag:hardware_interface,pluginlib,rclcpp, andrclcpp_lifecycle.Add at least the following package into
<test_depend>tag:ament_add_gmockandhardware_interface.
Compiling and testing the hardware component
Now everything is ready to compile the hardware component using the
colcon build <my_hardware_interface_package>command. Remember to go into the root of your workspace before executing this command.If compilation was successful, source the
setup.bashfile from the install folder and executecolcon test <my_hardware_interface_package>to check if the new controller can be found throughpluginliblibrary and be loaded by the controller manager.
That’s it! Enjoy writing great controllers!
Useful External References
Templates and scripts for generating controllers shell
Note
The script is currently only recommended to use with Foxy and Humble, not compatible with the API from Jazzy and onwards.