ROS 2 in Practice

Nodes, topics, services and actions, quality of service, launch files, parameters, tf2 and bags, with data rates and latency worked out.

The concepts

The Robot Operating System 2 (ROS 2) is not an operating system but a framework: libraries and tools for building robot software as many small programs that talk to each other, usually over a data-distribution service (DDS) network layer. Programs can be written in C++ (rclcpp) or Python (rclpy) and run on one computer or several.

ConceptWhat it isExample
NodeOne program doing one jobcamera_driver, planner
TopicA named stream of messages. Nodes publish to it or subscribe to it; they need not know about each other/cmd_vel carries velocity commands
MessageThe typed data on a topicgeometry_msgs/Twist: linear and angular velocity
ServiceA request and reply for quick questionsReset the odometry
ActionA long task with feedback and cancelNavigate to a goal, with progress updates
ParameterA named setting of a node that can be changed while it runsMaximum speed, PID gains
Launch fileStarts and configures many nodes togetherBring up the whole robot
tf2Keeps track of coordinate frames and their relationships over timeWhere is the camera relative to the wheels?
URDFAn XML description of the robot’s links, joints and sensorsUsed for visualization, simulation and planning
BagA recording of topics that can be replayedDebug a failure after the event
camera_driverdetectorplannermotor_ctrl/image_raw/detections/cmd_vel/odomOvals are nodes (programs). Boxes are topics (named message streams).Each node can publish or subscribe to any number of topics.Orange: feedback. Nodes can run on different computers and be swapped without changing the others.
A small ROS 2 graph. Nodes talk only through topics, so any node can be replaced, restarted or moved to another computer without changing the others.

Distributions, installation and workspaces

DistributionReleasedSupportNotes
Humble HawksbillMay 2022Long-term support to May 2027Ubuntu 22.04; very widely used and supported by packages
Jazzy JaliscoMay 2024Long-term support to May 2029Ubuntu 24.04; the current choice for new projects
RollingContinuousNoneThe development branch: for contributors
  • Pick a long-term-support release matching your Ubuntu version, and stay on it for a project: each distribution changes APIs and package versions.
  • Install from the official apt packages (or a Docker image), then source /opt/ros/jazzy/setup.bash in every shell.
  • Workspace: ~/ros2_ws/src holds your packages; colcon build builds them; source install/setup.bash makes them visible. Use --symlink-install to edit Python and launch files without rebuilding.
  • Containers (Docker, dev containers) make the environment reproducible across machines and give the same result in CI.

Writing nodes: publisher, subscriber, service, action

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan

