在ROS系统中,无论话题还是服务,或者我们后续将要学习的动作,都会用到一个重要的概念——通信接口。
一、通信接口
通信并不是一个人自言自语,而是两个甚至更多个人,你来我往的交流,交流的内容是什么呢?为了让大家都好理解,我们可以给传递的数据定义一个标准的结构,这就是通信接口。
1.1 概述
接口的概念在各个领域随处可见,无论是硬件结构还是软件开发,都有广泛的应用。
1.1.1 硬件接口
比如生活中最为常见的插头和插座,两者必须匹配才能使用,电脑和手机上的USB接口也是,什么Micro-USB、TypeC等等,都是关于接口的具体定义。
1.1.2 软件接口
软件开发中,接口的使用就更多了,比如我们在编写程序时,使用的函数和函数的输入输出也称之为接口,每一次调用函数的时候,就像是把主程序和调用函数通过这个接口连接到一起,系统才能正常工作。
更为形象的是图形化编程中使用的程序模块,每一个模块都有固定的结构和形状,只有两个模块相互匹配,才能在一起工作,这就很好的讲代码形象化了。
1.1.3 定义
所以什么是接口,它是一种相互关系,只有彼此匹配,才能建立连接。
回到ROS的通信系统,它的主要目的就是传输数据,那就得让大家高效的建立连接,并且准确包装和解析传输的数据内容,话题、服务等机制也就诞生了,他们传输的数据,都要符合通信接口的标准定义;
- 比如摄像头驱动发布的图像话题,由每个像素点的R、G、B三原色值组成;
- 控制机器人运动的速度指令,由线速度和角速度组成;
- 进行机器人配置的服务,有配置的参数和反馈的结果组成等等;
类似这些常用的定义,在ROS系统中都有提供,我们也可以自己开发。
这些接口看上去像是给我们加了一些约束,但却是ROS系统的精髓所在;
- 举个例子,我们使用相机驱动节点的时候,完全不用关注它是如何驱动相机的,只要一句话运行,我们就可以知道发布出来的图像数据是什么样的了,直接开始我们的应用开发;
- 类似的,键盘控制我们也可以安装一个ROS包,如何实现的呢?不用关心,反正它发布出来的肯定是线速度和角速度。
1.2 ROS通信接口
接口可以让程序之间的依赖降低,便于我们使用别人的代码,也方便别人使用我们的代码,这就是ROS的核心目标,减少重复造轮子。
ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。
1.2.1 语言无关
为了保证每一个节点可以使用不同语言编程,ROS将这些接口的设计做成了和语言无关的,比如这里看到的:
- int32表示32位的整型数;
- int64表示64位的整型数;
- bool表示布尔值;
还可以定义数组、结构体,这些定义在编译过程中,会自动生成对应到C++、Python等语言里的数据结构。
其中:
- 话题通信接口的定义使用的是.msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行,比如在这个定义里,会传输两个32位的整型数,x、y,我们可以用来传输二维坐标的数值;
- 服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的---区分,比如之前我们学习的加法求和功能,请求数据是两个64位整型数a和b,应答是求和的结果sum;
- 动作是另外一种通信机制,用来描述机器人的一个运动过程,使用.action文件定义,比如我们让小海龟转90度,一边转一边周期反馈当前的状态,此时接口的定义分成了三个部分,分别是:
- 动作的目标,比如是开始运动;
- 运动的结果,最终旋转的90度是否完成;
- 还有一个周期反馈,比如每隔1s反馈一下当前转到第10度、20度还是30度了,让我们知道运动的进度。
1.2.2 标准接口
那么ROS系统到底给我们定义了哪些接口呢?我们可以在ROS安装路径中的share文件夹中找到,涵盖众多标准定义,我们可以打开几个看看;
二、接口案例
接下来,我们就来看看, 接口到底该如何实现。我们创建<depend>my_learning_interface</depend>的C++版本的功能包;- pi@NanoPC-T6:~/dev_ws$ cd src
- pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_cmake <depend>my_learning_interface</depend>
复制代码 修改package.xml,添加接口依赖:- <buildtool_depend>ament_cmake</buildtool_depend>
- <build_depend>rosidl_default_generators</build_depend>
- <exec_depend>rosidl_default_runtime</exec_depend>
- <member_of_group>rosidl_interface_packages</member_of_group>
复制代码 2.1 服务接口
了解了通信接口的概念,接下来我们再从代码实现的角度,研究下如何定义以及使用一个接口。
在前面介绍服务的文章中,我们编写了这样一个例程,我们再来回顾下。
有三个节点:
- 第一个驱动相机发布图像话题;
- 第二个是机器视觉识别节点,封装了一个服务的服务端对象,提供目标识别位置的查询服务;
- 第三个节点在需要目标位置的时候,就可以发送请求,收到位置进行使用了。
2.1.1 接口定义
在这个例程中,我们使用GetObjectPosition.srv定义了服务通信的接口。那这个接口是怎么定义的呢?
我们使用VS Code加载功能包<depend>my_learning_interface</depend>,在<depend>my_learning_interface</depend>文件夹下创建子文件夹srv,接着新建文件GetObjectPosition.srv;- # 请求部分
- bool get # 获取目标位置的指令
- ---
- # 响应部分
- int32 x # 目标的X坐标
- int32 y # 目标的Y坐标
复制代码 定义中有两个部分,上边是获取目标位置的指令,get为true的话,就表示我们需要一次位置,服务端就会反馈这个x、y坐标了。
完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:- ...
- find_package(ament_cmake REQUIRED)
- find_package(rosidl_default_generators REQUIRED)
- # 添加要生成的消息/服务/动作
- rosidl_generate_interfaces(${PROJECT_NAME}
- "srv/GetObjectPosition.srv"
- )
- ...
复制代码 2.1.2 使用接口包
我们在《ROS2核心概念之服务》文章中创建了功能包my_learning_service,那在客户端和服务端代码中是如何使用接口的呢。
2.1.2.1 package.xml
修改功能包my_learning_service依赖package.xml;- <depend>my_learning_interface</depend>
复制代码 2.1.2.2 setup.py
Python版本修改功能包my_learning_service下的setup.py;- install_requires=['<depend>my_learning_interface</depend>']
复制代码 2.1.2.3 客户端
在my_learning_service目录下的service_adder_client.py代码中我们引入了GetObjectPosition接口;- ......
- from 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()
- ......
复制代码 2.1.2.4 服务端
同样在my_learning_service目录下的service_object_server.py代码中我们引入了GetObjectPosition接口;- ......from <depend>my_learning_interface</depend>.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 ......
复制代码 2.2 话题接口
话题通信接口的定义也是类似的,继续从之前的机器视觉案例中来衍生,我们想把服务换成话题,周期发布目标识别的位置,不管有没有人需要。
接着我们采用话题通信的机制对物体位置识别进行改造,此时会有三个节点出现:
- 相机驱动节点,将驱动相机并发布图像话题,此时的话题数据使用的是ROS中标准定义的Image图像消息;
- 视觉识别发布者节点,订阅图像数据,并运行视觉识别功能,识别目标的位置,这个位置我们希望封装成话题消息,发布出去,谁需要使用谁就来订阅;
- 目标位置订阅者节点,订阅目标的位置,打印到终端中。
2.2.1 接口定义
在这个例程中,我们使用ObjectPosition.msg定义了话题通信的接口。那这个接口是怎么定义的呢?
我们在<depend>my_learning_interface</depend>文件夹下创建子文件夹msg,接着新建文件ObjectPosition.msg;- int32 x # 表示目标的X坐标
- int32 y # 表示目标的Y坐标
复制代码 话题消息的内容是一个位置,我们使用x、y坐标值进行描述。
完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:- ...
- rosidl_generate_interfaces(${PROJECT_NAME}
- "msg/ObjectPosition.msg"
- )
- ...
复制代码 2.2.2 视觉识别发布者节点
打开my_learning_topic功能包,在my_learning_topic文件夹下创建interface_object_pub.py;- """ROS2接口示例-发布目标位置@author: zy@since : 2025/12/12"""import rclpy # ROS2 Python接口库from rclpy.node import Node # ROS2 节点类from sensor_msgs.msg import Image # 图像消息类型from cv_bridge import CvBridge # ROS与OpenCV图像转换类import cv2 # Opencv图像处理库import numpy as np # Python数值计算库from <depend>my_learning_interface</depend>.msg import ObjectPosition # 自定义的目标位置消息# 橘子的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.pub = self.create_publisher( ObjectPosition, "object_position", 10) # 创建发布者对象(消息类型、话题名、队列长度) self.cv_bridge = CvBridge() # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换 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( mmask_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图像 position = ObjectPosition() self.object_detect(image) # 橘子检测 position.x, position.y = int(self.objectX), int(self.objectY) self.pub.publish(position) # 发布目标位置def main(args=None): # ROS2节点主入口main函数 rclpy.init(args=args) # ROS2 Python接口初始化 node = ImageSubscriber("topic_webcam_sub") # 创建ROS2节点对象并进行初始化 rclpy.spin(node) # 循环等待ROS2退出 node.destroy_node() # 销毁节点对象 rclpy.shutdown() # 关闭ROS2 Python接口
复制代码 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'topic_helloworld_pub = my_learning_topic.topic_helloworld_pub:main',
- 'topic_helloworld_sub = my_learning_topic.topic_helloworld_sub:main',
- 'topic_webcam_pub = my_learning_topic.topic_webcam_pub:main',
- 'topic_webcam_sub = my_learning_topic.topic_webcam_sub:main',
- 'interface_object_pub = my_learning_topic.interface_object_pub:main'
- ],
- },
复制代码 2.2.3 目标位置订阅者节点
在my_learning_topic文件夹下创建interface_object_sub.py;- """ROS2接口示例-订阅目标位置@author: zy@since : 2015/12/12"""import rclpy # ROS2 Python接口库from rclpy.node import Node # ROS2 节点类from std_msgs.msg import String # 字符串消息类型from <depend>my_learning_interface</depend>.msg import ObjectPosition # 自定义的目标位置消息"""创建一个订阅者节点"""class SubscriberNode(Node): def __init__(self, name): super().__init__(name) # ROS2节点父类初始化 self.sub = self.create_subscription(\ ObjectPosition, "/object_position", self.listener_callback, 10) # 创建订阅者对象(消息类型、话题名、订阅者回调函数、队列长度 def listener_callback(self, msg): # 创建回调函数,执行收到话题消息后对数据的处理 self.get_logger().info('Target Position: "(%d, %d)"' % (msg.x, msg.y))# 输出日志信息,提示订阅收到的话题消息def main(args=None): # ROS2节点主入口main函数 rclpy.init(args=args) # ROS2 Python接口初始化 node = SubscriberNode("interface_position_sub") # 创建ROS2节点对象并进行初始化 rclpy.spin(node) # 循环等待ROS2退出 node.destroy_node() # 销毁节点对象 rclpy.shutdown() # 关闭ROS2 Python接口
复制代码 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'topic_helloworld_pub = my_learning_topic.topic_helloworld_pub:main',
- 'topic_helloworld_sub = my_learning_topic.topic_helloworld_sub:main',
- 'topic_webcam_pub = my_learning_topic.topic_webcam_pub:main',
- 'topic_webcam_sub = my_learning_topic.topic_webcam_sub:main',
- 'interface_object_pub = my_learning_topic.interface_object_pub:main',
- 'interface_object_sub = my_learning_topic.interface_object_sub:main'
- ],
- },
复制代码 2.2.4 编译运行
编译程序:- pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_topic
复制代码 启动第一个终端,第一个是视觉识别节点,订阅图像数据,并运行视觉识别功能,识别目标的位置,并封装成话题消息发布出去;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic interface_object_pub
复制代码 启动第二个终端,第二个节点是目标位置订阅者节点,订阅目标的位置,打印到终端中;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic interface_object_sub
复制代码 启动第三个终端,第三个节点是相机驱动节点,发布图像数据;- pi@NanoPC-T6:~/dev_ws$ ros2 run usb_cam usb_cam_node_exe
复制代码 2.3 接口命令行操作
查看系统接口列表:- pi@NanoPC-T6:~/dev_ws$ ros2 interface list
复制代码 查看某个接口的详细定义:- pi@NanoPC-T6:~/dev_ws$ ros2 interface show <interface_name>
复制代码 查看某个功能包中的接口定义:- pi@NanoPC-T6:~/dev_ws$ ros2 interface package <package_name>
复制代码 参考文章
[1] 古月居ROS2入门教程学习笔记
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |