在前面学习信息传递时我们用到了话题,其好处是可以适合多个节点相互传输数据、定时周期传输数据等功能,其传递是单向的、异步的
对于一个复杂的机器人系统,我们需要一个双向的、同步的数据传输方法,即服务。
注:一个服务中服务端只能有一个,客户端可以有多个
服务基本代码:从加法请求开始
服务端
在节点中创建一个服务对象:self.serv=self.create_service(AddTwoInts,#{服务名}#{回调函数})
class serviceNode(Node):
def __init__(self,name):
super().__init__(name)
self.serv=self.create_service(AddTwoInts,'service_a',self.serviceCallBack)
def serviceCallBack(self,request,response):
response.sum=request.a+request.b
self.get_logger().info('%d+%d=%d' %(request.a,request.b,response.sum))
return response
def main(args=None):
rclpy.init(args=args)
node=serviceNode('ServiceNode')
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
可以看到,与话题的实现类似,服务的实现通过调用从Node继承而来的方法create_service创建了该节点中作为服务端的实例self.serv。
在方法create_service中,三个参数分别为服务类型、服务名称、回调函数
其中服务名称与话题名称相类似,服务端与客户端需要进行统一。回调函数也与话题中类似,都是代表接到信息后执行的函数
此处AddTwoInts比较特殊,作为接口,它与服务具体执行有关,以后会再做讲解
在该服务的回调函数中,我们的参数在self以外又加了request、response,顾名思义,就是该服务的请求与响应,类似于web开发中的请求体。在这里,serv实例已经为我们编写好了回调函数的参数设置,不需要我们自己进行编写
作为请求响应的实例,我们需要调用request中的变量对response中的对象进行赋值,即语句response.sum=request.a+request.b。返回值中直接将response实例返回即可,ros2已经为我们准备好了如何将其带给请求体
客户端
客户端需要完成的事项是将数据打包发送给服务端,并等待其返回数据。具体实现代码如下:
class clientNode(Node):
def __init__(self,name):
super().__init__(name)
self.clie=self.create_client(AddTwoInts,'service_a')
while (not self.clie.wait_for_service(timeout_sec=1)):
self.get_logger().info('error! failed to find service')
self.request=AddTwoInts.Request()
def sendRequest(self):
self.request.a=1
self.request.b=2
self.future=self.clie.call_async(self.request)
def main(args=None):
rclpy.init(args=args)
node=clientNode('ClientNode')
node.sendRequest()
while(rclpy.ok()):
rclpy.spin_once(node)
if node.future.done():
try:
response=node.future.result()
except Exception as err:
node.get_logger().info('error! can not call service!'+err)
else:
node.get_logger().info('get Result:%d' % response.sum)
node.destroy_node()
rclpy.shutdown()
这段代码中,我们创建了作为客户端的类clientNode,在这个节点类中的构造函数中,我们创建节点的客户端(参数同服务端)
while (not self.client.wait_for_service(timeout_sec=1)):
self.get_logger().info('error! failed to find service')
该语句调用node父类继承得到的client类中wait_for_service函数来判断是否连接到服务端,等待时间为1秒。当1秒后仍未连接到服务端时执行报错代码
最后在构造函数中声明了一个AddTwoInts接口的request实现类,作为请求体填充数据后向服务端进行请求
在sendRequest函数中,我们首先在构建函数中声明的请求体中进行赋值,声明了该请求体中的两个变量a和b,使用声明的clie请求类中方法call_async,以request类作为参数,将请求赋值给变量future,后续我们会调用future实例进行对结果的询问
在主函数中,我们在正常的执行语句创建对象、调用方法sendRequest发送请求后开始试图收到response体。
在这里,我们的编程思维是:反复寻找response,直到找到响应后查看其结果,若结果符合要求,则将其打印。
实际实现过程中,我们调用spin_once函数执行一次node节点(可以理解为刷新),后调用future请求类中的done方法查看是否完成,若未完成重复执行node节点,若已完成尝试调用future请求类中的result方法将得到的值赋值给response,如果在这步出现错误则返回错误,否则直接将被赋值的变量打印即可。
接口
我们在上述的代码中使用到了AddTwoInt接口,这个接口描述的是该服务的过程,现在我们需要对该接口进行配置。
建立AddTwoInts.srv文件,在其中输入以下代码
int64 a
int64 b
---
int64 sum
这段接口是写给服务的,在服务类接口中我们的请求和响应用"---"区分,以上为请求,以下为响应。
在这里我们的请求是a和b,代表两个数,对应客户端中的代码
self.request=AddTwoInts.Request()
self.request.a=1
self.request.b=2
而响应是sum,对应服务端中的代码
response.sum=request.a+request.b
需要注意的是,在完成以上操作后,计算机并不能直接识别我们所写出的接口文档,需要将其编译为代码,在这里ros2已经帮我们完成了这一过程,我们需要做的只是在CMakeLists.txt文档中修改成对应位置如下代码:
rosidl_generate_interfaces(${PROJECT_NAME}
"AddTwoInts.srv"
)
注:引号中写的实际上是该接口文件的相对路径,若在文件夹中需要描述其文件夹以及文件名,形如srv/AddTwoInts.srv
另外,在使用到该接口的文件中,需要用from#{目录}import#{接口名}语句引入该接口