3、ROS2基础编程:Python客户端库(rclpy)入门

好,咱们正式开始写代码了。

前面两章我们把ROS2的环境搭好,也聊了聊它的核心概念。现在,是时候让机器人真正「开口说话」了。这一章,我会带你从零开始,用Python写一个发布者节点和一个订阅者节点。

说白了,就是让两个程序通过ROS2互相喊话。一个负责发,一个负责收。

3.1 rclpy:ROS2的Python大门

rclpy,全称是ROS Client Library for Python。它是ROS2为Python开发者提供的官方库。

我个人习惯把它理解为「ROS2的Python翻译官」。你写的Python代码,通过它才能跟ROS2的底层通信机制打交道。没有它,你的Python程序就是个孤岛。

核心要点:rclpy负责初始化节点、管理通信、处理回调。所有Python节点的起点,都是 rclpy.init()。

我在项目中遇到过不少新手,上来就写 import rclpy,然后直接调用发布订阅。结果程序跑起来没反应。为什么?因为忘了初始化。记住,rclpy.init() 是第一步,就像你开车前得先拧钥匙点火一样。

3.2 创建第一个发布者节点

我们先写一个发布者。它每隔一秒发一条消息,内容就是「Hello, ROS2!」。

嗯,这里要注意:发布者节点需要三个要素——节点名称、话题名称、消息类型。

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class SimplePublisher(Node):
    def __init__(self):
        super().__init__('simple_publisher')
        self.publisher_ = self.create_publisher(String, 'chatter', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.counter = 0

    def timer_callback(self):
        msg = String()
        msg.data = f'Hello, ROS2! 第 {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 = SimplePublisher()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

这段代码里,我做了几件事:

  • 继承了 Node 类,创建了一个叫 simple_publisher 的节点。
  • 用 create_publisher 创建了一个发布者,话题叫 chatter,消息类型是 String。
  • 用 create_timer 创建了一个定时器,每秒触发一次回调函数。

小技巧:队列长度参数 10 是什么意思?它表示消息缓冲区的大小。如果订阅者处理得慢,消息会暂存在这里。我曾经设成 1,结果丢消息丢到怀疑人生。一般设 10 或 100 比较稳妥。

3.3 创建第一个订阅者节点

有发的,就得有收的。订阅者节点负责监听 chatter 话题,一旦有消息进来,就打印出来。

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class SimpleSubscriber(Node):
    def __init__(self):
        super().__init__('simple_subscriber')
        self.subscription = self.create_subscription(
            String,
            'chatter',
            self.listener_callback,
            10
        )
        self.subscription  # 防止被垃圾回收

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

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

if __name__ == '__main__':
    main()

你看,订阅者的结构和发布者很像。区别在于:

  • 用 create_subscription 代替了 create_publisher。
  • 不需要定时器,消息来了自动触发回调。
  • 回调函数 listener_callback 接收一个 msg 参数,里面就是收到的数据。

注意:订阅者创建后,一定要把 self.subscription 赋值给一个成员变量。否则Python的垃圾回收机制会把它当成临时变量给清理掉。我刚开始学的时候就踩过这个坑,节点创建了,但就是收不到消息,排查了半天才发现是这个问题。

3.4 理解回调函数

回调函数,是ROS2编程里绕不开的概念。你想想看,你的程序怎么知道「什么时候该执行什么动作」?

传统写法是写一个死循环,不断检查有没有新消息。但ROS2用了另一种方式——回调。

说白了,回调函数就是「你告诉ROS2:当某件事发生时,请调用这个函数」。比如:

  • 定时器到期了 → 调用 timer_callback
  • 新消息到达了 → 调用 listener_callback

这样做的好处是,你的代码不用一直轮询等待。ROS2的 rclpy.spin() 会帮你管理这一切。它就像一个事件循环,不断检查有没有事件发生,有的话就调用对应的回调函数。

关键理解:回调函数是异步触发的。你永远不知道它什么时候会被调用。所以回调函数里不要做耗时操作,比如读写大文件、发网络请求。否则会阻塞整个节点,导致其他回调无法执行。

我曾经在一个项目里,把图像处理的代码直接写在了回调函数里。结果图像还没处理完,下一帧又来了,整个系统卡得跟幻灯片一样。后来我把耗时操作放到单独的线程里,回调只负责把数据丢进队列,问题就解决了。

3.5 发布-订阅模型流程图

下面这张图,展示了发布者和订阅者之间的通信关系。我画得比较简洁,但核心逻辑都在里面了。

发布者节点 simple_publisher 订阅者节点 simple_subscriber 话题 /chatter 消息类型: std_msgs/String 发布消息 接收消息 timer_callback() listener_callback() 发布者通过话题发布消息,订阅者从话题接收消息,回调函数处理数据

这张图你看懂了吗?发布者把消息扔到话题上,订阅者从话题上取消息。两者完全解耦,互不知道对方的存在。这就是ROS2通信的精髓。

3.6 运行你的第一个ROS2程序

代码写好了,怎么跑起来?

  1. 打开终端,先source一下ROS2环境:source /opt/ros/humble/setup.bash
  2. 运行发布者:python3 publisher.py
  3. 再开一个终端,同样source环境,运行订阅者:python3 subscriber.py

你会看到发布者终端每秒打印一条发布日志,订阅者终端同步打印接收日志。如果一切正常,恭喜你,你的第一个ROS2节点跑起来了!

调试技巧:如果收不到消息,先用 ros2 topic list 看看话题有没有创建。再用 ros2 topic echo /chatter 直接监听话题内容。这两个命令是我排查通信问题的首选工具。

嗯,这一章的内容就到这里。你学会了用rclpy创建发布者和订阅者,也理解了回调函数的工作原理。这些是ROS2编程的基石,后面的所有传感器接入、数据处理,都建立在这个基础之上。