class Avoid(Node):
    def __init__(self):
        super().__init__("avoid")
        self.pub = self.create_publisher(Twist, "cmd_vel", 10)
        self.sub = self.create_subscription(LaserScan, "scan", self.on_scan, 10)

    def on_scan(self, scan):                          # called for every message on /scan
        ahead = min(r for r in scan.ranges[len(scan.ranges)//2 - 20 : len(scan.ranges)//2 + 20] if r > scan.range_min)
        cmd = Twist()
        cmd.linear.x = 0.0 if ahead < 0.5 else 0.3    # stop when something is within 0.5 m
        cmd.angular.z = 0.6 if ahead < 0.5 else 0.0
        self.pub.publish(cmd)

def main():
    rclpy.init()
    rclpy.spin(Avoid())                               # run until shut down

if __name__ == "__main__":
    main()
Use a…WhenBehavior
TopicContinuous data (sensors, commands, state)One-way, many-to-many, no reply; the latest data matters
ServiceA short question with one answer (set a parameter, reset odometry, get a map)Request and response; the caller waits briefly; avoid long calls
ActionA goal that takes time and may be canceled (navigate, grasp, dock)Goal, periodic feedback, result, and cancel
ParameterA settingDeclared with a default, set from YAML or the command line, can change at run time
  • Callbacks should be short. A callback that takes 12 ms in a 20 Hz timer (period 50 ms) uses 24 % of one thread; if it takes longer than the period, messages queue up and the node falls behind.
  • Executors and callback groups decide which callbacks run in parallel: the single-threaded executor runs one at a time, the multi-threaded executor runs several, and a mutually exclusive group serializes its members.
  • Time stamp every message with the time the data was measured (header.stamp), not when it was published: filters and tf2 depend on it.
  • Composition loads several nodes into one process so that messages pass by pointer without copying (intra-process communication): a large saving for images and point clouds.
  • Lifecycle nodes have defined states (unconfigured, inactive, active), so a driver can be configured, started and stopped in order: Nav2 and ros2_control use them.

Quality of service

Each topic connection has quality-of-service (QoS) settings that decide how messages are delivered. A publisher and a subscriber must have compatible settings or no messages flow, and ROS 2 usually says nothing.

SettingOptionsTypical choice
ReliabilityReliable (retransmit lost messages) or best effort (send and forget)Sensor streams: best effort (a lost image is replaced by the next); commands and state changes: reliable
DurabilityVolatile (only live data) or transient local (the last message is kept for late joiners)Maps and static transforms: transient local
History and depthKeep the last N messages, or allDepth 1 to 10 for streams
Deadline, lifespan, livelinessDetect missing or stale dataSafety monitoring of a sensor that must publish

Worked example: what a queue holds

A camera publishes at 30 Hz and the subscriber has a queue depth of 5: it can fall behind by 5 ÷ 30 = 167 ms before it starts dropping frames. If the images are 640 × 480 color (922 kB each) the queue holds up to 5 × 922 kB = 4.6 MB. A deep queue hides a slow subscriber at the price of old data: for control, prefer a depth of 1 and the freshest message.

from rclpy.qos import qos_profile_sensor_data     # best effort, small queue: the right profile for sensors
self.create_subscription(LaserScan, "scan", self.on_scan, qos_profile_sensor_data)
# if the driver publishes best effort and you ask for reliable, no message arrives
# check with:  ros2 topic info /scan -v

Data rates and bandwidth

Topics carry real data, and a few of them dominate the traffic. Estimate before you put them on a wireless link.

TopicMessageRateData rate
/camera/image_rawsensor_msgs/Image (640×480 rgb8)30 Hz27.6 MB/s
/camera/image_raw/compressedsensor_msgs/CompressedImage (JPEG, about 30 kB)30 Hz900 kB/s
/scansensor_msgs/LaserScan (720 ranges, 4 bytes each + header)10 Hz30 kB/s
/pointssensor_msgs/PointCloud2 (64 × 1 024 points × 16 bytes)10 Hz10.5 MB/s
/odomnav_msgs/Odometry (with covariances, about 700 bytes)50 Hz35 kB/s
/imusensor_msgs/Imu (about 300 bytes)200 Hz60 kB/s
/cmd_velgeometry_msgs/Twist (48 bytes + header)20 Hz2 kB/s
/tftf2_msgs/TFMessage (a few transforms, about 400 bytes)100 Hz40 kB/s

Worked example: can the Wi-Fi carry it?

The raw image and the point cloud alone give 38.1 MB/s; the eight topics above together carry about 39.2 MB/s (314 Mbit/s). A good Wi-Fi 5 link delivers roughly 30 MB/s in practice, shared and with losses, so streaming everything to a laptop is not reliable. Send the compressed image (about 0.9 MB/s), downsample the cloud, or run the heavy processing on the robot and send only results. ros2 topic hz and ros2 topic bw measure the real rate and bandwidth.

Parameters and launch files

# config/nav.yaml
controller:
  ros__parameters:
    max_speed_mps: 0.5
    kp: 2.0
    ki: 0.1

# launch/robot.launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(package="my_robot", executable="driver", parameters=["config/driver.yaml"]),
        Node(package="my_robot", executable="controller", parameters=["config/nav.yaml"],
             remappings=[("cmd_vel", "cmd_vel_nav")]),
        Node(package="robot_state_publisher", executable="robot_state_publisher",
             parameters=[{"robot_description": open("urdf/my_robot.urdf").read()}]),
    ])
  • Everything you might change is a parameter, in a YAML file under version control, so tuning needs no rebuild.
  • Remapping renames topics at launch (cmd_vel to cmd_vel_nav) so that two programs can be connected without editing either.
  • Namespaces separate several robots on one network: /robot1/cmd_vel, /robot2/cmd_vel.
  • Launch arguments and conditions select simulation or hardware, and which sensors to start.
  • Parameter events let a node react when a value changes at run time: use them for tuning, then save the good values back to the file.

tf2: frames over time

The standard frame tree (REP 105)mapfixed by localizationodomfixed by odometrybase_linkon the robotlasercamera_linkimu_linkjumps on correctionsmooth but driftsfixed mounting transforms →Each arrow is a transform; tf2 chains them so any node can ask where one frame is relative to another at a given time.
The standard frame tree: map, odom, base_link and the sensor frames.

tf2 stores the transforms between frames with time stamps, so a node can ask “where was the laser relative to the map at time T?”. A transform is published either once (static_transform_publisher, for a fixed mount) or continuously (odometry, localization). The math of frames is in Frames, Transforms and Arm Kinematics.

Worked example: the cost of a stale transform

A robot moving at 1.0 m/s whose transform is 100 ms old is misplaced by 1.0 × 0.1 = 0.10 m; turning at 1.0 rad/s it is misaligned by 5.7°. A lidar point 5 m away then lands 0.50 m from where it should. This is why every message carries the time of the measurement, tf2 looks up the transform at that time, and why clocks must agree (the “extrapolation into the future” error is a clock problem).

  • Use one clock: the same time source on every machine (chrony or NTP, or PTP for precision), and use_sim_time:=true in simulation.
  • Look up with a timeout and handle the exception: the transform may not have arrived yet.
  • The tree must be a tree: each frame has exactly one parent; two nodes publishing the same transform cause flicker.
  • REP 103 and 105 fix the conventions: SI units, x forward, y left, z up; frames map, odom, base_link.

ros2_control and hardware interfaces

