Skip to main content

2.2 Nodes, Topics, Services, and Actions

Learning Objectives

  • Understand what ROS 2 nodes are and how they form the computational graph
  • Learn how to implement publishers and subscribers for topic-based communication
  • Master service-based request-response patterns
  • Implement action servers and clients for long-running tasks
  • Choose the appropriate communication pattern for different robotics scenarios

Understanding ROS 2 Nodes

A node is an autonomous process that performs a specific computational task within a ROS 2 system. Nodes are the building blocks of robotic applications, each designed to handle a well-defined responsibility.

Node Design Philosophy

The ROS 2 philosophy encourages creating many small, focused nodes rather than monolithic applications. This approach offers several advantages:

Modularity: Each node can be developed, tested, and debugged independently Reusability: Well-designed nodes can be used across different robots and projects Fault Isolation: A crash in one node doesn't necessarily bring down the entire system Language Flexibility: Nodes can be written in different languages (Python, C++) and still communicate Parallel Development: Multiple developers can work on different nodes simultaneously

Example Node Responsibilities

In a humanoid robot system, you might have nodes for:

  • IMU sensor driver (publishes orientation data)
  • Camera driver (publishes image streams)
  • Object detection (processes images, publishes detected objects)
  • Trajectory planner (computes walking paths)
  • Balance controller (maintains stability)
  • Joint state publisher (reports current joint angles)
  • Visualizer (displays robot state in RViz)

Each node focuses on one aspect of the robot's functionality, communicating with other nodes through well-defined interfaces.

Topics: Publish-Subscribe Communication

Topics are the primary mechanism for continuous data streaming in ROS 2. They implement a publish-subscribe pattern where data producers (publishers) and data consumers (subscribers) are decoupled.

How Topics Work

  1. A publisher announces it will publish messages on a specific topic (e.g., /camera/image)
  2. Subscribers express interest in receiving messages from that topic
  3. Through DDS discovery, publishers and subscribers find each other
  4. When the publisher sends a message, all subscribers receive it
  5. Communication is asynchronous—publishers don't wait for subscribers

Message Types

Every topic has an associated message type that defines the data structure. ROS 2 provides standard message types:

Common Message Types:

  • std_msgs/String: Simple text messages
  • std_msgs/Int32: Integer values
  • sensor_msgs/Image: Camera images
  • sensor_msgs/LaserScan: LiDAR data
  • sensor_msgs/Imu: Inertial measurement unit data
  • geometry_msgs/Twist: Velocity commands (linear and angular)
  • geometry_msgs/Pose: Position and orientation

You can also create custom message types for your specific needs.

Creating a Publisher

Here's a complete example of a publisher node in Python:

"""
Simple publisher that sends greeting messages periodically
"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class GreetingPublisher(Node):
"""
A node that publishes greeting messages every second
"""
def __init__(self):
# Initialize the node with a name
super().__init__('greeting_publisher')

# Create a publisher
# - Message type: String
# - Topic name: 'greetings'
# - Queue size: 10 (keeps last 10 messages if subscriber is slow)
self.publisher_ = self.create_publisher(String, 'greetings', 10)

# Create a timer that calls our callback every 1.0 seconds
self.timer = self.create_timer(1.0, self.timer_callback)

# Counter for message numbering
self.count = 0

self.get_logger().info('Greeting Publisher started!')

def timer_callback(self):
"""
Called every second by the timer
Creates and publishes a greeting message
"""
msg = String()
msg.data = f'Hello ROS 2! Message #{self.count}'

# Publish the message
self.publisher_.publish(msg)

# Log what we published
self.get_logger().info(f'Publishing: "{msg.data}"')

self.count += 1

def main(args=None):
"""
Main function to initialize and run the node
"""
# Initialize ROS 2 Python client library
rclpy.init(args=args)

# Create our node
node = GreetingPublisher()

try:
# Keep the node running and processing callbacks
rclpy.spin(node)
except KeyboardInterrupt:
# Handle Ctrl+C gracefully
pass
finally:
# Cleanup
node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Creating a Subscriber

Here's the corresponding subscriber node:

"""
Simple subscriber that receives and displays greeting messages
"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class GreetingSubscriber(Node):
"""
A node that subscribes to greeting messages and prints them
"""
def __init__(self):
super().__init__('greeting_subscriber')

# Create a subscription
# - Message type: String
# - Topic name: 'greetings' (must match publisher's topic)
# - Callback function: listener_callback
# - Queue size: 10
self.subscription = self.create_subscription(
String,
'greetings',
self.listener_callback,
10
)

