Best practices for ROS 2 robotics development, covering package structure, nodes, topics/services/actions, launch files, QoS, tf2 transforms, and testing. Use when creating ROS 2 packages, writing nodes in rclpy or rclcpp, defining custom messages/services/actions, writing launch files, configuring QoS profiles, working with tf2 transforms, or building and testing a colcon workspace.
Scanned 9/7/2026
Install to Claude Code
npx -y skills add Mindrally/skills --skill ros2-robotics --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Ros2 Robotics?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/mindrally-ros2-robotics)More formats (shields.io, HTML) on the badges page.
---
name: ros2-robotics
description: "Best practices for ROS 2 robotics development, covering package structure, nodes, topics/services/actions, launch files, QoS, tf2 transforms, and testing. Use when creating ROS 2 packages, writing nodes in rclpy or rclcpp, defining custom messages/services/actions, writing launch files, configuring QoS profiles, working with tf2 transforms, or building and testing a colcon workspace."
---
# ROS 2 Robotics Development
This skill covers building ROS 2 packages and nodes, including package structure, communication patterns (topics, services, actions), timing and frames, build/test workflows with colcon, and common pitfalls in robotics software.
## Workflow for Building a ROS 2 Package
1. **Scaffold the package** — Run `ros2 pkg create --build-type ament_python <name>` (Python) or `--build-type ament_cmake <name>` (C++) inside a colcon workspace's `src/` directory.
2. **Define interfaces** — Add custom `.msg`, `.srv`, or `.action` files under `msg/`, `srv/`, `action/` only when a standard interface (`std_msgs`, `geometry_msgs`, `sensor_msgs`, `nav_msgs`) doesn't fit.
3. **Implement nodes** — Write focused, composable nodes; declare parameters explicitly; choose topics for streams, services for quick request/response, and actions for long-running goals with feedback and cancellation.
4. **Write launch files** — Compose nodes, parameters, and remappings in a `launch/*.launch.py` file using `launch_ros.actions.Node`.
5. **Set QoS profiles intentionally** — Match publisher/subscriber QoS (reliability, durability, history depth) for sensor data vs. command/control paths.
6. **Build the workspace** — Run `colcon build --symlink-install` from the workspace root, then `source install/setup.bash`.
7. **Test** — Add unit tests (`pytest` for Python, `gtest`/`launch_testing` for C++/integration) and run `colcon test`.
8. **Run and inspect** — Use `ros2 run`, `ros2 launch`, `ros2 topic echo`, `ros2 node info`, and `ros2 doctor` to verify runtime behavior.
## Package Structure
- Keep each package focused on one robot capability or integration boundary (e.g., `lidar_driver`, `path_planner`, `arm_controller`) rather than a monolithic package.
- Use `package.xml` consistently with the build type declared in it, and either `CMakeLists.txt` (ament_cmake, C++) or `setup.py`/`setup.cfg` (ament_python).
- Organize files with `launch/` for launch scripts, `config/` for YAML parameter files, `msg/` for message definitions, `srv/` for services, `action/` for actions, and `urdf/`/`xacro/` for robot descriptions.
- Use namespaces and topic remapping instead of hardcoded topic names whenever a node might run multiple times (e.g., multi-robot setups) or be reused across projects.
- Declare all dependencies explicitly in `package.xml` (`<depend>`, `<exec_depend>`, `<build_depend>`) so the workspace builds reproducibly.
## Nodes and Interfaces
- Keep nodes small and composable — one node per logical responsibility, wired together via topics/services/actions rather than one giant node doing everything.
- Declare ROS 2 parameters explicitly with `self.declare_parameter('rate_hz', 10.0)` and read them with `self.get_parameter('rate_hz').value`; never rely on undeclared parameters.
- Prefer messages (topics) for continuous state streams, services for quick, blocking request/response operations, and actions for long-running goals that report feedback and support cancellation/preemption.
- Use standard message types (`geometry_msgs/Twist`, `sensor_msgs/LaserScan`, `nav_msgs/Odometry`) before creating custom interfaces — custom types fragment the ecosystem and lose compatibility with existing tools.
- Document topic, service, action, frame, and parameter contracts (name, type, units, frame_id, update rate) in the package README or node docstring.
### Example: A Minimal rclpy Publisher Node
```python
import rclpy
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from geometry_msgs.msg import Twist
class SafeVelocityPublisher(Node):
"""Publishes bounded velocity commands at a fixed rate."""
def __init__(self) -> None:
super().__init__("safe_velocity_publisher")
self.declare_parameter("rate_hz", 10.0)
self.declare_parameter("max_linear_speed", 0.5)
rate_hz = self.get_parameter("rate_hz").value
self._max_linear = self.get_parameter("max_linear_speed").value
# Reliable QoS for a low-rate command topic; sensor streams would
# typically use BEST_EFFORT with a shallow history instead.
qos = QoSProfile(
reliability=ReliabilityPolicy.RELIABLE,
history=HistoryPolicy.KEEP_LAST,
depth=10,
)
self._publisher = self.create_publisher(Twist, "cmd_vel", qos)
self._timer = self.create_timer(1.0 / rate_hz, self._on_timer)
self._target_linear = 0.0
def _on_timer(self) -> None:
msg = Twist()
msg.linear.x = max(-self._max_linear, min(self._max_linear, self._target_linear))
self._publisher.publish(msg)
def main(args: list | None = None) -> None:
rclpy.init(args=args)
node = SafeVelocityPublisher()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
```
## Timing and Frames
- Use ROS time (`self.get_clock().now()`, and `use_sim_time` when a simulation clock is in play) instead of wall-clock time whenever simulation or bag replay matters.
- Use `tf2` (`tf2_ros.Buffer` + `TransformListener`) for all frame transforms; never hand-roll coordinate transforms between named frames.
- Document frame names (`base_link`, `odom`, `map`, sensor frames) and their conventions (REP-103 for axis orientation, REP-105 for the `map`→`odom`→`base_link` tree).
- Avoid blocking callbacks in a node's executor thread — move long-running work to timers, a `MultiThreadedExecutor`, worker threads, or actions so the node keeps servicing other callbacks.
- Set QoS profiles intentionally: `BEST_EFFORT`/volatile for high-rate sensor data where dropped samples are acceptable, `RELIABLE`/`TRANSIENT_LOCAL` for latched-like configuration topics, and `RELIABLE`/volatile for command paths.
## Build and Test
- Use `colcon build` (typically `--symlink-install` during development) and keep inter-package dependencies explicit in `package.xml`.
- Run the linters and formatters the workspace already uses — `ament_flake8`, `ament_pep257` for Python; `ament_cpplint`, `ament_uncrustify` for C++ — via `colcon test`.
- Add `launch_testing`-based integration tests for multi-node behavior (e.g., "does node A receive what node B publishes within N seconds").
- Use simulation (Gazebo/Ignition), recorded rosbags, or fixture data for repeatable sensor scenarios instead of relying on live hardware for every test run.
- Explicitly test failure cases: missing/late transforms (`tf2.LookupException`), stale sensor data, and services or actions that are unavailable when called.
## Common Mistakes
- Hardcoding absolute filesystem paths instead of resolving them via `ament_index_python.packages.get_package_share_directory`.
- Publishing velocity or actuator commands without validating frame_id, units, and timestamp freshness — a stale or wrong-frame command can move a robot unsafely.
- Creating a custom message/service/action type when an existing standard interface already models the data.
- Ignoring QoS incompatibility warnings between a publisher and subscriber (e.g., a `BEST_EFFORT` publisher with a `RELIABLE` subscriber) — the connection silently never delivers data.
- Doing significant work inside a subscription callback without offloading it, starving the executor and delaying other callbacks (including safety-critical ones).
- Forgetting `use_sim_time:=true` when running against a simulator, causing timestamps and `tf2` lookups to disagree with the simulated clock.
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!