ROS2机器人项目架构与组件化开发 - 第4节
4. 第一个ROS2节点
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节点,不是凭空出现的。它有完整的生命周期:初始化、运行、销毁。
看上面的代码,流程是这样的:
- rclpy.init():初始化ROS2上下文。这步必须在创建任何节点之前调用。我刚开始学的时候,经常忘记写这行,然后节点死活起不来。
- 创建节点对象:
TalkerNode(),构造函数里完成所有初始化工作——创建发布者、创建定时器。 - 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的作用,也搞清楚了节点从生到死的完整流程。下一章,我们写一个订阅者,让两个节点真正“聊”起来。