前面我们已经学习到了spawn和Trigger两种在ROS中已经定义好的服务数据类型的使用方式,当已经定义好的服务数据不能满足我们需求时,就需要根据自己需求来定义一个服务数据的类型并使用
之前在自定义topic的message里的示例,当时是有一个发布者不断去发布一个person的信息,另外一个订阅者不断去显示接收该信息,我们会觉得不希望publisher不断去发信息,希望它是用service这样一个机制,每request一次才去发布显示一次同时接收到是不是发布成功了,呢么我们就用service这样的模型去改善这样一个示例
在这样的模型当中,我们用ROS Master来管理两个节点,一个Client,一个Server,都是要去实现的;Client这一端要去request发布一个显示某个人信息的请求,并且去把这个人的信息去通过我们的自定义的service的数据给发出去,呢么我们在server这一端就会收到这个request,同时里面会包含比如这个人的名字,年龄,性别等信息,就会通过日志去显示出来,同时通过response反馈显示结果。用到的Service叫做show_person(这是我们自定义的名字),用到的数据类型叫做learning_service::Person,这是我们接下来马上定义的服务数据类型,以上就是我们要实现的案例
第一步,自定义服务数据
1.定义srv文件
自定义服务数据的结构,对照之前message的数据定义方式,message里定义的是message.msg文件,服务类似定义的是一个.srv文件
服务和message的区别在于,服务是有反馈response的,所以会把它的数据分为两部分,前面讲过通过—做区分,—上是request数据,—下是response数据,在编译的时候会通过这样的定义来产生对应的头文件,这是在ros中固定的定义方式
定义解释:
—上面是我们通过Client这一端要去发给Server的这个人的名字年龄性别,与之前Topice的实现类似,—下面在response这一端会告诉Client,Server这一端是不是显示成功了
创建.srv文件
在工作空间里,主目录/catkin_ws/src/learning_service,创建名为srv文件夹(message时,创建的是msg文件夹)
在srv里打开终端使用touch命令,touch Person.srv,创建一个Person.srv文件;随后打开文件输入这些定义
随后ros在编译时就会根据这些定义来变成不同的c++和python的程序
(下面编译部分,与之前的message完全一样的配置,需要在package里先配置一下动态生成message的依赖的功能包,同样需要在CMakeLists里去添加几个编译的选项,最后在编译的时候就会生成对应语言的相关的头文件)
2.在package.xml中添加功能包依赖
这里我们会添加一个动态生成程序的一个功能包的依赖
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
将该两行拷贝到package.xml里
3.在CMakeLists.txt里添加编译选项
find_package( … message_generation)
我们需要通过 find_package找到这样一个功能包作为一个依赖
add_service_files(FILES Person.srv)
generate_messages(DEPENDENCIES std_msgs)
下面去添加我们的定义的.srv文件,让我们的编译器知道,你现在要根据哪一个srv文件来产生对应的头文件
第一句,它会自动去搜索srv文件夹下面的这个文件
第二句,根据我们的Person.srv文件定义,以及std_msgs这个依赖,来产生对应的头文件
catkin_package( … message_runtime)
4.编译生成语言相关文件
完成基本配置后,可以返回工作空间目录下编译,同样使用catkin_make编译
编译成功后,查看编译结果,即所生成的代码文件
devel/include/lenrning_service/三个文件Person.h 、PersonRequest.h 、PersonResponse.h
这是因为刚我们在srv里面,通过三个横线,下面部分会放到PersonReqest.h里,下面部分会放到PersonResponse.h里,同时会有一个总体的Person.h包含这样的两个头文件,所以用的时候只用这一个就可以
到此已经创建好了自定义的服务类型,person类型;同时编译成功后编译一系列头文件,接下来是如何使用这些头文件
第二步,创建服务器代码Server端和Client端的实现(C++)
将课件代码/learning_service/src里的person_client.cpp和person_server.cpp文件复制到工作空间里catkin_ws/src/learning_service/src里
Client端person_client.cpp代码
/**
* 该例程将请求/show_person服务,服务数据类型learning_service::Person
*/
#include <ros/ros.h>
#include "learning_service/Person.h" //头文件包含,这里包含的是刚动态生成的头文件
int main(int argc, char** argv)
{
// 初始化ROS节点
ros::init(argc, argv, "person_client");
// 创建节点句柄
ros::NodeHandle node;
// 发现/show_person服务后,创建一个服务客户端,连接名为/show_person的service
//waitForService,等待这个show_person这样一个服务的提供者运行起来
ros::service::waitForService("/show_person");
ros::ServiceClient person_client = node.serviceClient<learning_service::Person>("/show_person");
//在这里创建Client这一端,Client这一端会去发布的请求数据是learning_service::Person(自定义的),另外其数据的service的对象是要用的show_person通道
// 初始化learning_service::Person的请求数据
//针对person数据添加进来名字年龄性别,这里与之前的使用方法完全一样
learning_service::Person srv;
srv.request.name = "Tom";
srv.request.age = 20;
srv.request.sex = learning_service::Person::Request::male;
//因为之前定义的宏,所以male的定义可以用learning_service::Person因为是在request里面,所以与message的区别是新加了一个request命名空间
// 请求服务调用
//接下来通过person_client发布一个call去请求把该请求数据发出去,然后回卡在这里等待srv的反馈结果最终反馈收到之后会跳到最后一行显示反馈的结果
ROS_INFO("Call service to show person[name:%s, age:%d, sex:%d]",
srv.request.name.c_str(), srv.request.age, srv.request.sex);
person_client.call(srv);
// 显示服务调用结果
ROS_INFO("Show person result : %s", srv.response.result.c_str());
return 0;
};
要注意learning_service::Person的使用方式,要include头文件,然后置于尖括号里,之后填充具体数据,使用起来就是learning_service的具体空间加上Person名字learning_service::Person srv注意这里的Person名字与定义的srv的文件名Person srv是一样的,因为我们生成头文件的时候头文件的命名就会完全根据这个文件的名字来命名
//这就是我们在Client这一端使用person数据类型的方式
上述定义的srv文件
catkin_ws/src/learning_service/srv/Person srv
Server端person_server.cpp代码
/**
* 该例程将执行/show_person服务,服务数据类型learning_service::Person
*/
#include <ros/ros.h>
#include "learning_service/Person.h"
//一样先调用include头文件
// service回调函数,输入参数req,输出参数res
bool personCallback(learning_service::Person::Request &req,
learning_service::Person::Response &res)
{
// 显示请求数据,人的名字年龄性别
ROS_INFO("Person: name:%s age:%d sex:%d", req.name.c_str(), req.age, req.sex);
// 设置反馈数据,ok可以改,因为这是一个字符串反馈给Client客户端的,比如OK,success都可以
res.result = "OK";
return true;
}
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "person_server");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个名为/show_person的server,注册回调函数personCallback
//创建一个server的实例,server这边会一直在提供show_person这样一个server服务,然后一旦有服务请求进来之后调用personCallback进入到回调函数
ros::ServiceServer person_service = n.advertiseService("/show_person", personCallback);
// 循环等待回调函数
ROS_INFO("Ready to show person informtion.");
ros::spin();
//最终有一个spin,会不断的等待request数据进来,一旦有request进来之后就会进入到回调函数里来,所以我们的server端是一个循环,会一直在spin里执行,我们的Client端request一次之后会结束,我们可以不断的去运行这个Client这端的程序,server这端只用运行一次就可以
return 0;
}
//在这里我们需要注意定义好的Person,先包含头文件然后可以去使用,同时注意回调函数里的输入参数,一般都是这样的类型,前面learning_service::Person然后Request端和Response端,这两个命名空间是ROS强制定义的,只需要修改前面这段learning_service::Person就可以改成所定义的数据类型;还有后面数据类型的使用方式 req.name.c_str(), req.age, req.sex
//这就是我们自定义的数据在person的server端的使用的方式
第三步,配置服务器/客户端代码编译规则
将下列编译规则复制到CMakeLists.txt里
add_executable(person_server src/person_server.cpp)
target_link_libraries(person_server ${catkin_LIBRARIES})
add_dependencies(person_server ${PROJECT_NAME}_gencpp)add_executable(person_client src/person_client.cpp)
target_link_libraries(person_client ${catkin_LIBRARIES})
add_dependencies(person_client ${PROJECT_NAME}_gencpp)
第一个,把cpp编译成对应的可执行文件,一个server一个client
第二个,把server和client对应做一个链接
第三个,跟我们动态生成的头文件做一个依赖
第四步,编译并运行发布者和订阅者
cd ~/catkin_ws //回到工作空间根目录下
catkin_make //用该指令去做编译
source devel/setup.bash //设置环境变量,可通过下面方法减去该步骤,跳过
编译结果一样可以在catkin_ws/devel/lib/learning_service下可以看到编译生成的结果
roscore
rosrun learning_service person_server
(运行Server端,server端运行起来后会一直等待request)
rosrun learning_service person_client
(运行Client端)
client端发送了请求,server端做了显示,请求数据是人的名字,年龄,性别,然后回反馈ok,然后客户端会收到这个ok并做显示
每请求一次显示一次
注意:如果先运行client,因为有waitForService,所以会一直等待你的服务器运行起来
提示:注意每次运行程序之前,尤其运行一个新的程序之前要注意关掉roscore,因为roscore里有一个参数服务器里面会存很多参数,如果程序运行很多的话,有些参数名字一样的情况下会有冲突,而出现很多问题
创建客户端和服务端代码(python)
person_server.py代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 该例程将执行/show_person服务,服务数据类型learning_service::Person
import rospy
from learning_service.srv import Person, PersonResponse
def personCallback(req):
# 显示请求数据
rospy.loginfo("Person: name:%s age:%d sex:%d", req.name, req.age, req.sex)
# 反馈数据
return PersonResponse("OK")
def person_server():
# ROS节点初始化
rospy.init_node('person_server')
# 创建一个名为/show_person的server,注册回调函数personCallback
#创建一个server,给show_person这个service提供服务,他的数据类型是自己定义的Person 故我们需要from learning_service.srv import Person, PersonResponse,通过from learning_service.srv来import进来自己创建的数据类型Person, PersonResponse(针对server和client这两端,一个PersonResponse,一个PersonRequest)
s = rospy.Service('/show_person', Person, personCallback)
# 循环等待回调函数
print "Ready to show person informtion."
rospy.spin()
if __name__ == "__main__":
person_server()
person_client.py代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 该例程将请求/show_person服务,服务数据类型learning_service::Person
import sys
import rospy
from learning_service.srv import Person, PersonRequest
def person_client():
# ROS节点初始化
rospy.init_node('person_client')
# 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
#创建一个client端的对象,然后wait_for_service等待你的show_person运行起来,运行起来之后会创建client这样一个对象,他会给show_person发送请求,请求数据类型是Person
rospy.wait_for_service('/show_person')
try:
person_client = rospy.ServiceProxy('/show_person', Person)
# 请求服务调用,输入请求数据
#把这个数据类型通过person_client输入进来,请求的数据是人名,年龄,性别发送出去,然后return结果,然后print打印出来
response = person_client("Tom", 20, PersonRequest.male)
return response.result
except rospy.ServiceException, e:
print "Service call failed: %s"%e
if __name__ == "__main__":
#服务调用并显示调用结果
print "Show person result : %s" %(person_client())