朝朝辞暮i头像
关注

C++ 第 30 课:真正的 ROS2 Node 是什么

解释一下这一段

[this](const Msg::SharedPtr msg)
{
    current_angle = msg->angle;
}

你暂时不要把它当成“一整句代码”,而要看成四块:

[this]        (const Msg::SharedPtr msg)
   ↓                    ↓
我是谁?              消息是什么?

{
    current_angle = msg->angle;
          ↓               ↓
      存到哪里?       从消息里取什么?
}

一、先重新理解 this

假设我们有两个机器人对象:

Robot robot1;
Robot robot2;

每一个对象都有自己的:

current_angle

可以想象成:

robot1
┌──────────────────┐
│ current_angle=10 │
└──────────────────┘

robot2
┌──────────────────┐
│ current_angle=50 │
└──────────────────┘

现在执行:

robot1.update();

进入 robot1 的成员函数时,C++ 需要知道:

“现在到底是在操作 robot1,还是 robot2?”

于是 C++ 内部有一个特殊的指针:

this

你可以把它理解成:

“当前这个对象的地址。”

所以当:

robot1.update();

正在执行时:

this
 │
 ▼
robot1

而如果:

robot2.update();

那么:

this
 │
 ▼
robot2

所以一句最形象的记法:

this = “我自己”

类里面的代码在说:

this
=
现在正在执行这个函数的“我”

二、为什么 [this]?

现在 lambda 是:

[this]()
{
}

lambda 就像我们临时雇了一个“小助手”。

问题来了:

这个小助手怎么知道它是在为哪个 Robot 对象工作?

所以我们把:

this

交给它。

[this]

可以形象理解成:

“小助手,你把‘当前对象是谁’这个地址带进去。”

于是 lambda 里面就知道:

我现在服务的是 robot1

或者:

我现在服务的是 robot2

所以现阶段你看到:

[this]

就直接翻译成:

这个 lambda 允许操作当前对象自己的成员。

比如:

[this]()
{
    current_angle = 30;
}

如果当前对象是 robot1,实际上就是:

robot1.current_angle = 30

三、再重新理解 ->

这部分其实和你之前学的指针完全一样。

假设有一个普通对象:

Robot robot;

直接访问:

robot.angle;

因为:

你手里直接拿着对象。

所以使用:

.

但是如果你手里不是对象,而是:

对象的地址

例如:

Robot* p = &robot;

可以画成:

p
┌──────────────┐
│ robot的地址   │
└───────┬──────┘
        │
        ▼
      robot
┌────────────────┐
│ angle = 30     │
│ speed = 10     │
└────────────────┘

现在想访问 angle:

p->angle;

-> 可以非常形象地理解成:

顺着这个地址箭头找到对象,然后拿它里面的东西。

所以:

p->angle

读成人话:

顺着 p 指向的地址找到 Robot,然后取 Robot 里的 angle。

因此这个箭头符号其实画得非常形象:

p ---------> Robot对象
              │
              └── angle

四、. 和 -> 最形象区别

假设 Robot 是一间房。

情况 1:你就在房子里

robot.angle

相当于:

“robot 房间里的 angle。”

所以直接:

.

情况 2:你只有房子的地址

Robot* p = &robot;

你必须:

地址
↓
找到房子
↓
进去找 angle

所以:

p->angle

也就是:

沿着地址指过去,再访问成员。

记住:

对象本体        .
对象的地址/指针 ->

五、那 msg->angle 是什么?

现在回到:

msg->angle

因为:

msg

不是普通 Msg 对象,而是:

Msg::SharedPtr

它本质上是一个:

管理消息对象的智能指针。

你可以先画成:

msg
┌───────────────────┐
│ 消息对象的管理/地址 │
└─────────┬─────────┘
          │
          ▼
      Msg消息对象
┌────────────────────┐
│ angle = 45.0       │
│ speed = 10.0       │
└────────────────────┘

所以:

msg->angle

意思是:

顺着 msg 找到收到的消息对象,然后取里面的 angle。

如果消息是:

angle = 45

那么:

msg->angle

得到:

45

六、现在看完整一行

current_angle = msg->angle;

假设收到:

Msg
┌──────────────┐
│ angle = 45   │
└──────────────┘

那么:

msg->angle

先得到:

45

然后:

current_angle = 45;

把这个值保存到当前对象自己的成员变量里。


七、为什么这里需要 [this]?

因为:

current_angle

通常是类里面的成员变量。

例如:

