Skip to main content

Chapter 1: ROS 2 Basics

Introduction

The Robot Operating System 2 (ROS 2) is the backbone of modern robotics development, providing a flexible framework for writing robot software. Think of ROS 2 as the nervous system of a robot—it enables different components to communicate seamlessly, coordinate actions, and share sensor data in real-time. For humanoid robots, this is particularly crucial: a walking robot needs its balance controller to instantly access IMU data, its vision system to communicate with navigation, and its arm controllers to coordinate with grasp planning—all happening simultaneously.

ROS 2 represents a complete redesign from its predecessor, ROS 1. While ROS 1 revolutionized academic robotics, it had critical limitations for production systems: it required a central master node (a single point of failure), lacked real-time capabilities, and had poor security. ROS 2 addresses these issues with a decentralized architecture using DDS (Data Distribution Service), built-in security features, and real-time support. This makes ROS 2 suitable not just for research labs, but for commercial humanoid robots operating in unpredictable real-world environments.

In the humanoid robotics domain, ROS 2 powers everything from research platforms like NASA's Valkyrie to commercial robots being deployed in warehouses and hospitals. Its modular architecture allows teams to develop components independently—one engineer can work on the walking controller while another develops the manipulation system—and integrate them seamlessly. This chapter will teach you the fundamental building blocks of ROS 2 that make this coordination possible.

What is a ROS 2 Node?

A node is the fundamental computing unit in ROS 2. Each node is a process that performs a specific task: one node might read camera data, another processes images to detect objects, and a third plans navigation routes. This separation of concerns makes robot systems modular, testable, and maintainable. If your vision node crashes, your motor controllers keep running; if you want to swap out your navigation algorithm, you replace one node without touching the rest.

Every node has a unique name and can communicate with other nodes through various mechanisms (which we'll explore in the next section). Nodes can be written in Python, C++, or other supported languages. Here's a minimal ROS 2 node in Python:

import rclpy
from rclpy.node import Node

class MinimalNode(Node):
def __init__(self):
super().__init__('minimal_node')
self.get_logger().info('Minimal node has started!')

# Create a timer that calls a function every 1 second
self.timer = self.create_timer(1.0, self.timer_callback)
self.counter = 0

def timer_callback(self):
self.counter += 1
self.get_logger().info(f'Timer callback #{self.counter}')

def main(args=None):
rclpy.init(args=args)
node = MinimalNode()

try:
rclpy.spin(node) # Keep the node running
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

This node inherits from Node and uses rclpy.spin() to keep it alive, processing callbacks. The timer demonstrates how nodes handle periodic tasks—essential for control loops in robotics. Real humanoid controllers often run at specific frequencies (e.g., 100Hz for balance control, 30Hz for vision processing).

Communication Patterns

ROS 2 provides three primary communication mechanisms, each suited for different use cases:

Publishers and Subscribers (Pub/Sub)

The publish-subscribe pattern is ideal for streaming sensor data or continuous state information. A publisher sends messages to a topic (a named channel), and any number of subscribers can listen to that topic. This is a one-to-many, fire-and-forget pattern—publishers don't know or care who's listening.

Example: Publishing IMU data

from rclpy.node import Node
from sensor_msgs.msg import Imu

class ImuPublisher(Node):
def __init__(self):
super().__init__('imu_publisher')
self.publisher = self.create_publisher(Imu, '/imu/data', 10)
self.timer = self.create_timer(0.01, self.publish_imu) # 100 Hz

def publish_imu(self):
msg = Imu()
msg.header.stamp = self.get_clock().now().to_msg()
msg.header.frame_id = 'imu_link'
# In a real system, read from actual IMU hardware
msg.linear_acceleration.z = 9.81
self.publisher.publish(msg)

Use cases: Sensor streams (cameras, lidars, IMUs), robot state updates, telemetry.

Services (Request/Response)

Services implement synchronous request-response communication. A client sends a request and blocks until receiving a response. This is perfect for one-off queries or commands that need confirmation.

from example_interfaces.srv import AddTwoInts

class MathService(Node):
def __init__(self):
super().__init__('math_service')
self.srv = self.create_service(
AddTwoInts,
'add_two_ints',
self.add_callback
)

def add_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'{request.a} + {request.b} = {response.sum}')
return response

Use cases: Configuration queries, triggering calibration, requesting path plans, setting control modes.

Actions (Long-Running Tasks)

Actions are for tasks that take time and need feedback. Unlike services, actions are asynchronous, provide progress updates, and can be canceled. Think of actions as "services with feedback"—perfect for navigation, grasping, or other goal-oriented behaviors.

from rclpy.action import ActionServer
from example_interfaces.action import Fibonacci

class FibonacciActionServer(Node):
def __init__(self):
super().__init__('fibonacci_action_server')
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback
)

def execute_callback(self, goal_handle):
feedback_msg = Fibonacci.Feedback()
sequence = [0, 1]

for i in range(1, goal_handle.request.order):
sequence.append(sequence[i] + sequence[i-1])
feedback_msg.sequence = sequence
goal_handle.publish_feedback(feedback_msg) # Send progress

goal_handle.succeed()
result = Fibonacci.Result()
result.sequence = sequence
return result

Use cases: Navigation to goal, pick-and-place sequences, multi-step calibration procedures.

Creating Your First ROS 2 Package

ROS 2 organizes code into packages—collections of related nodes, libraries, and configuration files. Here's how to create a Python package:

# Navigate to your workspace src directory
cd ~/ros2_ws/src

# Create a package named 'my_robot_controller'
ros2 pkg create my_robot_controller \
--build-type ament_python \
--dependencies rclpy std_msgs sensor_msgs

