在 ROS 中,Action
是一种支持长时间异步任务的通信机制。与 Service
不同,Action
允许客户端发起一个请求,并在任务执行的过程中不断接收反馈,直到任务完成。这种机制非常适用于可能需要较长时间来完成的任务,比如机器人移动、导航或复杂的传感器数据采集。
ROS Action 的工作原理
Action
的通信流程由三个主要组成部分:Goal(目标)、Feedback(反馈) 和 Result(结果)。在 ROS 中,Action
通常包含以下几个节点角色:
- Action Server(服务器):负责执行具体的任务,并将执行过程中的反馈和最终结果发送给客户端。
- Action Client(客户端):发起任务请求,发送目标并接收服务器的反馈和结果。
1. ROS Action 的基本组成
每个 ROS Action 包括以下几个部分:
- Goal(目标):客户端发送的目标数据,定义了任务的具体内容。
- Feedback(反馈):服务器在执行过程中可以发送实时反馈,告知任务进展。
- Result(结果):服务器在任务完成后返回的最终结果。
2. 定义 ROS Action
在 ROS 中,我们使用 .action
文件定义 Action 的数据格式,它类似于 .srv
文件,包含三部分:Goal
、Feedback
和 Result
,各部分使用 ---
分隔。
示例:定义一个计数器的 Action
假设我们想定义一个计数器的 Action,它接收一个目标数字并从 0 计数到该数字,期间不断提供进度反馈,并在完成后返回最终结果。
首先我们先创建一个my_action的功能包,进入到自己的ros工作空间下面,执行:
catkin_create_pkg my_action roscpp actionlib actionlib_msgs std_msgs
在建立的功能包下再创建一个action文件夹,用来存放“.action”文件
定义 Countdown.action
文件:
# CountToNumber.action
# Goal: 目标数字
int32 target_number
---
# Result: 操作结果(是否成功)
bool success
---
# Feedback: 当前计数
int32 current_count
这意味着我们将向 Action 服务器发送一个目标数字,服务器会从 0 开始计数并每次提供当前的计数进度,最后返回一个表示成功的布尔值。
3. 配置 CMakeLists.txt 和 package.xml
然后,像之前的例子一样,我们需要更新 CMakeLists.txt
和 package.xml
文件,确保 Action 文件被编译并生成相应的消息类型。
- CMakeLists.txt:
find_package(catkin REQUIRED COMPONENTS
actionlib
actionlib_msgs
roscpp
std_msgs
)
add_action_files(
FILES
CountToNumber.action
)
generate_messages(
DEPENDENCIES
actionlib_msgs
std_msgs
)
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES my_action
CATKIN_DEPENDS roscpp rospy std_msgs actionlib actionlib_msgs
# DEPENDS system_lib
)
- package.xml:
<buildtool_depend>catkin</buildtool_depend>
<build_depend>actionlib</build_depend>
<build_depend>actionlib_msgs</build_depend>
<build_depend>roscpp</build_depend>
<build_depend>std_msgs</build_depend>
<build_export_depend>actionlib</build_export_depend>
<build_export_depend>roscpp</build_export_depend>
<build_export_depend>std_msgs</build_export_depend>
<exec_depend>actionlib</exec_depend>
<exec_depend>roscpp</exec_depend>
<exec_depend>actionlib_msgs</exec_depend>
<exec_depend>std_msgs</exec_depend>
确保上述内容无误后,回到工作空间编译我们的my_action功能包
cd ~/catkin_ws/
catkin_make -DCATKIN_WHITELIST_PACKAGES=my_action
(编译你可以直接使用catkin_make,这里我加 -DCATKIN_WHITELIST_PACKAGES
=my_action表示只编译my_action功能包)
编译通过后你就可以在工作空间目录下的devel/include/my_action路径中找到自定义的action的头文件
4. 实现 Action 服务器
接下来,我们实现 Action 服务器,接收目标数字并从 0 开始计数,定期提供反馈,直到计数完成或被取消。我们在my_action功能包的src下创建一个countdown_server.cpp
#include <ros/ros.h>
#include <actionlib/server/simple_action_server.h>
#include <my_action/CountToNumberAction.h>
class CountToNumberAction
{
protected:
ros::NodeHandle nh_; // ROS节点句柄,用于与ROS系统交互
actionlib::SimpleActionServer<my_action::CountToNumberAction> as_; // Action 服务器对象,类型是自定义的 `CountToNumberAction`
std::string action_name_; // Action的名称
my_action::CountToNumberFeedback feedback_; // 存储反馈的对象
my_action::CountToNumberResult result_; // 存储结果的对象
public:
CountToNumberAction(std::string name) :
as_(nh_, name, boost::bind(&CountToNumberAction::executeCB, this, _1), false),
action_name_(name)
{
as_.start();
}
void executeCB(const my_action::CountToNumberGoalConstPtr &goal)
{
int target = goal->target_number; // 获取目标数字
ROS_INFO("Counting to %d", target); // 打印目标数字
// 从 0 开始计数并提供反馈
for (int i = 0; i <= target; ++i) {
if (as_.isPreemptRequested()) {
ROS_INFO("%s: Preempted", action_name_.c_str());
as_.setPreempted(); // 如果请求了中断,则设置为已中断
return;
}
feedback_.current_count = i; // 更新当前计数
as_.publishFeedback(feedback_); // 发布反馈
ros::Duration(1.0).sleep(); // 每秒更新一次反馈
}
// 返回结果
result_.success = true; // 设置操作成功
as_.setSucceeded(result_); // 设置 Action 成功并返回结果
}
};
int main(int argc, char** argv)
{
ros::init(argc, argv, "count_to_number_action_server"); // 初始化ROS节点
CountToNumberAction count_to_number_action("count_to_number"); // 创建 Action 服务器实例
ros::spin(); // 进入 ROS 事件循环,等待请求
return 0;
}
代码我已经注释好,就不用过多篇幅重复介绍了,只想插一嘴构造函数中表现的回调函数机制,因为最近面试被问到了
这段内容可以通过目录选择性阅读哈
扩展:C++的回调函数机制
CountToNumberAction(std::string name) :
as_(nh_, name, boost::bind(&CountToNumberAction::executeCB, this, _1), false),
action_name_(name)
{
as_.start();
}
解释:
- 构造函数接收一个字符串
name
作为参数,指定 Action 的名称。 as_(nh_, name, boost::bind(&CountToNumberAction::executeCB, this, _1), false)
:actionlib::SimpleActionServer
需要三个参数:nh_
:ROS节点句柄,负责通信。name
:Action 名称。- 回调函数:使用
boost::bind
绑定一个成员函数executeCB
,这是处理客户端请求的回调函数。_1
表示传递给回调函数的第一个参数(即 Goal 对象)。
false
:这是一个布尔参数,表示是否立即启动服务器。这里设置为false
,表示服务器启动后不会自动开始等待请求,而是通过调用as_.start()
来启动。
as_.start()
:启动 Action 服务器,开始监听客户端请求。
在 ROS 的 Action 服务器中,boost::bind
是一种通过将成员函数或普通函数转换为可调用对象(仿函数)的方式。这样做的目的是为了实现回调机制,尤其是在 actionlib::SimpleActionServer
中。
这段代码通过 boost::bind
将 CountToNumberAction::executeCB
成员函数转换成一个可以作为回调的函数对象(仿函数)。让我们逐步分析:
-
boost::bind
的作用:-
boost::bind
用来将一个成员函数绑定到特定的对象上,并且返回一个可调用对象(仿函数)。这个对象可以像普通函数一样被调用。 -
&CountToNumberAction::executeCB
:这是CountToNumberAction
类的成员函数executeCB
的指针。 -
this
:它是指向当前对象的指针,告诉boost::bind
成员函数是作用于哪个对象。 -
_1
:这是一个占位符,表示传递给回调函数的第一个参数(即goal
)。在实际调用时,_1
会被替换为客户端发送的目标(Goal)。
-
-
actionlib::SimpleActionServer
:actionlib::SimpleActionServer
构造函数接受一个回调函数作为参数,这个回调函数会在 Action 服务器收到目标请求时被调用。- 在这个例子中,回调函数是
CountToNumberAction::executeCB
,通过boost::bind
转换成了一个仿函数,它接收goal
作为输入并处理相关逻辑。
-
仿函数的调用:
- 当 Action 服务器接收到客户端请求时,它会调用绑定的回调函数(仿函数),从而触发计数逻辑的执行。这个回调会处理客户端的目标请求并定期向客户端发送反馈。
5. 实现 Action 客户端
接下来,我们实现客户端来发送目标数字并接收进度反馈和最终结果。
我们在my_action功能包的src下创建一个countdown_client.cpp
#include <ros/ros.h>
#include <actionlib/client/simple_action_client.h>
#include <my_action/CountToNumberAction.h> //包含自定义 Action 类型的头文件,CountToNumberAction 这个类型定义了目标(Goal)、反馈(Feedback)和结果(Result)结构。
typedef actionlib::SimpleActionClient<my_action::CountToNumberAction> Client;
// doneCb - 任务完成后的回调
void doneCb(const actionlib::SimpleClientGoalState& state,
const my_action::CountToNumberResultConstPtr& result)
{
ROS_INFO("Finished in state: %s", state.toString().c_str());
if (result->success)
ROS_INFO("Counting completed successfully!");
else
ROS_WARN("Counting failed.");
}
/**
* activeCb:当目标开始被服务器处理时触发。此回调表示目标已经被接受并正在处理。
它简单地打印一条信息,表示目标正在被处理。
*/
void activeCb()
{
ROS_INFO("Goal is being processed...");
}
// feedbackCb - 反馈更新的回调
void feedbackCb(const my_action::CountToNumberFeedbackConstPtr& feedback)
{
ROS_INFO("Current count: %d", feedback->current_count);
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "count_to_number_action_client");
// 创建一个客户端,连接到服务器
Client ac("count_to_number", true);
ROS_INFO("Waiting for action server to start...");
ac.waitForServer(); // 等待服务器启动
my_action::CountToNumberGoal goal;
goal.target_number = 10; // 设置目标数字
// 发送目标并设置回调函数
ac.sendGoal(goal, &doneCb, &activeCb, &feedbackCb);
ros::spin();
return 0;
}
- 这段代码实现了一个 ROS Action 客户端,连接到名为
"count_to_number"
的服务器并发送目标(goal.target_number = 10
)。 - 客户端使用三个回调函数:
doneCb
:处理任务完成后的状态和结果。activeCb
:处理目标开始被服务器处理时的状态。feedbackCb
:处理服务器发送的反馈信息(当前计数值)。
- 客户端通过调用
ac.sendGoal
发送目标,并且使用ros::spin()
保持客户端运行,接收和处理服务器的反馈和状态。
(关于C++回调函数机制的设计,我会单独再写一篇)
6. 再次配置CMakeList.txt
接下来,在CMakeList.txt的末尾添加以下内容
add_executable(countdown_server src/countdown_server.cpp)
add_dependencies(countdown_server ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(countdown_server ${catkin_LIBRARIES})
add_executable(countdown_client src/countdown_client.cpp)
add_dependencies(countdown_server ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(countdown_client ${catkin_LIBRARIES})
此时功能包的目录结构大家可以对照看一下有没有问题
├── action
│ └── CountToNumber.action
├── CMakeLists.txt
├── include
│ └── my_action
├── package.xml
└── src
├── countdown_client.cpp
└── countdown_server.cpp
4 directories, 5 files
7. 启动 ROS Action 节点
编译
cd ~/catkin_ws/
catkin_make -DCATKIN_WHITELIST_PACKAGES=my_action
编译通过后起一个终端,输入roscore回车,启动ros master
roscore
再起一个终端来启动服务端
rosrun my_action countdown_server
再起一个终端来启动客户端
rosrun my_action countdown_client
客户端输出如下图
服务端打印信息如下图:
8. Action 的应用场景
- 机器人运动控制:如导航到指定点,机器人可以通过 Action 接收目标位置,在运动过程中反馈进度。
- 图像处理任务:复杂的图像处理任务(如识别、追踪)可能需要较长时间,可以通过 Action 提供进度反馈。
- 传感器数据采集:采集数据并持续返回采集进度。
Action 与 Service 的区别
特性 | Service | Action |
---|---|---|
通信方式 | 请求-响应,同步通信 | 目标-反馈-结果,异步通信 |
使用场景 | 短时间任务 | 长时间任务,提供进度反馈 |
回调机制 | 单次回调 | 多次反馈回调和结果回调 |
数据类型 | 请求和响应 | 目标、反馈和结果 |
中断任务 | 不支持 | 支持预取消和中断 |
总结
ROS Action 是 ROS 中一种异步的请求-反馈机制,适用于长时间运行任务。通过 Goal、Feedback 和 Result 的组合,Action 提供了一种更加灵活的任务管理方式,支持任务进度反馈和任务中断功能,使其在复杂的机器人任务中非常有用。