4. 订阅者节点编写:用Python写一个位姿订阅节点,实时打印机器人位置

好,咱们接着往下走。上一节我们写了个发布者,让机器人动起来了。但光发不收,就像你对着空气说话——没人知道你说得对不对。所以这一节,我们来写一个订阅者节点,专门监听机器人的位姿数据,然后实时打印出来。

说白了,就是让机器人一边跑,我们一边看它跑到哪儿了。

4.1 订阅者节点是干什么的?

订阅者节点,名字听着挺唬人,其实就干一件事:等着收消息。

在ROS2里,发布者把数据扔到话题(Topic)上,订阅者就去这个话题上取数据。取到了,你想怎么处理都行——打印、存文件、做决策,随你。

我个人习惯是,写任何机器人程序之前,先写一个订阅者节点。为啥?因为你要先确认数据有没有发出来。数据都没到,后面写再多控制逻辑也是白搭。

核心要点:订阅者节点 = 监听话题 + 回调函数处理数据

4.2 代码实战:位姿订阅节点

我们这次订阅的话题是 /turtle1/pose,这是Turtlesim里海龟的位姿话题。数据类型是 turtlesim/msg/Pose,里面包含 x、y、theta、线速度、角速度。

来,直接上代码。我习惯把文件命名为 pose_subscriber.py。

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose

class PoseSubscriber(Node):
    def __init__(self):
        super().__init__('pose_subscriber')
        self.subscription = self.create_subscription(
            Pose,               # 消息类型
            '/turtle1/pose',    # 话题名称
            self.pose_callback, # 回调函数
            10                  # 队列大小
        )
        self.subscription  # 防止被垃圾回收

    def pose_callback(self, msg):
        # 实时打印位置信息
        self.get_logger().info(
            f'位置: x={msg.x:.2f}, y={msg.y:.2f}, 角度={msg.theta:.2f}'
        )

def main(args=None):
    rclpy.init(args=args)
    node = PoseSubscriber()
    rclpy.spin(node)  # 保持节点运行
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

代码不长,但有几个地方我得重点说一下。

4.3 代码拆解:每一行都是坑

第一,回调函数的设计。

你看 pose_callback 这个函数,它会在每次收到消息时被自动调用。ROS2里这叫回调机制。你想想看,如果你在回调里做耗时操作,比如写文件、算复杂公式,那就会阻塞后面的消息接收。我在项目中遇到过,有人把图像处理放在回调里,结果帧率直接掉到1帧。

第二,队列大小设为10。

这个10是什么意思?就是缓冲区能存10条消息。如果处理不过来,旧消息会被丢弃。嗯,这里要注意:不是越大越好。设太大,内存占用高;设太小,容易丢消息。一般10~100够用。

第三,rclpy.spin(node) 这行。

这行代码让节点一直运行,等待消息。没有它,程序跑完就退出了。我曾经犯过这个错——写好了订阅者,运行后啥也没打印,查了半天才发现忘了加 spin。

小技巧:调试时可以先不加 spin,用 rclpy.spin_once(node) 单步执行,看回调是否被触发。

4.4 运行测试:看看效果

写完了,怎么跑?两步走。

  1. 先启动Turtlesim节点和键盘控制节点(如果你忘了,回去看上一节)。
  2. 再运行我们的订阅者节点。

终端命令如下:

# 终端1:启动海龟仿真
ros2 run turtlesim turtlesim_node

# 终端2:启动键盘控制
ros2 run turtlesim turtle_teleop_key

# 终端3:运行我们的订阅者
python3 pose_subscriber.py

然后你按键盘控制海龟移动,订阅者终端就会实时打印:

[INFO] [1700000000.123456] [pose_subscriber]: 位置: x=5.54, y=5.54, 角度=0.00
[INFO] [1700000000.223456] [pose_subscriber]: 位置: x=5.55, y=5.55, 角度=0.01
[INFO] [1700000000.323456] [pose_subscriber]: 位置: x=5.56, y=5.56, 角度=0.02

看到没?数据在实时更新。这就是订阅者的威力——你不需要主动去问,数据自己送上门来。

4.5 避坑指南:我踩过的三个坑

写订阅者节点,有几个地方特别容易翻车。我一个个说。

坑一:话题名写错

我曾经把 /turtle1/pose 写成 /turtle/pose,结果等了五分钟没反应。后来用 ros2 topic list 一查,才发现少了个1。记住:话题名必须完全匹配,大小写、斜杠都不能错。

坑二:消息类型不匹配

订阅的话题是 Pose 类型,你非要用 Twist 去接,那肯定报错。ROS2的类型检查很严格,类型不对,编译都过不了。

坑三:忘记 self.subscription 赋值

你看代码里我写了 self.subscription 这行。如果不写,Python的垃圾回收会把订阅对象删掉,节点就收不到消息了。这个坑特别隐蔽,我debug了整整一下午。

4.6 知识体系:订阅者节点的核心逻辑

为了让你更直观地理解,我画了一张流程图。这张图展示了从话题数据到回调函数的完整链路。

订阅者节点数据流 发布者节点 /turtle1/pose 话题 (Topic) 订阅者节点 回调函数 pose_callback() 打印位置信息 数据流向:发布者 → 话题 → 订阅者 → 回调 → 处理

这张图其实已经把订阅者的核心逻辑讲清楚了。你只要记住:数据从发布者来,经过话题中转,最终在回调函数里被消费。

4.7 扩展思考:订阅者还能做什么?

打印位置只是最基础的应用。在实际项目中,订阅者可以做更多事:

  • 数据记录:把位置信息写入CSV文件,用于后续分析
  • 条件触发:当机器人到达某个坐标时,触发其他动作
  • 可视化:把位置数据传给可视化工具,实时显示轨迹
  • 多传感器融合:同时订阅多个话题,综合判断机器人状态

我个人建议,初学者先把打印做好。能稳定打印了,再考虑扩展。步子迈太大,容易扯着。

进阶提示:如果你想同时订阅多个话题,可以用 create_subscription 多次,每个话题绑定不同的回调函数。ROS2支持多线程回调,但那是后话了。

4.8 本章小结

好,这一节的内容就到这儿。我们写了一个位姿订阅节点,学会了:

  • 如何创建订阅者
  • 回调函数的工作原理
  • 常见的三个坑以及怎么避开

说实话,订阅者节点是ROS2里最常用的模式之一。你后面写的每一个机器人程序,几乎都离不开它。把这个基础打牢,后面学控制、学导航,都会轻松很多。

嗯,今天就到这儿。代码写完了,记得跑一下试试。看到终端里实时跳动的坐标,那种成就感,比看教程爽多了。


可可精品资料库