Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Scaffold a ros2_control hardware component (SystemInterface / ActuatorInterface / SensorInterface) and its bringup — the plugin (on_init/on_configure/on_activate, RT-safe read()/write()), URDF <ros2_control> wiring, controllers.yaml + launch, pluginlib export. Trigger when the user asks to integrate hardware or bring up a robot under ros2_control.
.claude/skills/harunkurtdev-ros2-control-hardware-interface/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-03 | ✓→✗ | ▼ Worse | 155% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 45% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 108% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 12% | 0% |
How to bring up a real (or simulated) robot under ros2_control: write a hardware_interface plugin, wire it in the URDF, and launch it. Mirrors the patterns in ~/nav2_ws/src/ros2_control_demos/.
rules/ros2_control_architecture.md.rules/ros2_control_demos.md.skills/ros2_controller_creation/SKILL.md.
example_1 (RRBot SystemInterface)example_2 (DiffBot)example_5 (SensorInterface)example_6 (ActuatorInterface)example_10, diagnostics → example_17| You are integrating… | Base class | Exports | |----------------------|-----------|---------| | A whole robot (joints + maybe sensors) | hardware_interface::SystemInterface | command + state interfaces | | A single actuator | hardware_interface::ActuatorInterface | command + state for one actuator | | A read-only sensor | hardware_interface::SensorInterface | state interfaces only |
A hardware component is a pluginlib plugin loaded by the controller_manager (via the URDF), not a node. It runs in the RT loop: read() → controllers' update() → write() every cycle.
hardware/include/<pkg>/<robot>.hpp:
cpp#include "hardware_interface/system_interface.hpp" #include "hardware_interface/hardware_info.hpp" #include "rclcpp_lifecycle/state.hpp" namespace my_robot { class MyRobotHardware : public hardware_interface::SystemInterface { public: RCLCPP_SHARED_PTR_DEFINITIONS(MyRobotHardware) hardware_interface::CallbackReturn on_init( const hardware_interface::HardwareComponentInterfaceParams & params) override; hardware_interface::CallbackReturn on_configure(const rclcpp_lifecycle::State &) override; hardware_interface::CallbackReturn on_activate(const rclcpp_lifecycle::State &) override; hardware_interface::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &) override; hardware_interface::return_type read( const rclcpp::Time & time, const rclcpp::Duration & period) override; hardware_interface::return_type write( const rclcpp::Time & time, const rclcpp::Duration & period) override; private: double cfg_param_; // read from URDF <param> in on_init }; } // namespace my_robot
hardware/<robot>.cpp essentials:
cppCallbackReturn MyRobotHardware::on_init(const HardwareComponentInterfaceParams & params) { if (SystemInterface::on_init(params) != CallbackReturn::SUCCESS) return CallbackReturn::ERROR; // joints + their interfaces come from the URDF (get_hardware_info()); // read custom params: info_.hardware_parameters["my_param"] return CallbackReturn::SUCCESS; } CallbackReturn MyRobotHardware::on_activate(const rclcpp_lifecycle::State &) { for (const auto & [name, descr] : joint_state_interfaces_) set_state(name, 0.0); for (const auto & [name, descr] : joint_command_interfaces_) set_command(name, get_state(name)); return CallbackReturn::SUCCESS; // open device handles HERE } hardware_interface::return_type MyRobotHardware::read(const rclcpp::Time &, const rclcpp::Duration &) { // RT-safe: poll the device, push values into state interfaces set_state("joint1/position", read_encoder()); return hardware_interface::return_type::OK; } hardware_interface::return_type MyRobotHardware::write(const rclcpp::Time &, const rclcpp::Duration &) { // RT-safe: take command-interface values, send them to the device send_to_motor(get_command("joint1/position")); return hardware_interface::return_type::OK; }
Modern hardware_interface auto-creates the interface handles from the URDF, so you use get_state/set_state/get_command/set_command(name) helpers rather than hand-managing std::vector<double>. (Older demos override export_state_interfaces() / export_command_interfaces() — both patterns exist; prefer the URDF-driven one.)
read() / write() are the RT hot path — same rules as a controller's update(): no allocation, no locks, no throw, no blocking I/O, no unthrottled logging. Do device setup in on_configure/on_activate.
<pkg>.xml:
xml<library path="my_robot"> <class name="my_robot/MyRobotHardware" type="my_robot::MyRobotHardware" base_class_type="hardware_interface::SystemInterface"> <description>My robot hardware interface.</description> </class> </library>
cpp#include "pluginlib/class_list_macros.hpp" PLUGINLIB_EXPORT_CLASS(my_robot::MyRobotHardware, hardware_interface::SystemInterface)
cmakepluginlib_export_plugin_description_file(hardware_interface my_robot.xml)
package.xml depends: hardware_interface, pluginlib, rclcpp_lifecycle (+ device libs).
<ros2_control> tag (wires joints → your plugin)description/ros2_control/<robot>.ros2_control.xacro:
xml<ros2_control name="${name}" type="system"> <hardware> <plugin>my_robot/MyRobotHardware</plugin> <param name="my_param">0.5</param> <!-- read in on_init --> </hardware> <joint name="${prefix}joint1"> <command_interface name="position"> <param name="min">-1</param> <param name="max">1</param> </command_interface> <state_interface name="position"/> </joint> </ros2_control>
The <plugin> string must equal the pluginlib name alias above. The <command_interface> / <state_interface> names here are exactly what controllers claim (joint1/position).
bringup/config/<robot>_controllers.yaml:
yamlcontroller_manager: ros__parameters: update_rate: 100 # Hz — the read/update/write loop frequency joint_state_broadcaster: type: joint_state_broadcaster/JointStateBroadcaster forward_position_controller: type: forward_command_controller/ForwardCommandController forward_position_controller: ros__parameters: joints: [joint1, joint2] interface_name: position
bringup/launch/<robot>.launch.py (the standard shape): start robot_state_publisher with the xacro-expanded URDF, start controller_manager (ros2_control_node) with the controllers.yaml + robot_description, then spawner each controller (joint_state_broadcaster first, then your command controller). Copy example_1/bringup/launch/rrbot.launch.py and adapt names.
bashros2 launch my_robot_bringup my_robot.launch.py ros2 control list_hardware_interfaces # your command/state ifaces appear, claimed/available ros2 control list_controllers # active/inactive
read()/write() — allocation, blocking device I/Owithout a timeout, locks, unthrottled logging. Open sockets/serial in on_activate, not in the loop.
<plugin> must match the .xmlname alias and PLUGINLIB_EXPORT_CLASS; base_class_type must be the right hardware_interface::*Interface.
SensorInterface exporting command interfaces — sensors areread-only (state interfaces only).
joint_state_broadcaster —broadcaster first.
update_rate mismatch between controller_manager and a controller'sown rate, or a write() that ignores period.
rclcpp::spin; thecontroller_manager ticks it.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 10,281 | 3,644 | -65% | 1 | 1 | 0% | 1,970 | 2,847 | +45% | 0 | 0 | — |
case-02 | pass→pass | 7,185 | 2,595 | -64% | 1 | 1 | 0% | 1,275 | 2,656 | +108% | 0 | 0 | — |
case-03 | pass→fail | 6,631 | 3,649 | -45% | 1 | 1 | 0% | 1,085 | 2,766 | +155% | 0 | 0 | — |
case-04 | pass→pass | 16,768 | 5,181 | -69% | 1 | 1 | 0% | 2,742 | 3,062 | +12% | 0 | 0 | — |
case-05 | pass→pass | 14,268 | 9,217 | -35% | 1 | 1 | 0% | 2,471 | 3,716 | +50% | 0 | 0 | — |
case-06 | pass→pass | 14,505 | 7,426 | -49% | 1 | 1 | 0% | 2,562 | 3,549 | +39% | 0 | 0 | — |
case-07 | fail→pass | 14,504 | 13,053 | -10% | 1 | 1 | 0% | 2,415 | 4,323 | +79% | 0 | 0 | — |
case-08 | pass→pass | 12,476 | 8,976 | -28% | 1 | 1 | 0% | 2,646 | 3,940 | +49% | 0 | 0 | — |
case-09 | pass→pass | 4,737 | 3,179 | -33% | 1 | 1 | 0% | 847 | 2,756 | +225% | 0 | 0 | — |
case-10 | pass→pass | 6,695 | 2,870 | -57% | 1 | 1 | 0% | 1,251 | 2,714 | +117% | 0 | 0 | — |
case-11 | pass→pass | 12,414 | 3,823 | -69% | 1 | 1 | 0% | 2,361 | 2,878 | +22% | 0 | 0 | — |
case-12 | pass→pass | 6,503 | 2,266 | -65% | 1 | 1 | 0% | 1,220 | 2,532 | +108% | 0 | 0 | — |
case-13 | pass→pass | 9,513 | 5,821 | -39% | 1 | 1 | 0% | 1,623 | 3,101 | +91% | 0 | 0 | — |
case-14 | pass→pass | 5,317 | 2,635 | -50% | 1 | 1 | 0% | 991 | 2,542 | +157% | 0 | 0 | — |
case-15 | pass→pass | 6,160 | 2,412 | -61% | 1 | 1 | 0% | 1,159 | 2,586 | +123% | 0 | 0 | — |
case-16 | pass→pass | 10,504 | 7,994 | -24% | 1 | 1 | 0% | 1,871 | 3,478 | +86% | 0 | 0 | — |
case-17 | pass→pass | 16,233 | 7,704 | -53% | 1 | 1 | 0% | 2,829 | 3,540 | +25% | 0 | 0 | — |
case-18 | pass→pass | 2,564 | 2,199 | -14% | 1 | 1 | 0% | 444 | 2,521 | +468% | 0 | 0 | — |
case-19 | pass→pass | 9,809 | 3,910 | -60% | 1 | 1 | 0% | 1,785 | 2,804 | +57% | 0 | 0 | — |
case-20 | pass→pass | 15,888 | 13,952 | -12% | 1 | 1 | 0% | 2,933 | 4,957 | +69% | 0 | 0 | — |
case-21 | pass→pass | 14,586 | 13,105 | -10% | 1 | 1 | 0% | 2,601 | 4,352 | +67% | 0 | 0 | — |
case-22 | pass→pass | 18,814 | 13,457 | -28% | 1 | 1 | 0% | 3,640 | 4,984 | +37% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.