目录
摘要
功能包完整目录:
一、为什么还需要 Action 通信?
1.1 前面三种通信方式分别解决了什么?
(1)Topic 通信
(2)自定义 msg 通信
(3)自定义 srv 通信
1.2 Topic、Service、Action 到底有什么区别?
(1)Topic:一直持续,但是没有反馈
(2)Service:一次请求,一次处理,一次返回结果
(3)Service 是否一定立马返回?
(4)Action:发送一个目标,过程中持续反馈,最后返回结果
(5)更严谨地理解三种通信方式
1.3 Service 与 Action
(1)Service 的局限性
(2)Action 适合什么场景?
二、自定义 Action 接口文件
2.1 Action 文件放在哪里?
2.2 LimoAction.action 文件内容
(1)Goal:目标部分
(2)Result:结果部分
(3)Feedback:反馈部分
三、修改 limo_msgs 接口包配置
3.1 修改 CMakeLists.txt
3.2 修改 package.xml
3.3 编译接口包
四、Action 服务端代码
4.1 Action 服务端的作用
4.2 新建服务端文件
五、Action 客户端代码
5.1 Action 客户端的作用
5.2 新建客户端文件
六、修改 limo_learning 功能包
6.1 正常功能包目录:
6.2 修改 limo_learning 的 CMakeLists.txt
(1)添加 action 依赖
(2)添加可执行文件
(3)添加安装配置
6.3 修改 package.xml
七、编译与运行
7.1 编译功能包
7.2 先启动 Action 服务端
7.3 再启动 Action 客户端
7.4 查看 Action 列表
7.5 查看 Action 信息
7.6 使用命令行发送 Action 目标
八、Topic、Service、Action 对比总结
8.1 Topic 通信
8.2 Service 通信
8.3 Action 通信
8.4 三者对比表
九、本篇总结
摘要
前面几篇文章已经分别详细讲解了 ROS2 C++ 小车控制中的几种常见通信方式:
第一篇主要讲解:Topic + 官方标准 msg:
ROS2 C++ 小车控制完整实战(一):一文搞懂 Topic 发布订阅、Twist 消息与 /cmd_vel 速度控制-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/161829804?spm=1001.2014.3001.5502也就是使用 ROS2 官方已经定义好的消息类型,例如:
geometry_msgs/msg/Twist
通过 /cmd_vel 话题发布速度指令,实现对小车前进、后退、转向等基础运动控制。
第二篇主要讲解:Topic + 自定义 msg:
ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/161834874?spm=1001.2014.3001.5502我们自己定义了 LimoStatus.msg,用来发布小车状态信息,例如:
电池电压、车辆状态、控制模式、运动模式、错误码等。
第三篇主要讲解:Service + 自定义 srv:
ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信保姆级教程-CSDN博客
https://blog.csdn.net/m0_58954356/article/details/161834909?spm=1001.2014.3001.5502也就是客户端发送一次请求,服务端处理后返回一次响应。例如客户端发送 x、y、z 速度参数,服务端收到后控制小车运动,并返回 success 表示是否执行成功。
但是 Service 更适合“短时间、一次性、有明确返回结果”的任务。
如果任务执行时间比较长,比如:
这时候就更适合使用 Action 通信。
本篇文章就继续在前面 Topic、msg、srv 的基础上,讲解 ROS2 中更适合长时间任务的通信方式:
自定义 Action 通信。
需要说明的是:
本篇重点先放在 Action 通信的整体流程和工程配置上,让大家先知道 Action 是什么、Action 文件怎么写、功能包怎么配置、代码怎么编译运行、终端命令怎么测试。
至于 Action 服务端和客户端代码中每一行到底是什么意思,例如 create_server、create_client、GoalHandle、handle_goal、handle_cancel、handle_accepted、execute、feedback_callback、result_callback 等核心内容,会放到下一篇文章中单独进行详细拆解。
功能包完整目录:
ros2_ws/
└── src
├── limo_msgs
│ ├── msg
│ │ └── LimoStatus.msg
│ ├── srv
│ │ └── LimoSrv.srv
│ ├── action
│ │ └── LimoAction.action
│ ├── CMakeLists.txt
│ └── package.xml
│
└── limo_learning
├── src
│ ├── limo_topic_cmd.cpp
│ ├── limo_topic_sub.cpp
│ ├── limo_srv_server.cpp
│ ├── limo_srv_client.cpp
│ ├── limo_action_server.cpp
│ └── limo_action_client.cpp
├── CMakeLists.txt
└── package.xml
这里有两个包:
| limo_msgs | 定义自定义接口:msg、srv、action |
| limo_learning | 编写 C++ 节点代码:Topic、Service、Action 服务端和客户端 |
一、为什么还需要 Action 通信?

1.1 前面三种通信方式分别解决了什么?
在正式写 Action 代码之前,我们先回顾一下前面几篇文章中已经使用过的 ROS2 通信方式。
前面主要讲了三部分内容:
这三种通信方式各自解决的问题并不一样。
(1)Topic 通信
Topic 通信适合持续不断地发布数据。
例如:
小车速度控制:
/cmd_vel
小车状态发布:
/limo_status
雷达数据发布:
/scan
Topic 的特点是:
所以 Topic 适合:
- 持续发布速度
- 持续发布状态
- 持续发布传感器数据
Topic 就像广播。
(2)自定义 msg 通信
自定义 msg 的作用是:
当 ROS2 官方提供的消息类型不够用时,我们可以自己定义消息结构。
例如前面我们定义过:
LimoStatus.msg
里面包含:
std_msgs/Header header
uint8 vehicle_state
uint8 control_mode
uint8 motion_mode
float64 battery_voltage
uint16 error_code
这样就可以把小车状态封装成一个完整的自定义消息,然后通过 Topic 发布出去。
所以:
msg 解决的是“传什么数据”的问题。
(3)自定义 srv 通信
Service 通信适合一次请求、一次响应。
例如前面我们定义过:
LimoSrv.srv
内容类似:
float32 x
float32 y
float32 z
—
bool success
客户端发送请求:
x = 0.2
y = 0.0
z = 0.0
服务端收到请求后,转换成 Twist 速度指令发布到 /cmd_vel,然后返回:
success = true
所以 Service 适合:
Service 就像“问一句,答一句”。
1.2 Topic、Service、Action 到底有什么区别?

在正式讲 Action 之前,一定要先把 ROS2 中最常见的三种通信方式区分清楚:
这三种通信方式都可以让 ROS2 节点之间进行数据交互,但是它们适合的场景完全不一样。
可以先用一张表简单对比:
| Topic | 持续发布 / 持续接收 | 没有返回结果 | 速度、雷达、状态、里程计 |
| Service | 一次请求 / 一次响应 | 有最终响应 | 查询、开关、短时间任务 |
| Action | 一个目标 / 持续反馈 / 最终结果 | 有过程反馈,也有最终结果 | 导航、巡检、长时间任务 |
简单来说:
(1)Topic:一直持续,但是没有反馈
例如:
发布者 → /cmd_vel → 订阅者
发布者只管发布,订阅者只管接收。
Topic 的特点是:
比如在小车控制中:
/cmd_vel 持续发送速度指令
/scan 持续发送雷达数据
/odom 持续发送里程计数据
/limo_status 持续发送小车状态
这些数据都适合用 Topic。
因为它们不是问答关系,而是持续不断地发布和接收。
一句话理解:
Topic 就是一直发、一直收,但是不返回结果。
(2)Service:一次请求,一次处理,一次返回结果
整体流程是:
客户端发送请求
↓
服务端收到请求
↓
服务端处理请求
↓
服务端返回响应结果
↓
客户端拿到结果
所以 Service 不是一直通信,而是:
请求一次,处理一次,返回一次。
例如前面自定义 srv 通信中,客户端发送:
x = 0.2
y = 0.0
z = 0.0
服务端收到请求后,把它转换成 Twist 速度指令,然后发布到:
/cmd_vel
最后返回:
success = true
这就是典型的 Service 通信。
一句话理解:
Service 就是客户端问一次,服务端处理一次,然后返回一次结果。
(3)Service 是否一定立马返回?
这里是很多初学者最容易混淆的地方。
Service 不是一定“立马返回”,而是:
服务端处理完这一次请求后,才会返回结果。
也就是说,Service 返回快还是慢,取决于服务端回调函数里面到底做了多少事情。
比如客户端请求:
让小车前进 1 秒。
如果服务端代码里面这样写:
收到请求
发布速度
等待 1 秒
停止小车
返回 success = true
那么客户端就需要等服务端这一整套流程执行完,才能拿到返回结果。
但是如果服务端代码这样写:
收到请求
发布一次速度
立刻返回 success = true
那么客户端就会很快收到响应结果。
所以关键不在于 Service 本身,而在于:
服务端回调函数里面到底执行了多长时间。
可以这样理解:
Service 返回的时机 = 服务端处理完这一次请求并设置 response 的时机
(4)Action:发送一个目标,过程中持续反馈,最后返回结果
Action 通信适合长时间任务。
它不是简单的“一问一答”,而是:
客户端发送目标 Goal
↓
服务端开始执行任务
↓
执行过程中不断发送 Feedback
↓
任务完成后返回 Result
比如导航任务:
目标:去 A 点
反馈:当前完成 20%
反馈:当前完成 50%
反馈:当前完成 80%
结果:到达成功
Action 和 Service 最大的区别。
Action 的特点是:
Action 就是发送一个目标,服务端慢慢执行,执行过程中不断反馈,最后返回最终结果。
(5)更严谨地理解三种通信方式
结合小车控制来理解:
Topic:持续发送 /cmd_vel 速度控制小车
Service:请求小车执行一次简单动作,然后返回是否成功
Action:请求小车执行一个长时间任务,过程中反馈状态,完成后返回最终结果
到这里就可以自然引出本篇文章的重点:
当我们希望小车执行一个持续一段时间的任务,并且希望在执行过程中不断收到状态反馈时,就应该使用 Action 通信。
1.3 Service 与 Action
(1)Service 的局限性
Service 虽然很适合一次性请求,但是它不太适合长时间任务。
比如现在有一个需求:
让小车向前运动 10 秒,并且执行过程中不断反馈当前执行状态。
如果使用 Service,就会出现一个问题:
客户端发送请求后,只能等待服务端最终返回结果。
在任务执行过程中,客户端很难持续知道:
也就是说,Service 只适合短任务,不适合长任务。
(2)Action 适合什么场景?
Action 通信就是为长时间任务设计的。
Action 比 Service 多了一个非常重要的能力:
可以在任务执行过程中持续反馈状态。
Action 通信一般包含三部分:
可以理解为:
例如:
客户端发送目标:
让小车以 x=0.2 的速度前进
服务端执行过程中持续反馈:
任务执行完成后返回结果:
success = true
所以 Action 更适合:
一句话总结:
Action 适合“执行时间较长,并且需要反馈过程状态”的任务。
二、自定义 Action 接口文件

