【ROS2学习笔记】动作
前言
本系列博文是本人的学习笔记,自用为主,不是教程,学习请移步其他大佬的相关教程。前几篇学习资源来自鱼香ROS大佬的详细教程,适合深入学习,但对本人这样的初学者不算友好,后续笔记将以@古月居的ROS2入门21讲为主,侵权即删。
一、学习目标
- 搞懂 “动作通信” 的核心场景 —— 什么时候必须用动作,而不是话题 / 服务
- 掌握动作通信的 “客户端 - 服务器” 模型及底层原理(基于话题 + 服务)
- 学会自定义动作接口(.action 文件)的 “定义→编译” 流程
- 能独立编写动作 “服务端” 和 “客户端” 代码,理解反馈 / 结果的处理逻辑
- 熟练使用动作相关命令行工具,方便调试与验证
二、为什么需要动作通信?(小白先搞懂 “场景”)
之前学过话题(单向高频,比如传图像)和服务(双向低频,比如查位置),但遇到以下场景就不够用了:比如让机器人 “转一圈(360 度)”“导航到客厅”“机械臂抓取物体”—— 这些任务有 3 个特点:
- 耗时久:不是一瞬间完成(转一圈可能要 5 秒);
- 需进度反馈:想知道 “现在转到 100 度了”“离客厅还有 2 米”;
- 可取消:万一遇到障碍,能随时让机器人停止任务。
话题(只有单向数据,没有 “任务结束” 的反馈)和服务(只能等任务结束才给结果,中间没进度)都满足不了这些需求。→ 动作通信就是为 “长时间、需反馈、可取消” 的任务设计的,相当于给任务装了 “进度条 + 暂停按钮”。
三、动作通信的核心概念(类比理解)
3.1 一句话说清动作:带 “进度条” 的服务
动作和服务类似,都是 “客户端发请求,服务器执行”,但多了两个关键功能:
- 周期反馈:服务器执行过程中,不断给客户端发 “进度”(比如 “当前角度 10 度”);
- 任务结果:任务结束后,服务器给客户端发 “最终状态”(比如 “转完 360 度,成功”)。
类比生活场景:你(客户端)给外卖员(服务器)发指令 “送一份 pizza 到我家”:
- 任务中:外卖员每隔 5 分钟发消息(反馈):“已到小区门口”“正在上 3 楼”;
- 任务结束:外卖员说 “披萨送到了(结果:成功)” 或 “路上洒了(结果:失败)”;
- 可取消:你中途打电话说 “不用送了”(取消任务)。
3.2 动作通信的核心特性(对比话题 / 服务更清晰)
用表格对比 3 种通信机制,小白一眼看懂区别:
通信机制 | 核心场景 | 数据流向 | 关键优势 | 缺点 |
---|---|---|---|---|
话题 | 单向高频数据(如图像、速度) | 发布者→多个订阅者 | 实时性高,适合传流数据 | 没有 “任务结束” 反馈,无法确认接收方是否处理 |
服务 | 双向低频请求(如查位置、算加法) | 客户端→服务器(请求);服务器→客户端(响应) | 有来有回,适合短任务 | 中间无进度反馈,任务不能取消 |
动作 | 长时间任务(如转圈、导航) | 客户端→服务器(目标);服务器→客户端(反馈 + 结果) | 有进度反馈,支持取消任务 | 比服务复杂,适合长任务不适合短任务 |
3.3 动作的通信模型:客户端 - 服务器(和服务一样)
动作和服务采用相同的 “客户端 - 服务器” 模型,规则也一样:
- 客户端:可以有多个(比如你和家人都能给机器人发 “转圈” 指令);
- 服务器:只能有一个(机器人同一时间只能执行一个任务,比如先转完圈再导航);
- 数据交互流程:
- 客户端发送 “任务目标”(比如 “转 360 度”);
- 服务器确认 “收到目标”,开始执行任务;
- 服务器周期发送反馈(比如 “当前 100 度”“当前 200 度”);
- 任务结束后,服务器发送 “最终结果”(比如 “成功转完 360 度”);
- (可选)客户端中途发送 “取消指令”,服务器停止任务并返回结果。
3.4 底层原理:动作是 “话题 + 服务” 拼出来的
小白不用深究底层代码,但要知道这个关键知识点 —— 动作不是全新的通信方式,而是用之前学的话题和服务实现的:
- 用 2 个服务实现:
- 客户端发 “任务目标” → 服务器用服务响应 “收到目标”;
- 客户端发 “取消指令” → 服务器用服务响应 “已取消”;
- 用 1 个话题实现:服务器周期发 “进度反馈” → 客户端订阅这个话题接收进度;
- 最终结果:用服务的响应返回(任务结束时服务器通过服务给结果)。
→ 动作是 “应用层封装”,把 “服务(目标 / 取消)+ 话题(反馈)” 打包成一套简单的接口,小白不用自己写话题和服务的组合逻辑。
四、动作接口的定义(.action 文件)
和话题(.msg)、服务(.srv)一样,动作也需要定义 “数据格式”—— 用.action
文件,结构分 3 部分,用---
分隔。
4.1 .action 文件结构(3 部分,固定顺序)
动作的核心是 “目标、反馈、结果”,所以.action
文件分 3 块:
部分 | 作用 | 对应场景 |
---|---|---|
目标(Goal) | 客户端发给服务器的 “任务指令” | “转 360 度”“导航到 (1,2) 坐标” |
结果(Result) | 服务器任务结束后给的 “最终状态” | “转完 360 度(成功)”“导航失败” |
反馈(Feedback) | 服务器执行中给的 “进度” | “当前 100 度”“离目标还有 1 米” |
格式示例(比如让机器人转圈的MoveCircle.action
):
# 第一部分:动作目标(Goal)——客户端告诉服务器“要做什么”
bool enable # true=开始转圈,false=不执行(相当于任务开关)
---
# 第二部分:动作结果(Result)——服务器告诉客户端“任务做完了怎么样”
bool finish # true=转圈成功,false=转圈失败
---
# 第三部分:动作反馈(Feedback)——服务器告诉客户端“任务做到哪了”
int32 state # 当前转圈的角度(比如10、20、30度)
4.2 自定义动作接口的 “定义→编译” 流程
和之前自定义话题 / 服务接口步骤一样,分 3 步:
步骤 1:创建.action 文件
- 进入之前的接口功能包
learning_interface
(没有就新建,参考上一篇笔记); - 在功能包下新建
action
文件夹,创建MoveCircle.action
文件,内容如上;
步骤 2:配置编译依赖(修改 2 个文件)
需要让 ROS 知道 “要编译这个.action 文件”,修改package.xml
和CMakeLists.txt
。
1. 修改 package.xml(声明依赖)
打开learning_interface/package.xml
,添加动作编译需要的依赖(如果之前加过话题 / 服务的依赖,只需确认包含这些):
<!-- 编译时依赖:生成动作接口代码的工具 -->
<build_depend>rosidl_default_generators</build_depend>
<!-- 运行时依赖:节点运行时需要的接口库 -->
<exec_depend>rosidl_default_runtime</exec_depend>
<!-- 声明这是接口包,让ROS识别 -->
<member_of_group>rosidl_interface_packages</member_of_group>
2. 修改 CMakeLists.txt(配置编译规则)
打开learning_interface/CMakeLists.txt
,添加.action 文件的编译配置:
# 1. 查找生成接口代码的工具包(必须)
find_package(rosidl_default_generators REQUIRED)# 2. 生成动作接口代码:指定功能包名、.action文件路径
rosidl_generate_interfaces(${PROJECT_NAME}"action/MoveCircle.action" # 你的动作接口文件# 如果有其他接口(.msg/.srv),继续加在这里,比如:# "msg/ObjectPosition.msg"# "srv/GetObjectPosition.srv"
)# 3. 导出接口,让其他功能包(比如动作节点包)能调用
ament_export_dependencies(rosidl_default_runtime)
步骤 3:编译接口包
回到 ROS 工作空间根目录(比如dev_ws
),执行编译:
colcon build --packages-select learning_interface
编译后,ROS 会自动把.action
文件转换成 Python/C++ 能识别的代码(小白不用管生成的文件,会调用就行)。
五、实战案例:动作通信的代码实现(小白重点)
以 “机器人转圈” 为例,实现:
- 动作服务端:接收 “转圈” 目标,模拟机器人转圈,每 30 度发一次反馈,结束后返回 “成功” 结果;
- 动作客户端:发送 “开始转圈” 目标,接收进度反馈和最终结果。
5.1 准备工作:创建动作节点包
先创建一个专门放动作节点的功能包learning_action
,依赖动作相关库:
cd dev_ws/src
ros2 pkg create learning_action --build-type ament_python --dependencies rclpy action_msgs learning_interface
- 依赖说明:
rclpy
(ROS2 Python 核心库)、action_msgs
(动作基础库)、learning_interface
(我们自定义的动作接口包)。
5.2 案例 1:动作服务端代码(执行转圈任务,发反馈)
文件名:learning_action/action_move_server.py
代码 + 逐行注释:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ROS2动作服务端:执行机器人转圈任务
功能:1. 接收客户端的“转圈”目标 2. 模拟转圈(每0.5秒转30度) 3. 每转30度发一次反馈 4. 转完360度返回成功结果
"""# 1. 导入需要的库
import time # 用于延时(模拟转圈耗时)
import rclpy # ROS2 Python核心库
from rclpy.node import Node # ROS2节点类(所有节点必须继承)
from rclpy.action import ActionServer # ROS2动作服务器类(实现服务端核心功能)
from learning_interface.action import MoveCircle # 导入自定义的动作接口(MoveCircle.action)# 2. 定义动作服务端节点类(继承Node)
class MoveCircleActionServer(Node):def __init__(self, name):"""构造函数:初始化节点、创建动作服务器"""# 调用父类Node的构造函数,给节点起名字:"action_move_server"super().__init__(name)# 2.1 创建动作服务器:核心对象,负责接收目标、执行任务、发反馈/结果self._action_server = ActionServer(self, # 上下文(当前节点)MoveCircle, # 动作接口类型(自定义的MoveCircle)'move_circle', # 动作名(客户端必须用这个名字找到服务端)self.execute_callback # 收到目标后执行的回调函数(核心逻辑在这里))self.get_logger().info("动作服务端已启动!等待客户端发送转圈目标...")def execute_callback(self, goal_handle):"""核心回调函数:收到客户端目标后,执行转圈任务,发反馈和结果"""# goal_handle:目标句柄,用于控制任务(发反馈、标记成功/失败、取消任务)# 1. 打印日志:提示开始执行任务self.get_logger().info('开始执行转圈任务!')# 2. 创建反馈消息对象(对应MoveCircle.action的Feedback部分)feedback_msg = MoveCircle.Feedback()# 3. 模拟转圈过程:从0度到360度,每次转30度(步长30)for current_angle in range(0, 360, 30):# 3.1 设置当前反馈:把当前角度赋值给feedback_msg的state字段feedback_msg.state = current_angle# 3.2 发布反馈:通过goal_handle把反馈发给客户端goal_handle.publish_feedback(feedback_msg)# 3.3 延时0.5秒:模拟转圈耗时(实际机器人这里会控制电机转动)time.sleep(0.5)# (拓展:如果需要支持“取消任务”,这里可以加判断:if goal_handle.is_cancel_requested(): ...)# 4. 任务执行完成:标记任务“成功”goal_handle.succeed()# 5. 创建结果消息对象(对应MoveCircle.action的Result部分)result = MoveCircle.Result()# 设置结果:finish=True表示转圈成功result.finish = True# 6. 返回结果给客户端,结束任务self.get_logger().info('转圈任务完成!已转完360度')return result# 3. 主入口函数:启动节点
def main(args=None):# 3.1 初始化ROS2 Python接口(必须第一步)rclpy.init(args=args)# 3.2 创建动作服务端节点对象node = MoveCircleActionServer("action_move_server")# 3.3 启动节点循环:让节点一直运行,等待处理客户端请求rclpy.spin(node)# 3.4 关闭节点(程序退出时执行,释放资源)node.destroy_node()# 3.5 关闭ROS2 Python接口rclpy.shutdown()
5.3 案例 2:动作客户端代码(发目标,收反馈 / 结果)
文件名:learning_action/action_move_client.py
代码 + 逐行注释:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ROS2动作客户端:请求机器人转圈任务
功能:1. 发送“开始转圈”目标给服务端 2. 接收服务端的周期反馈(当前角度) 3. 接收任务最终结果(是否成功)
"""# 1. 导入需要的库
import rclpy # ROS2 Python核心库
from rclpy.node import Node # ROS2节点类
from rclpy.action import ActionClient # ROS2动作客户端类(实现客户端核心功能)
from learning_interface.action import MoveCircle # 导入自定义的动作接口# 2. 定义动作客户端节点类(继承Node)
class MoveCircleActionClient(Node):def __init__(self, name):"""构造函数:初始化节点、创建动作客户端"""# 调用父类Node的构造函数,给节点起名字:"action_move_client"super().__init__(name)# 2.1 创建动作客户端:负责发送目标、接收反馈/结果self._action_client = ActionClient(self, # 上下文(当前节点)MoveCircle, # 动作接口类型(和服务端一致)'move_circle' # 动作名(必须和服务端一致,才能找到服务端))self.get_logger().info("动作客户端已启动!准备发送转圈目标...")def send_goal(self, enable):"""发送动作目标的函数:enable=True表示请求开始转圈"""# 1. 等待服务端上线:如果服务端没启动,客户端会一直等(每隔1秒查一次)self._action_client.wait_for_server()# 2. 创建目标消息对象(对应MoveCircle.action的Goal部分)goal_msg = MoveCircle.Goal()# 设置目标:enable=True(请求开始转圈)goal_msg.enable = enable# 3. 异步发送目标:不会阻塞节点,发完后继续执行其他逻辑# feedback_callback:指定接收反馈的回调函数(收到反馈就执行)self._send_goal_future = self._action_client.send_goal_async(goal_msg,feedback_callback=self.feedback_callback)# 4. 设置“目标响应”的回调函数:服务端收到目标后,会调用这个函数self._send_goal_future.add_done_callback(self.goal_response_callback)def goal_response_callback(self, future):"""回调函数1:服务端收到目标后的响应处理"""# 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):"""回调函数2:接收任务最终结果的处理"""# 读取服务端返回的结果result = future.result().result# 判断结果:如果finish=True,表示转圈成功if result.finish:self.get_logger().info(f"任务成功!结果:{result.finish}(已转完360度)")else:self.get_logger().info(f"任务失败!结果:{result.finish}")def feedback_callback(self, feedback_msg):"""回调函数3:接收周期反馈的处理(服务端每发一次反馈,就执行一次)"""# 读取反馈消息中的“当前角度”(feedback_msg.feedback对应Feedback部分)feedback = feedback_msg.feedbackself.get_logger().info(f"收到反馈:当前转圈角度 = {feedback.state} 度")# 3. 主入口函数:启动客户端,发送目标
def main(args=None):# 3.1 初始化ROS2 Python接口rclpy.init(args=args)# 3.2 创建动作客户端节点对象node = MoveCircleActionClient("action_move_client")# 3.3 发送目标:enable=True(请求开始转圈)node.send_goal(True)# 3.4 启动节点循环:等待接收反馈和结果rclpy.spin(node)# 3.5 关闭节点和ROS2接口node.destroy_node()rclpy.shutdown()
5.4 配置节点入口(让 ROS 能找到程序)
打开learning_action/setup.py
,在entry_points
的console_scripts
里添加客户端和服务端的入口(告诉 ROS“运行某个命令时,执行哪个文件的 main 函数”):
entry_points={'console_scripts': [# 格式:"命令名 = 包名.文件名:main函数"'action_move_server = learning_action.action_move_server:main','action_move_client = learning_action.action_move_client:main',],
},
5.5 运行步骤(3 个终端)
终端 1:编译功能包
cd dev_ws # 进入工作空间
colcon build --packages-select learning_action # 只编译动作节点包
source install/setup.bash # 加载环境变量(每次编译后都要执行)
终端 2:启动动作服务端
cd dev_ws
source install/setup.bash
ros2 run learning_action action_move_server # 执行服务端命令
# 预期输出:[INFO] [action_move_server]:动作服务端已启动!等待客户端发送转圈目标...
终端 3:启动动作客户端
cd dev_ws
source install/setup.bash
ros2 run learning_action action_move_client # 执行客户端命令
# 预期输出:
# 1. 客户端发送目标,服务端接受;
# 2. 客户端每隔0.5秒收到反馈:“收到反馈:当前转圈角度 = 30 度”“收到反馈:当前转圈角度 = 60 度”...;
# 3. 转完360度后,客户端打印:“任务成功!结果:True(已转完360度)”
六、小海龟动作实战(验证动作通信)
除了自定义动作,ROS 自带的小海龟也支持动作,小白可以用这个案例快速验证动作的效果。
6.1 步骤 1:启动小海龟节点
打开 2 个终端,分别启动小海龟仿真器和键盘控制(可选,用于对比):
# 终端1:启动小海龟仿真器
ros2 run turtlesim turtlesim_node
# 终端2:启动键盘控制(可选,不用也能测试动作)
ros2 run turtlesim turtle_teleop_key
6.2 步骤 2:查看小海龟的动作列表
打开新终端,执行命令查看小海龟支持的动作:
ros2 action list
# 预期输出:/turtle1/rotate_absolute (小海龟的“绝对旋转”动作)
6.3 步骤 3:查看动作的接口定义
想知道这个动作需要什么 “目标”“反馈”“结果”,用ros2 action show
命令:
ros2 action show turtlesim/action/RotateAbsolute
# 预期输出(动作接口定义):
# # Goal(目标:要旋转到的角度,单位:弧度)
# float32 theta
# ---
# # Result(结果:实际旋转到的角度)
# float32 delta
# ---
# # Feedback(反馈:当前旋转到的角度)
# float32 theta
- 说明:
theta
是角度(弧度制,比如 - 1.57 弧度≈-90 度,即向左转 90 度)。
6.4 步骤 4:发送动作目标(让小海龟旋转)
命令格式:
ros2 action send_goal <动作名> <动作类型> <目标数据> [--feedback]
# --feedback:可选参数,加上后会显示实时反馈
实际命令(让小海龟向左转 90 度,即 theta=-1.57 弧度):
ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}" --feedback
预期输出:
- 小海龟在仿真窗口中向左转 90 度;
- 终端输出反馈和结果:
Sending goal: theta: -1.57Goal accepted with ID: a63b3f40-0f0d-4a3a-8c0a-78e7f1b8f5a0Feedback: theta: -0.785398163394928Feedback: theta: -1.570796326789856Result: delta: -1.570796326789856Goal finished with status: SUCCEEDED
七、动作通信命令行工具(调试必备)
整理常用命令,小白记下来,调试时直接用:
命令语法 | 功能说明 | 示例(小海龟案例) |
---|---|---|
ros2 action list | 查看系统中所有正在运行的动作 | ros2 action list → 输出/turtle1/rotate_absolute |
ros2 action info <动作名> | 查看动作的详细信息(类型、客户端 / 服务器数量) | ros2 action info /turtle1/rotate_absolute → 显示 “Action: /turtle1/rotate_absolute, Type: turtlesim/action/RotateAbsolute” |
ros2 action show <动作类型> | 查看动作接口的定义(Goal/Result/Feedback) | ros2 action show turtlesim/action/RotateAbsolute → 显示目标 / 结果 / 反馈的字段 |
ros2 action send_goal <动作名> <动作类型> <目标数据> [--feedback] | 发送动作目标,可选看反馈 | ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}" --feedback |
八、复习要点总结(小白必背)
- 动作的核心场景:长时间、需进度反馈、可取消的任务(转圈、导航、抓取);
- 动作的 3 个关键部分:
- Goal(目标):客户端告诉服务器 “做什么”;
- Feedback(反馈):服务器告诉客户端 “做到哪了”;
- Result(结果):服务器告诉客户端 “做完了怎么样”;
- 底层原理:动作是基于 “2 个服务(目标 / 取消)+1 个话题(反馈)” 实现的,是应用层封装;
- 代码核心逻辑:
- 服务端:创建
ActionServer
→ 实现execute_callback
(发反馈、返回结果); - 客户端:创建
ActionClient
→ 调用send_goal_async
(发目标)→ 实现 3 个回调(目标响应、反馈、结果);
- 服务端:创建
- 命令行工具:
list
查动作、info
查详情、show
查接口、send_goal
发目标。
通过 “自定义转圈动作” 和 “小海龟旋转动作” 两个案例,小白能掌握动作通信的核心流程,后续遇到 “导航”“机械臂控制” 等场景,只需替换动作接口和执行逻辑即可