ROS的结构是一个分布式的框架,为用户提供很多节点或者进程之间的通信服务,ROS的通信机制是ROS最底层也是最重要的技术。ROS大致上有三种通信机制,话题通信机制、服务通信机制以及参数管理机制。本文就浅谈一下自己对其中服务通信的学习。
服务通信它是一种自己带有应答的通信机制,它的原理如下图1所示。
图1、基于服务器/客户端的服务通信机制
1、Talker注册:Talker启动,通过123端口向Master注册发布者的信息。
2、Listener注册:Listener启动,向 Master注册订阅者的信息。
3、ROS Master进行信息的匹配:通过Listener订信息找到匹配的服务提供者信息,并向Listener发布Talker的TCP地址信息。
4、Listener与Talker建立网络链接。
5、Talker向Listener发布服务应答数据。
在常用的乌龟历程中提供了很多设置功能,这些设置功能都是以服务的形式提供。可以使用如下命令查看系统中的服务列表。
rosservice list
本文以简单的加法运算为例子,学习了解ROS中的服务应用。其中Client发布两个需要相加的数,Server节点接收后完成相加并反馈结果。在学习前需要创建相应的工作空间以及相应的功能包,因为此步骤与话题通信一致,此处不再赘述。
1、自定义服务数据
在自己的功能包下创建一个定义服务数据类型的srv文件/<功能包名称>/srv/AddTwoInts.srv:
int64 a
Int64 b
---
int64 sum
其中上部分为服务请求数据,用来储存两个需要相加的数据。下部分为服务应答数据,用来储存相加的结果。
2、配置依赖和编译规则
此处与话题通信一致,需要在package.xml和CMakeList.txt中进行一些添加。
打开package.xml文件,添加以下依赖:
message_generation
message_runtime
打开CMakeList.txt文件,添加以下配置:
find_package( ...... message_generation) #找到相应的位置进行添加
add_service_files(FILES Person.srv)
generate_messages(DEPENDENCIES std_msgs)
catkin_package(...... message_runtime) #找到相应位置,取消其注释并添加
添加完成后就可以进行编译,若编译成功就可以在工作空间下的devel/include/<功能包名称> 文件中可以找到编译出来的头文件。
3、创建Server和Client代码
首先创建Server节点,提供加法运算的功能,并且返回求和结果。在功能包下的src/server.cpp里面编写相应代码:
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"
//service 回调函数,输入参数req 输出参数res
bool add(learning_communication::AddTwoInts::Request &req,
learning_communication::AddTwoInts::Response &res)
{
//将输入参数中的请求数据相加,结果放入应答变量中
res.sum = req.a + req.b;
ROS_INFO("request: x = %ld, y = %ld",(long int)req.a,(long int)req.b);
ROS_INFO("sending back response: [%ld]",(long int)res.sum);
return true;
}
int main(int argc ,char **argv)
{
//ROS节点初始化
ros::init(argc,argv,"add_two_ints_server");
//创建节点句柄
ros::NodeHandle n;
//创建一个名为add_two_ints的server,注册回调函数add()
ros::ServiceServer service = n.advertiseService("add_two_ints",add);
//循环等待回调函数
ROS_INFO("Ready to add ints.");
ros::spin();
return 0;
}
然后进行Client节点的创建,通过终端输入两个加数,等待应答结果。在功能包下的src/client.cpp中编写相应的代码:
#include
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"
int main(int argc, char **argv)
{
ros::init(argc,argv,"add_two_ints_client");
//从终端命令行获取两个加数
if(argc != 3)
{
ROS_INFO("usage: add_two_ints_client X Y");
return 1;
}
ros::NodeHandle n;
//创建一个client,请求add_two_int service
//service消息类型是learning_communication::AddTwoInts的类型
ros::ServiceClient client = n.serviceClient("add_two_ints");
learning_communication::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
//发布服务,等待加法运算的应答结果
if (client.call(srv))
{
ROS_INFO("Sum: %ld",(long int)srv.response.sum);
}
else
{
ROS_INFO("Failed to call service add_two_ints");
}
return 0;
}
4、编译功能包
此时需要在CMakelist.txt中添加相应的编译规则。
add_executable(server src/server.cpp) #生成相应的执行文件
target_link_libraries(server ${catkin_LIBRARIES}) #server与client相连接
add_dependencies(server ${PROJECT_NAME}_gencpp) #与动态生成的头文件相依赖
add_executable(client src/client.cpp)
target_link_libraries(client ${catkin_LIBRARIES})
add_dependencies(client ${PROJECT_NAME}_gencpp)
添加了以上内容后就可以编译功能包。
5、运行Server与Client
首先启动roscore,在终端中输入以下命令:
roscore
运行Server节点:
rosrun learning_communication server
此处的learning_communication为本文所使用的功能包名称。运行成功后就会出现如图2所示结果。
图2、 Server节点启动结果
运行Client节点,并输入两个加数:
rosrun learning_communication client 2 3
Client发布服务请求,Server完成服务之后并反馈给Client。在两者终端就可以看见如图3和如图4所示的结果。
图3、Server接收到服务后完成加法,并反馈给Client
图4、Client发布请求并接收到反馈
总结
通过以上学习及其练习可以大致的掌握ROS通信机制中的服务通信,但是作为初学者在练习过程中会遇到很多问题,大多在于相关编译过则及其依赖或者代码编写的错误,所以自己手敲代码非常重要,多加练习才能熟能生巧。这里分享一个自己所遇到的错误,自己在最后运过程中当输入运行Client节点的命令之后,Server端就会得到如下反馈:
Reason given for shutdown:[new node registered with same name]
因为它不是以Error形式出现,所以没有告诉错误的位置,首先我的第一反应就是查看相应的Client和Server代码并检查节点名称,发现两者并没有错。然后就考虑是否是编译规则及其相关依赖出现错误(因为整个过程就只在这些里面进行过修改),发现CMakeList.txt中的:
add_executable(client src/client.cpp)
我误将client.cpp写成了server.cpp,导致出现了错误。因为这句的意思是生产相对应的可执行文件,如果误写为server.cpp,就意味着服务端与客户端两者可执行文件相同,所以就出现了节点名称重复。
参考
古月·ROS入门21讲
创客智造