ROS2动作通信C++实战:从接口定义到机械臂拾取放置完整实现

📅 2026/7/31 13:48:37 👁️ 阅读次数 📝 编程学习
ROS2动作通信C++实战:从接口定义到机械臂拾取放置完整实现

1. 项目概述:从话题到实战的跨越

最近在机器人开发者社区里,一个高频出现的话题是“如何让ROS2的动作(Action)真正用起来”。很多朋友,尤其是从ROS1迁移过来或者刚入门的开发者,在理解了话题(Topic)和服务(Service)之后,面对动作这个概念,总觉得隔着一层纱——知道它用于长时间运行、可抢占、有反馈的任务,但一到用C++实现,就卡在了客户端与服务端的交互逻辑、目标状态管理这些细节上。这其实非常正常,动作是ROS2通信机制中相对复杂的一环,它融合了话题和服务的特性,设计初衷就是为了解决那些“一言难尽”的耗时任务,比如让机器人移动到某个点位、执行一段复杂的抓取序列,或者完成一个SLAM建图过程。

如果你正在为机器人开发一个需要持续数十秒甚至几分钟,并且你希望随时能知道进度、能中途取消、能知道最终是成功还是失败的功能,那么动作就是你不可或缺的工具。它比单纯用话题发布指令再订阅状态更结构化,比用服务处理长任务更灵活可靠。本文的目的,就是抛开那些笼统的概念,直接深入到C++代码层面,手把手带你构建一个完整的、可运行的ROS2动作示例。我们会从一个具体的场景出发:控制一个虚拟的机械臂执行一段“拾取-放置”任务。通过这个例子,你将彻底掌握动作服务器(Action Server)和动作客户端(Action Client)的C++实现脉络,理解目标(Goal)、反馈(Feedback)、结果(Result)这三个核心消息是如何在两者之间流转的,并学会处理取消、抢占等现实开发中必然会遇到的边界情况。无论你是正在学习ROS2的学生,还是需要为产品添加复杂任务模块的工程师,这篇内容都能提供即插即用的代码框架和避坑指南。

2. 动作通信模型深度解析:不仅仅是“带反馈的服务”

在开始写代码之前,我们必须把动作的通信模型吃透。很多教程把它简单描述为“一个服务加两个话题”,这虽然形象,但容易让人忽略其内在的强状态机和并发管理机制。理解这个模型,是写出健壮动作代码的基础。

2.1 三层通信结构与状态机

一个ROS2动作本质上由三对发布/订阅关系构成,它们共同维护了一个清晰的任务状态机:

  1. 目标话题(Goal Topic):客户端向服务器发送任务目标。这类似于服务的请求,但它是单向的、异步的。服务器订阅此话题以接收新目标。
  2. 取消话题(Cancel Topic):客户端向服务器发送取消请求。服务器订阅此话题以响应取消操作。
  3. 状态话题(Status Topic):服务器向客户端广播当前所有被跟踪任务(Goal)的状态(例如“正在执行”、“已取消”、“已成功”)。客户端订阅此话题以了解任务的整体生命周期。
  4. 反馈话题(Feedback Topic):服务器在执行任务的过程中,定期向客户端发布进度信息。这是一个单向的、从服务器到客户端的流。
  5. 结果服务(Result Service):当任务最终完成(无论成功、失败或被取消)时,客户端会通过一个服务调用来获取最终的结果。这是一个同步的请求-响应,确保客户端能可靠地拿到任务结局。

这五条通道共同作用,背后对应着一个严格的状态机。对于服务器接收到的每一个目标(Goal),其生命周期会经历:ACCEPTED->EXECUTING-> 最终状态(SUCCEEDED,CANCELED,ABORTED等)。客户端和服务器端的API设计,正是为了让你方便地在这个状态机框架下工作,而不是自己去维护这些状态和通道。

2.2 与话题、服务的核心差异

为什么不用话题或服务替代动作?关键在于动作解决的痛点:

  • vs 话题(Topic):如果你用话题发送指令,再用另一个话题订阅状态,你需要自己设计消息协议来关联指令和状态,自己处理“哪个状态对应哪个指令”的匹配问题,自己实现取消逻辑。动作将这些标准化、内置化了。
  • vs 服务(Service):服务是同步的、阻塞的调用。一个长达一分钟的移动任务会阻塞客户端一分钟,期间无法接收任何其他信息,也无法取消。动作的异步特性让客户端在发送目标后即可解放,通过回调函数来处理反馈和结果。

