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等字段即可。
robot_name)。
3.3 话题通信的核心逻辑
为了让你更直观地理解话题通信的流程,我画了一张图:
从图中可以看到,发布者和订阅者之间没有直接连接,而是通过话题这个“中间人”来通信。这种设计的好处是:你可以随时增加新的发布者或订阅者,系统不需要做任何修改。
3.4 实战中的注意事项
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 收不到消息 | 话题名称拼写不一致 | 用 ros2 topic list 检查实际话题名 |
| 消息丢失 | 队列大小设置太小 | 增大队列大小,建议10以上 |
| 编译自定义消息失败 | CMakeLists.txt配置遗漏 | 检查 rosidl_generate_interfaces 是否添加 |
| 节点启动后立即退出 | 缺少 rclpy.spin() |
添加 rclpy.spin(node) 保持节点运行 |
ros2 topic hz /topic_name 可以查看消息发布频率,非常实用。
好了,这一章的内容就到这里。话题通信是ROS2的基石,后面的服务、动作等机制都建立在它的基础之上。建议你亲手敲一遍代码,把发布者和订阅者跑通,再试试自定义消息。动手实践永远是最好的学习方式。