节点之间通过服务通信时,发送请求的一端称之为客户端(Client),应答的一端称之为服务器(Server),请求和应答的数据结构使用srv文件描述。

接下来我们就尝试实现一个简单的服务通信模型,客户端发送两个加数,服务器完成加数求和之后应答求和结果。

1.创建功能包

在dev_ws工作空间的src文件夹下,使用如下命令创建一个新的功能包。
ros2 pkg create --build-type ament_python py_srvcli --dependencies rclpy example_interfaces

 

终端中会看到创建 py_srvcli功能包的验证信息。

--dependencies 参数会将后边的依赖自动添加到package.xml中,example_interfaces 功能包中提供了接下来要使用的srv文件,其中有对服务请求和应答数据的描述:
int64 a
int64 b
---
int64 sum

 

在以上描述中,---上边是两个加数,下边的是求和结果。
记得要修改下功能包中的package.xml文件中的功能包信息:
<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>

 还需要更新setup.py文件,修改以下信息:

maintainer='Your Name',
maintainer_email='you@email.com',
description='Python client server tutorial',
license='Apache License 2.0',

2.创建服务节点

dev_ws/src/py_srvcli/py_srvcli 路径下,创建一个文件并命名为 service_member_function.py ,拷贝如下代码:
from example_interfaces.srv import AddTwoInts

import rclpy
from rclpy.node import Node

class MinimalService(Node):
    def __init__(self):
        super().__init__('minimal_service')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

    def add_two_ints_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))

        return response

def main(args=None):
    rclpy.init(args=args)
    minimal_service = MinimalService()
    rclpy.spin(minimal_service)
    rclpy.shutdown()

if __name__ == '__main__':
    main()

 

我们来分析下以上代码:
from example_interfaces.srv import AddTwoInts

import rclpy
from rclpy.node import Node
首先从 example_interfaces 功能包导入 AddTwoInts 服务类型消息,然后导入python库。

 

def __init__(self):
    super().__init__('minimal_service')
    self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
MinimalService 类的构造函数中初始化节点 minimal_service ,然后创建服务并声明数据类型、名字和回调函数。

def add_two_ints_callback(self, request, response):
    response.sum = request.a + request.b
    self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))

    return response

回调函数add_two_ints_callback的传入参数是服务的请求和应答数据,在其中实现请求数据的求和,然后把求和结果放到应答数据中,同时在终端中打印信息。

def main(args=None):
    rclpy.init(args=args)

    minimal_service = MinimalService()

    rclpy.spin(minimal_service)

    rclpy.shutdown()

 

在main函数中主要实现以下配置:
  1. 初始化ROS2的python库
  2. 实例化 MinimalService 类
  3. 进入自旋锁,等待客户端请求

 

代码编写完成后,需要在 setup.py中设置程序入口,在 'console_scripts': 中添加:
'service = py_srvcli.service_member_function:main',

 

3.创建客户端节点

dev_ws/src/py_srvcli/py_srvcli 路径下,创建名为 client_member_function.py 的文件,拷贝如下代码:
import sys

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node

class MinimalClientAsync(Node):

    def __init__(self):
        super().__init__('minimal_client_async')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.req = AddTwoInts.Request()

    def send_request(self):
        self.req.a = int(sys.argv[1])
        self.req.b = int(sys.argv[2])
        self.future = self.cli.call_async(self.req)

def main(args=None):
    rclpy.init(args=args)

    minimal_client = MinimalClientAsync()
    minimal_client.send_request()

    while rclpy.ok():
        rclpy.spin_once(minimal_client)
        if minimal_client.future.done():
            try:
                response = minimal_client.future.result()
            except Exception as e:
                minimal_client.get_logger().info(
                    'Service call failed %r' % (e,))
            else:
                minimal_client.get_logger().info(
                    'Result of add_two_ints: for %d + %d = %d' %
                    (minimal_client.req.a, minimal_client.req.b, response.sum))
            break

    minimal_client.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
 
 
我们来分析下代码:
 
    def __init__(self):
        super().__init__('minimal_client_async')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        self.req = AddTwoInts.Request()
构造函数中创建一个节点,然后创建一个客户端实例,注意服务的名字和数据类型需要和服务器对应。while循环中是等待服务器端启动,如果在1秒之内没有等到,就报一个日志信息,进入下一个循环的等待。

 

    while rclpy.ok():
        rclpy.spin_once(minimal_client)
        if minimal_client.future.done():
            try:
                response = minimal_client.future.result()
            except Exception as e:
                minimal_client.get_logger().info(
                    'Service call failed %r' % (e,))
            else:
                minimal_client.get_logger().info(
                    'Result of add_two_ints: for %d + %d = %d' %
                    (minimal_client.req.a, minimal_client.req.b, response.sum))
            break
等待服务器端的反馈,如果用户按下Ctrl+C,也会退出,并且打印一个错误信息。
代码完成后同样需要配置程序入口,setup.py的程序入口描述如下:
entry_points={
    'console_scripts': [
        'service = py_srvcli.service_member_function:main',
        'client = py_srvcli.client_member_function:main',
    ],
},

 

4.编译运行

接下来先确认下所有的依赖是否都安装了。
rosdep install -i --from-path src --rosdistro foxy -y
然后在工作空间的根目录下执行如下编译命令
colcon build --packages-select py_srvcli
编译成功后就可以运行啦,先来运行服务器端
. install/setup.bash
ros2 run py_srvcli service
再打开一个新终端,cd到dev_ws下,使用类似的指令运行客户端:
. install/setup.bash
ros2 run py_srvcli client 2 3
终端中可以看到:
服务器端的终端也会有日志
 以上我们就用Python创建了两个节点实现了服务通信的功能。