0. 简介

计算框架是自动驾驶系统中的重中之重,也是整个系统得以高效稳定运行的基础。为了实时地完成感知、决策和执行,系统需要一系列的模块相互紧密配合,高效地执行任务流。由于各种原因,这些模块可能位于不同进程,也可能位于不同机器。这就要求计算框架中具有灵活的、高性能的通信机制。Apollo在3.5版本中推出了Cyber RT替代了原先的ROS。

和ROS & ROS2中类似,Cyber RT中支持两种数据交换模式:一种是Publish-Subscriber模式,常用于数据流处理中节点间通信。即发布者(Publisher)在channel(ROS中对应地称为topic)上发布消息,订阅该channel的订阅者(Subscriber)便会收到消息数据;另一种就是常见的Service-Client模式,常用于客户端与服务端的请求与响应。Node是整个数据拓扑网络中的基本单元。一个Node中可以创建多个读者/写者,服务端/客户端。读者和写者分别对应Reader和Writer,用于Publish-Subscribe模式。服务端和客户端分别对应Service和Client,用于Service-Client模式。

1. CyberRT BUILD文件

Apollo 是优秀的自动驾驶开发框架,出自百度之手,目前的Apollo是基于Cyber RT通信键实现的。Apollo的具体安装可以看Apollo 6.0 安装完全指南。Apollo (或者说CyberRT)使用 Bazel 进行代码构建,Bazel 是由 Google 开源的一款高效的软件构建工具。使用 Bazel 时,我们需要为每个参与构建的目录创建一个 BUILD 文件来定义一些构建规则,BUILD 文件使用类似 Python 的语法。其优点如下:

  • Bazel 仅重建必要的内容。借助高级的本地和分布式缓存,优化的依赖关系分析和并行执行,可以获得快速而增量的构建。
  • 构建和测试 Java、C++、Android、iOS、和其他各种语言平台。Bazel 可以在 Windows、macOS 和 Linux 上运行。
  • Bazel 帮助你扩展你的组织、代码库和持续集成系统。它可以处理任何规模的代码库。

1.1 Bazel项目结构

在构建项目之前,您需要设置其工作区。工作区是一个保存项目源文件和 Bazel 构建输出的目录。它还包含 Bazel 识别为特殊的文件:

  • 该WORKSPACE文件将目录及其内容标识为 Bazel 工作区并位于项目目录结构的根部,
  • 一个或多个BUILD文件,告诉 Bazel 如何构建项目的不同部分。
    其目录结构大致如下:
project
|-- pkg
|   |-- BUILD
|   |-- src.cc
|-- WORKSPACE

1.2 了解BUILD文件

一个BUILD文件包含多种不同类型的 Bazel 指令。最重要的类型是构建规则,它告诉 Bazel 如何构建所需的输出,例如可执行二进制文件或库。文件中构建规则的每个实例BUILD称为目标,并指向一组特定的源文件和依赖项。一个目标也可以指向其他目标。例如:

cc_binary(
    name = "hello_world",
    srcs = ["hello_world.cc"],
)

在示例中,hello-world目标实例化 Bazel 的内置 cc_binary规则。该规则告诉 Bazel 从源文件构建一个独立的可执行二进制文件。