所以,当你面临的任务具有“命令-执行-持续反馈-最终结果”这个模式时,动作就是最自然的选择。接下来,我们就以“机械臂拾取放置”为蓝本,开始C++实战。

3. 开发环境准备与接口定义

工欲善其事,必先利其器。一个清晰的接口定义是后续所有工作的基石。

3.1 创建ROS2工作空间与功能包

假设你已经安装了ROS2 Humble或Iron版本(安装过程不是本文重点,网上教程很多)。我们首先创建一个专门的工作空间和功能包。

# 创建并进入工作空间 mkdir -p ~/ros2_action_ws/src cd ~/ros2_action_ws/src # 使用CMake创建C++功能包,依赖rclcpp和action接口 ros2 pkg create cpp_action_tutorial --build-type ament_cmake --dependencies rclcpp rclcpp_action rclcpp_components example_interfaces

这里我们额外依赖了example_interfaces,只是为了方便使用其中预定义的Fibonacci.action作为示例接口。在实际项目中,你需要定义自己的.action文件。

3.2 设计自定义动作接口(.action文件)

在功能包的action目录下创建我们自己的动作接口文件PickAndPlace.action

cd cpp_action_tutorial mkdir action touch action/PickAndPlace.action

编辑PickAndPlace.action文件,定义任务的目标、反馈和结果:

# PickAndPlace.action # 定义目标消息:拾取和放置的坐标 geometry_msgs/Point pick_position geometry_msgs/Point place_position string object_id --- # 定义结果消息:最终是否成功,以及可能的信息 bool success string message --- # 定义反馈消息:当前阶段和完成百分比 string current_phase # 例如:“MOVING_TO_PICK”, “GRASPING”, “MOVING_TO_PLACE”, “RELEASING” float32 percent_complete

这个文件定义了一个完整的契约:

  • 目标(Goal):告诉机械臂去哪里拾取(pick_position),放到哪里(place_position),以及操作哪个物体(object_id)。
  • 结果(Result):任务结束后,告诉客户端是否成功(success)和附加信息(message)。
  • 反馈(Feedback):任务执行过程中,定期告诉客户端现在在哪个阶段(current_phase)和总体进度(percent_complete)。

3.3 配置构建系统

为了让CMake能识别并编译我们自定义的.action文件,需要修改CMakeLists.txtpackage.xml

CMakeLists.txt中,找到find_package部分,确保包含rosidl_default_generators,并添加以下内容:

find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_action REQUIRED) find_package(rosidl_default_generators REQUIRED) # 声明要生成消息的.action文件 rosidl_generate_interfaces(${PROJECT_NAME} "action/PickAndPlace.action" ) ... # 在 ament_package() 之前,添加依赖导出 ament_export_dependencies(rosidl_default_runtime) ament_export_include_directories(include)

package.xml中,添加构建依赖和运行时依赖:

<buildtool_depend>ament_cmake</buildtool_depend> <depend>rclcpp</depend> <depend>rclcpp_action</depend> <depend>geometry_msgs</depend> <depend>rosidl_default_generators</depend> <member_of_group>rosidl_interface_packages</member_of_group>

现在,你可以编译工作空间来生成对应的C++头文件了:

cd ~/ros2_action_ws colcon build --packages-select cpp_action_tutorial source install/setup.bash

编译成功后,在install/cpp_action_tutorial/include/下就能找到自动生成的cpp_action_tutorial/action/pick_and_place.hpp等头文件,我们将在代码中包含它们。

4. 动作服务器(Action Server)的C++实现

动作服务器是任务的执行者。它的核心职责是:接收目标、执行任务、发布反馈、返回结果,并优雅地处理取消请求。

4.1 服务器节点框架搭建

我们在src目录下创建服务器节点文件pick_and_place_server.cpp

