ROS2系列教程:Action动作通信(带反馈长任务)
本文摘要
动作接口用.action文件定义,三部分用---# action/CountUntil.action —— 计数到目标值int32 target_number #…
本文是 ROS2 系列教程的第 10 篇
本文是 ROS2 系列教程的第 10 篇:Action 动作通信——带反馈的长任务。话题是单向数据流,服务是一问一答,而机器人世界里还有一类需求:“导航到目标点”“机械臂抓取物体”“执行建图”——它们耗时长、需要进度反馈、还可能要中途取消。这就是动作(Action)的用武之地。本篇文章讲透 Goal/Feedback/Result 三通道模型,用 Python 与 C++ 实现动作服务端与客户端,掌握
ros2 action命令,并完成三种通信方式的选型总结。
一、动作通信模型
1.1 为什么需要 Action
用一个例子理解:让机器人“导航到厨房”。
- 话题方案:发一个目标点话题,然后就不知道了——没有完成确认,没有进度。
- 服务方案:调一个服务“导航到(厨房)”,服务端算完才返回——调用期间完全阻塞,无法取消,也没有中间反馈。
- 动作方案:客户端发目标 → 服务端持续回报“已走 30%、还有 5 米”→ 完成时返回最终结果 → 随时可取消。完美契合。
动作是为”长任务 + 进度反馈 + 可取消“设计的通信原语,是导航(Nav2)、机械臂(MoveIt2)等核心系统的标准接口。
1.2 三通道模型
一个动作由三个通道组成(对应三个话题 + 一个服务):
客户端 服务端 │ 1. 发送目标 Goal ──────────────────▶ 接受/拒绝目标 │ 2. ◀────────────── 持续反馈 Feedback 报告进度 │ 3. ◀────────────────── 最终结果 Result 任务完成 │ 4. 发送取消请求 Cancel ────────────▶ 终止任务| 通道 | 方向 | 内容 | 类比 |
|---|---|---|---|
| Goal | 客户端→服务端 | 任务目标(如目标坐标) | 下达任务 |
| Feedback | 服务端→客户端 | 进度信息(持续发送) | 汇报进度 |
| Result | 服务端→客户端 | 最终结果(一次) | 交付成果 |
| Cancel | 客户端→服务端 | 取消请求 | 中止任务 |
实现上:Goal 用服务传输(保证送达),Feedback/Result 用话题传输,Cancel 用服务——但用户不需要关心底层,API 已封装。
1.3 动作接口定义(.action 文件)
动作接口用 .action 文件定义,三部分用 --- 分隔:
# action/CountUntil.action —— 计数到目标值int32 target_number # 目标(Goal):要数到几---int32 current_number # 结果(Result):最终数到几---int32 current_number # 反馈(Feedback):当前数到几定义在接口包里(如 my_interfaces),构建后生成对应语言的类型。
二、Python 动作服务端
2.1 简单计数器动作服务端
用 ROS2 自带的 example_interfaces/action/Fibonacci(斐波那契数列)演示:
# py_pkg/py_action_server.py —— Python 动作服务端import rclpyimport timefrom rclpy.node import Nodefrom rclpy.action import ActionServerfrom example_interfaces.action import Fibonacci
class FibonacciServer(Node): def __init__(self): super().__init__('fibonacci_server') # 创建动作服务端:动作类型 + 动作名 + 三个回调 self.action_server = ActionServer( self, Fibonacci, 'fibonacci', self.execute_callback) # 执行回调(核心) self.get_logger().info('动作服务端就绪: /fibonacci')
def execute_callback(self, goal_handle): # 1. 接受目标 self.get_logger().info(f'收到目标: {goal_handle.request.order}') feedback_msg = Fibonacci.Feedback()
# 2. 计算斐波那契数列,持续发反馈 sequence = [0, 1] for i in range(1, goal_handle.request.order): sequence.append(sequence[i] + sequence[i-1]) feedback_msg.sequence = sequence goal_handle.publish_feedback(feedback_msg) # 发反馈 time.sleep(0.5) # 模拟耗时
# 3. 完成:返回结果 goal_handle.succeed() result = Fibonacci.Result() result.sequence = sequence return result
def main(): rclpy.init() node = FibonacciServer() rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()服务端三个关键回调:
- execute_callback:收到目标后执行(可同步写耗时逻辑,执行器会调度)。
- publish_feedback:随时发布进度。
- succeed()/abort():标记任务成功/失败,然后返回 Result。
2.2 支持取消的动作服务端
# py_pkg/cancellable_server.py —— 可取消的动作服务端import rclpy, timefrom rclpy.node import Nodefrom rclpy.action import ActionServer, CancelResponse, GoalResponsefrom example_interfaces.action import Fibonacci
class CancellableServer(Node): def __init__(self): super().__init__('cancellable_server') self.action_server = ActionServer( self, Fibonacci, 'fibonacci', execute_callback=self.execute_callback, goal_callback=self.goal_callback, # 目标接受回调 cancel_callback=self.cancel_callback, # 取消回调 )
def goal_callback(self, goal_request): # 可在此校验目标:拒绝非法目标 self.get_logger().info(f'收到目标 order={goal_request.order}') if goal_request.order <= 0: self.get_logger().warn('拒绝非法目标(order<=0)') return GoalResponse.REJECT return GoalResponse.ACCEPT
def cancel_callback(self, goal_handle): self.get_logger().info('收到取消请求,同意取消') return CancelResponse.ACCEPT
def execute_callback(self, goal_handle): feedback_msg = Fibonacci.Feedback() sequence = [0, 1] for i in range(1, goal_handle.request.order): # 检查是否被取消 if goal_handle.is_cancel_requested: goal_handle.canceled() # 标记取消 self.get_logger().info('任务已取消') return Fibonacci.Result() # 返回空结果 sequence.append(sequence[i] + sequence[i-1]) feedback_msg.sequence = sequence goal_handle.publish_feedback(feedback_msg) time.sleep(0.5) goal_handle.succeed() result = Fibonacci.Result() result.sequence = sequence return result
def main(): rclpy.init() node = CancellableServer() rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()取消处理三步:
cancel_callback决定是否同意取消(返回CancelResponse.ACCEPT)。- 执行循环里用
goal_handle.is_cancel_requested检查是否被请求取消。 - 同意后调
goal_handle.canceled()标记状态并返回。
三、Python 动作客户端
3.1 基础客户端
# py_pkg/py_action_client.py —— Python 动作客户端import rclpyfrom rclpy.node import Nodefrom rclpy.action import ActionClientfrom example_interfaces.action import Fibonacci
class FibonacciClient(Node): def __init__(self): super().__init__('fibonacci_client') # 创建动作客户端 self.action_client = ActionClient(self, Fibonacci, 'fibonacci')
def send_goal(self, order): # 1. 等待服务端 self.action_client.wait_for_server() # 2. 构造目标 goal_msg = Fibonacci.Goal() goal_msg.order = order self.get_logger().info(f'发送目标: order={order}') # 3. 发送目标(异步),注册反馈回调 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 feedback_callback(self, feedback_msg): # 进度反馈(服务端 publish_feedback 时触发) fb = feedback_msg.feedback self.get_logger().info(f'进度: {list(fb.sequence)}')
def goal_response_callback(self, future): goal_handle = future.result() if not goal_handle.accepted: self.get_logger().error('目标被拒绝') return self.get_logger().info('目标已接受,等待结果...') # 5. 请求结果(异步) self.get_result_future = goal_handle.get_result_async() self.get_result_future.add_done_callback(self.result_callback)
def result_callback(self, future): result = future.result().result self.get_logger().info(f'最终结果: {list(result.sequence)}') # 关闭节点 rclpy.shutdown()
def main(): rclpy.init() node = FibonacciClient() node.send_goal(10) # 数到 10 rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()客户端四步流程:
send_goal_async(目标, feedback_callback)发送目标。- 目标响应回调里检查
accepted(被接受/拒绝)。 - 接受后
get_result_async()请求最终结果。 result_callback拿到最终结果。
3.2 可取消客户端
# py_pkg/cancellable_client.py —— 可取消客户端import rclpyfrom rclpy.node import Nodefrom rclpy.action import ActionClientfrom example_interfaces.action import Fibonacciimport threading, time
class CancellableClient(Node): def __init__(self): super().__init__('cancellable_client') self.action_client = ActionClient(self, Fibonacci, 'fibonacci') self.goal_handle = None
def send_goal(self, order): self.action_client.wait_for_server() goal_msg = Fibonacci.Goal() goal_msg.order = order future = self.action_client.send_goal_async( goal_msg, feedback_callback=self.feedback_callback) future.add_done_callback(self.on_goal_response)
def feedback_callback(self, feedback_msg): self.get_logger().info(f'进度: {list(feedback_msg.feedback.sequence)}')
def on_goal_response(self, future): self.goal_handle = future.result() if not self.goal_handle.accepted: self.get_logger().error('目标被拒绝') return self.get_logger().info('目标已接受,2 秒后取消...') # 2 秒后发起取消 timer = threading.Timer(2.0, self.cancel_goal) timer.start()
def cancel_goal(self): self.get_logger().info('发送取消请求...') cancel_future = self.goal_handle.cancel_goal_async() cancel_future.add_done_callback(self.on_cancel_done)
def on_cancel_done(self, future): cancel_response = future.result() if len(cancel_response.goals_canceling) > 0: self.get_logger().info('取消成功') else: self.get_logger().warn('取消未生效')
def main(): rclpy.init() node = CancellableClient() node.send_goal(10) rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()四、C++ 动作服务端与客户端
4.1 C++ 动作服务端
#include "rclcpp/rclcpp.hpp"#include "rclcpp_action/rclcpp_action.hpp"#include "example_interfaces/action/fibonacci.hpp"#include <chrono>#include <memory>
using Fibonacci = example_interfaces::action::Fibonacci;using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>;
class FibonacciServer : public rclcpp::Node{public: FibonacciServer() : Node("fibonacci_server_cpp") { action_server_ = rclcpp_action::create_server<Fibonacci>( this, "fibonacci", std::bind(&FibonacciServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2), std::bind(&FibonacciServer::handle_cancel, this, std::placeholders::_1), std::bind(&FibonacciServer::handle_accepted, this, std::placeholders::_1)); RCLCPP_INFO(this->get_logger(), "动作服务端就绪"); }
private: // 目标回调:校验并决定接受/拒绝 rclcpp_action::GoalResponse handle_goal( const rclcpp_action::GoalUUID& uuid, std::shared_ptr<const Fibonacci::Goal> goal) { RCLCPP_INFO(this->get_logger(), "收到目标 order=%d", goal->order); if (goal->order <= 0) return rclcpp_action::GoalResponse::REJECT; return rclcpp_action::GoalResponse::ACCEPT; }
// 取消回调 rclcpp_action::CancelResponse handle_cancel( const std::shared_ptr<GoalHandleFibonacci> goal_handle) { RCLCPP_INFO(this->get_logger(), "收到取消请求"); return rclcpp_action::CancelResponse::ACCEPT; }
// 接受后:开线程执行(不能阻塞回调) void handle_accepted(const std::shared_ptr<GoalHandleFibonacci> goal_handle) { std::thread(std::bind(&FibonacciServer::execute, this, goal_handle)).detach(); }
// 执行逻辑 void execute(const std::shared_ptr<GoalHandleFibonacci> goal_handle) { auto feedback = std::make_shared<Fibonacci::Feedback>(); auto result = std::make_shared<Fibonacci::Result>(); std::vector<int> seq{0, 1}; for (int i = 1; i < goal_handle->get_goal()->order; i++) { seq.push_back(seq[i] + seq[i-1]); feedback->sequence = seq; goal_handle->publish_feedback(feedback); std::this_thread::sleep_for(std::chrono::milliseconds(500)); } goal_handle->succeed(); result->sequence = seq; goal_handle->publish_result(result); }
rclcpp_action::Server<Fibonacci>::SharedPtr action_server_;};
int main(int argc, char** argv){ rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<FibonacciServer>()); rclcpp::shutdown(); return 0;}C++ 关键差异:handle_accepted 里必须开新线程执行任务(不能阻塞执行器回调线程);三个回调分别对应 Goal 接受、取消、执行。
4.2 C++ 动作客户端
#include "rclcpp/rclcpp.hpp"#include "rclcpp_action/rclcpp_action.hpp"#include "example_interfaces/action/fibonacci.hpp"
using Fibonacci = example_interfaces::action::Fibonacci;
class FibonacciClient : public rclcpp::Node{public: FibonacciClient() : Node("fibonacci_client_cpp") { client_ = rclcpp_action::create_client<Fibonacci>(this, "fibonacci"); }
void send_goal(int order) { // 1. 等服务端 if (!client_->wait_for_action_server()) { RCLCPP_ERROR(this->get_logger(), "动作服务端不可用"); return; } // 2. 构造目标 auto goal = Fibonacci::Goal(); goal.order = order; // 3. 发送目标(异步),注册反馈与响应回调 auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions(); send_goal_options.goal_response_callback = std::bind(&FibonacciClient::goal_response_callback, this, std::placeholders::_1); send_goal_options.feedback_callback = std::bind(&FibonacciClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2); send_goal_options.result_callback = std::bind(&FibonacciClient::result_callback, this, std::placeholders::_1); client_->async_send_goal(goal, send_goal_options); }
private: void goal_response_callback(const GoalHandleFibonacci::SharedPtr& goal_handle) { if (!goal_handle) { RCLCPP_ERROR(this->get_logger(), "目标被拒绝"); } else { RCLCPP_INFO(this->get_logger(), "目标已接受"); } }
void feedback_callback(GoalHandleFibonacci::SharedPtr, const std::shared_ptr<const Fibonacci::Feedback> feedback) { RCLCPP_INFO(this->get_logger(), "进度: %zu 项", feedback->sequence.size()); }
void result_callback(const GoalHandleFibonacci::WrappedResult& result) { if (result.code == rclcpp_action::ResultCode::SUCCEEDED) { RCLCPP_INFO(this->get_logger(), "任务成功"); } }
rclcpp_action::Client<Fibonacci>::SharedPtr client_;};
int main(int argc, char** argv){ rclcpp::init(argc, argv); auto node = std::make_shared<FibonacciClient>(); node->send_goal(10); rclcpp::spin(node); rclcpp::shutdown(); return 0;}五、自定义动作接口实战
5.1 定义动作接口
在 my_interfaces 包加 action/NavigateTo.action:
float32 x # 目标 x 坐标float32 y # 目标 y 坐标---bool reached # 是否到达float32 final_distance # 最终距离---float32 distance_left # 剩余距离float32 speed # 当前速度修改 CMakeLists.txt:
set(action_files "action/NavigateTo.action")rosidl_generate_interfaces(${PROJECT_NAME} ${msg_files} ${srv_files} ${action_files} # 加入动作文件 DEPENDENCIES builtin_interfaces)重建并验证:
colcon build --packages-select my_interfacessource install/setup.bashros2 interface show my_interfaces/action/NavigateTo5.2 导航动作服务端(模拟)
# py_pkg/nav_action_server.py —— 模拟导航动作import rclpy, math, timefrom rclpy.node import Nodefrom rclpy.action import ActionServerfrom my_interfaces.action import NavigateTo
class NavServer(Node): def __init__(self): super().__init__('nav_server') self.action_server = ActionServer( self, NavigateTo, 'navigate_to', self.execute_callback) self.get_logger().info('导航动作服务端就绪')
def execute_callback(self, goal_handle): goal = goal_handle.request self.get_logger().info(f'导航到 ({goal.x}, {goal.y})') feedback = NavigateTo.Feedback() # 模拟:机器人从 (0,0) 以 1m/s 直线运动 total = math.sqrt(goal.x**2 + goal.y**2) distance_left = total while distance_left > 0.05: distance_left -= 0.2 feedback.distance_left = max(distance_left, 0.0) feedback.speed = 1.0 goal_handle.publish_feedback(feedback) time.sleep(0.2) goal_handle.succeed() result = NavigateTo.Result() result.reached = True result.final_distance = 0.0 return result
def main(): rclpy.init() node = NavServer() rclpy.spin(node) node.destroy_node() rclpy.shutdown()
if __name__ == '__main__': main()5.3 命令行调用动作
# 查看动作列表ros2 action listros2 action list -t
# 查看动作类型信息ros2 action info /navigate_to
# 发送动作目标(命令行)ros2 action send_goal /navigate_to my_interfaces/action/NavigateTo \ "{x: 5.0, y: 3.0}"
# 带反馈显示ros2 action send_goal /navigate_to my_interfaces/action/NavigateTo \ "{x: 5.0, y: 3.0}" --feedback
# 显示动作接口ros2 interface show my_interfaces/action/NavigateTo注意:命令行 ros2 action send_goal 会阻塞等待结果;--feedback 显示实时反馈;Ctrl+C 可取消。
六、三种通信方式选型总结
| 维度 | 话题 Topic | 服务 Service | 动作 Action |
|---|---|---|---|
| 通信模式 | 单向流 | 请求-应答 | 目标-反馈-结果 |
| 是否阻塞 | 否 | 客户端等待 | 客户端异步 |
| 进度反馈 | 无 | 无 | 有 |
| 取消支持 | 不适用 | 无 | 有 |
| 适用任务 | 持续数据 | 即时问答 | 长任务 |
| 典型场景 | 传感器、指令 | 查询、设置 | 导航、抓取、建图 |
| 接口定义 | .msg | .srv | .action(3段) |
| 底层实现 | 话题 | 服务 | 话题+服务组合 |
选型口诀:
持续流动 → 话题一问一答 → 服务长任务+进度+可取消 → 动作动作的使用误区:
- 别用动作做毫秒级操作(开销大,杀鸡用牛刀)。
- 别在 execute_callback 里做无限循环(要有退出条件与取消检查)。
- 客户端别忘记注册 feedback_callback 和 result_callback。
七、总结
本篇文章完整覆盖了动作通信:理解了三通道模型与适用场景、动作接口定义(.action 三段式),用 Python 与 C++ 实现了动作服务端(含可取消版本)与客户端(含取消流程),实战了自定义导航动作,掌握了 ros2 action 命令行工具,并完成了话题/服务/动作三大通信方式的选型总结。
关键要点回顾:
- 动作 = Goal(目标)+ Feedback(反馈)+ Result(结果)+ Cancel(取消)。
.action文件三段式定义接口,放在接口包的action/目录。- 服务端核心:execute_callback + publish_feedback + succeed()/canceled()。
- 取消三步:cancel_callback 同意 → is_cancel_requested 检查 → canceled() 标记。
- 客户端四步:send_goal_async → goal_response_callback → get_result_async → result_callback。
- C++ 服务端要在 handle_accepted 里开线程执行任务,避免阻塞执行器。
- 长任务带反馈可取消用动作;持续数据用话题;即时问答用服务。
下一篇预告
下一篇进入 launch 文件与多节点启动:手动开几十个终端运行节点太原始了。本篇文章讲透 launch——用 Python 编写 launch 文件启动多节点、传递参数、设置条件与复用、包含其他 launch 文件,掌握 ros2 launch 命令与 launch 调试技巧。学完你就能一键启动整个机器人系统。
ROS2系列教程:Action动作通信(带反馈长任务)
作者:Wang Shijie
本文链接:https://wsj-blog.pages.dev/posts/ros2-xi-lie-jiao-cheng-action-dong-zuo-tong-xin-dai-fan-kui/
本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论
评论资源加载失败,请重试。