ROS 2 Iron 教程 第二章 Client libraries 第五节 编写一个简单的服务端和客户端(Python))
前言
本系列文章是由笔者翻译自ROS 2 官方教程。笔者水平有限,如有错误,还请读者指正。
背景
当节点使用服务进行通信时,发送数据请求的节点被称为客户端,而响应请求的节点被称为服务端。请求和响应的数据结构被.srv文件决定。
本节教程使用的示例是一个简单的整数相加系统;一个节点请求两个整数的和,而另一个节点以结果响应。
先决条件
在先前的教程中,我们已经学会如何创建一个工作空间,以及创建一个包。
任务
Create a package (创建一个包)
打开一个新终端,并添加 ROS 2 安装,使得ros2
命令能运作。
cd
进入先前教程创建的ros2_ws目录。
回忆一下,包应被创建在src
目录中,而不是工作空间根目录。cd进入ros2_ws/src
目录,随后创建一个新包:
ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces
你的终端将会返回信息以证明py_srvcli
包的创建,以及必要文件和文件夹创建成功。
--dependencies
参数将会自动在package.xml
和CMakeLists.txt
中添加必要的依赖行。
example_interfaces
包含了.srv
的包,你将会需要这个文件来编写你的请求和响应的数据结构:
int64 a
int64 b
---
int64 sum
短横线上方两行是请求的参数,短横线下方是响应的参数。
1.1 Update package.xml (更新 package.xml)
因为我们在创建包的时候使用了--dependencies
选项,所以我们不必在package.xml
中手动添加依赖。
一如既往,在package.xml
中description
,maintainer
,email
等标签中写入信息。
<description>Python client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
1.2 Update setup.py (更新 setup.py)
在setup.py
文件中的maintainer_email
,description
和license
字段中写入相同的信息:
maintainer='Your Name',
maintainer_email='you@email.com',
description='Python client server tutorial',
license='Apache License 2.0',
2 Write the service node (编写服务节点)
在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 Examine the code (分析代码)
代码开头的第一行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
最后,main 函数初始化 ROS 2 Python 库,将MinimalService
类实例化来创建服务节点,启动节点来处理调用。
2.2 Add an entry point (添加启动点)
为了让ros2 run
命令运行你的代码,你必须在setup.py
中添加启动点(位于ros2_ws/src/py_srvcli
目录)。
在console_scripts
括弧中添加如下代码:
'service = py_srvcli.service_member_function:main',
3 Write the client node (编写客户端节点)
在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 Examine the code (分析代码)
同服务节点类似的是,我们先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()
在构造函数的下方是send_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()
最后是main
函数,其中构建了MinimalClientAsync
对象,使用传入的命令行参数发送请求并记录结果。
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()
3.2 Add an entry point (添加启动点)
与服务端节点相似,客户端代买需要添加一个启动点来运行。
最终,setup.py
中的entry_points
字段应该像如下这样:
entry_points={
'console_scripts': [
'service = py_srvcli.service_member_function:main',
'client = py_srvcli.client_member_function:main',
],
},
3 Build and run (构建并运行)
构建之前在你的工作空间的根目录(ros2_ws
)运行rosdep来检查是否有缺失的依赖是一个好习惯(Liunx):
rosdep install -i --from-path src --rosdistro iron -y
在你工作空间的根目录中(ros2_ws
)构建你的新包:
colcon build --packages-select py_srvcli
打开一个新终端,cd
进入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
来停止节点运行。
总结
我们创建了两个节点来通过服务请求和响应数据。而且我们已将其依赖项和可执行文件添加到包配置文件中,以便可以构建和运行它们,并查看正在工作的服务/客户端系统。