ros2-robotics

v2026.09.24

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.

GitHub
Install command
npx skhub add mindrally/ros2-robotics
Markdown
SKILL.md

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

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.
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Apache-2.0

Source path

ros2-robotics

Default branch

main

Latest commit

9718410

Tree SHA

81ba3c7