4、第一个ROS2节点:用Python编写发布者节点、理解rclpy、节点生命周期、spin机制

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

前面几章我们把ROS2的环境搭好,也跑通了几个现成的例子。但说实话,光跑别人的节点,你永远学不会怎么写。这一章,我带你亲手写一个发布者节点。这是ROS2编程的“Hello World”,但比打印一行字符串有意思多了——你写的是一个会说话的机器人节点。

4.1 从rclpy开始:ROS2的Python客户端库

rclpy,全称是“ROS Client Library for Python”。说白了,它就是ROS2给Python开发者准备的一把钥匙。没有它,你的Python代码跟ROS2根本说不上话。

我个人习惯,每次新建一个ROS2包,第一件事就是检查rclpy能不能正常导入。别笑,我遇到过好几次环境变量没配好,import直接报红的尴尬情况。

核心概念:rclpy是ROS2节点与底层DDS通信之间的桥梁。你写的所有Python节点,本质上都是在调用rclpy的API。

安装很简单,如果你用的是二进制安装,rclpy已经在了。如果是源码编译,记得编译rclpy这个包。

# 验证rclpy是否可用
python3 -c "import rclpy; print(rclpy.__version__)"

能打印出版本号,说明环境没问题。如果报错,别急着往下走,先回去检查ROS2环境是否source了。

4.2 编写第一个发布者节点

我们写一个最简单的发布者:每隔1秒发布一次“Hello ROS2”字符串消息。

先创建一个功能包:

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python py_pubsub
cd py_pubsub/py_pubsub

然后新建一个文件 talker.py:

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

class TalkerNode(Node):
    def __init__(self):
        super().__init__('talker')
        self.publisher_ = self.create_publisher(String, 'topic', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.get_logger().info('Talker节点已启动')

    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 = TalkerNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

代码不长,但每一行都有讲究。我拆开来讲。

4.3 节点生命周期:从出生到销毁

一个ROS2节点,不是凭空出现的。它有完整的生命周期:初始化、运行、销毁。

看上面的代码,流程是这样的:

  1. rclpy.init():初始化ROS2上下文。这步必须在创建任何节点之前调用。我刚开始学的时候,经常忘记写这行,然后节点死活起不来。
  2. 创建节点对象:TalkerNode(),构造函数里完成所有初始化工作——创建发布者、创建定时器。
  3. rclpy.spin():让节点“转起来”,等待回调触发。
  4. 节点销毁与关闭:destroy_node()和rclpy.shutdown(),清理资源。

我的经验:节点销毁这一步,很多人会忽略。但在实际项目中,尤其是长时间运行的机器人,不释放资源会导致内存泄漏。我曾经在一个巡检机器人项目里,就是因为没正确销毁节点,跑了三天后内存爆了。

为了让你更直观地理解,我画了一张节点生命周期的流程图:

rclpy.init() 创建节点对象 rclpy.spin() 回调循环 销毁 & 关闭 destroy_node() rclpy.shutdown()

4.4 spin机制:节点是怎么“活”起来的

很多初学者会问:为什么要有spin?我直接写个while循环不行吗?

嗯,你想想看,如果不用spin,你得自己写一个死循环,然后在循环里手动检查有没有新消息、定时器有没有到期。这活儿ROS2帮你干了。

spin的本质:它是一个事件循环。它会一直阻塞,等待回调函数被触发。触发源有两个:

  • 定时器:到了设定的时间间隔,自动调用回调函数。
  • 订阅话题:收到新消息时,触发订阅回调。

在我们的例子里,create_timer(1.0, self.timer_callback)创建了一个1秒触发一次的定时器。spin会每1秒调用一次timer_callback。

注意:spin是阻塞的。如果你在spin后面写了代码,那些代码要等到节点被销毁才会执行。我曾经犯过一个错:在spin后面写了一个打印语句,结果等了半天没输出,还以为是程序卡死了。

4.5 发布者与话题:数据是怎么送出去的

来看这行关键代码:

self.publisher_ = self.create_publisher(String, 'topic', 10)

三个参数:

参数 含义 说明
String 消息类型 来自std_msgs包,表示字符串消息
'topic' 话题名称 发布者往这个话题上发数据,订阅者从这收
10 队列大小 消息积压上限,超过则丢弃旧消息

队列大小这个参数,我多说两句。它决定了当订阅者处理不过来时,系统能缓存多少条消息。设得太小容易丢消息,设得太大又占内存。我在做激光雷达数据发布时,设过100,结果内存直接飙上去。后来改成10,刚刚好。

4.6 运行你的第一个节点

写完了,怎么跑?

先配置一下setup.py,把talker注册成可执行程序:

entry_points={
    'console_scripts': [
        'talker = py_pubsub.talker:main',
    ],
},

然后编译、运行:

cd ~/ros2_ws
colcon build --packages-select py_pubsub
source install/setup.bash
ros2 run py_pubsub talker

你会看到终端每隔1秒打印一条信息:

[INFO] [talker]: 发布: Hello ROS2
[INFO] [talker]: 发布: Hello ROS2
...

同时,你可以打开另一个终端,用ros2 topic echo /topic来验证消息是否真的在发布。

调试技巧:如果节点起不来,先检查ros2 node list能不能看到你的节点。看不到?大概率是setup.py配置错了,或者没有source环境。

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

写ROS2节点,有几个地方特别容易出问题。我把自己踩过的坑列出来,你遇到了可以直接对照:

  • 坑一:忘记rclpy.init()——节点创建时会直接崩溃,报错信息是“rclpy not initialized”。
  • 坑二:话题名称拼写错误——发布者和订阅者的话题名必须完全一致,大小写敏感。我因为把topic写成了Topic,排查了半小时。
  • 坑三:spin阻塞了后续代码——如果你需要在spin之后做清理工作,记得用try/finally包裹,确保shutdown一定会执行。

嗯,这一章的内容就到这。你亲手写了一个ROS2节点,理解了rclpy的作用,也搞清楚了节点从生到死的完整流程。下一章,我们写一个订阅者,让两个节点真正“聊”起来。