ROS之Action动作通信详解(分析+源码)

话题、服务、动作通信方式的对比

ROS之Action动作通信详解(分析+源码)_第1张图片

话题通信:异步通信

ROS之Action动作通信详解(分析+源码)_第2张图片

话题通信的好处在于:只要双方通过同一个topic建立联系,talker就可以向topic一直发送数据,listener也可以从topic中实时的获取数据。只要这个联系一直存在,那么数据的单向传输就不会中断。

那现在我们试想一下这样的情况:我想要控制机械臂一直在空中旋转并且实时的获取机械臂的运动位姿,那作为单向传输的话题通信就有些复杂了:

ROS之Action动作通信详解(分析+源码)_第3张图片

要用到两个topic才可以实现,这样一看确实有些复杂。

服务通信:同步通信

ROS之Action动作通信详解(分析+源码)_第4张图片

服务通信是一种“同步通信”,其优点就在于:无论何时只要客户端发送请求服务器一定给予实时响应。但是这种实时响应的缺点在于“only one”,也就是说服务通信双方一旦建立联系仅仅通信一次就立刻解裂。好处在于:相较于话题通信而言,服务通信有了反馈信息,我们可以通过服务端回传的response分析此次响应执行的情况。但最大的缺点在于response信息仅仅在响应完毕才给予回传,在任务执行过程中服务端仍不会回传任何反馈信息。

那再让我们想象一下:如果我的request是“小车按照指定路线行驶1km”,我们只有小车按照指定路线行驶1km到达目的地之后,服务端才会回传信息。在小车运动过程中,我们并不知道小车的任何信息。因此,服务通信的缺点在于它的实时性还不够:

ROS之Action动作通信详解(分析+源码)_第5张图片

动作通信

我们继续一种满足以下优势的通信模式:

  1. 可以实时反馈信息以及通信的状态
  2. 双方建立的通信关系在任务执行过程中稳定存在,并不需要一直存在
  3. 当出现意外情况时,我们可以先打断该通信联系,优先执行另一个通信
  4. 我们可以有条件地打断该通信联系
  5. 我们需要发送请求并且再执行完毕后知道响应结果

动作通信就可以完成这些要求。动作通信双方的逻辑关系如下:

ROS之Action动作通信详解(分析+源码)_第6张图片

其中,goal,cancel,feedback,state,result的含义如下:

  1. goal:客户端发送的请求,也就是客户端要求服务端做到的事情
  2. cancel:打断双方的通信联系,打断分两种:一种是两者的通信真正地被切断,另一种是两者的通信被意外的事件所打断,等处理完意外的事件之后,两者之间的通信自然恢复
  3. feedback:实时反馈的信息
  4. state:通信的状态(通信正在进行、通信已经完成、通信被挂起、通信被打断……)
  5. result:客户端最终执行的结果

我们可以整个通信拆解为两部分分别进行形象的比喻:

① 客户端-->服务端

ROS之Action动作通信详解(分析+源码)_第7张图片

② 服务端-->客户端

ROS之Action动作通信详解(分析+源码)_第8张图片

其中,state表征着通信链路的状态分为以下几种:

通信状态

含义

Pending

中间状态

目标服务器尚未处理目标

active

目标当前正在由操作服务器处理

recalled

最终状态

目标在开始执行前收到取消请求,并已成功取消

rejected

目标被操作服务器拒绝,因为目标无法实现或无效

preempted

目标在开始执行后收到取消请求,并已完成执行

aborted

由于某些故障,操作服务器在执行过程中中止了目标

succeeded

动作服务器成功地实现了目标

lost

行动客户端可以确定目标已丢失,由客户端自己判断

注意:这些变量都是使用枚举变量依次列举出来的,分别使用数字0-7表示。

以抢占状态为例:

ROS之Action动作通信详解(分析+源码)_第9张图片

 当我们在通信过程中,使用cancel_goal()函数取消了client之前发给server的请求(需要server达到的目标),那么就会触发抢占状态。

我的ROS项目文件的结构

ROS之Action动作通信详解(分析+源码)_第10张图片

动作通信的自定义文件格式

# goal  
uint32 targeted_washed_dishes  
---  
# result  
uint32 total_washed_dished  
---  
# feedback  
uint32 now_number_washed_dished 

