ROS2 Action通信:C++实现长时任务管理与反馈机制

📅 2026/8/13 23:17:25
ROS2 Action通信:C++实现长时任务管理与反馈机制
1. 从服务到动作为什么ROS2需要Action如果你用过ROS2的服务Service可能会觉得它挺方便的客户端发个请求服务端给个响应一来一回同步完成。但当你需要让机器人去执行一个耗时较长的任务比如“移动到(x, y)点”事情就变得棘手了。服务调用会一直阻塞直到任务完成或超时。在这漫长的几秒甚至几十秒里你的主程序啥也干不了更无法得知机器人是卡住了还是正在顺利移动。你可能会想那我开个线程去等不就行了但线程管理、状态同步、取消任务、获取中间进度……这些自己实现起来又是一堆麻烦。ROS2的动作Action就是为了解决这类“长时、可反馈、可取消”的任务而生的。你可以把它理解为“加强版的服务”。它底层基于话题Topic实现但封装了一套更完善的通信协议。一个Action交互包含三个角色Action Client动作客户端、Action Server动作服务器和Action Goal目标、Feedback反馈、Result结果三种消息。客户端发送一个目标Goal给服务器服务器开始执行。在执行过程中服务器可以持续地向客户端发送反馈Feedback比如“已移动30%”。客户端可以随时发送取消请求。最终服务器完成任务后会发送一个最终结果Result。这次我们就用C手把手实现一个最简单的Action通信示例一个模拟的“计数”服务器。客户端设定一个目标数字服务器从0开始累加每秒反馈当前数值累加到目标值后返回结果。通过这个例子你会彻底搞懂Action的代码骨架、编译运行方法以及那些官方文档里可能不会细说的调试技巧和常见坑位。2. 项目结构与依赖准备CMakeLists.txt与package.xml的配置要点在开始写代码前得先把房子工作空间和蓝图编译配置搭好。假设你的ROS2版本是Humble或Iron并且已经配置好了基础环境。首先创建一个功能包。我习惯将所有Action相关的定义、服务器和客户端代码放在一个包里结构清晰。# 在你的工作空间src目录下比如 ~/ros2_ws/src ros2 pkg create cpp_action_demo --build-type ament_cmake --dependencies rclcpp rclcpp_action rclcpp_components example_interfaces这里的关键依赖是rclcpp_action它提供了Action的C客户端和服务器接口。example_interfaces包里有我们即将用到的Fibonacci.action接口但为了彻底理解我们先从自定义Action接口开始。不过为了让第一个例子足够简单我们这次先使用ROS2内置的一个简单Action接口example_interfaces/action/Fibonacci。它是一个计算斐波那契数列的Action我们稍加改造用来模拟计数。现在看一下自动生成的package.xml确保依赖项都已包含?xml version1.0? ?xml-model hrefhttp://download.ros.org/schema/package_format3.xsd schematypenshttp://www.w3.org/2001/XMLSchema? package format3 namecpp_action_demo/name version0.0.0/version descriptionTODO: Package description/description maintainer emailyouexample.comYour Name/maintainer licenseTODO: License declaration/license buildtool_dependament_cmake/buildtool_depend dependrclcpp/depend dependrclcpp_action/depend dependrclcpp_components/depend dependexample_interfaces/depend test_dependament_lint_auto/test_depend test_dependament_lint_common/test_depend export build_typeament_cmake/build_type /export /package重点是CMakeLists.txt。自动生成的版本需要添加可执行文件的编译目标。一个常见的错误是忘记链接rclcpp_action库。我们来修改它cmake_minimum_required(VERSION 3.8) project(cpp_action_demo) # 默认使用C17标准ROS2 Humble之后推荐使用 if(NOT CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) endif() # 查找依赖包 find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_action REQUIRED) find_package(example_interfaces REQUIRED) # 声明可执行文件Action服务器 add_executable(action_server src/action_server.cpp) ament_target_dependencies(action_server rclcpp rclcpp_action example_interfaces ) # 安装到lib/pkg_name目录 install(TARGETS action_server DESTINATION lib/${PROJECT_NAME} ) # 声明可执行文件Action客户端 add_executable(action_client src/action_client.cpp) ament_target_dependencies(action_client rclcpp rclcpp_action example_interfaces ) install(TARGETS action_client DESTINATION lib/${PROJECT_NAME} ) # 安装启动文件如果有的话可以跳过 install(DIRECTORY launch DESTINATION share/${PROJECT_NAME} ) # 导出包含路径确保其他包能找到本包的头文件本例中无自定义头文件但好习惯是加上 ament_export_include_directories(include) ament_export_dependencies( rclcpp rclcpp_action example_interfaces ) # 最后调用ament_package()必须 ament_package()注意ament_target_dependencies这一行它确保了编译时和运行时都能找到正确的库。很多编译错误比如找不到rclcpp_action::Client之类的类型就是因为这里漏了依赖。3. Action服务器实现详解从接收到执行的全过程接下来是重头戏Action服务器的C实现。我们创建一个src/action_server.cpp文件。Action服务器的核心是继承自rclcpp::Node并创建一个rclcpp_action::Server对象。这个Server需要处理三种回调目标处理回调Handle Goal当客户端发送一个新目标时触发。在这里你可以决定是否接受这个目标。比如如果服务器正忙可以拒绝。取消处理回调Handle Cancel当客户端请求取消当前执行的目标时触发。你需要在这里处理取消逻辑并返回一个状态告诉系统取消是否被接受。执行回调Execute这是任务执行的主函数。它在一个独立的线程中运行默认由rclcpp_action::Server管理不会阻塞服务器的其他回调比如定时器或其他订阅者。在这里你要执行实际的任务并定期发布反馈最终设置结果。下面我们结合代码一步步拆解#include rclcpp/rclcpp.hpp #include rclcpp_action/rclcpp_action.hpp #include example_interfaces/action/fibonacci.hpp #include memory #include thread #include chrono using namespace std::chrono_literals; using Fibonacci example_interfaces::action::Fibonacci; using GoalHandleFibonacci rclcpp_action::ServerGoalHandleFibonacci; class FibonacciActionServer : public rclcpp::Node { public: // 构造函数中初始化Action Server FibonacciActionServer() : Node(fibonacci_action_server) { // 使用 create_server 模板函数创建服务器 // 第一个参数是服务器对象指针 // 第二个参数是Action名称客户端将通过这个名称来连接 // 第三个是目标处理回调绑定到本类的handle_goal方法 // 第四个是取消处理回调绑定到本类的handle_cancel方法 // 第五个是执行回调绑定到本类的handle_accepted方法 this-action_server_ rclcpp_action::create_serverFibonacci( this, fibonacci, // Action名 std::bind(FibonacciActionServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2), std::bind(FibonacciActionServer::handle_cancel, this, std::placeholders::_1), std::bind(FibonacciActionServer::handle_accepted, this, std::placeholders::_1)); RCLCPP_INFO(this-get_logger(), Fibonacci Action Server 已启动等待目标...); } private: rclcpp_action::ServerFibonacci::SharedPtr action_server_; // 1. 处理新目标决定是否接受 rclcpp_action::GoalResponse handle_goal( const rclcpp_action::GoalUUID uuid, std::shared_ptrconst Fibonacci::Goal goal) { // 这个uuid是系统为每个目标生成的唯一标识符可用于日志追踪 (void)uuid; // 显式忽略未使用参数警告 RCLCPP_INFO(this-get_logger(), 收到新目标计算阶数为 %d 的斐波那契数列, goal-order); // 简单的验证逻辑防止阶数过大导致计算时间过长或溢出 if (goal-order 20) { RCLCPP_WARN(this-get_logger(), 目标阶数 %d 过大拒绝执行。, goal-order); return rclcpp_action::GoalResponse::REJECT; } // 还可以检查服务器是否正在执行其他任务这里我们假设一次只处理一个目标 RCLCPP_INFO(this-get_logger(), 目标被接受。); return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; // 接受并立即执行 } // 2. 处理取消请求 rclcpp_action::CancelResponse handle_cancel( const std::shared_ptrGoalHandleFibonacci goal_handle) { RCLCPP_INFO(this-get_logger(), 收到取消请求。); // 在实际应用中这里应该设置一个标志位让执行线程安全地停止。 // 为了简单我们直接返回接受取消。 (void)goal_handle; return rclcpp_action::CancelResponse::ACCEPT; } // 3. 一旦目标被接受就调用此函数。它负责启动执行线程。 void handle_accepted(const std::shared_ptrGoalHandleFibonacci goal_handle) { // 这里使用std::thread启动一个新线程来执行任务。 // 注意必须将goal_handle的值捕获到lambda中不能直接使用this-goal_handle之类的成员变量 // 因为多个目标可能同时被接受虽然我们逻辑上串行处理。 std::thread{std::bind(FibonacciActionServer::execute, this, std::placeholders::_1), goal_handle}.detach(); } // 4. 真正的执行函数 void execute(const std::shared_ptrGoalHandleFibonacci goal_handle) { RCLCPP_INFO(this-get_logger(), 开始执行计算...); rclcpp::Rate loop_rate(1); // 设置反馈频率1Hz const auto goal goal_handle-get_goal(); // 获取目标消息 auto feedback std::make_sharedFibonacci::Feedback(); // 创建反馈消息 auto result std::make_sharedFibonacci::Result(); // 创建结果消息 // 初始化斐波那契数列的前两个数 int a 0, b 1; feedback-sequence.clear(); feedback-sequence.push_back(a); feedback-sequence.push_back(b); // 循环计算直到达到目标阶数或被取消 for (int i 1; i goal-order rclcpp::ok(); i) { // 检查是否被取消 if (goal_handle-is_canceling()) { result-sequence feedback-sequence; goal_handle-canceled(result); RCLCPP_INFO(this-get_logger(), 任务被取消。); return; } // 计算下一个数并加入反馈序列 int next a b; feedback-sequence.push_back(next); a b; b next; // 发布反馈 goal_handle-publish_feedback(feedback); RCLCPP_INFO(this-get_logger(), 发布反馈: 已计算 %zu 个数, feedback-sequence.size()); // 模拟耗时计算 loop_rate.sleep(); } // 任务完成设置结果 result-sequence feedback-sequence; goal_handle-succeed(result); RCLCPP_INFO(this-get_logger(), 任务成功完成最终序列长度: %zu, result-sequence.size()); } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node std::make_sharedFibonacciActionServer(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }几个关键点与避坑经验GoalResponse的返回值ACCEPT_AND_EXECUTE表示接受并立即执行。还有一个ACCEPT_AND_DEFER表示接受但延迟执行需要你之后手动调用goal_handle-execute()。对于大多数简单场景直接用ACCEPT_AND_EXECUTE就行。执行线程的分离在handle_accepted中我们创建了一个新线程并调用detach()。这意味着主线程rclcpp::spin不会等待这个线程结束。这是标准做法确保服务器能继续响应其他请求比如新的目标或取消请求。切记不要在这个线程里调用rclcpp::spin否则会导致多个spin冲突。资源管理与线程安全这个简单示例一次只处理一个目标。如果设计成并发处理多个目标你需要更复杂的逻辑来管理goal_handle和共享数据避免竞争条件。通常可以为每个目标创建一个独立的std::thread或使用线程池。反馈与结果的消息类型feedback和result都是std::shared_ptr类型指向Action定义中对应的消息结构Fibonacci::Feedback和Fibonacci::Result。你需要仔细查看Action的.msg定义知道里面有哪些字段可以填充。比如Fibonacci.action中Feedback有一个int32[] sequence字段Result也有一个int32[] sequence字段。循环中的rclcpp::ok()这是一个好习惯在长时间循环中检查ROS2系统是否还在正常运行比如没有被CtrlC中断。如果系统关闭了循环应该退出。4. Action客户端实现详解发送、监控与取消服务器准备好了现在需要一个客户端来驱动它。创建src/action_client.cpp。Action客户端的流程比服务器稍简单但异步操作是其核心也是容易迷惑的地方。我们将实现一个客户端它连接到指定的Action服务器。发送一个目标例如计算阶数为10的斐波那契数列。监听反馈并打印出来。等待结果并处理完成成功、取消、中止的情况。提供一个简单的超时机制。我们将使用异步方式这是ROS2 Action客户端推荐的做法因为它不会阻塞你的主线程。#include rclcpp/rclcpp.hpp #include rclcpp_action/rclcpp_action.hpp #include example_interfaces/action/fibonacci.hpp #include chrono #include functional #include future #include memory using namespace std::chrono_literals; using Fibonacci example_interfaces::action::Fibonacci; using GoalHandleFibonacci rclcpp_action::ClientGoalHandleFibonacci; class FibonacciActionClient : public rclcpp::Node { public: FibonacciActionClient() : Node(fibonacci_action_client) { // 创建Action客户端指定Action类型和名称必须与服务器一致 this-client_ptr_ rclcpp_action::create_clientFibonacci( this, fibonacci); // Action名称 RCLCPP_INFO(this-get_logger(), Fibonacci Action Client 已创建。); } // 发送目标的公有方法 void send_goal(int order) { // 等待Action服务器上线 if (!client_ptr_-wait_for_action_server(5s)) { RCLCPP_ERROR(this-get_logger(), Action服务器未在5秒内响应。); return; } // 构造目标消息 auto goal_msg Fibonacci::Goal(); goal_msg.order order; RCLCPP_INFO(this-get_logger(), 发送目标计算阶数为 %d 的斐波那契数列, order); // 设置发送目标的选项 auto send_goal_options rclcpp_action::ClientFibonacci::SendGoalOptions(); // 设置反馈回调当服务器发布反馈时触发 send_goal_options.feedback_callback [this](GoalHandleFibonacci::SharedPtr, const std::shared_ptrconst Fibonacci::Feedback feedback) { RCLCPP_INFO(this-get_logger(), 收到反馈 - 当前序列: ); for (auto number : feedback-sequence) { std::cout number ; } std::cout std::endl; }; // 设置结果回调当目标最终完成成功、取消、中止时触发 send_goal_options.result_callback [this](const GoalHandleFibonacci::WrappedResult result) { switch (result.code) { case rclcpp_action::ResultCode::SUCCEEDED: RCLCPP_INFO(this-get_logger(), 目标成功完成); RCLCPP_INFO(this-get_logger(), 最终结果序列: ); for (auto number : result.result-sequence) { std::cout number ; } std::cout std::endl; break; case rclcpp_action::ResultCode::CANCELED: RCLCPP_WARN(this-get_logger(), 目标被取消。); break; case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(this-get_logger(), 目标被中止。); break; default: RCLCPP_ERROR(this-get_logger(), 未知结果码。); break; } // 这里可以设置一个标志通知主循环任务结束 // 例如this-goal_done_ true; }; // 异步发送目标这是非阻塞调用。 // 它会返回一个 std::shared_future可以用来等待目标被接受或拒绝。 auto goal_handle_future client_ptr_-async_send_goal(goal_msg, send_goal_options); // 我们可以选择等待一段时间看目标是否被服务器接受 if (rclcpp::spin_until_future_complete(this-get_node_base_interface(), goal_handle_future) ! rclcpp::FutureReturnCode::SUCCESS) { RCLCPP_ERROR(this-get_logger(), 发送目标失败或超时。); return; } // 获取Goal Handle goal_handle_ goal_handle_future.get(); if (!goal_handle_) { RCLCPP_ERROR(this-get_logger(), 目标被服务器拒绝。); return; } RCLCPP_INFO(this-get_logger(), 目标已被服务器接受正在执行...); // 此时反馈回调和结果回调会在后台被自动调用。 // 主线程可以继续做其他事情比如监听用户输入来取消任务。 } // 一个取消当前目标的方法 void cancel_goal() { if (!goal_handle_) { RCLCPP_WARN(this-get_logger(), 没有活跃的目标可供取消。); return; } RCLCPP_INFO(this-get_logger(), 发送取消请求...); // 异步取消 auto future_cancel client_ptr_-async_cancel_goal(goal_handle_); // 可以等待取消操作完成这里简单忽略 } private: rclcpp_action::ClientFibonacci::SharedPtr client_ptr_; std::shared_ptrGoalHandleFibonacci goal_handle_; // 保存当前目标的句柄 }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto client_node std::make_sharedFibonacciActionClient(); // 发送一个目标例如计算阶数为10的数列 client_node-send_goal(10); // 为了让程序保持运行以接收反馈和结果我们需要spin。 // 但注意send_goal是异步的主线程会立刻继续执行到这里。 // 这里我们简单地spin节点直到用户按下CtrlC。 // 在实际应用中你可能会有一个主循环在循环里检查任务状态或处理其他逻辑。 rclcpp::spin(client_node); rclcpp::shutdown(); return 0; }客户端的关键细节与调试技巧wait_for_action_server在发送目标前调用这个函数至关重要。如果服务器还没启动客户端直接发送目标会失败。超时参数可以根据实际情况调整。SendGoalOptions这是配置客户端行为的核心。你必须设置feedback_callback和result_callback。这两个回调函数会在客户端节点的执行器executor线程中被调用因此它们内部可以安全地调用ROS2的日志、发布消息等API。异步发送与Futureasync_send_goal返回一个std::shared_future。调用goal_handle_future.get()会阻塞直到收到服务器的接受或拒绝响应。如果你想完全非阻塞可以忽略这个future完全依靠result_callback来得知最终结果。但通常我们至少需要知道目标是否被接受。结果码ResultCode在result_callback中result.code表明了任务的最终状态SUCCEEDED服务器调用goal_handle-succeed(result)。CANCELED服务器调用goal_handle-canceled(result)客户端或服务器取消。ABORTED服务器调用goal_handle-abort(result)任务执行失败。goal_handle_的生命周期我们将goal_handle_保存为成员变量以便在cancel_goal方法中使用。确保在任务完成后result_callback被调用后不再使用这个句柄或者将其重置。主循环设计这个示例的main函数很简单发送目标后直接spin。在真实的机器人应用中你的客户端节点可能是一个状态机的一部分在spin的同时定期检查任务状态或响应其他事件。一种常见模式是在send_goal后进入一个while(rclcpp::ok())循环在循环内spin_some处理回调并检查一个由result_callback设置的标志位如goal_done_来判断任务是否结束。5. 编译、运行与问题排查实战代码写完了接下来是编译和运行。进入你的工作空间根目录~/ros2_ws# 编译功能包 colcon build --packages-select cpp_action_demo # 激活环境每次新开终端都需要 source install/setup.bash运行与测试启动Action服务器ros2 run cpp_action_demo action_server你应该看到输出Fibonacci Action Server 已启动等待目标...启动Action客户端 另开一个终端激活环境后运行ros2 run cpp_action_demo action_client客户端会发送一个阶数为10的目标。观察两个终端的输出。服务器会每秒打印一次反馈客户端也会收到并打印反馈。最终服务器打印成功信息客户端打印最终结果序列。测试取消功能 我们需要修改一下客户端让它能在发送目标后等待几秒然后主动取消。为了演示我们可以写一个简单的带取消的客户端或者用ROS2命令行工具来模拟取消。方法一使用ros2 action命令行工具推荐用于调试先启动服务器。在新的终端发送目标ros2 action send_goal fibonacci example_interfaces/action/Fibonacci {order: 15} --feedback--feedback参数会实时显示反馈信息。在目标执行过程中另开一个终端取消这个目标。首先需要知道目标的UUID可以通过ros2 action list查看活跃的目标或者直接使用ros2 action cancel_goal。最简单的方法是在发送目标的命令后快速按CtrlC中断发送命令的终端然后立即运行ros2 action cancel_goal fibonacci这会取消该Action服务器上最新的目标。你会看到服务器和如果原命令还在运行反馈终端都显示任务被取消。方法二修改客户端代码在send_goal后等待几秒然后调用cancel_goal。这需要你管理好线程和时间稍微复杂一些。常见问题与排查编译错误找不到rclcpp_action或example_interfaces检查CMakeLists.txt中的find_package和ament_target_dependencies是否已添加。检查package.xml的depend标签。确保工作空间已正确编译 (colcon build) 并激活 (source install/setup.bash)。运行时错误客户端报Action服务器未在5秒内响应确认服务器节点是否已经启动。确认Action名称是否一致。服务器创建时用的名字是fibonacci客户端连接时也必须是fibonacci。使用ros2 node list和ros2 topic list检查节点和话题是否存在。Action底层是话题你应该能看到名为/fibonacci/_action/feedback,/fibonacci/_action/status等话题。服务器收不到目标或客户端收不到反馈最常见原因没有调用rclcpp::spin。确保服务器和客户端的节点对象都被spin了。spin是ROS2接收和处理所有回调包括定时器、订阅、Action请求的必需步骤。检查日志级别。有时默认的日志级别是WARNINFO级别的日志看不到。可以在启动节点时设置日志级别ros2 run cpp_action_demo action_server --ros-args --log-level info。任务无法取消在服务器的execute函数中必须定期检查goal_handle-is_canceling()。如果循环执行得很快比如没有sleep可能来不及检查取消标志。确保客户端的取消请求确实发送了。可以在服务器的handle_cancel回调中添加日志来确认。反馈或结果消息字段不对仔细核对Action定义文件。对于Fibonacci.action其Feedback和Result中的字段名都是sequence一个整数数组。如果你自定义Action务必确保服务器填充的字段和客户端期望读取的字段名称、类型完全一致。通过这个完整的“编码-编译-运行-调试”循环你应该对ROS2 Action的C实现有了扎实的理解。它不仅仅是API的调用更涉及异步编程、线程管理和节点间协作的思维模式。掌握了这个基础框架你就可以将其应用到机器人导航、机械臂抓取等任何需要长时间运行且需监控进度的任务中了。