第1章:ROS2话题基础回顾——发布订阅模型、命名规则与消息类型
各位同学,咱们正式开始这门课之前,先把地基夯实一下。ROS2的话题(Topic)机制,说白了就是节点之间“喊话”和“听广播”的通信方式。我见过不少新手一上来就写代码,结果节点之间死活收不到数据,最后发现是话题名拼错了——这种坑,咱们今天一次性说清楚。
1.1 发布-订阅模型:谁在说话,谁在听?
ROS2的通信核心就是发布-订阅模式。一个节点可以发布数据到某个话题,另一个节点订阅这个话题就能收到数据。整个过程是异步的、一对多的——发布者不知道谁在听,订阅者也不知道谁在说。
关键点:发布者和订阅者完全解耦。你只管发,我只管收,中间由ROS2的DDS(数据分发服务)负责调度。这也是ROS2比ROS1更灵活的地方——DDS帮我们处理了网络发现、QoS策略等底层细节。
我个人习惯把话题想象成“电台频道”。发布者是电台主播,订阅者是听众。主播只管播,听众可以随时打开收音机听,也可以关掉。主播不需要知道有多少人在听,听众也不需要知道主播长什么样。
举个例子,你写一个摄像头节点发布图像数据,另一个节点订阅它做目标检测。这两个节点可能运行在同一台电脑上,也可能分布在不同的机器人上——DDS会自动发现对方。
# 发布者示例(Python)
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class Talker(Node):
def __init__(self):
super().__init__('talker')
self.pub = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
msg = String()
msg.data = 'Hello, ROS2!'
self.pub.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
# 订阅者示例(Python)
class Listener(Node):
def __init__(self):
super().__init__('listener')
self.sub = self.create_subscription(String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'I heard: "{msg.data}"')
小技巧:创建发布者或订阅者时,最后一个参数是QoS深度(这里填10)。它表示消息队列最多缓存多少条。如果订阅者处理速度跟不上发布者,旧消息会被丢弃。我曾经在调试激光雷达数据时把QoS设成1,结果丢帧严重——后来改成10就正常了。
1.2 话题命名规则:别让名字坑了你
话题名看起来简单,但坑不少。ROS2的话题名必须符合以下规则:
- 以斜杠开头:比如
/camera/image_raw。如果不加斜杠,ROS2会把它当作相对名称,自动拼接节点的命名空间。 - 只允许小写字母、数字、下划线:别用大写字母,别用连字符。我见过有人用
/My-Topic,结果DDS直接报错。 - 层级结构用斜杠分隔:比如
/robot/arm/joint_state。这样方便管理和过滤。 - 避免使用特殊字符:空格、中文、@#¥%……统统不行。
| 合法示例 | 非法示例 | 原因 |
|---|---|---|
/camera/image_raw |
/Camera/Image_Raw |
大写字母 |
/robot/arm/joint_state |
/robot-arm/joint-state |
连字符 |
/sensor/temperature |
/sensor/temperature data |
空格 |
/lidar/scan |
/lidar/扫描 |
中文 |
避坑指南:我曾经在调试多机器人系统时,两个机器人用了相同的话题名但不同的命名空间,结果数据串了。后来我强制要求所有话题名加上机器人ID前缀,比如 /robot1/camera/image_raw。记住:话题名是全局的,命名空间是局部的——别搞混。
1.3 消息类型与接口定义:数据长什么样?
ROS2的消息类型就是数据的“模板”。每个话题必须绑定一个消息类型,发布者和订阅者必须使用相同的类型才能通信。ROS2内置了很多常用消息类型,比如:
std_msgs/msg/String:字符串std_msgs/msg/Int32:32位整数geometry_msgs/msg/Twist:速度指令(线速度+角速度)sensor_msgs/msg/LaserScan:激光雷达扫描数据nav_msgs/msg/Odometry:里程计数据
但很多时候,内置类型不够用。你需要自定义消息类型。ROS2使用.msg文件来定义消息结构,语法很简单:
# 自定义消息文件:my_interfaces/msg/MyMessage.msg
int32 id
string name
float64[3] position
bool is_active
定义好之后,在CMakeLists.txt和package.xml里注册,然后编译。之后你就可以在代码里直接使用这个类型了。
核心原则:消息类型是通信的“契约”。发布者和订阅者必须使用完全相同的消息定义,包括字段名、字段类型、字段顺序。哪怕只改了一个字段名,通信就会失败。所以,我建议把消息定义放在一个独立的接口包中,多个节点共同依赖它。
你想想看,如果两个团队各自定义了一个Point消息,一个用x, y, z,另一个用X, Y, Z——那它们永远无法通信。这就是为什么ROS2社区鼓励使用标准消息类型,实在不行再自定义。
1.4 本章知识体系总览
下面这张图帮你把本章的核心逻辑串起来。从发布者到订阅者,中间经过话题、消息类型、DDS调度——每一步都环环相扣。
嗯,这张图把整个流程讲清楚了。发布者把数据扔到话题上,话题绑定了消息类型,底层由DDS负责传输,订阅者从话题上取数据。只要类型匹配、名字正确,通信就能跑通。
我的经验:刚开始学ROS2时,我总喜欢把所有节点写在一个包里。后来项目大了,发现消息定义散落在各处,改一个字段要改好几个地方。现在我强制自己把消息定义单独放在一个_interfaces包里,所有节点依赖它。这样改消息定义时,编译期就能发现所有受影响的地方——省心多了。