第二章:Cyber RT通信机制解析与实践
我们想要?直获取?的速度,该需求不需要向发送?返回什么消息,也不需要发送?对消息进?进?步处理。所以我们选择了Listener-Talker通信?式实现该功能。
Listener-Talker通信??主动送消息,??被动接收。如图2-1所示,Listener-Talker通信?先创建了两个Node,分别是Talker Node和 Listener Node。每个Node实例化Writer类和Reader类对Channel进?消息的读写。Writer和Reader通过Topic连接,对同?块共享内存(Channel)进?读写处理。在这?,Talker Node 为了实现其“诉说”的功能,选择实例化Writer,通过Writer来对Channel进?消息写操
作。?istener Node为了实现其“聆听”功能,选择实例化reader类,通过Reader来对channel进?读操作。在这?,通信中?的的数据格式的定义car message 定义在car.proto中。该通信?式适合于持续性通信的应?场景,?如雷达信号,摄像头图像信息这类数据的传输。
我们想要获得??的详细信息,?如?牌这些,但是?不需要?直获得该信息,想要在需要知道这些信息的时候请求?下就好,于是考虑?Server- Client通信实现该功能。如图2-1所示,Server-Client通信可以在客户端发出消息请求时,服务端才进?请求回应,并将客户端所需的数据返回给客户端。该通信模式适合临时的消息传输,适?于不需要持续性发送数据的场景。其传输的数据定义依然在对应的proto?件中。
有?些参数?如该?的最?限速,最多乘客以及是否?动驾驶等需要被各个模块所使?,?如是否?动驾驶这个参数可能同时影响这很多模块,也可能被很多模块运?时所更改。我们希望有?个类似于“全局变量”的?式来存储这些参数,并定义?些?定义参数来进?使?。Cyber中设计了全局参数服务器来实现这个功能,其通信依然基于RTPS协议,如图2-8所示。该通信?式服务端和客户端都可以设置参数和更改参数。
Protobuf是Google公司开发的一种跨语言和平台的序列化数据结构的方式,是一个灵活的、高效的用于序列化数据的协议,与XML和JSON格式相比,Protobuf更小、更快、更便捷。
Protobuf是跨语言的,并且自带一个编译器(protoc),只需要用protoc进行编译,就可以编译成Java、Python、C++、C#、Go等多种语言代码,然后可以直接使用,不需要再写其它代码,自带有解析的代码。只需要将要被序列化的结构化数据定义一次(在.proto文件定义),便可以使用特别生成的源代码(使用protobuf提供的生成工具)轻松的使用不同的数据流完成对结构数据的读写操作。甚至可以更新.proto文件中对数据结构的定义而不会破坏依赖旧格式编译出来的程序。
其优点如下:
为了方便讲解,使用cyber/examples/proto/examples.proto
文件来讲解,syntax
?表示使用Protobuf的版本,目前Protobuf支持proto3,但在Apollo中使用的是proto2;package 表示该文件的路径;每一个message都表示一种数据结构,message后面跟的是数据结构名字,括号里的字段定义格式为:字段规则 数据类型 字段名称 字段编号。
字段规则主要有三种:
# 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;
};
Protobuf的编译要分为两个步骤,首先要根据.proto文件生成proto库,然后再根据生产的proto库生成C++相关的源文件。这个源文件是C++语言自动编写的,可以被C++程序自动识别。每一个message会被解析生一个类,里面的字段就相当于这个类的属性。在源文件中也会根据属性生成额外的成员,如获取和设置属性的函数。
package(default_visibility = ["//visibility:public"])
#1、生成proto库
proto_library(
name = "examples_proto",
srcs = ["examples.proto"],
)
#2、生成源文件
cc_proto_library(
name = "examples_cc_proto",
deps = [
":examples_proto",
],
)
目的:使用Protobuf来定义数据格式,在main程序中设置数据值并输出。
流程:1、创建目录并编写相关文件;2、编译执行。
内容如下:
test
|-- test_proto
|-- BUILD
|-- car.cc
|-- proto
|-- BUILD
|-- car_msg.proto
各个结构内容如下:
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
load("//tools/install:install.bzl", "install", "install_src_files")
load("//tools:cpplint.bzl", "cpplint")
package(default_visibility = ["//visibility:public"])
cc_binary(
name = "car",
srcs = ["car.cc"],
deps = ["//test/proto:car_msg_cc_proto"],
)
install(
name = "install",
runtime_dest = "test/bin",
targets = [
":car"
],
)
install_src_files(
name = "install_src",
src_dir = ["."],
dest = "test/src/cyberatest",
filter = "*",
)
#include "test/proto/car_msg.pb.h"
using namespace std;
int main()
{
apollo::cyber::test::proto::CarMsg car;
car.set_owner("apollo");
car.set_license_plate("京A88888");
car.set_max_passenger(6);
car.add_car_info("SUV"); //车型
car.add_car_info("Red"); //车身颜色
car.add_car_info("electric"); //电动
string owner = car.owner();
string license_plate = car.license_plate();
uint64_t max_passenger = car.max_passenger();
cout << "owner:" << owner << endl;
cout << "license_plate:" << license_plate << endl;
cout << "max_passenger:" << max_passenger << endl;
for (int i = 0; i < car.car_info_size(); ++i){
string info = car.car_info(i);
cout << info << " ";
}
cout << endl;
return 0;
}
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("//tools:python_rules.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
proto_library(
name = "car_msg_proto",
srcs = ["car_msg.proto"],
)
cc_proto_library(
name = "car_msg_cc_proto",
deps = [":car_msg_proto"],
)
syntax = "proto2";
package apollo.cyber.test.proto;
message CarMsg {
required string owner = 1;
optional string license_plate = 2;
optional uint64 max_passenger = 3;
repeated string car_info = 4;
}
代码到这里就可以进行编译运行:
// 编译
cd /apollo_workspace
buildtool build -p test
// 执行
cd /apollo_workspace/bazel-bin/test/test_proto
./car
运行结果如下图所示:
所有者:apollo
车牌号:京A88888
最大乘客人数:6
SUV红色电动
在第一章中,我们已经说明了Node、Channel等Cyber的基础概念,本章我们基于这些基础概念,进一步介绍cyber中的三种通信方式,也即发送接收信息的三种方式。
为了更好的说明这三种通信机制的用法,该章我们通过实现一个案例来说明该部分内容。
案例如下,假设我们拥有了一辆自己的“无人车“,apollo号,现在我们想通过通信,获得该车辆的“实时”速度、车的详细信息以及车的通用参数。那么现在会有几个问题:
目录结构:
/apollo_workspace/
|--test
| |--communication
| | |--BUILD //cyber_test编译文件
| |--talker.cc //talker-listener通信实现
| |--listener.cc
| |--server.cc //server-client通信实现
| |--client.cc
| |--param_server.cc //parameter server-client通信实现
| |--param_client.cc
|--proto
|--BUILD //car.proto 编译文件
|--car.proto //小车数据定义的文件
通过第一节内容,我们知道了proto文件的使用方法,那么该章我们来自己编写一个proto文件,来实现我们“车”的变量定义,在该章的三种通信方式的案例中都是用这一数据定义。对car.proto文件进行编写,其内容如下:
首先我们要通过第一节内容,编写一个自己的proto文件,来实现我们“车”的变量定义,在本节中的三种通信方式的案例中都是用这一数据定义。对car.proto文件进行编写,其内容如下:
car.proto
// 定义proto使用的版本
syntax = "proto2";
//定义包名,在cc文件中调用
package apollo.cyber.test.proto;
//定义一个车的消息,车的型号,车主,车的车牌号,已跑公里数,车速
message Car{
optional string plate = 1;
optional string type = 2;
optional string owner = 3;
optional uint64 kilometers = 4;
optional uint64 speed = 5;
};
我们来编写一个talker.cc来实现主动对话
talker.cc
//头文件引用
#include "test/proto/car.pb.h"
#include "cyber/cyber.h"
#include "cyber/time/rate.h"
//car数据定义的引用,可以看出其定义来源于一个proto
using apollo::cyber::examples::cyber_test_proto::Car;
int main(int argc, char *argv[]) {
// 初始化一个cyber框架
apollo::cyber::Init(argv[0]);
// 创建talker节点
auto talker_node = apollo::cyber::CreateNode("talker");
// 从节点创建一个Topic,来实现对车速的查看
auto talker = talker_node->CreateWriter<Car>("car_speed");
AINFO << "I'll start telling you the current speed of the car.";
//设置初始速度为0,然后速度每秒增加5km/h
uint64_t speed = 0;
while (apollo::cyber::OK()) {
auto msg = std::make_shared<Car>();
msg->set_speed(speed);
//假设车速持续增加
speed += 5;
talker->Write(msg);
sleep(1);
}
return 0;
}
编写一个listener来实现对talker发送过来的内容进行接收。
listener.cc
#include "test/proto/car.pb.h"
#include "cyber/cyber.h"
using apollo::cyber::examples::cyber_test_proto::Car;
//接收到消息后的响应函数
void message_callback(
const std::shared_ptr<Car>& msg) {
AINFO << "now speed is: " << msg->speed();
}
int main(int argc, char* argv[]) {
//初始化cyber框架
apollo::cyber::Init(argv[0]);
//创建监听节点
auto listener_node = apollo::cyber::CreateNode("listener");
//创建监听响应进行消息读取
auto listener = listener_node->CreateReader<Car>(
"car_speed", message_callback);
apollo::cyber::WaitForShutdown();
return 0;
}
编写在cyber/examples/cyber_test/BUILD中。
talker 和 listener 编译文件。
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
load("//tools/install:install.bzl", "install", "install_src_files")
load("//tools:cpplint.bzl", "cpplint")
package(default_visibility = ["//visibility:public"])
cc_binary(
name = "talker",
srcs = ["talker.cc"],
deps = [
"//cyber",
"//test/proto:car_cc_proto",
],
linkstatic = True,
)
cc_binary(
name = "listener",
srcs = ["listener.cc"],
deps = [
"//cyber",
"//test/proto:car_cc_proto",
],
linkstatic = True,
)
aem start //启动docker环境
aem enter //进入docker环境
使用apollo 包管理开发方式提供的
buildtool build -p test/communication/
编译成功显示:
首先,先将输出方法改为控制台输出。
export GLOG_alsologtostderr=1
打开两个终端,都进入Apollo的docker环境,一个终端运行talker,另一个运行listener,会发现listener运行后开始接收talker发送的小车速度的消息。
运行talker
./bazel-bin/test/communication/talker
终端显示:
运行listener
./bazel-bin/test/communication/listener
结果显示:
恭喜你,完成Apollo Cyber RT 的C++ 的话题通信实验喽!!!
小测试!!!
期待结果!!!
Apollo Cyber?? RT框架是基于组件的概念构建的。作为Apollo Cyber?? RT框架的基本构建块,每个组件都包含一个特定的算法模块,该模块处理一组数据输入并生成一组输出。
为了成功创建和启动新组件,需要执行四个基本步骤:
设置组件文件结构
实现组件类
设置配置文件
启动组件
下面的示例演示如何创建一个简单的组件,然后在屏幕上生成,运行和观看最终输出。如果您想了解有关Apollo Cyber?? RT的更多信息,您可以在directory目录下找到几个示例,展示如何使用框架的不同功能/apollo/cyber/examples/。
注意:该示例必须在apollo docker环境中运行,并使用Bazel进行编译。
1、设置组件文件结构
请创建以下文件,假定位于目录下/apollo/cyber/examples/common_component_example/:
头文件:common_component_example.h
源文件:common_component_example.cc
构建文件:BUILD
DAG依赖项文件:common.dag
启动文件:common.launch
2、实现组件类
实现组件的头文件
实施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)
Proc类似于回调函数;
实现示例组件的源文件
对于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;
}
为示例组件创建构建文件
创建bazel BUILD文件。
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()
3、设置配置文件
配置DAG依赖文件
要配置DAG依赖文件(common.dag),请指定以下各项:
通道名称:用于数据输入和输出
库路径:从组件类构建的库
类名:组件的类名
# Define all coms 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"
? ? ? ? ? ? }
? ? ? ? }
? ? }
}
配置启动文件
要配置启动(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>
4、启动组件
通过运行以下命令来构建组件:
bash /apollo/apollo.sh build
注意:确保示例组件构建良好
然后配置环境:
cd /apollo/cyber
source setup.bash
有两种启动组件的方法:
与启动文件一起启动(推荐)
cyber_launch start /apollo/cyber/examples/common_component_example/common.launch
使用DAG文件启动
mainboard -d /apollo/cyber/examples/common_component_example/common.dag
参考案例:
https://github.com/gruminions/apollo/blob/record/cyber/examples/talker.cc
?