车辆数据服务技术分享
车辆信号通路概览
📷 [diagram: 车辆信号通路概览](飞书原生结构图,源文档 readonly-block doxcntbpZ2ZKffo6aVKJpEcYONe,无法导出)
目标(为什么需要这样一套车辆数据体系):
- 由于车型定义的区别,如果实现在不同的车型上,不同业务方使用同一套API,标准化相关的数据获取,计算能力调用的操作,使得不同平台上功能一致(参考手机多SKU,多代码基线的困境)
- 降低重复的业务实现,通过统一的工具提升效率
- 传统CAN的接口定义,与围绕业务”对象”作为主题的接口定义方式的区别
- 业务私有通信接口管理混乱,整体打点,安全机制落地困难等问题
信号体系构成
为什么需要重新定义
- DBC 信号需要一个中间件来承接, 定义各个信号的含义,以及具体取值的含义
- DBC 信号由各个厂商生成,需要将各个具体信号值定义为不同语言容易理解,且能保证版本、硬件更替下的稳定性
- 需要将同一功能的状态、指令相关定义统一起来,避免指令与状态使用不同的定义
- 避免应用处理重复的底层逻辑,将通信中常见问题(超时、置零、反转等)在信号状态中反应出来
- 对接标准 Android Automotive OS(AAOS) 体系
相关参考:
- SOA 服务接口定义表格(sheets,MS11 当前版本,信号管理系统维护)
- Mi AAOS 信号体系实现(docx,已入库)
- SOA 服务接口定义表格 MS11(E2/E3 备份 | 弃用)
信号构成示例
📷 [嵌入电子表格: 信号构成示例](飞书 sheet token=VNlAsBu3FhKNm5tR01zccSfnnDd,sheet_id=Da3yos)
- 一个具体信号由 propId(具体信号ID)、areaId(位置ID)、以及 value(payload)组成
// 信号 Payload 格式
{
"value": xxx, // 具体数值
"relative": boolean(true, false) // 设置增减还是绝对值, optional (default: false => 绝对值)
"type": int, long, float, int[], long[], float[], String, custom // optional (default: int)? enum
"timestamp": long long // optional
"state": int(invalid unavailable available) 是否有效
}- 其中 propId 的构成如下
// Seat.java
@PropertyDef(
permission = @RequiresPermission(Car.PERMISSION_CONTROL_CAR_SEATS),
areaRef = VehicleAreaSeat.class,
value = @ValueDef(range = @Range(from = 0, to = 100)))
public static final int HORIZONTAL_POSITION = 0x102
| MiVehiclePropertyGroup.MI_CAR
| MiVehicleArea.SEAT
| VehiclePropertyType.INT32
| MiVehicleZone.SEAT;/*0x65406102*/propId 位掩码(MASK)构成表:
| MASK | 0xf0000000 | 0x0f000000 | 0x00ff0000 | 0x0000f000 | 0x00000f00 | 0x000000ff |
|---|---|---|---|---|---|---|
| AAOS 体系 | VehiclePropertyGroup | VehicleArea | VehiclePropertyType | — | — | — |
| AAOS | SYSTEM 1 / VENDOR 2 | GLOBAL 1 / WINDOW 3 / MIRROR 4 / SEAT 5 / DOOR 6 / WHEEL 7 / BODY 8 | STRING 0x00100000 / BOOLEAN 0x00200000 / INT32 0x00400000 / INT32_VEC 0x00410000 / INT64 0x00500000 / INT64_VEC 0x00510000 / FLOAT 0x00600000 / FLOAT_VEC 0x00610000 / BYTES 0x00700000 / MIXED 0x00e00000 | unique id from 0x0000 - 0xffff( colspan=3) | ||
| MiCar | MICAR 6 | A0 0x0-0xf MiVehicleZone | A1 0x0-0xf MiVehicleZone* | A2 0x00-0xff MiVehicleSpecificProp |
/*
* 0x03 (A2)
* | MiVehicleZoneBody:KEYBOARD (A1)
* | MiVehicleZone:BODY (A0)
* | VehiclePropertyType:INT32
* | VehicleArea:GLOBAL
* | MiVehiclePropertyGroup:MICAR
*/
int32_t KEYBOARD_UP = 1631589891;信号值定义
- 值定义上,期望零值作为默认值(状态)
- 零值可以作为关闭状态,或者范围的最小状态
- 档位、范围类型只定义范围,不定义具体变量(例如 Level_1, Level_2, Level_x)
- 部分在取值范围上有关联的(比如空调吹风方向),可以支持位运算
- 需要考虑未来可能添加的需求
- 在表达同级别含义的时候,信号值应该连续
CarService
3.1 服务架构
📷 [diagram: CarService 服务架构](飞书原生结构图,源文档 readonly-block doxcnjplqHtVlVwUIOMBS6edCHh,无法导出)
3.2 QNX Carservice UML 类图
- 提供对外统一 SOA 接口(同 Android)
- 提供上行信号 1→1、N→1,下行信号 1→1、1→N 的转换
- 提供上层针对不同对手域,进行相同定义输出的协议转换
为什么需要 CarService 来处理信号,而不是直接将信号透传给业务层来处理?
类图如下:
📷 [diagram: QNX CarService UML 类图](飞书原生结构图,源文档 readonly-block doxcnpAnFscixFdhDw2iaj80vAh,无法导出)
相关角色:
- Executor:执行器,用以传输或者接收其他端传输的数据,提供数据传输接口
SubscribeExecutor:用于接收数据PublishExecutor:用于发送数据
- MessageChannel:消息传输管道,并负责消息队列的管理,支持同步、异步、优先级配置等多种模式,调用执行器
AsyncMessageChannel:异步传输通道SyncMessageChannel:同步传输通道PriorityMessageChannel:带有优先级策略的传输通道
- Parser:从 SourceType(CAN/PPS/Network) 到 ServiceType(Protobuf/FlatBuffers) 的信号解析类。子类需要实现数据源到目标类型的业务。父类会完成相关
from_service()和to_service()方法调用,与 MessageCenter 进行通信,将远端数据转换成为 MessageCenter 的目标类型。类似会有 CAN/PPS 等不同的实现 - AdapterFactory:Executor、Parser 和 MessageChannel 子类的实例化工厂
- Adapter:提供 Parser 到原子类型 NodeService 的数据路由方法
- NodeService:基于 ROS2 的 Node 类的封装类型,标准父类的实现仅仅只是 Node 的封装,并提供所包含的 Node 实例以提供更多的能力。组合服务会基于 NodeService 实现相应的扩展,实现例如监听其他数据、计算之后暴露其他数据能力的功能
- MessageCenter:所有的 NodeService 的管理中心,通过加载 ConfigManager 来获取相关的 Parser 与 NodeService 的配置
- ConfigManager:通过实现加载文件或者通过网络同步等方式,通过配置文件管理 MessageCenter 中的 Parser 与 NodeService 的类型
- DataQueue:用于接收、发送数据的缓存队列
3.3 Pipeline
📷 [diagram: Pipeline 流水线](飞书原生结构图,源文档 readonly-block doxcnrpoyeUnYVJHODuf1vUW8fm,无法导出)
整个系统的任务类似流水线一样,在信号定义时依赖 SC 文档进行预设,可以快速构建及清晰信号流转查阅:整个流程体现了信号从哪里来、如何进行解析、如何进行转换以及如何与 PropID 进行对应,一目了然。
任务工厂为整个”流水线”提供如下的原子化处理单元:
- RawData pipeline:针对不同的业务数据流,提供不同的 raw_data 到 raw_value 的解析过程;同时又对相似的业务进行封装,比如空调与车辆设置采用相同的处理方式,透传的仪表信号与其他仪表信号又采用不同的处理方式——既体现了处理逻辑的原子化,又尽最大可能对处理逻辑进行复用。
- Value mapping pipeline:针对不同的信号值,提供不同类别的处理方式,主要有以下三种情况:
- 信号值的 mapping 映射,包括值透传,此方式也是 SOA 表中常见的情况,大概占整体的 80% 以上
- 信号值通过运算而来,如空调温度及带精度及偏移量的信号值运算
- 带有 vector 属性值的信号赋值
- Property value pipeline:将 Property Id 对应的不同信号依据信号类型进行整合,将具有 value 属性的信号赋值给 property value 的 value 值,同时又对赋予 value 的异常值进行异常流程(status 属性)绑定,实现对一个 pipeline 同时能够满足处理正常和异常值的两种逻辑;对 status 及 error message 也采取类似处理,使整个处理流程清晰明了。
📷 [diagram: Pipeline 详细流程](飞书原生结构图,源文档 readonly-block doxcnfttscIvyUKsPq3NO5gQqMf,无法导出)
中间件
4.1 什么是中间件?
中间件是以 DDS(Data Distribution Service,数据分发服务) 为核心的支持域内及域外数据交互的中间件软件。
- 什么是 DDS?
DDS(Data Distribution Service)是数据分发服务的首字母缩略词。DDS 采用发布/订阅体系架构,强调以数据为中心,提供丰富的 QoS 服务质量策略,能保障数据进行实时、高效、灵活地分发,可满足各种分布式实时通信应用需求。根据 OMG(对象管理组织,Object Management Group)定义的标准,由 RTPS 协议定义。
对于一般用户只需要关注以下几点:
-
TOPIC 是什么?
-
数据格式是什么?
-
QOS?
-
Domain?
-
主要特点:
- 去中心化:新应用服务上线发现不依赖服务注册中心,实现分布式发现
- 性能和服务质量(QoS):属性为使用标准 IP 网络的实时应用程序提供最大努力和可靠的发布-订阅通信
- 容错:允许创建没有单点故障的网络
- 可扩展性:允许通过协议扩展和新服务增强实现向后兼容性和互操作性
- 新应用程序和服务的即插即用连接:允许应用程序随时加入和离开网络进行自动、无配置的发现
- 可配置性:允许平衡每个数据交付事务的可靠性和及时性要求
- 模块化:允许简单的设备实现协议的一个子集并且仍然参与发布-订阅网络
- 可扩展性:使系统能够扩展到非常大的发布订阅网络
- 类型安全:可防止应用程序编程错误危及发布-订阅网络中远程节点的操作
详细介绍参考:DDS 详解(docx)
4.2 为什么要有中间件?
随着车内以太网的日益普及,汽车软件系统呈现以下发展趋势:
- 各个域上数据量越来越大
- 各个域之间的交互越来越复杂
- 实时性要求的日益增加
- 软件趋向 SOA 服务化
基于以上发展趋势,我们需要有一套服务化的数据和通信框架,以支撑车内以太网上各个模块之间通信的需求。这套服务中间件需要支持硬件/OS/编程语言级别的异构,大体包括:
- 硬件:娱乐域、中控仪表、MCU、中央计算单元、ADAS
- OS:QNX、Linux、Android Native/Java、RTOS、Autosar CP/AP、windows(打通测试工具)
- 编程语言:C/C++、Java、Go、Rust、Python 等
4.3 中间件是什么样子的?
经典中间件架构图
4.4 中间件选型
| DDS | 特点 |
|---|---|
| Fast DDS | 支持网络和共享内存,跟 RTI 设计思路一致 |
| Cyclone DDS | 只支持网络,共享内存是通过集成 ice oryx 实现 |
| IceOryx 冰羚 | 只支持系统内共享内存。发现方式是集中式发现,需要单独启动发现服务器 daemon 进程。多平台适配做的比较差,android 要做共享内存 ashm 适配 |
| RTI | 商业软件:支持网络/共享内存传输 |
- 为什么选择 fast-dds?
- 选型阶段定位为能够支持域间和域内通信,能够输出给其他域直接使用,需要支持共享内存和网络通信
- 做过 fast-dds 和 cyclonedds 性能对比,fast-dds 更优秀
| ROS2 DDS 中间件系统吞吐量测试(主机和 docker 实例) | ROS2 DDS 中间件系统吞吐测试(本机) |
|---|---|
上图:cyclonedds 吞吐量在 5000-5500 条/秒左右,fast dds 在 7000-8000 条/秒左右
- 为什么选择 ros2? 选型标准:
- 对上层应用方不暴露任何 dds 相关的接口,方便后面 dds 等方案替换
- 有丰富的工具链,方便开发联调
- 简单丰富的 API 接口,支持 c++/c/python 接口
- 丰富的 dds 适配方案,支持 fast dds、cyclone dds、RTI dds
以上是第一版中间件的选型思路。
最终第一版中间件的架构图如下:
📷 [diagram: 第一版中间件架构图](飞书原生结构图,源文档 readonly-block doxcnN6zy49OHutElnFIRf8cCwb,无法导出;图片版见下)
HOST A / HOST B 内含 Process A 1、Process A 2 等,进程内含 DDS / RTPS Reader / RTPS Writer / Transport encapsulation;图中体现了 Memory mapped file、Data-sharing、Intraprocess 等进程间数据共享和传输机制
4.5 中间件 V2.0
4.5.1 背景
随着各个域进入开发阶段,前期很多的不确定性逐渐变得明确起来,主要有以下几个方面:
- DCD 选型确定,android/qnx 双系统,系统间通信考虑使用 HAB
- 整车购买 RTI DDS,并在各个域推统一使用 RTI + IDL DDS 模式
随着对 ROS 代码的深入研究,我们发现 ros2 代码对于一个座舱内部使用的中间件来说,有以下几个缺点:
- 太臃肿:层次太多,如果要做一个简单的改动,需要改动的模块太多。开发效率太低,维护成本太高
- 编译后的库太多:总共几十个库,对于使用者来说是一个灾难
- API 大而全:对于我们来说很多 API 都是没用的,放在那里对用户是一种误导
同时,我们也研究了 CYBER-RT 的代码,发现 cyber-rt 也识别到了 ros 的这些问题。在 CYBER 的整个架构设计中规避了 ROS 的缺点,同时保留了 ros 中的一些优秀的设计。于是我们借鉴 CYBER-RT 设计了中间件 2.0 版本,架构如下:
📷 [diagram: 中间件 V2.0 架构](飞书原生结构图,源文档 readonly-block doxcnLhLfgNByqs780cfdU6FLGe,无法导出)
4.5.2 设计框架
设计目标:
- 接口简单统一
- 域间通信使用 RTI IDL 和其他域保持一致
- 域内通信使用 fast dds + protobuf,域内 host 内通信使用系统共享内存,host 间(qnx 和 android)使用 HAB
💡 protobuf 对比 IDL 的优势:
- 对 android 友好,方便源码构建阶段生成中间文件,不用提前生成放到代码里
- 序列化后数据量会有所减小
- 兼容性好,一方增加了字段,对方如果因为某些原因没有更新对原有数据的解析不影响(IDL 做不到)
- 端云同步
- 工具链借鉴 dds 的优势,最大限度设计成非侵入式
📷 [diagram: 工具链设计](飞书原生结构图,源文档 readonly-block doxcn2WEn9UJlIp5PyP2rzIaWjg,无法导出)
- 减小库的数量和大小,减小中间件不必要的内存占用
中间件数据流转图:
- 对于业务层统一按照 DDS 接口来进行调用
- 底层根据 DDS 的接口将 HAB 进行包装,同时提供 QAC Service 作为 HAB 通信的管理,使得 DDS 相关的服务发现与数据传输能够复用跨系统的 IPC 通道
- 同时对于 HAB 的 QAC Service 的封装,也提供基础的点对点通信能力,根据业务需求作为轻量化的使用场景
- 后续可能考虑基于 HAB 的原理实现独立的 IPC 通道
📷 [diagram: 中间件数据流转图](飞书原生结构图,源文档 readonly-block doxcn13h85itlTZpS3xdVuQ5jcc,无法导出)
QAC 数据流转图:
- 跨系统 Intent 实现
- 跨系统数据共享
📷 [diagram: QAC 数据流转图](飞书原生结构图,源文档 readonly-block doxcne5HZGY7mbHgdk1a2ypWLph,无法导出)
4.5.3 API 设计
/**
* @class Node
* @brief Node is the fundamental building block of fast channel.
* every module contains and communicates through the node.
* A module can have different types of communication by defining
* publisher/subscriber in a node.
* @warning Duplicate name is not allowed in topo objects, such as node,
* r publisher/subscriber, in the topo.
*/
class Node {
public:
Node(const std::string&, const std::string&);
virtual ~Node();
/**
* @brief Get node's name.
* @warning duplicate node name is not allowed in the topo.
*/
const std::string& name() const;
/**
* @brief Create a Publisher with specific message type.
*
* @tparam MessageT Message Type
* @param topic_name is publiser top name.
* @param domain_id is publiser domain .
* @return std::unique_ptr<Writer<MessageT>> result Writer Object
*/
template <typename MessageT>
auto createPublisher(const std::string& topic_name, const unsigned int domain_id = 0)
-> std::unique_ptr<AbstractPublisher<MessageT>>;
/**
* @brief Create a Subscriber with specific message type with channel name
* qos and other configs used will be default
*
* @tparam MessageT Message Type
* @param topic_name the topic of the subscriber subscribed.
* @param domain_id the domain of the subscriber belong.
* @param subscribe_func invoked when message receive
* invoked when the message is received.
* @return std::unique_ptr<mi::car::Reader<MessageT>> result Reader Object
*/
template <typename MessageT>
auto createSubscriber(const std::string& topic_name,
const CallbackFunc<MessageT> &subscribe_func = nullptr,
const unsigned int domain_id = 0)
-> std::unique_ptr<AbstractSubscriber<MessageT>>;
/**
* @brief Create a Service object with specific `service_name`
*
* @tparam Request Message Type of the Request
* @tparam Response Message Type of the Response
* @param service_name specific service name to a serve
* @param service_callback invoked when a service is called
* @return std::shared_ptr<Service<Request, Response>> result `Service`
*/
template <typename ServiceT>
auto createService(const unsigned int domain_id, const std::string& service_name,
std::shared_ptr<void> service_impl)
-> std::shared_ptr<AbstractService<ServiceT>>;
/**
* @brief Create a Client object to request Service with `service_name`
*
* @tparam Request Message Type of the Request
* @tparam Response Message Type of the Response
* @param service_name specific service name to a Service
* @return std::shared_ptr<Client<Request, Response>> result `Client`
*/
template <typename ClientT>
auto createClient(const unsigned int domain_id, const std::string& service_name)
-> std::shared_ptr<AbstractClient<ClientT>>;
std::string node_name_;
std::string name_space_;
std::mutex readers_mutex_;
};
/**
* @class AbstractPublisher
* @brief AbstractPublisher is the fundamental building block of fast channel.
* which defined publisher public API.
* A module can send different types of data by offer MessageT.
*/
template <typename MessageT>
class AbstractPublisher {
public:
virtual ~AbstractPublisher() {}
virtual bool publish(MessageT &message) = 0;
virtual void setListener(std::unique_ptr<AbstractDataWriterListener> &listener) {
(void)listener;
}
};
template <typename MessageT>
using CallbackFunc = std::function<void(const MessageT&)>;
template <typename MessageT>
class AbstractSubscriber {
public:
virtual ~AbstractSubscriber() {}
virtual void setListener(std::unique_ptr<AbstractDataReaderListener> &listener) {
(void)listener;
}
};4.6 计算业务管理及调度中间件服务
📷 [diagram: 计算业务管理及调度中间件服务](飞书原生结构图,源文档 readonly-block doxcnVQEfNRCq6csKKsptsnCwvb,无法导出)
Android MiVehicleHal & MiVehicleService
📷 [diagram: Android MiVehicleHal & MiVehicleService 总览](飞书原生结构图,源文档 readonly-block doxcnxy6zYofiT1MtKhgRB80dGh,无法导出)
5.1 MiVehicleHal
📷 [diagram: MiVehicleHal 架构](飞书原生结构图,源文档 readonly-block doxcnZduZ0L7x5hmQJuQRuiqS8m,无法导出)
- 对外接口
- Vhal 按照 Mi AAOS 信号体系实现 MiVehicleProp,并提供对 Android 原生 Prop 的映射,可确保 Android 原有体系正常使用
- HIDL 接口未更改,确保原有依赖正常使用
- 实现
- MiVhal 在现有 Android Default 实现上进行扩展,将信号通路对接到 MiCar MW DDS 上与 QNX-CarService 通信
5.2 MiVehicleService
- 使用 Aspectj 注入体系,来做原生 CarService 以及新增信号体系的融合
- 使用注解体系对每个信号在 JAVA 接口中进行声明(参考 SOA 服务接口定义表格 / Mi AAOS 信号体系实现 / SOA 表 MS11 备份)
// Hvac.java
// 空调吹风方向
@PropertyDef(
permission = @RequiresPermission(Car.PERMISSION_CONTROL_CAR_CLIMATE),
areaRef = VehicleAreaSeat.class,
value = @ValueDef(valueEnum = HvacFanDirection.class))
public static final int WIND_DIRECTION = 0x106
| MiVehiclePropertyGroup.MI_CAR
| MiVehicleArea.SEAT
| VehiclePropertyType.INT32
| MiVehicleZone.HVAC; /*0x65404106*/
/**
* note : Most of hvac do not support
* {@link HvacFanDirection#FACE_AND_FLOOR} and {@link HvacFanDirection#DEFROST_FACE_FLOOR}
*/
public static final class HvacFanDirection implements ValueDef.ValueEnum {
/** Constant for unknown fan direction. */
public static final int UNKNOWN = 0x0;
/** Constant for face direction. */
public static final int FACE = 0x01;
/** Constant for floor direction. */
public static final int FLOOR = 0x02;
/** Constant for face and floor direction. */
public static final int FACE_AND_FLOOR = 0x03; // FACE_AND_FLOOR = FACE | FLOOR
/** Constant for defrost direction. */
public static final int DEFROST = 0x04;
/** Constant for defrost and face direction. */
public static final int FACE_AND_DEFROST = 0x05; // FACE_AND_DEFROST = FACE | DEFROST
/** Constant for defrost and floor direction.*/
public static final int DEFROST_AND_FLOOR = 0x06; // DEFROST_AND_FLOOR = DEFROST | FLOOR
/** Constant for defrost, face and floor direction.*/
public static final int DEFROST_FACE_FLOOR = 0x07; // DEFROST | FACE | FLOOR
}
//Seat.java
@PropertyDef(
permission = @RequiresPermission(Car.PERMISSION_CONTROL_CAR_SEATS),
areaRef = VehicleAreaSeat.class,
value = @ValueDef(range = @Range(from = 0, to = 100)))
public static final int HORIZONTAL_POSITION = 0x102
| MiVehiclePropertyGroup.MI_CAR
| MiVehicleArea.SEAT
| VehiclePropertyType.INT32
| MiVehicleZone.SEAT;/*0x65406102*/- 实现自定义信号到部分原生信号的转换支持,确保 Android Car 体系的正常使用
- Mock 功能
- MiVehicleService 中提供 Mock 功能,方便调试以及 PAD 开发
# enable mock,仅为在信号通路不可用时,测试 APP 使用
adb shell settings put --user 0 GLOBAL vehicle_mock_data 1
# disable mock,切换至正常模式
adb shell settings put --user 0 GLOBAL vehicle_mock_data 0- 提供支持全信号的 CarDemo(测试, support Mock),使用信号自动生成 UI(需更新 micar-support-api 版本)
- 提供 maven 支持,以及原生 car-lib 的 JAVA compat 版本,方便其他平台使用
自动化测试工具
📷 [diagram: 自动化测试工具架构](飞书原生结构图,源文档 readonly-block doxcnL2NpSml0k3s4BszkRKmQCc,无法导出)
自动化测试工具打通了整个信号链路,无论信号的接收还是发送,只要有一套标准的测试用例,即可完成对信号输入、输出的自动化校验,并给出详细的结果反馈,大大降低了人工手动发送信号的复杂度,极大地提升了效率。不仅如此,在问题暴露的初期能快速定位是中间件层还是应用层的问题,使不同开发同学快速、并行地处理解决问题。
6.1 测试用例图
6.2 自动化测试运行视频
🎬 [视频: 自动化测试运行视频](mp4,token=B5Aubcyufo1MlHx617xcM0U2nSf,未导出)
- Python 集成测试
- 中间件测试工具规划(@张佃伦):测试结果自动生成、测试程序自动部署、测试结果可视化,能够追踪测试结果的变化趋势,为后期迭代优化提供数据支撑。参考 8295 中间件性能测试(sheets, sheet_id=mN954I)
- gtest