RuyiSDK Board Docs

RISC-V ROS2 机器人操作系统编程技术

源码仓库

ch05 · ROS 2 动作通信

教材编程语言运行环境课程文档实验文档
RISC-VC++17SpacemiT K3 CoM260 Kit / Bianbu 4.0.6 / Humble,配合 x86 Ubuntu 22.04 / Humble / Harmonic 课程容器阅读课程开始实验
x86PythonUbuntu 22.04 / Humble 或 Ubuntu 24.04 / Jazzy阅读课程开始实验

第5章:动作通信(Actions)

课程:ROS2 Python 编程
章节:第5章
课时:2 课时(90 分钟)
教学方式:讲授 + 演示


5.1 动作通信架构

知识点 5.1.1:动作 vs 话题 vs 服务

特性 话题 (Topic) 服务 (Service) 动作 (Action)
通信模式 异步多对多 同步一对一 异步+长期任务
时长 持续 短时 长时间(秒~分钟)
反馈 无 无 有(进度反馈)
取消 不支持 不支持 支持取消和抢占

知识点 5.1.2:动作通信时序图

Action Client                              Action Server
     │                                          │
     │ ──── Goal Request ◄── /send_goal ────   │
     │ ◄── Goal Accepted ◄── 服务 ──────────    │
     │                                          │ (开始执行)
     │ ◄── Feedback →→ /feedback 话题 →→─     │
     │ ◄── Feedback →→     ...       →→─     │
     │                                          │
     │ ──── Cancel Request  服务 ─────────►    │
     │ ◄── Cancel Done ◄── 服务 ───────────    │
     │                                          │
     │ ◄── Final Result ◄── 服务 ──────────    │

图 5-1:动作通信时序图。动作结合了话题(Feedback 流)和服务(Goal/Result/Cancel)的优势。


5.2 动作接口定义

知识点 5.2.1:.action 文件结构

# DoDishes.action — 洗碗任务定义
# Goal(目标)
uint32 total_dishes       # 需要洗的碗数量
---
# Result(结果)
uint32 cleaned_dishes     # 实际洗干净的碗数量
bool success              # 是否成功
---
# Feedback(反馈)
float32 progress          # 进度百分比 (0.0 ~ 1.0)
uint32 current_dish       # 当前正在洗的碗编号

三条 --- 分隔线分别定义:Goal(目标)、Result(结果)、Feedback(反馈)。

知识点 5.2.2:Action Server Python API

import rclpy
from rclpy.node import Node
from rclpy.action import ActionServer, GoalResponse, CancelResponse
from action_demo_interfaces.action import DoDishes
 
class DoDishesServer(Node):
    def __init__(self):
        super().__init__('do_dishes_server')
        self.action_server = ActionServer(
            self,                          # 所属节点
            DoDishes,                      # 动作类型
            'do_dishes',                   # 动作名称
            execute_callback=self.execute, # 执行回调
            goal_callback=self.goal,       # 目标接收回调
            cancel_callback=self.cancel)   # 取消回调
 
    def goal(self, goal_request):
        """接收目标时调用 — 决定是否接受"""
        self.get_logger().info(
            f'收到洗碗目标: {goal_request.total_dishes} 个盘子')
        return GoalResponse.ACCEPT         # 或 REJECT
 
    def cancel(self, goal_handle):
        """收到取消请求时调用"""
        self.get_logger().info('收到取消请求')
        return CancelResponse.ACCEPT
 
    async def execute(self, goal_handle):
        """执行目标 — 异步函数,支持 await"""
        total = goal_handle.request.total_dishes
        feedback_msg = DoDishes.Feedback()
 
        for i in range(1, total + 1):
            # 检查是否被取消
            if goal_handle.is_cancel_requested:
                goal_handle.canceled()
                result = DoDishes.Result()
                result.cleaned_dishes = i - 1
                result.success = False
                return result
 
            # 模拟任务执行(1秒/盘子)
            await asyncio.sleep(1.0)
            feedback_msg.progress = i / total
            feedback_msg.current_dish = i
            goal_handle.publish_feedback(feedback_msg)
            self.get_logger().info(
                f'进度: {feedback_msg.progress:.0%} ({i}/{total})')
 
        # 返回最终结果
        goal_handle.succeed()
        result = DoDishes.Result()
        result.cleaned_dishes = total
        result.success = True
        return result

程序 5-1:Action Server 完整示例。核心方法:execute(异步执行)、goal(接受/拒绝)、cancel(取消)。

