1、为什么要用action
在ros中,大部分情况下的信息我们使用publisher与subscriber的机制来处理,也就是topic的机制。它能处理一些不用带状态返回的数据处理。当涉及到需要数据返回的节点间数据交互时,首选也可以采用servicer的机制进行处理与结果的返回。
但是存在一种使用上述两种方式都不是很合适的情况:当一个节点向另一个节点请求处理数据,但是另一个节点在处理该请求时需要花费很长的等待时间。这时候如果前一个节点一直处于一种等待的情况无法知道对方的处理情况的话就会很糟糕。一种极端的情况就是处理数据的节点陷入死循环了请求的节点一直收不到结果,程序就会卡死。
而ROS action在一定程度上就弥补了这个问题,相对于service它更适合用于处理这种长时间处理的情况,首先在action中是可以带feedback反馈的,能够在一定程度上知道运行程度的。其次在action中,是可以通过cancel取消当前在执行的action的。像是在service中,当我们请求一个程序运行的时候,如果中途我们发现运行的轨迹偏离了预期,是无法请求停止的,需要让其主动返回失败才可以。但是在cation中,如果我们发现运行的结果偏离预期,可以请求取消继续执行。所以改机制在很多场景下都是比topic以及service更加适合。
2、建立一个action消息
之前用过msg也用过service,其实很多地方都有类似的。一点小小的区别可能再于msg只有一个类,而service有两个类:请求的request部分以及返回的response。而action则包含了三个部分:请求的goal、返回的result以及执行过程中的feedback
简单的写一个action如下:
首先建立一个action文件:
catkin_create_pkg action_test roscpp rospy message_generation
cd action_test
mkdir action
cd cation
gedit Actiontest.action
然后在Actiontest.action中输入:
#goal definition
int32 numb
---
#result definition
bool result
---
#feedback
int32 temp_sum
同样修改cmakelist文件与package.xml文件。
对于cmakelist文件,需要修改三个地方:
1、find_package中添加最后两行
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
tf
actionlib_msgs
actionlib
)
2、add_action_files中添加action的名称
add_action_files(
FILES
XunJiePhoto.action
#DIRECTORY
)
3、generate_messages中添加actionlib_msgs
generate_messages(
DEPENDENCIES
actionlib_msgs
)
对于package.xml文件需要添加下面内容:
<build_depend>actionlib</build_depend>
<build_depend>actionlib_msgs</build_depend>
<build_export_depend>actionlib</build_export_depend>
<build_export_depend>actionlib_msgs</build_export_depend>
<exec_depend>actionlib</exec_depend>
<exec_depend>actionlib_msgs</exec_depend>
<exec_depend>message_runtime</exec_depend>
<build_depend>message_generation</build_depend>
不过似乎上面六行我没加编译也成功了,不知道会不会对执行产生影响,为了保险起见建议还是加一下。
完成上述部分后,catkin_make编译即可生成对应的.h文件:
3、如何使用一个action
action机制部分跟service类似,也是需要一个客户端一个服务端。首先我们先写一个自加的服务端:
3.1、action服务端
我们同样使用C++来完成这部分代码:
#include "ros/ros.h"
#include "action_test/ActiontestAction.h"
#include "action_test/ActiontestGoal.h"
#include <actionlib/server/simple_action_server.h>
#include <actionlib/client/simple_action_client.h>
class action_server
{
private:
/* data */
ros::NodeHandle n;
action_test::ActiontestResult add_sum;
action_test::ActiontestFeedback add_feedback;
actionlib::SimpleActionServer<action_test::ActiontestAction> as_;
public:
action_server(/* args */);
~action_server();
void actioncb(const actionlib::SimpleActionServer<action_test::ActiontestAction>::GoalConstPtr& goal);
};
action_server::action_server(/* args */):
as_(n, "test_add", boost::bind(&action_server::actioncb, this, _1),false)
{
as_.start();
}
void action_server::actioncb(const actionlib::SimpleActionServer<action_test::ActiontestAction>::GoalConstPtr& goal)
{
int numb = goal->numb;
int sum = 0;
for(int i=0;i<numb;i++)
{
sum+=i;
add_feedback.temp_sum = sum;
as_.publishFeedback(add_feedback);
if (as_.isPreemptRequested())
{
add_sum.result = sum;
as_.setPreempted(add_sum);
return;
}
ros::Duration(0.2).sleep();
}
add_sum.result = sum;
as_.setSucceeded(add_sum);
}
action_server::~action_server()
{
}
int main(int argc, char **argv)
{
ros::init(argc, argv,"action_server");
action_server action_server;
ros::spin();
return 0;
}
首先在最上面我们需要添加几个头文件,这里比service或者topic要多一点:
#include "action_test/ActiontestAction.h"
#include "action_test/ActiontestGoal.h"
#include <actionlib/server/simple_action_server.h>
#include <actionlib/client/simple_action_client.h>
这里:
#include "action_test/ActiontestGoal.h"
好像不是必要的。
然后声明一个类,在类函数中需要声明你的action:
actionlib::SimpleActionServer<action_test::ActiontestAction> as_;
前面是固定格式<>中是action消息类型。以及需要一个对应的处理函数:
void actioncb(const actionlib::SimpleActionServer<action_test::ActiontestAction>::GoalConstPtr& goal);
另外这里还声明了两个变量,当然如果不使用的话也不影响。
action_test::ActiontestResult add_sum;
action_test::ActiontestFeedback add_feedback;
上面一个作为最后的返回值result的,下面一个是反馈值feedback。
接下来我们需要启动这个action:
action_server::action_server(/* args */):
as_(n, "test_add", boost::bind(&action_server::actioncb, this, _1),false)
{
as_.start();
}
注意到:
as_(n, "test_add", boost::bind(&action_server::actioncb, this, _1),false)
是写在“{}“外面的,刚开始不是很理解,但是看了这篇博客后也还是不是特别理解,不过确实是应该这么写的,否则是无法通过编译的。
最后看一下处理函数中用到的一些东西。本身逻辑很简单,从0开始0.2秒加一次,一直加到给定的数值。最后通过setSucceeded函数返回。但是在其中我们需要注意两个函数:
publishFeedback(add_feedback)
以及
isPreemptRequested()
前面我们提到action相对于service的区别再于action有反馈以及可以中途停止。其中action的反馈就是通过publishFeedback函数实现的。前面我们定义了一个action_test::ActiontestFeedback add_feedback,所以在使用时给add_feedback赋值然后通过publishFeedback函数发布出来就可以了。
而下面的isPreemptRequested则是用来中止的,action函数的中止通过client发送canceled消息执行,而server端的判断通过isPreemptRequested判断。当收到中止指令时,isPreemptRequested为true。然后我们可以使用setPreempted(add_sum)函数返回当前结果然后退出循环。
编译上述代码后运行,其一共会生成5个topic:
这里面cancel是用来取消当前action的,goal是用来接收cation的。这两个由client发布server接收。而feedback用来实时反馈当前情况,status也是状态的一种,代表当前action状态。运行中是1,被取消时变成2,空闲时为3,abort时会变成4。result则是返回最后的结果。
我们可以使用rqt测试server。在rqt中添加goal的topic。goal给100,如下:
当我们执行该topic时,echo几个话题可以得到对应的数据:像publishFeedback由于回调函数中是循环发布的所以它会一直更新:
而result设置了结束时候发布一次,所以当程序结束时它会被发布:
另外注意到cliect端还可以用于控制action在特定的时候关闭,比如运行到一半的时候,不想继续执行的时候我们可以通过cancel的topic停止当前循环,在isPreemptRequested()函数接收到cancel请求时会进入函数处理。在rqt测试时需要注意cancel的goal_id必须跟上面是一样的,要不然是不会生效的。比如我们在程序运行中执行一下cancel:
会发现终端中的输出如下:
可以看到result本来应该是4950的但是由于执行了cancel所以提前退出了,最后的结果通过setPreempted(add_sum)发布。
3.1、action客户端
客户端比服务端简洁一点,因为它没有回调函数:
#include "ros/ros.h"
#include "action_test/ActiontestAction.h"
#include "action_test/ActiontestGoal.h"
#include <actionlib/server/simple_action_server.h>
#include <actionlib/client/simple_action_client.h>
using namespace std;
class action_client
{
private:
/* data */
ros::NodeHandle n;
public:
action_client();
~action_client();
void action_call();
actionlib::SimpleActionClient<action_test::ActiontestAction> action_test_;
};
action_client::action_client():
action_test_("test_add",true)
{
}
void action_client::action_call()
{
cout<<"call server"<<endl;
action_test::ActiontestGoal addgoal;
addgoal.numb = 100;
action_test_.sendGoal(addgoal);
//action_test_.sendGoalAndWait(addgoal);
}
action_client::~action_client()
{
}
int main(int argc,char **argv)
{
ros::init(argc, argv,"action_client");
action_client action_client;
ros::spinOnce();
action_client.action_test_.waitForServer();
action_client.action_call();
ros::spin();
return 0;
}
这个代码比较简单,可以参考server端的解释。它的效果是请求一个goal给server,可以起到跟上面的rqt同样的效果。需要注意的是其中这一行:
action_client.action_test_.waitForServer();
比较特殊,在服务端不加这个似乎会出现bug,就是服务端客户端都正常打开但是服务端收不到客户端发送的Goal。所以保险起见还是加一下。
https://docs.ros.org/en/kinetic/api/actionlib/html/classactionlib_1_1SimpleActionServer.html
https://docs.ros.org/en/kinetic/api/actionlib/html/classactionlib_1_1SimpleActionClient.html
https://blog.csdn.net/qq_42145185/article/details/107508640