第 6 章:Action 动作通信

第 6 章:Action 动作通信

本章目标:理解 Action 的设计动机和架构,掌握自定义 Action 接口,编写完整的 Action Server 和 Client,并通过"导航到目标点"的实战案例建立直观认识。


6.1 为什么需要 Action?

6.1.1 Topic 和 Service 的局限性

回顾前两章的内容:

通信机制优势局限性
Topic持续流、松耦合、多对多无反馈、无结果确认、不适合离散任务
Service请求-响应、有结果阻塞风险、无中间进度、无法取消

考虑一个实际场景:机器人导航到目标点

如果用Topic

Client 发布 Goal ──→ Server 接收 ↑ ↓ └──── ??? ───────────┘ ← 不知道进度、不知道是否到达、无法中途取消

如果用Service

Client ──Request(Goal)──→ Server │ ↓ │ [阻塞等待...] [长时间处理中...] │ [可能几秒到几分钟] [无法反馈进度] │ ↓ │ ←─Response(Result)── ← 只有最终结果,中间一无所知

Service 的问题

  • 导航可能需要几秒到几分钟,同步调用会长时间阻塞

  • 用户想知道"走到哪了""还剩多远"

  • 中途遇到障碍物需要重新规划,用户想取消当前任务

  • Service 无法提供这些能力

6.1.2 Action 的设计目标

Action 是为了解决长时间运行任务而设计的通信机制,它结合了:

  • Service 的请求-响应模式(发送 Goal,接收 Result)

  • Topic 的持续反馈机制(实时报告进度)

Action 的核心能力:

表格

能力说明
发送目标(Goal)Client 向 Server 发送任务目标
实时反馈(Feedback)Server 持续报告执行进度
获取结果(Result)Server 返回最终执行结果
取消任务(Cancel)Client 可随时请求取消正在执行的任务
目标抢占(Preempt)新 Goal 可覆盖旧 Goal
6.1.3 Action vs Service 时序对比

Service 同步调用时序:

Action 时序:


6.2 Action 的三部分组成:Goal / Feedback / Result

Action 接口定义文件使用.action扩展名,包含三个部分,用---分隔:

plain

# Goal: 客户端发送的目标 --- # Result: 服务器返回的最终结果 --- # Feedback: 执行过程中的实时反馈
6.2.1 三部分详解
部分方向次数用途
GoalClient → Server一次定义任务目标,Server 可选择接受或拒绝
FeedbackServer → Client多次(持续)报告进度、中间状态
ResultServer → Client一次返回最终执行结果
6.2.2 实际案例:导航到目标点

创建~/ros2_ws/src/my_action_pkg/action/NavigateToPose.action

# Goal geometry_msgs/PoseStamped target_pose float32 tolerance # 到达容忍距离(米) bool avoid_obstacles # 是否启用避障 --- # Result bool success string message # 状态描述 float32 total_distance # 实际行驶距离 duration total_time # 实际耗时 --- # Feedback geometry_msgs/Pose current_pose float32 distance_remaining # 剩余距离 float32 estimated_time # 预计剩余时间 string current_state # 当前状态:"规划路径"/"行驶中"/"避障中"
6.2.3 另一个案例:机械臂抓取

plain

# Goal string object_id # 目标物体 ID geometry_msgs/Pose grasp_pose # 抓取位姿 float32 force_limit # 最大夹持力 --- # Result bool success string message float32 actual_force # 实际夹持力 --- # Feedback string phase # "接近"/"抓取"/"提升"/"验证" float32 progress # 0.0 ~ 1.0 geometry_msgs/Pose current_pose

6.3 自定义 Action 接口(.action 文件)

6.3.1 创建 Action 功能包

bash

cd ~/ros2_ws/src ros2 pkg create --build-type ament_cmake --license Apache-2.0 my_action_pkg cd my_action_pkg mkdir action
6.3.2 编写 .action 文件

创建~/ros2_ws/src/my_action_pkg/action/Fibonacci.action(ROS2 官方教学示例,简单易懂):

plain

