4、节点(Node)基础:节点的概念、生命周期,使用Python/C++创建并运行第一个节点

各位同学,欢迎来到第四讲。从这一章开始,我们终于要亲手写代码了。

前面三章我们聊了ROS2的生态、安装和通信概览。说实话,那些都是铺垫。真正让机器人“活”起来的,是节点。你可以把节点想象成一个机器人里的“小员工”——有的负责看路(摄像头节点),有的负责走路(底盘控制节点),有的负责说话(语音节点)。它们各司其职,通过话题、服务这些“消息管道”互相沟通。

我个人习惯把节点叫做“ROS2世界里的最小执行单元”。你写再多代码,如果不包装成一个节点,ROS2就认不出它。今天我们就来彻底搞懂节点是什么,怎么生,怎么死,以及怎么用Python和C++把它跑起来。

4.1 节点的本质:一个会“说话”的进程

先别急着敲键盘。我们得先想清楚一个问题:节点到底是个什么东西?

从操作系统角度看,节点就是一个普通的进程。但它不是普通的进程——它内部嵌入了ROS2的客户端库(rclcpp或rclpy),所以它能注册到ROS2的“大管家”守护进程(daemon)里,跟其他节点交换数据。

我刚开始学ROS1的时候,犯过一个低级错误:以为节点就是Python脚本本身。结果跑起来发现,两个脚本之间根本收不到消息。后来才明白——你得用rclpy.init()初始化,用Node类包装,节点才算“活”了。

核心要点:

  • 节点 = 进程 + ROS2客户端库
  • 每个节点有唯一的名称(不能重名)
  • 节点通过话题、服务、动作等机制与其他节点通信
  • 节点可以发布/订阅话题,提供/调用服务

说白了,节点就是ROS2世界里的一张“身份证”。没有这张证,你的代码再漂亮,也进不了ROS2的圈子。

4.2 节点的生命周期:从出生到消亡

每个节点都有固定的生命周期。我把它总结为四个阶段:

阶段 状态 说明
1. 初始化 未配置 调用rclcpp::init()或rclpy.init(),建立与ROS2后端的连接
2. 构造 未激活 创建节点对象,设置参数、创建发布者/订阅者
3. 旋转 激活 进入spin()循环,持续处理回调、收发消息
4. 销毁 终止 调用rclcpp::shutdown()或Ctrl+C,释放资源

嗯,这里要注意:很多新手以为节点跑起来就完事了。其实spin()才是节点的“心脏”。没有spin,你的回调函数永远不会被触发,消息也收不到。我曾经在项目里调试了半天,发现订阅者就是不打印数据——结果忘了加spin()。这种坑,踩过一次就记住了。

避坑指南:

我曾经在写多节点程序时,把两个节点的spin()写在了同一个线程里。结果第二个节点永远跑不到。记住:每个节点需要独立的spin()线程,或者使用MultiThreadedExecutor。

4.3 用Python创建第一个节点

好,理论说够了。我们来写代码。Python版本是最快上手的,我建议初学者从这里开始。

4.3.1 创建功能包

先创建一个工作空间和功能包。打开终端:

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create my_first_pkg --build-type ament_python --dependencies rclpy std_msgs

这个命令会生成一个Python包,自动依赖rclpy和std_msgs。你想想看,如果手动创建这些文件得多麻烦?ROS2的工具链帮我们省了不少事。

4.3.2 编写节点代码

进入my_first_pkg/my_first_pkg/目录,新建一个文件first_node.py:

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

class MyFirstNode(Node):
    def __init__(self):
        super().__init__('my_first_node')
        self.get_logger().info('节点已启动!')
        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 = MyFirstNode()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

这段代码做了几件事:

  • 继承Node类,定义了一个叫my_first_node的节点
  • 创建了一个发布者,每秒在chatter话题上发一条消息
  • 用spin()让节点一直运行

我个人习惯在__init__里加一句get_logger().info(),这样节点启动时能确认它活了。调试的时候特别有用。

4.3.3 配置入口点

修改setup.py,把entry_points改成:

entry_points={
    'console_scripts': [
        'first_node = my_first_pkg.first_node:main',
    ],
},