关于请求响应的信息一共有三个,其先后顺序依次为:goal 、result、feedback。在配置完Package.xml和CMakelist.txt(无需配置“关于源文件编译链接的声明”)文件之后,我们可以开始Crtl+Shift+B开始编译.action文件,编译完成后我们在/devel/share/对应的package名称(actionProject)/msg下找到如下内容:

ROS之Action动作通信详解(分析+源码)_第11张图片

其实我们不难发现,action通信模式就是建立在.msg的基础之上,由于.msg话题通信可以实时的回传和发布信息,因此action通信是基于.msg而非基于.srv建立的:

ROS之Action动作通信详解(分析+源码)_第12张图片

由于客户端没有向服务器发送cancel信息,因此两者之间的通信大致可以如下图所示:

ROS之Action动作通信详解(分析+源码)_第13张图片

“用什么编译”以及“怎么编译”

Package.xml配置——用什么编译

catkin  
actionlib  
actionlib_msgs  
roscpp  
std_msgs  
actionlib  
actionlib_msgs  
roscpp  
std_msgs  
actionlib  
actionlib_msgs  
roscpp  
std_msgs  

CMakelist.txt配置——如何编译

CMakelist.txt文件的详细配解析:CMakelist.txt文件中常用部分最通俗易懂的解析(示例+解析)_超级霸霸强的博客-CSDN博客https://blog.csdn.net/weixin_45590473/article/details/122608048

① 声明编译依赖项

​
find_package(catkin REQUIRED COMPONENTS  
  actionlib  
  actionlib_msgs  
  roscpp  
  std_msgs  
)

​

② 声明功能包中自定义.action文件

add_action_files(  
  FILES  
  wash.action  
)  

③ 声明“用于编译.action文件”的功能包

generate_messages(  
  DEPENDENCIES  
  actionlib_msgs  
  std_msgs  
)  

④ 声明“生成CMake文件”所需的功能包

catkin_package(  
#  INCLUDE_DIRS include  
#  LIBRARIES actionProject  
 CATKIN_DEPENDS actionlib actionlib_msgs roscpp std_msgs  
#  DEPENDS system_lib  
)  

⑤ 关于源文件编译链接的声明

// 声明“需要编译成可执行文件”的源文件  
add_executable(actionClient src/actionClient.cpp)  
add_executable(actionServer src/actionServer.cpp)  
// 声明“单个源文件编译后需要连接库文件”的源文件  
target_link_libraries(actionClient  
  ${catkin_LIBRARIES}  
)  
target_link_libraries(actionServer  
  ${catkin_LIBRARIES}  
)  

客户端.cpp源文件的构建

基本流程

① 初始化节点:

ros::init(argc,argv,"actionClient");  

② 在master主节点注册信息:

// define ActionClient-type object  
ros::NodeHandle nh;  
actionlib::SimpleActionClient obj(nh,"chatter",true);
// true代表按照官方的方法管理线程,false代表自己手动管理线程

我们可以仔细地看一下“actionlib::SimpleActionClient”的构造函数:

SimpleActionClient(ros::NodeHandle & n, const std::string & name, bool spin_thread = true)  
 : cur_simple_state_(SimpleGoalState::PENDING)  
 {  
   initSimpleClient(n, name, spin_thread);  
 } 

其中参数的含义如下:

1. ros::NodeHandle & n:用于指定函数句柄,这个参数可以省略,因为如果你不指定系统会给设定默认的节点句柄。该句柄的作用是向master主节点上报本node的信息;

2. const std::string & name:用于指定client的topic,因为action通信是基于msg话题通信实现的,因此通信双方需要一个topic使得通信双方可以建立通信联系;

3. bool spin_thread = true:表征着构造函数中是否需要使用额外线程。注意参数spin_thread的设置。如果spin_thread为false则需要自行开启线程。

如果设置为false,请参考如下博文:

ROS actionlib学习(三) - XXX已失联 - 博客园 (cnblogs.com)https://www.cnblogs.com/21207-iHome/p/8304658.html

③ client等待server开启之后,在发送消息:

// wait until the server appears  
ROS_INFO("waiting for action server to start.");  
obj.waitForServer();  
ROS_INFO("Action server started, sending goal.");  

这是为了避免:当客户端启动时服务端未启动,这导致客户端认为服务端不存在,最终导致客户端放弃建立通信关系。

④ 打包“client-->server发送的goal请求数据”:

// pack data  
actionProject::washGoal goal;  
goal.targeted_washed_dishes = 10;  

我们这里设置的是:洗碗目标为10个。

⑤ client-->server提交请求,并且设置回调函数