# Goal: 计算斐波那契数列的前 n 个数 int32 order --- # Result: 完整的数列 int32[] sequence --- # Feedback: 当前已计算的部分数列 int32[] partial_sequence

创建~/ros2_ws/src/my_action_pkg/action/NavigateToPose.action(实战案例):

# Goal geometry_msgs/PoseStamped target_pose float32 tolerance --- # Result bool success string message float32 distance_traveled --- # Feedback geometry_msgs/Pose current_pose float32 distance_remaining string status
6.3.3 配置 package.xml
<?xml version="1.0"?> <package format="3"> <name>my_action_pkg</name> <version>0.0.1</version> <description>Action definitions for robot tasks</description> <maintainer email="user@example.com">User</maintainer> <license>Apache-2.0</license> <buildtool_depend>ament_cmake</buildtool_depend> <build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group> <depend>geometry_msgs</depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <export> <build_type>ament_cmake</build_type> </export> </package>
6.3.4 配置 CMakeLists.txt
cmake_minimum_required(VERSION 3.8) project(my_action_pkg) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() find_package(ament_cmake REQUIRED) find_package(rosidl_default_generators REQUIRED) find_package(geometry_msgs REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "action/Fibonacci.action" "action/NavigateToPose.action" DEPENDENCIES geometry_msgs ) ament_export_dependencies(rosidl_default_runtime) ament_package()
6.3.5 编译与验证
cd ~/ros2_ws colcon build --packages-select my_action_pkg source install/setup.bash # 查看生成的 Action 接口 ros2 interface show my_action_pkg/action/Fibonacci ros2 interface show my_action_pkg/action/NavigateToPose

生成原理rosidl_generate_interfaces会在编译时自动生成 C++ 类(Fibonacci::GoalFibonacci::FeedbackFibonacci::Result)和 Python 模块。这些生成的代码包含序列化逻辑,使 Action 可以通过 DDS 传输。


6.4 编写 Action Server 和 Client

6.4.1 Action Server(C++)

Server 负责接收 Goal、执行动作、发送 Feedback、返回 Result。

创建~/ros2_ws/src/my_action_pkg/src/fibonacci_action_server.cpp

#include <functional> #include <memory> #include <thread> #include "rclcpp/rclcpp.hpp" #include "rclcpp_action/rclcpp_action.hpp" #include "my_action_pkg/action/fibonacci.hpp" class FibonacciActionServer : public rclcpp::Node { public: using Fibonacci = my_action_pkg::action::Fibonacci; using GoalHandleFibonacci = rclcpp_action::ServerGoalHandle<Fibonacci>; explicit FibonacciActionServer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()) : Node("fibonacci_action_server", options) { using namespace std::placeholders; // 创建 Action Server this->action_server_ = rclcpp_action::create_server<Fibonacci>( this, "fibonacci", // Action 名称 std::bind(&FibonacciActionServer::handle_goal, this, _1, _2), // 处理 Goal std::bind(&FibonacciActionServer::handle_cancel, this, _1), // 处理 Cancel std::bind(&FibonacciActionServer::handle_accepted, this, _1)); // Goal 被接受后 } private: rclcpp_action::Server<Fibonacci>::SharedPtr action_server_; // 处理收到的 Goal:决定是否接受 rclcpp_action::GoalResponse handle_goal( const rclcpp_action::GoalUUID & uuid, std::shared_ptr<const Fibonacci::Goal> goal) { RCLCPP_INFO(this->get_logger(), "Received goal request with order %d", goal->order); // 拒绝不合理的 Goal if (goal->order < 0) { RCLCPP_WARN(this->get_logger(), "Rejecting goal: order must be >= 0"); return rclcpp_action::GoalResponse::REJECT; } RCLCPP_INFO(this->get_logger(), "Accepting goal"); return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; } // 处理取消请求 rclcpp_action::CancelResponse handle_cancel( const std::shared_ptr<GoalHandleFibonacci> goal_handle) { RCLCPP_INFO(this->get_logger(), "Received request to cancel goal"); (void)goal_handle; return rclcpp_action::CancelResponse::ACCEPT; } // Goal 被接受后:启动执行线程 void handle_accepted(const std::shared_ptr<GoalHandleFibonacci> goal_handle) { using namespace std::placeholders; // 在新线程中执行,避免阻塞 Executor std::thread{std::bind(&FibonacciActionServer::execute, this, _1), goal_handle}.detach(); } // 实际执行逻辑 void execute(const std::shared_ptr<GoalHandleFibonacci> goal_handle) { RCLCPP_INFO(this->get_logger(), "Executing goal..."); const auto goal = goal_handle->get_goal(); auto feedback = std::make_shared<Fibonacci::Feedback>(); auto result = std::make_shared<Fibonacci::Result>(); // 初始化斐波那契数列 auto & sequence = feedback->partial_sequence; sequence.push_back(0); sequence.push_back(1); // 执行循环 rclcpp::Rate loop_rate(1); // 1Hz for (int i = 1; i < goal->order && rclcpp::ok(); ++i) { // 检查是否收到取消请求 if (goal_handle->is_canceling()) { result->sequence = sequence; goal_handle->canceled(result); RCLCPP_INFO(this->get_logger(), "Goal canceled"); return; } // 计算下一个数 sequence.push_back(sequence[i] + sequence[i - 1]); // 发布 Feedback goal_handle->publish_feedback(feedback); RCLCPP_INFO(this->get_logger(), "Publish feedback: [%s]", sequence_to_string(sequence).c_str()); loop_rate.sleep(); } // 检查 Goal 是否仍然有效(未被抢占) if (rclcpp::ok()) { result->sequence = sequence; goal_handle->succeed(result); RCLCPP_INFO(this->get_logger(), "Goal succeeded"); } } std::string sequence_to_string(const std::vector<int32_t> & seq) { std::string s; for (auto num : seq) { s += std::to_string(num) + " "; } return s; } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<FibonacciActionServer>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }

