The Problem: Two Ways to Request Something
Your robot needs to:
- Fetch the camera calibration (takes 1 millisecond)
- Plan a path to the goal (takes 5 seconds)
- Move the arm to a position (takes 10 seconds and can be interrupted)
Do you use a Service? An Action? How are they different?
This question will haunt you. Here’s the answer you’ve been looking for.
The Quick Answer
| Feature | Service | Action |
|---|---|---|
| Call type | Synchronous (request-response) | Asynchronous (goal-feedback-result) |
| Blocking | Caller waits | Caller continues |
| Feedback | No | Yes |
| Preemption | No | Yes (can cancel) |
| Typical latency | Milliseconds | Seconds+ |
| Example | Get camera calibration | Move arm to position |
Rule of thumb:
- Service = “Quick question, wait for answer”
- Action = “Do this long task and let me know when done”
Part 1: Services (The Simple Case)
1.1 When to Use Services
Use services when:
- Operation is fast (< 1 second typically)
- You need synchronous behavior (caller waits)
- No feedback during execution needed
- Can’t be interrupted mid-operation
Examples:
- Get sensor calibration
- Compute IK solution
- Set a parameter
- Save/load data
1.2 Creating a Service Server (Python)
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts # Pre-defined service
class AdditionServer(Node):
def __init__(self):
super().__init__('add_server')
# Create service
self.service = self.create_service(
AddTwoInts,
'add_two_ints',
self.add_callback
)
self.get_logger().info('Addition service ready')
def add_callback(self, request, response):
"""Handle service request"""
response.sum = request.a + request.b
self.get_logger().info(f'Adding {request.a} + {request.b} = {response.sum}')
return response # Send back response
def main():
rclpy.init()
node = AdditionServer()
rclpy.spin(node)
if __name__ == '__main__':
main()1.3 Calling a Service
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AdditionClient(Node):
def __init__(self):
super().__init__('add_client')
# Create client
self.client = self.create_client(AddTwoInts, 'add_two_ints')
# Wait for service to be available
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().warn('Service not available, waiting...')
# Call the service (BLOCKING)
self.call_service()
def call_service(self):
"""Call the service and wait for response"""
request = AddTwoInts.Request()
request.a = 5
request.b = 3
# This blocks until response arrives
future = self.client.call_async(request)
# Wait for response
rclpy.spin_until_future_complete(self, future)
if future.result() is not None:
response = future.result()
self.get_logger().info(f'Result: {response.sum}')
else:
self.get_logger().error('Service call failed')
def main():
rclpy.init()
node = AdditionClient()
rclpy.spin(node)
if __name__ == '__main__':
main()1.4 The Gotcha: Services Block the Whole Node
# Service callback takes 5 seconds
def slow_callback(self, request, response):
time.sleep(5) # Blocking!
response.result = "done"
return response
# Meanwhile, if another request comes in while processing the first...
# ❌ It gets queued and waits
# ❌ Your node is unresponsiveSolution: Use Actions for long-running operations.
Part 2: Actions (The Complex Case)
2.1 When to Use Actions
Use actions when:
- Operation is long-running (seconds or more)
- You need asynchronous behavior (caller continues)
- You need feedback during execution
- Can be preempted (interrupted/canceled)
Examples:
- Move robot to goal
- Pick and place operation
- Image capture and process
- Any multi-step operation
2.2 Action Structure
An action has three parts:
Goal: What you want done
Feedback: Updates while working
Result: Final outcome
Example: Move arm to position
Goal: target_x=0.5, target_y=0.5, target_z=0.3
Feedback: current_x=0.1, current_y=0.1, percent_complete=30%
Result: success=true, final_x=0.5, final_y=0.5
2.3 Defining an Action Interface
# action/MoveArm.action
float64 target_x
float64 target_y
float64 target_z
---
bool success
float64 final_x
float64 final_y
float64 final_z
---
float64 current_x
float64 current_y
int32 percent_completeThe three parts separated by ---:
- First block: Goal
- Second block: Result
- Third block: Feedback
2.4 Creating an Action Server
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from example_interfaces.action import Fibonacci
class FibonacciServer(Node):
def __init__(self):
super().__init__('fibonacci_server')
# Create 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):
"""Execute the action"""
self.get_logger().info(f'Executing goal: {goal_handle.request.order}')
sequence = []
for i in range(goal_handle.request.order):
# Check if canceled
if goal_handle.is_cancel_requested:
goal_handle.canceled()
self.get_logger().info('Goal canceled')
return Fibonacci.Result()
# Compute fibonacci
if i == 0:
sequence.append(0)
elif i == 1:
sequence.append(1)
else:
sequence.append(sequence[i-1] + sequence[i-2])
# Send feedback
feedback = Fibonacci.Feedback()
feedback.sequence = sequence
goal_handle.publish_feedback(feedback)
self.get_logger().info(f'Feedback: {feedback.sequence}')
# Simulate work
time.sleep(0.1)
# Set result
goal_handle.succeed()
result = Fibonacci.Result()
result.sequence = sequence
return result
def main():
rclpy.init()
node = FibonacciServer()
rclpy.spin(node)
if __name__ == '__main__':
main()2.5 Creating an Action Client
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from example_interfaces.action import Fibonacci
class FibonacciClient(Node):
def __init__(self):
super().__init__('fibonacci_client')
# Create action client
self.action_client = ActionClient(self, Fibonacci, 'fibonacci')
# Wait for server
if not self.action_client.wait_for_server(timeout_sec=5.0):
self.get_logger().error('Action server not available')
return
# Send goal
self.send_goal()
def send_goal(self):
"""Send a goal to the action server"""
goal = Fibonacci.Goal()
goal.order = 10 # Compute first 10 fibonacci numbers
# Callbacks for feedback and result
self.action_client.send_goal_async(
goal,
feedback_callback=self.feedback_callback,
done_callback=self.done_callback
)
def feedback_callback(self, feedback_msg):
"""Called when server sends feedback"""
feedback = feedback_msg.feedback
self.get_logger().info(f'Feedback: {feedback.sequence}')
def done_callback(self, future):
"""Called when server finishes"""
goal_handle = future.result()
if goal_handle.cancelled():
self.get_logger().info('Goal was canceled')
elif goal_handle.succeeded():
result = goal_handle.result()
self.get_logger().info(f'Final result: {result.sequence}')
else:
self.get_logger().error('Goal failed')
def main():
rclpy.init()
node = FibonacciClient()
rclpy.spin(node)
if __name__ == '__main__':
main()2.6 Canceling an Action
def cancel_action(self):
"""Cancel the running action"""
goal_handle = self.goal_handle # Saved from send_goal_async
future = goal_handle.cancel_goal_async()
rclpy.spin_until_future_complete(self, future)
if future.result().cancel_accepted:
self.get_logger().info('Goal canceled')
else:
self.get_logger().warn('Cancel request was refused')Part 3: Comparison in Real Scenarios
Scenario 1: Get Robot Configuration
# ✅ Use Service
srv = self.create_service(
GetConfig,
'get_config',
self.get_config_callback
)
# Why: Fast operation, needs immediate responseScenario 2: Move Arm to Position
# ✗ Don't use Service (blocks caller too long)
# ✅ Use Action (async, with feedback)
action = ActionServer(self, MoveArm, 'move_arm', self.move_arm_callback)
# Why: Takes time, caller wants to continue, needs feedbackScenario 3: Capture Image
# ✅ Use Service (if synchronous capture OK)
# ✅ Use Action (if you want feedback on processing steps)
# Choose based on whether you need:
# - Feedback during capture? → Action
# - Just need result? → ServicePart 4: The Gotchas
Gotcha 1: Calling Service from Callback
# ❌ WRONG: Blocks the node while waiting
def callback1(self, request, response):
result = self.client.call(other_service) # Blocks!
response.result = result
return response
# ✅ Better: Use async or move logic elsewhere
def callback1(self, request, response):
future = self.client.call_async(other_service)
# Handle future asynchronouslyGotcha 2: Not Checking Service Availability
# ❌ WRONG: Assumes service exists
self.client.call_async(request)
# ✅ CORRECT: Wait for service
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().warn('Waiting for service...')Gotcha 3: Action Callbacks Are Complex
# The difference between:
# - goal_handle.is_cancel_requested (check if cancel was requested)
# - goal_handle.canceled() (mark as canceled)
# - goal_handle.succeed() (mark as succeeded)
# - goal_handle.abort() (mark as aborted)
# If you forget these, the client hangs waiting for resultGotcha 4: Mixing Services and Actions
# ❌ Don't create 100 services when you should use 1 action
self.create_service(Grasp, 'grasp_object', ...)
self.create_service(Open, 'open_gripper', ...)
self.create_service(Close, 'close_gripper', ...)
# ✅ Better: One action for the whole sequence
ActionServer(self, PickPlace, 'pick_place', ...)Part 5: Real-World Pattern - Pick and Place with Action
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
import time
class PickPlaceServer(Node):
def __init__(self):
super().__init__('pick_place_server')
self.action_server = ActionServer(
self,
PickPlace,
'pick_place',
self.execute_callback
)
def execute_callback(self, goal_handle):
"""Execute pick and place"""
goal = goal_handle.request
try:
# Step 1: Move to approach position
self.get_logger().info('Moving to approach position...')
self.move_arm(goal.target_x, goal.target_y, goal.target_z + 0.1)
self._publish_feedback(goal_handle, 20)
# Step 2: Move to grasp position
self.get_logger().info('Moving to grasp position...')
self.move_arm(goal.target_x, goal.target_y, goal.target_z)
self._publish_feedback(goal_handle, 40)
# Step 3: Close gripper
self.get_logger().info('Closing gripper...')
self.close_gripper()
self._publish_feedback(goal_handle, 60)
# Step 4: Lift object
self.get_logger().info('Lifting object...')
self.move_arm(goal.target_x, goal.target_y, goal.target_z + 0.2)
self._publish_feedback(goal_handle, 80)
# Step 5: Move to place position
self.get_logger().info('Moving to place position...')
self.move_arm(goal.place_x, goal.place_y, goal.place_z)
self._publish_feedback(goal_handle, 90)
# Step 6: Open gripper
self.get_logger().info('Opening gripper...')
self.open_gripper()
self._publish_feedback(goal_handle, 100)
# Success!
goal_handle.succeed()
result = PickPlace.Result()
result.success = True
return result
except Exception as e:
self.get_logger().error(f'Pick and place failed: {e}')
goal_handle.abort()
return PickPlace.Result(success=False)
def _publish_feedback(self, goal_handle, percent):
"""Send progress feedback"""
feedback = PickPlace.Feedback()
feedback.percent_complete = percent
goal_handle.publish_feedback(feedback)
def move_arm(self, x, y, z):
"""Simulated arm movement"""
time.sleep(0.5)
def close_gripper(self):
"""Simulated gripper close"""
time.sleep(0.2)
def open_gripper(self):
"""Simulated gripper open"""
time.sleep(0.2)
def main():
rclpy.init()
node = PickPlaceServer()
rclpy.spin(node)
if __name__ == '__main__':
main()Decision Tree
Does the operation take < 1 second?
├─ YES → Service
└─ NO ─┐
├─ Need feedback during execution?
│ ├─ YES → Action
│ └─ NO ─→ Service (if OK to block)
│
└─ Need to cancel mid-operation?
├─ YES → Action
└─ NO ─→ Service (if OK to block)
Quick Reference
Service:
# Server
self.create_service(MyService, 'service_name', callback)
# Client
self.client.call_async(request)Action:
# Server
ActionServer(self, MyAction, 'action_name', execute_callback)
# Client
self.action_client.send_goal_async(goal, feedback_callback, done_callback)Remember: Services are for quick queries. Actions are for long tasks with updates.