ROS Action

在 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 文件,包含三部分:GoalFeedbackResult,各部分使用 --- 分隔。

示例:定义一个计数器的 Action

假设我们想定义一个计数器的 Action,它接收一个目标数字并从 0 计数到该数字,期间不断提供进度反馈,并在完成后返回最终结果。

首先我们先创建一个my_action的功能包,进入到自己的ros工作空间下面,执行:

bash 复制代码
catkin_create_pkg my_action roscpp actionlib actionlib_msgs std_msgs

在建立的功能包下再创建一个action文件夹,用来存放".action"文件

定义 Countdown.action 文件:

XML 复制代码
# CountToNumber.action

# Goal: 目标数字
int32 target_number

---
# Result: 操作结果(是否成功)
bool success

---
# Feedback: 当前计数
int32 current_count

这意味着我们将向 Action 服务器发送一个目标数字,服务器会从 0 开始计数并每次提供当前的计数进度,最后返回一个表示成功的布尔值。

3. 配置 CMakeLists.txt 和 package.xml

然后,像之前的例子一样,我们需要更新 CMakeLists.txtpackage.xml 文件,确保 Action 文件被编译并生成相应的消息类型。

  • CMakeLists.txt
XML 复制代码
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
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功能包

bash 复制代码
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

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++的回调函数机制

cpp 复制代码
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 需要三个参数:
      1. nh_:ROS节点句柄,负责通信。
      2. name:Action 名称。
      3. 回调函数 :使用 boost::bind 绑定一个成员函数 executeCB,这是处理客户端请求的回调函数。_1 表示传递给回调函数的第一个参数(即 Goal 对象)。
    • false :这是一个布尔参数,表示是否立即启动服务器。这里设置为 false,表示服务器启动后不会自动开始等待请求,而是通过调用 as_.start() 来启动。
  • as_.start():启动 Action 服务器,开始监听客户端请求。

在 ROS 的 Action 服务器中,boost::bind 是一种通过将成员函数或普通函数转换为可调用对象(仿函数)的方式。这样做的目的是为了实现回调机制,尤其是在 actionlib::SimpleActionServer 中。

这段代码通过 boost::bindCountToNumberAction::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

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)。
  • 客户端使用三个回调函数:
    1. doneCb:处理任务完成后的状态和结果。
    2. activeCb:处理目标开始被服务器处理时的状态。
    3. feedbackCb:处理服务器发送的反馈信息(当前计数值)。
  • 客户端通过调用 ac.sendGoal 发送目标,并且使用 ros::spin() 保持客户端运行,接收和处理服务器的反馈和状态。

(关于C++回调函数机制的设计,我会单独再写一篇)

6. 再次配置CMakeList.txt

接下来,在CMakeList.txt的末尾添加以下内容

XML 复制代码
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})

此时功能包的目录结构大家可以对照看一下有没有问题

XML 复制代码
├── action
│   └── CountToNumber.action
├── CMakeLists.txt
├── include
│   └── my_action
├── package.xml
└── src
    ├── countdown_client.cpp
    └── countdown_server.cpp

4 directories, 5 files

7. 启动 ROS Action 节点

编译
bash 复制代码
cd ~/catkin_ws/
catkin_make -DCATKIN_WHITELIST_PACKAGES=my_action

编译通过后起一个终端,输入roscore回车,启动ros master

bash 复制代码
roscore

再起一个终端来启动服务端

XML 复制代码
rosrun my_action countdown_server

再起一个终端来启动客户端

bash 复制代码
rosrun my_action countdown_client 

客户端输出如下图

服务端打印信息如下图:

8. Action 的应用场景

  • 机器人运动控制:如导航到指定点,机器人可以通过 Action 接收目标位置,在运动过程中反馈进度。
  • 图像处理任务:复杂的图像处理任务(如识别、追踪)可能需要较长时间,可以通过 Action 提供进度反馈。
  • 传感器数据采集:采集数据并持续返回采集进度。

Action 与 Service 的区别

特性 Service Action
通信方式 请求-响应,同步通信 目标-反馈-结果,异步通信
使用场景 短时间任务 长时间任务,提供进度反馈
回调机制 单次回调 多次反馈回调和结果回调
数据类型 请求和响应 目标、反馈和结果
中断任务 不支持 支持预取消和中断

总结

ROS Action 是 ROS 中一种异步的请求-反馈机制,适用于长时间运行任务。通过 Goal、Feedback 和 Result 的组合,Action 提供了一种更加灵活的任务管理方式,支持任务进度反馈和任务中断功能,使其在复杂的机器人任务中非常有用。

相关推荐
唐诺2 小时前
几种广泛使用的 C++ 编译器
c++·编译器
冷眼看人间恩怨3 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
红龙创客3 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
Lenyiin3 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
yuanbenshidiaos5 小时前
c++---------数据类型
java·jvm·c++
十年一梦实验室5 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
taoyong0016 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
这是我586 小时前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
fpcc6 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
呆萌很6 小时前
C++ 集合 list 使用
c++