然后编译运行:

cd ~/ros2_ws
colcon build
source install/setup.bash
ros2 run my_first_pkg first_node

你应该能看到终端每秒打印一次“发布: Hello ROS2, 第 X 次”。

小技巧:

如果不想每次编译后都source install/setup.bash,可以把这行加到~/.bashrc里。我一般会在工作空间根目录写一个source.sh脚本,一键source所有依赖。

4.4 用C++创建第一个节点

Python版本跑通了?好,我们来看看C++版本。C++的性能更好,适合对实时性要求高的场景——比如底盘控制、激光雷达数据处理。

4.4.1 创建C++功能包

cd ~/ros2_ws/src
ros2 pkg create my_cpp_pkg --build-type ament_cmake --dependencies rclcpp std_msgs

4.4.2 编写节点代码

在my_cpp_pkg/src/下创建first_cpp_node.cpp:

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class MyFirstCppNode : public rclcpp::Node
{
public:
    MyFirstCppNode() : Node("my_first_cpp_node"), counter_(0)
    {
        RCLCPP_INFO(this->get_logger(), "C++节点已启动!");
        publisher_ = this->create_publisher("chatter", 10);
        timer_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&MyFirstCppNode::timer_callback, this));
    }

private:
    void timer_callback()
    {
        auto msg = std_msgs::msg::String();
        msg.data = "Hello from C++ ROS2 node, count: " + std::to_string(counter_++);
        publisher_->publish(msg);
        RCLCPP_INFO(this->get_logger(), "发布: %s", msg.data.c_str());
    }

    rclcpp::Publisher::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
    int counter_;
};

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);
    auto node = std::make_shared();
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

C++版本比Python多了些类型声明和指针操作。但核心逻辑一模一样:初始化、创建节点、发布消息、spin。

4.4.3 修改CMakeLists.txt

在CMakeLists.txt里添加:

add_executable(first_cpp_node src/first_cpp_node.cpp)
ament_target_dependencies(first_cpp_node rclcpp std_msgs)
install(TARGETS first_cpp_node DESTINATION lib/${PROJECT_NAME})

编译运行:

cd ~/ros2_ws
colcon build
source install/setup.bash
ros2 run my_cpp_pkg first_cpp_node

Python vs C++ 节点对比:

维度 Python C++
开发速度 快,无需编译 慢,需要编译
运行性能 中等 高
内存管理 自动GC 手动/智能指针
适用场景 原型验证、工具脚本 实时控制、传感器驱动

4.5 节点核心机制:一张图看懂

说了这么多,我们来画一张图,把节点的内部结构理清楚。

ROS2 节点内部结构图 节点 (Node) 名称: my_node 发布者 (Publisher) 话题: /chatter 订阅者 (Subscriber) 话题: /cmd_vel 定时器 (Timer) 回调队列 发布消息 接收消息 话题 /chatter /cmd_vel /odom 核心流程:定时器触发 → 回调函数执行 → 发布者发送消息 → 话题传递 → 订阅者接收

这张图把节点的核心部件都标出来了。你想想看,一个节点内部有发布者、订阅者、定时器,它们通过回调队列串起来。定时器到点就触发回调,回调里可以发布消息,也可以处理收到的数据。这就是ROS2节点最基本的运行模式。

4.6 节点调试小技巧

最后分享几个我平时调试节点时常用的命令。这些命令能帮你快速确认节点是否正常工作:

  • ros2 node list —— 查看当前所有活跃节点
  • ros2 node info /my_first_node —— 查看某个节点的详细信息(发布者、订阅者、服务等)
  • ros2 topic echo /chatter —— 监听某个话题,看消息内容
  • ros2 topic list —— 列出所有话题

我曾经在调试一个多节点系统时,发现某个节点收不到数据。用ros2 node info一看,原来它订阅的话题名拼写错了——多了一个下划线。这种问题,光看代码很难发现,但用命令行工具一眼就能揪出来。

我的习惯:

每写一个节点,我都会开三个终端:一个跑节点,一个跑ros2 topic echo看消息,一个跑ros2 node list确认节点注册成功。三屏联动,问题无处遁形。