ros2_control is the standard way to connect controllers to hardware: a hardware interface plugin talks to your motors and encoders (reading states, writing commands) in a real-time loop, and controllers (a differential-drive controller, a joint trajectory controller, a position controller) run on top. The same controllers work in simulation, because the simulator supplies a fake hardware interface.

  • diff_drive_controller turns /cmd_vel into wheel speeds (the kinematics of Mobile Robot Kinematics and Odometry) and publishes odometry.
  • joint_trajectory_controller follows a path of joint positions with timing, for arms (see Motor Control, PID and Trajectories).
  • The update loop runs at a fixed rate (100 to 1 000 Hz) in the controller manager: keep it on a real-time kernel, or move the fast loop into the motor driver’s firmware and let ros2_control command it.
  • Write a hardware interface once per robot; everything above it is reusable.

Tools: introspection, bags and visualization

ToolCommandUse
List and inspectros2 node list, ros2 topic list -t, ros2 topic info /scan -vWhat is running, with types and QoS
Watch dataros2 topic echo /odom, ros2 topic hz /scan, ros2 topic bw /pointsValues, rate and bandwidth
Call and sendros2 service call /reset std_srvs/srv/Empty, ros2 topic pub /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1}}"Test a node by hand
Parametersros2 param list, ros2 param set /controller kp 3.0Tune while running
Record and replayros2 bag record -a, ros2 bag play run.mcapCapture a test; debug offline
Graph and transformsrqt_graph, ros2 run tf2_tools view_framesSee connections and the frame tree
VisualizeRViz 2, Foxglove Studio, PlotJuggler3D view of the robot and sensors; time-series plots
Diagnoseros2 doctorChecks the installation and network
  • Record bags in MCAP format (the default in recent distributions), with the topics you need and not the raw images unless you must.
  • Replay with --clock so that nodes see the recorded time; run perception code against a bag as a repeatable test.
  • Use rqt_console or ros2 topic echo /rosout for log messages, and set log levels per node.

Networks, multiple machines and discovery

By default ROS 2 nodes find each other by multicast discovery: every node announces itself and learns about every other, so the number of connections grows with the square of the node count.

Nodes on the networkPossible node pairs: n(n − 1) ÷ 2
1045
30435
1004 950
  • Same ROS_DOMAIN_ID (0 to 101) on machines that must talk, and different ones for separate robots or teams on one network.
  • Wi-Fi blocks or drops multicast on many access points, so discovery fails: use a discovery server (Fast DDS), a wired link, or set ROS_LOCALHOST_ONLY on a single machine.
  • DDS vendor: the default and alternatives (Fast DDS, Cyclone DDS, Zenoh) differ in latency and in how they behave on a lossy network: try another if you have trouble.
  • Security: SROS2 adds authentication and encryption; without it, anyone on the network can publish to /cmd_vel. Put a robot on its own network.

Troubleshooting

SymptomLikely causeTry
Nodes cannot see each otherDifferent ROS_DOMAIN_ID, firewall, different networks, multicast blockedCheck the domain ID on both machines; use a discovery server; allow UDP between them
A topic is listed but no data arriveQoS mismatch (best effort publisher, reliable subscriber)ros2 topic info -v; use qos_profile_sensor_data for sensors
TF: “extrapolation into the future”Clock or time-stamp mismatchSame time source everywhere; use_sim_time in simulation; check header.stamp
Everything lagsA callback that is too slow, a deep queue, a saturated network or CPUtop, ros2 topic hz, shorter callbacks, compressed images, a lower rate
Nodes crash on startParameter missing, wrong type, an unsourced workspaceDeclare parameters with defaults; source install/setup.bash; read the first error
Changes have no effectOld build or a different installed copy is usedcolcon build again; check ros2 pkg prefix; use --symlink-install
Robot jerks or runs away after a lost messageNo timeout on /cmd_vel in the firmwareA heartbeat timeout (see Robot Software Architecture)

Related

FAQ

Python or C++ for ROS 2 nodes?

Python for glue, tools, prototyping and anything not time-critical; C++ for drivers, perception and control that need speed or intra-process zero-copy. The two work together on the same graph.

Why can I see the topic but receive no messages?

Almost always a QoS mismatch. Check ros2 topic info /topic -v and match the reliability and durability of the publisher, using the sensor-data profile for sensors.

How do I run ROS 2 on Windows or macOS?

Windows is supported (Tier 1 for some distributions); macOS is limited. Most people use Ubuntu, a dev container, or WSL2 for development, and a Linux computer on the robot.

How do I send commands safely from a laptop?

Over a wired or dedicated link, with a heartbeat timeout in the firmware so that the robot stops when messages stop, and with an independent hard-wired emergency stop.

References

  • Open Robotics, ROS 2 Documentation (docs.ros.org), including the tutorials, the QoS and tf2 guides, and REP 103 and REP 105.
  • Macenski, S. et al., “Robot Operating System 2: Design, architecture, and uses in the wild”, Science Robotics, 2022.
  • ros2_control documentation; Fast DDS and Cyclone DDS documentation (discovery and QoS).

Last reviewed: October 2026. Distribution dates and package names change: check the current documentation.