Action是ROS2中最复杂的通信方式,专为长时间运行的异步任务设计。它结合了Topic和Service的优点:支持目标下发(类似Service)、进度反馈(类似Topic)、结果返回(类似Service)。
| 特性 | Topic | Service | Action |
|---|---|---|---|
| 模型 | 发布/订阅 | 请求/响应 | 目标/反馈/结果 |
| 方向 | 单向 | 双向 | 双向 |
| 异步 | ✅ | ❌(同步等待) | ✅ |
| 反馈 | ❌ | ❌ | ✅ |
| 可取消 | ❌ | ❌ | ✅ |
| 适用场景 | 传感器数据 | 快速查询/操作 | 导航/运动/长时间任务 |
| 典型例子 | /scan, /image | /enable, /get_status | /navigate, /move_arm |
#!/usr/bin/env python3
"""导航Action服务端 - 模拟机器人导航到目标点"""
import math
import time
import rclpy
from rclpy.action import ActionServer, CancelResponse, GoalResponse
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.node import Node
from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped
class NavigationActionServer(Node):
"""导航Action服务端"""
def __init__(self):
super().__init__('navigation_action_server')
self._action_server = ActionServer(
self,
NavigateToPose,
'navigate_to_pose',
execute_callback=self.execute_callback,
goal_callback=self.goal_callback,
handle_accepted_callback=self.handle_accepted_callback,
cancel_callback=self.cancel_callback,
callback_group=ReentrantCallbackGroup()
)
# 当前机器人位置(模拟)
self.current_x = 0.0
self.current_y = 0.0
self.current_theta = 0.0
self.get_logger().info('🧭 导航Action服务端已启动')
def goal_callback(self, goal_request):
"""决定是否接受目标"""
pose = goal_request.pose
self.get_logger().info(
f'收到导航目标: ({pose.pose.position.x:.2f}, '
f'{pose.pose.position.y:.2f})'
)
return GoalResponse.ACCEPT
def handle_accepted_callback(self, goal_handle):
"""目标被接受后开始执行"""
self.get_logger().info('目标已接受,开始导航...')
goal_handle.execute()
def cancel_callback(self, goal_handle):
"""处理取消请求"""
self.get_logger().warn('⚠️ 收到取消导航请求')
return CancelResponse.ACCEPT
async def execute_callback(self, goal_handle):
"""执行导航(模拟)"""
target = goal_handle.request.pose.pose.position
target_x, target_y = target.x, target.y
# 计算总距离
dx = target_x - self.current_x
dy = target_y - self.current_y
total_dist = math.sqrt(dx**2 + dy**2)
step_size = 0.1 # 每步移动0.1m
num_steps = max(int(total_dist / step_size), 1)
self.get_logger().info(f'导航距离: {total_dist:.2f}m, 预计步数: {num_steps}')
result = NavigateToPose.Result()
feedback = NavigateToPose.Feedback()
for step in range(num_steps):
# 检查是否被取消
if goal_handle.is_cancel_requested:
goal_handle.canceled()
result.result = PoseStamped()
self.get_logger().info('导航已取消')
return result
# 模拟移动
progress = (step + 1) / num_steps
self.current_x += dx / num_steps
self.current_y += dy / num_steps
# 计算剩余距离
remaining = total_dist * (1.0 - progress)
# 发布反馈
feedback.current_pose = PoseStamped()
feedback.current_pose.pose.position.x = self.current_x
feedback.current_pose.pose.position.y = self.current_y
feedback.distance_remaining = remaining
feedback.navigation_time.sec = int(step * 0.1)
feedback.number_of_recoveries = 0
goal_handle.publish_feedback(feedback)
if step % 10 == 0:
self.get_logger().info(
f'导航进度: {progress*100:.0f}%, '
f'剩余: {remaining:.2f}m'
)
time.sleep(0.05) # 模拟耗时
# 导航完成
goal_handle.succeed()
result.result = PoseStamped()
result.result.pose.position.x = target_x
result.result.pose.position.y = target_y
self.get_logger().info(f'✅ 导航完成!到达 ({target_x:.2f}, {target_y:.2f})')
return result
def main(args=None):
rclpy.init(args=args)
node = NavigationActionServer()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""导航Action客户端 - 发送导航目标"""
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from nav2_msgs.action import NavigateToPose
from geometry_msgs.msg import PoseStamped
class NavigationActionClient(Node):
"""导航Action客户端"""
def __init__(self):
super().__init__('navigation_action_client')
self._action_client = ActionClient(
self, NavigateToPose, 'navigate_to_pose'
)
self._get_result_future = None
self._send_goal_future = None
def send_goal(self, x, y, theta=0.0):
"""发送导航目标"""
self.get_logger().info(f'发送导航目标: ({x}, {y})')
goal_msg = NavigateToPose.Goal()
goal_msg.pose = PoseStamped()
goal_msg.pose.header.frame_id = 'map'
goal_msg.pose.pose.position.x = float(x)
goal_msg.pose.pose.position.y = float(y)
# 设置朝向
import math
goal_msg.pose.pose.orientation.z = math.sin(theta / 2)
goal_msg.pose.pose.orientation.w = math.cos(theta / 2)
self._action_client.wait_for_server()
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().error('❌ 目标被拒绝')
return
self.get_logger().info('✅ 目标已接受')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.result_callback)
def feedback_callback(self, feedback_msg):
"""反馈回调"""
feedback = feedback_msg.feedback
self.get_logger().info(
f'📍 反馈: 剩余距离 {feedback.distance_remaining:.2f}m, '
f'当前位置 ({feedback.current_pose.pose.position.x:.2f}, '
f'{feedback.current_pose.pose.position.y:.2f})'
)
def result_callback(self, future):
"""结果回调"""
result = future.result().result
status = future.result().status
if status == 4: # SUCCEEDED
self.get_logger().info('🎉 导航成功完成!')
elif status == 5: # CANCELED
self.get_logger().warn('⚠️ 导航被取消')
else:
self.get_logger().error(f'❌ 导航失败,状态: {status}')
def main(args=None):
rclpy.init(args=None)
client = NavigationActionClient()
# 发送导航目标
client.send_goal(3.0, 2.0, theta=1.57)
rclpy.spin(client)
client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
# my_interfaces/action/MoveRobot.action
# Goal(目标)
float64 target_x
float64 target_y
float64 target_theta
float64 max_speed
---
# Result(结果)
float64 final_x
float64 final_y
float64 final_theta
float64 distance_traveled
float64 time_elapsed
bool success
string message
---
# Feedback(反馈)
float64 current_x
float64 current_y
float64 current_theta
float64 distance_remaining
float64 speed
float64 progress # 0.0 ~ 1.0
// move_robot_action_server.cpp
#include "rclcpp/rclcpp.hpp"
#include "rclcpp_action/rclcpp_action.hpp"
#include "my_interfaces/action/move_robot.hpp"
#include <memory>
#include <thread>
#include <cmath>
#include <chrono>
using MoveRobot = my_interfaces::action::MoveRobot;
using GoalHandleMoveRobot = rclcpp_action::ServerGoalHandle<MoveRobot>;
class MoveRobotActionServer : public rclcpp::Node {
public:
MoveRobotActionServer() : Node("move_robot_action_server") {
action_server_ = rclcpp_action::create_server<MoveRobot>(
this, "move_robot",
std::bind(&MoveRobotActionServer::handle_goal, this,
std::placeholders::_1, std::placeholders::_2),
std::bind(&MoveRobotActionServer::handle_cancel, this,
std::placeholders::_1),
std::bind(&MoveRobotActionServer::handle_accepted, this,
std::placeholders::_1));
RCLCPP_INFO(this->get_logger(), "C++ MoveRobot Action服务端已启动");
}
private:
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID&,
std::shared_ptr<const MoveRobot::Goal> goal) {
RCLCPP_INFO(this->get_logger(),
"收到目标: (%.2f, %.2f, %.2f)",
goal->target_x, goal->target_y, goal->target_theta);
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<GoalHandleMoveRobot>) {
RCLCPP_INFO(this->get_logger(), "收到取消请求");
return rclcpp_action::CancelResponse::ACCEPT;
}
void handle_accepted(
const std::shared_ptr<GoalHandleMoveRobot> goal_handle) {
std::thread{std::bind(&MoveRobotActionServer::execute, this,
std::placeholders::_1), goal_handle}.detach();
}
void execute(const std::shared_ptr<GoalHandleMoveRobot> goal_handle) {
auto goal = goal_handle->get_goal();
double tx = goal->target_x, ty = goal->target_y;
double total_dist = std::sqrt(tx*tx + ty*ty);
auto feedback = std::make_shared<MoveRobot::Feedback>();
auto result = std::make_shared<MoveRobot::Result>();
int steps = std::max(static_cast<int>(total_dist / 0.1), 1);
for (int i = 0; i < steps; ++i) {
if (goal_handle->is_canceling()) {
result->success = false;
result->message = "Canceled";
goal_handle->canceled(result);
return;
}
double progress = static_cast<double>(i + 1) / steps;
feedback->current_x = tx * progress;
feedback->current_y = ty * progress;
feedback->distance_remaining = total_dist * (1.0 - progress);
feedback->progress = progress;
goal_handle->publish_feedback(feedback);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
result->final_x = tx;
result->final_y = ty;
result->distance_traveled = total_dist;
result->success = true;
result->message = "Arrived!";
goal_handle->succeed(result);
}
rclcpp_action::Server<MoveRobot>::SharedPtr action_server_;
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MoveRobotActionServer>());
rclcpp::shutdown();
return 0;
}
| 命令 | 功能 |
|---|---|
ros2 action list | 列出所有Action |
ros2 action info /navigate_to_pose | 查看Action信息 |
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "{pose: ...}" | 发送目标 |
ros2 action send_goal --feedback /navigate_to_pose ... | 发送目标并显示反馈 |
ros2 action type /navigate_to_pose | 查看Action类型 |
创建一个倒计时Action:输入秒数,每秒发布剩余时间反馈,到0时返回结果。
创建巡逻Action:机器人按给定路径点循环巡逻,支持中途取消。
客户端依次发送多个导航目标,每个完成后再发送下一个。统计总时间和距离。
经验值:+200 XP