self.get_logger().info('Greeting Subscriber started!')

def listener_callback(self, msg):
"""
Called automatically whenever a message arrives

Args:
msg: The received String message
"""
self.get_logger().info(f'Received: "{msg.data}"')

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

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

if __name__ == '__main__':
main()

Topic Communication Patterns

One-to-Many: One publisher, multiple subscribers (most common)

  • Example: Camera publishes images; object detector, face recognizer, and logger all subscribe

Many-to-One: Multiple publishers, one subscriber

  • Example: Multiple sensor nodes publish to /diagnostics; a monitoring node subscribes

Many-to-Many: Multiple publishers and subscribers

  • Example: Multi-robot coordination where all robots publish and subscribe to positions

Services: Request-Response Communication

Services implement a synchronous request-response pattern. A client sends a request and waits for the server to process it and return a response.

When to Use Services

Services are appropriate when:

  • You need a response to confirm an operation completed
  • The operation is occasional rather than continuous
  • The operation is relatively quick (seconds, not minutes)
  • You need to pass parameters and get results

Service Example: Add Two Integers

Let's create a service that adds two integers:

Service Server:

"""
Service server that adds two integers
"""
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class AdditionServer(Node):
"""
Service server that provides integer addition
"""
def __init__(self):
super().__init__('addition_server')

# Create a service
# - Service type: AddTwoInts (request has a and b, response has sum)
# - Service name: 'add_two_ints'
# - Callback function: add_callback
self.srv = self.create_service(
AddTwoInts,
'add_two_ints',
self.add_callback
)

self.get_logger().info('Addition service ready!')

def add_callback(self, request, response):
"""
Called when a client makes a request

Args:
request: Contains 'a' and 'b' integers
response: Object to fill with result

Returns:
response: Contains 'sum' field
"""
# Perform the addition
response.sum = request.a + request.b

self.get_logger().info(
f'Request: {request.a} + {request.b} = {response.sum}'
)

return response

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

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

if __name__ == '__main__':
main()

Service Client:

"""
Service client that requests integer addition
"""
import sys
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts

class AdditionClient(Node):
"""
Service client that calls the addition service
"""
def __init__(self):
super().__init__('addition_client')

# Create a client
self.client = self.create_client(AddTwoInts, 'add_two_ints')

# Wait for the service to be available
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Waiting for service to be available...')

def send_request(self, a, b):
"""
Send an addition request

Args:
a: First integer
b: Second integer

Returns:
Future object that will contain the response
"""
request = AddTwoInts.Request()
request.a = a
request.b = b

self.get_logger().info(f'Sending request: {a} + {b}')

# Call the service asynchronously
future = self.client.call_async(request)
return future

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

# Get numbers from command line arguments
if len(sys.argv) != 3:
print('Usage: addition_client <a> <b>')
return

a = int(sys.argv[1])
b = int(sys.argv[2])

node = AdditionClient()
future = node.send_request(a, b)

# Wait for the result
rclpy.spin_until_future_complete(node, future)

if future.result() is not None:
response = future.result()
node.get_logger().info(f'Result: {response.sum}')
else:
node.get_logger().error('Service call failed!')

node.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

Actions: Long-Running Tasks with Feedback

Actions extend the service pattern for tasks that:

  • Take significant time to complete (seconds to minutes)
  • Benefit from progress feedback
  • May need to be cancelled mid-execution

Action Components

An action has three message types:

  1. Goal: What you want to accomplish
  2. Feedback: Periodic progress updates
  3. Result: Final outcome when complete

Action Example: Fibonacci Sequence

Let's implement an action that calculates Fibonacci numbers with feedback:

Action Server (Simplified):

"""
Action server that computes Fibonacci sequence with feedback
"""
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from action_tutorials_interfaces.action import Fibonacci
import time

class FibonacciActionServer(Node):
"""
Action server that generates Fibonacci sequences
"""
def __init__(self):
super().__init__('fibonacci_action_server')

# Create an action server
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback
)

self.get_logger().info('Fibonacci action server ready!')

def execute_callback(self, goal_handle):
"""
Execute the Fibonacci sequence generation

Args:
goal_handle: Contains the goal and allows sending feedback
"""
self.get_logger().info('Executing goal...')

# Get the requested sequence length from goal
order = goal_handle.request.order

# Initialize the sequence
sequence = [0, 1]