// submit data to ActionServer  
obj.sendGoal(goal,&SimpleDoneCallbackFunc,&SimpleActiveCallbackFunc,&SimpleFeedbackCallbackFunc);  

1. SimpleDoneCallbackFunc:收到result时的回调函数

void SimpleDoneCallbackFunc(const actionlib::SimpleClientGoalState& state,const actionProject::washResultConstPtr& result)  
{  
    ROS_INFO("finish!\n\t");   
    ROS_INFO("Now State:%d\n\t",state.state_);  
    ROS_INFO("total_washed_dished:%d\n\t",result->total_washed_dished);  
    ros::shutdown(); // shut down the operation of node explicitly  
}  

2. SimpleActiveCallbackFunc:通信建立时的回调函数

void SimpleActiveCallbackFunc()  
{  
    ROS_INFO("the node is active!\n\t");  
}  

3. SimpleFeedbackCallbackFunc:收到feedback反馈信息时的回调函数

void SimpleFeedbackCallbackFunc(const actionProject::washFeedbackConstPtr& feedback)  
{  
    ROS_INFO("now_number_washed_dished:%d\n\t",feedback->now_number_washed_dished);  
}  

其实最难的一点就在于:我们不太容易获知函数参数的具体形式,我们可以参照函数原型进行确定:

函数模板:  
template  
void SimpleActionClient::sendGoal(const Goal & goal,  
  SimpleDoneCallback done_cb,  
  SimpleActiveCallback active_cb,  
  SimpleFeedbackCallback feedback_cb)  
函数参数——函数模板:  
typedef boost::function SimpleDoneCallback;  
typedef boost::function SimpleActiveCallback;  
typedef boost::function SimpleFeedbackCallback; 

⑥ 启动client:

obj.start();  

void start()子函数用于启动client,当调用此函数时,client开始于server寻求建立通信联系。

⑦ 不断轮询以便实时接收server传回的信息:

// loop continueously to receive information  
ros::spin(); // when finishing goal, shutdown() will end the node  

但是有个疑问:我们如何在server完成任务之后终止client的轮询状态呢?

这就需要我们注意:我们在使用setGoal函数时,我们设置了SimpleDoneCallbackFunc函数,这个函数用于“处理client接收到的来自server的result信息”。我们在该函数中加入了“void shutdown()”,这个函数的作用就是用于结束spin()的轮询工作。

附加知识

我们需要说明一下编写客户端.cpp源文件所需要用到的功能包:

#include "actionlib/server/ simple_action_client.h"  
#include "actionProject/washAction.h"

① “数据打包”工具包

首先,我们要重点介绍一下“actionProject/washAction”功能包,这个功能包的功能主要是:打包数据。前面介绍过,client-->server发送的数据一共有两个:goal和cancel,但其中,cancel作为由client发出的通信中止信号,无需变量承载,只需要调用相关函数即可实现,因此我们只需要打包goal——client-->server的请求数据即可。因此,我们只要给变量赋值即可完成数据打包操作:

// pack data  
actionProject::washGoal goal;  
goal.targeted_washed_dishes = 10;  

这里,client和server双方双向通信需要打包的变量一共有三个:feedback,result,goal:

actionProject::washFeedback feedback;  
actionProject::washGoal goal;  
actionProject::washResult result;  

这个功能包是我们.action自定义文件生成的,唯一的有用之处就在于打包数据,并且用得到的所有变量就是上面这三个。

② “通信”工具包

“actionlib/client/simple_action_client”工具包的用处在于“将打包好的数据信息client-->server进行传输”以及“接收server-->client回传的数据信息”。其成员函数如下:

1. 构造函数:

SimpleActionClient (ros::NodeHandle &n, const std::string &name, bool spin_thread=true)  
SimpleActionClient (const std::string &name, bool spin_thread=true)  

我们总是疑惑系统默认指定的NodeHandle和我们自己指定的NodeHandle有啥区别呢?这需要我们从NodeHandle的作用来进行说明:NodeHandle的作用是在master主节点注册本node的信息以及建立通信所需的topic,除此之外,NodeHandle还可以指定namespace:

ros::NodeHandle nh("/my_global_namespace");  // 指定一个全局域名
actionlib::SimpleActionClient obj(nh,"chatter",true);  

2. “等待此客户端连接到动作服务器”的函数:

bool waitForServer (const ros::Duration &timeout=ros::Duration(0, 0))

