1、为什么要使用ROS service
之前写过一篇关于ROS topic的内容。对于实时性、周期性的消息,使用topic来传输是最佳的选择。topic是一种点对点的单向通信方式,这里的“点”指的是node,也就是说node之间可以通过topic方式来传递信息。topic要经历下面几步的初始化过程:首先,publisher节点和subscriber节点都要到节点管理器进行注册,然后publisher会发布topic,subscriber在master的指挥下会订阅该topic,从而建立起sub-pub之间的通信。
在大部分情况下,ROS topic能够满足需求,但是在某些特殊情况下,比如当我传输给某个节点一个信号让对方处理的时候需要让它告诉我处理结果的时候,使用topic的机制就不是很适合了。虽然可能也能做哈,就是一个A通过topic发布处理信号,B通过subscriber订阅这个信号。处理完成后B再通过一个topic发布处理结果,A再通过一个subscriber订阅这个结果。但是这样子属实有点繁琐。
因此,这里的service机制就应运而生了。service服务通讯机制是一种双向同步数据传输模式。基于客户端/服务器模型,两部分通信数据类型:一个用于请求,一个用于应答,类似web服务器。当客户端发送请求信号时,服务端会处理信号同时返回处理结果,这样子就会把整个过程简化很多。
2、如何编写ROS service
ROS service的使用也需要定义一个msg,但是名称用的是srv。这块类似于ROS msg的用法。首先建立一个文件夹,这里我们就用service_test吧:
catkin_create_pkg service_test roscpp rospy
然后建立一个srv文件:
cd service_test
mkdir srv
cd srv
gedit service_test.srv
这时候会打开一个新建的service_test.srv文件,在其中输入:
float64 numb1
float64 numb2
----
float64 sum
这里与msg的写法有点不同,它其中的数据被分为了两部分,中间用“—”分割。这是在于msg是不带返回值的,但是srv是需要返回值的。分割线下面的数据就是srv中服务端到时候要返回给客户端的内容。
然后同样的需要修改cmakelist以及package.xml文件:
对于cmakelist文件需要修改下面三个内容:
首先需要在find_package中增加message_generagetion:
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
message_generation
)
其次在add_service_files中添加刚才写的srv文件:
add_service_files(
FILES
service_test.srv
# Service2.srv
)
最后打开message_generation:
generate_messages(
DEPENDENCIES
# std_msgs # Or other packages containing msgs
)
对于package.xml文件中则只需要添加下面两行代码即可:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
完成上述后catkin_make编译该文件夹,即可生成对应的.h文件:
3、如何使用ROS service
ROS service需要一个客户端一个服务端
3.1、cient
一个非常简单的客户端长这个样子:
#include"ros/ros.h"
#include "service_test/service_test.h"
using namespace std;
class service_client
{
private:
/* data */
ros::NodeHandle nh;
int a,b;
public:
service_client();
~service_client();
ros::ServiceClient client;
void request();
};
service_client::service_client()
{
a = 0;
b = 0;
client = nh.serviceClient<service_test::service_test::Request>("/service_test");
}
void service_client::request()
{
cout<<"request"<<endl;
service_test::service_test req;
req.request.numb1 = a;
req.request.numb2 = b;
if(client.call(req))
{
cout<<a<<"+"<<b<<"="<<req.response.sum<<endl;
}
else
{
cout<<"request falied"<<endl;
}
a++;
b+=2;
}
service_client::~service_client()
{
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "service_client");
ROS_INFO("service_client");
service_client service_client;
ros::Rate rate(0.5);
int count=0;
while(count < 30)
{
service_client.request();
rate.sleep();
count++;
}
return 0;
}
这里我用C++写了类函数,因为在ROS中很多地方C++类函数会比C好用,所以为了养成习惯就简单定义了下类函数。代码构造很简单,首先我们在头文件中引入之前写好的srv文件,注意编译后它已经变成了.h文件:
#include "service_test/service_test.h"
然后我们定义一个客户端,这里分为了两行,当然其实写成一行好像也是可以的:
ros::ServiceClient client;
以及:
client = nh.serviceClient<service_test::service_test::Request>("/service_test");
这里我们定义了一个ROSservice的客户端,使用的数据格式是service_test文件夹下的service_test数据格式,service的名称是"/service_test"
到这里声明就完成了,接下来如何使用?那就是调用这个client就行了,所以我们定义了一个函数request(),让它循环执行调用client。当然调用的时候是需要传参的。使用的参数就是我们之前定义好的service_test::service_test格式的参数,至于参数名称叫什么就无所谓了。
另外赋值的时候记得加上request,这是service标准所需要的。最后通过一个call函数调用这个client,成功情况下一般会返回true,所以也可以外接一个if语句判断一下service是否执行成功。
service_test::service_test req;
req.request.numb1 = a;
req.request.numb2 = b;
if(client.call(req))
{
cout<<a<<"+"<<b<<"="<<req.response.sum<<endl;
}
else
{
cout<<"request falied"<<endl;
}
3.2、server
然后我们再看server,先放程序:
#include"ros/ros.h"
#include "service_test/service_test.h"
using namespace std;
class service_server
{
private:
/* data */
public:
service_server(/* args */);
~
service_server();
ros::ServiceServer server;
bool requestCallback(service_test::service_test::Request &request,service_test::service_test::Response &response);
};
service_server::service_server(/* args */)
{
ros::NodeHandle nh;
server = nh.advertiseService("/service_test",&service_server::requestCallback,this);
}
bool service_server::requestCallback(service_test::service_test::Request &request,service_test::service_test::Response &response)
{
cout<<"a="<<request.numb1<<endl;
cout<<"b="<<request.numb2<<endl;
cout<<"a+b="<<request.numb1+request.numb2<<endl;
response.sum = request.numb1+request.numb2;
return true;
}
service_server::~service_server()
{
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "service_server");
ROS_INFO("service_server");
service_server server;
ros::Rate rate(0.5);
ros::spin();
return 0;
}
server的程序比client看起来还短一点,内容其实差不多。前面定义的是client这边改成了server而已。但是它的注册函数是不太一样的,需要带回调函数处理请求:
nh.advertiseService("/service_test",&service_server::requestCallback,this);
这里包括了请求的话题名称以及使用的回调函数名称。
然后我们会在回调函数中处理我们的请求,其实也就是相当于对传参的处理而已了。注意到service不是所有都需要返回值的。比如这里因为我们写了返回值sum且我在client端使用了:
cout<<a<<"+"<<b<<"="<<req.response.sum<<endl;
所以server端处理的时候注意传值回去,如果client端不需要传值的话可以不用写也是无所谓的。
4、测试结果
在cmakelist中加入:
add_executable(service_client src/service_client.cpp)
target_link_libraries(service_client
${catkin_LIBRARIES})
add_dependencies(service_client ${catkin_EXPORTED_TARGETS})
add_executable(service_server src/service_server.cpp)
target_link_libraries(service_server
${catkin_LIBRARIES})
add_dependencies(service_server ${catkin_EXPORTED_TARGETS})
最后编译生成两个可执行文件,运行结果如下:
客户端:
服务端:
5、留下一个问题
另外注意到这里有个小问题:
当时在写客户端的时候,由于按照之前的习惯有时候我会将
ros::NodeHandle nh;
写到:
service_client::service_client()
{
a = 0;
b = 0;
client = nh.serviceClient<service_test::service_test::Request>("/service_test");
}
类函数初始化中,编译完虽然没有报错但是无法正常请求service,这里有点疑惑,因为之前好像也这么写过没有问题。如果有知道原因的小伙伴可以留言告知,感谢!