Action Server 核心 API:

方法作用
create_server<>创建 Action Server,注册三个回调
handle_goal()收到 Goal 时调用,返回 ACCEPT/REJECT
handle_cancel()收到 Cancel 请求时调用
handle_accepted()Goal 被接受后调用,通常启动执行线程
publish_feedback()发送 Feedback
succeed(result)任务成功完成
canceled(result)任务被取消
abort(result)任务异常终止
is_canceling()检查是否收到取消请求

关键设计execute()在独立线程中运行,避免阻塞 Executor 的事件循环。这样 Server 可以同时处理其他 Topic/Service 回调。

6.4.2 Action Client — 异步版本(C++)

创建~/ros2_ws/src/my_action_pkg/src/fibonacci_action_client.cpp

#include <functional> #include <future> #include <memory> #include <string> #include <sstream> #include "rclcpp/rclcpp.hpp" #include "rclcpp_action/rclcpp_action.hpp" #include "my_action_pkg/action/fibonacci.hpp" class FibonacciActionClient : public rclcpp::Node { public: using Fibonacci = my_action_pkg::action::Fibonacci; using GoalHandleFibonacci = rclcpp_action::ClientGoalHandle<Fibonacci>; explicit FibonacciActionClient(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()) : Node("fibonacci_action_client", options) { // 创建 Action Client this->client_ptr_ = rclcpp_action::create_client<Fibonacci>(this, "fibonacci"); // 定时器:等待 Server 就绪后发送 Goal this->timer_ = this->create_wall_timer( std::chrono::milliseconds(500), std::bind(&FibonacciActionClient::send_goal, this)); } void send_goal() { using namespace std::placeholders; // 取消定时器(只发送一次) this->timer_->cancel(); // 等待 Server 就绪 if (!this->client_ptr_->wait_for_action_server(std::chrono::seconds(10))) { RCLCPP_ERROR(this->get_logger(), "Action server not available after waiting"); rclcpp::shutdown(); return; } // 构造 Goal auto goal_msg = Fibonacci::Goal(); goal_msg.order = 10; RCLCPP_INFO(this->get_logger(), "Sending goal: order %d", goal_msg.order); // 配置 Goal 选项 auto send_goal_options = rclcpp_action::Client<Fibonacci>::SendGoalOptions(); send_goal_options.goal_response_callback = std::bind(&FibonacciActionClient::goal_response_callback, this, _1); send_goal_options.feedback_callback = std::bind(&FibonacciActionClient::feedback_callback, this, _1, _2); send_goal_options.result_callback = std::bind(&FibonacciActionClient::result_callback, this, _1); // 异步发送 Goal this->client_ptr_->async_send_goal(goal_msg, send_goal_options); } private: rclcpp_action::Client<Fibonacci>::SharedPtr client_ptr_; rclcpp::TimerBase::SharedPtr timer_; // Goal 被接受/拒绝的回调 void goal_response_callback(const GoalHandleFibonacci::SharedPtr & goal_handle) { if (!goal_handle) { RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server"); } else { RCLCPP_INFO(this->get_logger(), "Goal accepted by server, waiting for result"); } } // 收到 Feedback 的回调 void feedback_callback( GoalHandleFibonacci::SharedPtr, const std::shared_ptr<const Fibonacci::Feedback> feedback) { std::stringstream ss; ss << "Next number in sequence received: "; for (auto num : feedback->partial_sequence) { ss << num << " "; } RCLCPP_INFO(this->get_logger(), ss.str().c_str()); } // 收到 Result 的回调 void result_callback(const GoalHandleFibonacci::WrappedResult & result) { switch (result.code) { case rclcpp_action::ResultCode::SUCCEEDED: RCLCPP_INFO(this->get_logger(), "Goal was succeeded"); break; case rclcpp_action::ResultCode::ABORTED: RCLCPP_ERROR(this->get_logger(), "Goal was aborted"); return; case rclcpp_action::ResultCode::CANCELED: RCLCPP_ERROR(this->get_logger(), "Goal was canceled"); return; default: RCLCPP_ERROR(this->get_logger(), "Unknown result code"); return; } std::stringstream ss; ss << "Result sequence: "; for (auto num : result.result->sequence) { ss << num << " "; } RCLCPP_INFO(this->get_logger(), ss.str().c_str()); rclcpp::shutdown(); } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<FibonacciActionClient>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }

Action Client 核心 API:

方法作用
create_client<>创建 Action Client
wait_for_action_server()等待 Server 就绪
async_send_goal()异步发送 Goal
async_cancel_goal()异步取消 Goal
goal_response_callbackGoal 被接受/拒绝时触发
feedback_callback收到 Feedback 时触发
result_callback收到 Result 时触发
6.4.3 配置与编译

CMakeLists.txt:

cmake_minimum_required(VERSION 3.8) project(my_action_pkg) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED) find_package(rclcpp_action REQUIRED) find_package(rosidl_default_generators REQUIRED) find_package(geometry_msgs REQUIRED) # 生成 Action 接口 rosidl_generate_interfaces(${PROJECT_NAME} "action/Fibonacci.action" "action/NavigateToPose.action" DEPENDENCIES geometry_msgs ) ament_export_dependencies(rosidl_default_runtime) # 可执行文件 find_package(my_action_pkg REQUIRED) # 需要自身生成的接口 add_executable(fibonacci_action_server src/fibonacci_action_server.cpp) ament_target_dependencies(fibonacci_action_server rclcpp rclcpp_action my_action_pkg) add_executable(fibonacci_action_client src/fibonacci_action_client.cpp) ament_target_dependencies(fibonacci_action_client rclcpp rclcpp_action my_action_pkg) install(TARGETS fibonacci_action_server fibonacci_action_client DESTINATION lib/${PROJECT_NAME} ) ament_package()

编译与运行:

cd ~/ros2_ws colcon build --packages-select my_action_pkg source install/setup.bash # 终端 1:启动 Server ros2 run my_action_pkg fibonacci_action_server # 终端 2:启动 Client ros2 run my_action_pkg fibonacci_action_client

预期输出:

# Server [INFO] [fibonacci_action_server]: Received goal request with order 10 [INFO] [fibonacci_action_server]: Accepting goal [INFO] [fibonacci_action_server]: Executing goal... [INFO] [fibonacci_action_server]: Publish feedback: [0 1 1 ] [INFO] [fibonacci_action_server]: Publish feedback: [0 1 1 2 ] ... [INFO] [fibonacci_action_server]: Goal succeeded # Client [INFO] [fibonacci_action_client]: Sending goal: order 10 [INFO] [fibonacci_action_client]: Goal accepted by server, waiting for result [INFO] [fibonacci_action_client]: Next number in sequence received: 0 1 1 [INFO] [fibonacci_action_client]: Next number in sequence received: 0 1 1 2 ... [INFO] [fibonacci_action_client]: Goal was succeeded [INFO] [fibonacci_action_client]: Result sequence: 0 1 1 2 3 5 8 13 21 34 55
6.4.4 取消任务示例

在 Client 中添加取消逻辑:

// 发送 Goal 后 3 秒取消 auto cancel_timer = this->create_wall_timer( std::chrono::seconds(3), [this, goal_handle]() { RCLCPP_INFO(this->get_logger(), "Canceling goal..."); this->client_ptr_->async_cancel_goal(goal_handle); });
6.4.5 Python 版本

Server:

Python

# my_action_pkg/my_action_pkg/fibonacci_action_server.py import rclpy from rclpy.action import ActionServer from rclpy.node import Node from my_action_pkg.action import Fibonacci class FibonacciActionServer(Node): def __init__(self): super().__init__('fibonacci_action_server') self._action_server = ActionServer( self, Fibonacci, 'fibonacci', self.execute_callback) async def execute_callback(self, goal_handle): self.get_logger().info('Executing goal...') feedback_msg = Fibonacci.Feedback() feedback_msg.partial_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('Goal canceled') return Fibonacci.Result() feedback_msg.partial_sequence.append( feedback_msg.partial_sequence[i] + feedback_msg.partial_sequence[i-1]) self.get_logger().info(f'Feedback: {feedback_msg.partial_sequence}') goal_handle.publish_feedback(feedback_msg) # 模拟耗时操作 import time time.sleep(1) goal_handle.succeed() result = Fibonacci.Result() result.sequence = feedback_msg.partial_sequence self.get_logger().info('Goal succeeded') return result def main(args=None): rclpy.init(args=args) node = FibonacciActionServer() rclpy.spin(node) rclpy.shutdown() if __name__ == '__main__': main()

Client:

Python

# my_action_pkg/my_action_pkg/fibonacci_action_client.py import rclpy from rclpy.action import ActionClient from rclpy.node import Node from my_action_pkg.action import Fibonacci class FibonacciActionClient(Node): def __init__(self): super().__init__('fibonacci_action_client') self._action_client = ActionClient(self, Fibonacci, 'fibonacci') def send_goal(self, order): goal_msg = Fibonacci.Goal() goal_msg.order = order self._action_client.wait_for_server() self.get_logger().info(f'Sending goal: order {order}') self._send_goal_future = self._action_client.send_goal_async( goal_msg, feedback_callback=self.feedback_callback) self._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('Goal rejected') return self.get_logger().info('Goal accepted') self._get_result_future = goal_handle.get_result_async() self._get_result_future.add_done_callback(self.get_result_callback) def feedback_callback(self, feedback_msg): sequence = feedback_msg.feedback.partial_sequence self.get_logger().info(f'Feedback: {sequence}') def get_result_callback(self, future): result = future.result().result self.get_logger().info(f'Result: {result.sequence}') rclpy.shutdown() def main(args=None): rclpy.init(args=args) node = FibonacciActionClient() node.send_goal(10) rclpy.spin(node) if __name__ == '__main__': main()

6.5 实战:实现"导航到目标点"动作(模拟 AGV 运动控制)

6.5.1 场景描述

模拟一个 AGV(自动导引车)从当前位置导航到目标位置的过程:

  • 收到 Goal:目标位姿(x, y, theta)

  • 执行过程:直线行驶 + 转向,实时反馈当前位置和剩余距离

  • 可能事件:遇到障碍物需暂停、用户取消任务、新目标抢占

  • 最终结果:成功到达 / 失败 / 取消