BUILD文件中常见的两种规则:

  • cc_binary:表示要构建对应文件变成二进制文件。
    • name:表示构建完成后的文件名字。
    • srcs:表示要构建的源文件。
    • deps:表示构建该文件所依赖相关的库。
  • cc_library:表示要构建对应文件变成相关依赖库。
    • hdrs:表示的是源文件对应的头文件路径。
    • package(default_visibility = [“//visibility:public”])这段代码则表示该文件是公开的,能被所有对象找到并依赖。
load("//tools:cpplint.bzl", "cpplint")

package(default_visibility = ["//visibility:public"])

cc_binary(
    name = "libcommon_component_example.so",
    deps = [":common_component_example_lib"],
    linkopts = ["-shared"],
    linkstatic = False,
)

cc_library(
    name = "common_component_example_lib",
    srcs = [
        "common_component_example.cc",
    ],
    hdrs = [
        "common_component_example.h",
    ],
    deps = [
        "//cyber",
        "//cyber/examples/proto:examples_cc_proto",
    ],
)

cpplint()

2. 算法组件创建(内容引用文档)

Apollo Cyber 运行时框架 (Apollo Cyber RT Framework) 是基于组件概念来构建的。每个组件都是 Cyber 框架的一个构建块,它包括一个特定的算法模块, 此算法模块处理一组输入数椐并产生一组输出数椐。要创建并启动一个算法组件,需要通过以下 4 个步骤:

2.1 初如化组件的文件结构

例如组件的根目录为/apollo/cyber/examples/common_component_example/需要创建以下文件:

Header file: common_component_example.h
Source file: common_component_example.cc
Build file: BUILD
DAG dependency file: common.dag
Launch file: common.launch

2.2 实现组件类

2.2.1 实现组件头文件

如何实现common_component_example.h:

继承 Component
定义自己的 InitProc 函数。Proc 需要指定输入数椐类型。
使用CYBER_REGISTER_COMPONENT宏定义把组件类注册成全局可用。

#include <memory>
#include "cyber/class_loader/class_loader.h"
#include "cyber/component/component.h"
#include "cyber/examples/proto/examples.pb.h"

using apollo::cyber::examples::proto::Driver;
using apollo::cyber::Component;
using apollo::cyber::ComponentBase;

class CommonComponentSample : public Component<Driver, Driver> {
 public:
  bool Init() override;
  bool Proc(const std::shared_ptr<Driver>& msg0,
            const std::shared_ptr<Driver>& msg1) override;
};

CYBER_REGISTER_COMPONENT(CommonComponentSample)
2.2.2 实现组件源文件

对于源文件 common_component_example.cc, InitProc 这两个函数需要实现。

#include "cyber/examples/common_component_example/common_component_example.h"
#include "cyber/class_loader/class_loader.h"
#include "cyber/component/component.h"

bool CommonComponentSample::Init() {
  AINFO << "Commontest component init";
  return true;
}

bool CommonComponentSample::Proc(const std::shared_ptr<Driver>& msg0,
                               const std::shared_ptr<Driver>& msg1) {
  AINFO << "Start common component Proc [" << msg0->msg_id() << "] ["
        << msg1->msg_id() << "]";
  return true;
}

2.3 设置配置文件

2.3.1 配置 DAG 依赖文件

在 DAG 依赖配置文件 (例如 common.dag) 中配置下面的项:

  • Channel names: 输入输出数椐的 Channel 名字
  • Library path: 此组件最终编译出的库的名字
  • Class name: 此组件的入口类的名字
# Define all components in DAG streaming.
component_config {
    component_library : "/apollo/bazel-bin/cyber/examples/common_component_example/libcommon_component_example.so"
    components {
        class_name : "CommonComponentSample"
        config {
            name : "common"
            readers {
                channel: "/apollo/prediction"
            }
            readers {
                channel: "/apollo/test"
            }
        }
    }
}
2.3.2 配置 launch 启动文件

在 launch 启动文件中 (common.launch), 配置下面的项:

  • 组件的名字
  • 上一步创建的 dag 配置的名字。
  • 组件运行时所在的进程目录。
<cyber>
    <component>
        <name>common</name>
        <dag_conf>/apollo/cyber/examples/common_component_example/common.dag</dag_conf>
        <process_name>common</process_name>
    </component>
</cyber>

2.4 启动组件

通过下面的命令来编译组件:

bash /apollo/apollo.sh build

然后配置环境:

cd /apollo/cyber
source setup.bash

有两种方法来启动组件:

  • 使用 launch 文件来启动 (推荐这种方式)
cyber_launch start /apollo/cyber/examples/common_component_example/common.launch
  • 使用 dag 文件来启动
mainboard -d /apollo/cyber/examples/common_component_example/common.dag

3. 订阅发布与服务客户

3.1 节点 Node

Node 是整个数据拓扑网络中的基本单元。Node 对象会根据需要创建和管理 Writer,Reader,Service 和 Client 对象。Reader 和 Writer,用于发布—订阅模式。Service 和 Client,用于服务—客户模式。

在cyberRT中创建方法如下:

std::unique_ptr<Node> apollo::cyber::CreateNode(const std::string& node_name, const std::string& name_space = "");

参数:

  • node_name:node 的名称,必须保证全局是唯一的,不能重名
  • name_space:node 所在的命名空间。主要用来防止node_name发生冲突。
  • name_space默认为空,加入命名空间后,node的name 变成 /namespace/node_name
  • return value:node的独占智能指针

在cyber::Init()还未执行前,系统还处于未初始化状态,没法创建node,会直接返回nullptr

3.2 发布者

writer 是CyberRT中,用来发布消息最基本的方法。每个writer对应一个特定消息类型的channel。writer可以通过Node类中的 CreateWriter接口创建。具体如下:

template <typename MessageT>
   auto CreateWriter(const std::string& channel_name)
       -> std::shared_ptr<Writer<MessageT>>;
template <typename MessageT>
   auto CreateWriter(const proto::RoleAttributes& role_attr)
       -> std::shared_ptr<Writer<MessageT>>;

参数:

  • channel_name:字面意思,就是该writer对应的channel_name
  • MessageT:写入的消息类型
  • return value:std::shared_ptr<writer></writer

3.3 订阅者

reader是接收消息最基础的方法。reader必须绑定一个回调函数。当channel中的消息到达后,回调会被调用。
reader 可以通过Node类中 CreateReader接口创建。具体如下:

   template <typename MessageT>
   auto CreateReader(const std::string& channel_name, const std::function<void(const std::shared_ptr<MessageT>&)>& reader_func)
       -> std::shared_ptr<Reader<MessageT>>;

   template <typename MessageT>
   auto CreateReader(const ReaderConfig& config,
                     const CallbackFunc<MessageT>& reader_func = nullptr)
       -> std::shared_ptr<cyber::Reader<MessageT>>;

   template <typename MessageT>
   auto CreateReader(const proto::RoleAttributes& role_attr,
                     const CallbackFunc<MessageT>& reader_func = nullptr)
   -> std::shared_ptr<cyber::Reader<MessageT>>;

参数:

  • channel_name:字面意思,就是该reader对应的channel_name
  • MessageT:读取的消息类型
  • reader_func:回调函数
  • return value:std::shared_ptr<reader></reader

3.4 服务端&客户端

除 Reader/Writer 外,Cyber RT 还提供了用于模块通信的 Service/Client 模式。它支持节点之间的双向通信。当对服务发出请求时,客户端节点将收到响应。

// filename: cyber/examples/service.cc
#include "cyber/cyber.h"
#include "cyber/examples/proto/examples.pb.h"

using apollo::cyber::examples::proto::Driver;

int main(int argc, char* argv[]) {
  apollo::cyber::Init(argv[0]);
  std::shared_ptr<apollo::cyber::Node> node(
      apollo::cyber::CreateNode("start_node"));
  auto server = node->CreateService<Driver, Driver>(
      "test_server", [](const std::shared_ptr<Driver>& request,
                        std::shared_ptr<Driver>& response) {
        AINFO << "server: I am driver server";
        static uint64_t id = 0;
        ++id;
        response->set_msg_id(id);
        response->set_timestamp(0);
      });
  auto client = node->CreateClient<Driver, Driver>("test_server");
  auto driver_msg = std::make_shared<Driver>();
  driver_msg->set_msg_id(0);
  driver_msg->set_timestamp(0);
  while (apollo::cyber::OK()) {
    auto res = client->SendRequest(driver_msg);
    if (res != nullptr) {
      AINFO << "client: response: " << res->ShortDebugString();
    } else {
      AINFO << "client: service may not ready.";
    }
    sleep(1);
  }

  apollo::cyber::WaitForShutdown();
  return 0;
}

3.5 参数服务

参数服务被用于节点之间共享的数据,并提供了诸如基本操作setgetlist。参数服务基于Service实现,并包含服务(service)和客户端(client)。

通过 cyber 传递的所有参数都是apollo::cyber::Parameter对象,下表列出了5种受支持的参数类型。

参数类型 C++数据类型 protobuf数据类型
apollo::cyber::proto::ParamType::INT int64_t int64
apollo::cyber::proto::ParamType::DOUBLE double double
apollo::cyber::proto::ParamType::BOOL bool bool
apollo::cyber::proto::ParamType::STRING std::string string
apollo::cyber::proto::ParamType::PROTOBUF std::string string
apollo::cyber::proto::ParamType::NOT_SET - -

除了上述5种类型外,Parameter还支持使用protobuf对象作为传入参数的接口。执行序列化后处理该对象,并将其转换为STRING类型以进行传输。具体示例:

#include "cyber/cyber.h"
#include "cyber/parameter/parameter_client.h"
#include "cyber/parameter/parameter_server.h"

using apollo::cyber::Parameter;
using apollo::cyber::ParameterServer;
using apollo::cyber::ParameterClient;

int main(int argc, char** argv) {
  apollo::cyber::Init(*argv);
  std::shared_ptr<apollo::cyber::Node> node =
      apollo::cyber::CreateNode("parameter");
  auto param_server = std::make_shared<ParameterServer>(node);
  auto param_client = std::make_shared<ParameterClient>(node, "parameter");
  param_server->SetParameter(Parameter("int", 1));
  Parameter parameter;
  param_server->GetParameter("int", &parameter);
  AINFO << "int: " << parameter.AsInt64();
  param_client->SetParameter(Parameter("string", "test"));
  param_client->GetParameter("string", &parameter);
  AINFO << "string: " << parameter.AsString();
  param_client->GetParameter("int", &parameter);
  AINFO << "int: " << parameter.AsInt64();
  return 0;
}

3.6 Log API(日志)

3.6.1 日志库

cyber 日志库建立在glog之上。需要包括以下头文件:

#include "cyber/common/log.h"
#include "cyber/init.h"
3.6.2 日志配置

默认全局配置路径:cyber / setup.bash

以下配置可以由devloper修改:

export GLOG_log_dir=/apollo/data/log
export GLOG_alsologtostderr=0
export GLOG_colorlogtostderr=1
export GLOG_minloglevel=0
3.6.3 登录初始化

在代码条目处调用Init方法以初始化日志:

apollo::cyber::cyber::Init(argv[0]) is initialized.
If no macro definition is made in the previous component, the corresponding log is printed to the binary log.
3.6.4 日志输出宏

日志库封装在日志打印宏中。相关的日志宏的用法如下:

ADEBUG << "hello cyber.";
AINFO  << "hello cyber.";
AWARN  << "hello cyber.";
AERROR << "hello cyber.";
AFATAL << "hello cyber.";

与默认glog唯一不同的输出行为是模块的不同日志级别将被写入同一日志文件。

4. 使用实例

proto(cyber/examples/proto/examples.proto)消息类型

syntax = "proto2";

package apollo.cyber.examples.proto;

message SamplesTest1 {
    optional string class_name = 1;
    optional string case_name = 2;
};

message Chatter {
    optional uint64 timestamp = 1;
    optional uint64 lidar_timestamp = 2;
    optional uint64 seq = 3;
    optional bytes content = 4;
};

message Driver {
    optional string content = 1;
    optional uint64 msg_id = 2;
    optional uint64 timestamp = 3;
};

Talker (cyber/examples/talker.cc)

#include "cyber/cyber.h"
#include "cyber/proto/chatter.pb.h"
#include "cyber/time/rate.h"
#include "cyber/time/time.h"
using apollo::cyber::Rate;
using apollo::cyber::Time;
using apollo::cyber::proto::Chatter;
int main(int argc, char *argv[]) {
  // cyber初始化
  apollo::cyber::Init(argv[0]);
  // 创建 node
  std::shared_ptr<apollo::cyber::Node> talker_node(apollo::cyber::CreateNode("talker"));
  // 创建发布者
  auto talker = talker_node->CreateWriter<Chatter>("channel/chatter");
  // 发布频率
  Rate rate(1.0);
  while (apollo::cyber::OK()) {
    static uint64_t seq = 0;
    //构造消息
    auto msg = std::make_shared<apollo::cyber::proto::Chatter>();
    msg->set_timestamp(Time::Now().ToNanosecond());
    msg->set_lidar_timestamp(Time::Now().ToNanosecond());
    msg->set_seq(seq++);
    msg->set_content("Hello, apollo!");
    //发布消息
    talker->Write(msg);
    AINFO << "talker sent a message!";
    //延时等待
    rate.Sleep();
  }
  return 0;
}

Listener (cyber/examples/listener.cc)

#include "cyber/cyber.h"
#include "cyber/proto/chatter.pb.h"
//回调函数
void MessageCallback(
    const std::shared_ptr<apollo::cyber::proto::Chatter>& msg) {
  AINFO << "Received message seq-> " << msg->seq();
  AINFO << "msgcontent->" << msg->content();
}

int main(int argc, char *argv[]) {
  // 初始化cyber
  apollo::cyber::Init(argv[0]);  
  // 创建node
  auto listener_node = apollo::cyber::CreateNode("listener");
  // 创建 listener
  auto listener =listener_node->CreateReader<apollo::cyber::proto::Chatter>("channel/chatter", MessageCallback);
  //阻塞
  apollo::cyber::WaitForShutdown();
  return 0;
}

Bazel BUILD file(cyber/samples/BUILD)——编译会生成二进制的可执行文件

# 编译生成bin文件
cc_binary(
    name = "talker",
    srcs = [ "talker.cc", ],
    deps = [
        "//cyber",
        "//cyber/examples/proto:examples_cc_proto",
    ],
)

cc_binary(
    name = "listener",
    srcs = [ "listener.cc", ],
    deps = [
        "//cyber",
        "//cyber/examples/proto:examples_cc_proto",
    ],
)

Bazel BUILD file(cyber/samples/proto/BUILD)——消息类型也必须编译生成c++可用的库文件,上式中均调用了该库

package(default_visibility = ["//visibility:public"])

cc_proto_library(
    name = "examples_cc_proto",
    deps = [
        ":examples_proto",
    ],
)

proto_library(
    name = "examples_proto",
    srcs = [
        "examples.proto",
    ],
)

5. 常用软件

5.1 Cyber_visualizer

cyber_visualizer 是用于在 Apollo Cyber RT 中显示通道数据的可视化工具。

source /your-path-to-apollo-install-dir/cyber/setup.bash

cyber_visualizer

启动cyber_visualizer后,会看到如下界面

当数据流经Apollo Cyber RT 中的通道时,所有通道的列表ChannelNames如下图所示。例如,可以使用Apollo Cyber RT的记录工具(cyber_recorder)从另一个终端重放数据,然后cyber_visualizer将接收所有活动通道的信息(来自重放数据)并显示出来。

通过单击工具栏中的选项,可以启用参考网格、显示点云、添加图像或同时显示多个相机的数据。如果Show Grid启用了选项,可以通过双击下面列表中的Color项目来设置网格的颜色,默认颜色为灰色。还可以编辑的值以调整网格中的单元格数量。对于点云或图像,可以通过其子项选择源通道,子项可以播放或停止来自相应通道的数据。如下图,按钮部分的三个摄像机通道数据和顶部的一个点云通道数据同时显示。

要在 3D 点云场景中调整虚拟摄像机,可以在点云显示部分右键单击。将弹出一个对话框,如下图所示:

点云场景支持两种类型的相机:自由和目标。(从上面的弹出对话框中选择类型)

  • 自由型相机:对于点云场景中的这种类型的相机,可以通过按住鼠标左键或右键并移动它来更改相机的姿势。要更改相机的间距,可以滚动鼠标滚轮。

  • 目标类型相机:对于点云场景中的这种类型的相机,要改变相机的视角,可以按住鼠标左键再移动。要更改相机到观察点的距离(默认观察点为坐标系原点 (0,0,0)),可以滚动鼠标滚轮。

也可以直接在对话框中修改相机信息,改变相机在点云场景中的观察状态。“Step”项是对话框中的步长值。

将鼠标放在摄像机通道的图像上,可以双击左键在左侧菜单栏突出显示对应的数据通道。右击图像,弹出删除摄像机通道的菜单。

播放和暂停按钮:单击Play按钮时,将显示所有频道。当单击Pause按钮时,所有通道将停止显示在工具上。

5.2 Cyber_monitor

命令行工具cyber_monitor提供了终端中实时频道信息列表的清晰视图 Apollo Cyber RT。

source /your-path-to-apollo-install-dir/cyber/setup.bash
cyber_monitor

具体指令如下:

  • 获取关于 Cyber_monitor 的帮助
cyber_monitor -h
  • Cyber_monitor 仅监控指定的频道
cyber_monitor -c ChannelName

启动命令行工具后,会注意到它类似于cyber_visualizer。它通过拓扑自动收集所有通道的信息,并在两列(通道名称、通道数据类型)中显示。

通道信息的默认显示为红色。但是,如果有数据流经 a 通道,则该通道对应的行显示为绿色。如下图所示:

5.3 Cyber_recorder

cyber_recorder是 Apollo Cyber RT 提供的录制/播放工具。它提供了许多有用的功能,包括录制录制文件、播放录制文件、拆分录制文件、查看录制文件信息等。

$$ source /your-path-to-apollo-install-dir/cyber/setup.bash
$$ cyber_recorder
usage: cyber_recorder <command>> [<args>]
The cyber_recorder commands are:
    info                               Show information of an exist record.
    play                               Play an exist record.
    record                             Record same topic.
    split                              Split an exist record.
    recover                            Recover an exist record.

6. 参考链接

https://blog.shipengx.com/archives/e4b9c8ad.html

https://blog.csdn.net/jinzhuojun/article/details/108066714

https://apollo.baidu.com/community/article/1093

https://blog.csdn.net/Lo_Bamboo/article/details/105191644

https://blog.csdn.net/ygsdytx/article/details/126342706

https://dingfen.github.io/apollo/2020/11/03/CyberCommu1.html

https://blog.csdn.net/yechen1/article/details/105786340