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.hSource file
: common_component_example.ccBuild file
: BUILDDAG dependency file
: common.dagLaunch file
: common.launch
2.2 实现组件类
2.2.1 实现组件头文件
如何实现common_component_example.h
:
继承 Component
类
定义自己的 Init
和 Proc
函数。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
, Init
和 Proc
这两个函数需要实现。
#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 参数服务
参数服务被用于节点之间共享的数据,并提供了诸如基本操作set
,get
和list
。参数服务基于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", ¶meter);
AINFO << "int: " << parameter.AsInt64();
param_client->SetParameter(Parameter("string", "test"));
param_client->GetParameter("string", ¶meter);
AINFO << "string: " << parameter.AsString();
param_client->GetParameter("int", ¶meter);
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
点击使用CyberRT写第一个代码 - 古月居可查看全文