Duration(0,0)的含义是:client无限期等待server上线。其中参数表示的是等待时间:

obj.waitForServer(ros::Duration(3));//等待3s  

3. “检查是否成功连上动作服务器”的函数:

bool isServerConnected ();  

4. “发送一个目标值到动作服务器,然后等待直到目标完成或者超时”的函数:

SimpleClientGoalState sendGoalAndWait (const Goal &goal, const ros::Duration &execute_timeout=ros::Duration(0, 0), const ros::Duration &preempt_timeout=ros::Duration(0, 0)) 

例如:如若3s内完成不了goal,client就会发送一个新的goal去取代旧的goal

obj.sendGoalAndWait(goal,ros::Duration(0, 0),ros::Duration(3));  

5. “等待直至目标完成或者在指定的时间内等待result”的函数:

bool waitForResult (const ros::Duration &timeout=ros::Duration(0, 0))

该阻塞函数的意义在于:在指定时间内等待server回传给client端result或者一直等待直至server回传给client端result。

6. “获得通信状态”的函数:

SimpleClientGoalState   getState ();  

示例:

if (obj.getState() == actionlib::SimpleClientGoalState::SUCCEEDED)  
ROS_INFO("You have reached the goal!");

7. 其他成员函数:

获取当前目标的结果:  
ResultConstPtr  getResult ()  
取消正在动作服务器上运行的所有目标:  
void cancelAllGoals ()  
取消我们当前正在追踪的的目标:  
void cancelGoal ()  
取消在指定时间之前和之前标记的所有目标:  
void cancelGoalsAtAndBeforeTime (const ros::Time &time)  

客户端程序——actionClient.cpp

#include "actionlib/client/simple_action_client.h"  
#include "actionProject/washAction.h"  
#include "ros/ros.h"  
  
void SimpleDoneCallbackFunc(const actionlib::SimpleClientGoalState& state,const actionProject::washResultConstPtr& result)  
{  
    ROS_INFO("finish!\n\t"); 
    // state中有两个成员enumerate类型的state_和string类型的说明文本text_  
    ROS_INFO("Now State:%d\n\t",state.state_);  
    ROS_INFO("total_washed_dished:%d\n\t",result->total_washed_dished);  
    ros::shutdown(); // shut down the operation of node explicitly  
}  
void SimpleActiveCallbackFunc()  
{  
    ROS_INFO("the node is active!\n\t");  
}  
void SimpleFeedbackCallbackFunc(const actionProject::washFeedbackConstPtr& feedback)  
{  
    ROS_INFO("now_number_washed_dished:%d\n\t",feedback->now_number_washed_dished);  
}  
  
int main(int argc,char* argv[])  
{  
    // localize  
    setlocale(LC_ALL,"");  
    // initial the node  
    ros::init(argc,argv,"actionClient");  
    // define ActionClient-type object  
    ros::NodeHandle nh;  
    actionlib::SimpleActionClient obj(nh,"chatter",true);  
    // wait until the server appears  
    ROS_INFO("waiting for action server to start.");  
    obj.waitForServer();  
    ROS_INFO("Action server started, sending goal.");  
    // pack data  
    actionProject::washGoal goal;  
    goal.targeted_washed_dishes = 10;  
    // submit data to ActionServer  
    obj.sendGoal(goal,&SimpleDoneCallbackFunc,&SimpleActiveCallbackFunc,&SimpleFeedbackCallbackFunc);  
    // loop continueously to receive information  
    ros::spin(); // when finishing goal, shutdown() will end the node  
    return 0;  
} 

注意:sendGoal函数的调用一定要在waitForServer之后,因为只有server出现之后client才可以向server发送goal请求信息,如果两者顺序颠倒,则会出现死机的情况:

 服务端.cpp源文件的构建

基本流程

① 初始化节点

ros::init(argc,argv,"actionServer");  

一个节点就是一个.cpp源文件,习惯上来说节点名称和源文件名称一致,有些人疑惑“节点的名称和.cpp源文件的名称有啥关系呢?”其实,二者并没有任何关系。我们调用rqt_graph显示出的节点之间的关系图,上面的节点的名称就是我们节点初始化时的名称。.cpp源文件的名称我们在配置CMakelist.txt文件中用到过,编译器链接器根据.cpp源文件的名称将其编译链接为CMake文件。

② 在主节点注册server信息以及声明回调函数

// create the ActionServer-type object  
ros::NodeHandle nh;  
actionlib::SimpleActionServer obj(nh,"chatter",boost::bind(&ExecuteCallback,_1,&obj),false);  

