
1. 项目概述从话题到代码的跨越如果你正在看这篇内容大概率是已经对ROS2有了初步了解正卡在如何将“动作”这个听起来有点抽象的概念用C实实在在地写出来。我刚开始接触ROS2动作时也经历过这个阶段官方文档讲得比较分散网上例子要么太简单要么跑不通。今天我们就来彻底搞定它。这篇文章的核心就是手把手带你用C实现一个完整的ROS2动作通信从零开始包含服务端、客户端、参数配置、编译运行以及我踩过的所有坑和对应的填坑方案。无论你是想实现一个让机器人移动到指定点的导航动作还是控制机械臂完成一套抓取流程动作都是你绕不开的核心通信机制。它比话题更可靠比服务更灵活是构建复杂机器人行为逻辑的基石。简单来说ROS2动作Action是一种建立在话题和服务之上的高级通信接口专为执行时间较长、且可能被取消或需要反馈的任务而设计。想象一下你让机器人去另一个房间取一杯水你发出“取水”的指令目标机器人会一边移动一边告诉你“我正在穿过走廊”、“我已看到水杯”反馈最后把水递给你时告诉你“任务完成”结果。这个完整的、带状态的长流程就是动作的典型应用场景。而C作为机器人领域性能要求较高的首选语言其实现方式有其特定的细节和技巧。接下来我们将深入代码层面把每一个环节都掰开揉碎讲清楚。2. ROS2动作通信的核心机制拆解在动手写代码之前我们必须先理解动作在ROS2底层是如何工作的。这能帮你未来在调试时一眼看穿问题本质而不是盲目地修改代码。2.1 动作的三元组结构目标、反馈、结果一个动作接口由三个核心部分组成它们各自独立又相互关联目标Goal客户端发送给服务端的任务请求。例如MoveToPosition {x: 1.0, y: 2.0, theta: 0.0}。这定义了任务要达成的最终状态。反馈Feedback服务端在执行任务过程中周期性发送给客户端的进度信息。例如NavigationFeedback {current_x: 0.5, current_y: 1.0, percent_complete: 50}。反馈是单向的从服务端流向客户端让客户端知晓任务进展。结果Result任务结束时无论成功或失败服务端发送给客户端的最终输出。例如MoveToPositionResult {success: true, message: “Arrived at destination”}。每个目标有且仅有一个结果。这种设计模式完美解耦了指令、过程监控和结果汇报比单纯使用一个服务请求-响应来处理长任务要优雅和健壮得多。2.2 底层实现话题与服务的组合拳很多初学者会困惑动作到底是什么其实你可以把它看作ROS2提供的一个“语法糖”或“设计模式”它底层并没有发明新的通信机制而是巧妙地组合使用了1个服务Action Server和2个话题。目标服务用于客户端提交目标。这是一个标准的服务调用确保了目标传递的可靠性。反馈话题服务端定期发布客户端订阅。这是一个话题适合流式数据。结果服务用于服务端在任务结束时发送结果。这也是一个服务调用确保结果能准确送达。此外动作还内置了取消Cancel和状态查询的机制。所以当你启动一个动作服务端时在ros2 topic list和ros2 service list中会看到一系列自动生成的接口。理解这一点对后续用命令行工具调试至关重要。2.3 C实现的关键类与生命周期在C中实现动作主要涉及以下几个核心类rclcpp::Node: 所有一切的基石你的动作服务端和客户端都需要继承或拥有一个Node。rclcpp_action::ServerActionT: 动作服务端模板类。你需要为它提供一个handle_goal回调处理新目标、handle_cancel回调处理取消请求和execute回调实际执行任务的主体函数。rclcpp_action::ClientActionT: 动作客户端模板类。你需要用它来发送目标并设置反馈回调、结果回调。一个动作的生命周期大致如下客户端通过Client::async_send_goal发送目标。服务端的handle_goal回调被触发决定是否接受该目标。如果接受服务端开始执行execute函数。在execute中服务端可以通过goal_handle-publish_feedback()发布反馈。客户端在等待过程中其设置的反馈回调函数会不断被触发。任务完成后服务端在execute中调用goal_handle-succeed()或canceled()/abort()并设置结果。客户端的result_callback被触发收到最终结果。整个流程是异步的充分体现了ROS2的异步设计思想。下面我们就进入实战环节从零开始构建一个具体的例子。3. 实战构建一个斐波那契数列计算动作为了聚焦于动作机制本身我们选择一个计算密集型但易于理解的例子计算斐波那契数列。服务端接收一个订单N然后计算斐波那契数列的前N项在计算过程中定期反馈当前进度最后返回整个数列作为结果。3.1 创建功能包与定义动作接口首先创建一个专门的功能包。我建议将动作接口定义单独放在一个包中这样服务端和客户端包都可以依赖它实现解耦。# 假设你的工作空间是 ~/ros2_ws cd ~/ros2_ws/src # 创建接口包依赖 rosidl_default_generators 用于生成代码 ros2 pkg create fibonacci_action_interfaces --build-type ament_cmake --dependencies rosidl_default_generators action_msgs接下来在fibonacci_action_interfaces目录下创建action文件夹并在其中创建Fibonacci.action文件# Fibonacci.action # 目标我们想要计算前多少项 int32 order --- # 结果最终计算出的完整数列 int32[] sequence --- # 反馈当前已计算出的部分数列 int32[] partial_sequence这个文件定义了我们动作的“合约”。order是输入sequence是最终输出partial_sequence是过程中的反馈。然后需要修改CMakeLists.txt和package.xml来声明这个动作文件。package.xml中确保有buildtool_dependament_cmake/buildtool_depend dependaction_msgs/depend member_of_grouprosidl_interface_packages/member_of_groupCMakeLists.txt中关键部分find_package(ament_cmake REQUIRED) find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} action/Fibonacci.action ) ... ament_export_dependencies(rosidl_default_runtime)编译这个接口包cd ~/ros2_ws colcon build --packages-select fibonacci_action_interfaces source install/setup.bash编译成功后你可以用ros2 interface show fibonacci_action_interfaces/action/Fibonacci来查看生成的动作接口详情。你会看到ROS2自动为它生成了Goal, Result, Feedback等结构体。3.2 C动作服务端实现详解现在创建动作服务端的功能包和节点。cd ~/ros2_ws/src ros2 pkg create fibonacci_action_server --build-type ament_cmake --dependencies rclcpp rclcpp_action fibonacci_action_interfaces在src目录下创建fibonacci_action_server.cpp#include rclcpp/rclcpp.hpp #include rclcpp_action/rclcpp_action.hpp #include fibonacci_action_interfaces/action/fibonacci.hpp #include memory #include thread #include vector using Fibonacci fibonacci_action_interfaces::action::Fibonacci; using GoalHandleFibonacci rclcpp_action::ServerGoalHandleFibonacci; class FibonacciActionServer : public rclcpp::Node { public: explicit FibonacciActionServer(const rclcpp::NodeOptions options rclcpp::NodeOptions()) : Node(fibonacci_action_server) { // 1. 创建动作服务端 this-action_server_ rclcpp_action::create_serverFibonacci( this, fibonacci, // 动作名 // handle_goal 回调当新目标到达时调用决定是否接受 std::bind(FibonacciActionServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2), // handle_cancel 回调当取消请求到达时调用 std::bind(FibonacciActionServer::handle_cancel, this, std::placeholders::_1), // execute 回调接受目标后在新线程中执行的任务函数 std::bind(FibonacciActionServer::handle_accepted, this, std::placeholders::_1) ); RCLCPP_INFO(this-get_logger(), Fibonacci Action Server has been started.); } private: rclcpp_action::ServerFibonacci::SharedPtr action_server_; // 处理新目标请求 rclcpp_action::GoalResponse handle_goal( const rclcpp_action::GoalUUID uuid, std::shared_ptrconst Fibonacci::Goal goal) { RCLCPP_INFO(this-get_logger(), Received goal request with order %d, goal-order); (void)uuid; // 防止未使用变量警告UUID可用于唯一标识目标 // 简单的验证order必须为正数 if (goal-order 0) { RCLCPP_WARN(this-get_logger(), Goal rejected: order must be positive.); return rclcpp_action::GoalResponse::REJECT; } // 防止计算量过大这里设置一个上限实际项目中根据资源调整 if (goal-order 100) { RCLCPP_WARN(this-get_logger(), Goal rejected: order %d is too large (max 100)., goal-order); return rclcpp_action::GoalResponse::REJECT; } RCLCPP_INFO(this-get_logger(), Goal accepted.); return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; } // 处理取消请求 rclcpp_action::CancelResponse handle_cancel( const std::shared_ptrGoalHandleFibonacci goal_handle) { RCLCPP_INFO(this-get_logger(), Received request to cancel goal); (void)goal_handle; // 这里我们简单地同意取消。在实际应用中你可能需要检查任务状态。 return rclcpp_action::CancelResponse::ACCEPT; } // 目标被接受后启动执行线程 void handle_accepted(const std::shared_ptrGoalHandleFibonacci goal_handle) { // 使用std::thread在新线程中执行避免阻塞ROS2的executor std::thread{std::bind(FibonacciActionServer::execute, this, std::placeholders::_1), goal_handle}.detach(); } // 实际的任务执行函数 void execute(const std::shared_ptrGoalHandleFibonacci goal_handle) { RCLCPP_INFO(this-get_logger(), Executing goal...); const auto goal goal_handle-get_goal(); auto result std::make_sharedFibonacci::Result(); auto feedback std::make_sharedFibonacci::Feedback(); // 初始化斐波那契数列 std::vectorint32_t sequence; sequence.push_back(0); sequence.push_back(1); // 设置循环计算并定期发布反馈 rclcpp::Rate loop_rate(1); // 1 Hz每秒反馈一次 for (int i 1; i goal-order; i) { // 检查是否被取消 if (goal_handle-is_canceling()) { result-sequence sequence; goal_handle-canceled(result); RCLCPP_INFO(this-get_logger(), Goal canceled.); return; } // 计算下一项 if (i 1) { sequence.push_back(sequence[i-1] sequence[i-2]); } // 发布反馈当前的部分序列 feedback-partial_sequence sequence; goal_handle-publish_feedback(feedback); RCLCPP_INFO(this-get_logger(), Publishing feedback: computed %ld items, sequence.size()); // 模拟计算耗时 loop_rate.sleep(); } // 任务完成设置结果 result-sequence sequence; goal_handle-succeed(result); RCLCPP_INFO(this-get_logger(), Goal succeeded. Final sequence size: %ld, 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; }关键点解析与避坑线程分离execute函数通过std::thread在新线程中运行。这是至关重要的一点。如果直接在回调中执行长时间任务会阻塞ROS2的executor导致整个节点无法响应其他消息如取消请求。detach()让线程独立运行但要注意资源管理这里因为线程生命周期与目标绑定所以是安全的。目标验证在handle_goal中对order进行校验。在生产环境中这里是进行资源检查、权限验证的好地方。拒绝无效目标可以避免浪费计算资源。取消检查在execute循环中使用goal_handle-is_canceling()定期检查取消状态。这是实现“可取消”动作的关键。一旦检测到取消立即设置结果并返回。反馈频率这里用rclcpp::Rate控制每秒反馈一次。实际项目中反馈频率需要权衡太频繁消耗网络资源太稀疏则客户端感知迟钝。应根据任务特性和需求调整。接下来修改服务端包的CMakeLists.txt添加可执行文件的构建规则然后编译。3.3 C动作客户端实现详解创建客户端功能包cd ~/ros2_ws/src ros2 pkg create fibonacci_action_client --build-type ament_cmake --dependencies rclcpp rclcpp_action fibonacci_action_interfaces在src目录下创建fibonacci_action_client.cpp#include rclcpp/rclcpp.hpp #include rclcpp_action/rclcpp_action.hpp #include fibonacci_action_interfaces/action/fibonacci.hpp #include chrono #include functional #include memory #include future using Fibonacci fibonacci_action_interfaces::action::Fibonacci; using GoalHandleFibonacci rclcpp_action::ClientGoalHandleFibonacci; class FibonacciActionClient : public rclcpp::Node { public: explicit FibonacciActionClient(const rclcpp::NodeOptions node_options rclcpp::NodeOptions()) : Node(fibonacci_action_client), goal_done_(false) { // 1. 创建动作客户端 this-client_ptr_ rclcpp_action::create_clientFibonacci( this, fibonacci // 动作名必须与服务端一致 ); RCLCPP_INFO(this-get_logger(), Fibonacci Action Client created.); } // 发送目标的公共接口 void send_goal(int order) { // 等待动作服务端上线 if (!this-client_ptr_-wait_for_action_server(std::chrono::seconds(10))) { RCLCPP_ERROR(this-get_logger(), Action server not available after waiting); this-goal_done_ true; return; } // 构造目标 auto goal_msg Fibonacci::Goal(); goal_msg.order order; RCLCPP_INFO(this-get_logger(), Sending goal with order %d, order); // 设置发送目标的选项 auto send_goal_options rclcpp_action::ClientFibonacci::SendGoalOptions(); // 设置反馈回调 send_goal_options.feedback_callback std::bind(FibonacciActionClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2); // 设置结果回调 send_goal_options.result_callback std::bind(FibonacciActionClient::result_callback, this, std::placeholders::_1); // 异步发送目标 auto future_goal_handle this-client_ptr_-async_send_goal(goal_msg, send_goal_options); } // 检查任务是否完成的简单方法用于主循环 bool is_goal_done() const { return this-goal_done_; } private: rclcpp_action::ClientFibonacci::SharedPtr client_ptr_; bool goal_done_; // 反馈回调函数 void feedback_callback( GoalHandleFibonacci::SharedPtr, const std::shared_ptrconst Fibonacci::Feedback feedback) { RCLCPP_INFO(this-get_logger(), Received feedback: ); for (auto number : feedback-partial_sequence) { RCLCPP_INFO(this-get_logger(), %d , number); } } // 结果回调函数 void result_callback(const GoalHandleFibonacci::WrappedResult result) { this-goal_done_ true; switch (result.code) { case rclcpp_action::ResultCode::SUCCEEDED: RCLCPP_INFO(this-get_logger(), Goal succeeded!); RCLCPP_INFO(this-get_logger(), Result sequence: ); for (auto number : result.result-sequence) { RCLCPP_INFO(this-get_logger(), %d , number); } break; case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(this-get_logger(), Goal was aborted); break; case rclcpp_action::ResultCode::CANCELED: RCLCPP_WARN(this-get_logger(), Goal was canceled); break; default: RCLCPP_ERROR(this-get_logger(), Unknown result code); break; } // 结果收到后可以在这里触发后续逻辑例如关闭节点或发送新目标 rclcpp::shutdown(); // 示例收到结果后关闭客户端 } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto action_client std::make_sharedFibonacciActionClient(); int order 10; // 默认计算前10项 if (argc 1) { order std::stoi(argv[1]); // 允许通过命令行参数指定order } // 发送目标 action_client-send_goal(order); // 保持节点运行直到收到结果result_callback中调用了shutdown rclcpp::spin(action_client); rclcpp::shutdown(); // 再次确保关闭spin返回后执行 return 0; }关键点解析与避坑等待服务端client_ptr_-wait_for_action_server是必要的。在发送目标前必须确保服务端已启动并注册了动作服务否则会发送失败。回调绑定SendGoalOptions结构体用于配置发送目标时的行为。其中feedback_callback和result_callback是成员函数必须使用std::bind进行绑定并传入this指针和占位符。这是C中处理类成员回调的常见模式。异步发送async_send_goal是非阻塞的它会立即返回一个std::shared_future。在这个例子中我们没有处理这个future因为我们通过回调来处理结果。如果你需要同步等待结果可以使用future_goal_handle.get()来获取GoalHandle然后在其上调用async_get_result并等待。结果处理result_callback的参数是WrappedResult它包含了结果码成功、中止、取消和实际的结果数据。务必根据结果码进行分支处理不能默认任务一定成功。节点生命周期这个示例在收到结果后直接调用了rclcpp::shutdown()来结束程序。在实际的机器人应用中客户端节点可能是一个长期运行的状态机的一部分需要在收到结果后触发下一个状态而不是直接关闭。同样修改客户端包的CMakeLists.txt添加构建规则。4. 编译、运行与调试全流程4.1 一站式编译在~/ros2_ws目录下一次性编译所有相关包cd ~/ros2_ws colcon build --packages-select fibonacci_action_interfaces fibonacci_action_server fibonacci_action_client source install/setup.bash如果编译成功你将看到三个包都被构建完成。4.2 分步运行与观察首先在一个终端启动动作服务端source ~/ros2_ws/install/setup.bash ros2 run fibonacci_action_server fibonacci_action_server你应该看到输出[INFO] [fibonacci_action_server]: Fibonacci Action Server has been started.接着在另一个终端启动动作客户端并指定要计算斐波那契数列的前8项source ~/ros2_ws/install/setup.bash ros2 run fibonacci_action_client fibonacci_action_client 8观察现象客户端终端会立即打印Sending goal with order 8。服务端终端会打印Received goal request with order 8和Goal accepted.。随后服务端每秒打印一次反馈信息客户端终端也会同步收到并打印反馈的序列。大约8秒后服务端打印Goal succeeded客户端打印Goal succeeded!并输出完整的斐波那契数列结果然后两者都退出。4.3 使用ROS2命令行工具深入洞察动作的强大之处在于其可观测性。你可以用ROS2命令行工具查看其内部状态查看动作列表ros2 action list。你会看到/fibonacci。查看动作信息ros2 action info /fibonacci。这会显示该动作的服务端和客户端数量。手动发送目标测试你甚至可以不写客户端直接用命令行动作客户端发送目标ros2 action send_goal /fibonacci fibonacci_action_interfaces/action/Fibonacci {order: 5} --feedback加上--feedback参数你就能在终端实时看到反馈流。这是一个极其强大的调试和快速测试功能。5. 进阶技巧与生产环境注意事项当你掌握了基础实现后下面这些经验能帮你写出更健壮、更高效的代码。5.1 动作服务器的多目标处理与资源管理上面的示例服务器一次只处理一个目标因为每个目标都detach了一个线程。在实际机器人系统中你可能需要处理并发目标。策略一队列化。在handle_accepted中将goal_handle放入一个任务队列由一个或多个工作线程从队列中取出执行。这可以控制并发度避免系统过载。策略二资源检查与抢占。在handle_goal中不仅检查目标有效性还检查当前系统资源如CPU、内存或执行单元如机器人底盘、机械臂是否被占用。如果被占用可以返回REJECT或者ACCEPT_AND_DEFER如果支持。关键点goal_handle是管理目标生命周期的核心。使用goal_handle-is_canceling()和goal_handle-canceled()来妥善处理取消避免僵尸任务。5.2 客户端的超时、重试与状态机集成生产级的客户端不能假设服务端永远可用或任务永远成功。发送超时async_send_goal可以搭配std::future和wait_for实现超时控制。结果等待超时可以使用async_get_result返回的future来设置等待结果的超时。重试逻辑如果目标被拒绝或执行失败客户端应根据错误类型如服务器忙、目标无效决定是否重试、重试几次、以及重试间隔指数退避是一种好策略。集成到状态机客户端的result_callback应该触发状态机的状态转移。例如导航客户端在收到“成功”结果后状态从NAVIGATING转移到IDLE收到“取消”结果则可能转移到PAUSED。5.3 性能优化与最佳实践反馈频率优化反馈消息的序列化、发布和反序列化是有成本的。对于高速控制如1kHz的关节控制每秒发布上千次反馈是不可取的。通常反馈用于进度汇报每秒几次到几十次而非实时数据流。实时数据流应使用专门的Topic。动作 vs 服务 vs 话题牢记三者的适用场景。动作长时、可取消、需反馈的任务导航、抓取、长时间计算。服务短时、请求-响应式的命令开关灯、查询状态、触发一个立即完成的动作。话题持续的、单向的数据流传感器数据、机器人状态发布。接口设计动作的Goal、Result、Feedback消息设计要简洁、明确。避免在Goal中传递过大的数据如图像这会影响发送效率。大数据应通过Topic传递在Goal中只包含一个资源标识符如话题名或ID。6. 实战中遇到的典型问题与排查指南即使理解了原理第一次实现时也难免遇到问题。下面是我总结的几个常见坑点及其解决方法。6.1 编译错误“找不到 action 头文件”问题描述编译客户端或服务端时报错fatal error: fibonacci_action_interfaces/action/fibonacci.hpp: No such file or directory。根本原因CMake依赖关系未正确设置或工作空间未重新source。排查步骤检查CMakeLists.txt确保在find_package中包含了动作接口包并且ament_target_dependencies或target_link_libraries中链接了它。find_package(fibonacci_action_interfaces REQUIRED) ... ament_target_dependencies(fibonacci_action_server rclcpp rclcpp_action fibonacci_action_interfaces)检查package.xml确保在depend标签中声明了对fibonacci_action_interfaces的依赖。清理并重新编译在ros2_ws目录下执行colcon build --packages-select 你的包名 --symlink-install并确保接口包已先编译。Source 环境在运行或编译前务必source ~/ros2_ws/install/setup.bash。最好将此命令加入你的~/.bashrc。6.2 运行时错误“Action server not available”问题描述客户端启动后立刻打印错误并退出。排查步骤检查服务端是否运行ros2 node list和ros2 action list。确保服务端节点已启动且注册了动作。检查动作名称客户端和服务端创建动作时使用的名称如fibonacci必须完全一致包括命名空间。如果服务端节点有命名空间如/my_ns/fibonacci_action_server则客户端连接时需要指定完整路径。增加等待时间客户端wait_for_action_server的超时时间示例中是10秒可能太短。可以适当增加或在服务端启动后再启动客户端。6.3 客户端收不到反馈或结果问题描述客户端发送目标后服务端有日志显示在执行和反馈但客户端没有任何反馈或结果日志。排查步骤检查回调函数绑定这是最常见的原因。确保SendGoalOptions中的feedback_callback和result_callback已正确绑定到你的成员函数。回调函数签名必须完全匹配。检查Executor你的节点是否被正确地spin起来了如果主线程在发送目标后立即退出回调可能来不及执行。确保有rclcpp::spin(node)或rclcpp::executors::SingleThreadedExecutor在运行。使用命令行工具验证用ros2 action send_goal --feedback /fibonacci ...手动发送目标。如果命令行能收到反馈和结果那么问题一定出在你的客户端代码很可能是回调绑定或Executor。如果命令行也收不到问题出在服务端反馈发布逻辑。6.4 服务端的 execute 函数阻塞了整个节点问题描述服务端在接受一个目标后无法响应新的目标或取消请求。根本原因execute函数是长时间运行的如果在主线程或同一个回调线程中同步执行就会阻塞Executor。解决方案正如我们在示例中所做必须将execute函数放在独立的线程中运行。使用std::thread或线程池。记住要将线程detach或妥善管理其生命周期。6.5 动作状态在 rqt_action 中显示异常问题描述使用rqt_action图形化工具查看动作时状态显示不正确。排查步骤确保正确终止目标在execute函数中任务结束时必须调用goal_handle-succeed(result),goal_handle-canceled(result), 或goal_handle-abort(result)。如果不调用动作服务器会认为目标仍在执行状态卡住。检查UUID冲突理论上ROS2会生成唯一UUID但极端情况下需确保goal_handle管理正确。查看底层话题/服务用ros2 topic echo /fibonacci/_action/feedback等命令查看原始数据流这有助于判断是服务端状态设置问题还是GUI工具的问题。通过以上从原理到实践从代码到调试的完整梳理相信你已经对ROS2动作的C实现有了透彻的理解。记住动作是构建复杂机器人任务流的利器其核心思想——目标、反馈、结果的分离与异步处理——在分布式机器人系统中至关重要。现在你可以尝试将这里的斐波那契计算器替换成让机器人移动、让机械臂抓取的真实逻辑了。