ROS 2 Basics on Raspberry Pi: Nodes, Topics and a Safety Stop Node

As a robot grows, a single Arduino sketch that reads every sensor and drives every motor becomes hard to manage. ROS 2, the Robot Operating System, solves this by splitting the robot's software into small independent programs called nodes that exchange data over named channels called topics. It is not an operating system in the usual sense; it is a set of libraries and tools that runs on Linux and is used by research labs and companies around the world.

In this tutorial you will set up ROS 2 on a Raspberry Pi, create a Python package, and write two nodes: one publishes a distance reading, and the other subscribes to it and publishes velocity commands on /cmd_vel, stopping the robot when an obstacle is close. This connects directly to the v and omega commands from the differential drive kinematics tutorial.

Core Concepts

ConceptMeaningExample
NodeOne process with one jobA lidar driver, a motor controller, a planner
TopicNamed channel carrying one message type, many-to-many/scan, /odom, /cmd_vel
MessageTyped data structuregeometry_msgs/msg/Twist holds linear and angular velocity
Publisher / subscriberWriter and reader of a topic; they never call each other directlyPlanner publishes /cmd_vel, base controller subscribes
ServiceRequest-response call for occasional actionsReset odometry
PackageUnit of code that is built and installed togetherrobot_basics

Because publishers and subscribers only agree on a topic name and message type, you can replace the simulated distance node below with a real sensor driver later without changing the safety node at all. ROS 2 uses DDS middleware for discovery, so nodes on the same network and the same ROS_DOMAIN_ID find each other automatically, even across different computers.

Setting Up the Raspberry Pi

ROS 2 releases are tied to specific Ubuntu versions. ROS 2 Jazzy Jalisco is a long-term support release targeting Ubuntu 24.04, which runs on the Raspberry Pi 4 and Pi 5 in its 64-bit server edition. ROS 2 Humble targets Ubuntu 22.04 if you already have that installed. Use a Pi with at least 4 GB of RAM and a good quality microSD card or SSD.

  1. Flash Ubuntu Server 24.04 LTS (64-bit) for Raspberry Pi using Raspberry Pi Imager, enabling SSH and Wi-Fi in its settings.
  2. Log in over SSH and run sudo apt update and sudo apt upgrade.
  3. Follow the official ROS 2 Jazzy installation guide for Ubuntu (deb packages) to set the locale and add the ROS 2 apt repository.
  4. Install the base packages and build tools: sudo apt install ros-jazzy-ros-base ros-dev-tools.
  5. Add source /opt/ros/jazzy/setup.bash to the end of ~/.bashrc so every new terminal has ROS 2 available.

The ros-base variant has no graphical tools, which suits a headless Pi. You can run tools such as rqt and RViz on a laptop on the same network instead.

Creating the Package

Create a workspace and a Python package with its dependencies
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python robot_basics \
  --dependencies rclpy std_msgs geometry_msgs
# Node files go in ~/ros2_ws/src/robot_basics/robot_basics/

Node 1: distance publisher

robot_basics/distance_pub.py: publishes a simulated distance at 10 Hz
import math
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32


class DistancePublisher(Node):
    def __init__(self):
        super().__init__('distance_pub')
        # Topic 'distance', queue depth 10
        self.pub = self.create_publisher(Float32, 'distance', 10)
        self.t = 0.0
        self.timer = self.create_timer(0.1, self.tick)  # 10 Hz

    def tick(self):
        # Simulated obstacle moving between 0.1 m and 1.1 m.
        # Replace this with a real sensor reading later.
        self.t += 0.1
        msg = Float32()
        msg.data = 0.6 + 0.5 * math.sin(self.t * 0.5)
        self.pub.publish(msg)


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


if __name__ == '__main__':
    main()

Node 2: safety stop

robot_basics/safety_stop.py: subscribes to distance and publishes cmd_vel
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
from geometry_msgs.msg import Twist


class SafetyStop(Node):
    def __init__(self):
        super().__init__('safety_stop')
        # Parameters can be changed at launch: --ros-args -p stop_distance:=0.3
        self.declare_parameter('stop_distance', 0.25)
        self.declare_parameter('cruise_speed', 0.15)
        self.sub = self.create_subscription(Float32, 'distance', self.on_distance, 10)
        self.pub = self.create_publisher(Twist, 'cmd_vel', 10)

    def on_distance(self, msg):
        stop_d = self.get_parameter('stop_distance').value
        speed = self.get_parameter('cruise_speed').value
        cmd = Twist()                     # all fields default to zero
        if msg.data > stop_d:
            cmd.linear.x = speed          # forward speed in m/s
        else:
            cmd.angular.z = 0.8           # obstacle: rotate on the spot (rad/s)
            self.get_logger().info(f'Obstacle at {msg.data:.2f} m, turning')
        self.pub.publish(cmd)


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


if __name__ == '__main__':
    main()

Register both nodes as executables by editing the entry_points section in the package's setup.py:

setup.py entry_points section
entry_points={
    'console_scripts': [
        'distance_pub = robot_basics.distance_pub:main',
        'safety_stop = robot_basics.safety_stop:main',
    ],
},

Building, Running and Inspecting

Build the workspace, start both nodes and inspect the topics
cd ~/ros2_ws
colcon build --packages-select robot_basics
source install/setup.bash

# Terminal 1
ros2 run robot_basics distance_pub

# Terminal 2 (source install/setup.bash first)
ros2 run robot_basics safety_stop --ros-args -p stop_distance:=0.3

# Terminal 3: inspect the running system
ros2 node list
ros2 topic list
ros2 topic hz /distance
ros2 topic echo /cmd_vel
ros2 topic info /cmd_vel --verbose

ros2 topic hz should report about 10 Hz, and ros2 topic echo /cmd_vel shows linear.x at 0.15 until the simulated distance drops below the threshold, when angular.z becomes 0.8. In a real robot, a base controller node would subscribe to /cmd_vel, apply the inverse kinematics vR = v + omega L / 2 and vL = v - omega L / 2, and send wheel speeds to an Arduino or ESP32 over a serial link.

Why the queue depth matters

The number 10 passed when creating each publisher and subscription is the history depth of the default quality-of-service profile: how many messages are buffered if a subscriber cannot keep up. For control topics such as /cmd_vel, only the newest command matters, so a small depth is appropriate. For sensor streams on a busy Pi, a slow subscriber may drop old messages, which is usually what you want for real-time control rather than acting on stale data.

Troubleshooting

  • ros2: command not found: the setup file was not sourced in this terminal.
  • No executable found: the entry point is missing from setup.py, or you did not source install/setup.bash after building.
  • Nodes on two computers cannot see each other: check both use the same ROS 2 release and ROS_DOMAIN_ID, are on the same network, and that the router allows multicast.
  • Build is very slow or the Pi freezes: limit parallel jobs with colcon build --parallel-workers 1 and use a swap file on 4 GB boards.

Summary

ROS 2 organises robot software into nodes connected by typed topics. On a Raspberry Pi running Ubuntu 24.04 with ROS 2 Jazzy, you can create an ament_python package, write publisher and subscriber nodes with rclpy, register them in setup.py, build with colcon and inspect everything with the ros2 command-line tools. The /cmd_vel Twist topic is the standard bridge between high-level planning and your differential drive base.