# This creates the following structure:
# my_robot_controller/
# ├── package.xml # Package metadata
# ├── setup.py # Python package configuration
# ├── setup.cfg
# ├── my_robot_controller/ # Python module
# │ └── __init__.py
# └── resource/

The key file is setup.py, which defines your package's entry points (executable nodes):

from setuptools import setup

package_name = 'my_robot_controller'

setup(
name=package_name,
version='0.0.1',
packages=[package_name],
install_requires=['setuptools'],
zip_safe=True,
entry_points={
'console_scripts': [
'sensor_node = my_robot_controller.sensor_publisher:main',
'command_node = my_robot_controller.command_subscriber:main',
],
},
)

After adding your Python scripts to my_robot_controller/, build your workspace:

cd ~/ros2_ws
colcon build --packages-select my_robot_controller
source install/setup.bash # Load the newly built package

colcon is ROS 2's build tool, which handles dependency resolution and parallel builds across multiple packages.

Running and Inspecting ROS 2 Systems

ROS 2 provides powerful command-line tools for introspection. Here are the essential commands:

Running nodes:

ros2 run my_robot_controller sensor_node

Listing active nodes:

ros2 node list
# Output: /sensor_publisher

Inspecting a node's details:

ros2 node info /sensor_publisher
# Shows publishers, subscribers, services, and actions

Listing all topics:

ros2 topic list
# Output: /sensor/data, /robot/cmd_vel, etc.

Viewing messages on a topic in real-time:

ros2 topic echo /sensor/data

Checking message structure:

ros2 interface show sensor_msgs/msg/Imu
# Shows all fields in the Imu message type

Publishing from command line (useful for testing):

ros2 topic pub /cmd_vel geometry_msgs/msg/Twist \
"linear: {x: 0.5, y: 0.0, z: 0.0}"

These tools are invaluable for debugging. If your robot isn't responding to commands, you can check if the topic exists, verify message formats, and manually publish test commands—all without modifying code.

Hands-On Exercise: Sensor Publisher and Command Subscriber

Let's build a practical example: a sensor node that publishes temperature readings, and a command node that subscribes to control commands. This mirrors real humanoid systems where sensors stream data and controllers respond to commands.

Step 1: Create the Sensor Publisher

Create my_robot_controller/sensor_publisher.py:

import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
import random

class SensorPublisher(Node):
def __init__(self):
super().__init__('sensor_publisher')
self.publisher = self.create_publisher(Float32, '/sensor/temperature', 10)
self.timer = self.create_timer(1.0, self.publish_sensor_data)
self.get_logger().info('Sensor publisher started!')

def publish_sensor_data(self):
msg = Float32()
# Simulate temperature sensor (20-25°C with noise)
msg.data = 22.5 + random.uniform(-2.5, 2.5)
self.publisher.publish(msg)
self.get_logger().info(f'Published temperature: {msg.data:.2f}°C')

def main(args=None):
rclpy.init(args=args)
node = SensorPublisher()

try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Step 2: Create the Command Subscriber

Create my_robot_controller/command_subscriber.py:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class CommandSubscriber(Node):
def __init__(self):
super().__init__('command_subscriber')
self.subscription = self.create_subscription(
String,
'/robot/command',
self.command_callback,
10
)
self.get_logger().info('Command subscriber started! Waiting for commands...')

def command_callback(self, msg):
command = msg.data
self.get_logger().info(f'Received command: {command}')

# Process commands
if command == 'stand':
self.get_logger().info('Executing: Standing up')
elif command == 'walk':
self.get_logger().info('Executing: Walking forward')
elif command == 'stop':
self.get_logger().info('Executing: Stopping all motors')
else:
self.get_logger().warn(f'Unknown command: {command}')

def main(args=None):
rclpy.init(args=args)
node = CommandSubscriber()

try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Step 3: Run the Exercise

Terminal 1 - Start the sensor publisher:

source ~/ros2_ws/install/setup.bash
ros2 run my_robot_controller sensor_node

Expected output:

[INFO] [sensor_publisher]: Sensor publisher started!
[INFO] [sensor_publisher]: Published temperature: 24.32°C
[INFO] [sensor_publisher]: Published temperature: 21.87°C
[INFO] [sensor_publisher]: Published temperature: 23.45°C

Terminal 2 - Monitor the sensor data:

ros2 topic echo /sensor/temperature

Terminal 3 - Start the command subscriber:

ros2 run my_robot_controller command_node

Terminal 4 - Send commands:

# Send a walk command
ros2 topic pub --once /robot/command std_msgs/msg/String "data: 'walk'"

# Send a stop command
ros2 topic pub --once /robot/command std_msgs/msg/String "data: 'stop'"

Expected behavior:

  • Terminal 1 continuously publishes sensor readings
  • Terminal 2 shows live sensor data stream
  • Terminal 3 logs each received command and executes corresponding actions
  • Terminal 4 allows you to send commands interactively

This exercise demonstrates the fundamental ROS 2 workflow: nodes communicate via topics, publishers send data continuously, and subscribers react to messages. This same pattern scales to complex humanoid systems with dozens of sensors and actuators.

Next Steps

You've now mastered ROS 2's foundational concepts: nodes, topics, and basic communication patterns. In Chapter 2, you'll learn how to integrate Python AI agents with ROS 2 controllers using rclpy, bridging high-level decision-making with low-level motor control. You'll also explore URDF (Unified Robot Description Format) to define humanoid robot structures, and discover how ROS 2 services and actions enable sophisticated behaviors like motion planning and manipulation.

Practice the hands-on exercise until you're comfortable running multiple nodes and inspecting their communication. Try modifying the sensor publisher to send different message types (e.g., sensor_msgs/msg/Imu for accelerometer data) or adding multiple subscribers to the same topic. The more you experiment, the more intuitive ROS 2's architecture becomes.