Skip to main content
ClaudeWave
Skill200 repo starsupdated 4d ago

ros2_controller_creation

Scaffold or extend a ros2_control controller or broadcaster (ControllerInterface / ChainableControllerInterface) — base-class choice, command/state interface configuration, lifecycle, real-time-safe update(), generate_parameter_library, pluginlib export, tests. Trigger when the user asks to write a ros2_control controller or broadcaster.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/harunkurtdev/ros2-claude-code-template /tmp/ros2_controller_creation && cp -r /tmp/ros2_controller_creation/.claude/skills/ros2_controller_creation ~/.claude/skills/ros2_controller_creation
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Writing a ros2_control Controller / Broadcaster

How to create or extend a controller in the **ros2_control** framework,
following the conventions used across `~/nav2_ws/src/ros2_controllers/`.

- Framework + lifecycle + RT rules: `rules/ros2_control_architecture.md`.
- Existing controller catalog (copy the closest one): `rules/ros2_controllers_reference.md`.
- Reference implementations to mirror:
  - simplest controller → `forward_command_controller/`
  - feedback + odometry + chaining → `diff_drive_controller/`
  - read-only sensor → `imu_sensor_broadcaster/`
  - PID / pure chaining → `pid_controller/`, `chained_filter_controller/`

## First decision: which base class?

| You are building… | Base class | Override |
|-------------------|-----------|----------|
| A controller that just writes commands | `ControllerInterface` | `update()` |
| A controller others can chain into / that exposes a reference | `ChainableControllerInterface` | `update_reference_from_subscribers()` + `update_and_write_commands()` |
| A read-only sensor/state publisher | `ControllerInterface` (or `Chainable` to re-export state) with **NONE** command interfaces | `update()` (read state → publish) |

A controller is a **pluginlib plugin**, not a node. It runs inside
`controller_manager`. It never opens hardware directly — only through the
command/state interfaces the manager loans it.

## Minimal package skeleton

```
my_controller/
├── include/my_controller/my_controller.hpp
├── src/my_controller.cpp
├── src/my_controller_parameters.yaml
├── my_controller_plugin.xml
├── CMakeLists.txt
├── package.xml
├── doc/userdoc.rst
└── test/test_my_controller.cpp
```

### 1. Header — the class

```cpp
#include "controller_interface/controller_interface.hpp"   // or chainable_controller_interface.hpp
#include "my_controller/my_controller_parameters.hpp"      // generated
#include "realtime_tools/realtime_publisher.hpp"
#include "realtime_tools/realtime_thread_safe_box.hpp"

namespace my_controller
{
class MyController : public controller_interface::ControllerInterface
{
public:
  controller_interface::CallbackReturn on_init() override;
  controller_interface::InterfaceConfiguration command_interface_configuration() const override;
  controller_interface::InterfaceConfiguration state_interface_configuration() const override;
  controller_interface::CallbackReturn on_configure(const rclcpp_lifecycle::State &) override;
  controller_interface::CallbackReturn on_activate(const rclcpp_lifecycle::State &) override;
  controller_interface::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &) override;
  controller_interface::return_type update(
    const rclcpp::Time & time, const rclcpp::Duration & period) override;

protected:
  std::shared_ptr<ParamListener> param_listener_;
  Params params_;
};
}  // namespace my_controller
```

### 2. Interface configuration

```cpp
controller_interface::InterfaceConfiguration
MyController::command_interface_configuration() const
{
  return { controller_interface::interface_configuration_type::INDIVIDUAL,
           { params_.joint + "/velocity" } };          // names you will write
}
controller_interface::InterfaceConfiguration
MyController::state_interface_configuration() const
{
  return { controller_interface::interface_configuration_type::INDIVIDUAL,
           { params_.joint + "/position", params_.joint + "/velocity" } };
}
// A broadcaster returns type NONE for command_interface_configuration().
```

### 3. Lifecycle + the hot path

```cpp
controller_interface::CallbackReturn MyController::on_init()
{
  param_listener_ = std::make_shared<ParamListener>(get_node());
  return controller_interface::CallbackReturn::SUCCESS;
}

controller_interface::CallbackReturn MyController::on_configure(const rclcpp_lifecycle::State &)
{
  params_ = param_listener_->get_params();
  // create subscribers / RealtimePublisher / preallocate messages HERE
  return controller_interface::CallbackReturn::SUCCESS;
}

controller_interface::CallbackReturn MyController::on_activate(const rclcpp_lifecycle::State &)
{
  // cache references into command_interfaces_ / state_interfaces_ by index here
  return controller_interface::CallbackReturn::SUCCESS;
}

controller_interface::return_type MyController::update(
  const rclcpp::Time &, const rclcpp::Duration &)
{
  // RT-safe ONLY: no new/malloc, no locks, no throw, no unthrottled logging
  const double fb = state_interfaces_[0].get_optional().value_or(0.0);
  command_interfaces_[0].set_value(compute(fb));
  return controller_interface::return_type::OK;
}
```

