程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

ROS2核心概念之通信接口

ROS系统中,无论话题还是服务,或者我们后续将要学习的动作,都会用到一个重要的概念——通信接口。

一、通信接口

通信并不是一个人自言自语,而是两个甚至更多个人,你来我往的交流,交流的内容是什么呢?为了让大家都好理解,我们可以给传递的数据定义一个标准的结构,这就是通信接口。

1.1 概述

接口的概念在各个领域随处可见,无论是硬件结构还是软件开发,都有广泛的应用。

1.1.1 硬件接口

比如生活中最为常见的插头和插座,两者必须匹配才能使用,电脑和手机上的USB接口也是,什么Micro-USBTypeC等等,都是关于接口的具体定义。

1.1.2 软件接口

软件开发中,接口的使用就更多了,比如我们在编写程序时,使用的函数和函数的输入输出也称之为接口,每一次调用函数的时候,就像是把主程序和调用函数通过这个接口连接到一起,系统才能正常工作。

更为形象的是图形化编程中使用的程序模块,每一个模块都有固定的结构和形状,只有两个模块相互匹配,才能在一起工作,这就很好的讲代码形象化了。

1.1.3 定义

所以什么是接口,它是一种相互关系,只有彼此匹配,才能建立连接。

回到ROS的通信系统,它的主要目的就是传输数据,那就得让大家高效的建立连接,并且准确包装和解析传输的数据内容,话题、服务等机制也就诞生了,他们传输的数据,都要符合通信接口的标准定义;

  • 比如摄像头驱动发布的图像话题,由每个像素点的RGB三原色值组成;
  • 控制机器人运动的速度指令,由线速度和角速度组成;
  • 进行机器人配置的服务,有配置的参数和反馈的结果组成等等;

类似这些常用的定义,在ROS系统中都有提供,我们也可以自己开发。

这些接口看上去像是给我们加了一些约束,但却是ROS系统的精髓所在;

  • 举个例子,我们使用相机驱动节点的时候,完全不用关注它是如何驱动相机的,只要一句话运行,我们就可以知道发布出来的图像数据是什么样的了,直接开始我们的应用开发;
  • 类似的,键盘控制我们也可以安装一个ROS包,如何实现的呢?不用关心,反正它发布出来的肯定是线速度和角速度。

1.2 ROS通信接口

接口可以让程序之间的依赖降低,便于我们使用别人的代码,也方便别人使用我们的代码,这就是ROS的核心目标,减少重复造轮子。

ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。

1.2.1 语言无关

为了保证每一个节点可以使用不同语言编程,ROS将这些接口的设计做成了和语言无关的,比如这里看到的:

  • int32表示32位的整型数;
  • int64表示64位的整型数;
  • bool表示布尔值;

还可以定义数组、结构体,这些定义在编译过程中,会自动生成对应到C++Python等语言里的数据结构。

其中:

  • 话题通信接口的定义使用的是.msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行,比如在这个定义里,会传输两个32位的整型数,xy,我们可以用来传输二维坐标的数值;

  • 服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的---区分,比如之前我们学习的加法求和功能,请求数据是两个64位整型数ab,应答是求和的结果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_interfaceC++版本的功能包;

pi@NanoPC-T6:~/dev_ws$ cd src
pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_cmake my_learning_interface

移除当前目录下的文件:srcinclude

修改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坐标

定义中有两个部分,上边是获取目标位置的指令,gettrue的话,就表示我们需要一次位置,服务端就会反馈这个xy坐标了。

完成定义后,还需要在功能包的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坐标

话题消息的内容是一个位置,我们使用xy坐标值进行描述。

完成定义后,还需要在功能包的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入门教程学习笔记

posted @ 2025-12-13 00:20  大奥特曼打小怪兽  阅读(103)  评论(0)    收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步