其实,actionlib::SimpleActionServer类对象的初始化方式有多种:

SimpleActionServer(ros::NodeHandle n, std::string name, ExecuteCallback execute_callback, bool auto_start);  
SimpleActionServer(std::string name, ExecuteCallback execute_callback, bool auto_start); // 使用默认节点句柄  
SimpleActionServer(ros::NodeHandle n, std::string name, bool auto_start); // 省去了回调函数  
SimpleActionServer(std::string name, bool auto_start); // 使用默认节点句柄且省去了回调函数 

其实,回调函数的作用就是处理从client传来的信息,我们前面提到过clientàserver传递的信息一共有两个:goal和cancel。其中,goal信息标志着client向server发送的请求,因此当server收到从client发来的goal信息时,就会通过ros::spin()调用回调函数。回调函数如果没有的话,就说明server不会对client传来的请求给予响应。

最后一个函数为bool auto_start:true表征着“server节点自启动”,false则代表“我们必须手动启动server节点”。其实,自动启动中“自动”的含义就是:我们在设置为true之后,就无需在使用服务端的成员函数obj.start()手动自启动了。

③ 启动server

obj.start();  

我们应该注意到:client和server启动方式不同(为什么在客户端没有start函数呢?)。其原因就在于通信双方的角色不同,client担当请求的发起者,而server担当请求的承受者。因此,当client发送goal信息时就表明client已经发起了此次通信的建立,而server只有自己调用start成员函数启动才可以接收到来自client的请求从而被动的与其建立联系。

除此之外,如果server端的actionlib::SimpleActionServer::SimpleActionServer实例化对象时最后一个参数为true则不用再使用actionlib::SimpleActionServer::SimpleActionServer实例化对象调用start成员函数启动server了,因为auto_start=true代表着server节点自启动。

自启动和手动启动的区别:

其实都是启动server节点的数据接收功能,使其可以接收到来自client的信息并进行处理。但是自启动的含义是:当server向master上报自己信息之后紧接着就会启动请求接收功能,但是如果我们不想立刻启动server的数据接收功能,即有条件的启动server数据接收功能呢?这就得需要我们调用start函数,在有需要的时候在开启server的数据接收功能。与start函数对应的是shutdown函数,shutdown函数的作用:关闭server接收来自client请求的功能。

④ 调用阻塞函数以便实时接收来自client的请求从而给出响应

ros::spin();  

调用回调函数的两个条件:正在执行ros::spin()阻塞函数+server接收到来自client的goal信息,满足这两个条件,才会调用server端的回调函数。

⑤ 回调函数的编写

首先,我们要明确回调函数的作用:通过收到来自client的goal请求信号,产生响应向client发送feedback、state、result。回调函数的功能大致如下:

ROS之Action动作通信详解(分析+源码)_第14张图片

函数的编写逻辑如下:

1. 设置一套响应的方案;

2. 按照一定的频率上报feedback;

3. 完成后,将state设置为SUCCEEDED

相应的代码如下:

void ExecuteCallback(const actionProject::washGoalConstPtr& goalptr,actionlib::SimpleActionServer* ptr)  
{  
    ros::Rate r(1);  
    actionProject::washFeedback feedback;  
    ROS_INFO("final aim:%d\n\t",goalptr->targeted_washed_dishes);  
    // submit feedback  
    for(int i=0;i<=goalptr->targeted_washed_dishes;i++)  
    {  
        feedback.now_number_washed_dished = i;  
        ptr->publishFeedback(feedback);  
        r.sleep();  
    }  
    ROS_INFO("finished number:%d\n\t",feedback.now_number_washed_dished);  
    ptr->setSucceeded();  
}  

这里的响应逻辑很简单就是不断地加一而已,但是一定要注意从client传来的goal信息必须作为我们评价工作是否完成的依据,只有达到client请求的目标我们才可以设置state为SUCCEEDED。如下所示:

// submit feedback  
for(int i=0;i<=goalptr->targeted_washed_dishes;i++)  
{  
    feedback.now_number_washed_dished = i;  
    ptr->publishFeedback(feedback);  
    r.sleep();  
}  

只有当feedback.now_number_washed_dished= goalptr->targeted_washed_dishes时,循环才会结束(server响应才会结束),此时才会调用ptr->setSucceeded()将Result返回给client。