### 4. Parameters (`generate_parameter_library`)

`src/my_controller_parameters.yaml`:

```yaml
my_controller:
  joint: {
    type: string,
    default_value: "",
    description: "Joint whose interfaces this controller claims",
    read_only: true,
    validation: { not_empty<>: [] }
  }
  gain: {
    type: double, default_value: 1.0,
    validation: { gt<>: [0.0] }
  }
```

Never `declare_parameter` by hand in a controller — use the generated
`ParamListener`/`Params`. Re-call `get_params()` in `update()` only if you
need live updates (and guard with `param_listener_->is_old(params_)`).

### 5. Plugin export + CMake + macro

`my_controller_plugin.xml`:

```xml
<library path="my_controller">
  <class name="my_controller/MyController" type="my_controller::MyController"
         base_class_type="controller_interface::ControllerInterface">
    <description>What it does.</description>
  </class>
</library>
```

`CMakeLists.txt` essentials:

```cmake
find_package(generate_parameter_library REQUIRED)
find_package(controller_interface REQUIRED)
find_package(pluginlib REQUIRED)

generate_parameter_library(my_controller_parameters src/my_controller_parameters.yaml)

add_library(my_controller SHARED src/my_controller.cpp)
target_link_libraries(my_controller PUBLIC
  controller_interface::controller_interface
  my_controller_parameters
  pluginlib::pluginlib)

pluginlib_export_plugin_description_file(controller_in
behaviortree-reviewerSubagent

Use proactively before opening a PR that adds or changes BehaviorTree.CPP nodes or BehaviorTree.ROS2 wrappers (RosActionNode/RosServiceNode/RosTopicPub/SubNode, TreeExecutionServer). Reviews a diff against BT.CPP v4 conventions — node base-class choice, non-blocking ticks, ports/blackboard typing, factory/plugin registration, XML v4, and the ROS 2 wrapper contract. Returns a punch list with file:line anchors, not a rewrite.

clean-arch-architectSubagent

Use when a design decision touches Clean Architecture boundaries in a ROS 2 project — which layer a new behaviour belongs to, whether a port belongs in domain or application, whether a new node should be lifecycle-managed, whether to compose nodes or split packages. Returns an architectural recommendation with trade-offs, not implementation.

ecs-architectSubagent

Use when a design decision touches the gz-sim ECS — where new state should live, which system phase should write it, how to avoid coupling, whether to add a component vs. a member variable, whether a new system should be split or merged with an existing one. Returns an architectural recommendation with trade-offs, not implementation.

gz-style-reviewerSubagent

Use proactively before opening any gz-sim PR. Reviews a diff against the project's C++17 style, ECS conventions, plugin registration patterns, CMake structure, test placement, Migration.md / Changelog.md expectations, and pre-commit configuration. Returns a punch list, not a rewrite.

ros2-controllers-reviewerSubagent

Use proactively before opening a PR that adds or changes a ros2_control controller, broadcaster, or hardware component (incl. URDF <ros2_control> bringup). Reviews a diff against ros2_controllers / ros2_control_demos conventions — controller & hardware lifecycle, command/state interface configuration, real-time safety of update()/read()/write(), generate_parameter_library usage, pluginlib registration, chainable-controller correctness, URDF wiring, and tests. Returns a punch list with file:line anchors, not a rewrite.

ros2-style-reviewerSubagent

Use proactively before opening any ROS 2 / Nav 2 PR. Reviews a diff against this template's Clean Architecture, ROS 2 communication, lifecycle, testing, and Nav 2 plugin conventions. Returns a punch list with file:line anchors, not a rewrite.

vda5050-reviewerSubagent

Use proactively before opening a PR that touches a VDA 5050 connector / fleet bridge. Reviews a diff against VDA 5050 v3.0.0 protocol compliance (topics, QoS, header rules, base/horizon, action state machine, schema validation) and the template's Clean Architecture for the MQTT↔Nav 2 bridge. Returns a punch list with file:line anchors, not a rewrite.

buildSlash Command

Build the colcon workspace (optionally a single package) and report the outcome.