知识点 5.2.3:Action Client Python API

from rclpy.action import ActionClient
 
class DoDishesClient(Node):
    def __init__(self):
        super().__init__('do_dishes_client')
        self.client = ActionClient(self, DoDishes, 'do_dishes')
 
    def send_goal(self, total_dishes):
        """发送目标到 Action Server"""
        # 等待 Server 上线
        self.client.wait_for_server()
        goal_msg = DoDishes.Goal()
        goal_msg.total_dishes = total_dishes
 
        # 发送目标,注册反馈和结果回调
        send_goal_future = self.client.send_goal_async(
            goal_msg,
            feedback_callback=self.feedback_callback)
        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('目标被拒绝')
            return
        self.get_logger().info('目标已接受,开始执行...')
        result_future = goal_handle.get_result_async()
        result_future.add_done_callback(self.result_callback)
 
    def feedback_callback(self, feedback_msg):
        """接收进度反馈的回调"""
        progress = feedback_msg.feedback.progress
        current = feedback_msg.feedback.current_dish
        self.get_logger().info(f'反馈: {progress:.0%} (盘子 {current})')
 
    def result_callback(self, future):
        """获取最终结果的回调"""
        result = future.result().result
        self.get_logger().info(
            f'完成: {result.cleaned_dishes} 个, 成功: {result.success}')
 
    def cancel_goal(self):
        """取消当前目标"""
        self.get_logger().info('请求取消...')
        self.goal_handle.cancel_goal_async()

程序 5-2:Action Client 完整示例。通过 send_goal_async 发送目标,feedback_callback 接收进度。

知识点 5.2.4:官方要点——动作模型与命令行工具

官方 Understanding ROS 2 actions 教程指出:动作(Action)是面向「长时间运行、可反馈、可取消」任务的通信模式,由目标(Goal)、反馈(Feedback)、结果(Result)三部分组成,底层实现为「一对服务 + 一对话题」。教程要求掌握的动作命令与本章 5.2 节一致:ros2 action list -t、ros2 action info、ros2 action send_goal。其中 send_goal 加 --feedback 参数可在命令行实时打印反馈,如斐波那契示例 ros2 action send_goal /fibonacci action_tutorials/action/Fibonacci "{order: 5}" --feedback。

Articulated Robotics 用「点外卖并要求送货跟踪」作比喻:下单是发送目标,外卖 App 的进度推送是反馈,餐到是结果;中途取消订单对应动作的取消机制。这个比喻有助于区分服务(问一次答一次)与动作(任务全程有往返)。

知识点 5.2.5:官方要点——编写动作服务器与客户端

官方 Python 教程的 Fibonacci 是标准范例:服务器继承 ActionServer,回调分解为三个钩子——goal_callback(接受/拒绝目标)、execute_callback(执行体,必须返回 GoalResponse/Result)、cancel_callback(响应取消请求);执行期间通过 goal_handle.publish_feedback() 周期发布反馈,用 rclpy.spin 驱动。客户端则用 ActionClient.wait_for_server() 等待可用,send_goal_async() 发送异步目标,并通过 add_done_callback 链式处理响应与结果。

教程逐步演进了三个版本:单目标顺序执行 → 多目标排队 → 支持中止与抢占。官方明确指出,execute 回调中的循环应周期性检查 goal_handle.is_cancel_requested,收到取消后设置 goal_handle.canceled() 并返回 ABORTED/CANCELED 结果——这正是本章 5.3.2 节抢占逻辑的官方出处。

知识点 5.2.6:官方要点——自定义动作接口

Creating an action 教程演示了 .action 文件的定义:三段式结构 # Goal\n---\n# Result\n---\n# Feedback,各段间以 --- 分隔,例如 uint32 order\n---\nint32[] sequence\n---\nint32[] partial_sequence。这与本章 5.2.1 节的 .action 文件定义完全一致。接口包需在 package.xml 中声明 rosidl_default_generators,编译后即可用 ros2 interface show action_tutorials/action/Fibonacci 验证。

一个易错点:.action 文件必须放在接口包的 action/ 目录下,且包类型须为纯接口包(package.xml 中 rosidl_default_generators + rosidl_default_runtime + member_groups_group 三件套),否则编译期报「未找到 action 生成器」。The Construct 的课程建议把 .msg、.srv、.action 统一放在一个 interfaces 包中管理,便于跨项目复用。