6.5.2 Action 接口定义

使用前面定义的NavigateToPose.action

geometry_msgs/PoseStamped target_pose float32 tolerance --- bool success string message float32 distance_traveled --- geometry_msgs/Pose current_pose float32 distance_remaining string status
6.5.3 模拟 AGV Action Server

创建~/ros2_ws/src/my_action_pkg/src/navigate_action_server.cpp

#include <cmath> #include <memory> #include <string> #include <thread> #include "rclcpp/rclcpp.hpp" #include "rclcpp_action/rclcpp_action.hpp" #include "geometry_msgs/msg/pose.hpp" #include "geometry_msgs/msg/pose_stamped.hpp" #include "my_action_pkg/action/navigate_to_pose.hpp" class NavigateActionServer : public rclcpp::Node { public: using NavigateToPose = my_action_pkg::action::NavigateToPose; using GoalHandleNavigate = rclcpp_action::ServerGoalHandle<NavigateToPose>; NavigateActionServer() : Node("navigate_action_server"), current_x_(0.0), current_y_(0.0), current_theta_(0.0) { using namespace std::placeholders; action_server_ = rclcpp_action::create_server<NavigateToPose>( this, "navigate_to_pose", std::bind(&NavigateActionServer::handle_goal, this, _1, _2), std::bind(&NavigateActionServer::handle_cancel, this, _1), std::bind(&NavigateActionServer::handle_accepted, this, _1)); RCLCPP_INFO(this->get_logger(), "Navigate Action Server ready"); } private: rclcpp_action::Server<NavigateToPose>::SharedPtr action_server_; double current_x_, current_y_, current_theta_; rclcpp_action::GoalResponse handle_goal( const rclcpp_action::GoalUUID &, std::shared_ptr<const NavigateToPose::Goal> goal) { RCLCPP_INFO(this->get_logger(), "Received navigation goal: [%.2f, %.2f, %.2f]", goal->target_pose.pose.position.x, goal->target_pose.pose.position.y, get_yaw_from_quaternion(goal->target_pose.pose.orientation)); return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; } rclcpp_action::CancelResponse handle_cancel( const std::shared_ptr<GoalHandleNavigate> goal_handle) { RCLCPP_INFO(this->get_logger(), "Received cancel request"); (void)goal_handle; return rclcpp_action::CancelResponse::ACCEPT; } void handle_accepted(const std::shared_ptr<GoalHandleNavigate> goal_handle) { using namespace std::placeholders; std::thread{std::bind(&NavigateActionServer::execute, this, _1), goal_handle}.detach(); } void execute(const std::shared_ptr<GoalHandleNavigate> goal_handle) { const auto goal = goal_handle->get_goal(); double target_x = goal->target_pose.pose.position.x; double target_y = goal->target_pose.pose.position.y; double tolerance = goal->tolerance; auto feedback = std::make_shared<NavigateToPose::Feedback>(); auto result = std::make_shared<NavigateToPose::Result>(); double total_distance = 0.0; rclcpp::Rate loop_rate(10); // 10Hz 控制频率 while (rclcpp::ok()) { // 检查取消 if (goal_handle->is_canceling()) { result->success = false; result->message = "Canceled by user"; result->distance_traveled = total_distance; goal_handle->canceled(result); RCLCPP_INFO(this->get_logger(), "Navigation canceled"); return; } // 计算距离 double dx = target_x - current_x_; double dy = target_y - current_y_; double distance = std::sqrt(dx * dx + dy * dy); // 检查是否到达 if (distance < tolerance) { result->success = true; result->message = "Goal reached successfully"; result->distance_traveled = total_distance; goal_handle->succeed(result); RCLCPP_INFO(this->get_logger(), "Navigation succeeded"); return; } // 模拟运动(向目标移动一小步) double step = 0.05; // 每周期移动 0.05m if (distance < step) step = distance; current_x_ += step * dx / distance; current_y_ += step * dy / distance; total_distance += step; // 发布 Feedback feedback->current_pose.position.x = current_x_; feedback->current_pose.position.y = current_y_; feedback->distance_remaining = distance; if (distance > 2.0) { feedback->status = "行驶中"; } else if (distance > 0.5) { feedback->status = "接近目标"; } else { feedback->status = "精确定位中"; } goal_handle->publish_feedback(feedback); loop_rate.sleep(); } } double get_yaw_from_quaternion(const geometry_msgs::msg::Quaternion & q) { // 简化:从四元数提取偏航角 return std::atan2(2.0 * (q.w * q.z + q.x * q.y), 1.0 - 2.0 * (q.y * q.y + q.z * q.z)); } }; int main(int argc, char ** argv) { rclcpp::init(argc, argv); auto node = std::make_shared<NavigateActionServer>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }
6.5.4 导航客户端与 RViz 可视化
// navigate_action_client.cpp(关键片段) void result_callback(const GoalHandleNavigate::WrappedResult & result) { if (result.code == rclcpp_action::ResultCode::SUCCEEDED) { RCLCPP_INFO(this->get_logger(), "Navigation succeeded! Traveled: %.2f m", result.result->distance_traveled); } } void feedback_callback( GoalHandleNavigate::SharedPtr, const std::shared_ptr<const NavigateToPose::Feedback> feedback) { RCLCPP_INFO(this->get_logger(), "Status: %s, Remaining: %.2f m, Pos: [%.2f, %.2f]", feedback->status.c_str(), feedback->distance_remaining, feedback->current_pose.position.x, feedback->current_pose.position.y); }
6.5.5 命令行测试
# 查看 Action 列表 ros2 action list # /fibonacci # /navigate_to_pose # 查看 Action 信息 ros2 action info /navigate_to_pose # 发送 Goal(命令行) ros2 action send_goal /navigate_to_pose my_action_pkg/action/NavigateToPose \ "{target_pose: {pose: {position: {x: 5.0, y: 3.0, z: 0.0}}}, tolerance: 0.1}" # 带反馈显示 ros2 action send_goal /navigate_to_pose my_action_pkg/action/NavigateToPose \ "{target_pose: {pose: {position: {x: 5.0, y: 3.0}}}, tolerance: 0.1}" \ --feedback

