agentleFS
enesbirlikenesbirlik/claude-code-robotics/skills/ros2-control/SKILL.md

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

  • Other versions1 project uses a version of this.

    Word for word · 0

    None found.

    Edited · 0

    None found.

    Wrote their own version · 1

    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.

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
  1. Very specific:

    Always load joint_state_broadcaster alongside any motion controller — it's what actually publishes /joint_states (and therefore what …

    line 217
  2. Very specific:

    Before trusting a ros2_control setup on real hardware:

    line 232
  3. Very specific:

    Every command interface has an enforced hardware-level clamp in write(), independent of any URDF/controller-side limit — defense in depth …

    line 237

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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, or SensorInterface) is the only layer that talks to real I/O. It exposes fixed sets of command_interfaces (what controllers write, e.g. velocity, position, effort) and state_interfaces (what controllers read).
  • The controller_manager owns the real-time update loop, calling read() → each active controller's update() → write() on hardware, every cycle.
  • Controllers (ros2_controllers package: 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() and write() run on the real-time control loop thread — never allocate memory, lock a non-realtime mutex, log with RCLCPP_INFO at 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_init from info_.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_deactivate to actually enable/disable hardware power or engage/ disengage brakes — don't leave them as no-ops that return SUCCESS unconditionally; 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_rate must be a rate your hardware interface's read()/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_timeout on diff_drive_controller (and equivalent timeouts on other controllers) must be set to a real, non-zero value — without it, a lost /cmd_vel publisher (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_broadcaster alongside any motion controller — it's what actually publishes /joint_states (and therefore what robot_state_publisher's TF depends on); a robot with a working diff_drive_controller but no broadcaster looks fine in command handling but has broken TF.
  • command_interfaces/state_interfaces in joint_trajectory_controller config must be a subset of what the hardware interface actually exports (§2) and must be consistent with what MoveIt 2's moveit_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_manager spawner executable in a launch file, not by shelling out to ros2 control load_controller from 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:

  1. read()/write() contain no dynamic allocation, no logging above RCLCPP_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.
  2. 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.
  3. on_activate/on_deactivate correctly 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.
  4. A cmd_vel_timeout (or equivalent watchdog for a custom controller) exists for every velocity-command path — see §3.
  5. update_rate has been validated against the hardware transport's real round-trip latency, not just picked as a round number.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.