Skip to main content

Chapter 2: ROS 2 Nodes and Topics

Welcome to hands-on ROS 2 programming! In Chapter 1, you learned the conceptual foundations—nodes, topics, services, and the computational graph. Now you'll write Python code using rclpy (ROS Client Library for Python) to create nodes that communicate via topics.

By the end of this chapter, you'll build a temperature monitoring system with a sensor node publishing data and a monitor node subscribing to it—your first multi-node ROS 2 application.

2.1 Understanding Nodes

Every ROS 2 application starts with nodes. A node is a process that performs computation—reading sensors, processing data, controlling motors, etc.

2.1.1 Basic Node Structure

Every rclpy node follows this pattern:

minimal_node.py
Beginner
1import rclpy
2from rclpy.node import Node
3
4class MinimalNode(Node):
5 def __init__(self):
6 super().__init__('minimal_node') # Node name
7 self.get_logger().info('Minimal node has been started')
8
9def main(args=None):
10 rclpy.init(args=args) # Initialize ROS 2 communication
11 node = MinimalNode()
12 rclpy.spin(node) # Keep node alive
13 node.destroy_node()
14 rclpy.shutdown() # Cleanup
15
16if __name__ == '__main__':
17 main()

Key Components Explained:

  1. rclpy.init(): Initializes the ROS 2 client library. Must be called before creating nodes.
  2. Node class: Base class providing all ROS 2 functionality. Pass a unique node name to super().__init__().
  3. rclpy.spin(): Blocks and processes callbacks (subscriptions, timers, services). Without this, your node exits immediately.
  4. destroy_node() / shutdown(): Clean up resources when the node exits.

2.1.2 Node Lifecycle

stateDiagram-v2
[*] --> Unconfigured: rclpy.init()
Unconfigured --> Active: Node.__init__()
Active --> Active: rclpy.spin() (processing callbacks)
Active --> Shutdown: Ctrl+C / destroy_node()
Shutdown --> [*]: rclpy.shutdown()

Lifecycle States:

  • Unconfigured: ROS 2 initialized, but no nodes created yet
  • Active: Node running, processing callbacks (subscriptions, timers, services)
  • Shutdown: Node destroyed, resources released

2.2 Publisher/Subscriber Pattern

The publish-subscribe pattern is the most common ROS 2 communication method for streaming data (sensor readings, robot state, commands).

2.2.1 Creating a Publisher

A publisher sends messages to a topic at regular intervals.

temperature_publisher.py
Beginner⏱ ~15 min
1import rclpy
2from rclpy.node import Node
3from std_msgs.msg import Float32
4
5class TemperaturePublisher(Node):
6 def __init__(self):
7 super().__init__('temperature_publisher')
8
9 # Create publisher: message type Float32, topic '/temperature', queue size 10
10 self.publisher_ = self.create_publisher(Float32, 'temperature', 10)
11
12 # Create timer: call timer_callback every 1.0 seconds
13 self.timer = self.create_timer(1.0, self.timer_callback)
14
15 self.temperature = 20.0 # Simulated temperature in Celsius
16 self.get_logger().info('Temperature Publisher started')
17
18 def timer_callback(self):
19 msg = Float32()
20 msg.data = self.temperature
21
22 self.publisher_.publish(msg)
23 self.get_logger().info(f'Publishing temperature: {msg.data:.2f}°C')
24
25 # Simulate temperature fluctuation
26 self.temperature += 0.5
27
28def main(args=None):
29 rclpy.init(args=args)
30 node = TemperaturePublisher()
31 rclpy.spin(node)
32 node.destroy_node()
33 rclpy.shutdown()
34
35if __name__ == '__main__':
36 main()

Publisher API Breakdown:

self.publisher_ = self.create_publisher(
msg_type=Float32, # Message type (from std_msgs.msg)
topic='temperature', # Topic name
qos_profile=10 # Queue size (or QoSProfile object)
)
  • msg_type: The message type (e.g., Float32, String, Twist). Subscribers must use the same type.
  • topic: Topic name (string). Conventionally uses lowercase with underscores.
  • qos_profile: Integer (queue size) or QoSProfile object (covered in Section 2.3).

Timer for Periodic Publishing:

self.timer = self.create_timer(
timer_period_sec=1.0, # Publish every 1 second
callback=self.timer_callback # Function to call
)

Timers are essential for periodic tasks like sensor sampling or control loops.

2.2.2 Creating a Subscriber

A subscriber receives messages from a topic and processes them in a callback function.

