4、Python客户端库(rclpy)实战:编写发布者与订阅者节点,理解spin与回调机制
好,咱们今天来点真家伙。
前面几章我们把ROS2的架构、通信原理、还有rclpy的基本概念都捋了一遍。说实话,光看理论容易犯困。我当年学的时候也是这样,看了一堆文档,结果一写代码就懵——"这个spin到底是个啥?回调函数怎么老不执行?"
所以这一章,咱们直接上手写代码。你跟着我敲一遍,保证比看十遍文档都管用。
4.1 一个最简单的发布者节点
先来个最基础的——发布"Hello ROS2"字符串消息。
我个人习惯,写任何节点之前,先想清楚三件事:
- 我要发什么类型的数据?——这里用
std_msgs.msg.String - 话题名叫什么?——就叫
/hello_topic - 多久发一次?——每秒2次,也就是0.5秒间隔
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class HelloPublisher(Node):
def __init__(self):
super().__init__('hello_publisher')
self.publisher_ = self.create_publisher(String, '/hello_topic', 10)
self.timer = self.create_timer(0.5, self.timer_callback)
self.get_logger().info('发布者节点已启动!')
def timer_callback(self):
msg = String()
msg.data = 'Hello ROS2!'
self.publisher_.publish(msg)
self.get_logger().info(f'发布了: {msg.data}')
def main(args=None):
rclpy.init(args=args)
node = HelloPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
代码不长,但有几个关键点我得跟你说道说道。
核心要点:
create_publisher第三个参数是队列深度,我一般设10。设太小容易丢消息,设太大占内存。10是个比较稳妥的值。create_timer第一个参数是秒数,0.5就是500ms触发一次回调。rclpy.spin(node)这行是灵魂——它让节点一直活着,等待事件发生。
4.2 订阅者节点:把消息接住
有发的就得有收的。咱们再写一个订阅者节点,专门听 /hello_topic 上的消息。
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class HelloSubscriber(Node):
def __init__(self):
super().__init__('hello_subscriber')
self.subscription = self.create_subscription(
String,
'/hello_topic',
self.listener_callback,
10
)
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 = HelloSubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
你看,订阅者的结构和发布者几乎是对称的。区别就在于:
- 发布者用
create_publisher+ 定时器主动发 - 订阅者用
create_subscription+ 回调函数被动收
嗯,这里要注意:回调函数的参数 msg 就是接收到的消息对象。你可以在回调里做任何处理——打印、存数据库、甚至触发另一个动作。
4.3 spin到底在干什么?
这个问题,我当年纠结了整整两天。说白了,rclpy.spin(node) 就是一个死循环,它不停地做两件事:
- 检查有没有新消息到达——如果有,就调用对应的回调函数
- 检查定时器是否到期——如果到期,就触发定时器回调
你可以把它想象成一个前台服务员:
- 客人(消息)来了,他就喊一声(触发回调)
- 闹钟(定时器)响了,他也喊一声(触发定时器回调)
- 没事情做的时候,他就站在那儿等着(阻塞)
小技巧: 如果你不想让spin阻塞整个程序,可以用 rclpy.spin_once(node) 或者把spin放到单独的线程里。我在做多传感器融合项目时,经常把不同传感器的节点放在不同线程里spin。
4.4 回调机制:你写代码,ROS2帮你调用
回调(callback)是ROS2里最核心的编程模式。你想想看,你不需要写一个while循环去轮询有没有新消息——你只需要注册一个回调函数,ROS2会在合适的时候自动调用它。
这就是所谓的"事件驱动"编程。
我曾经犯过一个低级错误:在回调函数里写了一个死循环,结果整个节点卡死了。为什么?因为回调函数是在spin的线程里执行的,你把它堵住了,spin就没办法处理其他事件了。
警告: 回调函数里不要做耗时操作!如果确实需要处理大量数据,请用多线程或者异步方式。否则你的节点会变得非常"迟钝"。
4.5 运行起来看看效果
好,代码写完了,咱们跑一下。
打开两个终端:
# 终端1:运行发布者
ros2 run your_package hello_publisher
# 终端2:运行订阅者
ros2 run your_package hello_subscriber
你应该会看到:
[INFO] [hello_publisher]: 发布了: Hello ROS2!
[INFO] [hello_subscriber]: 收到了: Hello ROS2!
[INFO] [hello_publisher]: 发布了: Hello ROS2!
[INFO] [hello_subscriber]: 收到了: Hello ROS2!
...
每秒两条,整整齐齐。看到这个输出,说明你的第一个ROS2节点对已经跑通了。
4.6 避坑指南:我踩过的几个坑
| 常见问题 | 原因 | 解决办法 |
|---|---|---|
| 节点启动后没反应 | 忘记调用 rclpy.spin() |
加上spin,节点才能"活"起来 |
| 回调函数不执行 | 话题名拼写不一致 | 检查发布者和订阅者的话题名是否完全一样 |
| 程序卡死 | 回调里做了耗时操作 | 把耗时操作放到单独线程或异步执行 |
| 消息丢失 | 队列深度太小 | 增大 create_publisher 的队列参数 |
我曾经在调试一个机器人导航项目时,发现订阅者总是收不到部分消息。查了半天,原来是发布者的队列深度设成了1,消息一多就丢了。改成10之后,问题立刻解决。这种坑,踩过一次就记住了。
4.7 小结与思考
这一章我们做了三件事:
- 写了一个发布者节点,用定时器周期性地发消息
- 写了一个订阅者节点,用回调函数接收消息
- 理解了spin的本质——一个等待事件的事件循环
说实话,ROS2编程的80%工作,就是在跟发布者、订阅者、回调函数打交道。你把这套模式玩熟了,后面学服务、动作、参数,都会轻松很多。
下一章,我们会聊聊更复杂的消息类型——比如如何发布图像数据、激光雷达数据。到时候你会发现,核心逻辑跟今天讲的完全一样,只是消息结构变复杂了而已。
好,今天就到这儿。你自己动手把代码敲一遍,遇到问题随时翻翻上面的避坑指南。咱们下章见。