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 运行测试:看看效果
写完了,怎么跑?两步走。
- 先启动Turtlesim节点和键盘控制节点(如果你忘了,回去看上一节)。
- 再运行我们的订阅者节点。
终端命令如下:
# 终端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 知识体系:订阅者节点的核心逻辑
为了让你更直观地理解,我画了一张流程图。这张图展示了从话题数据到回调函数的完整链路。
这张图其实已经把订阅者的核心逻辑讲清楚了。你只要记住:数据从发布者来,经过话题中转,最终在回调函数里被消费。
4.7 扩展思考:订阅者还能做什么?
打印位置只是最基础的应用。在实际项目中,订阅者可以做更多事:
- 数据记录:把位置信息写入CSV文件,用于后续分析
- 条件触发:当机器人到达某个坐标时,触发其他动作
- 可视化:把位置数据传给可视化工具,实时显示轨迹
- 多传感器融合:同时订阅多个话题,综合判断机器人状态
我个人建议,初学者先把打印做好。能稳定打印了,再考虑扩展。步子迈太大,容易扯着。
进阶提示:如果你想同时订阅多个话题,可以用 create_subscription 多次,每个话题绑定不同的回调函数。ROS2支持多线程回调,但那是后话了。
4.8 本章小结
好,这一节的内容就到这儿。我们写了一个位姿订阅节点,学会了:
- 如何创建订阅者
- 回调函数的工作原理
- 常见的三个坑以及怎么避开
说实话,订阅者节点是ROS2里最常用的模式之一。你后面写的每一个机器人程序,几乎都离不开它。把这个基础打牢,后面学控制、学导航,都会轻松很多。
嗯,今天就到这儿。代码写完了,记得跑一下试试。看到终端里实时跳动的坐标,那种成就感,比看教程爽多了。