注意:state信号不是server传给client的,而是client根据传回的result进行判断得出的!

注意函数的两个入口参数:

const actionProject::washGoalConstPtr& goalptr

从client传来的goal信息

actionlib::SimpleActionServer* ptr

用于向client发布信息

比较容易忽略的是函数入口参数类型的确定,我们可以参考函数类型的原型来确定参数类型:

typedef boost::function ExecuteCallback;

我当时就是因为漏掉了const导致编译失败但却找不到原因所在。我们读了上述代码可能带有些许疑惑:上述回调函数只回传了feedback和state,那result咋由server传给client的呢?代码咋没体现呢?其实ptr->setSucceeded()函数已经包含了"回传state状态"和"回传result",我们可以看该函数的实现:

void setSucceeded(const demo02::infoResult &result = actionlib::SimpleActionServer::Result(), const std::string &text = ((std::string)("")))
{
  boost::recursive_mutex::scoped_lock lock(lock_);
  ROS_DEBUG_NAMED("actionlib", "Setting the current goal as succeeded");
  current_goal_.setSucceeded(result, text);
}

我们在代码中调用了其无参构造形式,无参构造中的第一个默认参数就是"actionlib::SimpleActionServer::Result()",第二个默认参数是字符串类型的说明文本,对应于client中的state.text_变量。

或许有人又会问:为何回调函数形式为boost::function execute_callback形式,而我们编写的回调函数却有两个入口参数呢?而且我们为何使用boost::bind函数呢?

首先我们要注意一点:这个函数不是C++标准函数对象,因此我们不可以像如下这样写:

#include "actionlib/server/simple_action_server.h"  
#include "actionProject/washAction.h"  
#include "ros/ros.h"  
  
void ExecuteCallback(const actionProject::washGoalConstPtr& goalptr)  
{
    ......
}
  
int main(int argc, char* argv[])  
{  
    ......
    actionlib::SimpleActionServer obj(...,ExecuteCallback,...); 
}

 因为ExecuteCallback只是一个C++标准函数对象,并不是boost标准的函数对象,因此我们常常使用boost::bind进行绑定,经boost::bind绑定之后,C++标准函数对象就转化为了boost标准的函数对象。那为何有两个参数呢?多的那个参数有啥用呢?

这就要从boost::bind绑定的对象说起,boost::bind在这里绑定的是类对象,返回的是该类成员函数的函数对象,为何要绑定类对象呢?

我们前面提到过以下两个C++库的作用:

// 用于进行client和server间的通信
#include "actionlib/server/simple_action_server.h" 
// 用于封装通信数据 
#include "actionProject/washAction.h"  

 我们在代码中实例化了一个动作通信的服务端(实例化的类对象),这个实例化的类对象通过topic与对面的client建立了通信通道,我们要在回调函数中向client传输信息就必须借助这个通信通道,因此在回调函数中要想要把信息发出去就必须借助这个类对象。

如何绑定类的实例化对象从而使用该实例化对象调用其成员函数?

ROS之Action动作通信详解(分析+源码)_第15张图片

详见:Boost::bind使用详解 - jackjoe - 博客园 (cnblogs.com)https://www.cnblogs.com/blueoverflow/p/4740093.html

boost标准的函数对象和C++标准的函数对象有何区别?

其实他们两者的区别主要出现在绑定成员函数时,其他时候并无差异,比如在客户端中的三个回调函数:

#include "actionlib/client/simple_action_client.h"
#include "demo02/infoAction.h"
#include "ros/ros.h"

void SimpleDoneCallback(const actionlib::SimpleClientGoalState& state, const demo02::infoResultConstPtr& result)
{
    ROS_INFO("state:%d,result:%d\n\t",state.state_,result->ResultNum);
}
void SimpleActiveCallback()
{
    ROS_INFO("Active!");
}
void SimpleFeedbackCallback(const demo02::infoFeedbackConstPtr &feedback)
{
    ROS_INFO("feedback:%d\n\t",feedback->FeedbackNum);
}

int main(int argc,char* argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"actionClient");
    ros::NodeHandle nh;
    actionlib::SimpleActionClient Client(nh,"chatter",true);
    demo02::infoGoal goal; goal.GoalNum = 10;
    Client.sendGoal(goal,SimpleDoneCallback,SimpleActiveCallback,SimpleFeedbackCallback);
    Client.waitForServer();
    Client.waitForResult();
    return 0;
}

 他们并没有使用boost::bind(...)将其转化为boost标准的函数对象也可以使用,原因就在于“这三个函数并不是类的成员函数,也就是说这三个函数并不需要绑定实例化的类就可以运行”,我们也可以从通信信号传输的逻辑关系中就可以看出一些端详:

