话题通信可以实现多个ROS节点之间数据的单向传输,使用这种异步通信机制,发布者无法准确知道订阅者是否收到消息,本节我们将一起学习ROS另外一种常用的通信方法——服务,可以实现类似你问我答的同步通信效果。
一、 通信模型
在话题章节中,我们通过一个节点驱动相机,发布图像话题,另外一个节点订阅图像话题,并实现对其中橙色物体的识别,此时我们可以按照图像识别的频率,周期得到物体在图片中的位置。
这个位置信息可以继续发给机器人的上层应用使用,比如可以跟随目标运动,或者运动到目标位置附近。然而,有些场景我们并不需要这么高的频率一直订阅物体的位置,而是更希望在需要这个数据的时候,发一个查询的请求,然后尽快得到此时目标的最新位置。
这样的通信模型和话题单向传输有所不同,变成了发送一个请求,反馈一个应答的形式,好像是你问我答一样,这种通信机制在ROS中成为服务,Service。
1.1 客户端/服务器模型
从服务的实现机制上来看,这种你问我答的形式叫做客户端/服务器模型,简称为CS模型,客户端在需要某些数据的时候,针对某个具体的服务,发送请求信息,服务器端收到请求之后,就会进行处理并反馈应答信息。
这种通信机制在生活中也很常见,比如我们经常浏览的各种网页,此时你的电脑浏览器就是客户端,通过域名或者各种操作,向网站服务器发送请求,服务器收到之后返回需要展现的页面数据。
1.2 同步通信
这个过程一般要求越快越好,假设服务器半天没有反应,你的浏览器一直转圈圈,那有可能是服务器宕机了,或者是网络不好,所以相比话题通信,在服务通信中,客户端可以通过接收到的应答信息,判断服务器端的状态,我们也称之为同步通信。
1.3 一对多通信
比如博客园这个网站,服务器是唯一存在的,并没有多个完全一样的博客园网站,但是可以访问博客园网站的客户端是不唯一的,大家每一个人都可以看到同样的界面。所以服务通信模型中,服务器端唯一,但客户端可以不唯一。
1.4 服务接口
和话题通信类似,服务通信的核心还是要传递数据,数据变成了两个部分:
- 一个请求的数据,比如请求橘子位置的命令;
- 还有一个反馈的数据,比如反馈橘子坐标位置的数据;
这些数据和话题消息一样,在ROS中也是要标准定义的,话题使用.msg文件定义,服务使用的是.srv文件定义,后续我们会介绍定义的方法。
二、服务案例
接下来,我们就来看看, 服务到底该如何实现。创建my_learning_service的Python版本的功能包;- pi@NanoPC-T6:~/dev_ws$ cd src
- pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_python my_learning_service
复制代码 2.1 加法求解器
前面我们对ROS服务通信应该有了基本了解,接下来我们就要开始编写代码啦。还是从一个相对简单的例程开始,也是ROS官方的一个例程,通过服务实现一个加法求解器的功能。
当我们需要计算两个加数的求和结果时,我们应该怎么做呢?我们需要两个实现节点:
- 客户端节点:将两个加数封装成请求数据,针对服务add_two_ints发送出去;
- 服务器端节点:收到客户端发送的请求数据后,开始进行加法计算,并将求和结果封装成应答数据反馈给客户端。
2.1.1 客户端
使用VS Code加载功能包my_learning_service,在my_learning_service文件夹下创建service_adder_client.py;- """
- ROS2服务示例-发送两个加数,请求加法器计算
- @author: zy
- @since : 2025/12/12
- """
- import sys
- import rclpy # ROS2 Python接口库
- from rclpy.node import Node # ROS2 节点类
- from my_learning_interface.srv import AddTwoInts # 自定义的服务接口
- class adderClient(Node):
- def __init__(self, name):
- super().__init__(name) # ROS2节点父类初始化
- self.client = self.create_client(AddTwoInts, 'add_two_ints') # 创建服务客户端对象(服务接口类型,服务名)
- while not self.client.wait_for_service(timeout_sec=1.0): # 循环等待服务器端成功启动
- self.get_logger().info('service not available, waiting again...')
- self.request = AddTwoInts.Request() # 创建服务请求的数据对象
- def send_request(self): # 创建一个发送服务请求的函数
- self.request.a = int(sys.argv[1])
- self.request.b = int(sys.argv[2])
- self.future = self.client.call_async(self.request) # 异步方式发送服务请求
- def main(args=None):
- rclpy.init(args=args) # ROS2 Python接口初始化
- node = adderClient("service_adder_client") # 创建ROS2节点对象并进行初始化
- node.send_request() # 发送服务请求
- while rclpy.ok(): # ROS2系统正常运行
- rclpy.spin_once(node) # 循环执行一次节点,看看服务是不是返回了
- if node.future.done(): # 数据是否处理完成,即服务数据返回了
- try:
- response = node.future.result() # 接收服务器端的反馈数据
- except Exception as e:
- node.get_logger().info(
- 'Service call failed %r' % (e,))
- else:
- node.get_logger().info( # 将收到的反馈信息打印输出
- 'Result of add_two_ints: for %d + %d = %d' %
- (node.request.a, node.request.b, response.sum))
- break
- node.destroy_node() # 销毁节点对象
- rclpy.shutdown() # 关闭ROS2 Python接口
复制代码 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'service_adder_client = my_learning_service.service_adder_client:main',
- ],
- },
复制代码 对以上程序进行分析,如果我们想要实现一个客户端,流程如下:
- 编程接口初始化;
- 创建节点并初始化;
- 创建客户端对象;
- 创建并发送请求数据;
- 等待服务器端应答数据;
- 销毁节点并关闭接口;
- 服务端代码解析。
2.1.2 服务端
在my_learning_service文件夹下创建service_adder_server.py;- """
- ROS2服务示例-提供加法器的服务器处理功能
- @author: zy
- @since : 2025/12/12
- """
- import rclpy # ROS2 Python接口库
- from rclpy.node import Node # ROS2 节点类
- from my_learning_interface.srv import AddTwoInts # 自定义的服务接口
- class adderServer(Node):
- def __init__(self, name):
- super().__init__(name) # ROS2节点父类初始化
- self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.adder_callback) # 创建服务器对象(接口类型、服务名、服务器回调函数)
- def adder_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): # ROS2节点主入口main函数
- rclpy.init(args=args) # ROS2 Python接口初始化
- node = adderServer("service_adder_server") # 创建ROS2节点对象并进行初始化
- rclpy.spin(node) # 循环等待ROS2退出
- node.destroy_node() # 销毁节点对象
- rclpy.shutdown() # 关闭ROS2 Python接口
复制代码 服务器端的实现,有点类似话题通信中的订阅者,并不知道请求数据什么时间出现,也用到了回调函数机制。
完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'service_adder_client = my_learning_service.service_adder_client:main',
- 'service_adder_server = my_learning_service.service_adder_server:main',
- ],
- },
复制代码 对以上程序进行分析,如果我们想要实现一个服务端,流程如下:
- 编程接口初始化;
- 创建节点并初始化;
- 创建服务器端对象;
- 通过回调函数处进行服务;
- 向客户端反馈应答结果;
- 销毁节点并关闭接口。
2.1.3 编译运行
编译程序:- pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_service
复制代码 启动第一个终端,第一个节点是服务端,等待请求数据并提供求和功能;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_adder_server
复制代码 启动第二个终端,第二个节点是客户端,发送传入的两个加数并等待求和结果;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_adder_client 2 3
复制代码 2.2 机器视觉识别
好啦,加法求解器已经实现了,回想下刚才我们提到的视觉识别流程,当我们需要知道目标物体位置的时候,通过服务通信的机制,岂不是更加合理。
接着我们采用服务通信的机制对物体位置识别进行改造,此时会有三个节点出现:
- 相机驱动节点,发布图像数据;
- 视觉识别节点,订阅图像数据,并且集成了一个服务器端对象,随时准备提供目标位置;
- 客户端节点,我们可以认为是一个机器人目标跟踪的节点,当需要根据目标运动时,就发送一次请求,然后拿到一个当前的目标位置。
2.2.1 客户端
在my_learning_service文件夹下创建service_object_client.py;- """
- ROS2服务示例-请求目标识别,等待目标位置应答
- @author: zy
- @since : 2025/12/12
- """
- import rclpy # ROS2 Python接口库
- from rclpy.node import Node # ROS2 节点类
- from my_learning_interface.srv import GetObjectPosition # 自定义的服务接口
- class objectClient(Node):
- def __init__(self, name):
- super().__init__(name) # ROS2节点父类初始化
- self.client = self.create_client(GetObjectPosition, 'get_target_position')
- while not self.client.wait_for_service(timeout_sec=1.0):
- self.get_logger().info('service not available, waiting again...')
- self.request = GetObjectPosition.Request()
- def send_request(self):
- self.request.get = True
- self.future = self.client.call_async(self.request)
- def main(args=None):
- rclpy.init(args=args) # ROS2 Python接口初始化
- node = objectClient("service_object_client") # 创建ROS2节点对象并进行初始化
- node.send_request()
- while rclpy.ok():
- rclpy.spin_once(node)
- if node.future.done():
- try:
- response = node.future.result()
- except Exception as e:
- node.get_logger().info(
- 'Service call failed %r' % (e,))
- else:
- node.get_logger().info(
- 'Result of object position:\n x: %d y: %d' %
- (response.x, response.y))
- break
- node.destroy_node() # 销毁节点对象
- rclpy.shutdown() # 关闭ROS2 Python接口
复制代码 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'service_adder_client = my_learning_service.service_adder_client:main',
- 'service_adder_server = my_learning_service.service_adder_server:main',
- 'service_object_client = my_learning_service.service_object_client:main',
- ],
- },
复制代码 2.2.2 服务端
在my_learning_service文件夹下创建service_object_server.py;- """
- ROS2服务示例-提供目标识别服务
- @author: zy
- @since : 2025/12/12
- """
- import rclpy # ROS2 Python接口库
- from rclpy.node import Node # ROS2 节点类
- from sensor_msgs.msg import Image # 图像消息类型
- import numpy as np # Python数值计算库
- from cv_bridge import CvBridge # ROS与OpenCV图像转换类
- import cv2 # Opencv图像处理库
- from my_learning_interface.srv import GetObjectPosition # 自定义的服务接口
- # 橘子的HSV颜色范围
- lower_orange = np.array([10, 100, 100]) # 橙色的HSV阈值下限
- upper_orange = np.array([25, 255, 255]) # 橙色的HSV阈值上限
- class ImageSubscriber(Node):
- def __init__(self, name):
- super().__init__(name) # ROS2节点父类初始化
- self.sub = self.create_subscription(
- Image, 'image_raw', self.listener_callback, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度)
- self.cv_bridge = CvBridge() # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换
- self.srv = self.create_service(GetObjectPosition, # 创建服务器对象(接口类型、服务名、服务器回调函数)
- 'get_target_position',
- self.object_position_callback)
- self.objectX = 0
- self.objectY = 0
- def object_detect(self, image):
- hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 图像从BGR颜色模型转换为HSV模型
- mmask_orange = cv2.inRange(hsv_img, lower_orange, upper_orange) # 图像二值化
- contours, hierarchy = cv2.findContours(
- mask_orange , cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) # 图像中轮廓检测
- for cnt in contours: # 去除一些轮廓面积太小的噪声
- if cnt.shape[0] < 150:
- continue
- (x, y, w, h) = cv2.boundingRect(cnt) # 得到橘子所在轮廓的左上角xy像素坐标及轮廓范围的宽和高
- cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)# 将橘子的轮廓勾勒出来
- cv2.circle(image, (int(x+w/2), int(y+h/2)), 5,
- (0, 255, 0), -1) # 将橘子的图像中心点画出来
- self.objectX = int(x+w/2)
- self.objectY = int(y+h/2)
- cv2.imshow("object", image) # 使用OpenCV显示处理后的图像效果
- cv2.waitKey(50)
- def listener_callback(self, data):
- self.get_logger().info('Receiving video frame') # 输出日志信息,提示已进入回调函数
- image = self.cv_bridge.imgmsg_to_cv2(data, 'bgr8') # 将ROS的图像消息转化成OpenCV图像
- self.object_detect(image) # 橘子检测
- def object_position_callback(self, request, response): # 创建回调函数,执行收到请求后对数据的处理
- if request.get == True:
- response.x = self.objectX # 目标物体的XY坐标
- response.y = self.objectY
- self.get_logger().info('Object position\nx: %d y: %d' %
- (response.x, response.y)) # 输出日志信息,提示已经反馈
- else:
- response.x = 0
- response.y = 0
- self.get_logger().info('Invalid command') # 输出日志信息,提示已经反馈
- return response
- def main(args=None): # ROS2节点主入口main函数
- rclpy.init(args=args) # ROS2 Python接口初始化
- node = ImageSubscriber("service_object_server") # 创建ROS2节点对象并进行初始化
- rclpy.spin(node) # 循环等待ROS2退出
- node.destroy_node() # 销毁节点对象
- rclpy.shutdown() # 关闭ROS2 Python接口
复制代码 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'service_adder_client = my_learning_service.service_adder_client:main',
- 'service_adder_server = my_learning_service.service_adder_server:main',
- 'service_object_client = my_learning_service.service_object_client:main',
- 'service_object_server = my_learning_service.service_object_server:main',
- ],
- },
复制代码 2.2.3 编译运行
编译程序:- pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_service
复制代码 启动第一个终端,第一个是视觉识别节点,订阅图像数据,并且集成了一个服务器端对象,随时准备提供目标位置;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_object_server
复制代码 启动第二个终端,第二个节点是客户端,当需要根据目标运动时,就发送一次请求,然后拿到一个当前的目标位置;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_object_client
复制代码 启动第三个终端,第三个节点是相机驱动节点,发布图像数据;- pi@NanoPC-T6:~/dev_ws$ ros2 run usb_cam usb_cam_node_exe
复制代码 2.3 服务命令行操作
查看服务列表:- pi@NanoPC-T6:~/dev_ws$ ros2 service list
复制代码 查看服务数据类型:- pi@NanoPC-T6:~/dev_ws$ ros2 service type <service_name>
复制代码 发送服务请求:- pi@NanoPC-T6:~/dev_ws$ ros2 service call <service_name> <service_type> <service_data>
复制代码 2.4 思考题
话题和服务是ROS中最为常用的两种数据通信方法;
- 前者适合传感器、控制指令等周期性、单向传输的数据
- 后者适合一问一答,同步性要求更高的数据,比如获取机器视觉识别到的目标位置。
在机器人开发过程中,类似的通信应用比比皆是,ROS针对绝大部分通用场景,都设计了标准的话题和服务数据类型,比如图像数据、雷达数据、里程计数据等等,不过机器人软硬件繁杂,很多时候这些标准定义也无法满足我们的需求,这个时候,我们就要自定义通信接口了。
参考文章
[1] 古月居ROS2入门教程学习笔记
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |