ROS 2 Installation and First Steps: Nodes, Topics, and Messages

ROS 2 is a message-passing middleware, and everything you build on it reduces to three primitives: processes that compute (nodes), named channels that carry data (topics), and typed payloads (messages). This guide installs the current LTS distribution on Ubuntu, verifies the install, and builds a working sensor pipeline in rclpy. You also get the CLI commands to inspect it and the QoS settings that decide whether data arrives.

Quick Takeaways

  • Install: Use ROS 2 Lyrical Luth (an LTS release supported until May 2031) on Ubuntu 26.04, or Jazzy Jalisco on Ubuntu 24.04 (supported until May 2029).
  • Nodes are single-purpose executables. Topics are many-to-many, anonymous publish/subscribe channels.
  • Messages are typed .msg structures serialized with CDR over DDS. Use ros2 interface show to read any definition.
  • QoS mismatch is the most common reason a subscriber receives nothing. Match reliability and durability on both ends.

Choosing a ROS 2 Distribution

Pick the distribution from your OS, not from preference. Binary packages exist only for Tier 1 platforms.

Distribution Type Ubuntu Base Support Ends Use Case
Lyrical Luth LTS 26.04 (Resolute) May 2031 New projects
Jazzy Jalisco LTS 24.04 (Noble) May 2029 Existing 24.04 fleets
Humble Hawksbill LTS 22.04 (Jammy) May 2027 Legacy maintenance
Kilted Kaiju Non-LTS 24.04 (Noble) Dec 2026 Avoid for new work

Lyrical binaries target Ubuntu 26.04 (amd64/aarch64), RHEL 10 (amd64), and Windows 11 (amd64). The commands below use lyrical. On Ubuntu 24.04, replace lyrical with jazzy; everything else is identical.

Some ecosystem packages lag a new release. Around launch, community reports noted that Nav2, slam-toolbox, and cartographer-ros had not yet been released for Lyrical. Check the package index for your distro before committing a navigation project to a fresh release.

Installing ROS 2 on Ubuntu

Step 1: Locale and repositories

# ROS 2 requires a UTF-8 locale
locale   # confirm LANG=en_US.UTF-8 or C.UTF-8

sudo apt install software-properties-common -y
sudo add-apt-repository universe
sudo apt update && sudo apt install curl -y

Step 2: Add the ROS 2 apt source

# Fetch the latest ros-apt-source release tag
export ROS_APT_SOURCE_VERSION=$(curl -s https://api.github.com/repos/ros-infrastructure/ros-apt-source/releases/latest | grep -F "tag_name" | awk -F\" '{print $4}')

# Download the .deb matching your Ubuntu codename (resolute on 26.04, noble on 24.04)
curl -L -o /tmp/ros2-apt-source.deb \
  "https://github.com/ros-infrastructure/ros-apt-source/releases/download/${ROS_APT_SOURCE_VERSION}/ros2-apt-source_${ROS_APT_SOURCE_VERSION}.$(. /etc/os-release && echo ${UBUNTU_CODENAME:-${VERSION_CODENAME}})_all.deb"

sudo dpkg -i /tmp/ros2-apt-source.deb

This package installs both the repository definition and the GPG key.

Step 3: Install the packages

sudo apt update && sudo apt upgrade -y

# Desktop = ROS core + RViz + demos + tutorials
sudo apt install ros-lyrical-desktop -y

# colcon, rosdep, vcstool and other build tooling
sudo apt install ros-dev-tools -y

On a headless robot computer (Raspberry Pi, Jetson), install ros-lyrical-ros-base instead. It omits the GUI tools.

Step 4: Source the environment

echo "source /opt/ros/lyrical/setup.bash" >> ~/.bashrc
source ~/.bashrc

printenv | grep ROS
# ROS_DISTRO=lyrical
# ROS_VERSION=2

ROS_DOMAIN_ID isolates DDS traffic. Machines on the same network with the same ID see each other’s topics. Set a unique value (0–101) per robot or per student:

export ROS_DOMAIN_ID=42

Step 5: Verify with the talker and listener

Open two terminals.

# Terminal 1
ros2 run demo_nodes_cpp talker

# Terminal 2
ros2 run demo_nodes_py listener

The listener prints I heard: [Hello World: N]. A C++ publisher talking to a Python subscriber proves the middleware, type support, and language bindings all work.

Core Architecture: Nodes, Topics, Messages

Nodes

A node is a process (or a component inside one) with one job: read a LiDAR, run a controller, fuse an IMU with odometry. Keep nodes small. A monolithic node couples failure modes and prevents reuse.

ros2 node list              # all running nodes
ros2 node info /talker      # publishers, subscribers, services, actions

Topics

A topic is a named, typed channel. Publishers and subscribers never reference each other. Each only knows the topic name and message type.

Property Behavior
Pattern Many publishers → many subscribers
Coupling Decoupled (anonymous)
Typing One message type per topic, enforced at connection time
Delivery Governed by QoS profile
Best for Streaming data: sensors, state, commands

For request/response, use services. For long-running goals with feedback, use actions.

Messages

A message is a typed struct defined in a .msg file. Inspect any message with:

ros2 interface show geometry_msgs/msg/Twist
Vector3  linear     # m/s along x, y, z (float64 each)
Vector3  angular    # rad/s about x, y, z (float64 each)

Standard messages live in std_msgs, geometry_msgs, and sensor_msgs. Prefer sensor_msgs and geometry_msgs over std_msgs for robot data. They carry a std_msgs/Header with a timestamp and frame_id, which TF2 and sensor fusion need.

Bandwidth estimate. Serialized size is approximately the sum of field sizes plus a 4-byte CDR header:

Twist = 6 × float64 = 48 bytes  →  ~52 bytes on the wire
Rate  = 50 Hz
BW    = 52 B × 50 Hz = 2,600 B/s ≈ 2.6 kB/s

A 640×480 rgb8 Image is 921,600 bytes. At 30 Hz that is about 27.6 MB/s, which is why camera topics need compressed transport or intra-process communication.

Building Your First Publisher and Subscriber

You will build a temperature sensor node and a monitor node that filters the readings and flags overheating.

Create the workspace and package

mkdir -p ~/ros2_ws/src && cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python first_steps \
  --dependencies rclpy sensor_msgs std_msgs

Publisher node

Create first_steps/first_steps/temp_publisher.py:

import random

import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data  # BEST_EFFORT, depth 5: standard for sensors
from sensor_msgs.msg import Temperature


class TempPublisher(Node):
    def __init__(self):
        super().__init__('temp_publisher')
        # Topic name, message type, and QoS profile
        self.pub = self.create_publisher(Temperature, 'engine/temperature',
                                         qos_profile_sensor_data)
        # 10 Hz timer -> period T = 1/f = 0.1 s
        self.timer = self.create_timer(0.1, self.publish_reading)
        self.base = 60.0  # simulated baseline, deg C

    def publish_reading(self):
        msg = Temperature()
        msg.header.stamp = self.get_clock().now().to_msg()  # builtin_interfaces/Time
        msg.header.frame_id = 'engine_link'                 # TF frame of the sensor
        msg.temperature = self.base + random.gauss(0.0, 1.5)  # field unit: deg C
        msg.variance = 2.25                                  # sigma^2 = 1.5^2
        self.pub.publish(msg)


def main():
    rclpy.init()
    node = TempPublisher()
    try:
        rclpy.spin(node)  # blocks, runs callbacks
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.try_shutdown()


if __name__ == '__main__':
    main()

Subscriber node with an exponential filter

Raw readings carry noise. The subscriber applies a first-order low-pass filter:

x̂[k] = α · z[k] + (1 − α) · x̂[k−1]       0 < α ≤ 1

Here z[k] is the raw measurement and x̂[k] is the filtered estimate. A smaller α gives smoother output but more lag. The time constant is approximately τ ≈ T(1−α)/α. With T = 0.1 s and α = 0.2, τ = 0.4 s.

Create first_steps/first_steps/temp_monitor.py:

import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import Temperature


class TempMonitor(Node):
    def __init__(self):
        super().__init__('temp_monitor')
        # Declare parameters so they can be changed at runtime via `ros2 param set`
        self.declare_parameter('alpha', 0.2)         # filter coefficient
        self.declare_parameter('limit_c', 65.0)      # alert threshold, deg C
        self.est = None                              # filtered estimate x_hat
        self.sub = self.create_subscription(
            Temperature, 'engine/temperature', self.on_temp,
            qos_profile_sensor_data)                 # must be compatible with publisher QoS

    def on_temp(self, msg: Temperature):
        alpha = self.get_parameter('alpha').value
        limit = self.get_parameter('limit_c').value
        z = msg.temperature
        # Initialize on first sample, then apply the recursive filter
        self.est = z if self.est is None else alpha * z + (1.0 - alpha) * self.est
        if self.est > limit:
            self.get_logger().warn(f'Overtemp: {self.est:.1f} C > {limit:.1f} C')
        else:
            self.get_logger().info(f'raw={z:.1f}  filtered={self.est:.1f}')


def main():
    rclpy.init()
    node = TempMonitor()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.try_shutdown()


if __name__ == '__main__':
    main()

Register entry points

Edit setup.py:

entry_points={
    'console_scripts': [
        'temp_publisher = first_steps.temp_publisher:main',
        'temp_monitor   = first_steps.temp_monitor:main',
    ],
},

Build, source, and run

cd ~/ros2_ws
rosdep install --from-paths src --ignore-src -y   # resolve declared dependencies
colcon build --symlink-install --packages-select first_steps
source install/setup.bash

# Terminal 1
ros2 run first_steps temp_publisher
# Terminal 2
ros2 run first_steps temp_monitor

--symlink-install links Python files into install/, so edits take effect without rebuilding.

Inspecting the Graph with CLI Tools

The ros2 CLI is your primary debugging instrument. Learn these before reaching for a GUI.

Command Purpose
ros2 topic list -t List topics with their types
ros2 topic echo /engine/temperature Print messages live
ros2 topic hz /engine/temperature Measure publish rate (expect ~10 Hz)
ros2 topic bw /engine/temperature Measure bandwidth
ros2 topic info /engine/temperature -v Show endpoints and their QoS
ros2 param set /temp_monitor alpha 0.5 Change a parameter live
ros2 run rqt_graph rqt_graph Visualize nodes and topics
ros2 bag record -a Record all topics for replay

Run ros2 topic info -v first when data fails to arrive. It prints each endpoint’s reliability, durability, and history settings side by side.

Custom Messages

Use a custom message when no standard type fits. Interfaces require a CMake package, even if your nodes are Python.

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake first_interfaces
mkdir first_interfaces/msg

first_interfaces/msg/ThermalAlert.msg:

std_msgs/Header header
string          source         # sensor or subsystem name
float32         temperature_c  # filtered value, deg C
float32         limit_c        # threshold that was exceeded
uint8           severity       # 0=info, 1=warn, 2=critical

Add to CMakeLists.txt before ament_package():

find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/ThermalAlert.msg"
  DEPENDENCIES std_msgs
)

Add to package.xml:

<buildtool_depend>rosidl_default_generators</buildtool_depend>
<depend>std_msgs</depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

Build, source, and confirm:

colcon build --packages-select first_interfaces && source install/setup.bash
ros2 interface show first_interfaces/msg/ThermalAlert

QoS: Why Your Subscriber Hears Nothing

Quality of Service profiles control delivery guarantees. A publisher and subscriber connect only if their policies are compatible.

Policy Options Effect
Reliability RELIABLE / BEST_EFFORT Retransmit lost samples vs. drop them
Durability VOLATILE / TRANSIENT_LOCAL Late joiners miss history vs. receive last samples
History KEEP_LAST(n) / KEEP_ALL Queue depth
Deadline duration Alert if no message in period

Compatibility is request-vs-offer: the subscriber’s requirement must not exceed what the publisher offers.

Publisher Subscriber Connects?
RELIABLE RELIABLE Yes
RELIABLE BEST_EFFORT Yes
BEST_EFFORT BEST_EFFORT Yes
BEST_EFFORT RELIABLE No (silent mismatch)

Use BEST_EFFORT with qos_profile_sensor_data for high-rate sensor streams, where a stale sample is worthless. Use RELIABLE for commands and configuration. Use TRANSIENT_LOCAL for latched data such as maps.

Real-World Workflow: Driving a Robot in a Circle with cmd_vel

Before wiring real hardware, validate your command pipeline in turtlesim. It uses the same geometry_msgs/msg/Twist interface as a differential-drive mobile base.

sudo apt install ros-lyrical-turtlesim -y   # skip if you installed desktop
ros2 run turtlesim turtlesim_node

In a second terminal, publish a constant velocity at 10 Hz:

ros2 topic pub -r 10 /turtle1/cmd_vel geometry_msgs/msg/Twist \
  "{linear: {x: 0.5}, angular: {z: 0.5}}"

For a differential-drive robot, the turning radius follows directly from the commanded velocities:

r = v / ω = 0.5 / 0.5 = 1.0 (distance units)
T_lap = 2π / ω = 2π / 0.5 ≈ 12.57 s

Check the result with the CLI:

ros2 topic hz /turtle1/pose        # turtlesim reports pose at ~62.5 Hz
ros2 topic echo /turtle1/pose --once

Compare the measured lap time with 12.57 s. A mismatch on real hardware points to wheel slip, an incorrect wheel-separation parameter, or command latency from a congested network. The same workflow transfers to a physical base: publish Twist to /cmd_vel, read odometry, and compare commanded and measured trajectories.

Troubleshooting Checklist

Symptom Likely Cause Fix
ros2: command not found Environment not sourced source /opt/ros/lyrical/setup.bash
Topic listed, no data QoS mismatch ros2 topic info -v, align policies
Nodes invisible across machines Different ROS_DOMAIN_ID or multicast blocked Match IDs, check firewall and switch IGMP
Package not found after build Workspace not sourced source install/setup.bash
Python edits ignored Built without --symlink-install Rebuild with the flag
Apt cannot find ros-lyrical-* Wrong Ubuntu release Confirm 26.04, or use Jazzy on 24.04

FAQ

Which ROS 2 version should I install in 2026?

Install Lyrical Luth on Ubuntu 26.04 for new projects. It is an LTS release with support through May 2031. Use Jazzy Jalisco on Ubuntu 24.04. Check that your key dependencies (Nav2, SLAM packages) have been released for your chosen distribution before you commit.

What is the difference between a ROS 2 node and a topic?

A node is a running process that performs computation. A topic is a named channel that nodes use to exchange typed messages. Nodes publish to or subscribe to topics without knowing about each other.

Why does my ROS 2 subscriber receive no messages?

The most common cause is a QoS mismatch. A BEST_EFFORT publisher cannot serve a RELIABLE subscriber. Run ros2 topic info <topic> -v to compare both endpoints’ QoS profiles. Also confirm that both nodes share the same ROS_DOMAIN_ID and message type.

Can I use Python for custom ROS 2 messages?

Yes, to use them, but not to define them. Define .msg files in an ament_cmake interface package, build it, then import the generated Python classes in any rclpy node.

Hot this week

Humanoids in Factories and Warehouses: What They Actually Do Today

Which humanoid robots do real work in factories and warehouses today? See deployment data, ROI math and ROS 2 code. Read the 2026 guide.

Debugging Robot Code: Logs, Serial Output, rosbag, and Foxglove

Debug robot code faster with ROS 2 logging, serial capture, rosbag2 recording, and Foxglove. Copy the configs and scripts into your stack today.

Build a Mapping Robot With SLAM From Scratch

Build a SLAM mapping robot from scratch: wiring, odometry, LiDAR, ROS 2 slam_toolbox config and tuning. Follow the full guide and map your first room.

Depth Cameras: Stereo, Time-of-Flight, and Structured Light Compared

Compare stereo, time-of-flight, and structured light depth cameras: accuracy, range, sunlight, ROS2 setup. Pick the right sensor for your robot.

Python vs C++ for Robotics: Which Should You Use?

Python vs C++ for robotics compared: latency, ROS2 performance, control loops, and code examples. Learn which language fits your robot. Read the guide.

Topics

Humanoids in Factories and Warehouses: What They Actually Do Today

Which humanoid robots do real work in factories and warehouses today? See deployment data, ROI math and ROS 2 code. Read the 2026 guide.

Debugging Robot Code: Logs, Serial Output, rosbag, and Foxglove

Debug robot code faster with ROS 2 logging, serial capture, rosbag2 recording, and Foxglove. Copy the configs and scripts into your stack today.

Build a Mapping Robot With SLAM From Scratch

Build a SLAM mapping robot from scratch: wiring, odometry, LiDAR, ROS 2 slam_toolbox config and tuning. Follow the full guide and map your first room.

Depth Cameras: Stereo, Time-of-Flight, and Structured Light Compared

Compare stereo, time-of-flight, and structured light depth cameras: accuracy, range, sunlight, ROS2 setup. Pick the right sensor for your robot.

Python vs C++ for Robotics: Which Should You Use?

Python vs C++ for robotics compared: latency, ROS2 performance, control loops, and code examples. Learn which language fits your robot. Read the guide.

Build a ROS 2 Rover: Raspberry Pi 5, Motors, and LiDAR

Build a ROS 2 rover with Raspberry Pi 5, RPLIDAR C1, and a Pico motor controller. Copy the code, wire it, and map your room. Start building now.

Imitation Learning for Robots: Teach by Demonstration

Learn imitation learning for robots: behavior cloning, DAgger, ACT, and diffusion policies with ROS2 code. Build your first policy today.

LiDAR for Robots: 2D, 3D, and Solid-State Compared

Compare 2D, 3D, and solid-state LiDAR for robots. See specs, ROS2 configs, code, and a selection workflow. Pick the right sensor now.

Related Articles

Popular Categories