ROS之Action动作通信详解(分析+源码)_第16张图片

actionClient作为action通信的client客户端发送给server的信息只有goal目标请求和cancel取消请求这两个信号,feedback、result、state这三个信号都是client接收到的信号。此外,“给参数传入类的实例化对象的指针”的大背景是:server要在回调函数中向client发送feedback、result、state这三个信号,必须要有server通信句柄才可以,在函数中你不给函数传如通信句柄,你得到了feedback、result、state这三个信号但是没有通信句柄,那你一个也发不出去!

 actionlib::SimpleActionServer的成员函数

① 轮询判断函数

轮询判断server是否正在执行当前client的请求  
bool isActive()  
轮询判断server是否接收到来自client的新请求  
bool isNewGoalAvailable()  
轮询判断server是否接受到抢占的请求  
bool isPreemptRequested()  

② server向client发送feedback数据的函数

void publishFeedback (const FeedbackConstPtr &feedback)  
void publishFeedback (const Feedback &feedback)  

③ 注册回调函数的函数

注册“接收到来自client请求信息时的回调函数”  
void registerGoalCallback (boost::function< void()> cb)  
注册“接收到抢占信号的回调函数”  
void registerPreemptCallback (boost::function< void()> cb)  

我们前面提到过actionlib::SimpleActionServer对象的初始化方式中有“可以无需在创建对象时就指定回调函数”的构造函数。我们在声明时不指定回调函数,可以在创建完对象之后再指定回调函数。

④ state状态机设置函数

// 将state置为ABORTED——由于某些故障,操作服务器在执行过程中中止了目标  
void setAborted (const Result &result=Result(), const std::string &text=std::string(""))  
// 将state置为PREEMPTED——目标在开始执行后收到取消请求,并已完成执行  
void setPreempted (const Result &result=Result(), const std::string &text=std::string(""))  
// 将state置为SUCCEEDED——动作服务器成功地实现了目标  
void setSucceeded (const Result &result=Result(), const std::string &text=std::string("")) 

使用示例:

actionlib::SimpleActionServer::Result result;  
ptr->setSucceeded(result,"SUCCEEDED\n\t");  

⑤ 中止/启动server函数

// 中止服务端的运行  
void shutdown ()  
// 启动服务端的运行  
void start ()  

⑥ 用于“接收来自client的新的goal请求信息“的函数

template  
boost::shared_ptr::Goal> SimpleActionServer  
::acceptNewGoal()  

顾名思义,这个函数是用来接收从client发来的新的goal请求信息的。在使用这个函数时一定要特别注意:isNewGoalAvailable、acceptNewGoal、isPreemptRequested这三个函数的使用次序。三者的作用分别为:

isNewGoalAvailable

判断server是否已经接收到来自client的新请求

acceptNewGoal

接收一个来自client的新请求

isPreemptRequested

判断旧的goal被新的goal抢占了吗

官方文档特别标注三者的使用次序:

ROS之Action动作通信详解(分析+源码)_第17张图片

 当从client发来的新的goal请求信息已经覆盖了旧的goal请求信息,server端才会调用抢占回调函数。“从client发来的新的goal请求信息已经覆盖了旧的goal请求信息”的标志就是obj.isNewGoalAvailable()=true(obj是actionlib::SimpleActionServer的模板实例化对象),只有在判断obj.isNewGoalAvailable()之后,我们才可以用obj.isPreemptRequested()来判断旧的goal是否被抢占,如果obj.isNewGoalAvailable()=true并且我们使用函数obj.registerPreemptCallback (boost::function< void()> cb)注册了抢占回调函数,那么在新goal覆盖旧goal之后,即obj.isNewGoalAvailable()=true之后,server端会立刻调用抢占回调函数。

服务端程序——actionServer.cpp

#include "actionlib/server/simple_action_server.h"  
#include "actionProject/washAction.h"  
#include "ros/ros.h"  
  
