ROS 2 Services and Actions: Synchronous and Long-Running Communication
In the previous chapter, we explored the asynchronous, one-to-many communication pattern of topics. While topics are excellent for continuous data streams, many robotic tasks require a more direct request-response interaction or involve long-running operations with feedback. This chapter introduces ROS 2 services and actions, which provide these capabilities.
ROS 2 Services: Synchronous Request/Response
Services enable a synchronous, one-to-one communication pattern. A client sends a request to a service server and blocks until it receives a response. This is ideal for tasks that need an immediate result or confirmation, such as requesting sensor data once, or triggering a specific movement command.
Implementing Service Servers in Python (rclpy)
To create a service server:
- Import
rclpyand service type: Import the necessary ROS 2 Python client library and the service message type (e.g.,example_interfaces.srv.AddTwoInts). - Create a node: Every ROS 2 component runs within a node.
- Define a callback function: This function will be executed when a request is received, process the request, and return a response.
- Create a service: Instantiate a service, providing the service type, name, and callback function.
Pseudo-code for a Simple Service Server
This pseudo-code illustrates a service server that adds two integers.
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts # Example service type
class AddTwoIntsService(Node):
def __init__(self):
super().__init__('add_two_ints_server')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
self.get_logger().info('AddTwoInts service server started.')
def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'Incoming request: a={request.a}, b={request.b}. Sending response: {response.sum}')
return response
def main(args=None):
rclpy.init(args=args)
minimal_service = AddTwoIntsService()
rclpy.spin(minimal_service)
minimal_service.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Implementing Service Clients in Python (rclpy)
To create a service client:
- Import
rclpyand service type: Similar to the server, import the service message type. - Create a node: All ROS 2 components need a node.
- Create a client: Instantiate a client for the service you want to call.
- Send request and await response: Create a request, send it, and wait for the synchronous response.
Pseudo-code for a Simple Service Client
This pseudo-code illustrates a client calling the AddTwoInts service.
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
import sys
class AddTwoIntsClient(Node):
def __init__(self):
super().__init__('add_two_ints_client')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddTwoInts.Request()
def send_request(self, a, b):
self.req.a = a
self.req.b = b
self.future = self.cli.call_async(self.req)
rclpy.spin_until_future_complete(self, self.future) # Blocks until response
return self.future.result()
def main(args=None):
rclpy.init(args=args)
minimal_client = AddTwoIntsClient()
if len(sys.argv) != 3:
minimal_client.get_logger().info('Usage: ros2 run <package_name> add_two_ints_client A B')
minimal_client.destroy_node()
rclpy.shutdown()
sys.exit(1)
a = int(sys.argv[1])
b = int(sys.argv[2])
response = minimal_client.send_request(a, b)
minimal_client.get_logger().info(f'Result of add_two_ints: {a} + {b} = {response.sum}')
minimal_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
ROS 2 Actions: Long-Running Tasks with Feedback
Actions are designed for tasks that take a long time to complete and where the client needs continuous feedback on the progress, and potentially the ability to cancel the task. Examples include navigating a robot to a destination, or performing a complex manipulation sequence. Actions are built on top of topics and services.
Implementing Action Servers in Python (rclpy)
An action server typically:
- Receives a goal: The task to be performed.
- Provides continuous feedback: Updates the client on the progress.
- Executes the task: Performs the actual work.
- Sends a result: Notifies the client upon completion (success or failure).
Pseudo-code for a Simple Action Server (Count-to-Goal)
This pseudo-code illustrates an action server that counts from 0 to a target goal, providing feedback at each step.
import rclpy
from rclpy.action import ActionServer, ActionClient
from rclpy.node import Node
from example_interfaces.action import Fibonacci # Example action type
import time
class FibonacciActionServer(Node):
def __init__(self):
super().__init__('fibonacci_action_server')
self._action_server = ActionServer(
self,
Fibonacci,
'fibonacci',
self.execute_callback)
self.get_logger().info('Fibonacci action server started.')
def execute_callback(self, goal_handle):
self.get_logger().info('Executing goal...')
feedback_msg = Fibonacci.Feedback()
feedback_msg.sequence = [0, 1]
for i in range(1, goal_handle.request.order):
if goal_handle.is_cancel_requested:
goal_handle.canceled()
self.get_logger().info('Goal canceled!')
return Fibonacci.Result() # Return an empty result
feedback_msg.sequence.append(feedback_msg.sequence[i] + feedback_msg.sequence[i-1])
self.get_logger().info(f'Feedback: {feedback_msg.sequence}')
goal_handle.publish_feedback(feedback_msg)
time.sleep(1) # Simulate work
goal_handle.succeed()
result = Fibonacci.Result()
result.sequence = feedback_msg.sequence
self.get_logger().info('Goal succeeded!')
return result
def main(args=None):
rclpy.init(args=args)
fibonacci_action_server = FibonacciActionServer()
rclpy.spin(fibonacci_action_server)
fibonacci_action_server.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Implementing Action Clients in Python (rclpy)
An action client sends a goal to an action server and can monitor its progress, receive feedback, and cancel the goal.
Pseudo-code for a Simple Action Client
This pseudo-code illustrates an action client requesting the Fibonacci sequence.
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from example_interfaces.action import Fibonacci
class FibonacciActionClient(Node):
def __init__(self):
super().__init__('fibonacci_action_client')
self._action_client = ActionClient(self, Fibonacci, 'fibonacci')
def send_goal(self, order):
goal_msg = Fibonacci.Goal()
goal_msg.order = order
self._action_client.wait_for_server()
self._send_goal_future = self._action_client.send_goal_async(goal_msg, feedback_callback=self.feedback_callback)
self._send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected :(')
return
self.get_logger().info('Goal accepted :)')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info(f'Result: {result.sequence}')
rclpy.shutdown()
def feedback_callback(self, feedback_msg):
self.get_logger().info(f'Received feedback: {feedback_msg.feedback.sequence}')
def main(args=None):
rclpy.init(args=args)
action_client = FibonacciActionClient()
action_client.send_goal(10) # Request Fibonacci sequence of order 10
rclpy.spin(action_client)
if __name__ == '__main__':
main()
Conclusion
Services and actions extend ROS 2's communication capabilities beyond simple topics, enabling synchronous request/response interactions and robust handling of long-running tasks with progress feedback and preemption. These patterns are essential for building complex, reliable robotic behaviors. With an understanding of topics, services, and actions, you now have a comprehensive toolkit for inter-node communication in ROS 2.