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:
1import rclpy2from rclpy.node import Node34class MinimalNode(Node):5 def __init__(self):6 super().__init__('minimal_node') # Node name7 self.get_logger().info('Minimal node has been started')89def main(args=None):10 rclpy.init(args=args) # Initialize ROS 2 communication11 node = MinimalNode()12 rclpy.spin(node) # Keep node alive13 node.destroy_node()14 rclpy.shutdown() # Cleanup1516if __name__ == '__main__':17 main()
Key Components Explained:
rclpy.init(): Initializes the ROS 2 client library. Must be called before creating nodes.Nodeclass: Base class providing all ROS 2 functionality. Pass a unique node name tosuper().__init__().rclpy.spin(): Blocks and processes callbacks (subscriptions, timers, services). Without this, your node exits immediately.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.
1import rclpy2from rclpy.node import Node3from std_msgs.msg import Float3245class TemperaturePublisher(Node):6 def __init__(self):7 super().__init__('temperature_publisher')89 # Create publisher: message type Float32, topic '/temperature', queue size 1010 self.publisher_ = self.create_publisher(Float32, 'temperature', 10)1112 # Create timer: call timer_callback every 1.0 seconds13 self.timer = self.create_timer(1.0, self.timer_callback)1415 self.temperature = 20.0 # Simulated temperature in Celsius16 self.get_logger().info('Temperature Publisher started')1718 def timer_callback(self):19 msg = Float32()20 msg.data = self.temperature2122 self.publisher_.publish(msg)23 self.get_logger().info(f'Publishing temperature: {msg.data:.2f}°C')2425 # Simulate temperature fluctuation26 self.temperature += 0.52728def main(args=None):29 rclpy.init(args=args)30 node = TemperaturePublisher()31 rclpy.spin(node)32 node.destroy_node()33 rclpy.shutdown()3435if __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) orQoSProfileobject (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.
1import rclpy2from rclpy.node import Node3from std_msgs.msg import Float3245class TemperatureSubscriber(Node):6 def __init__(self):7 super().__init__('temperature_subscriber')89 # Create subscriber: message type Float32, topic '/temperature', callback function10 self.subscription = self.create_subscription(11 Float32,12 'temperature',13 self.listener_callback,14 10 # Queue size15 )1617 self.get_logger().info('Temperature Subscriber started')1819 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')2223 # Add temperature warning logic24 if msg.data > 30.0:25 self.get_logger().warn(f'HIGH TEMPERATURE ALERT: {msg.data:.2f}°C')2627def main(args=None):28 rclpy.init(args=args)29 node = TemperatureSubscriber()30 rclpy.spin(node)31 node.destroy_node()32 rclpy.shutdown()3334if __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:
| Policy | Options | Use Case |
|---|---|---|
| Reliability | RELIABLE, BEST_EFFORT | Reliable: retransmit lost packets (e.g., commands). Best-effort: no retries (e.g., high-frequency sensor data) |
| Durability | TRANSIENT_LOCAL, VOLATILE | Transient: new subscribers get last message. Volatile: only get messages after subscription |
| History | KEEP_LAST(n), KEEP_ALL | Keep last N messages or all messages in queue |
| Deadline | Time duration | Expect messages within deadline (e.g., 100ms). Trigger callback if missed |
| Lifespan | Time duration | Messages expire after lifespan (e.g., 1 second old commands ignored) |
2.3.2 Using QoS Profiles
1import rclpy2from rclpy.node import Node3from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy, DurabilityPolicy4from std_msgs.msg import String56class QoSExampleNode(Node):7 def __init__(self):8 super().__init__('qos_example_node')910 # Create custom QoS profile for sensor data11 sensor_qos = QoSProfile(12 reliability=ReliabilityPolicy.BEST_EFFORT, # Don't retransmit13 history=HistoryPolicy.KEEP_LAST, # Keep only latest message14 depth=1, # Queue size 115 durability=DurabilityPolicy.VOLATILE # Don't persist for late subscribers16 )1718 # Create custom QoS profile for commands (reliable delivery)19 command_qos = QoSProfile(20 reliability=ReliabilityPolicy.RELIABLE, # Retransmit until ACK21 history=HistoryPolicy.KEEP_LAST,22 depth=10, # Buffer up to 10 commands23 durability=DurabilityPolicy.TRANSIENT_LOCAL # Late subscribers get last message24 )2526 # Publishers with different QoS27 self.sensor_pub = self.create_publisher(String, 'sensor_data', sensor_qos)28 self.command_pub = self.create_publisher(String, 'commands', command_qos)2930 self.get_logger().info('QoS Example Node started')3132def main(args=None):33 rclpy.init(args=args)34 node = QoSExampleNode()35 rclpy.spin(node)36 node.destroy_node()37 rclpy.shutdown()3839if __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, volatileqos_profile_system_default: Reliable, keep last 10, volatileqos_profile_services_default: Reliable, keep last 10, volatileqos_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 nodesros2 node list# Get detailed info about a noderos2 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 topicsros2 topic list# Get topic typeros2 topic type /temperature# Output: std_msgs/msg/Float32# Get detailed topic info (publishers, subscribers, QoS)ros2 topic info /temperature --verbose# Echo messages in real-timeros2 topic echo /temperature# Measure publishing frequencyros2 topic hz /temperature# Publish a message from command lineros2 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
Temperature Monitoring System
Learning Objectives
Problem Statement
You are building a temperature monitoring system for a robotics lab. The system has:
- Sensor Node: Simulates a temperature sensor that publishes readings every 0.5 seconds
- Monitor Node: Subscribes to temperature data and logs warnings for extreme values
Requirements
Temperature Sensor Node (temperature_sensor.py):
- Publish to
/lab/temperaturetopic (type:std_msgs/msg/Float32) - Initial temperature: 22.0°C
- Add random fluctuation:
random.uniform(-1.0, 1.0)degrees per reading - Log each published temperature
- Use QoS: Reliable, Keep Last 10
Temperature Monitor Node (temperature_monitor.py):
- Subscribe to
/lab/temperaturetopic - Log all received temperatures
- Warn if temperature > 30°C: "HIGH TEMPERATURE ALERT"
- Warn if temperature < 10°C: "LOW TEMPERATURE ALERT"
- Use QoS: Reliable, Keep Last 10
Testing
# Terminal 1: Run sensor
python3 temperature_sensor.py
# Terminal 2: Run monitor
python3 temperature_monitor.py
# Terminal 3: Inspect system
ros2 node list
ros2 topic list
ros2 topic info /lab/temperature
ros2 topic echo /lab/temperature
ros2 topic hz /lab/temperature
# Expected: ~2 Hz (0.5 second period)
Deliverable
- Two working Python files:
temperature_sensor.pyandtemperature_monitor.py - Screenshot of both nodes running with temperature warnings appearing
- Answer: What happens when you kill the sensor node? (Monitor stops receiving messages, but doesn't crash)
- Answer: What happens when you change sensor to
BEST_EFFORTand monitor toRELIABLE? (No communication - QoS mismatch)
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/infofor inspecting running systems - Node lifecycle follows init → spin → shutdown with
rclpy.init(),rclpy.spin(), andrclpy.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 echoandros2 topic hzto debug communication
Next: Chapter 3: Python and rclpy Deep Dive (coming soon)