车辆数据服务技术分享

车辆信号通路概览

📷 [diagram: 车辆信号通路概览](飞书原生结构图,源文档 readonly-block doxcntbpZ2ZKffo6aVKJpEcYONe,无法导出)

目标(为什么需要这样一套车辆数据体系):

  • 由于车型定义的区别,如果实现在不同的车型上,不同业务方使用同一套API,标准化相关的数据获取,计算能力调用的操作,使得不同平台上功能一致(参考手机多SKU,多代码基线的困境)
  • 降低重复的业务实现,通过统一的工具提升效率
  • 传统CAN的接口定义,与围绕业务”对象”作为主题的接口定义方式的区别
  • 业务私有通信接口管理混乱,整体打点,安全机制落地困难等问题

信号体系构成

为什么需要重新定义

  1. DBC 信号需要一个中间件来承接, 定义各个信号的含义,以及具体取值的含义
  2. DBC 信号由各个厂商生成,需要将各个具体信号值定义为不同语言容易理解,且能保证版本、硬件更替下的稳定性
  3. 需要将同一功能的状态、指令相关定义统一起来,避免指令与状态使用不同的定义
  4. 避免应用处理重复的底层逻辑,将通信中常见问题(超时、置零、反转等)在信号状态中反应出来
  5. 对接标准 Android Automotive OS(AAOS) 体系

相关参考:

信号构成示例

📷 [嵌入电子表格: 信号构成示例](飞书 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)构成表:

MASK0xf00000000x0f0000000x00ff00000x0000f0000x00000f000x000000ff
AAOS 体系VehiclePropertyGroupVehicleAreaVehiclePropertyType
AAOSSYSTEM 1 / VENDOR 2GLOBAL 1 / WINDOW 3 / MIRROR 4 / SEAT 5 / DOOR 6 / WHEEL 7 / BODY 8STRING 0x00100000 / BOOLEAN 0x00200000 / INT32 0x00400000 / INT32_VEC 0x00410000 / INT64 0x00500000 / INT64_VEC 0x00510000 / FLOAT 0x00600000 / FLOAT_VEC 0x00610000 / BYTES 0x00700000 / MIXED 0x00e00000unique id from 0x0000 - 0xffff( colspan=3)
MiCarMICAR 6A0 0x0-0xf MiVehicleZoneA1 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 类图

  1. 提供对外统一 SOA 接口(同 Android)
  2. 提供上行信号 11、N1,下行信号 11、1N 的转换
  3. 提供上层针对不同对手域,进行相同定义输出的协议转换

为什么需要 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 进行对应,一目了然。

任务工厂为整个”流水线”提供如下的原子化处理单元:

  1. RawData pipeline:针对不同的业务数据流,提供不同的 raw_data 到 raw_value 的解析过程;同时又对相似的业务进行封装,比如空调与车辆设置采用相同的处理方式,透传的仪表信号与其他仪表信号又采用不同的处理方式——既体现了处理逻辑的原子化,又尽最大可能对处理逻辑进行复用。
  2. Value mapping pipeline:针对不同的信号值,提供不同类别的处理方式,主要有以下三种情况:
    • 信号值的 mapping 映射,包括值透传,此方式也是 SOA 表中常见的情况,大概占整体的 80% 以上
    • 信号值通过运算而来,如空调温度及带精度及偏移量的信号值运算
    • 带有 vector 属性值的信号赋值
  3. Property value pipeline:将 Property Id 对应的不同信号依据信号类型进行整合,将具有 value 属性的信号赋值给 property value 的 value 值,同时又对赋予 value 的异常值进行异常流程(status 属性)绑定,实现对一个 pipeline 同时能够满足处理正常和异常值的两种逻辑;对 status 及 error message 也采取类似处理,使整个处理流程清晰明了。

Value mapping pipeline 代码片段

📷 [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 中间件是什么样子的?

经典中间件架构图 1

经典中间件架构图 2

经典中间件架构图

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 中间件系统吞吐测试(本机)
吞吐量测试 - 主机和 docker吞吐测试 - 本机

上图:cyclonedds 吞吐量在 5000-5500 条/秒左右,fast dds 在 7000-8000 条/秒左右

  • 为什么选择 ros2? 选型标准:
  1. 对上层应用方不暴露任何 dds 相关的接口,方便后面 dds 等方案替换
  2. 有丰富的工具链,方便开发联调
  3. 简单丰富的 API 接口,支持 c++/c/python 接口
  4. 丰富的 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 背景

随着各个域进入开发阶段,前期很多的不确定性逐渐变得明确起来,主要有以下几个方面:

  1. DCD 选型确定,android/qnx 双系统,系统间通信考虑使用 HAB
  2. 整车购买 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 设计框架

设计目标:

  1. 接口简单统一
  2. 域间通信使用 RTI IDL 和其他域保持一致
  3. 域内通信使用 fast dds + protobuf,域内 host 内通信使用系统共享内存,host 间(qnx 和 android)使用 HAB

💡 protobuf 对比 IDL 的优势:

  1. 对 android 友好,方便源码构建阶段生成中间文件,不用提前生成放到代码里
  2. 序列化后数据量会有所减小
  3. 兼容性好,一方增加了字段,对方如果因为某些原因没有更新对原有数据的解析不影响(IDL 做不到)
  4. 端云同步
  1. 工具链借鉴 dds 的优势,最大限度设计成非侵入式

📷 [diagram: 工具链设计](飞书原生结构图,源文档 readonly-block doxcn2WEn9UJlIp5PyP2rzIaWjg,无法导出)

  1. 减小库的数量和大小,减小中间件不必要的内存占用

中间件数据流转图:

  • 对于业务层统一按照 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

// 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 版本)

CarDemo

  • 提供 maven 支持,以及原生 car-lib 的 JAVA compat 版本,方便其他平台使用

自动化测试工具

📷 [diagram: 自动化测试工具架构](飞书原生结构图,源文档 readonly-block doxcnL2NpSml0k3s4BszkRKmQCc,无法导出)

自动化测试工具打通了整个信号链路,无论信号的接收还是发送,只要有一套标准的测试用例,即可完成对信号输入、输出的自动化校验,并给出详细的结果反馈,大大降低了人工手动发送信号的复杂度,极大地提升了效率。不仅如此,在问题暴露的初期能快速定位是中间件层还是应用层的问题,使不同开发同学快速、并行地处理解决问题。

6.1 测试用例图

自动化测试用例图

6.2 自动化测试运行视频

🎬 [视频: 自动化测试运行视频](mp4,token=B5Aubcyufo1MlHx617xcM0U2nSf,未导出)

  • Python 集成测试
  • 中间件测试工具规划(@张佃伦):测试结果自动生成、测试程序自动部署、测试结果可视化,能够追踪测试结果的变化趋势,为后期迭代优化提供数据支撑。参考 8295 中间件性能测试(sheets, sheet_id=mN954I)
  • gtest