5.3 取消与抢占机制

知识点 5.3.1:取消流程

Client 发送取消 ──► Server.execute() 中检测 is_cancel_requested
                        ├── True  → goal_handle.canceled() → return
                        └── False → 继续执行

知识点 5.3.2:抢占机制

# 发送新目标时,可选择抢占旧目标
# Server 端通过 GoalResponse.REJECT 拒绝旧目标
def goal(self, goal_request):
    # 如果有正在执行的目标,拒绝新目标
    if self.current_goal is not None:
        self.get_logger().info('已有目标在执行,拒绝新目标')
        return GoalResponse.REJECT
    return GoalResponse.ACCEPT

知识点 5.3.3:官方要点——反馈、取消与抢占的工程实践

在真实机器人上,动作的典型应用是导航(Nav2 的 navigate_to_pose)、机械臂运动(MoveIt 2 的执行接口)与 SLAM 建图(slam_toolbox 的异步建图)。这些系统共同体现了三个工程准则:其一,反馈频率要适中(一般 1–10 Hz),过低客户端无进度感,过高则挤占通信带宽;其二,抢占语义必须明确——新目标到来时旧目标是中止还是排队,要在文档中写清楚,本章 FibonacciServer 采用的「单执行线程 + 排队」与 Nav2 的「抢占式」是两种典型选择;其三,客户端要处理全部三种终态(SUCCEEDED、CANCELED、ABORTED),官方教程的 get_result_callback 正是这样写的。

建议读者将本章练习 5.4 的抢占测试与 Nav2 源码(nav2_simple_commander)对照阅读,体会同一套动作机制在工业级系统中的封装方式。


5.4 本章小结

动作通信可以概括为五个要点:动作通信 = 服务(Goal/Result/Cancel)+ 话题(Feedback),适用于长时间任务;.action 文件分 Goal / Result / Feedback 三部分;ActionServer 核心方法为 execute(异步)、goal、cancel;ActionClient 通过 send_goal_async 发送目标,feedback_callback 接收进度;支持取消(cancel_goal_async)和抢占(GoalResponse.REJECT)。


5.5 练习题

练习 5.1:基于 action_demo_interfaces/action/DoDishes 编写完整的 Action Server 和 Client。

练习 5.2:设计自定义 Action Tracking.action(Goal:目标ID, speed; Result:成功与否; Feedback:当前位置、距离)。

练习 5.3:在 Client 中实现中途取消:发送目标后等待 3 秒,然后发送取消请求。

练习 5.4:测试抢占行为:Server 执行中收到新目标时的处理策略。

练习 5.5:使用 ros2 action 命令行工具查看动作列表、发送目标和获取结果。


仿真结合实例(当前仓库):用 Action 编排一次巡检任务

目标与知识点对应

动作适合持续时间较长、需要反馈和最终结果的任务。本实例启动移动机器人仿真作为任务背景,同时运行 DoDishes Action Server/Client,观察目标接受、百分比反馈和最终结果,理解 Action 与 Gazebo 传感器 Topic 的分工。

运行步骤

# 工作区根目录;三个终端均先执行
source /opt/ros/jazzy/setup.bash
source install/setup.bash
# 终端 1:启动机器人仿真
ros2 launch robot_sim_demo gazebo2.launch.py gui:=false rviz:=false drive:=false
# 终端 2:启动 Action Server
ros2 run action_demo_cpp dishes_server
# 终端 3:发送目标并查看动作状态
ros2 run action_demo_cpp dishes_client
ros2 action list
ros2 action info /dishes

观察结果

运行后应看到三类现象:客户端依次收到 20% 到 100% 的反馈,并得到清洗数量结果;ros2 action info /dishes 显示 Action Server 和 Client,同时 Gazebo 继续提供 /odom、/scan 等连续数据;使用 action_demo_py 的同名入口重做一次,可比较 C++ 与 Python 的动作回调实现。

源码与边界

C++ 实现位于 src/action_demo_cpp/src/dishes_server.cpp、dishes_client.cpp;Python 实现位于 src/action_demo_py/action_demo_py/dishes_server.py、dishes_client.py;接口定义位于 src/action_demo_interfaces/;仿真位于 src/robot_sim_demo/。

本例的动作任务是可重复的教学任务,不会直接驱动 Gazebo 底盘;若要控制机器人,仍需另行向 /cmd_vel 发布 geometry_msgs/msg/Twist。

ch05 动作通信运行输出


学习材料: