2、ROS2基础通信机制(一):深入理解话题(Topic)通信——发布者与订阅者的创建、自定义消息接口、QoS配置实战

各位同学,欢迎来到ROS2实战的第二讲。

今天咱们聊的话题,是ROS2里最核心、最常用的通信方式——话题(Topic)。说白了,它就是节点之间传递数据的“管道”。一个节点发,一个节点收,一对多、多对一都行。我刚开始接触ROS时,觉得这玩意儿不就是个消息队列嘛,有啥好学的?后来在项目里踩了坑才发现,里面的门道真不少。

2.1 发布者与订阅者的创建:从“Hello World”开始

先来点实在的。咱们写一个最简单的发布者和订阅者。我习惯用Python,因为调试快,但C++性能更好。今天先用Python演示,原理是通用的。

发布者节点(talker)

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class Talker(Node):
    def __init__(self):
        super().__init__('talker')
        self.publisher = self.create_publisher(String, 'chatter', 10)
        timer_period = 0.5  # 0.5秒发一次
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.i = 0

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello, ROS2! %d' % self.i
        self.publisher.publish(msg)
        self.get_logger().info('发布: "%s"' % msg.data)
        self.i += 1

def main(args=None):
    rclpy.init(args=args)
    node = Talker()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

订阅者节点(listener)

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class Listener(Node):
    def __init__(self):
        super().__init__('listener')
        self.subscription = self.create_subscription(
            String, 'chatter', self.listener_callback, 10)
        self.subscription  # 防止被垃圾回收

    def listener_callback(self, msg):
        self.get_logger().info('收到: "%s"' % msg.data)

def main(args=None):
    rclpy.init(args=args)
    node = Listener()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

嗯,这里要注意:create_publisher和create_subscription的最后一个参数是QoS深度。我刚开始写的时候,随手填了个10,觉得够用了。后来在实车上,数据量一大,直接丢包丢到怀疑人生。所以,QoS不是随便填的,咱们后面细说。

小技巧: 如果你用C++,记得在CMakeLists.txt里添加依赖。我当年第一次编译C++节点,忘了加rclcpp依赖,卡了半小时。嗯,都是泪。

2.2 自定义消息接口:别再用String凑合了

实际项目中,你不可能只传个字符串。比如自动驾驶里,你要传车辆状态、GPS坐标、障碍物列表。这时候就需要自定义消息接口。

ROS2的消息接口定义在.msg文件里。我习惯在包目录下建一个msg文件夹,比如VehicleStatus.msg:

# VehicleStatus.msg
float32 speed          # 车速,单位 m/s
float32 steering_angle # 方向盘转角,单位 rad
bool brake_active      # 刹车是否激活
uint8 gear             # 档位:0-P, 1-R, 2-N, 3-D

然后在CMakeLists.txt里添加:

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/VehicleStatus.msg"
)

编译后,你就可以在代码里直接用了:

from your_package.msg import VehicleStatus

msg = VehicleStatus()
msg.speed = 10.5
msg.steering_angle = 0.3
msg.brake_active = False
msg.gear = 3
避坑指南: 我曾经在定义消息时,把字段类型写成了float,结果编译报错。ROS2里浮点数必须用float32或float64,没有float。另外,字段名不要用关键字,比如type、class,否则你会哭的。

2.3 QoS配置实战:为什么你的数据丢了?

QoS(Quality of Service)是ROS2里最容易被忽视,但也是最关键的部分。你想想看,自动驾驶里,控制指令丢了,车可能就撞了。所以,QoS必须搞明白。

ROS2的QoS策略主要有几个维度:

策略 说明 常用场景
History 保留多少历史数据 KEEP_LAST(保留最近N条)、KEEP_ALL(全保留)
Depth 队列深度(History为KEEP_LAST时有效) 控制指令用1,传感器数据用10
Reliability 可靠性策略 RELIABLE(保证送达)、BEST_EFFORT(尽力而为)
Durability 持久性 VOLATILE(不持久)、TRANSIENT_LOCAL(持久化最后一条)

举个例子。在自动驾驶的控制指令场景下,我建议用:

from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy

qos = QoSProfile(
    depth=1,
    reliability=ReliabilityPolicy.RELIABLE,
    history=HistoryPolicy.KEEP_LAST
)
self.publisher = self.create_publisher(Twist, 'cmd_vel', qos)

为什么深度设成1?因为控制指令是实时性的,旧的指令没意义。你想想看,如果队列里积压了10条指令,车还在执行第1条,第10条早就过时了。所以,深度1就够了。

而对于激光雷达数据,我建议用:

qos = QoSProfile(
    depth=10,
    reliability=ReliabilityPolicy.BEST_EFFORT,
    history=HistoryPolicy.KEEP_LAST
)

激光雷达数据量大,丢几帧没关系,但延迟不能高。所以用BEST_EFFORT,丢了就丢了,别等重传。

核心原则: 控制类话题用RELIABLE + depth=1;传感器类话题用BEST_EFFORT + depth=10。这是我多年项目经验的总结,你直接拿去用,基本不会错。

2.4 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

话题(Topic)通信 发布者(Publisher) create_publisher() publish(msg) 订阅者(Subscriber) create_subscription() 回调函数处理数据 QoS配置关键参数 History(历史策略) | Reliability(可靠性) | Durability(持久性) 控制类:RELIABLE + depth=1 | 传感器类:BEST_EFFORT + depth=10 自定义消息接口

这张图把本章的核心逻辑串起来了。发布者通过话题发布数据,订阅者接收并处理。中间的关键是QoS配置和消息接口定义。你把这个图记在脑子里,后面实战就不会乱。

2.5 实战中的避坑指南

  • 消息类型不匹配: 我曾经在调试时,发布者发的是Twist,订阅者却用Pose接收,结果一直收不到数据。检查了半小时才发现类型对不上。所以,发布者和订阅者的消息类型必须完全一致。
  • QoS不匹配: 发布者用RELIABLE,订阅者用BEST_EFFORT,会导致连接失败。ROS2会尝试协商,但最好保持一致。
  • 节点命名冲突: 同一个话题下,节点名不能重复。我习惯在节点名里加个前缀,比如/vehicle/control_node,避免冲突。
调试小工具: 用ros2 topic list查看所有话题,用ros2 topic echo /chatter实时查看数据。这两个命令我每天用几十次,比看代码快多了。

好了,今天的内容就到这里。话题通信是ROS2的基石,你把它搞透了,后面的服务、动作通信就轻松多了。记住,多动手写代码,别光看。我当年就是看了三天理论,一写代码就报错。嗯,实践出真知。

可可精品资料库