void ExecuteCallback(const actionProject::washGoalConstPtr& goalptr,actionlib::SimpleActionServer* ptr)  
{  
    ros::Rate r(1);  
    actionProject::washFeedback feedback;  
    ROS_INFO("final aim:%d\n\t",goalptr->targeted_washed_dishes);  
    // submit feedback  
    for(int i=0;i<=goalptr->targeted_washed_dishes;i++)  
    {  
        feedback.now_number_washed_dished = i;  
        ptr->publishFeedback(feedback);  
        r.sleep();  
    }  
    ROS_INFO("finished number:%d\n\t",feedback.now_number_washed_dished);  
    ptr->setSucceeded();  
}  
  
int main(int argc, char* argv[])  
{  
    // localize  
    setlocale(LC_ALL,"");  
    // initial the node  
    ros::init(argc,argv,"actionServer");  
    // create the ActionServer-type object  
    ros::NodeHandle nh;  
    actionlib::SimpleActionServer obj(nh,"chatter",boost::bind(&ExecuteCallback,_1,&obj),false);  
    obj.start();  
    ros::spin();  
    return 0;  
}

附加知识

我们在编写action通信双方的源文件时一定要注意:

① 在编写client源文件时

// define ActionClient-type object  
 ros::NodeHandle nh;  
 actionlib::SimpleActionClient obj(nh,"chatter",true);  

spin_thread参数是true

② 在编写server源文件时

// create the ActionServer-type object  
ros::NodeHandle nh;  
actionlib::SimpleActionServer obj(nh,"chatter",boost::bind(&ExecuteCallback,_1,&obj),false);  

auto_start参数是false

一旦两者均为true,则运行之后在server端会出现如下警示:

You've passed in true for auto_start for the C++ action server at [/topic_name]. You should always pass in false to avoid race conditions.

原因如下:

在官方的文档说明中,明确指出

server端的auto_start=false,如果设置为true会引发race conditions,具体请参考:Race Condition(竞争条件)_涂涂的博客-CSDN博客_竞争条件https://blog.csdn.net/u012562273/article/details/56486776#:~:text=Race%20Condition%20Race%20Condition%20%EF%BC%88,%E7%AB%9E%E4%BA%89%E6%9D%A1%E4%BB%B6%20%EF%BC%89%E6%98%AF%E4%B8%80%E7%A7%8D%E6%83%85%E5%BD%A2%EF%BC%8C%E5%9C%A8%E8%AF%A5%E6%83%85%E5%BD%A2%E4%B8%8B%E7%B3%BB%E7%BB%9F%E6%88%96%E8%80%85%E7%A8%8B%E5%BA%8F%E7%9A%84%E8%BE%93%E5%87%BA%E5%8F%97%E5%85%B6%E4%BB%96%E4%B8%8D%E5%8F%AF%E6%8E%A7%E4%BA%8B%E4%BB%B6%E7%9A%84%E9%A1%BA%E5%BA%8F%E6%88%96%E4%BA%8B%E4%BB%B6%E7%9A%84%E5%BD%B1%E5%93%8D%E3%80%82%20%E8%BD%AF%E4%BB%B6%E4%B8%AD%E7%9A%84%20Race%20Condition%20%E9%80%9A%E5%B8%B8%E5%87%BA%E7%8E%B0%E5%9C%A8%E4%B8%A4%E4%B8%AA%E5%B9%B6%E5%8F%91%E7%BA%BF%E7%A8%8B%E8%AE%BF%E9%97%AE%E5%90%8C%E4%B8%80%E4%B8%AA%E5%85%B1%E4%BA%AB%E8%B5%84%E6%BA%90%E3%80%82

由于两个或者多个进程竞争使用不能被同时访问的资源,使得这些进程有可能因为时间上推进的先后原因而出现问题,这叫做竞争条件(Race Condition)。。

相关知识解析

捆绑函数boost::bind的使用

int f(int a, int b)  
{  
    return a + b;  
}  
  
int g(int a, int b, int c)  
{  
    return a + b + c;  
}  

普通用法:函数传参

bind(f, 1, 2)等价于f(1, 2); bind(g, 1, 2, 3)等价于g(1, 2, 3);

灵活用法:有选择性地绑定参数

bind(f, _1, 5)(x)等价于f(x, 5),其中_1是一个占位符,表示用第一个参数来替换;

bind(f, _2, _1)(x, y)等价于f(y, x);

bind(g, _1, 9, _1)(x)等价于g(x, 9, x);

bind(g, _3, _3, _3)(x, y, z)等价于g(z, z, z);

最终运行结果

① server端:

② client端:

ROS之Action动作通信详解(分析+源码)_第18张图片

你可能感兴趣的:(ROS,ROS,通信,action通信,C++)