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节点最基本的运行模式。
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确认节点注册成功。三屏联动,问题无处遁形。