class RobotController
{
private:
    double current_angle = 0.0;
};

lambda 想修改:

current_angle

本质是在修改:

this->current_angle

所以需要知道:

“当前是哪一个 RobotController 对象?”

这就是:

[this]

的作用。


八、整个过程画出来

假设:

RobotController controller;

ROS2 收到一条消息:

Msg消息
angle = 45

lambda:

[this](const Msg::SharedPtr msg)
{
    current_angle = msg->angle;
}

执行过程:

ROS2收到消息
      │
      ▼
msg 智能指针
      │
      ▼
┌──────────────────┐
│ Msg              │
│ angle = 45       │
└──────────────────┘
      │
      │ msg->angle
      ▼
     45
      │
      ▼
[this]
知道当前对象是 controller
      │
      ▼
controller.current_angle = 45

这就是整段代码干的事情。


九、你可以直接翻译成人话

以后看到:

[this](const Msg::SharedPtr msg)
{
    current_angle = msg->angle;
}

不要逐符号硬背。

直接读:

“收到一条消息 msg 后,这个小函数可以访问当前对象,然后从消息中读取 angle,保存到当前对象的 current_angle。”

这样就够了。


十、再给你一个更生活化的例子

假设你是仓库管理员。

你自己:

this
=
当前这个仓库管理员

快递员送来一个包裹:

msg
=
包裹地址/包裹引用

包裹里面有:

angle = 45

你做:

msg->angle

相当于:

顺着包裹信息找到包裹,打开,拿出 angle。

然后:

current_angle = msg->angle;

就是:

把包裹里的角度,抄到自己手里的“当前角度记录表”上。

而:

[this]

就是告诉这个临时小助手:

“你操作的是我这个管理员自己的记录表。”


十一、再补一个非常重要的对比

普通对象:

Msg msg;

msg.angle;

因为 msg 本身就是对象。

使用:

.

智能指针:

Msg::SharedPtr msg;

使用:

msg->angle;

因为 msg 是指向/管理 Msg 对象的智能指针。

所以以后判断用哪个,先问自己:

左边这个变量,是对象本身,还是指针?


上一课 Callback 标准答案

第 1 题

普通函数:

hello();

是:

我们自己的代码主动调用。


第 2 题

回调函数最大的特点:

先把函数交给系统,等未来某个事件发生时,由系统调用它。


第 3 题

subscriber 订阅:

/joint_states

那么 callback 一般在:

新的 /joint_states 消息到达时执行。


第 4 题

lambda 很适合 callback,因为:

可以直接在需要注册回调的位置写一个短函数,不需要额外单独定义函数,而且还能通过 [this] 等方式访问当前对象的数据。


第 5 题

VLA 发布新动作后,控制节点 callback 最可能负责:

接收动作消息
↓
读取目标动作
↓
转换/检查
↓
更新机械臂控制目标
↓
进一步发给控制器或仿真

第 30 课:ROS2 Node 与继承

现在正式开始看一句真正非常典型的 ROS2 C++:

class MyNode : public rclcpp::Node

第一次看主要有三个陌生点:

MyNode
:
public rclcpp::Node

我们一个一个拆。


1. 先不用 ROS2,理解“继承”

假设我们有一个基础机器人:

class Robot
{
public:
    void stop()
    {
        std::cout << "Stop" << std::endl;
    }
};

现在我们想设计:

CleaningRobot

扫地机器人。

它本质上:

也是 Robot。

所以它应该自动拥有:

stop()

同时再增加:

clean()

可以写:

class CleaningRobot : public Robot
{
public:
    void clean()
    {
        std::cout << "Cleaning" << std::endl;
    }
};

2. : 在这里是什么意思?

注意,这里的:

:

不是初始化列表那个冒号的具体用途。

这里出现在:

class CleaningRobot : public Robot

表示:

CleaningRobot 继承自 Robot。

可以画成:

Robot
│
│ 原本有 stop()
│
▼
CleaningRobot
├── stop()     ← 继承来的
└── clean()    ← 自己新增的

3. “继承”最形象怎么理解?

可以理解成:

在已有机器的基础上继续改装。

比如:

Robot
=
基础底盘

它已经有:

stop
power
move

现在:

CleaningRobot

不是从零重新造一台。

而是:

把 Robot 的能力拿过来,再加清扫功能。

这就是 inheritance。


4. 为什么叫父类 / 子类?

例如:

class Robot

叫:

基类 / 父类。

而:

class CleaningRobot : public Robot

