编写一个简单的服务和客户端(Python)

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

教程级别: 初学者

**时间:**20分钟

背景

当使用:doc:services <../Beginner-CLI-Tools/Understanding-ROS2-Services/Understanding-ROS2-Services>`的方式进行通信时,发送请求数据的节点称为客户端节点,而响应请求的节点则是服务节点。请求和响应的结构由`.srv``文件确定。

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

先决条件

在之前的教程中,你学会了如何 创建工作空间创建包

任务

1 创建一个包

在一个新的终端中 源化你的 ROS 2 安装,这样 ros2 命令才能正常工作。

进入在 上一个教程 中创建的 ros2_ws 目录。

请记住,包应该在“src”目录中创建,而不是工作区的根目录。进入“ros2_ws/src”并创建一个新的包:

ros2 pkg create --build-type ament_python py_srvcli --dependencies rclpy example_interfaces

您的终端将返回一条消息,确认已创建名为``py_srvcli``的包及其所有必要的文件和文件夹。

``--dependencies``参数将自动向``package.xml``添加必要的依赖项。``example_interfaces``是包含 the .srv file 的包,您将需要使用它来构建请求和响应的结构:

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``文件的``maintainer``、maintainer_email``description``和``license``字段中:

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 服务类型。接下来的 import 语句导入了 ROS 2 Python 客户端库,具体导入了 Node 类。

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 添加入口点

要允许``ros2 run``命令运行您的节点,您必须将入口点添加到``setup.py``(位于``ros2_ws/src/py_srvcli``目录中)。

在``'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
        self.future = self.cli.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()


def main():
    rclpy.init()

    minimal_client = MinimalClientAsync()
    response = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
    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``。客户端节点代码使用`sys.argv <https://docs.python.org/3/library/sys.html#sys.argv>`__ 来获取请求的命令行输入参数访问权限。

构造函数定义创建一个与服务节点相同类型和名称的客户端。客户端和服务必须具有匹配的类型和名称才能进行通信。

构造函数中的``while``循环每秒检查是否有与客户端类型和名称匹配的服务可用。

构造函数下方是请求定义,然后是``main``函数。

客户端的``main``函数中唯一重要的区别是``while``循环。循环检查``future``以查看服务是否有响应,只要系统正在运行。如果服务发送了响应,结果将写入日志消息中。

3.2 添加一个入口点

像服务节点一样,您还必须添加一个入口点才能运行客户端节点。

您的``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 install/setup.bash

现在运行服务节点:

ros2 run py_srvcli service

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

打开另一个终端,并再次从``ros2_ws``内部源化设置文件。启动客户端节点,然后输入由一个空格分隔的两个整数:

ros2 run py_srvcli client 2 3

例如,如果你选择了``2``和``3``,客户端将会收到如下响应:

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

返回到运行服务节点的终端。您将看到当它接收到请求时,它会发布日志消息:

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

在服务器终端中输入``Ctrl+C``以停止节点的运行。

总结

您创建了两个节点来请求和响应服务中的数据。您将它们的依赖项和可执行文件添加到软件包配置文件中,以便您可以构建和运行它们,从而让您看到一个工作中的服务/客户端系统。

下一步

在过去的几个教程中,您已经利用接口在话题和服务之间传递数据。接下来,您将学习如何:doc:创建自定义接口