第二章:Cyber RT核心机制详解
各位同学,今天我们来聊聊Cyber RT的核心机制。说实话,我刚接触Apollo的时候,也被Cyber RT这套东西搞得有点懵。但用久了你会发现,它其实就是一套消息通信框架,类似ROS,但比ROS更轻量、更高效。
这一章,我会从四个维度带你吃透Cyber RT:Component机制与生命周期、Dag文件配置、Launch文件启动、消息定义与Proto文件。每个点我都会结合项目中的实际踩坑经历来讲。
2.1 Component机制与生命周期
Component是Cyber RT里最基本的执行单元。说白了,它就是你的算法模块的载体。你写的感知、规划、控制代码,最终都要包装成一个Component跑起来。
我刚开始写Component时,总搞不清楚它什么时候初始化、什么时候运行。后来看了源码才明白,Component的生命周期其实就三个阶段:
| 阶段 | 方法 | 说明 |
|---|---|---|
| 初始化 | Init() |
只调用一次,用于加载配置、创建对象 |
| 运行 | Proc() |
每次收到消息时触发,核心处理逻辑 |
| 销毁 | 析构函数 | 模块退出时清理资源 |
关键点:Init()里千万别做耗时操作。我有个同事在Init里加载了一个100MB的模型文件,结果整个系统启动慢了5秒。后来我建议他把模型加载放到后台线程里,问题就解决了。
写一个Component其实很简单。你只需要继承cyber::Component模板类,然后实现Init和Proc两个方法。举个例子:
// 头文件:my_component.h
#include "cyber/component/component.h"
class MyComponent : public apollo::cyber::Component<SomeMessage> {
public:
bool Init() override {
// 加载参数、初始化资源
AINFO << "MyComponent initialized";
return true;
}
bool Proc(const std::shared_ptr<SomeMessage>& msg) override {
// 处理消息
AINFO << "Received message: " << msg->content();
return true;
}
};
// 注册组件
CYBER_REGISTER_COMPONENT(MyComponent)
嗯,这里要注意:CYBER_REGISTER_COMPONENT这个宏必须写在cpp文件里,不能写在头文件。我曾经犯过这个错,编译没问题,但运行时就是找不到组件,排查了半天。
2.2 Dag文件配置
Dag文件是Cyber RT的"接线图"。它告诉系统:你的Component要订阅哪些话题、发布哪些话题、跑在哪个线程上。
我个人习惯把Dag文件放在conf/目录下,和Launch文件分开管理。一个典型的Dag文件长这样:
# my_component.dag
# 定义模块
module_config {
module_library : "/apollo/bazel-bin/modules/my_module/libmy_component.so"
components {
class_name : "MyComponent"
config {
name : "my_component"
readers {
channel: "/sensor/camera/image"
}
writers {
channel: "/perception/objects"
}
}
}
}
这里有几个坑,我一个个说:
- module_library路径:必须写绝对路径或相对于Cyber工作目录的路径。我建议用绝对路径,省得找半天。
- readers和writers:readers是订阅的话题,writers是发布的话题。注意,一个Component可以有多个reader,但一般只有一个writer。
- class_name:必须和
CYBER_REGISTER_COMPONENT里注册的名字一致,大小写敏感。
避坑指南:我曾经把class_name写成了"my_component"(小写),结果Cyber RT死活找不到这个类。后来查文档才发现,class_name必须和注册时的名字完全一致,包括大小写。
2.3 Launch文件启动
Launch文件是Cyber RT的"启动脚本"。它负责启动一个或多个Dag文件,还可以设置全局参数。
你想想看,如果每次启动都要手动敲一堆命令,那多麻烦。Launch文件就是帮你省事的:
# my_module.launch
<cyber>
<module>
<name>my_module</name>
<dag_conf>/apollo/modules/my_module/conf/my_component.dag</dag_conf>
<process_name>my_module</process_name>
</module>
</cyber>
启动命令也很简单:
mainboard -d /apollo/modules/my_module/conf/my_component.dag
或者用Launch文件:
cyber_launch start my_module.launch
我个人更喜欢用Launch文件,因为可以同时启动多个模块。比如启动感知模块时,我需要同时启动相机驱动、感知算法、可视化工具,一个Launch文件全搞定。
小技巧:调试时可以用cyber_launch status查看所有模块的运行状态。如果某个模块没起来,它会显示"DEAD",方便你快速定位问题。
2.4 消息定义与Proto文件
Cyber RT的消息格式用的是Google的Protobuf。说白了,就是定义一种结构化的数据格式,让不同模块之间能互相理解。
我记得第一次写Proto文件时,被那些字段编号搞得一头雾水。后来才明白,字段编号是Protobuf的序列化标识,不能重复,而且一旦定义好就别改了,否则会导致新旧版本不兼容。
一个典型的Proto文件长这样:
syntax = "proto2";
package apollo.my_module;
message MyMessage {
optional int32 id = 1;
optional string name = 2;
optional double timestamp = 3;
// 嵌套消息
message Point {
optional float x = 1;
optional float y = 2;
optional float z = 3;
}
repeated Point points = 4;
}
这里有几个要点:
- syntax:Cyber RT用的是proto2语法,别写成proto3,否则有些特性用不了。
- package:建议和你的模块路径一致,避免命名冲突。
- 字段编号:1-15用1个字节编码,16-2047用2个字节。所以频繁使用的字段用小编号,能省带宽。
- repeated:相当于数组,可以存多个值。
经验之谈:定义消息时,尽量把timestamp字段放在前面。我在做多传感器融合时,经常需要对齐时间戳,如果timestamp在消息体后面,解析起来会多一次内存跳转,影响性能。
定义好Proto文件后,用protoc编译生成C++代码:
protoc --cpp_out=. my_message.proto
然后就可以在Component里使用了:
#include "my_message.pb.h"
// 创建消息
auto msg = std::make_shared<MyMessage>();
msg->set_id(1);
msg->set_name("test");
msg->set_timestamp(cyber::Time::Now().ToSecond());
// 添加点
auto point = msg->add_points();
point->set_x(1.0);
point->set_y(2.0);
point->set_z(3.0);
知识体系总览
说了这么多,我画了一张图帮你梳理一下整个知识体系:
好了,这一章的内容就到这里。Component机制、Dag文件、Launch文件、Proto文件,这四个东西是Cyber RT的基石。你只要把它们搞懂了,后面写模块、做联调就会顺手很多。
可可精品资料库