2.1 Action 文件放在哪里?
前面我们已经创建过接口功能包:
limo_msgs
这个功能包中可以放:
msg
srv
action
对应三种自定义接口:
limo_msgs/
├── msg
│ └── LimoStatus.msg
├── srv
│ └── LimoSrv.srv
├── action
│ └── LimoAction.action
├── CMakeLists.txt
└── package.xml
本篇要新增的就是:
action/LimoAction.action
2.2 LimoAction.action 文件内容
创建文件:
limo_msgs/action/LimoAction.action
写入内容:
float32 x
float32 y
float32 z
—
bool success
—
uint32 status
这个 action 文件中有三个部分,中间用两个 — 分隔。
(1)Goal:目标部分
第一部分是 Goal,也就是客户端发送给服务端的目标。
float32 x
float32 y
float32 z
这里的含义是:
x:小车线速度 x 方向 y:小车线速度 y 方向 z:小车角速度 z 方向
后面服务端会把它们转换成:
geometry_msgs::msg::Twist
对应关系是:
twist.linear.x = goal->x;
twist.linear.y = goal->y;
twist.angular.z = goal->z;
也就是说:
客户端通过 Action 发送目标速度,服务端根据目标速度控制小车运动。
(2)Result:结果部分
第二部分是 Result,也就是任务执行完成后,服务端返回给客户端的最终结果。
bool success
这里表示任务是否执行成功。
如果任务正常完成:
success = true
如果任务执行失败:
success = false
(3)Feedback:反馈部分
第三部分是 Feedback,也就是任务执行过程中,服务端持续反馈给客户端的状态。
uint32 status
这里可以表示当前执行状态。
例如:
status = 1
status = 2
status = 3
status = 4
status = 5
表示任务正在一步步执行。
当然,在真实项目中,status 也可以设计成更具体的含义,例如:
- 0:空闲
- 1:正在执行
- 2:任务暂停
- 3:任务完成
- 4:任务失败
本篇为了方便理解,先用 status 表示执行进度。
三、修改 limo_msgs 接口包配置
3.1 修改 CMakeLists.txt
前面自定义 msg 和 srv 时,已经配置过 rosidl_generate_interfaces。
现在需要把 action 文件也加入进去。
打开:
limo_msgs/CMakeLists.txt
找到:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LimoStatus.msg"
"srv/LimoSrv.srv"
DEPENDENCIES std_msgs
)
修改为:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LimoStatus.msg"
"srv/LimoSrv.srv"
"action/LimoAction.action"
DEPENDENCIES std_msgs
)
这样编译时,ROS2 才会根据 LimoAction.action 自动生成 C++ 代码。
3.2 修改 package.xml
打开:
limo_msgs/package.xml
确认里面有下面这些依赖:
<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>
如果前面自定义 msg 和 srv 已经配置过,那么这里一般不用重复修改。
3.3 编译接口包
回到工作空间根目录:
cd ~/agilex_open_class_ws
编译:
colcon build –packages-select limo_msgs
编译完成后刷新环境变量:
source install/setup.bash
查看 action 接口是否生成成功:
ros2 interface show limo_msgs/action/LimoAction
正常情况下会看到:
float32 x
float32 y
float32 z
—
bool success
—
uint32 status
说明自定义 Action 接口已经生成成功。
四、Action 服务端代码
4.1 Action 服务端的作用
Action 服务端负责:
在本篇案例中,Action 服务端的核心功能是:
也就是说:
Action 服务端是真正执行任务的一方。
4.2 新建服务端文件
在 limo_learning 功能包下创建:
limo_learning/src/limo_action_server.cpp
写入代码:
// 引入智能指针相关头文件
// std::shared_ptr、std::make_shared 会用到
#include <memory>
// 引入线程相关头文件
// std::thread 会用到
#include <thread>
// 引入 ROS2 C++ 客户端库
#include "rclcpp/rclcpp.hpp"
// 引入 ROS2 Action 通信相关头文件
#include "rclcpp_action/rclcpp_action.hpp"
// 引入 Twist 速度消息类型
// 用于向 /cmd_vel 发布速度指令
#include "geometry_msgs/msg/twist.hpp"
// 引入自定义 Action 接口
// 对应 limo_msgs/action/LimoAction.action
#include "limo_msgs/action/limo_action.hpp"
// 自定义 Action 服务端节点类
// 继承 rclcpp::Node,说明它是一个 ROS2 节点
class LimoActionServer : public rclcpp::Node
{
public:
// 给自定义 Action 类型起一个别名,方便后面书写
// limo_msgs::action::LimoAction 对应 LimoAction.action 生成的 C++ 类型
using LimoAction = limo_msgs::action::LimoAction;
// 给 Action 目标句柄类型起一个别名
// GoalHandle 用来表示客户端发送过来的一个目标任务
using GoalHandleLimoAction = rclcpp_action::ServerGoalHandle<LimoAction>;
// 构造函数
// Node("limo_action_server") 表示当前节点名称为 limo_action_server
LimoActionServer() : Node("limo_action_server")
{
// 创建 /cmd_vel 速度发布者
// 发布 geometry_msgs::msg::Twist 类型消息
// 队列长度为 10
cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel",
10
);
// 创建 Action 服务端
// Action 类型:LimoAction
// Action 名称:/limo_action
// handle_goal:收到目标请求时调用
// handle_cancel:收到取消请求时调用
// handle_accepted:目标被接受后调用
action_server_ = rclcpp_action::create_server<LimoAction>(
this,
"/limo_action",
std::bind(
&LimoActionServer::handle_goal,
this,
std::placeholders::_1,
std::placeholders::_2
),
std::bind(
&LimoActionServer::handle_cancel,
this,
std::placeholders::_1
),
std::bind(
&LimoActionServer::handle_accepted,
this,
std::placeholders::_1
)
);
// 打印节点启动日志
RCLCPP_INFO(this->get_logger(), "limo_action_server node has started.");
}
private:
// /cmd_vel 速度发布者对象
// 用于控制小车运动
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
// Action 服务端对象
// 用于提供 /limo_action 动作服务
rclcpp_action::Server<LimoAction>::SharedPtr action_server_;
// 处理客户端发送过来的 Goal 目标请求
// 当客户端发送目标时,会先进入这个函数
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID & uuid,
std::shared_ptr<const LimoAction::Goal> goal)
{
// 当前代码中没有使用 uuid
// 写 (void)uuid 是为了避免编译器提示“变量未使用”
(void)uuid;
// 打印客户端发送过来的目标参数
// goal->x 表示目标中的 x 方向速度
// goal->y 表示目标中的 y 方向速度
// goal->z 表示目标中的 z 方向角速度
RCLCPP_INFO(
this->get_logger(),
"Received goal: x=%.2f, y=%.2f, z=%.2f",
goal->x,
goal->y,
goal->z
);
// 接受并立即执行该目标
// ACCEPT_AND_EXECUTE 表示服务端同意执行客户端发来的任务
return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE;
}
// 处理客户端发送的取消请求
// 当客户端想取消正在执行的 Action 任务时,会进入这个函数
rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<GoalHandleLimoAction> goal_handle)
{
// 当前代码中没有直接使用 goal_handle
// 写 (void)goal_handle 是为了避免编译器提示“变量未使用”
(void)goal_handle;
// 打印收到取消请求的日志
RCLCPP_INFO(this->get_logger(), "Received request to cancel goal.");
// 接受客户端的取消请求
return rclcpp_action::CancelResponse::ACCEPT;
}
// 当目标被接受后,会进入这个函数
// 这里不直接执行任务,而是开启一个新线程执行 execute()
void handle_accepted(
const std::shared_ptr<GoalHandleLimoAction> goal_handle)
{
// 创建一个新线程执行 execute 函数
// 这样做的目的:
// 避免长时间任务阻塞 ROS2 主线程
//
// detach() 表示线程独立运行
// 主线程不会等待这个线程结束
std::thread{
std::bind(
&LimoActionServer::execute,
this,
std::placeholders::_1
),
goal_handle
}.detach();
}
// 真正执行 Action 任务的函数
// 目标被接受后,会在新线程中执行这个函数
void execute(
const std::shared_ptr<GoalHandleLimoAction> goal_handle)
{
// 打印开始执行任务的日志
RCLCPP_INFO(this->get_logger(), "Executing goal…");
// 获取客户端发送过来的目标数据
const auto goal = goal_handle->get_goal();
// 创建反馈对象
// Feedback 对应 LimoAction.action 中第二个 — 后面的反馈部分
auto feedback = std::make_shared<LimoAction::Feedback>();
// 创建结果对象
// Result 对应 LimoAction.action 中第一个 — 后面的结果部分
auto result = std::make_shared<LimoAction::Result>();
// 设置循环频率为 1Hz
// 也就是每秒执行一次循环
rclcpp::Rate loop_rate(1);
// 创建 Twist 速度消息对象
geometry_msgs::msg::Twist cmd_vel;
// 将 Action 目标中的 x、y、z 转换成 Twist 速度指令
// goal->x 对应 linear.x,表示前后方向速度
// goal->y 对应 linear.y,表示左右方向速度
// goal->z 对应 angular.z,表示绕 z 轴旋转角速度
cmd_vel.linear.x = goal->x;
cmd_vel.linear.y = goal->y;
cmd_vel.angular.z = goal->z;
// 循环执行 5 次
// 因为 loop_rate 是 1Hz,所以这里大约执行 5 秒
for (uint32_t i = 1; i <= 5; i++)
{
// 判断当前目标是否正在被取消
// 如果客户端发送了取消请求,并且服务端接受取消,就会进入这里
if (goal_handle->is_canceling())
{
// 创建停止速度指令
// Twist 默认值都是 0,所以表示停止小车
geometry_msgs::msg::Twist stop_cmd;
// 发布停止指令到 /cmd_vel
cmd_pub_->publish(stop_cmd);
// 设置最终结果为失败
result->success = false;
// 通知客户端:目标已经取消
goal_handle->canceled(result);
// 打印取消日志
RCLCPP_INFO(this->get_logger(), "Goal canceled.");
// 结束 execute 函数
return;
}
// 发布速度指令,让小车按照目标速度运动
cmd_pub_->publish(cmd_vel);
// 设置反馈状态
// 这里用 i 表示当前执行进度
// 第 1 秒 status=1,第 2 秒 status=2,以此类推
feedback->status = i;
// 向客户端发布反馈信息
// 客户端可以在任务执行过程中不断收到 feedback
goal_handle->publish_feedback(feedback);
// 打印当前反馈状态
RCLCPP_INFO(
this->get_logger(),
"Publishing feedback: status=%d",
feedback->status
);
// 按照 1Hz 频率休眠
// 也就是每次循环间隔约 1 秒
loop_rate.sleep();
}
// 任务执行完成后,发布停止指令
// 防止小车继续运动
geometry_msgs::msg::Twist stop_cmd;
cmd_pub_->publish(stop_cmd);
// 设置最终结果为成功
result->success = true;
// 通知客户端:目标执行成功
goal_handle->succeed(result);
// 打印任务成功日志
RCLCPP_INFO(this->get_logger(), "Goal succeeded.");
}
};
// main 函数,程序入口
int main(int argc, char ** argv)
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建 LimoActionServer 节点对象
auto node = std::make_shared<LimoActionServer>();
// 运行节点
// spin 会让节点持续运行,等待 Action 请求、取消请求等回调触发
rclcpp::spin(node);
// 关闭 ROS2
rclcpp::shutdown();
return 0;
}
```
五、Action 客户端代码
5.1 Action 客户端的作用
Action 客户端负责:
在本篇案例中,客户端发送:
x = 0.2
y = 0.0
z = 0.0
表示让小车向前运动。
服务端执行过程中,客户端会不断收到 feedback。
最后收到 result,判断任务是否执行成功。
5.2 新建客户端文件
在 limo_learning 功能包下创建:
limo_learning/src/limo_action_client.cpp
写入代码:
// 引入智能指针相关头文件
// std::shared_ptr、std::make_shared 会用到
#include <memory>
// ROS2 C++ 客户端库头文件
#include "rclcpp/rclcpp.hpp"
// ROS2 Action 通信相关头文件
#include "rclcpp_action/rclcpp_action.hpp"
// 引入自定义 Action 接口头文件
// 对应 limo_msgs/action/LimoAction.action
#include "limo_msgs/action/limo_action.hpp"
// 自定义 Action 客户端节点类
class LimoActionClient : public rclcpp::Node
{
public:
// 给自定义 Action 类型起一个别名,方便后面使用
using LimoAction = limo_msgs::action::LimoAction;
// 给 Action 客户端目标句柄类型起一个别名
// ClientGoalHandle<LimoAction> 表示客户端这边用于管理目标任务的句柄
using GoalHandleLimoAction = rclcpp_action::ClientGoalHandle<LimoAction>;
// 构造函数
// 创建节点时,节点名称为 limo_action_client
LimoActionClient() : Node("limo_action_client")
{
// 创建 Action 客户端对象
// LimoAction 表示使用的 Action 接口类型
// "/limo_action" 表示要连接的 Action 服务名称
action_client_ = rclcpp_action::create_client<LimoAction>(
this,
"/limo_action"
);
// 打印日志,提示 Action 客户端节点已经启动
RCLCPP_INFO(this->get_logger(), "limo_action_client node has started.");
}
// 发送 Action 目标任务的函数
void send_goal()
{
// 等待 Action 服务端启动
// 最多等待 5 秒
// 如果 5 秒内没有发现 Action 服务端,则直接返回
if (!action_client_->wait_for_action_server(std::chrono::seconds(5)))
{
RCLCPP_ERROR(this->get_logger(), "Action server not available.");
return;
}
// 创建一个 Action 目标消息
// LimoAction::Goal 对应 .action 文件中第一段 Goal 部分
auto goal_msg = LimoAction::Goal();
// 设置目标参数
// 这里表示希望小车按照 x、y、z 指定的速度执行运动
goal_msg.x = 0.2;
goal_msg.y = 0.0;
goal_msg.z = 0.0;
// 打印日志,提示正在发送目标
RCLCPP_INFO(this->get_logger(), "Sending goal…");
// 创建发送目标时的选项对象
// 里面可以绑定三个回调函数:
// 1. goal_response_callback:目标是否被服务端接受
// 2. feedback_callback:执行过程中接收反馈
// 3. result_callback:执行结束后接收最终结果
auto send_goal_options = rclcpp_action::Client<LimoAction>::SendGoalOptions();
// 绑定目标响应回调函数
// 当服务端接受或拒绝目标时,会自动调用 goal_response_callback
send_goal_options.goal_response_callback =
std::bind(
&LimoActionClient::goal_response_callback,
this,
std::placeholders::_1
);
// 绑定反馈回调函数
// 当服务端执行过程中发布 feedback 时,会自动调用 feedback_callback
send_goal_options.feedback_callback =
std::bind(
&LimoActionClient::feedback_callback,
this,
std::placeholders::_1,
std::placeholders::_2
);
// 绑定结果回调函数
// 当 Action 任务执行完成、取消或失败时,会自动调用 result_callback
send_goal_options.result_callback =
std::bind(
&LimoActionClient::result_callback,
this,
std::placeholders::_1
);
// 异步发送目标任务
// goal_msg 是目标内容
// send_goal_options 中包含三个回调函数
action_client_->async_send_goal(goal_msg, send_goal_options);
}
private:
// Action 客户端对象
// 用于连接 /limo_action,并向 Action 服务端发送目标任务
rclcpp_action::Client<LimoAction>::SharedPtr action_client_;
// 目标响应回调函数
// 当客户端发送目标后,服务端会决定是否接受该目标
void goal_response_callback(
const GoalHandleLimoAction::SharedPtr & goal_handle)
{
// 如果 goal_handle 为空,说明服务端拒绝了目标
if (!goal_handle)
{
RCLCPP_ERROR(this->get_logger(), "Goal was rejected by server.");
}
// 如果 goal_handle 不为空,说明服务端接受了目标
else
{
RCLCPP_INFO(this->get_logger(), "Goal accepted by server.");
}
}
// 反馈回调函数
// 服务端执行任务过程中,可以不断向客户端发送 feedback
void feedback_callback(
GoalHandleLimoAction::SharedPtr,
const std::shared_ptr<const LimoAction::Feedback> feedback)
{
// 打印服务端返回的反馈状态
// feedback->status 对应 .action 文件中 Feedback 部分的 status 字段
RCLCPP_INFO(
this->get_logger(),
"Received feedback: status=%d",
feedback->status
);
}
// 最终结果回调函数
// 当 Action 任务结束后,会进入这个函数
void result_callback(
const GoalHandleLimoAction::WrappedResult & result)
{
// 根据 result.code 判断任务最终状态
switch (result.code)
{
// 任务成功完成
case rclcpp_action::ResultCode::SUCCEEDED:
RCLCPP_INFO(
this->get_logger(),
"Goal succeeded, success=%d",
result.result->success
);
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;
}
// 收到最终结果后关闭 ROS2 节点
rclcpp::shutdown();
}
};
int main(int argc, char ** argv)
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建 Action 客户端节点对象
auto node = std::make_shared<LimoActionClient>();
// 调用 send_goal(),向 Action 服务端发送目标任务
node->send_goal();
// 进入 ROS2 事件循环
// 用于等待并处理 Action 回调函数
// 包括目标响应、过程反馈、最终结果
rclcpp::spin(node);
// 关闭 ROS2
rclcpp::shutdown();
return 0;
}
六、修改 limo_learning 功能包
6.1 正常功能包目录:
ros2_ws/
└── src
├── limo_msgs
│ ├── msg
│ │ └── LimoStatus.msg
│ ├── srv
│ │ └── LimoSrv.srv
│ ├── action
│ │ └── LimoAction.action
│ ├── CMakeLists.txt
│ └── package.xml
│
└── limo_learning
├── src
│ ├── limo_topic_cmd.cpp
│ ├── limo_topic_sub.cpp
│ ├── limo_srv_server.cpp
│ ├── limo_srv_client.cpp
│ ├── limo_action_server.cpp
│ └── limo_action_client.cpp
├── CMakeLists.txt
└── package.xml
6.2 修改 limo_learning 的 CMakeLists.txt
(1)添加 action 依赖
因为 Action 代码中用到了:
rclcpp_action
所以需要在 CMakeLists.txt 中增加依赖。
打开:
limo_learning/CMakeLists.txt
添加:
find_package(rclcpp REQUIRED)
find_package(rclcpp_action REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(limo_msgs REQUIRED)
如果前面已经有 rclcpp、geometry_msgs、limo_msgs,就只需要补充:
find_package(rclcpp_action REQUIRED)
(2)添加可执行文件
继续添加:
add_executable(limo_action_server src/limo_action_server.cpp)
ament_target_dependencies(
limo_action_server
rclcpp
rclcpp_action
geometry_msgs
limo_msgs
)
add_executable(limo_action_client src/limo_action_client.cpp)
ament_target_dependencies(
limo_action_client
rclcpp
rclcpp_action
limo_msgs
)
(3)添加安装配置
在 install 中加入:
install(TARGETS
limo_action_server
limo_action_client
DESTINATION lib/${PROJECT_NAME}
)
如果前面已经有其他可执行文件,可以统一写成:
install(TARGETS
limo_topic_cmd
limo_topic_sub
limo_status_pub
limo_status_sub
limo_srv_server
limo_srv_client
limo_action_server
limo_action_client
DESTINATION lib/${PROJECT_NAME}
)
6.3 修改 package.xml
打开:
limo_learning/package.xml
添加依赖:
<depend>rclcpp</depend>
<depend>rclcpp_action</depend>
<depend>geometry_msgs</depend>
<depend>limo_msgs</depend>
其中:
<depend>rclcpp_action</depend>
是本篇 Action 通信需要新增的重点依赖。
七、编译与运行

7.1 编译功能包
回到工作空间根目录:
cd ~/agilex_open_class_ws
编译:
colcon build
如果只想编译相关功能包,也可以:
colcon build –packages-select limo_msgs limo_learning
编译完成后刷新环境变量:
source install/setup.bash
7.2 先启动 Action 服务端
打开第一个终端:
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_action_server
正常输出类似:
[INFO] [limo_action_server]: limo_action_server node has started.
说明 Action 服务端已经启动。
7.3 再启动 Action 客户端
打开第二个终端:
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_action_client
客户端输出类似:
[INFO] [limo_action_client]: limo_action_client node has started.
[INFO] [limo_action_client]: Sending goal…
[INFO] [limo_action_client]: Goal accepted by server.
[INFO] [limo_action_client]: Received feedback: status=1
[INFO] [limo_action_client]: Received feedback: status=2
[INFO] [limo_action_client]: Received feedback: status=3
[INFO] [limo_action_client]: Received feedback: status=4
[INFO] [limo_action_client]: Received feedback: status=5
[INFO] [limo_action_client]: Goal succeeded, success=1
服务端输出类似:
[INFO] [limo_action_server]: Received goal: x=0.20, y=0.00, z=0.00
[INFO] [limo_action_server]: Executing goal…
[INFO] [limo_action_server]: Publishing feedback: status=1
[INFO] [limo_action_server]: Publishing feedback: status=2
[INFO] [limo_action_server]: Publishing feedback: status=3
[INFO] [limo_action_server]: Publishing feedback: status=4
[INFO] [limo_action_server]: Publishing feedback: status=5
[INFO] [limo_action_server]: Goal succeeded.
7.4 查看 Action 列表
可以使用命令查看当前系统中的 Action:
ros2 action list
正常情况下可以看到:
/limo_action
7.5 查看 Action 信息
ros2 action info /limo_action
可以看到 Action 的服务端和客户端数量。
7.6 使用命令行发送 Action 目标
除了写客户端代码,也可以直接用终端命令测试 Action。
ros2 action send_goal /limo_action limo_msgs/action/LimoAction "{x: 0.2, y: 0.0, z: 0.0}"
如果想看到反馈信息,可以加上:
–feedback
完整命令:
ros2 action send_goal /limo_action limo_msgs/action/LimoAction "{x: 0.2, y: 0.0, z: 0.0}" –feedback
这样终端就会显示服务端不断返回的 feedback。
八、Topic、Service、Action 对比总结
8.1 Topic 通信
Topic 适合持续发送数据。
例如:
特点是:
可以理解为:
Topic 是广播型通信。
8.2 Service 通信
Service 适合一次请求、一次响应。
例如:
特点是:
可以理解为:
Service 是问答型通信。
8.3 Action 通信
Action 适合长时间任务。
例如:
特点是:
可以理解为:
Action 是任务型通信。
8.4 三者对比表
| Topic | 发布/订阅 | 是 | 否 | 否 | 速度、状态、传感器数据 |
| Service | 请求/响应 | 否 | 是 | 否 | 短时间一次性任务 |
| Action | 目标/反馈/结果 | 是 | 是 | 是 | 长时间任务、导航、巡检 |
九、本篇总结

本篇文章在前面 Topic、msg、srv 的基础上,继续讲解了 ROS2 中非常重要的 Action 通信机制。
前面我们已经知道:
但是当任务执行时间比较长,并且需要在执行过程中持续反馈状态时,Service 就不太适合了。
这时候就需要 Action。
本篇通过自定义:
LimoAction.action
实现了一个小车 Action 控制案例。
其中:
- 服务端收到目标后,将 x、y、z 转换成 Twist 速度指令,并发布到 /cmd_vel 控制小车运动。
- 客户端发送目标后,可以持续接收服务端反馈,并在任务结束后获取 success 结果。
同时,本篇还完成了 limo_msgs 接口功能包和 limo_learning 代码功能包的配置。
其中:
至此,ROS2 中最核心的三种通信方式已经串起来了:
下一篇文章会专门进入 Action 服务端和客户端代码解析,重点讲清楚:
本篇先把 Action 通信完整跑通。 下一篇再把 Action 服务端和客户端代码彻底讲明白。
掌握这三种通信方式后,再去看 ROS2 机器人项目中的导航、巡检、底盘控制、机械臂控制等代码,就会清晰很多。

