完整教程:【ROS2学习笔记】动作

前言

本系列博文是本人的学习笔记,自用为主,不是教程,学习请移步其他大佬的相关教程。前几篇学习资源来自鱼香ROS大佬的详细教程,适合深入学习,但对本人这样的初学者不算友好,后续笔记将以@古月居的ROS2入门21讲为主,侵权即删。

一、学习目标

  1. 搞懂 “动作通信” 的核心场景 —— 什么时候必须用动作,而不是话题 / 服务
  2. 掌握动作通信的 “客户端 - 服务器” 模型及底层原理(基于话题 + 服务)
  3. 学会自定义动作接口(.action 文件)的 “定义→编译” 流程
  4. 能独立编写动作 “服务端” 和 “客户端” 代码,理解反馈 / 结果的处理逻辑
  5. 熟练使用动作相关命令行工具,方便调试与验证

二、为什么需要动作通信?(小白先搞懂 “场景”)

之前学过话题(单向高频,比如传图像)和服务(双向低频,比如查位置),但遇到以下场景就不够用了:比如让机器人 “转一圈(360 度)”“导航到客厅”“机械臂抓取物体”—— 这些任务有 3 个特点:

  1. 耗时久:不是一瞬间完成(转一圈可能要 5 秒);
  2. 需进度反馈:想知道 “现在转到 100 度了”“离客厅还有 2 米”;
  3. 可取消:万一遇到障碍,能随时让机器人停止任务。

话题(只有单向数据,没有 “任务结束” 的反馈)和服务(只能等任务结束才给结果,中间没进度)都满足不了这些需求。→ 动作通信就是为 “长时间、需反馈、可取消” 的任务设计的,相当于给任务装了 “进度条 + 暂停按钮”。

三、动作通信的核心概念(类比理解)

3.1 一句话说清动作:带 “进度条” 的服务

动作和服务类似,都是 “客户端发请求,服务器执行”,但多了两个关键功能:

  • 周期反馈:服务器执行过程中,不断给客户端发 “进度”(比如 “当前角度 10 度”);
  • 任务结果:任务结束后,服务器给客户端发 “最终状态”(比如 “转完 360 度,成功”)。

类比生活场景:你(客户端)给外卖员(服务器)发指令 “送一份 pizza 到我家”:

  • 任务中:外卖员每隔 5 分钟发消息(反馈):“已到小区门口”“正在上 3 楼”;
  • 任务结束:外卖员说 “披萨送到了(结果:成功)” 或 “路上洒了(结果:失败)”;
  • 可取消:你中途打电话说 “不用送了”(取消任务)。

3.2 动作通信的核心特性(对比话题 / 服务更清晰)

用表格对比 3 种通信机制,小白一眼看懂区别:

通信机制核心场景数据流向关键优势缺点
话题单向高频数据(如图像、速度)发布者→多个订阅者实时性高,适合传流数据没有 “任务结束” 反馈,无法确认接收方是否处理
服务双向低频请求(如查位置、算加法)客户端→服务器(请求);服务器→客户端(响应)有来有回,适合短任务中间无进度反馈,任务不能取消
动作长时间任务(如转圈、导航)客户端→服务器(目标);服务器→客户端(反馈 + 结果)有进度反馈,支持取消任务比服务复杂,适合长任务不适合短任务

3.3 动作的通信模型:客户端 - 服务器(和服务一样)

动作和服务采用相同的 “客户端 - 服务器” 模型,规则也一样:

  • 客户端:可以有多个(比如你和家人都能给机器人发 “转圈” 指令);
  • 服务器:只能有一个(机器人同一时间只能执行一个任务,比如先转完圈再导航);
  • 数据交互流程
    1. 客户端发送 “任务目标”(比如 “转 360 度”);
    2. 服务器确认 “收到目标”,开始执行任务;
    3. 服务器周期发送反馈(比如 “当前 100 度”“当前 200 度”);
    4. 任务结束后,服务器发送 “最终结果”(比如 “成功转完 360 度”);
    5. (可选)客户端中途发送 “取消指令”,服务器停止任务并返回结果。

3.4 底层原理:动作是 “话题 + 服务” 拼出来的

小白不用深究底层代码,但要知道这个关键知识点 —— 动作不是全新的通信方式,而是用之前学的话题和服务实现的:

  • 用 2 个服务实现:
    1. 客户端发 “任务目标” → 服务器用服务响应 “收到目标”;
    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 文件
  1. 进入之前的接口功能包learning_interface(没有就新建,参考上一篇笔记);
  2. 在功能包下新建action文件夹,创建MoveCircle.action文件,内容如上;
步骤 2:配置编译依赖(修改 2 个文件)

需要让 ROS 知道 “要编译这个.action 文件”,修改package.xmlCMakeLists.txt

1. 修改 package.xml(声明依赖)

打开learning_interface/package.xml,添加动作编译需要的依赖(如果之前加过话题 / 服务的依赖,只需确认包含这些):


rosidl_default_generators

rosidl_default_runtime

rosidl_interface_packages
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.feedback
        self.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_pointsconsole_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
预期输出:
  1. 小海龟在仿真窗口中向左转 90 度;
  2. 终端输出反馈和结果:
    Sending goal:
    theta: -1.57
    Goal accepted with ID: a63b3f40-0f0d-4a3a-8c0a-78e7f1b8f5a0
    Feedback:
    theta: -0.785398163394928
    Feedback:
    theta: -1.570796326789856
    Result:
    delta: -1.570796326789856
    Goal 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

八、复习要点总结(小白必背)

  1. 动作的核心场景:长时间、需进度反馈、可取消的任务(转圈、导航、抓取);
  2. 动作的 3 个关键部分
    • Goal(目标):客户端告诉服务器 “做什么”;
    • Feedback(反馈):服务器告诉客户端 “做到哪了”;
    • Result(结果):服务器告诉客户端 “做完了怎么样”;
  3. 底层原理:动作是基于 “2 个服务(目标 / 取消)+1 个话题(反馈)” 实现的,是应用层封装;
  4. 代码核心逻辑
    • 服务端:创建ActionServer → 实现execute_callback(发反馈、返回结果);
    • 客户端:创建ActionClient → 调用send_goal_async(发目标)→ 实现 3 个回调(目标响应、反馈、结果);
  5. 命令行工具list查动作、info查详情、show查接口、send_goal发目标。

通过 “自定义转圈动作” 和 “小海龟旋转动作” 两个案例,小白能掌握动作通信的核心流程,后续遇到 “导航”“机械臂控制” 等场景,只需替换动作接口和执行逻辑即可

posted @ 2025-10-22 10:21  yxysuanfa  阅读(70)  评论(0)    收藏  举报