1.5 ROS节点通信
前面已经学习了大量ROS的基础知识,到这里终于可以开始编写ROS代码了。ROS代码的编写围绕节点通信过程中的消息机制和消息类型两个核心点展开,因此先详细阐述话题(topic)、服务(service)和动作(action)三种消息机制的原理,然后介绍这三种消息机制中使用的消息类型,最后用C++编写基于这三种消息机制的代码实例。
话题通信方式是单向异步的,发布者只负责将消息发布到话题,订阅者只从话题订阅消息,发布者与订阅者之间并不需要事先确定对方的身份,话题充当消息存储容器的角色。这种机制很好地实现了发布者与订阅者程序之间的解耦。由于话题通信方式是单向的,即发布者并不能确定订阅者是否按时接收到消息,所以这种机制也是异步的。话题通信一般用在实时性要求不高的场景中,比如传感器广播其采集的数据。图1-6所示为一个通过话题消息机制传递hello消息内容的过程。
图1-6 通过话题消息机制通信的过程
服务通信方式是双向同步的,服务客户端向服务提供端发送请求,服务提供端在收到请求后立即进行处理并返回响应信息。图1-7所示为一个通过服务消息机制计算两个数之和的实例。服务通信一般用在实时性要求比较高且使用频次低的场景下,比如获取全局静态地图。
图1-7 通过服务消息机制通信的过程
动作通信方式是双向异步的,动作客户端向动作服务端发送目标,动作服务端要达到目标需要一个过程,动作服务端在执行目标的过程中实时地反馈消息,并在目标完成后返回结果。动作通信用于过程性的任务执行场景下,比如导航任务。图1-8所示为一个通过动作消息机制实现倒计时任务的实例。
图1-8 通过动作消息机制通信的过程
进一步探究消息机制的底层实现,能够帮助大家更深入地理解ROS的性能特点。ROS的消息机制基于XMLRPC(XML Remote Procedure Call,XML远程过程调用)协议,这是一种采用XML编码格式,传输方式既不保持连接状态,也不检测连接状态的请求/响应式的HTTP协议。ROS的主节点Master采用XMLRPC协议对节点的注册和连接进行管理,一旦两个节点建立了连接,节点之间就可以利用TCP/UDP协议传输消息数据了。图1-9所示为XMLRPC通信模型。在话题通信中,节点需要借助XMLRPC完成注册和连接,然后订阅者发起订阅之后,发布者就开始持续发布消息;在服务通信中,节点借助XMLRPC完成注册,但不需要建立连接就可以直接发起请求,响应完成后就自动断开;在动作通信中,节点借助XMLRPC完成注册,与服务通信类似,也不需要建立连接就可以直接发起目标,只是在响应的基础上多了一个反馈过程,完成后就自动断开。
图1-9 XMLRPC通信模型
了解ROS消息机制的原理后,接下来讨论ROS中的消息类型。其实消息类型就是一种数据结构,最底层的数据结构还是C++/Python的基本数据类型,只是ROS基于这些基本数据类型做了自己的封装。ROS中的消息类型分两种:一种是ROS定义的标准消息类型,另一种是用户利用标准消息类型自己封装的非标准消息类型,后者是对标准消息类型的有效补充。不管是标准的消息类型还是自定义的消息类型,都需要在功能包中进行封装,因此使用消息类型时需要使用功能包名和子类型名同时对其进行标识。ROS标准消息类型主要封装在std_msgs、sensor_msgs、geometry_msgs、nav_msgs、actionlib_msgs等功能包中。消息类型按消息通信机制也相应分为三种类型:话题消息类型、服务消息类型、动作消息类型。如果想要了解ROS标准消息类型的详细定义以及具体用法,可以查阅以下ROS的官方wiki页面:https://wiki.ros.org/std_msgs、https://wiki.ros.org/sensor_msgs、https://wiki.ros.org/geometry_msgs、https://wiki.ros.org/nav_msgs、https://wiki.ros.org/actionlib_msgs。
1.5.1 话题通信方式
现在我们就来编写真正意义上使用ROS进行节点间通信的程序。由于之前已经建好了catkin_ws工作空间,以后开发的功能包都将放在这个工作空间。这里给新建的功能包取名为topic_example,在这个功能包中分别编写publish_node.cpp和subscribe_node.cpp两个节点程序,发布节点publish_node向话题chatter发布std_msgs::String类型的消息,订阅节点subscribe_node从话题chatter订阅std_msgs::String类型的消息,这里消息传递的具体内容是一句问候语hello,具体过程如图1-6所示。开发步骤包括创建功能包、编写源码、配置编译和启动节点。创建功能包就是在工作空间用catkin_create_pkg命令新建功能包并显式指明对roscpp和std_msgs等的依赖;编写源码就是为节点编写具体的代码实现;配置编译就是在功能包的CMakeLists.txt和package.xml中添加编译依赖项以及编译目标项后用catkin_make命令执行编译;启动节点就是利用rosrun或者roslaunch启动编译生成的可执行节点。限于篇幅,下面就只讨论编写源码步骤相关内容了,整个功能包的详细实现请参考本书GitHub仓库[1]中的相应源码。
功能包中需要编写两个独立可执行的节点:一个节点用来发布消息,另一个节点用来订阅消息,所以需要在新建的功能包topic_example/src/目录下新建两个文件——publish_node.cpp和subscribe_node.cpp。首先来看看发布节点publish_node.cpp的代码内容,如代码清单1-1所示。
代码清单1-1 发布节点publish_node.cpp
1 #include "ros/ros.h" 2 #include "std_msgs/String.h" 3 4 #include <sstream> 5 6 int main(int argc, char **argv) 7 { 8 ros::init(argc, argv, "publish_node"); 9 ros::NodeHandle nh; 10 11 ros::Publisher chatter_pub = nh.advertise<std_msgs::String>("chatter", 1000); 12 ros::Rate loop_rate(10); 13 int count = 0; 14 15 while (ros::ok()) 16 { 17 std_msgs::String msg; 18 19 std::stringstream ss; 20 ss << "hello " << count; 21 msg.data = ss.str(); 22 ROS_INFO("%s", msg.data.c_str()); 23 24 chatter_pub.publish(msg); 25 26 ros::spinOnce(); 27 loop_rate.sleep(); 28 ++count; 29 } 30 31 return 0; 32 }
第1行,是必须包含的头文件,这是ROS提供的C++客户端库。
第2行,是包含ROS提供的标准消息类型std_msgs::String的头文件,这里使用标准消息类型来发布话题通信的消息。
第8行,初始化ROS节点并指明节点的名称,这里给节点取名为publish_node,一旦程序运行后就可以在ROS的计算图中被注册为publish_node名称标识的节点。
第9行,声明一个ROS节点的句柄,初始化ROS节点所必需的。
第11行,告诉ROS节点管理器,publish_node将会在chatter这个话题上发布std_msgs::String类型的消息。这里的参数1000表示发布序列的大小,如果消息发布太快,缓冲区中的消息数大于1000个,则会开始丢弃先前发布的消息。
第12行,指定自循环的频率,这里的参数10表示10Hz,需要配合该对象的sleep()方法来使用。
第15行,调用roscpp库时会默认安装SIGINT句柄,以处理使ros:ok()被触发并返回false值的情况,如Ctrl+C键盘操作、该节点被另一同名节点踢出ROS网络、ros::shutdown()被程序在某个地方调用、所有ros::NodeHandle句柄都被销毁等。
第17行,定义了一个std_msgs::String消息类型的对象,该对象有一个数据成员data,用于存放我们即将发布的数据。要发布的数据将被填充到这个对象的data成员中。
第24行,利用定义好的发布器对象将消息数据发布出去,这一句执行后,ROS网络中的其他节点便可以收到此消息中的数据。
第26行,让回调函数有机会被执行的声明,该程序中并没有回调函数,所以这一句可以不要,这里只是为了实现程序的完整规范性。
第27行,前面讲过,这一句是通过休眠来控制自循环的频率。
其实将发布节点的代码稍做修改,就能得到订阅节点。接着来看看订阅节点subscribe_node.cpp的代码内容,如代码清单1-2所示。
代码清单1-2 订阅节点subscribe_node.cpp
1 #include "ros/ros.h" 2 #include "std_msgs/String.h" 3 4 void chatterCallback(const std_msgs::String::ConstPtr& msg) 5 { 6 ROS_INFO("I heard: [%s]",msg->data.c_str()); 7 } 8 9 int main(int argc, char **argv) 10 { 11 ros::init(argc, argv, "subscribe_node"); 12 ros::NodeHandle nh; 13 14 ros::Subscriber chatter_sub = nh.subscribe("chatter", 1000,chatterCallback); 15 16 ros::spin(); 17 18 return 0; 19 }
在发布节点中已经解释过的类似代码,就不再赘述了。这里重点解释一下前面没遇到过的代码。
第4~7行,一个回调函数,当有消息到达chatter话题时会自动被调用一次,这个回调函数里只有一句话,用来打印从话题中订阅的消息数据。
第14行,告诉ROS节点管理器我们将从chatter这个话题中订阅消息,当有消息到达时会自动调用这里指定的chatterCallback回调函数。参数1000表示订阅序列的大小,如果消息处理的速度不够快,缓冲区中的消息数大于1000就会开始丢弃先前接收的消息。
第16行,让程序进入自循环的挂起状态,从而让程序以最大效率接收消息并调用回调函数。如果没有消息到达,这行代码的执行不会占用很多CPU资源,所以可以放心使用。一旦ros::ok()被触发而返回false,ros::spin()的挂起状态将停止并自动跳出。简单点说,程序执行到这一句,就会进行自循环,同时检查是否有消息到达并决定是否调用回调函数。
当功能包编译完成后,先用roscore命令启动ROS节点管理器,ROS节点管理器是所有节点运行的基础。打开命令行终端,输入如下命令。
roscore
然后,就可以用rosrun命令来启动功能包topic_example中的节点publish_node,发布消息。打开另外一个命令行终端,输入如下命令。
rosrun topic_example publish_node
启动完publish_node节点后,可以在终端中看到打印信息不断输出,如图1-10所示。这就说明发布节点已经正常启动,并不断向chatter话题发布消息数据。
图1-10 启动publish_node节点发布消息
最后,用rosrun命令来启动功能包topic_example中的节点subscribe_node,订阅上面发布出来的消息。打开另外一个命令行终端,输入如下命令。
rosrun topic_example subscribe_node
启动完subscribe_node节点后,可以在终端中看到打印信息不断输出,如图1-11所示。这就说明订阅节点已经正常启动,并不断从chatter话题订阅消息数据。
图1-11 启动subscribe_node节点订阅消息
1.5.2 服务通信方式
本节的服务通信例程给大家增加一点难度,教大家使用自定义的消息类型。这里以实现两个整数求和为例来讨论服务通信(参见图1-7),client节点(节点1)向server节点(节点2)发送a、b的请求,server节点返回响应sum=a+b给client节点。开发步骤包括创建功能包、自定义服务消息类型、编写源码、配置编译和启动节点,除了自定义服务消息类型之外,其他步骤与1.5.1节一样。限于篇幅,下面就只介绍自定义服务消息类型和编写源码步骤,整个功能包的详细实现请参考本书GitHub仓库[2]中的相应源码。
本节将在功能包service_example中封装自定义服务消息类型。服务类型的定义文件都是以*.srv为扩展名,并且被放在功能包的srv/文件夹下。首先在功能包service_example目录下新建srv目录,然后在service_example/srv/目录中创建AddTwoInts.srv文件,并在该文件中填充如下内容。
int64 a int64 b --- int64 sum
定义好服务消息类型后,要想让该服务消息类型能在C++、Python等代码中使用,必须要进行相应的编译与运行配置。编译依赖message_generation,运行依赖message_runtime。打开功能包中的CMakeLists.txt文件,将依赖message_generation添加进find_package(...)配置字段中,如下所示。
find_package(catkin REQUIRED COMPONENTS roscpp std_msgs message_generation )
接着将该CMakeLists.txt文件中add_service_files(...)配置字段前面的注释去掉,并将自己编写的类型定义文件AddTwoInts.srv填入该字段,如下所示。
add_service_files( FILES AddTwoInts.srv )
接着将该CMakeLists.txt文件中generate_messages(...)配置字段前面的注释去掉。generate_messages的作用是自动创建自定义的消息类型*.msg、服务类型*.srv和动作类型*.action相对应的*.h,由于我们定义的服务消息类型使用了std_msgs中的int64基本类型,所以必须向generate_messages指明该依赖,如下所示。
generate_messages( DEPENDENCIES std_msgs )
最后打开功能包中的package.xml文件,将以下依赖添加进去即可。到这里自定义服务消息类型的步骤就算完成了。
<build_depend>message_generation</build_depend> <build_export_depend>message_generation</build_export_depend> <exec_depend>message_runtime</exec_depend>
做好了上面类型定义的编译配置后,可以用命令检测新建的消息类型是否可以被ROS系统自动识别。前面说过,消息类型通过功能包和子类型名共同标识,需用下面的命令进行检测。如果能正确输出类型的数据结构,就说明新建的消息类型成功了,即可以被ROS系统自动识别到。
rossrv show service_example/AddTwoInts
功能包中需要编写两个独立可执行的节点,一个节点是用来发起请求的client节点,另一个节点是用来响应请求的server节点,所以需要在新建的功能包service_example/src/目录下新建server_node.cpp和client_node.cpp两个文件。首先来看server节点server_node.cpp代码内容,如代码清单1-3所示。
代码清单1-3 server节点server_node.cpp
1 #include "ros/ros.h" 2 #include "service_example/AddTwoInts.h" 3 4 bool add_execute(service_example::AddTwoInts::Request &req, 5 service_example::AddTwoInts::Response &res) 6 { 7 res.sum = req.a + req.b; 8 ROS_INFO("recieve request: a=%ld,b=%ld",(long int)req.a,(long int)req.b); 9 ROS_INFO("send response: sum=%ld",(long int)res.sum); 10 return true; 11 } 12 13 int main(int argc,char **argv) 14 { 15 ros::init(argc,argv,"server_node"); 16 ros::NodeHandle nh; 17 18 ros::ServiceServer service = nh.advertiseService("add_two_ints",add_execute); 19 ROS_INFO("service is ready!!!"); 20 ros::spin(); 21 22 return 0; 23 }
第1行,包含ROS的C++客户端roscpp的头文件。
第2行,service_example/AddTwoInts.h是由编译系统自动根据功能包和在功能包中创建的*.srv文件生成的对应头文件,包含这个头文件,在程序中就可以使用我们自定义的服务消息类型了。
第4~11行,这个函数实现了两个int64整数求和的服务,两个int64整数需从request获取,之后返回求和结果并装入response,request与response的具体数据类型都定义在前面创建的*.srv文件中,该函数返回值为bool型。
第15~16行,初始化ROS节点,指明节点的名称,并声明一个ROS节点的句柄。
第18行,创建服务,并将服务加入ROS网络中,且这个服务在ROS网络中以名称add_two_ints为唯一标识,以便于其他节点通过服务名称进行服务请求。
第20行,让程序进入自循环的挂起状态,从而让程序以最大效率接收客户端的请求并调用回调函数。
其实将server节点的代码稍做修改,就能得到client节点。接着来看看client节点client_node.cpp代码内容,如代码清单1-4所示。
代码清单1-4 client节点client_node.cpp
1 #include "ros/ros.h" 2 #include "service_example/AddTwoInts.h" 3 4 #include <iostream> 5 6 int main(int argc,char **argv) 7 { 8 ros::init(argc,argv,"client_node"); 9 ros::NodeHandle nh; 10 11 ros::ServiceClient client = 12 nh.serviceClient<service_example::AddTwoInts>("add_two_ints"); 13 service_example::AddTwoInts srv; 14 15 while(ros::ok()) 16 { 17 long int a_in,b_in; 18 std::cout<<"please input a and b:"; 19 std::cin>>a_in>>b_in; 20 21 srv.request.a = a_in; 22 srv.request.b = b_in; 23 if(client.call(srv)) 24 { 25 ROS_INFO("sum=%ld",(long int)srv.response.sum); 26 } 27 else 28 { 29 ROS_INFO("failed to call service add_two_ints"); 30 } 31 } 32 return 0; 33 }
在server节点已经解释过的类似代码,就不再赘述了。这里重点解释一下前面没遇到过的代码。
第11~12行,创建client对象,用来向ROS网络中名称为add_two_ints的服务发起请求。
第13行,定义了一个service_example::AddTwoInts服务消息类型的对象,该对象中的成员正是我们在*.srv文件中定义的a、b、sum,我们将待请求的数据填充到数据成员a、b,请求成功后返回结果会被自动填充到数据成员sum中。
第23行,通过client的call方法来向服务发起请求,请求传入的参数srv在上面已经介绍过,这里不再赘述。
在配置编译步骤中,除了在add_executable(...)配置字段创建可执行文件以及在target_link_libraries(...)配置字段连接可执行文件运行时需要的依赖库之外,还需要用到add_dependencies(...)配置字段,该字段用于声明可执行文件的依赖项。由于我们自定义了*.srv,service_example_gencpp,以让编译系统自动根据功能包名和功能包中创建的*.srv文件生成对应头文件与库文件,因此需要在add_dependencies(...)配置字段内填入service_example_gencpp。service_example_gencpp这个名称是由功能包名称service_example加上_gencpp后缀而来的,后缀很好理解:生成C++文件就是_gencpp,生成Python文件就是_genpy。当功能包编译完成后,先用roscore命令来启动ROS节点管理器,ROS节点管理器是所有节点运行的基础。打开命令行终端,输入如下命令。
roscore
然后,就可以用rosrun命令来启动功能包service_example中的节点server_node,为别的节点提供两个整数求和的服务。打开另外一个命令行终端,输入如下命令。
rosrun service_example server_node
启动完server_node节点后,可以在终端中看到服务已就绪的打印信息输出,如图1-12所示。这就说明服务节点已经正常启动,为两个整数求和的服务已经就绪,只要客户端发起请求就能即刻给出响应。
图1-12 启动server_node节点提供服务
最后,用rosrun命令来启动功能包service_example中的节点client_node,向server_node发起请求。打开另外一个命令行终端,输入如下命令。
rosrun service_example client_node
启动完client_node节点后,用键盘键入两个整数,以空格分隔,输入后按回车键。如果看到输出信息sum=xxx,就说明client节点向server节点发起的请求得到了响应,打印出来的sum就是响应结果,这样就完成了一次服务请求的通信过程,如图1-13所示。
图1-13 启动client_node节点发起请求
1.5.3 动作通信方式
与服务通信方式类似,动作通信方式只是在响应中多了一个反馈机制。与服务通信例程一样,这里的动作通信例程也是使用自定义消息类型。这里以实现倒计数器为例,动作客户端节点向动作服务端节点发送倒计数的请求,动作服务端节点执行递减计数任务,并给出反馈和结果,具体过程如图1-8所示。开发步骤包括创建功能包、自定义动作消息类型、编写源码、配置编译和启动节点,除了自定义动作消息类型之外其他步骤与1.5.2节一样。限于篇幅,下面只介绍自定义动作消息类型和编写源码步骤,整个功能包的详细内容请参考本书GitHub仓库[3]中的相应源码。
前面已经介绍过封装自己的服务消息类型了,这里按类似方法在功能包action_example中封装自定义的动作消息类型。动作类型的定义文件都是以*.action为扩展名,并且放在功能包的action/文件夹下。首先,在功能包action_example目录下新建action目录,然后在action_example/action/目录中创建CountDown.action文件,并在文件中填充如下内容。动作消息分为目标、结果和反馈三个部分,每个部分的定义内容用三个连续的短线分隔。每个部分内部可以定义一个或多个数据成员,具体根据需要定义。
#goal define int32 target_number int32 target_step --- #result define bool finish --- #feedback define float32 count_percent int32 count_current
定义好动作消息类型后,要想让该动作消息类型可在C++、Python等代码中使用,必须要做相应的编译与运行配置。编译依赖message_generation已经在新建功能包时显式指定了,运行依赖message_runtime需要手动添加一下。打开功能包中的CMakeLists.txt文件,将find_package(...)配置字段前面的注释去掉,即将依赖Boost放出来,因为代码中用到了Boost库,如下所示。
find_package(Boost REQUIRED COMPONENTS system)
接着将该CMakeLists.txt文件中add_action_files(...)配置字段前面的注释去掉,并将自己编写的类型定义文件CountDown.action填入,如下所示。
add_action_files( FILES CountDown.action )
接着将该CMakeLists.txt文件中generate_messages(...)配置字段前面的注释去掉,如下所示。由于actionlib_msgs是动作通信中的必要依赖项,而std_msgs在自定义动作类型中有使用,所以需要在generate_messages(...)配置字段中指明这两个依赖。
generate_messages( DEPENDENCIES actionlib_msgs std_msgs )
最后打开功能包中的package.xml文件,将以下依赖添加进去即可。到这里自定义动作消息类型的步骤就算完成了。
<exec_depend>message_runtime</exec_depend>
做好了上面类型定义的编译配置,一旦功能包编译后,ROS系统将会生成自定义动作类型的调用头文件,同时会产生很多供调用的配套子类型。这一点要特别说明一下,因为后面程序中会使用这些类型,往往初学者搞不懂这些没见过的子类型是来自哪里。本实例中CountDown.action经过编译会产生对应的*.msg和*.h文件,如图1-14所示。在程序中,只需要引用action_example/CountDownAction.h头文件,就能使用自定义动作类型以及配套的子类型。
图1-14 自定义动作消息类型创建过程
功能包中需要编写两个独立可执行的节点,一个节点是用来发起目标的动作客户端,另一个节点是用来执行目标任务的动作服务端,所以需要在新建的功能包action_example/src/目录下新建action_server_node.cpp和action_client_node.cpp两个文件。首先来看动作服务端节点action_server_node.cpp代码内容,如代码清单1-5所示。
代码清单1-5 动作服务端节点action_server_node.cpp
1 #include "ros/ros.h" 2 #include "actionlib/server/simple_action_server.h" 3 #include "action_example/CountDownAction.h" 4 5 #include <string> 6 #include <boost/bind.hpp> 7 8 class ActionServer 9 { 10 private: 11 ros::NodeHandle nh_; 12 actionlib::SimpleActionServer<action_example::CountDownAction> as_; 13 14 action_example::CountDownGoal goal_; 15 action_example::CountDownResult result_; 16 action_example::CountDownFeedback feedback_; 17 18 public: 19 ActionServer(std::string name): 20 as_(nh_,name,boost::bind(&ActionServer::executeCB,this,_1),false) 21 { 22 as_.start(); 23 ROS_INFO("action server started!"); 24 } 25 ~ActionServer(void){} 26 void executeCB(const action_example::CountDownGoalConstPtr &goal) 27 { 28 ros::Rate r(1); 29 goal_.target_number=goal->target_number; 30 goal_.target_step=goal->target_step; 31 ROS_INFO("get goal:[%d,%d]",goal_.target_number,goal_.target_step); 32 33 int count_num=goal_.target_number; 34 int count_step=goal_.target_step; 35 bool flag=true; 36 for(int i=count_num;i>0;i=i-count_step) 37 { 38 if(as_.isPreemptRequested() || !ros::ok()) 39 { 40 as_.setPreempted(); 41 flag=false; 42 ROS_INFO("Preempted"); 43 break; 44 } 45 feedback_.count_percent=1.0*i/count_num; 46 feedback_.count_current=i; 47 as_.publishFeedback(feedback_); 48 49 r.sleep(); 50 } 51 if(flag) 52 { 53 result_.finish=true; 54 as_.setSucceeded(result_); 55 ROS_INFO("Succeeded"); 56 } 57 } 58 }; 59 59 int main(int argc, char** argv) 60 { 61 ros::init(argc, argv, "action_server_node"); 62 63 ActionServer my_action_server("/count_down"); 64 ros::spin(); 65 return 0; 66 }
后续章节所涉及的复杂项目基本都需要采用面向对象的编程方式,即将复杂的功能模块封装到类中,方便调用和管理。
第1行,包含ROS的C++客户端roscpp的头文件。
第2行,创建动作服务端需要的头文件。
第3行,引用自定义动作消息类型头文件。
第5~6行,使用C++的string和boost库的头文件。
第8~58行,定义类ActionServer,并将动作服务端及任务处理逻辑封装在这个类里面。第11行,声明一个ROS节点的句柄。第12行,创建一个动作服务端对象as_,后面的操作都通过调用这个对象的方法来实现。第14~16行,新建三个变量用于动作服务端与客户端交互时的数据缓存。第19~24行,类的构造函数,带一个字符串的传参,并且对动作服务端对象as_进行初始化,初始化采用boost方法与executeCB回调函数进行关联,当动作服务端收到目标后会自动跳到executeCB回调函数,具体任务逻辑就在该回调函数里实现。第26~57行,executeCB回调函数的具体实现,其实逻辑大致就是获取目标goal的值,goal有两个成员:一个是计数值,另一个是计数的步数;然后执行递减操作,并且输出反馈feedback,feedback也有两个成员:一个是执行百分比,另一个是计数当前值;最后是执行完成并输出结果result值。
第61行,初始化ROS节点并指明节点的名称,是main函数里面必须有的。
第63行,创建一个类ActionServer的实例对象。实例对象初始参数是/count_down,这个参数是动作服务访问的标识名,动作客户端可以利用/count_down标识名与动作服务端进行连接。
其实将动作服务端节点的代码稍做修改,就能得到动作客户端节点。接着来看看动作客户端节点action_client_node.cpp代码内容,如代码清单1-6所示。
代码清单1-6 动作客户端节点action_client_node.cpp
1 #include "ros/ros.h" 2 #include "actionlib/client/simple_action_client.h" 3 #include "actionlib/client/terminal_state.h" 4 #include "action_example/CountDownAction.h" 5 6 void doneCB(const actionlib::SimpleClientGoalState& state, 7 const action_example::CountDownResultConstPtr& result) 8 { 9 ROS_INFO("done"); 10 ros::shutdown(); 11 } 12 void activeCB() 13 { 14 ROS_INFO("active"); 15 } 16 void feedbackCB(const action_example::CountDownFeedbackConstPtr& feedback) 17 { 18 ROS_INFO("feedback:[%f,%d]", feedback->count_percent, feedback->count_current); 19 } 20 int main(int argc, char **argv) 21 { 22 ros::init(argc, argv, "action_client_node"); 23 24 actionlib::SimpleActionClient<action_example::CountDownAction> ac("/count_down",true); 25 26 ROS_INFO("wait for action server to start!"); 27 ac.waitForServer(); 28 29 action_example::CountDownGoal goal; 30 std::cout<<"please input target_number and target_step:"<<std::endl; 31 std::cin>>goal.target_number>>goal.target_step; 32 33 ac.sendGoal(goal,&doneCB,&activeCB,&feedbackCB); 34 35 ros::spin(); 36 return 0; 37 }
在动作服务端节点已经解释过的类似代码,就不再赘述了。这里重点解释一下前面没遇到过的代码。
第2~3行,创建动作客户端需要的头文件。
第6~19行,定义了三个函数,分别用来处理结果、开始、反馈的消息。
第24行,创建一个动作客户端对象ac,使用动作服务端的标识名/count_down作为初始化参数。
第27行,客户端ac与动作服务端建立连接同步的过程。
第29~31行,获取键盘值来填充goal的值。
第33行,客户端ac调用发送目标成员函数,向动作服务端发送目标,并且关联三个回调函数用来处理反馈和结果。
当功能包编译完成后,先用roscore命令来启动ROS节点管理器,ROS节点管理器是所有节点运行的基础。打开命令行终端,输入如下命令。
roscore
然后可以用rosrun命令来启动功能包action_example中的节点action_server_node,为其他节点提供动作的服务。打开另外一个命令行终端,输入如下命令。
rosrun action_example action_server_node
启动完action_server_node节点后,可以在终端中看到动作服务已就绪的打印信息输出,如图1-15所示。这就说明动作服务节点已经正常启动,只要客户端发起目标就能开始执行目标和反馈。
图1-15 启动action_server_node节点提供动作服务
最后,用rosrun命令来启动功能包action_example中的节点action_client_node,向action_server_node发送目标。打开另外一个命令行终端,输入如下命令。
rosrun action_example action_client_node
启动完action_client_node节点后,按照终端输出提示信息,用键盘键入两个整数,以空格分割,输入完毕后回车。如果看到输出反馈信息,就说明动作客户端节点向动作服务端发起的目标已经开始执行,目标执行完成后,客户端程序自动结束,这样就完成了一次动作目标请求的通信过程,如图1-16所示。
图1-16 启动action_client_node节点发送动作目标
[1]参见https://github.com/xiihoo/Books_Robot_SLAM_Navigation。