6.6 Action 设计最佳实践

6.6.1 Server 侧
实践说明
独立线程执行execute()必须在独立线程中运行,避免阻塞 Executor
定期检查取消在循环中频繁调用is_canceling(),及时响应取消
合理的发送频率Feedback 频率建议 10~100Hz,过高会增加网络负载
优雅处理抢占新 Goal 到来时,正确终止旧任务(ROS2 Action Server 自动处理)
状态机设计复杂任务使用状态机管理(INIT → RUNNING → PAUSED → DONE)
6.6.2 Client 侧
实践说明
异步优先始终使用async_send_goal(),避免阻塞
处理所有回调必须注册goal_responsefeedbackresult三个回调
超时处理设置合理的等待超时,Server 未就绪时降级
取消机制提供用户取消接口(如 GUI 的"停止"按钮)
6.6.3 Action 底层实现

Action 底层基于三个 Topic实现:

Topic方向用途
/action_name/_action/send_goalClient → Server发送 Goal
/action_name/_action/cancel_goalClient → Server发送 Cancel
/action_name/_action/statusServer → Client发布 Goal 状态
/action_name/_action/feedbackServer → Client发布 Feedback
/action_name/_action/get_resultClient ↔ Server请求/返回 Result

这些 Topic 由rclcpp_action自动管理,开发者无需直接操作。


6.7 三种通信机制对比总结

特性TopicServiceAction
通信模式Pub-SubReq-ResGoal-Feedback-Result
方向性单向双向双向 + 持续反馈
调用次数持续流一次一次 Goal + 多次 Feedback + 一次 Result
阻塞性非阻塞同步/异步异步
取消能力
进度反馈
抢占能力
典型应用传感器数据参数查询导航、抓取、长时间任务