ROS2核心概念之通信接口
在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文件夹中找到,涵盖众多标准定义,我们可以打开几个看看;
pi@NanoPC-T6:~$ ls /opt/ros/humble/share/geometry_msgs/msg/
Accel.idl PointStamped.idl PoseStamped.idl TwistStamped.idl
Accel.msg PointStamped.msg PoseStamped.msg TwistStamped.msg
AccelStamped.idl Polygon.idl PoseWithCovariance.idl TwistWithCovariance.idl
AccelStamped.msg PolygonInstance.idl PoseWithCovariance.msg TwistWithCovariance.msg
AccelWithCovariance.idl PolygonInstance.msg PoseWithCovarianceStamped.idl TwistWithCovarianceStamped.idl
AccelWithCovariance.msg PolygonInstanceStamped.idl PoseWithCovarianceStamped.msg TwistWithCovarianceStamped.msg
AccelWithCovarianceStamped.idl PolygonInstanceStamped.msg Quaternion.idl Vector3.idl
AccelWithCovarianceStamped.msg Polygon.msg Quaternion.msg Vector3.msg
Inertia.idl PolygonStamped.idl QuaternionStamped.idl Vector3Stamped.idl
Inertia.msg PolygonStamped.msg QuaternionStamped.msg Vector3Stamped.msg
InertiaStamped.idl Pose2D.idl Transform.idl VelocityStamped.idl
InertiaStamped.msg Pose2D.msg Transform.msg VelocityStamped.msg
Point32.idl PoseArray.idl TransformStamped.idl Wrench.idl
Point32.msg PoseArray.msg TransformStamped.msg Wrench.msg
Point.idl Pose.idl Twist.idl WrenchStamped.idl
Point.msg Pose.msg Twist.msg WrenchStamped.msg
pi@NanoPC-T6:~$ ls /opt/ros/humble/share/std_srvs/srv/
Empty.idl Empty_Response.msg SetBool.idl SetBool_Response.msg Trigger.idl Trigger_Response.msg
Empty_Request.msg Empty.srv SetBool_Request.msg SetBool.srv Trigger_Request.msg Trigger.srv
pi@NanoPC-T6:~$ ls /opt/ros/humble/share/std_msgs/msg/
Bool.idl Empty.idl Header.idl Int64.idl MultiArrayLayout.idl UInt32MultiArray.idl
Bool.msg Empty.msg Header.msg Int64.msg MultiArrayLayout.msg UInt32MultiArray.msg
Byte.idl Float32.idl Int16.idl Int64MultiArray.idl String.idl UInt64.idl
Byte.msg Float32.msg Int16.msg Int64MultiArray.msg String.msg UInt64.msg
ByteMultiArray.idl Float32MultiArray.idl Int16MultiArray.idl Int8.idl UInt16.idl UInt64MultiArray.idl
ByteMultiArray.msg Float32MultiArray.msg Int16MultiArray.msg Int8.msg UInt16.msg UInt64MultiArray.msg
Char.idl Float64.idl Int32.idl Int8MultiArray.idl UInt16MultiArray.idl UInt8.idl
Char.msg Float64.msg Int32.msg Int8MultiArray.msg UInt16MultiArray.msg UInt8.msg
ColorRGBA.idl Float64MultiArray.idl Int32MultiArray.idl MultiArrayDimension.idl UInt32.idl UInt8MultiArray.idl
ColorRGBA.msg Float64MultiArray.msg Int32MultiArray.msg MultiArrayDimension.msg UInt32.msg UInt8MultiArray.msg
二、接口案例
接下来,我们就来看看, 接口到底该如何实现。我们创建my_learning_interface的C++版本的功能包;
pi@NanoPC-T6:~/dev_ws$ cd src
pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_cmake my_learning_interface
移除当前目录下的文件:src、include。
修改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加载功能包my_learning_interface,在my_learning_interface文件夹下创建子文件夹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"
)
...
编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_interface
2.1.2 使用接口包
我们在《ROS2核心概念之服务》文章中创建了功能包my_learning_service,那在客户端和服务端代码中是如何使用接口的呢。
修改功能包my_learning_service依赖package.xml;
<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 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
......
2.2 话题接口
话题通信接口的定义也是类似的,继续从之前的机器视觉案例中来衍生,我们想把服务换成话题,周期发布目标识别的位置,不管有没有人需要。
接着我们采用话题通信的机制对物体位置识别进行改造,此时会有三个节点出现:
- 相机驱动节点,将驱动相机并发布图像话题,此时的话题数据使用的是
ROS中标准定义的Image图像消息; - 视觉识别发布者节点,订阅图像数据,并运行视觉识别功能,识别目标的位置,这个位置我们希望封装成话题消息,发布出去,谁需要使用谁就来订阅;
- 目标位置订阅者节点,订阅目标的位置,打印到终端中。
2.2.1 接口定义
在这个例程中,我们使用ObjectPosition.msg定义了话题通信的接口。那这个接口是怎么定义的呢?
我们在my_learning_interface文件夹下创建子文件夹msg,接着新建文件ObjectPosition.msg;
int32 x # 表示目标的X坐标
int32 y # 表示目标的Y坐标
话题消息的内容是一个位置,我们使用x、y坐标值进行描述。
完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:
...
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/ObjectPosition.msg"
)
...
编译程序:
pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_interface
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 my_learning_interface.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模型
mask_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图像
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 : 2025/12/12
"""
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from std_msgs.msg import String # 字符串消息类型
from my_learning_interface.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 usb_cam usb_cam_node_exe --ros-args -p video_device:="/dev/video21"
启动第二个终端,第二个是视觉识别节点,订阅图像数据,并运行视觉识别功能,识别目标的位置,并封装成话题(/object_position)消息发布出去;
pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic interface_object_pub
[INFO] [1765721881.329790004] [topic_webcam_sub]: Receiving video frame
[INFO] [1765721881.389374344] [topic_webcam_sub]: Receiving video frame
[INFO] [1765721881.447783021] [topic_webcam_sub]: Receiving video frame
[INFO] [1765721881.503004156] [topic_webcam_sub]: Receiving video frame
[INFO] [1765721881.558974970] [topic_webcam_sub]: Receiving video frame
[INFO] [1765721881.614349516] [topic_webcam_sub]: Receiving video frame
[INFO] [1765721881.672467705] [topic_webcam_sub]: Receiving video frame
启动第三个终端,第三个节点是目标位置订阅者节点,订阅目标的位置,打印到终端中;
pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_topic interface_object_sub
[INFO] [1765721902.910225255] [interface_position_sub]: Target Position: "(387, 293)"
[INFO] [1765721902.965309682] [interface_position_sub]: Target Position: "(387, 293)"
[INFO] [1765721903.025286418] [interface_position_sub]: Target Position: "(387, 293)"
[INFO] [1765721903.078277510] [interface_position_sub]: Target Position: "(387, 292)"
[INFO] [1765721903.134822213] [interface_position_sub]: Target Position: "(387, 293)"
2.3 接口命令行操作
接口相关命令:
pi@NanoPC-T6:~/dev_ws$ ros2 interface
usage: ros2 interface [-h] Call `ros2 interface <command> -h` for more detailed usage. ...
Show information about ROS interfaces
options:
-h, --help show this help message and exit
Commands:
list List all interface types available
package Output a list of available interface types within one package
packages Output a list of packages that provide interfaces
proto Output an interface prototype
show Output the interface definition
Call `ros2 interface <command> -h` for more detailed usage.
2.3.1 查看系统接口列表
pi@NanoPC-T6:~/dev_ws$ ros2 interface list
Messages:
action_msgs/msg/GoalInfo
action_msgs/msg/GoalStatus
action_msgs/msg/GoalStatusArray
actionlib_msgs/msg/GoalID
actionlib_msgs/msg/GoalStatus
......
my_learning_interface/msg/ObjectPosition
......
Services:
action_msgs/srv/CancelGoal
composition_interfaces/srv/ListNodes
composition_interfaces/srv/LoadNode
composition_interfaces/srv/UnloadNode
......
my_learning_interface/srv/AddTwoInts
my_learning_interface/srv/GetObjectPosition
......
Actions:
action_tutorials_interfaces/action/Fibonacci
example_interfaces/action/Fibonacci
learning_interface/action/MoveCircle
tf2_msgs/action/LookupTransform
turtlesim/action/RotateAbsolute
2.3.2 查看接口的详细定义
pi@NanoPC-T6:~/dev_ws$ ros2 interface show my_learning_interface/msg/ObjectPosition
int32 x # 表示目标的X坐标
int32 y # 表示目标的Y坐标
2.3.3 查看功能包中的接口定义
pi@NanoPC-T6:~/dev_ws$ ros2 interface package my_learning_interface
my_learning_interface/srv/GetObjectPosition
my_learning_interface/msg/ObjectPosition
my_learning_interface/srv/AddTwoInts
通过rqt_graph可以将节点和话题以及他们之间的连接可视化;
pi@NanoPC-T6:~/dev_ws$ rqt_graph
参考文章
[1] 古月居ROS2入门教程学习笔记

浙公网安备 33010602011771号