3. 第一个ROS2节点:用Python写一个发布者

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

前面聊了那么多概念,什么节点、话题、通信,其实说白了,ROS2里最核心的单元就是节点。你写一个节点,它就能干活。今天我们就来写第一个节点——一个简单的发布者。

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

在写代码之前,得先有个“家”。ROS2里,代码都放在功能包里。我个人习惯用命令行创建,干净利落。

ros2 pkg create --build-type ament_python my_first_pkg

这条命令会生成一个叫 my_first_pkg 的Python功能包。里面有个 my_first_pkg 文件夹,我们的代码就放那。

小提示: 包名别用中文,也别用横杠。ROS2对命名有要求,用下划线最安全。

3.2 写一个最简单的发布者

在 my_first_pkg/my_first_pkg/ 目录下,新建一个文件,叫 talker.py。

嗯,这里要注意:ROS2的节点,本质上就是一个Python类。我们继承 Node 类,然后重写它的初始化方法。

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, 'chatter', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)
        self.get_logger().info('Talker节点已启动!')

    def timer_callback(self):
        msg = String()
        msg.data = '你好,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()

代码不长,但每个部分都有讲究。我拆开来讲。

3.3 理解节点初始化

你看 __init__ 方法里,第一行是 super().__init__('talker')。这行干了什么?

说白了,就是给这个节点起个名字,叫 talker。ROS2里每个节点必须有唯一的名字。我在项目中遇到过,两个节点同名,结果后启动的那个直接报错退出了。所以名字别乱起。

接着是创建发布者:

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

这里三个参数:

  • 消息类型:String,来自 std_msgs.msg。ROS2里消息类型很多,字符串是最基础的。
  • 话题名:chatter。订阅者也得用这个名字才能收到。
  • 队列大小:10。如果订阅者处理不过来,消息会暂存10条。再多就丢了。
核心理解: 创建发布者,就是告诉ROS2:“我要往这个话题发消息了,你们谁想听谁听。”

然后是一个定时器:

self.timer = self.create_timer(1.0, self.timer_callback)

定时器每隔1秒调用一次 timer_callback。为什么用定时器?因为节点不能自己一直循环,得靠ROS2的事件循环来驱动。你想想看,如果自己写个 while True,那整个节点就卡死了,其他回调都进不来。

3.4 spin机制:让节点活起来

看 main 函数里最关键的一行:

rclpy.spin(node)

这个 spin 是什么意思?

我刚开始学的时候也困惑。其实 spin 就是“旋转等待”。它会让节点一直运行,监听各种事件——定时器到了、收到消息了、服务请求来了等等。一旦有事件,它就调用对应的回调函数。

没有 spin,节点就是个空壳,啥也不干。有了 spin,节点才真正“活”过来。

注意: spin 是阻塞的。它后面的代码要等节点关闭才会执行。所以一般把 spin 放在最后。

我曾经犯过一个错:在 spin 后面又写了些逻辑,结果发现永远跑不到。后来才明白,spin 不结束,后面的代码就没机会。

3.5 运行看看效果

写完了怎么跑?先编译:

colcon build --packages-select my_first_pkg
source install/setup.bash

然后运行:

ros2 run my_first_pkg talker

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

[INFO] [talker]: Talker节点已启动!
[INFO] [talker]: 发布了:你好,ROS2!
[INFO] [talker]: 发布了:你好,ROS2!
...

如果想看话题数据,另开一个终端:

ros2 topic echo /chatter

你会看到同样的消息。这说明发布者正常工作。

3.6 核心逻辑流程图

下面这张图,把整个流程串起来了。你看一遍,应该就全明白了。

ROS2 发布者节点运行流程 rclpy.init() 初始化ROS2通信 TalkerNode() 创建发布者+定时器 rclpy.spin() 进入事件循环 timer_callback() 发布消息到 /chatter 定时器触发 关键点 • 节点名唯一 • 话题名匹配 • 队列大小10 • spin阻塞 • 回调驱动 • 定时器周期 • 消息类型 • 日志输出 • 资源释放

3.7 几个容易踩的坑

写这个节点的时候,有几个地方容易出问题。我列出来,你写的时候注意一下。

问题 表现 原因 解决
节点名重复 启动报错 ROS2不允许同名节点 改个名字
忘记 spin 节点一闪而过 main函数直接结束了 加上 rclpy.spin()
话题名拼错 订阅者收不到 大小写或符号不一致 仔细核对
队列太小 消息丢失 订阅者处理慢 调大队列
我的经验: 调试的时候,多用 ros2 topic list 和 ros2 topic echo 看看话题有没有数据。这是最直接的排查方法。

好了,第一个节点就写完了。你把它跑起来,看到终端里不断打印消息,那种感觉还是挺有成就感的。记住,节点初始化就是搭骨架,spin就是让骨架动起来。这两个概念搞懂了,后面的内容就顺了。