// pick_and_place_server.cpp #include <rclcpp/rclcpp.hpp> #include <rclcpp_action/rclcpp_action.hpp> #include <cpp_action_tutorial/action/pick_and_place.hpp> #include <thread> #include <chrono> using namespace std::chrono_literals; using PickAndPlace = cpp_action_tutorial::action::PickAndPlace; using GoalHandlePickAndPlace = rclcpp_action::ServerGoalHandle<PickAndPlace>; class PickAndPlaceActionServer : public rclcpp::Node { public: explicit PickAndPlaceActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()) : Node("pick_and_place_action_server") { // 使用rclcpp_action创建动作服务器 this->action_server_ = rclcpp_action::create_server<PickAndPlace>( this, // 所属节点 "pick_and_place", // 动作名称,客户端将通过此名称连接 std::bind(&PickAndPlaceActionServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2), std::bind(&PickAndPlaceActionServer::handle_cancel, this, std::placeholders::_1), std::bind(&PickAndPlaceActionServer::handle_accepted, this, std::placeholders::_1) ); RCLCPP_INFO(this->get_logger(), "Pick and Place Action Server 已启动,等待目标..."); } private: rclcpp_action::Server<PickAndPlace>::SharedPtr action_server_; // 1. 处理目标请求:决定是否接受新目标 rclcpp_action::GoalResponse handle_goal( const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const PickAndPlace::Goal> goal) { RCLCPP_INFO(this->get_logger(), "收到新目标请求,物体ID: %s", goal->object_id.c_str()); // 此处可添加目标验证逻辑,例如检查坐标是否在可达范围内 (void)uuid; // 防止未使用变量警告 return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; // 接受并立即执行 } // 2. 处理取消请求 rclcpp_action::CancelResponse handle_cancel( const std::shared_ptr<GoalHandlePickAndPlace> goal_handle) { RCLCPP_INFO(this->get_logger(), "收到取消请求"); // 此处应设置一个标志位,通知执行函数需要退出 // 为了示例,我们假设取消总是被允许 return rclcpp_action::CancelResponse::ACCEPT; } // 3. 目标被接受后,在新线程中执行任务 void handle_accepted(const std::shared_ptr<GoalHandlePickAndPlace> goal_handle) { // 使用std::thread避免阻塞主线程(执行器) std::thread{std::bind(&PickAndPlaceActionServer::execute, this, std::placeholders::_1), goal_handle}.detach(); } // 核心执行函数 void execute(const std::shared_ptr<GoalHandlePickAndPlace> goal_handle) { RCLCPP_INFO(this->get_logger(), "开始执行拾取放置任务"); const auto goal = goal_handle->get_goal(); auto feedback = std::make_shared<PickAndPlace::Feedback>(); auto result = std::make_shared<PickAndPlace::Result>(); // 模拟一个长时间运行的任务,分为4个阶段 std::vector<std::string> phases = {"MOVING_TO_PICK", "GRASPING", "MOVING_TO_PLACE", "RELEASING"}; for (size_t i = 0; i < phases.size(); ++i) { // 检查任务是否被取消 if (goal_handle->is_canceling()) { result->success = false; result->message = "任务被用户取消"; goal_handle->canceled(result); RCLCPP_INFO(this->get_logger(), "任务取消"); return; } // 更新反馈 feedback->current_phase = phases[i]; feedback->percent_complete = (i + 1) * 25.0; // 模拟25%,50%,75%,100% goal_handle->publish_feedback(feedback); RCLCPP_INFO(this->get_logger(), "阶段: %s, 进度: %.0f%%", feedback->current_phase.c_str(), feedback->percent_complete); // 模拟该阶段耗时工作 std::this_thread::sleep_for(2s); } // 任务成功完成 result->success = true; result->message = "拾取放置任务成功完成"; goal_handle->succeed(result); RCLCPP_INFO(this->get_logger(), "任务成功完成: %s", result->message.c_str()); } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<PickAndPlaceActionServer>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }

4.2 服务器实现关键点剖析

  1. 三回调机制:服务器的核心是三个回调函数:handle_goal,handle_cancel,handle_accepted。它们分别处理目标到达、取消请求和目标被接受后的动作。这种设计将状态判断与任务执行解耦,非常清晰。
  2. 异步执行:在handle_accepted中,我们使用std::thread来在新线程中运行execute函数。这是至关重要的一步。如果你在回调函数中直接执行耗时任务,会阻塞ROS2的执行器(executor),导致整个节点无法响应其他消息(包括取消请求!)。分离线程是标准做法。
  3. 状态检查与响应:在execute函数的循环中,每次迭代都通过goal_handle->is_canceling()检查是否收到了取消请求。如果收到,则调用goal_handle->canceled(result)来通知客户端任务已取消,并设置相应的结果。任务成功完成后,则调用goal_handle->succeed(result)
  4. 反馈发布:通过goal_handle->publish_feedback(feedback)定期向客户端发送进度信息。反馈的频率和内容需要根据实际任务调整,既要让客户端感知进度,又不能过于频繁造成网络负担。

注意:在实际的机器人控制中,execute函数内部应该是与机器人控制器(如MoveIt!、底层驱动节点)交互的代码,而不是sleep_for。这里的睡眠仅用于模拟耗时过程。

5. 动作客户端(Action Client)的C++实现

客户端是任务的发起者。它负责发送目标、监听反馈、处理结果,并可能发送取消指令。

5.1 客户端节点框架搭建

src目录下创建客户端节点文件pick_and_place_client.cpp

// pick_and_place_client.cpp #include <rclcpp/rclcpp.hpp> #include <rclcpp_action/rclcpp_action.hpp> #include <cpp_action_tutorial/action/pick_and_place.hpp> #include <geometry_msgs/msg/point.hpp> #include <thread> using namespace std::chrono_literals; using PickAndPlace = cpp_action_tutorial::action::PickAndPlace; using GoalHandlePickAndPlace = rclcpp_action::ClientGoalHandle<PickAndPlace>; class PickAndPlaceActionClient : public rclcpp::Node { public: explicit PickAndPlaceActionClient(const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions()) : Node("pick_and_place_action_client") { this->client_ = rclcpp_action::create_client<PickAndPlace>( this->get_node_base_interface(), this->get_node_graph_interface(), this->get_node_logging_interface(), this->get_node_waitables_interface(), "pick_and_place" // 动作名称,必须与服务器一致 ); this->timer_ = this->create_wall_timer( std::chrono::milliseconds(500), std::bind(&PickAndPlaceActionClient::send_goal, this)); } void send_goal() { // 只发送一次目标 this->timer_->cancel(); // 等待动作服务器上线 if (!this->client_->wait_for_action_server(10s)) { RCLCPP_ERROR(this->get_logger(), "动作服务器未在10秒内响应"); rclcpp::shutdown(); return; } // 构造目标消息 auto goal_msg = PickAndPlace::Goal(); goal_msg.object_id = "red_box"; goal_msg.pick_position.x = 0.5; goal_msg.pick_position.y = 0.2; goal_msg.pick_position.z = 0.1; goal_msg.place_position.x = -0.3; goal_msg.place_position.y = 0.4; goal_msg.place_position.z = 0.1; RCLCPP_INFO(this->get_logger(), "发送拾取放置目标..."); // 设置发送目标的选项 auto send_goal_options = rclcpp_action::Client<PickAndPlace>::SendGoalOptions(); send_goal_options.goal_response_callback = std::bind(&PickAndPlaceActionClient::goal_response_callback, this, std::placeholders::_1); send_goal_options.feedback_callback = std::bind(&PickAndPlaceActionClient::feedback_callback, this, std::placeholders::_1, std::placeholders::_2); send_goal_options.result_callback = std::bind(&PickAndPlaceActionClient::result_callback, this, std::placeholders::_1); // 异步发送目标,并立即返回 this->client_->async_send_goal(goal_msg, send_goal_options); } private: rclcpp_action::Client<PickAndPlace>::SharedPtr client_; rclcpp::TimerBase::SharedPtr timer_; // 目标响应回调:服务器是否接受了我们的目标? void goal_response_callback(std::shared_future<GoalHandlePickAndPlace::SharedPtr> future) { auto goal_handle = future.get(); if (!goal_handle) { RCLCPP_ERROR(this->get_logger(), "目标被服务器拒绝"); } else { RCLCPP_INFO(this->get_logger(), "目标已被服务器接受,正在执行"); // 这里可以保存goal_handle,用于后续可能的取消操作 // this->goal_handle_ = goal_handle; } } // 反馈回调:定期接收来自服务器的进度更新 void feedback_callback( GoalHandlePickAndPlace::SharedPtr, const std::shared_ptr<const PickAndPlace::Feedback> feedback) { RCLCPP_INFO(this->get_logger(), "收到反馈: 阶段[%s], 进度 %.0f%%", feedback->current_phase.c_str(), feedback->percent_complete); } // 结果回调:任务最终完成时调用 void result_callback(const GoalHandlePickAndPlace::WrappedResult & result) { switch (result.code) { case rclcpp_action::ResultCode::SUCCEEDED: RCLCPP_INFO(this->get_logger(), "任务成功! 结果: %s", result.result->message.c_str()); break; case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(this->get_logger(), "任务被中止"); break; case rclcpp_action::ResultCode::CANCELED: RCLCPP_WARN(this->get_logger(), "任务被取消"); break; default: RCLCPP_ERROR(this->get_logger(), "未知结果代码"); break; } // 结果收到,可以关闭节点或发送新目标 rclcpp::shutdown(); } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<PickAndPlaceActionClient>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }

5.2 客户端实现关键点剖析

  1. 等待服务器:在发送目标前,必须使用client_->wait_for_action_server()等待服务器上线。这是一个好习惯,可以避免发送目标到不存在的服务器。
  2. 异步发送与回调async_send_goal是异步调用,它立即返回,不会阻塞。你需要通过SendGoalOptions设置三个关键回调函数:
    • goal_response_callback:通知你服务器是接受了还是拒绝了目标。
    • feedback_callback:在任务执行过程中,每当服务器发布反馈时被调用。
    • result_callback:当任务最终完成(成功、失败、取消)时被调用,并携带最终的结果。
  3. 结果处理result_callback的参数WrappedResult包含了结果码(ResultCode)和具体的结果消息(result)。根据结果码进行不同的处理是客户端逻辑的核心。
  4. 取消操作:示例中没有展示取消,但实际操作很简单。如果你保存了goal_handle(在goal_response_callback中获得),可以调用client_->async_cancel_goal(goal_handle)来发送取消请求。

6. 编译、运行与调试实战

现在,让我们把代码跑起来,看看动作通信的完整流程。

6.1 修改CMakeLists.txt以编译节点

CMakeLists.txtament_package()之前,添加可执行目标的构建指令:

add_executable(action_server src/pick_and_place_server.cpp) ament_target_dependencies(action_server rclcpp rclcpp_action geometry_msgs) rosidl_target_interfaces(action_server ${PROJECT_NAME} "rosidl_typesupport_cpp") add_executable(action_client src/pick_and_place_client.cpp) ament_target_dependencies(action_client rclcpp rclcpp_action geometry_msgs) rosidl_target_interfaces(action_client ${PROJECT_NAME} "rosidl_typesupport_cpp") install(TARGETS action_server action_client DESTINATION lib/${PROJECT_NAME} )

6.2 运行与观察

  1. 编译

    cd ~/ros2_action_ws colcon build --packages-select cpp_action_tutorial source install/setup.bash
  2. 启动动作服务器(终端1):

    ros2 run cpp_action_tutorial action_server

    你应该看到输出:Pick and Place Action Server 已启动,等待目标...

  3. 启动动作客户端(终端2):

    ros2 run cpp_action_tutorial action_client

    观察两个终端的输出。客户端会发送目标,服务器会开始执行并发布反馈,最终客户端会收到成功的结果。

  4. 使用命令行工具监控(终端3):

    # 查看动作列表 ros2 action list # 你应该能看到 /pick_and_place # 查看动作信息 ros2 action info /pick_and_place # 这会显示该动作的服务器和客户端数量 # 手动发送目标(可选) ros2 action send_goal /pick_and_place cpp_action_tutorial/action/PickAndPlace "{pick_position: {x: 0.1, y: 0.2, z: 0.3}, place_position: {x: 0.4, y: 0.5, z: 0.6}, object_id: 'test'}" --feedback # --feedback 参数会让你实时看到反馈信息

6.3 测试取消功能

为了测试取消,我们可以修改客户端,在发送目标后稍等片刻就发起取消。在send_goal函数中,async_send_goal之后添加:

