ros2-control
Skill2 starsChanged 54 days agoMITView on GitHub
Use when integrating real or simulated hardware with ROS 2 via ros2_control — writing a hardware_interface::SystemInterface plugin, configuring the controller_manager and standard controllers (diff_drive_controller, joint_trajectory_controller, forward_command_controller), controller YAML, lifecycle management, and the sim/hardware swap.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- jewel-nath/agentic-roboticstheir own version, first seen Nov 1, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- jewel-nath/agentic-robotics★ 3same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository5 other files it gives its agents.
- embedded-ros-bridgeSkill
- nav2-moveit2-configSkill
- ros2-ecosystem-packagesSkill
- ros2-workspaceSkill
- urdf-xacro-builderSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 217
Always load joint_state_broadcaster alongside any motion controller — it's what actually publishes /joint_states (and therefore what …
- Very specific: line 232
Before trusting a ros2_control setup on real hardware:
- Very specific: line 237
Every command interface has an enforced hardware-level clamp in write(), independent of any URDF/controller-side limit — defense in depth …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
ros2_control: Hardware Abstraction and Controllers
ros2_control is the standard real-time-safe bridge between ROS 2 and actuator/sensor
hardware. Reach for it instead of a hand-rolled node that polls sensors and writes motor
commands in a while loop — that pattern reimplements (poorly) what the controller manager,
standard controllers, and hardware interface lifecycle already give you for free, tested
against real hardware quirks (encoder wraparound, staleness, e-stop) this plugin's other
skills don't otherwise cover.
1. Architecture
┌───────────────┐ command_interfaces ┌─────────────────────┐ read()/write() ┌──────────┐
│ Controllers │◄──────────────────────►│ controller_manager │◄──────────────────►│ Hardware │
│ (diff_drive, │ state_interfaces │ (resource_manager + │ (your plugin) │ (motors, │
│ joint_traj...) │ │ controller loop) │ │ IMU...) │
└───────────────┘ └─────────────────────┘ └──────────┘- The hardware interface (
hardware_interface::SystemInterface,ActuatorInterface, orSensorInterface) is the only layer that talks to real I/O. It exposes fixed sets ofcommand_interfaces(what controllers write, e.g.velocity,position,effort) andstate_interfaces(what controllers read). - The controller_manager owns the real-time update loop, calling
read()→ each active controller'supdate()→write()on hardware, every cycle. - Controllers (
ros2_controllerspackage:diff_drive_controller,joint_trajectory_controller,forward_command_controller,imu_sensor_broadcaster, ...) are the only layer that should contain control logic — never put PID/kinematics logic directly in the hardware interface. - Swapping simulation for real hardware means swapping the
<plugin>in the URDF's<ros2_control>tag (see [[urdf-xacro-builder]] §4) — the controller YAML and everything above the hardware interface stays identical.
2. Writing a hardware_interface::SystemInterface
// my_robot_hardware/include/my_robot_hardware/my_robot_system.hpp
#include <hardware_interface/system_interface.hpp>
#include <hardware_interface/handle.hpp>
class MyRobotSystem : public hardware_interface::SystemInterface
{
public:
hardware_interface::CallbackReturn on_init(const hardware_interface::HardwareInfo & info) override;
std::vector<hardware_interface::StateInterface> export_state_interfaces() override;
std::vector<hardware_interface::CommandInterface> export_command_interfaces() 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 &, const rclcpp::Duration &) override;
hardware_interface::return_type write(const rclcpp::Time &, const rclcpp::Duration &) override;
private:
std::vector<double> hw_velocity_commands_;
std::vector<double> hw_position_states_;
std::vector<double> hw_velocity_states_;
};// my_robot_hardware/src/my_robot_system.cpp (key methods)
hardware_interface::CallbackReturn MyRobotSystem::on_init(const hardware_interface::HardwareInfo & info)
{
if (hardware_interface::SystemInterface::on_init(info) != hardware_interface::CallbackReturn::SUCCESS) {
return hardware_interface::CallbackReturn::ERROR;
}
hw_velocity_commands_.resize(info.joints.size(), 0.0);
hw_position_states_.resize(info.joints.size(), 0.0);
hw_velocity_states_.resize(info.joints.size(), 0.0);
return hardware_interface::CallbackReturn::SUCCESS;
}
std::vector<hardware_interface::StateInterface> MyRobotSystem::export_state_interfaces()
{
std::vector<hardware_interface::StateInterface> state_interfaces;
for (size_t i = 0; i < info_.joints.size(); ++i) {
state_interfaces.emplace_back(info_.joints[i].name, "position", &hw_position_states_[i]);
state_interfaces.emplace_back(info_.joints[i].name, "velocity", &hw_velocity_states_[i]);
}
return state_interfaces;
}
std::vector<hardware_interface::CommandInterface> MyRobotSystem::export_command_interfaces()
{
std::vector<hardware_interface::CommandInterface> command_interfaces;
for (size_t i = 0; i < info_.joints.size(); ++i) {
command_interfaces.emplace_back(info_.joints[i].name, "velocity", &hw_velocity_commands_[i]);
}
return command_interfaces;
}
hardware_interface::return_type MyRobotSystem::read(const rclcpp::Time &, const rclcpp::Duration & period)
{
// Poll the real encoder/driver here. Never block: a read() that stalls stalls the whole
// control loop for every controller and every joint, not just this one.
for (size_t i = 0; i < hw_position_states_.size(); ++i) {
hw_position_states_[i] += hw_velocity_states_[i] * period.seconds();
}
return hardware_interface::return_type::OK;
}
hardware_interface::return_type MyRobotSystem::write(const rclcpp::Time &, const rclcpp::Duration &)
{
// Send hw_velocity_commands_ to the real driver here. Clamp to the hardware's actual safe
// range even if the URDF/controller already enforces limits -- this is the last line of
// defense before current reaches a motor.
for (size_t i = 0; i < hw_velocity_commands_.size(); ++i) {
hw_velocity_states_[i] = hw_velocity_commands_[i];
}
return hardware_interface::return_type::OK;
}<!-- my_robot_hardware.xml: pluginlib export, referenced by the URDF's <ros2_control> <plugin> tag -->
<library path="my_robot_hardware">
<class name="my_robot_hardware/MyRobotSystem"
type="my_robot_hardware::MyRobotSystem"
base_class_type="hardware_interface::SystemInterface">
<description>Hardware interface for my_robot's real motor controllers.</description>
</class>
</library>Rules:
read()andwrite()run on the real-time control loop thread — never allocate memory, lock a non-realtime mutex, log withRCLCPP_INFOat every cycle, or perform blocking I/O (a serial read with no timeout) inside either. Buffer/queue data from a separate non-RT thread instead if the real driver's transport is inherently blocking.- Size every state/command vector in
on_initfrominfo_.joints.size()— never a hardcoded joint count, or the hardware interface silently breaks the moment the URDF's joint list changes. export_state_interfaces()/export_command_interfaces()interface names ("position","velocity","effort") must exactly match what's declared in the URDF's<ros2_control><joint>blocks (see [[urdf-xacro-builder]] §4) and what the controllers you plan to load actually claim — a mismatch fails controller activation with an error naming the missing interface, not a silent no-op.- Implement
on_activate/on_deactivateto actually enable/disable hardware power or engage/ disengage brakes — don't leave them as no-ops that returnSUCCESSunconditionally; a controller switch or an e-stop event depends on these actually taking effect.
3. controller_manager YAML and standard controllers
# config/controllers.yaml
controller_manager:
ros__parameters:
update_rate: 100 # Hz -- match this to what your hardware transport can actually sustain
use_sim_time: true
diff_drive_controller:
type: diff_drive_controller/DiffDriveController
joint_trajectory_controller:
type: joint_trajectory_controller/JointTrajectoryController
joint_state_broadcaster:
type: joint_state_broadcaster/JointStateBroadcaster
diff_drive_controller:
ros__parameters:
left_wheel_names: ["left_wheel_joint"]
right_wheel_names: ["right_wheel_joint"]
wheel_separation: 0.4
wheel_radius: 0.1
use_stamped_vel: false
cmd_vel_timeout: 0.5
publish_rate: 50.0
joint_trajectory_controller:
ros__parameters:
joints: [joint_1, joint_2, joint_3, joint_4, joint_5, joint_6]
command_interfaces: [position]
state_interfaces: [position, velocity]
allow_partial_joints_goal: false# Load and start controllers (typically done from a launch file via controller_manager's
# spawner, shown below, not by hand -- these are the equivalent CLI calls):
ros2 control load_controller diff_drive_controller
ros2 control set_controller_state diff_drive_controller active
ros2 control list_controllers
ros2 control list_hardware_interfaces# launch/controllers.launch.py -- the idiomatic way to bring controllers up
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description() -> LaunchDescription:
joint_state_broadcaster_spawner = Node(
package='controller_manager', executable='spawner',
arguments=['joint_state_broadcaster'],
)
diff_drive_spawner = Node(
package='controller_manager', executable='spawner',
arguments=['diff_drive_controller'],
)
return LaunchDescription([joint_state_broadcaster_spawner, diff_drive_spawner])Rules:
controller_manager.update_ratemust be a rate your hardware interface'sread()/write()can actually sustain without missing deadlines — setting 1000 Hz over a 115200-baud serial link to real hardware is a guaranteed missed-deadline/jitter problem, not just a suboptimal choice.cmd_vel_timeoutondiff_drive_controller(and equivalent timeouts on other controllers) must be set to a real, non-zero value — without it, a lost/cmd_velpublisher (a crashed teleop node, a dropped network link) leaves the robot driving at its last commanded velocity forever instead of stopping.- Always load
joint_state_broadcasteralongside any motion controller — it's what actually publishes/joint_states(and therefore whatrobot_state_publisher's TF depends on); a robot with a workingdiff_drive_controllerbut no broadcaster looks fine in command handling but has broken TF. command_interfaces/state_interfacesinjoint_trajectory_controllerconfig must be a subset of what the hardware interface actually exports (§2) and must be consistent with what MoveIt 2'smoveit_controllers.yaml(see [[nav2-moveit2-config]]) expects — a mismatch here is a common source of a planned trajectory that MoveIt reports as "sent" but the robot never executes.- Spawn controllers via the
controller_managerspawnerexecutable in a launch file, not by shelling out toros2 control load_controllerfrom application code — the spawner handles the load → configure → activate sequence and reports failure correctly to the launch system.
4. Real-time and safety checklist
Before trusting a ros2_control setup on real hardware:
read()/write()contain no dynamic allocation, no logging aboveRCLCPP_DEBUG, no blocking calls, and no exceptions that escape the function — an uncaught exception in the RT loop can crash the whole controller manager, taking every controller down with it.- Every command interface has an enforced hardware-level clamp in
write(), independent of any URDF/controller-side limit — defense in depth against a bad trajectory or a controller bug. on_activate/on_deactivatecorrectly gate real power/torque, so deactivating a controller (or the controller_manager shutting down) actually stops the hardware rather than leaving it at its last command.- A
cmd_vel_timeout(or equivalent watchdog for a custom controller) exists for every velocity-command path — see §3. update_ratehas been validated against the hardware transport's real round-trip latency, not just picked as a round number.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