temperature_subscriber.py
Beginner⏱ ~15 min
1import rclpy
2from rclpy.node import Node
3from std_msgs.msg import Float32
4
5class TemperatureSubscriber(Node):
6 def __init__(self):
7 super().__init__('temperature_subscriber')
8
9 # Create subscriber: message type Float32, topic '/temperature', callback function
10 self.subscription = self.create_subscription(
11 Float32,
12 'temperature',
13 self.listener_callback,
14 10 # Queue size
15 )
16
17 self.get_logger().info('Temperature Subscriber started')
18
19 def listener_callback(self, msg):
20 """Called every time a message is received"""
21 self.get_logger().info(f'Received temperature: {msg.data:.2f}°C')
22
23 # Add temperature warning logic
24 if msg.data > 30.0:
25 self.get_logger().warn(f'HIGH TEMPERATURE ALERT: {msg.data:.2f}°C')
26
27def main(args=None):
28 rclpy.init(args=args)
29 node = TemperatureSubscriber()
30 rclpy.spin(node)
31 node.destroy_node()
32 rclpy.shutdown()
33
34if __name__ == '__main__':
35 main()

Subscriber API Breakdown:

self.subscription = self.create_subscription(
msg_type=Float32, # Must match publisher's message type
topic='temperature', # Must match publisher's topic name
callback=self.listener_callback, # Function to call when message arrives
qos_profile=10 # Queue size (or QoSProfile object)
)

Callback Function Signature:

def listener_callback(self, msg):
# msg is an instance of Float32 (or whatever msg_type you specified)
temperature_value = msg.data
# Process the message...

Callbacks execute asynchronously whenever a message arrives. Keep them short (< 10ms) to avoid blocking other callbacks.

2.2.3 Communication Flow

sequenceDiagram
participant Timer
participant Publisher as Temperature<br/>Publisher
participant Topic as /temperature<br/>(DDS Topic)
participant Subscriber as Temperature<br/>Subscriber

loop Every 1 second
Timer->>Publisher: timer_callback()
Publisher->>Publisher: Create Float32 msg
Publisher->>Topic: publish(msg)
Topic->>Subscriber: listener_callback(msg)
Subscriber->>Subscriber: Log temperature
Subscriber->>Subscriber: Check threshold
end

Note over Publisher,Subscriber: Asynchronous, non-blocking

Running the Example:

Open two terminals and source ROS 2:

# Terminal 1: Run publisher
source /opt/ros/humble/setup.bash
python3 temperature_publisher.py

# Terminal 2: Run subscriber
source /opt/ros/humble/setup.bash
python3 temperature_subscriber.py

You'll see the publisher logging temperatures and the subscriber receiving them in real-time.

2.3 Quality of Service (QoS)

Quality of Service (QoS) policies control how messages are delivered between publishers and subscribers. This is critical for robotics where some data (sensor readings) can be dropped, but other data (safety commands) must be guaranteed.

2.3.1 QoS Policies

ROS 2 uses DDS QoS policies. The most important ones are:

PolicyOptionsUse Case
ReliabilityRELIABLE, BEST_EFFORTReliable: retransmit lost packets (e.g., commands). Best-effort: no retries (e.g., high-frequency sensor data)
DurabilityTRANSIENT_LOCAL, VOLATILETransient: new subscribers get last message. Volatile: only get messages after subscription
HistoryKEEP_LAST(n), KEEP_ALLKeep last N messages or all messages in queue
DeadlineTime durationExpect messages within deadline (e.g., 100ms). Trigger callback if missed
LifespanTime durationMessages expire after lifespan (e.g., 1 second old commands ignored)

2.3.2 Using QoS Profiles

qos_example.py
Intermediate
1import rclpy
2from rclpy.node import Node
3from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy
4from std_msgs.msg import String
5
6class QoSExampleNode(Node):
7 def __init__(self):
8 super().__init__('qos_example_node')
9
10 # Create custom QoS profile for sensor data
11 sensor_qos = QoSProfile(
12 reliability=ReliabilityPolicy.BEST_EFFORT, # Don't retransmit
13 history=HistoryPolicy.KEEP_LAST, # Keep only latest message
14 depth=1, # Queue size 1
15 durability=DurabilityPolicy.VOLATILE # Don't persist for late subscribers
16 )
17
18 # Create custom QoS profile for commands (reliable delivery)
19 command_qos = QoSProfile(
20 reliability=ReliabilityPolicy.RELIABLE, # Retransmit until ACK
21 history=HistoryPolicy.KEEP_LAST,
22 depth=10, # Buffer up to 10 commands
23 durability=DurabilityPolicy.TRANSIENT_LOCAL # Late subscribers get last message
24 )
25
26 # Publishers with different QoS
27 self.sensor_pub = self.create_publisher(String, 'sensor_data', sensor_qos)
28 self.command_pub = self.create_publisher(String, 'commands', command_qos)
29
30 self.get_logger().info('QoS Example Node started')
31
32def main(args=None):
33 rclpy.init(args=args)
34 node = QoSExampleNode()
35 rclpy.spin(node)
36 node.destroy_node()
37 rclpy.shutdown()
38
39if __name__ == '__main__':
40 main()

