4. 第一个ROS2节点:用Python写一个“Hello World”节点,理解节点生命周期

好,咱们开始动手写代码了。

说实话,我当年学ROS1的时候,第一个节点也是“Hello World”。那时候踩了不少坑,比如节点跑起来却看不到输出,或者节点一启动就自己退出了。后来才明白,这些问题的根源,都是对节点生命周期理解不到位。

今天咱们就用Python写一个最简的ROS2节点。你跟着我走一遍,保证能跑起来。

4.1 先搭个架子:创建功能包

在ROS2里,节点不是孤零零的。它得待在一个功能包里。就像你写Python代码,得先建个文件夹一样。

打开终端,执行这几条命令:

cd ~/ros2_ws/src
ros2 pkg create hello_pkg --build-type ament_python --dependencies rclpy

解释一下:

  • hello_pkg 是功能包的名字,你可以随便起
  • --build-type ament_python 告诉系统这是Python包
  • --dependencies rclpy 引入ROS2的Python客户端库

命令跑完后,你会看到 hello_pkg 文件夹里自动生成了不少文件。别慌,咱们只关心 hello_pkg/hello_pkg/ 这个目录,里面有个 __init__.py 文件。待会儿咱们就在这旁边新建一个Python文件。

小提示: 我个人习惯把节点文件直接放在功能包的同名目录下。这样路径清晰,不容易搞混。

4.2 写一个最简节点:Hello World

hello_pkg/hello_pkg/ 目录下,新建一个文件,叫 hello_node.py

然后敲入以下代码:

import rclpy
from rclpy.node import Node

class HelloNode(Node):
    def __init__(self):
        super().__init__('hello_node')
        self.get_logger().info('Hello, ROS2! 节点已启动')

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

if __name__ == '__main__':
    main()

代码不长,但每一行都有讲究。咱们拆开来看:

  • 导入库import rclpyfrom rclpy.node import Node。这是必须的,没有它们,ROS2就认不出你的节点。
  • 定义类class HelloNode(Node)。你的节点本质上是一个类,继承自 Node。所有ROS2的功能,比如发布、订阅、日志,都从这个基类来。
  • 构造函数__init__ 里调用了 super().__init__('hello_node')。这个 'hello_node' 就是节点的名字。注意,名字不能重复,否则会冲突。
  • 日志输出self.get_logger().info(...)。这是ROS2的日志系统,比 print 强多了。它会自动加上时间戳、节点名等信息。
  • 主函数main() 里做了四件事:初始化、创建节点、挂起、清理。顺序不能乱。
重点: rclpy.spin(node) 这行是关键。它让节点一直活着,等待事件发生。没有它,节点执行完 __init__ 就退出了,你啥也看不到。

4.3 配置入口点

代码写好了,但ROS2还不知道怎么启动它。你得告诉系统:这个节点的主函数在哪里。

打开 hello_pkg/setup.py 文件,找到 entry_points 部分,改成这样:

entry_points={
    'console_scripts': [
        'hello_node = hello_pkg.hello_node:main',
    ],
},

格式是固定的:'命令名 = 包名.文件名:函数名'。这里 hello_node 是你在终端里要敲的命令名,后面是路径。

注意: 我曾经在这里吃过亏。冒号前面是 hello_pkg.hello_node,不是 hello_pkg/hello_node。Python里用点号,不是斜杠。写错了编译能过,但运行时会报模块找不到。

4.4 编译并运行

回到工作空间根目录,编译:

cd ~/ros2_ws
colcon build --packages-select hello_pkg

编译成功后,记得 source 一下:

source install/setup.bash

然后运行节点:

ros2 run hello_pkg hello_node

如果一切顺利,你会看到终端里打印出:

[INFO] [1700000000.123456789] [hello_node]: Hello, ROS2! 节点已启动

看到这行输出,恭喜你,第一个ROS2节点跑起来了!

4.5 理解节点生命周期

节点不是一直傻傻地活着。它有明确的生命周期。咱们刚才的代码,其实展示了四个阶段:

阶段 对应代码 说明
初始化 rclpy.init() 启动ROS2通信层,分配全局资源
创建 HelloNode() 实例化节点,注册到ROS2网络中
旋转 rclpy.spin() 进入事件循环,等待并处理消息
销毁 destroy_node() + shutdown() 释放资源,优雅退出

你想想看,为什么要有 spin 这一步?

说白了,ROS2节点本质上是事件驱动的。它得一直监听有没有新消息、有没有定时器触发。如果没有 spin,节点就像个一次性程序,跑完就没了。而 spin 就是那个让节点“活”起来的循环。

避坑指南: 我曾经在写一个传感器驱动节点时,忘了加 spin。结果节点启动后,只打印了一行日志就退出了。我查了半天,最后发现是 spin 没写。嗯,从那以后,我写节点第一件事就是检查 spin 在不在。

4.6 验证节点是否在运行

节点跑起来后,你可以打开另一个终端,用ROS2的命令行工具看看它:

ros2 node list

你会看到输出:

/hello_node

这就是你的节点。前面有个斜杠,表示它是全局唯一的。

还可以查看节点信息:

ros2 node info /hello_node

它会告诉你这个节点有哪些发布者、订阅者、服务等。目前咱们的节点啥都没有,所以列表是空的。别急,后面几章咱们会慢慢给它加功能。

4.7 小结

今天咱们干了三件事:

  1. 创建了一个Python功能包
  2. 写了一个能打印“Hello World”的节点
  3. 理解了节点的四个生命周期阶段

这个节点虽然简单,但它是你ROS2之旅的起点。后面所有的发布、订阅、服务、动作,都建立在这个基础之上。

下一章,咱们会在这个节点的基础上,加上发布者和订阅者,让两个节点真正“聊”起来。到时候你会发现,ROS2的通信机制其实没那么神秘。

课后练习: 试着修改 HelloNode 的构造函数,让它在启动时打印当前时间。提示:可以用 self.get_clock().now() 获取ROS2时间。