叫:

派生类 / 子类。

可以画:

        Robot
        父类
          │
          ▼
   CleaningRobot
        子类

子类会继承父类的一部分能力。


5. public Robot 是什么意思?

class CleaningRobot : public Robot

这里:

public

表示一种:

公有继承。

现在先不要深入 C++ 各种继承权限。

对 ROS2 现阶段直接理解成:

CleaningRobot 公开继承 Robot,保留“它是一种 Robot”的关系。

就够了。


6. 使用起来是什么样?

CleaningRobot robot;

然后:

robot.stop();

虽然 CleaningRobot 里面没有重新写 stop(),

但它从:

Robot

继承到了。

同时:

robot.clean();

也能调用自己新增的功能。


7. 现在回到 ROS2

典型写法:

class MyNode : public rclcpp::Node

翻译:

定义一个叫 MyNode 的类,它继承 ROS2 提供的 rclcpp::Node。

也就是说:

rclcpp::Node
=
ROS2已经给你造好的“基础节点”

里面已经包含很多 ROS2 能力,比如:

创建 publisher
创建 subscriber
创建 timer
参数
日志
节点名称

我们不想自己重新写。

于是:

MyNode
=
在 ROS2 Node 基础上继续扩展

8. 为什么 ROS2 不让我们从零写 Node?

因为一个 ROS2 节点需要很多基础功能。

如果从零开始,你可能要自己处理:

ROS通信
节点注册
消息系统
定时器
日志
参数
执行器
……

太复杂。

所以 ROS2 已经给你:

rclcpp::Node

你只需要继承它。

相当于:

ROS2 把“通用底盘”造好了,你只负责安装自己的设备。


9. VLA 控制节点例子

比如以后你想写:

VLAControlNode

负责:

接收 VLA Action
↓
转换成机器人目标
↓
发布控制命令

那么:

class VLAControlNode : public rclcpp::Node
{
};

表示:

创建一种 ROS2 节点,名字叫 VLAControlNode。

它本身拥有 ROS2 Node 的基础能力,同时我们往里面增加 VLA 逻辑。


10. 第一个 ROS2 节点骨架

你以后会看到:

class MyNode : public rclcpp::Node
{
public:
    MyNode()
        : Node("my_node")
    {
    }
};

这里现在先拆第一层。

class MyNode

定义自己的类。


: public rclcpp::Node

继承 ROS2 Node。


MyNode()

是:

MyNode 的构造函数。

你已经学过。


: Node("my_node")

这是:

调用父类 Node 的构造函数,把节点名字设置成 "my_node"。


11. 这一句为什么又有 :

完整:

MyNode()
    : Node("my_node")
{
}

这个 : 是你之前学过的:

构造函数初始化列表。

也就是:

创建 MyNode
↓
先初始化它的父类 Node
↓
给 Node 名字 "my_node"

12. 两个冒号别混

你现在已经碰到两种 :。

类定义后面的:

class MyNode : public rclcpp::Node

表示:

继承。


构造函数后面的:

MyNode()
    : Node("my_node")

表示:

初始化列表。

位置不同,意义不同。


13. 整个 Node 骨架翻译

class MyNode : public rclcpp::Node
{
public:
    MyNode()
        : Node("my_node")
    {
    }
};

翻译成人话:

“我要定义一种自己的 ROS2 节点 MyNode。它继承 ROS2 提供的 Node。当创建这个节点对象时,把它在 ROS2 系统里的名字初始化为 my_node。”

这就是你以后几乎每个 ROS2 C++ 节点的起点。


14. 和 VLA 联系起来

以后可能写:

class VLAController : public rclcpp::Node
{
public:
    VLAController()
        : Node("vla_controller")
    {
    }
};

含义:

这是一个 ROS2 Node
名字叫 vla_controller
以后里面可以:
├── subscribe VLA action
├── subscribe joint_states
├── publish joint commands
└── 做动作转换

这已经开始真正进入你的目标了。


本课小练习

第 1 题

class Dog : public Animal

这里谁是父类?

谁是子类?


第 2 题

一句话解释:

class MyNode : public rclcpp::Node

第 3 题

下面:

MyNode()
    : Node("my_node")
{
}

MyNode() 是什么?


第 4 题

Node("my_node")

在这里大概是在做什么?


第 5 题

为什么 ROS2 C++ 节点常常要继承:

rclcpp::Node

而不是我们从零自己写通信功能?

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_56657939/article/details/167039143

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--