3、ROS2基础通信机制(上):话题(Topic)编程实战——发布者与订阅者模型、自定义消息接口

各位同学,欢迎来到第三章。从这一章开始,我们正式进入ROS2的“灵魂”——通信机制。

ROS2的通信,说白了就是节点之间怎么“说话”。你想想看,一个机器人身上有激光雷达、有电机、有摄像头,它们之间必须高效地交换数据。ROS2提供了好几种通信方式,其中话题(Topic)是最常用、最核心的一种。

我个人习惯把话题通信比作“电台广播”。发布者就是电台主持人,只管对着麦克风说话;订阅者就是听众,只要调到正确的频率,就能收到内容。双方不需要知道对方是谁,甚至不需要同时在线。这种松耦合的设计,让系统扩展起来非常方便。

核心概念速览:
  • 发布者(Publisher):发送数据的节点
  • 订阅者(Subscriber):接收数据的节点
  • 话题(Topic):数据传输的通道名称
  • 消息(Message):传输的数据结构

3.1 发布者与订阅者模型

我们先来看一个最简单的例子。假设我们要写一个“数字发布者”,每隔一秒发布一个递增的整数;再写一个“数字订阅者”,收到数字后打印出来。

3.1.1 编写发布者节点

创建一个新的功能包,我建议你命名为topic_demo。在ROS2中,我们通常用C++或Python来写节点。这里我用Python演示,因为上手更快。

# number_publisher.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import Int32

class NumberPublisher(Node):
    def __init__(self):
        super().__init__('number_publisher')
        self.publisher_ = self.create_publisher(Int32, 'number_topic', 10)
        self.counter = 0
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.get_logger().info('数字发布者已启动!')

    def timer_callback(self):
        msg = Int32()
        msg.data = self.counter
        self.publisher_.publish(msg)
        self.get_logger().info(f'发布: {msg.data}')
        self.counter += 1

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

if __name__ == '__main__':
    main()

这段代码里,create_publisher的三个参数分别是:消息类型、话题名称、队列大小。队列大小设为10,意思是如果订阅者处理不过来,最多缓存10条消息。我在项目中遇到过队列设置太小导致丢数据的情况,所以一般建议设大一点,比如10或100。

3.1.2 编写订阅者节点

# number_subscriber.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import Int32

class NumberSubscriber(Node):
    def __init__(self):
        super().__init__('number_subscriber')
        self.subscription = self.create_subscription(
            Int32, 'number_topic', self.listener_callback, 10)
        self.subscription  # 防止被垃圾回收
        self.get_logger().info('数字订阅者已启动!')

    def listener_callback(self, msg):
        self.get_logger().info(f'收到: {msg.data}')

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

订阅者的回调函数listener_callback会在每次收到消息时被调用。注意那个self.subscription赋值——嗯,这里要注意,如果不保存这个引用,Python的垃圾回收可能会把它删掉,导致收不到消息。我曾经因为这个bug排查了半天,后来才发现是少了一行赋值。

3.1.3 运行测试

打开两个终端,分别运行:

# 终端1
ros2 run topic_demo number_publisher

# 终端2
ros2 run topic_demo number_subscriber

你会看到订阅者终端不断打印出递增的数字。这就是最基础的话题通信。

小技巧: 用 ros2 topic list 可以查看当前所有活跃的话题;用 ros2 topic echo /number_topic 可以直接在终端查看话题数据,非常适合调试。

3.2 自定义消息接口

标准消息类型(如Int32、String)只能传简单数据。实际项目中,我们经常需要传更复杂的信息,比如机器人的位置、速度、状态等。这时候就需要自定义消息接口。

3.2.1 创建接口定义文件

在功能包中创建msg文件夹,新建RobotStatus.msg文件:

# RobotStatus.msg
int32 robot_id
string robot_name
float64 position_x
float64 position_y
float64 velocity
bool is_online

每一行定义了一个字段:类型 + 字段名。ROS2支持的消息类型包括int32、float64、string、bool等,也支持嵌套其他消息类型。

3.2.2 配置CMakeLists.txt和package.xml

在CMakeLists.txt中添加:

find_package(rosidl_default_generators REQUIRED)

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

在package.xml中添加:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

编译后,ROS2会自动生成对应的Python和C++代码。你可以用ros2 interface show topic_demo/msg/RobotStatus查看生成的消息结构。

3.2.3 使用自定义消息

发布者代码修改如下:

from topic_demo.msg import RobotStatus

# 在回调函数中
msg = RobotStatus()
msg.robot_id = 1
msg.robot_name = 'NavBot'
msg.position_x = 1.5
msg.position_y = 2.3
msg.velocity = 0.5
msg.is_online = True
self.publisher_.publish(msg)

订阅者接收时,直接访问msg.robot_name、msg.position_x等字段即可。

避坑指南: 我曾经在自定义消息中使用了float类型,结果编译报错。ROS2的消息类型是float32和float64,没有float。另外,字段名不要用驼峰命名,ROS2官方推荐蛇形命名(如robot_name)。

3.3 话题通信的核心逻辑

为了让你更直观地理解话题通信的流程,我画了一张图:

ROS2话题通信模型 发布者节点 Publisher Node 订阅者节点 Subscriber Node 话题:/number_topic 发布 订阅 消息内容示例(RobotStatus.msg) robot_id: 1 robot_name: "NavBot" position_x: 1.5 position_y: 2.3 velocity: 0.5 is_online: True 发布者只管发,订阅者只管收,双方通过话题名称匹配

从图中可以看到,发布者和订阅者之间没有直接连接,而是通过话题这个“中间人”来通信。这种设计的好处是:你可以随时增加新的发布者或订阅者,系统不需要做任何修改。

3.4 实战中的注意事项

问题 原因 解决方法
收不到消息 话题名称拼写不一致 用 ros2 topic list 检查实际话题名
消息丢失 队列大小设置太小 增大队列大小,建议10以上
编译自定义消息失败 CMakeLists.txt配置遗漏 检查 rosidl_generate_interfaces 是否添加
节点启动后立即退出 缺少 rclpy.spin() 添加 rclpy.spin(node) 保持节点运行
个人经验: 调试话题通信时,我习惯先启动订阅者,再启动发布者。这样能确保订阅者不会错过第一条消息。另外,ros2 topic hz /topic_name 可以查看消息发布频率,非常实用。

好了,这一章的内容就到这里。话题通信是ROS2的基石,后面的服务、动作等机制都建立在它的基础之上。建议你亲手敲一遍代码,把发布者和订阅者跑通,再试试自定义消息。动手实践永远是最好的学习方式。