# Generate the sequence
for i in range(1, order):
# Check if cancellation was requested
if goal_handle.is_cancel_requested:
goal_handle.canceled()
self.get_logger().info('Goal canceled')
return Fibonacci.Result()

# Calculate next number
sequence.append(sequence[i] + sequence[i-1])

# Send feedback
feedback = Fibonacci.Feedback()
feedback.partial_sequence = sequence
goal_handle.publish_feedback(feedback)

self.get_logger().info(f'Feedback: {sequence}')

# Simulate computation time
time.sleep(1)

# Goal completed successfully
goal_handle.succeed()

# Return the result
result = Fibonacci.Result()
result.sequence = sequence

self.get_logger().info(f'Result: {sequence}')
return result

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

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

if __name__ == '__main__':
main()

Real-World Action Examples

Robot Navigation:

  • Goal: Target position
  • Feedback: Current position, distance remaining
  • Result: Success/failure status

Object Manipulation:

  • Goal: Object to grasp and target location
  • Feedback: Gripper position, grasp status
  • Result: Whether object was successfully moved

Autonomous Exploration:

  • Goal: Area to explore
  • Feedback: Map coverage percentage, current exploring region
  • Result: Complete map

Choosing the Right Communication Pattern

ScenarioUse ThisWhy
Continuous sensor dataTopicEfficient streaming, many subscribers can listen
Check if path is collision-freeServiceQuick check with yes/no response
Navigate to a positionActionLong-running with progress updates and cancellation
Read robot configurationParameterStatic or slowly-changing values
Emergency stopTopicNeeds to reach all nodes quickly

Key Takeaways

  • Nodes are independent processes that perform specific computational tasks in a ROS 2 system
  • Topics implement publish-subscribe for continuous data streaming; publishers and subscribers are decoupled
  • Publishers send messages without knowing who receives them; subscribers receive all messages on subscribed topics
  • Services provide synchronous request-response communication for quick operations needing confirmation
  • Actions support long-running tasks with goal submission, periodic feedback, and cancellation capability
  • Choose topics for streaming data, services for quick transactions, and actions for time-consuming tasks
  • Well-designed nodes are focused, modular, and reusable across different robots and projects

Self-Assessment

Test your understanding of ROS 2 communication patterns:

Question 1

What is the primary purpose of a ROS 2 node?

A) To store robot configuration files
B) To perform a specific computational task
C) To physically connect robot components
D) To compile robot code

Question 2

In the topic communication pattern, what happens if there are no subscribers when a publisher sends a message?

A) The system crashes
B) The message is queued indefinitely
C) The message is sent but not received by anyone
D) The publisher receives an error

Question 3

Which communication pattern should you use for a task that takes 30 seconds and requires progress updates?

A) Topic
B) Service
C) Action
D) Parameter

Question 4

What are the three message types associated with an action?

A) Request, Response, Status
B) Goal, Feedback, Result
C) Start, Progress, End
D) Input, Output, Error

Question 5

In the publisher-subscriber code examples, what does the queue size parameter (10) represent?

A) Maximum number of subscribers
B) Number of messages to keep if subscriber is slower than publisher
C) Publishing frequency in Hz
D) Maximum message size in bytes

Click to reveal answers

Answer Key:

  1. B - A node is an independent process designed to perform a specific computational task, such as reading a sensor, processing data, or controlling actuators
  2. C - In publish-subscribe, publishers send messages without waiting for acknowledgment. If no subscribers exist, the message is sent but simply not received by anyone—this is by design for decoupled communication
  3. C - Actions are specifically designed for long-running tasks that benefit from progress updates and cancellation capability. A 30-second task fits this pattern perfectly
  4. B - Actions consist of three message types: Goal (what to accomplish), Feedback (progress updates), and Result (final outcome)
  5. B - The queue size determines how many messages are buffered if the subscriber processes messages slower than the publisher produces them. When the queue is full, oldest messages are discarded

Scoring Guide:

  • 5/5: Excellent! You understand ROS 2 communication patterns thoroughly
  • 4/5: Great work! Review the concept you missed
  • 3/5: Good foundation. Re-read sections on choosing communication patterns
  • 2/5 or below: Recommended to review this chapter and practice with the code examples

Next Steps

Now that you understand communication patterns, you're ready to build complete ROS 2 packages. Continue to Building ROS 2 Packages with Python to learn the practical skills of creating, organizing, and running ROS 2 Python packages.