4、ROS2基础通信机制(下):服务与动作编程实战
上一章我们把话题通信聊透了,那种“发布-订阅”的模式,说白了就是机器人里的广播体操——一个人喊口令,一群人跟着做。但实际干活的时候,你总会遇到需要“一问一答”的场景。
比如你让机械臂抓个杯子,你不仅得告诉它“去抓”,还得知道它“抓到没有”。这时候,话题就不够用了。我们需要的是服务(Service)和动作(Action)。
4.1 服务通信:客户端与服务器模型
服务的本质很简单:一个节点发请求,另一个节点回响应。就像你去柜台办事,递上材料(请求),工作人员办完给你回执(响应)。
我个人习惯把服务端叫做“干活的”,客户端叫做“派活的”。干活的一次只接一个单,派活的发完请求就等着,直到拿到结果才继续往下走。
4.1.1 服务的数据接口定义
在ROS2里,服务接口用 .srv 文件定义。它分上下两部分,用 --- 隔开。上面是请求,下面是响应。
举个例子,我们做一个“加法计算器”的服务:
# AddTwoInts.srv
int64 a
int64 b
---
int64 sum
嗯,这里要注意:--- 是必须的,少写了编译会报错。我在项目中遇到过新手把 --- 写成 --- 的变体,结果卡了半天。
4.1.2 服务端编程实战
服务端的任务很简单:注册服务,等待请求,处理并返回。
来看代码:
# server.py
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AddServer(Node):
def __init__(self):
super().__init__('add_server')
self.srv = self.create_service(
AddTwoInts, 'add_two_ints', self.add_callback)
self.get_logger().info('服务端已启动,等待请求...')
def add_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'收到请求: {request.a} + {request.b} = {response.sum}')
return response
def main(args=None):
rclpy.init(args=args)
node = AddServer()
rclpy.spin(node)
rclpy.shutdown()
你想想看,回调函数 add_callback 里,request 就是客户端发来的数据,response 就是我们要返回的结果。这里有个坑:回调函数必须返回 response 对象,少写了服务端会卡死。
4.1.3 客户端编程实战
客户端稍微复杂一点,因为它需要“等待服务上线”。
# client.py
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AddClient(Node):
def __init__(self):
super().__init__('add_client')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().warn('等待服务上线...')
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)
return self.future.result()
def main(args=None):
rclpy.init(args=args)
node = AddClient()
response = node.send_request(3, 5)
node.get_logger().info(f'计算结果: {response.sum}')
node.destroy_node()
rclpy.shutdown()
wait_for_service,结果程序直接崩溃。记住:客户端必须先确认服务端活着,才能发请求。另外,call_async 是异步调用,如果你想要同步效果,就用 spin_until_future_complete 等着。
4.2 动作通信:让机器人“边干边汇报”
服务的缺点很明显:如果任务耗时很长(比如导航到某个点),客户端就得一直等着,中间发生了什么完全不知道。这时候,动作(Action) 就派上用场了。
动作可以理解为“带反馈的服务”。它有三个核心部分:
- Goal:目标,告诉机器人要做什么
- Feedback:反馈,机器人干活过程中不断汇报进度
- Result:结果,任务完成后的最终状态
4.2.1 动作接口定义
动作接口用 .action 文件定义,分三块,用 --- 隔开:
# MoveRobot.action
# 目标
geometry_msgs/Pose2D target_pose
---
# 结果
bool success
string message
---
# 反馈
float32 progress
string status_description
我个人觉得,动作接口的设计有点像“项目管理的三个阶段”:定目标、给反馈、看结果。你想想看,如果导航过程中没有反馈,你只能干等着,多难受。
4.2.2 动作服务端编程
动作服务端比普通服务端复杂,因为它要处理“取消请求”和“周期反馈”。
# action_server.py
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from your_package.action import MoveRobot
class MoveRobotServer(Node):
def __init__(self):
super().__init__('move_robot_server')
self._action_server = ActionServer(
self,
MoveRobot,
'move_robot',
self.execute_callback)
async def execute_callback(self, goal_handle):
self.get_logger().info(f'收到目标: {goal_handle.request.target_pose}')
feedback_msg = MoveRobot.Feedback()
for i in range(10):
if goal_handle.is_cancel_requested:
goal_handle.canceled()
return MoveRobot.Result(success=False, message='任务被取消')
feedback_msg.progress = (i + 1) * 10.0
feedback_msg.status_description = f'正在移动... {feedback_msg.progress}%'
goal_handle.publish_feedback(feedback_msg)
self.get_logger().info(feedback_msg.status_description)
rclpy.spin_once(self, timeout_sec=1.0)
goal_handle.succeed()
return MoveRobot.Result(success=True, message='移动完成')
goal_handle.is_cancel_requested。我在项目中遇到过机器人正在移动,用户突然取消任务,如果不检查这个标志,机器人会一直跑到终点才停下来,那体验就太差了。
4.2.3 动作客户端编程
动作客户端要处理三个回调:反馈回调、完成回调。
# action_client.py
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from your_package.action import MoveRobot
class MoveRobotClient(Node):
def __init__(self):
super().__init__('move_robot_client')
self._action_client = ActionClient(self, MoveRobot, 'move_robot')
def send_goal(self, target_pose):
goal_msg = MoveRobot.Goal()
goal_msg.target_pose = target_pose
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 feedback_callback(self, feedback_msg):
self.get_logger().info(f'进度: {feedback_msg.feedback.progress}% - {feedback_msg.feedback.status_description}')
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('目标被拒绝')
return
self.get_logger().info('目标被接受')
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.message}')
嗯,这里要注意:反馈回调是在子线程里执行的,不要在反馈回调里做耗时操作,否则会影响通信性能。
4.3 服务与动作的核心区别
我整理了一张表,方便你对比:
| 特性 | 服务(Service) | 动作(Action) |
|---|---|---|
| 通信模式 | 同步请求-响应 | 异步目标-反馈-结果 |
| 适用场景 | 短耗时任务(<1秒) | 长耗时任务(>1秒) |
| 是否支持取消 | 否 | 是 |
| 是否有进度反馈 | 否 | 是 |
| 接口文件后缀 | .srv | .action |
说白了,服务适合“问个事”,动作适合“干个活”。比如获取当前温度用服务,导航到某个点用动作。
4.4 本章知识体系图
下面这张图展示了服务与动作在ROS2通信中的位置和关系: