ROS 2
The Robot Operating System, version 2: nodes, topics, services and actions, parameters and launch files, tf2, DDS and quality of service, the distributions and their support dates, and MoveIt, Nav2 and micro-ROS.
What ROS 2 is
ROS 2 (the Robot Operating System, version 2) is an open-source framework for writing robot software: a communication layer, client libraries for Python and C++, build tools, command-line tools and a large ecosystem of ready-made packages for drivers, navigation, manipulation and simulation. Despite the name it is not an operating system; it runs on top of one, normally Ubuntu Linux.
The idea is division of labor. A robot needs a camera driver, a motor driver, a localization module, a planner and a user interface. In ROS 2 each is a separate program that publishes and receives messages in agreed formats, so a lidar driver from one vendor works with a navigation stack from another, and a team can replace one part without touching the rest. Universities, start-ups and industrial vendors share packages this way, which is the main reason to use ROS at all.
ROS is developed in the open under Open Robotics. Since 18 March 2024 the Open Source Robotics Alliance (OSRA), set up by Open Robotics, governs ROS, the Gazebo simulator and related projects.
The ROS 2 graph
All running ROS 2 programs and the connections between them form the ROS graph. A ROS 2 node is one participant in that graph: a process (or part of one) that does one job and communicates through topics, services, actions and parameters. The ROS 2 documentation puts the design rule plainly: "each node should do one logical thing". A camera driver node, a detector node and a motor controller node are three nodes, even if they run on the same computer.
Topics
A ROS 2 topic is a named channel
on which nodes publish messages of one fixed type, and to which any
number of nodes can subscribe. The documentation calls it "a
strongly-typed, anonymous publish/subscribe system": strongly typed
because every message on /cmd_vel is a
geometry_msgs/msg/Twist; anonymous because a subscriber
does not know or care which node published it. It is the
publish-subscribe
pattern, used for continuous streams: camera images, lidar scans,
wheel speeds, velocity commands.
Here is a minimal node in Python (rclpy, the Python client library) that tells a mobile robot to drive slowly forward. It follows the form of the current tutorials for ROS 2 Lyrical and Rolling. ROS 2 is not installed on the 64-bit Linux machine (ARM64) that checks this site's examples, so it is not run here:
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from geometry_msgs.msg import Twist
class SlowForward(Node):
"""Drive forward slowly: publish a velocity command twice a second."""
def __init__(self):
super().__init__('slow_forward')
self.publisher_ = self.create_publisher(Twist, 'cmd_vel', 10)
self.timer = self.create_timer(0.5, self.timer_callback)
def timer_callback(self):
msg = Twist()
msg.linear.x = 0.10 # meters per second, forward
msg.angular.z = 0.0 # radians per second, no turning
self.publisher_.publish(msg)
def main(args=None):
try:
with rclpy.init(args=args):
node = SlowForward()
rclpy.spin(node)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
Line by line:
-
The node is a class that inherits from
Node(see inheritance) and gives itself the nameslow_forward. -
create_publisher(Twist, 'cmd_vel', 10)announces a topic, its message type, and a queue depth of 10 — "a required Quality of Service (QoS) setting", explained below. -
A timer calls
timer_callbackevery 0.5 seconds; ROS uses meters, radians and seconds throughout. -
rclpy.spinkeeps the node alive and runs its callbacks until Ctrl+C. Thewith rclpy.init(...)form shuts ROS down cleanly; tutorials for older distributions callrclpy.init(),destroy_node()andrclpy.shutdown()by hand instead.
Services
A ROS 2 service is a named request-and-reply call between nodes: one node offers it, another sends a request and waits for exactly one response. Services suit short questions and commands — "switch to calibration mode", "what is the current map?" — and the documentation says they are "expected to return quickly". Anything that takes seconds or longer belongs in an action. With the turtlesim demo simulator running, this command calls one of its services from the terminal:
ros2 service call /turtle1/teleport_absolute turtlesim/srv/TeleportAbsolute "{x: 2.0, y: 2.0, theta: 0.0}"
Actions
A ROS 2 action is a long-running task that a client sends to a server as a goal; the server reports progress as feedback, sends a result at the end, and can be told to cancel. The documentation describes it as "a long-running remote procedure call with feedback and the ability to cancel or preempt the goal". Driving to a position, moving an arm along a planned path, or docking at a charger are actions: they take time, the operator wants to see progress, and they must be stoppable. Under the hood an action is built from three services (goal, cancel, result) and two topics (feedback, status).
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 1.57}" --feedback
Parameters
Parameters are named settings of a node — a controller gain, a maximum speed, a camera's frame rate. In the documentation's words they "configure nodes at startup (and during runtime), without changing the code". They are usually kept in a YAML file per robot:
line_follower:
ros__parameters:
kp: 4.0
ki: 0.5
kd: 0.2
max_speed: 0.20 # meters per second
ros2 run my_robot line_follower --ros-args --params-file line_follower.yaml
Keeping tuning values out of the code means the same program runs on two robots with different motors, and a change of gain is a reviewed configuration change rather than a new build. Where those gains come from is on Motion Control and PID.
Launch files
A real robot starts dozens of nodes with their parameters, names and
connections. A launch file is a
file that describes which nodes to start, with which parameters,
names and remappings, so that one command brings up a whole system.
Launch files "can be written in XML, YAML, or Python" and are run
with ros2 launch. A YAML launch file that starts the
turtlesim simulator and a line follower with one parameter:
launch:
- node:
pkg: "turtlesim"
exec: "turtlesim_node"
name: "sim"
- node:
pkg: "my_robot"
exec: "line_follower"
name: "line_follower"
param:
- name: "kp"
value: 4.0
ros2 launch my_robot bringup.launch.yaml
XML and YAML launch files are short and declarative; Python launch files can contain logic (start this node only on the real robot, not in simulation) at the cost of being harder to read. Many projects use XML or YAML for simple bring-up and Python only where they need the logic.
Building and running
ROS 2 code lives in packages inside a
workspace: a folder with a
src subfolder. The build tool
colcon builds every package in it and writes a
setup script that makes them available in the terminal:
ros2_ws/ ├── src/ │ └── my_robot/ ← your package (Python or C++) ├── build/ ← created by colcon ├── install/ ← created by colcon; contains setup.bash └── log/
cd ~/ros2_ws
colcon build --symlink-install
source install/setup.bash
--symlink-install links Python files instead of copying
them, so edits take effect without rebuilding. The
ros2 command-line tool then inspects the running graph.
With the turtlesim simulator started by
ros2 run turtlesim turtlesim_node, a second terminal
shows:
ros2 node list
ros2 topic list
/turtlesim /parameter_events /rosout /turtle1/cmd_vel /turtle1/color_sensor /turtle1/pose
ros2 topic echo /turtle1/pose
x: 5.544444561004639 y: 5.544444561004639 theta: 0.0 linear_velocity: 0.0 angular_velocity: 0.0 ---
tf2: keeping track of frames
A robot has many coordinate frames: the map, the robot base, each wheel, the lidar, the camera, the gripper. tf2 is ROS 2's transform library, "which lets the user keep track of multiple coordinate frames over time". Nodes publish the transform between pairs of frames (a parent and a child); tf2 keeps them in a tree, buffered in time, and any node can ask for the transform between any two frames at a given moment. The camera example on Robot Vision — a detected part, converted from the camera frame to the robot base — is exactly the question tf2 answers.
For mobile robots, the convention REP 105 names the main frames:
map (a fixed world frame), odom (smooth
but drifting, from wheel odometry) and base_link
(attached to the robot). Localization publishes
map → odom, odometry publishes
odom → base_link, and tf2 chains them. The arithmetic
is a composition of poses, here in 2D:
import math
def compose(a, b):
"""Pose b, given in frame a, expressed in a's parent frame (2D: x, y, theta)."""
ax, ay, at = a
bx, by, bt = b
x = ax + math.cos(at) * bx - math.sin(at) * by
y = ay + math.sin(at) * bx + math.cos(at) * by
return x, y, at + bt
def show(name, pose):
x, y, t = pose
print(f"{name:20} x = {x:6.3f} y = {y:6.3f} heading = {math.degrees(t):5.1f} deg")
map_to_odom = (0.20, -0.10, math.radians(5)) # from localization
odom_to_base = (3.00, 1.50, math.radians(90)) # from wheel odometry
show("odom -> base_link", odom_to_base)
show("map -> base_link", compose(map_to_odom, odom_to_base))
odom -> base_link x = 3.000 y = 1.500 heading = 90.0 deg map -> base_link x = 3.058 y = 1.656 heading = 95.0 deg
The 5-degree part of the correction alone shifts the robot by about
29 cm, because it rotates the whole 3.35 m odometry path around the
odom origin; together with the 22 cm offset, the robot ends up 17 cm
from where odometry alone would put it. The matrix form of the same
operation is on
Coordinate Frames and Transforms; why odom drifts is on
Mobile Robots and Odometry.
DDS and quality of service
ROS 2 does not move messages itself. Underneath sits a middleware,
plugged in through a layer called the RMW (ROS middleware
interface). The default is an implementation of the
Data Distribution Service
(DDS): an industrial publish-subscribe standard from the Object
Management Group in which participants discover each other on the
network and exchange typed data directly, with configurable delivery
guarantees. As of October 2026 the default RMW is eProsima's Fast
DDS; Cyclone DDS and RTI Connext are alternatives, and Eclipse Zenoh
(rmw_zenoh_cpp), which uses a small router instead of
DDS discovery, has been a Tier 1 option since the Kilted release.
Discovery is automatic: by default every ROS 2 node on the same
network segment and with the same domain number
(ROS_DOMAIN_ID) sees every other. That is convenient on
a lab bench and a risk on a shared network — two teams can steer
each other's robots. Give each robot or test setup its own domain
ID, and put robots on their own network segment (see
segmentation
in the OT topic). ROS 2 can also use the DDS security plugins for
authentication, encryption and access control; they are off by
default.
Quality of service
Each publisher and subscriber has a QoS (quality of service) profile. According to the documentation, ROS 2 can be "as reliable as TCP or as best-effort as UDP" — here TCP means the network protocol (see TCP versus UDP), not a robot's tool center point. The main settings:
| Setting | Choices | Typical use |
|---|---|---|
| Reliability | reliable · best effort | commands and maps reliable; camera and lidar streams best effort (a late image is useless anyway) |
| History and depth | keep last N · keep all | keep last 10 is the common default |
| Durability | volatile · transient local | transient local lets a late subscriber still get the last message, for example a static map |
With "keep last 10", a slow subscriber does not get an ever-growing
backlog; the oldest messages are dropped. Python's
collections.deque with a maximum length behaves the
same way:
from collections import deque
depth = 10 # QoS history: keep last 10
queue = deque(maxlen=depth)
# A camera node publishes 25 images while the subscriber is busy.
for number in range(1, 26):
queue.append(f"image #{number}")
print(f"waiting in the queue: {len(queue)}")
print(f"oldest: {queue[0]}, newest: {queue[-1]}")
print(f"dropped: {25 - len(queue)}")
waiting in the queue: 10 oldest: image #16, newest: image #25 dropped: 15
Reliability is the other trade-off. A simplified model of a lossy Wi-Fi link: best effort sends each message once, reliable resends until it gets through. Reliable delivers everything, but resends cost bandwidth and add delay — on a busy link, retrying old camera frames can make every new frame late.
import random
random.seed(7)
LOSS = 0.05 # 5% of packets get lost on a busy Wi-Fi link
MESSAGES = 200
best_effort = 0
reliable_sends = 0
for _ in range(MESSAGES):
if random.random() > LOSS: # best effort: one try only
best_effort += 1
while True: # reliable: resend until it arrives
reliable_sends += 1
if random.random() > LOSS:
break
print(f"best effort: {best_effort} of {MESSAGES} arrived")
print(f"reliable: {MESSAGES} of {MESSAGES} arrived, "
f"{reliable_sends - MESSAGES} resends")
best effort: 190 of 200 arrived reliable: 200 of 200 arrived, 9 resends
Distributions and support dates
ROS 2 is released as named distributions, a matched set of packages. According to the documentation, "there is a new ROS 2 distribution released yearly on May 23rd (World Turtle Day)". Since Humble (2022), the releases of even years (2022, 2024, 2026) have been long-term support (LTS) releases with five years of support; the others are supported for about a year and a half. Rolling Ridley is the continuously updated development line, for people who build ROS itself — it may break at any time.
| Distribution | Released | End of support | Type |
|---|---|---|---|
| Humble Hawksbill | 23 May 2022 | May 2027 | LTS |
| Jazzy Jalisco | 23 May 2024 | May 2029 | LTS |
| Kilted Kaiju | 23 May 2025 | end of 2026 | non-LTS |
| Lyrical Luth | 22 May 2026 | May 2031 | LTS (newest) |
| Rolling Ridley | continuous | — | development |
Distributions and dates as of October 2026.
For a project that must run for years, the support window matters more than the newest features:
# ROS 2 distributions and end of support, as of October 2026
distros = [
("Humble Hawksbill", "LTS", 2027, 5),
("Jazzy Jalisco", "LTS", 2029, 5),
("Kilted Kaiju", "non-LTS", 2026, 12), # end of 2026
("Lyrical Luth", "LTS", 2031, 5),
]
today = (2026, 10)
def months_between(start, end):
return (end[0] - start[0]) * 12 + (end[1] - start[1])
for name, kind, year, month in sorted(distros, key=lambda d: (d[2], d[3])):
left = months_between(today, (year, month))
print(f"{name:17} {kind:8} ends {year}-{month:02d} {left:3d} months left")
Kilted Kaiju non-LTS ends 2026-12 2 months left Humble Hawksbill LTS ends 2027-05 7 months left Jazzy Jalisco LTS ends 2029-05 31 months left Lyrical Luth LTS ends 2031-05 55 months left
In practice a team also checks that the drivers it needs exist for the chosen distribution: a vendor driver that only supports Jazzy can be a good reason to start on Jazzy rather than Lyrical.
Supported platforms
Each distribution names its platforms in tiers. Tier 1 platforms are fully built and tested: for Humble, Jazzy, Kilted and Lyrical that is Ubuntu (on both amd64 and arm64, so also on many ARM boards) and Windows. Lyrical's Tier 1 platforms are Ubuntu 26.04 and Windows 11. Debian appears only as Tier 3 (community-tested, without guarantees), and so do macOS and 32-bit ARM. Lyrical needs Python 3.12 to 3.14, C++20 and C17.
MoveIt, Nav2 and micro-ROS
Three large projects built on ROS 2 cover much of what a new robot needs:
- MoveIt — "a motion planning, manipulation, and kinematics framework for ROS" (its website moved to moveit.ai). It plans collision-free arm motions using the kinematics of the robot's description and the obstacles around it. As of October 2026, MoveIt 2.15 is the LTS version for Lyrical, 2.12 for Jazzy.
- Nav2 — the navigation stack for mobile robots, which calls itself a "production-grade autonomy framework": maps, localization, path planning, obstacle avoidance and recovery behaviors. A navigation goal is an action. As of October 2026 it is actively supported for Lyrical and Jazzy.
- micro-ROS — ROS 2 on microcontrollers. A small board such as an ESP32 (with ESP-IDF and FreeRTOS) or a Raspberry Pi Pico runs a micro-ROS client and appears in the ROS graph through an agent on the main computer — useful for motor controllers and sensors that need hard timing. As of October 2026 its ESP-IDF component has branches for Humble, Jazzy, Kilted and Rolling, but not yet for Lyrical. It is an optional next step on Building Your First Robot.
Industrial robot vendors increasingly ship ROS 2 drivers of their own; which ones, and how they fit next to the vendor's own language, is on Industrial Robots and Cobots in Practice. Testing a ROS 2 system without hardware is on Simulation and Digital Twins.