2.3.3 Predefined QoS Profiles

ROS 2 provides common presets:

from rclpy.qos import qos_profile_sensor_data, qos_profile_system_default
# Use sensor data preset (best-effort, keep last 1)
self.sensor_pub = self.create_publisher(
Image,
'camera/image',
qos_profile_sensor_data
)
# Use system default preset (reliable, keep last 10)
self.command_pub = self.create_publisher(
Twist,
'cmd_vel',
qos_profile_system_default
)

Common Presets:

  • qos_profile_sensor_data: Best-effort, keep last 5, volatile
  • qos_profile_system_default: Reliable, keep last 10, volatile
  • qos_profile_services_default: Reliable, keep last 10, volatile
  • qos_profile_parameters: Reliable, keep last 1000, transient local

2.4 Introspection Tools

ROS 2 provides powerful CLI tools to inspect running systems without modifying code.

2.4.1 Node Introspection

# List all active nodes
ros2 node list
# Get detailed info about a node
ros2 node info /temperature_publisher
# Output:
# /temperature_publisher
# Subscribers:
# /parameter_events: rcl_interfaces/msg/ParameterEvent
# Publishers:
# /temperature: std_msgs/msg/Float32
# /rosout: rcl_interfaces/msg/Log
# Service Servers:
# /temperature_publisher/describe_parameters: rcl_interfaces/srv/DescribeParameters
# Service Clients:
# Action Servers:
# Action Clients:

2.4.2 Topic Introspection

# List all active topics
ros2 topic list
# Get topic type
ros2 topic type /temperature
# Output: std_msgs/msg/Float32
# Get detailed topic info (publishers, subscribers, QoS)
ros2 topic info /temperature --verbose
# Echo messages in real-time
ros2 topic echo /temperature
# Measure publishing frequency
ros2 topic hz /temperature
# Publish a message from command line
ros2 topic pub /temperature std_msgs/msg/Float32 "{data: 25.5}"

2.4.3 Debugging Communication Issues

2.5 Hands-On Exercise: Build a Temperature Monitor

Key Takeaways

✓ Core Concepts Mastered

  • rclpy provides the Python API for ROS 2 with the Node class as the foundation for all node implementations
  • Publishers send messages to topics using create_publisher() and timers for periodic publishing
  • Subscribers receive messages from topics using create_subscription() with callback functions for asynchronous processing
  • Quality of Service (QoS) policies control message delivery with reliability (reliable vs. best-effort), durability (volatile vs. transient), and history (keep last N) settings
  • QoS compatibility matters: Publishers and subscribers must have compatible QoS profiles (e.g., BEST_EFFORT publisher cannot connect to RELIABLE subscriber)
  • ROS 2 CLI tools enable debugging with ros2 node list/info, ros2 topic list/echo/hz/info for inspecting running systems
  • Node lifecycle follows init → spin → shutdown with rclpy.init(), rclpy.spin(), and rclpy.shutdown()

What's Next?

In Chapter 3: Python and rclpy Deep Dive, you'll learn advanced rclpy patterns:

  • Service clients and servers for request-response communication
  • Action servers for long-running tasks with feedback
  • Parameters for runtime configuration
  • Callback groups for managing concurrent execution
  • Integration patterns for AI agents (bridging AI logic with ROS nodes)

Prerequisite Check: Before proceeding, ensure you can:

  • ✅ Create a publisher node that sends messages at a fixed rate
  • ✅ Create a subscriber node that processes messages in a callback
  • ✅ Explain the difference between RELIABLE and BEST_EFFORT QoS
  • ✅ Use ros2 topic echo and ros2 topic hz to debug communication

Next: Chapter 3: Python and rclpy Deep Dive (coming soon)