第二章: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);

知识体系总览

说了这么多,我画了一张图帮你梳理一下整个知识体系:

Cyber RT 核心机制知识体系 Cyber RT 核心机制 Component机制 Init() Proc() 析构 Dag文件配置 module_library readers/writers class_name Launch文件启动 cyber_launch start mainboard -d 状态监控 Proto消息定义 字段编号 repeated protoc编译 四个核心机制相互配合,构成Cyber RT的完整运行体系

好了,这一章的内容就到这里。Component机制、Dag文件、Launch文件、Proto文件,这四个东西是Cyber RT的基石。你只要把它们搞懂了,后面写模块、做联调就会顺手很多。

可可精品资料库