这里之前的问题是我突然把 "ROS2 节点"这个新概念 当成你已经认识的东西直接用了,确实会造成跳跃。
以后遇到这种情况,我会明确告诉你:
这是旧知识,还是新概念;如果是新概念,它和我们已经学过的 C++ 知识是什么关系。
先把这里彻底捋顺。
一、你目前理解的这句话基本正确
代码:
class MyNode : public rclcpp::Node
{
public:
MyNode()
: Node("my_node")
{
}
};
你现在理解成:
MyNode是子类,继承父类Node;然后在MyNode的构造函数中调用父类Node的构造函数,并传入"my_node"这个名字。
完全正确。
如果暂时不管 ROS2,它就是普通的 C++ 继承与构造:
父类:
Node
↑ 继承
子类:
MyNode
创建:
MyNode node;
时:
先构造父类 Node
↓
Node("my_node")
↓
再构造 MyNode 自己的部分
这就是你已经学过的 C++ 知识。
二、"ROS2 节点"是我们之前学过的东西吗?
不是。
这是一个刚加入的 ROS2 新概念。
我们以前学的是:
变量
函数
class
对象
继承
构造函数
指针
这些都是 C++ 概念。
而:
ROS2 Node / ROS2 节点
是 ROS2 提出来的一种软件概念。
所以现在要把两个世界连接起来:
C++世界 ROS2世界
class
对象 ←→ Node节点
继承
构造函数
三、什么叫"ROS2 节点"?
先不看代码。
假设一台机器人里面同时运行很多程序:
程序A:读取摄像头
程序B:跑VLA
程序C:控制机械臂
程序D:读取关节状态
ROS2 不把它们简单称为:
程序 A、程序 B......
而经常把这种独立参与 ROS2 通信的程序单元叫:
Node,节点
例如:
camera_node
负责相机
vla_node
负责VLA推理
robot_controller
负责机械臂控制
所以现在先把 Node 理解成:
ROS2 系统中一个独立工作的程序单元。
它能够:
发消息
收消息
提供服务
接收请求
运行定时任务
四、那 ROS2 Node 是不是"对象"?
这里要分成两个层次。
从 ROS2 概念上讲
Node 是:
ROS2 系统中的一个运行节点。
它是一种软件角色/运行实体。
从我们现在使用的 C++ 实现上讲
ROS2 的 C++ 库提供了:
rclcpp::Node
这个 C++ 类。
我们创建出来的对象,例如:
MyNode node;
就是一个:
C++ 对象
同时,因为 MyNode 继承了:
rclcpp::Node
它也具有:
ROS2 Node 的身份和能力。
所以你可以先这样记:
ROS2里的"节点"
= 一个概念
rclcpp::Node
= ROS2为了在C++里实现这个概念而提供的类
MyNode node;
= 我们真正创建出来的C++对象
同时也是一个ROS2节点
这个区分很重要。
五、用机械设计类比
用机械概念比较容易理解。
假设有一个概念:
"汽车"
这是一个现实概念。
C++ 里面定义:
class Car
{
};
这是:
用代码描述汽车。
然后:
Car my_car;
这是:
真正创建出来的一辆具体"汽车对象"。
对应 ROS2:
ROS2 Node
↓
"节点"这个概念
rclcpp::Node
↓
C++里描述Node的类
MyNode node;
↓
实际创建出来的Node对象
所以我以后如果说:
"创建一个 ROS2 节点"
更严格地说,就是:
创建一个继承
rclcpp::Node的 C++ 对象,让它作为 ROS2 Node 运行。
六、再看你的代码
class MyNode : public rclcpp::Node
【旧知识:class + 继承】
表示:
定义
MyNode类,它继承rclcpp::Node。
其中:
MyNode
是我们自己定义的子类。
rclcpp::Node
是 ROS2 提供的父类。
然后:
MyNode()
【旧知识:构造函数】
这是 MyNode 的构造函数。
然后:
: Node("my_node")
【旧知识:父类构造 + 初始化列表】
表示:
构造
MyNode对象时,先调用父类Node的构造函数。
传入:
"my_node"
七、那 "my_node" 到底给谁?
严格说:
它传给的是父类
Node的构造函数。
也就是说:
Node("my_node")
负责初始化当前 MyNode 对象内部继承来的 Node 部分。
但这个 Node 部分就是:
当前整个对象作为 ROS2 节点所依赖的基础部分。
所以 ROS2 最后会把这个节点识别为:
my_node
八、你可以这样理解整个对象
创建:
MyNode node;
内部粗略画成:
node 这个C++对象
┌──────────────────────────────┐
│ rclcpp::Node 父类部分 │
│ │
│ ROS2节点名 = "my_node" │
│ ROS2通信能力 │
│ Publisher / Subscriber能力 │
├──────────────────────────────┤
│ MyNode 自己新增的部分 │
│ │
│ 我们自己的变量 │
│ 我们自己的函数 │
└──────────────────────────────┘
它不是:
一个父类Node对象
+
另一个MyNode对象
而是:
一个完整的 MyNode 对象,里面包含父类 Node 部分。
九、所以你的那句话我稍微修改一下就非常准确
你说:
"这里的子类 MyNode 继承了父类 Node,之后在构造函数里面为我们的 Node 赋予名字。"
更严谨地改成:
MyNode 继承了父类
rclcpp::Node;创建 MyNode 对象时,通过初始化列表调用父类 Node 的构造函数,把"my_node"作为这个 ROS2 节点的名字传进去。
这就是当前最准确、也最适合你的理解。
十、以后我会加这种提示
例如以后出现:
rclcpp::spin(node);
我会先告诉你:
【旧知识】
node 是一个对象/智能指针
【ROS2新知识】
spin 是 ROS2 的事件循环机制
而不是直接说:
"让节点 spin。"
这样你就不会突然碰到没定义过的概念。
第 32 课:main()、rclcpp::init()、spin()、shutdown()
这一课我们正式解决:
一个 ROS2 节点到底怎么"跑起来"?
先看最小骨架:
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
auto node =
std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
先不要怕,我们只拆四步。
1. main() ------【旧知识】
你已经学过:
int main()
这是:
C++ 程序入口。
程序从这里开始执行。
现在 ROS2 常写:
int main(int argc, char** argv)
这里多出来的:
int argc
char** argv
是:
命令行参数。
暂时不用深入。
你只需要知道:
ROS2 可能需要读取启动程序时附带的参数。
2. rclcpp::init(argc, argv) ------【ROS2新知识】
rclcpp::init(argc, argv);
先把:
rclcpp::
理解成:
ROS2 C++ 库里面的。
所以:
rclcpp::init(...)
就是:
调用 ROS2 C++ 库的初始化功能。
可以理解成:
C++程序刚启动
↓
初始化ROS2环境
↓
之后才能正常使用Node等ROS2功能
3. 创建 Node 对象 ------【旧知识】
auto node =
std::make_shared<MyNode>();
这个我们已经学过。
拆开:
MyNode
我们的类。
std::make_shared<MyNode>()
创建一个 MyNode 对象,并交给 shared_ptr 管理。
auto node
让编译器自动判断类型。
所以:
node
↓
智能指针
↓
指向/管理 MyNode 对象
而这个 MyNode 对象:
同时也是我们的 ROS2 节点对象。
4. spin(node) ------【ROS2新知识,最重要】
rclcpp::spin(node);
先形象理解成:
让 ROS2 开始值班。
为什么?
假设节点有 Subscriber:
等待 /number 消息
如果程序创建完 Node 后马上结束:
创建Node
↓
程序结束
它根本来不及等待任何消息。
所以我们需要:
rclcpp::spin(node);
告诉 ROS2:
"这个 node 继续运行,不要退出;有事件来了就处理。"
5. spin 可以理解成事件值班室
假设 Node 是值班人员。
执行:
rclcpp::spin(node);
以后:
Node开始值班
│
├── 没消息 → 等
│
├── 消息来了 → 调callback
│
├── Timer到时间 → 调timer callback
│
├── Service请求 → 处理
│
└── 继续等
所以:
spin()是为什么 Subscriber 能"一直等消息"的核心原因之一。
6. callback 为什么自动执行?
这就连起来了。
前面我们说:
ROS2 消息到达后自动调用 callback。
但是谁一直在那里等?
答案就是运行中的:
rclcpp::spin(node);
可以粗略画:
spin(node)
│
│ 一直等待事件
▼
新消息到达
│
▼
找到对应Subscriber
│
▼
执行callback
│
▼
处理完成
│
└────────→继续spin
7. shutdown() ------【ROS2新知识】
rclcpp::shutdown();
意思:
关闭 ROS2 运行环境。
正常情况下:
init
↓
运行
↓
spin
↓
准备退出
↓
shutdown
8. 完整执行顺序
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
auto node =
std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
脑子里翻译成:
程序启动
↓
初始化ROS2
↓
创建MyNode对象
↓
让这个Node持续运行、等待事件
↓
退出时关闭ROS2
↓
程序结束
9. 和 VLA 场景联系
以后你的控制节点可能:
启动
↓
创建 VLAController Node
↓
spin
↓
等待 /vla_action
↓
VLA Action来了
↓
callback执行
↓
读取动作
↓
发送给机械臂
↓
继续等待下一条Action
所以:
rclcpp::spin(node);
不是一个无关紧要的语法。
它对应:
机器人程序为什么能够一直在线等待数据并处理事件。
本课小练习
-
MyNode是 C++ 类还是 ROS2 运行时对象?
auto node = std::make_shared<MyNode>();
执行以后,node 是什么?
rclcpp::init(argc, argv);
大概负责什么?
rclcpp::spin(node);
为什么不能简单理解成"什么也不干地卡住"?
- 如果没有
spin(),一个 Subscriber 节点可能出现什么问题?