// 在 async_send_goal 后,启动一个定时器,3秒后取消任务 this->cancel_timer_ = this->create_wall_timer( 3s, [this, goal_handle_future]() { // 注意:这里需要一种方式获取到goal_handle,实际代码需调整 RCLCPP_INFO(this->get_logger(), "尝试取消任务..."); // this->client_->async_cancel_goal(goal_handle); // 由于goal_handle在回调中,实际实现可能需要一个类成员变量来保存它 });

更健壮的做法是在goal_response_callback中将返回的goal_handle保存为类的成员变量(例如this->goal_handle_),然后在定时器回调中调用this->client_->async_cancel_goal(this->goal_handle_)。运行修改后的客户端,你会看到服务器在MOVING_TO_PICKGRASPING阶段接收到取消请求,并终止任务,客户端在result_callback中收到CANCELED的结果码。

7. 进阶话题与生产环境注意事项

掌握了基础实现后,要将其用于实际机器人项目,还需要考虑更多。

7.1 动作服务器的并发与抢占处理

我们的示例服务器一次只处理一个目标。但在现实中,新的目标可能在旧目标未完成时到达。handle_goal回调的返回值决定了服务器的行为:

  • ACCEPT_AND_EXECUTE:接受新目标并立即执行。如果已有任务在执行,你需要决定是让新目标排队,还是抢占旧目标。抢占需要你在execute函数中检查是否有新目标被接受,并安全地终止当前任务。
  • ACCEPT_AND_DEFER:接受新目标,但暂不执行。你可以将其放入队列,等当前任务完成后再执行。
  • REJECT:直接拒绝新目标。

实现一个健壮的、支持任务队列或优先级抢占的动作服务器是复杂的,需要仔细设计任务调度逻辑和状态管理。

7.2 反馈频率与网络负载

反馈消息虽然有用,但发送过于频繁(例如在高频控制循环中)会占用大量网络带宽和CPU资源。一个常见的优化策略是节流(Throttling):在服务器端,不要每次循环都发布反馈,而是累积一定时间或达到某个进度阈值后再发布。例如:

auto now = this->now(); if ((now - last_feedback_time_).seconds() > 0.1) { // 每100ms发布一次反馈 goal_handle->publish_feedback(feedback); last_feedback_time_ = now; }

7.3 超时与错误处理

  • 客户端超时:客户端发送目标后,可能希望在一定时间内得到结果,否则视为失败。这可以通过在发送目标后启动一个定时器来实现,在result_callback或定时器回调中处理超时逻辑。
  • 服务器端错误:在execute函数中,如果发生不可恢复的错误(如机械臂失联、坐标越界),应调用goal_handle->abort(result)来中止任务,并将错误信息通过result传回客户端。
  • 连接丢失:网络可能中断。客户端和服务器都需要考虑对端可能意外消失的情况。rclcpp_action库在一定程度上处理了连接问题,但你的应用层逻辑可能需要重置状态或重试。

7.4 与现有框架集成(如MoveIt 2)

在真实的机械臂应用中,你的动作服务器execute函数内部很可能是调用 MoveIt 2 的MoveGroupInterface来执行运动规划。例如:

// 伪代码 moveit::planning_interface::MoveGroupInterface move_group(node, "arm_group"); move_group.setPoseTarget(pick_pose); moveit::planning_interface::MoveGroupInterface::Plan my_plan; bool success = (move_group.plan(my_plan) == moveit::core::MoveItErrorCode::SUCCESS); if(success) { move_group.execute(my_plan); // 发布“移动到拾取点完成”的反馈 } // ... 后续进行抓取、移动、放置等操作

这时,你的动作服务器就成了一个高级任务编排器,将复杂的“拾取-放置”序列分解为一系列 MoveIt 2 可以执行的基本动作,并通过动作接口向外部提供一个统一、可监控的服务。

从理解动作的三层通信模型,到亲手实现一个完整的、带取消和反馈的C++动作服务器与客户端,我们走完了ROS2动作开发的核心路径。动作是ROS2中构建可靠、复杂机器人任务的基石。记住,关键不在于记住API,而在于理解其背后的状态机思想和异步编程模式。当你下次需要让机器人执行一个“需要时间、需要知道进度、可能被打断”的任务时,动作应该是你脑海中的首选方案。在实际项目中,多考虑边界情况(并发、取消、错误恢复),你的机器人系统将会更加稳健。