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.
On this page
- The concepts
- Distributions, installation and workspaces
- Writing nodes: publisher, subscriber, service, action
- Quality of service
- Data rates and bandwidth
- Parameters and launch files
- tf2: frames over time
- ros2_control and hardware interfaces
- Tools: introspection, bags and visualization
- Networks, multiple machines and discovery
- Troubleshooting
- FAQ
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.
| Concept | What it is | Example |
|---|---|---|
| Node | One program doing one job | camera_driver, planner |
| Topic | A named stream of messages. Nodes publish to it or subscribe to it; they need not know about each other | /cmd_vel carries velocity commands |
| Message | The typed data on a topic | geometry_msgs/Twist: linear and angular velocity |
| Service | A request and reply for quick questions | Reset the odometry |
| Action | A long task with feedback and cancel | Navigate to a goal, with progress updates |
| Parameter | A named setting of a node that can be changed while it runs | Maximum speed, PID gains |
| Launch file | Starts and configures many nodes together | Bring up the whole robot |
| tf2 | Keeps track of coordinate frames and their relationships over time | Where is the camera relative to the wheels? |
| URDF | An XML description of the robot’s links, joints and sensors | Used for visualization, simulation and planning |
| Bag | A recording of topics that can be replayed | Debug a failure after the event |
Distributions, installation and workspaces
| Distribution | Released | Support | Notes |
|---|---|---|---|
| Humble Hawksbill | May 2022 | Long-term support to May 2027 | Ubuntu 22.04; very widely used and supported by packages |
| Jazzy Jalisco | May 2024 | Long-term support to May 2029 | Ubuntu 24.04; the current choice for new projects |
| Rolling | Continuous | None | The 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.bashin every shell. - Workspace:
~/ros2_ws/srcholds your packages;colcon buildbuilds them;source install/setup.bashmakes them visible. Use--symlink-installto 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… | When | Behavior |
|---|---|---|
| Topic | Continuous data (sensors, commands, state) | One-way, many-to-many, no reply; the latest data matters |
| Service | A short question with one answer (set a parameter, reset odometry, get a map) | Request and response; the caller waits briefly; avoid long calls |
| Action | A goal that takes time and may be canceled (navigate, grasp, dock) | Goal, periodic feedback, result, and cancel |
| Parameter | A setting | Declared 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_controluse 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.
| Setting | Options | Typical choice |
|---|---|---|
| Reliability | Reliable (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 |
| Durability | Volatile (only live data) or transient local (the last message is kept for late joiners) | Maps and static transforms: transient local |
| History and depth | Keep the last N messages, or all | Depth 1 to 10 for streams |
| Deadline, lifespan, liveliness | Detect missing or stale data | Safety 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.
| Topic | Message | Rate | Data rate |
|---|---|---|---|
/camera/image_raw | sensor_msgs/Image (640×480 rgb8) | 30 Hz | 27.6 MB/s |
/camera/image_raw/compressed | sensor_msgs/CompressedImage (JPEG, about 30 kB) | 30 Hz | 900 kB/s |
/scan | sensor_msgs/LaserScan (720 ranges, 4 bytes each + header) | 10 Hz | 30 kB/s |
/points | sensor_msgs/PointCloud2 (64 × 1 024 points × 16 bytes) | 10 Hz | 10.5 MB/s |
/odom | nav_msgs/Odometry (with covariances, about 700 bytes) | 50 Hz | 35 kB/s |
/imu | sensor_msgs/Imu (about 300 bytes) | 200 Hz | 60 kB/s |
/cmd_vel | geometry_msgs/Twist (48 bytes + header) | 20 Hz | 2 kB/s |
/tf | tf2_msgs/TFMessage (a few transforms, about 400 bytes) | 100 Hz | 40 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_veltocmd_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
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:=truein 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_velinto 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
| Tool | Command | Use |
|---|---|---|
| List and inspect | ros2 node list, ros2 topic list -t, ros2 topic info /scan -v | What is running, with types and QoS |
| Watch data | ros2 topic echo /odom, ros2 topic hz /scan, ros2 topic bw /points | Values, rate and bandwidth |
| Call and send | ros2 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 |
| Parameters | ros2 param list, ros2 param set /controller kp 3.0 | Tune while running |
| Record and replay | ros2 bag record -a, ros2 bag play run.mcap | Capture a test; debug offline |
| Graph and transforms | rqt_graph, ros2 run tf2_tools view_frames | See connections and the frame tree |
| Visualize | RViz 2, Foxglove Studio, PlotJuggler | 3D view of the robot and sensors; time-series plots |
| Diagnose | ros2 doctor | Checks 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
--clockso that nodes see the recorded time; run perception code against a bag as a repeatable test. - Use
rqt_consoleorros2 topic echo /rosoutfor 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 network | Possible node pairs: n(n − 1) ÷ 2 |
|---|---|
| 10 | 45 |
| 30 | 435 |
| 100 | 4 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_ONLYon 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
| Symptom | Likely cause | Try |
|---|---|---|
| Nodes cannot see each other | Different ROS_DOMAIN_ID, firewall, different networks, multicast blocked | Check the domain ID on both machines; use a discovery server; allow UDP between them |
| A topic is listed but no data arrive | QoS 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 mismatch | Same time source everywhere; use_sim_time in simulation; check header.stamp |
| Everything lags | A callback that is too slow, a deep queue, a saturated network or CPU | top, ros2 topic hz, shorter callbacks, compressed images, a lower rate |
| Nodes crash on start | Parameter missing, wrong type, an unsourced workspace | Declare parameters with defaults; source install/setup.bash; read the first error |
| Changes have no effect | Old build or a different installed copy is used | colcon build again; check ros2 pkg prefix; use --symlink-install |
| Robot jerks or runs away after a lost message | No timeout on /cmd_vel in the firmware | A heartbeat timeout (see Robot Software Architecture) |
Related
- Robotics Tools: the overview of this guide.
- Robot Software Architecture: where ROS 2 sits among the layers.
- Simulation and Digital Twins: running the same graph in a simulator.
- Localization, Mapping and Navigation: Nav2 and SLAM Toolbox build on these concepts.
- Programming Languages: Python and C++ for nodes.
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.
