实现服务与客户端(Service and Client)(Python)

目标: 使用 Python 创建并运行服务和客户端节点.

教程等级: 初级

预计时长: 20 分钟

背景

节点 之间使用 服务 通信时,发送数据请求的节点称为客户端节点,而响应请求的节点称为服务节点。 请求和响应(request and response)的结构由 .srv 文件决定。

在这里使用的示例是一个简单的整数加法系统;一个节点请求两个整数的和,另一个节点返回结果。

前提条件

从之前的教程中学习了如何 创建工作空间创建包

任务

1 创建包

打开一个新终端并 source ROS 2 安装,这样 ros2 命令就能用了。

进入之前创建的 ros2_ws 目录。

之前学过,包应该在 src 目录中创建,而不是工作空间的根目录。 进入 ros2_ws/src 并创建一个新包:

ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces

终端会返回一个消息,确认 py_srvcli 包及其所有必要的文件和文件夹已创建。 --dependencies 参数会自动将必要的依赖行添加到 package.xmlexample_interfaces 中包含 .srv 文件 ,在这个文件中定义请求和响应的结构:

int64 a
int64 b
---
int64 sum

前两行定义请求,横线下面定义响应。

1.1 更新 package.xml

因为在包创建过程中使用了 --dependencies 选项,所以不需要手动将依赖项添加到 package.xml

和之前一样,记得向 package.xml 添加描述、维护者邮箱和姓名,以及许可信息。

<description>Python client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>

1.2 更新 setup.py

setup.py 文件中的 maintainermaintainer_emaildescriptionlicense 中添加同样的信息:

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

2 编写服务节点

ros2_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():
    rclpy.init()

    minimal_service = MinimalService()

    rclpy.spin(minimal_service)

    rclpy.shutdown()


if __name__ == '__main__':
    main()

2.1 检查代码

第一行的 import 语句从 example_interfaces 包中导入 AddTwoInts 服务类型。 后续两行导入 ROS 2 Python 客户端库,特别是 Node class 。

from example_interfaces.srv import AddTwoInts

import rclpy
from rclpy.node import Node

MinimalService 构造函数使用 minimal_service 作为节点名称。 然后,它创建一个服务并定义类型、名称和回调。

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

最后,主类初始化 ROS 2 Python 客户端库,实例化 MinimalService 类以创建服务节点,并运行节点以处理回调。

2.2 添加 entry point

你必须在 ros2_ws/src/py_srvcli 目录中的 setup.py 文件中添加 entry point ,才能用 ros2 run 命令运行你的节点。

将下面这行内容添加到 'console_scripts': 中:

'service = py_srvcli.service_member_function:main',

3 编写客户端节点

ros2_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, a, b):
        self.req.a = a
        self.req.b = b
        return self.cli.call_async(self.req)


def main():
    rclpy.init()

    minimal_client = MinimalClientAsync()
    future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
    rclpy.spin_until_future_complete(minimal_client, future)
    response = future.result()
    minimal_client.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (int(sys.argv[1]), int(sys.argv[2]), response.sum))

    minimal_client.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

3.1 检查代码

和服务端代码一样,首先 import 必要的库。

import sys

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

MinimalClientAsync 构造函数使用 minimal_client_async 作为节点名称。 客户端使用和服务节点匹配的类型和名称,两端必须匹配才能通信。 构造函数中的 while 循环每秒检查一次是否有匹配客户端的服务。 最后创建一个新的 AddTwoInts 请求。

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()

Below the constructor is the send_request method, which will send the request and spin until it receives the response or fails.

def send_request(self, a, b):
    self.req.a = a
    self.req.b = b
    return self.cli.call_async(self.req)

Finally we have the main method, which constructs a MinimalClientAsync object, sends the request using the passed-in command-line arguments, calls rclpy.spin_until_future_complete to wait for the result, and logs the results.

def main():
    rclpy.init()

    minimal_client = MinimalClientAsync()
    future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
    rclpy.spin_until_future_complete(minimal_client, future)
    response = future.result()
    minimal_client.get_logger().info(
        'Result of add_two_ints: for %d + %d = %d' %
        (int(sys.argv[1]), int(sys.argv[2]), response.sum))

    minimal_client.destroy_node()
    rclpy.shutdown()

Warning

Do not use rclpy.spin_until_future_complete in a ROS 2 callback. For more details see the sync deadlock article.

3.2 添加 entry point

和服务节点一样,你必须在 setup.py 文件中添加 entry point 才能运行客户端节点。

setup.py 文件的 entry_points 应该如下所示:

entry_points={
    'console_scripts': [
        'service = py_srvcli.service_member_function:main',
        'client = py_srvcli.client_member_function:main',
    ],
},

4 构建和运行

在构建之前,最好在工作空间的根目录( ros2_ws )中运行 rosdep 检查是否有缺少的依赖项:

rosdep install -i --from-path src --rosdistro humble -y

返回到工作空间的根目录, ros2_ws ,构建新包:

colcon build --packages-select py_srvcli

构建完成后,打开一个新终端,进入 ros2_ws 并 source setup 文件:

source install/setup.bash

现在运行服务节点:

ros2 run py_srvcli service

服务节点将等待客户端的请求。

打开另一个终端并再次 source ros2_ws 中的 setup 文件。 运行客户端节点,后面跟着两个整数,用空格分隔:

ros2 run py_srvcli client 2 3

如果你发了 23,客户端将收到这样的响应:

[INFO] [minimal_client_async]: Result of add_two_ints: for 2 + 3 = 5

返回到服务节点运行的终端。 你会看到它在收到请求时发布了日志消息:

[INFO] [minimal_service]: Incoming request
a: 2 b: 3

输入 Ctrl+C 服务节点。

总结

你创建了两个节点,用于通过服务请求和响应数据。 将它们的依赖项和可执行文件添加到包配置文件中,以便构建和运行并观察到服务/客户端系统的的工作情况。

下一步

在最近的几个教程中,你一直在使用接口(interfaces)在 topic 和服务间传递数据。 接下来,你将学习如何 创建自定义接口

相关内容

  • 有几种方法可以在 Python 中编写服务和客户端;查看 ros2/examples 中的 minimal_clientminimal_service

  • 在这个教程中,你使用了客户端节点中的 call_async() API 来调用服务。 Python 中还有另一种服务调用 API,称为同步调用。 我们不建议使用同步调用,但如果你想了解更多,请阅读 同步 vs. 异步客户端 指南。