Use when creating, structuring, or modifying ROS 2 (Humble/Jazzy) packages and workspaces — package.xml/CMakeLists.txt conventions, Python (rclpy) and C++ (rclcpp) node patterns, executors, launch files, and colcon build/test workflows.
Scanned 9/19/2026
Install to Claude Code
npx -y skills add enesbirlik/claude-code-robotics --skill ros2-workspace --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ros2 Workspace?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/enesbirlik-ros2-workspace)More formats (shields.io, HTML) on the badges page.
---
name: ros2-workspace
description: Use when creating, structuring, or modifying ROS 2 (Humble/Jazzy) packages and workspaces — package.xml/CMakeLists.txt conventions, Python (rclpy) and C++ (rclcpp) node patterns, executors, launch files, and colcon build/test workflows.
---
# ROS 2 Workspace Engineering
Rules and templates for producing ROS 2 packages that build cleanly with `colcon`, pass
`ament_lint`, and follow the conventions expected in Humble and Jazzy.
## 1. Workspace layout
```
ros2_ws/
├── src/
│ ├── my_robot_bringup/ # launch + top-level config, no code
│ ├── my_robot_description/ # URDF/Xacro + meshes (see urdf-xacro-builder skill)
│ ├── my_robot_msgs/ # custom .msg/.srv/.action, ament_cmake only
│ └── my_robot_controller/ # the actual node(s), ament_cmake or ament_python
├── install/ # generated by colcon build — never hand-edit, gitignore it
├── build/ # generated — gitignore it
└── log/ # generated — gitignore it
```
Rules:
- One package = one clear responsibility (bringup, description, msgs, driver, algorithm).
Never mix message/service/action definitions into a package that also has other build
dependents — put custom interfaces in their own `_msgs` or `_interfaces` package to avoid
circular dependencies.
- Package names are `snake_case`, never start with a digit, and should be prefixed with the
robot/project name (`my_robot_controller`, not `controller`).
- Never commit `build/`, `install/`, or `log/` — the plugin's `.gitignore` template already
excludes them.
## 2. `package.xml` (format 3)
```xml
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>my_robot_controller</name>
<version>0.1.0</version>
<description>Velocity controller node for my_robot.</description>
<maintainer email="you@example.com">Your Name</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>geometry_msgs</depend>
<depend>sensor_msgs</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
```
For a Python package, use `<build_type>ament_python</build_type>` and
`<buildtool_depend>ament_python</buildtool_depend>`.
Rules:
- Every runtime import/link dependency is a `<depend>` (or split `<build_depend>` /
`<exec_depend>` only when build-time and run-time deps genuinely differ — otherwise prefer
the combined `<depend>` tag for readability).
- Always declare `ament_lint_auto` + `ament_lint_common` as `test_depend` so `colcon test` runs
static checks (`ament_cpplint`, `ament_flake8`, `ament_xmllint`, ...).
- `<version>` follows SemVer and should be bumped alongside `CHANGELOG.rst` entries.
## 3. C++ package: `CMakeLists.txt`
```cmake
cmake_minimum_required(VERSION 3.8)
project(my_robot_controller)
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(sensor_msgs REQUIRED)
add_executable(velocity_controller_node
src/velocity_controller_node.cpp
)
ament_target_dependencies(velocity_controller_node
rclcpp
geometry_msgs
sensor_msgs
)
target_include_directories(velocity_controller_node PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
install(TARGETS velocity_controller_node
DESTINATION lib/${PROJECT_NAME}
)
install(DIRECTORY launch config
DESTINATION share/${PROJECT_NAME}
OPTIONAL
)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
endif()
ament_package()
```
Rules:
- Warnings-as-errors are opt-in per repo, but `-Wall -Wextra -Wpedantic` is always on.
- Every installed executable target gets an explicit `install(TARGETS ...)`. Every shared
`launch/`, `config/`, `urdf/`, `meshes/` directory gets an `install(DIRECTORY ...)`.
- `ament_package()` must be the last line.
## 4. Python package: `setup.py` / `setup.cfg`
```python
# setup.py
from setuptools import find_packages, setup
package_name = 'my_robot_controller'
setup(
name=package_name,
version='0.1.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch', ['launch/controller.launch.py']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='Your Name',
maintainer_email='you@example.com',
description='Velocity controller node for my_robot.',
license='Apache-2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'velocity_controller_node = my_robot_controller.velocity_controller_node:main',
],
},
)
```
```ini
# setup.cfg
[develop]
script_dir=$base/lib/my_robot_controller
[install]
install_scripts=$base/lib/my_robot_controller
```
Rules:
- Every `console_scripts` entry point maps `executable_name = package.module:main`; the
`main()` function must exist and take no required arguments.
- Don't forget the `resource/<package_name>` marker file (empty) — without it `ros2 pkg list`
and `ros2 run` won't find the package.
## 5. rclpy node template (Python)
```python
#!/usr/bin/env python3
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
import rclpy
class VelocityControllerNode(Node):
def __init__(self) -> None:
super().__init__('velocity_controller_node')
self.declare_parameter('max_linear_speed', 0.5)
self.declare_parameter('scan_topic', '/scan')
self._max_linear_speed = self.get_parameter('max_linear_speed').value
sensor_qos = QoSProfile(
reliability=ReliabilityPolicy.BEST_EFFORT,
history=HistoryPolicy.KEEP_LAST,
depth=5,
)
self._scan_sub = self.create_subscription(
LaserScan,
self.get_parameter('scan_topic').value,
self._on_scan,
sensor_qos,
)
self._cmd_pub = self.create_publisher(Twist, 'cmd_vel', 10)
self._timer = self.create_timer(0.1, self._on_timer)
def _on_scan(self, msg: LaserScan) -> None:
self._latest_scan = msg
def _on_timer(self) -> None:
cmd = Twist()
cmd.linear.x = self._max_linear_speed
self._cmd_pub.publish(cmd)
def main(args: list[str] | None = None) -> None:
rclpy.init(args=args)
node = VelocityControllerNode()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
```
Rules:
- Always declare parameters with `declare_parameter` before reading them — never assume a
default exists implicitly.
- Choose QoS explicitly. Sensor data: `BEST_EFFORT` + shallow `KEEP_LAST` depth. Commands and
state: `RELIABLE` + `KEEP_LAST` with a depth matched to consumer speed. Never leave QoS at
an implicit default when publisher and subscriber are in different packages — mismatches
silently drop all messages.
- Use `rclpy.spin(node)` for single-purpose nodes; use `MultiThreadedExecutor` with explicit
`ReentrantCallbackGroup`/`MutuallyExclusiveCallbackGroup` assignment only when you have
callbacks that must run concurrently (e.g. a long-running service alongside a fast timer) —
don't reach for it by default, it adds real concurrency hazards.
- Always `destroy_node()` then `rclpy.shutdown()` in a `finally` block so Ctrl+C and exceptions
both clean up the node's middleware resources.
## 6. rclcpp node template (C++)
```cpp
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <sensor_msgs/msg/laser_scan.hpp>
class VelocityControllerNode : public rclcpp::Node
{
public:
VelocityControllerNode()
: Node("velocity_controller_node")
{
max_linear_speed_ = this->declare_parameter("max_linear_speed", 0.5);
const auto scan_topic = this->declare_parameter("scan_topic", std::string("/scan"));
auto sensor_qos = rclcpp::SensorDataQoS();
scan_sub_ = this->create_subscription<sensor_msgs::msg::LaserScan>(
scan_topic, sensor_qos,
std::bind(&VelocityControllerNode::onScan, this, std::placeholders::_1));
cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 10);
timer_ = this->create_wall_timer(
std::chrono::milliseconds(100),
std::bind(&VelocityControllerNode::onTimer, this));
}
private:
void onScan(const sensor_msgs::msg::LaserScan::SharedPtr msg)
{
latest_scan_ = msg;
}
void onTimer()
{
geometry_msgs::msg::Twist cmd;
cmd.linear.x = max_linear_speed_;
cmd_pub_->publish(cmd);
}
double max_linear_speed_;
sensor_msgs::msg::LaserScan::SharedPtr latest_scan_;
rclcpp::Subscription<sensor_msgs::msg::LaserScan>::SharedPtr scan_sub_;
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<VelocityControllerNode>());
rclcpp::shutdown();
return 0;
}
```
Rules:
- Prefer composition-friendly node classes (inherit `rclcpp::Node`, no raw `main`-only logic)
so the node can later be loaded as a component without rewriting it.
- Use `rclcpp::SensorDataQoS()` / `rclcpp::SystemDefaultsQoS()` helpers instead of hand-rolling
`rclcpp::QoS` profiles unless you need a genuinely custom depth/durability.
- Never call `shared_from_this()` inside a constructor — the `shared_ptr` doesn't exist yet.
If you need it (e.g. to register a callback that captures `this` as a `shared_ptr`), do it
from a separate `init()` method called right after construction.
- Use `rclcpp::executors::MultiThreadedExecutor` with explicit `CallbackGroup`s only when you
have a proven concurrency need (e.g. a blocking service call must not stall a control-loop
timer); default to the implicit single-threaded `rclcpp::spin`.
## 7. Launch files
```python
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
def generate_launch_description() -> LaunchDescription:
max_speed_arg = DeclareLaunchArgument(
'max_linear_speed', default_value='0.5',
description='Maximum commanded linear speed (m/s).',
)
controller_node = Node(
package='my_robot_controller',
executable='velocity_controller_node',
name='velocity_controller_node',
output='screen',
parameters=[{
'max_linear_speed': LaunchConfiguration('max_linear_speed'),
}],
)
return LaunchDescription([max_speed_arg, controller_node])
```
Rules:
- Every tunable value exposed as a `DeclareLaunchArgument`, never hardcoded, so the same
launch file works in simulation and on hardware.
- Use `FindPackageShare` + `PathJoinSubstitution` to locate config/URDF files — never a
hardcoded absolute path.
- Prefer one launch file per package that composes into a top-level `bringup` launch file via
`IncludeLaunchDescription`, instead of one monolithic launch file per robot.
## 8. colcon build & test workflow
```bash
# From the workspace root (parent of src/):
colcon build --symlink-install # symlink python/config for fast iteration
colcon build --packages-select my_robot_controller # scope a build to one package
colcon test --packages-select my_robot_controller
colcon test-result --verbose
source install/setup.bash # re-source after every structural change
```
Rules:
- Use `--symlink-install` in development workspaces so Python source and non-compiled
resources update without a rebuild.
- Scope builds with `--packages-select` while iterating; only build the whole workspace before
a final integration check.
- Always re-run `colcon test` after touching `package.xml` dependencies or CMake install rules
— `ament_lint` catches missing/unused dependencies that only surface at test time.
- This plugin's `colcon_build_check` hook automatically runs a scoped `colcon build
--packages-select <pkg>` whenever a new node source file is created inside a detected colcon
workspace — no need to trigger it manually during normal editing.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!