跳转至

EVCC AIDL / 米德宝联动 v2 协议

最后更新:

适用于外部应用通过 EVCC 导出的 Binder Service 读取联动快照,以及经用户授权后写入车辆属性。

当前协议版本:2

最后按源码核对:2026-08-27

历史属性协议参考:EVCC 3.1.3 的能力版本已升为 3,下述原六个方法和属性快照 version: 2 保持兼容。 新增音乐通道见 v3 协议与独立音乐接口定义。

1. 文档范围

本文档描述:

  • EVCC 如何对外暴露米德宝联动 Binder Service;
  • 两个 AIDL 接口及其 Binder ABI;
  • 银河悬浮歌词与自定义应用的绑定、握手、初始同步和回调流程;
  • AIDL 数据配置、导入、导出、开放读取和多应用写入授权;
  • JSON 快照的字段、类型和兼容行为;
  • 推送时机、断线恢复、读写权限边界和协议升级约束;
  • 当前实现已知的边界和风险。

本文档不把以下两条链路视为同一协议:

  1. 车机厂商的 ecarxcar_service / car_signal AIDL。它是车辆属性进入 EVCC 的数据源之一。
  2. 米德宝 BLE 直发模式。该模式使用 BLE 广播、GATT 或 MIBO 离线转发,不使用本文的 JSON 属性快照协议。

2. 角色与端点

角色 应用 包名 职责
服务端 EVCC com.kooo.evcc 采集属性、生成快照、校验写入授权并提供 Binder Service
米德宝客户端 银河悬浮歌词 com.galaxy.lyricsnext 在“米德宝 AIDL 联动”开启时接收快照
自定义读取客户端 任意外部应用 不限制 在“旧版 AIDL 联动”开启时读取状态、快照并注册回调
授权写入客户端 用户在 EVCC 中勾选的应用 多选 包名和安装时固定的签名均匹配后,可调用属性写入方法

EVCC Service:

Class:  com.kooo.evcc.midebao.MidebaoLinkBridgeService
Action: com.kooo.evcc.midebao.BIND
Exported: true

Manifest 源码:app/src/main/AndroidManifest.xml

客户端使用带包名限定的 Action 绑定:

Intent intent = new Intent("com.kooo.evcc.midebao.BIND");
intent.setPackage("com.kooo.evcc");
bindService(intent, connection, Context.BIND_AUTO_CREATE);

3. 整体数据链路

flowchart LR
    A["VHAL / APVP / 车机 AIDL / CAPI / AdaptAPI / SOA"] --> B["EVCC 统一 Property"]
    V["EVCC 自动化变量"] --> C["米德宝项目匹配"]
    B --> C
    C --> D["提取 rawValue / map 映射 / 去重"]
    D --> E["v2 全量 JSON 快照"]
    E --> F["IMidebaoLinkBridge"]
    F --> G["开放读取客户端"]
    G --> H["数据面板 / 规则判断 / 客户端自有动作"]
    W["用户授权写入 App"] -->|"writeProperty"| F
    F --> I["VHAL / APVP / AIDL / CAPI / AdaptAPI / SOA 写入"]

如果某个属性恰好来自车机 AIDL,整体会经过两次 Binder 跨界:

车机 ecarxcar_service
    -> EVCC AidlManager
    -> EVCC 统一 Property
    -> MidebaoLinkageManager
    -> EVCC IMidebaoLinkBridge
    -> 外部读取客户端

其他属性源只会经过最后一次跨应用 AIDL。

4. AIDL 接口定义

4.1 主接口

客户端通过主接口读取 EVCC 状态、注册回调;获得写入授权的客户端还可写入车辆属性。

package com.kooo.evcc.midebao;

import com.kooo.evcc.midebao.IMidebaoLinkCallback;

interface IMidebaoLinkBridge {
    int getProtocolVersion();
    String getStatus();
    String getSnapshotJson();
    void registerCallback(IMidebaoLinkCallback callback);
    void unregisterCallback(IMidebaoLinkCallback callback);
    boolean writeProperty(int propertyId, int areaId, String value);
}

源码:shared/src/main/aidl/com/kooo/evcc/midebao/IMidebaoLinkBridge.aidl

4.2 回调接口

EVCC 通过回调接口主动通知客户端。

package com.kooo.evcc.midebao;

oneway interface IMidebaoLinkCallback {
    void onSnapshotChanged(String snapshotJson);
    void onStatusChanged(String status);
}

源码:shared/src/main/aidl/com/kooo/evcc/midebao/IMidebaoLinkCallback.aidl

EVCC 与每个客户端保存的 .aidl 必须保持一致。

5. Binder ABI

5.1 主接口 ABI

接口描述符:

com.kooo.evcc.midebao.IMidebaoLinkBridge
方法 Transaction code 调用方式 Parcel 输入 Parcel 返回
getProtocolVersion() FIRST_CALL_TRANSACTION + 0 同步 无 int
getStatus() +1 同步 无 String
getSnapshotJson() +2 同步 无 String
registerCallback(callback) +3 同步 Callback Binder 无
unregisterCallback(callback) +4 同步 Callback Binder 无
writeProperty(propertyId, areaId, value) +5 同步 int, int, String boolean

所有主接口方法均使用普通同步 transact(..., flags = 0):

  • Proxy 创建请求 Parcel 和回复 Parcel;
  • 请求 Parcel 首先写入接口描述符;
  • Stub 使用 enforceInterface() 校验描述符;
  • 成功时 Stub 写入 writeNoException() 和返回值;
  • SecurityException 和 RemoteException 可通过 Binder 回传。

5.2 回调接口 ABI

回调接口描述符:

com.kooo.evcc.midebao.IMidebaoLinkCallback
方法 Transaction code 调用方式 Parcel 输入
onSnapshotChanged(snapshotJson) FIRST_CALL_TRANSACTION + 0 FLAG_ONEWAY String
onStatusChanged(status) +1 FLAG_ONEWAY String

oneway 含义:

  • EVCC 只写请求 Parcel,不创建回复 Parcel;
  • EVCC 不等待客户端完成 JSON 解析或规则执行;
  • 协议层没有 ACK,单次回调失败不会重发该消息。

6. 绑定与握手时序

sequenceDiagram
    participant G as "AIDL 读取客户端"
    participant A as "Android Service"
    participant E as "EVCC Bridge"

    G->>A: bindService(action + package)
    A->>E: onCreate / onBind
    E-->>G: IBinder

    G->>E: getProtocolVersion()
    E-->>G: 2

    G->>E: registerCallback(callback Binder)
    E--)G: onStatusChanged(currentStatus)
    E--)G: onSnapshotChanged(currentSnapshot)
    E-->>G: registerCallback 返回

    G->>E: getSnapshotJson()
    E-->>G: 当前全量 JSON

    loop 属性或变量发生变化
        E--)G: onSnapshotChanged(fullSnapshot)
    end

    opt 用户已授权该 App 写入
        G->>E: writeProperty(propertyId, areaId, value)
        E-->>G: true / false
    end

银河悬浮歌词参考客户端当前的具体顺序是:

  1. 将 IBinder 转换为 IMidebaoLinkBridge;
  2. 调用 getProtocolVersion();
  3. 调用 registerCallback();
  4. 调用 getSnapshotJson() 主动拉取当前快照;
  5. 之后依赖 onSnapshotChanged() 接收变化。

registerCallback() 内部会立即发送一次状态和快照,客户端随后又主动拉取一次。因此首次连接通常会处理两份内容相同或接近的快照。

其他客户端可以只依赖 registerCallback() 的首次推送,也可以在注册后再调用 getSnapshotJson()。无论采用哪种方式,都应把快照当作全量状态并允许重复处理。

7. 主接口方法语义

7.1 getProtocolVersion()

当前固定返回:

2

它表示跨应用快照协议版本,不是 EVCC 应用版本,也不是车机 AIDL 版本。

旧版读取客户端只把该值写入日志,尚未强制验证 version == 1;由于 v2 只在接口末尾追加方法,原有五个方法的 transaction code 保持不变。

7.2 getStatus()

返回人类可读的运行状态,例如:

未启用
运行中,已加载 3 个项目
配置错误: ...

该字段是用户可读文本,不是稳定枚举。客户端不应通过匹配具体中文来决定业务逻辑。

7.3 getSnapshotJson()

返回 EVCC 内存中最新的一份全量快照。

该方法:

  • 不会因为本次调用而强制刷新车辆属性;
  • 不等待上游属性源完成连接;
  • 适用于首次同步、断线恢复和调试查询。

7.4 registerCallback(callback)

服务端将 Callback Binder 注册到 RemoteCallbackList,然后立即调用:

  1. onStatusChanged(statusText);
  2. onSnapshotChanged(snapshotJson)。

RemoteCallbackList 可在客户端 Binder 死亡后清理失效回调。

7.5 unregisterCallback(callback)

客户端正常停止时主动注销。如果客户端异常退出,Binder 死亡后也会由 RemoteCallbackList 清理。

7.6 writeProperty(propertyId, areaId, value)

仅用户在 EVCC 的“允许写入的应用”列表中勾选并保存的 App 可以调用。参数语义:

  • propertyId:目标车辆属性 ID,必须为正整数;
  • areaId:目标区域 ID,不得小于 0;
  • value:按目标属性数据类型解析的原始字符串,最大 4096 个字符。

EVCC 按以下优先级寻找精确匹配的 propertyId + areaId:

VHAL -> APVP -> AIDL -> CAPI -> AdaptAPI -> SOA

一旦某个来源命中,该来源的执行结果就是最终结果;写入失败后不会继续向下一来源重复下发。成功返回 true,参数无效、未找到属性、数据源拒绝、解析失败、异常或 5 秒超时均返回 false。未通过写入鉴权时不是返回 false,而是抛出 SecurityException。

该方法不限制目标必须出现在 AIDL 快照配置中。给某个 App 授权,意味着允许它尝试写入 EVCC 当前数据源能够识别的任意车辆属性,因此只应选择明确可信且确有写入需求的 App。

服务端入口:app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkBridgeService.kt;快照与回调:app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkageManager.kt

8. JSON 快照协议

8.1 正常运行快照

{
  "version": 2,
  "enabled": true,
  "updatedAt": 1786075800000,
  "items": [
    {
      "key": "gear",
      "name": "档位",
      "hasValue": true,
      "value": "D",
      "rawValue": "3",
      "updatedAt": 1786075799123
    },
    {
      "key": "speed",
      "name": "车速",
      "hasValue": true,
      "value": "52",
      "rawValue": "52",
      "updatedAt": 1786075799456
    }
  ]
}

8.2 停用快照

{
  "version": 2,
  "enabled": false,
  "status": "未启用",
  "updatedAt": 1786075800000,
  "items": []
}

8.3 顶层字段

字段 类型 必需 语义
version int 是 快照协议版本,当前为 2
enabled boolean 是 属性联动的启用状态
status string 否 停用或状态说明
updatedAt long 是 本次快照生成时间,Unix 毫秒
items array 是 当前所有已配置项目的完整列表

8.4 项目字段

字段 类型 必需 语义
key string 是 稳定且唯一的业务标识,客户端规则依赖该值
name string 是 用户可见名称
hasValue boolean 是 是否已经成功读取有效值
value JSON 值 是 经过 map 映射后的值
rawValue string 是 映射前的原始字符串
updatedAt long 是 该项目最后变化时间,未取到值时为 0

未取到值的项目:

{
  "key": "speed",
  "name": "车速",
  "hasValue": false,
  "value": "",
  "rawValue": "",
  "updatedAt": 0
}

value 通常为字符串,但映射 JSON 也可使其成为数字或布尔值。当前客户端最终通过 String.valueOf() 将其统一转成字符串。新配置应将 value 限制为标量值,不应在其中嵌套对象或数组。

9. 客户端宽松解析行为

当前客户端使用 JSONObject.opt*() 宽松解析:

  • 缺少 enabled:按 false 处理;
  • 缺少 items:按空数组处理;
  • key 缺失或为空:丢弃该项目;
  • name 缺失或为空:使用 key;
  • value 和 rawValue:统一转成字符串;
  • 缺少 hasValue:根据 updatedAt 和值是否为空推断;
  • 额外未知字段:忽略;
  • 顶层 version:当前未强制验证。

因此,JSON 格式适合通过“只追加新字段”的方式扩展,不适合直接修改已有字段的类型或语义。

10. 值处理与推送时机

10.1 属性匹配

属性项目按下列条件精确匹配:

item.propertyId == property.id
&&
item.areaId == property.areaId

匹配后的处理顺序:

  1. 提取 rawValue;
  2. 通过 map[rawValue] 查找映射值;
  3. 未命中映射时使用原值;
  4. 与内存中上一个原值和映射值比较;
  5. 值未改变时不生成新快照。

10.2 推送策略

  • realtime=true:变化后立即生成并推送快照;
  • realtime=false:300 毫秒内的多次变化合并为一次推送;
  • 自动化变量:事件监听之外,每 1 秒补查一次;
  • 初始值未读取成功:前 60 秒每 2 秒重试,之后每 10 秒重试;
  • 每次 onSnapshotChanged() 都传输完整 items 数组,不是单属性增量。

10.3 AIDL 数据配置

EVCC 中“米德宝 AIDL 联动”和“旧版 AIDL 联动”共用同一份项目配置。顶层格式:

{
  "version": 1,
  "items": [
    {
      "key": "speed",
      "name": "车速",
      "propertyId": 557849089,
      "areaId": 0,
      "realtime": true
    },
    {
      "key": "music_bpm",
      "name": "音乐 BPM",
      "variable": "bpm",
      "map": {
        "0": "未识别"
      },
      "realtime": true
    }
  ]
}

这里的配置文件 version 仍为 1,它只表示本地项目配置结构;Binder 接口和运行时快照的协议版本为 2。两者独立升级,客户端不要混用。

每个项目必须满足:

  • key 非空且在整份配置中唯一;客户端规则应依赖 key,不要依赖显示名;
  • name 非空,用于界面显示;
  • propertyId 和 variable 必须且只能填写一个;
  • propertyId 可为 JSON 数字、十进制字符串或 0x 十六进制字符串;运行时统一解析为 32 位正整数,使用表单编辑新项目时会写成十进制数字,直接导入的原始 JSON 则会按原字段形式保存并导出;
  • areaId 仅用于属性,省略时为 0;
  • variable 使用不带 $ 的 EVCC 变量名,可读取自定义、时间、GPS、bpm 和 music_state;
  • map 可选,键按原始字符串精确匹配,值应使用字符串、数字、布尔或 null 等标量;
  • realtime 省略时为 false。

配置至少需要一个有效项目。导入文件最大为 8 MiB;EVCC 会在校验后去除格式空白,去除后的有效配置最大为 5 MiB。支持带 UTF-8 BOM 的 JSON;校验失败时不会覆盖当前配置。导出文件写入:

Download/EVCC/exports/aidl_linkage/EVCC_AIDL_linkage_yyyyMMdd_HHmmss_SSS.json

“恢复默认”会按当前车型读取云端车型配置中的 AIDL 数据默认值;车型没有配置时使用 EVCC 内置示例。

11. 连接生命周期与恢复

客户端遇到以下事件时会清除当前 Bridge 并进入重连:

  • onServiceDisconnected();
  • onBindingDied();
  • onNullBinding();
  • 握手方法抛出异常。

当前重连间隔:

5,000 ms

协议没有持久化消息队列或离线重放。断线期间的中间变化不会逐条补发;恢复连接后,客户端直接获得当前最新的全量状态。

12. 读取开放与写入鉴权

Service 为 exported=true。v2 把读取和写入权限彻底分开。

12.1 读取权限

  • 总开关和“旧版 AIDL 联动”同时开启时,getProtocolVersion()、getStatus()、getSnapshotJson()、registerCallback() 和 unregisterCallback() 不校验调用 App 的包名或签名,任何能绑定 Service 的 App 均可读取;
  • “旧版 AIDL 联动”关闭时,开放读取立即停止;如果“米德宝 AIDL 联动”仍开启,则只允许包名和证书都匹配的银河悬浮歌词继续读取;
  • 总开关关闭时,两种读取出口都停止。

因此,自定义读取快照中不应配置不希望其他车机 App 获知的数据。开放读取只影响上述五个读取/订阅方法,不会自动获得属性写入能力。

12.2 写入权限

普通旧版外部应用写入必须同时满足:

  1. 总开关已开启;
  2. “旧版 AIDL 联动”已开启;
  3. 用户在“允许写入的应用”弹窗中勾选了该 App;
  4. 当前 Binder UID 对应的包名与保存时固定的 APK 签名 SHA-256 均匹配。

App 选择支持多选。保存时 EVCC 读取每个已安装 App 的签名并与包名一起固定;空名单表示普通旧版外部 App 均为只读。旧版本中曾用于读取的调用方不会自动迁移为可写 App,升级后仍需用户主动选择。

当前官方银河通道另有包名和签名校验的属性写入规则:开启总开关和“米德宝 AIDL 联动”后可写入,不依赖普通旧版应用名单。新增音乐权限独立于属性读写权限,仍仅向身份明确的银河开放。

每次 writeProperty() 调用的验证流程:

Binder.getCallingUid()
    -> 枚举 UID 对应包名
    -> 在用户保存的多应用写入名单中查找包名
    -> 读取目标 APK 当前签名
    -> 计算并规范化 SHA-256
    -> 任一实际摘要命中该包名允许列表才放行

未通过验证时抛出 SecurityException,不会进入车辆属性写入链路。

鉴权实现:app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkBridgeService.kt

银河悬浮歌词通道的允许摘要可通过车辆适配配置更新;自定义写入名单由用户在 EVCC 界面管理。不要把具体证书摘要写进公开业务 payload 或当作协议字段传给客户端。

需要注意:

  • bindService() 本身不执行读取或写入鉴权;
  • 自定义读取开放时,调用读取方法和注册回调都不要求授权;
  • 写入名单只保护 writeProperty(),不会限制已开放的读取;
  • 关闭自定义 AIDL 后,之前开放注册的回调不再接收后续推送;
  • App 卸载后换证书重装,即使包名相同也不能沿用原写入授权。

13. 协议不提供的能力

当前 AIDL 不包含:

  • 调用 EVCC 自动化动作;
  • 反向发送机器人指令;
  • 写入 EVCC 变量或修改 AIDL 数据配置;
  • 属性元数据,如 propertyId、areaId、数据类型和访问权限;
  • 属性来源,如 VHAL、APVP 或 AIDL;
  • 消息序号、ACK、重放、分片和压缩。

客户端收到快照后,可在本地根据 key 和 value 展示或评估规则;如需写入车辆属性,必须使用 v2 的 writeProperty() 并由用户授权。其他后续动作仍由客户端自己的能力执行。

14. 协议升级约束

本接口是普通应用 AIDL,不是 Android Stable AIDL,没有 @VintfStability 或系统级 ABI 稳定保证。

修改时必须遵守:

  1. 不得调整现有 AIDL 方法顺序;
  2. 不得删除或修改现有方法的参数和返回类型;
  3. 新方法只能追加到接口末尾;
  4. EVCC 和客户端必须同步同一份 .aidl;
  5. 不兼容变更必须提升 PROTOCOL_VERSION;
  6. JSON 新能力应优先通过追加可选字段实现;
  7. 保留 key / name / hasValue / value / rawValue / updatedAt 的现有语义。

由于旧版客户端只记录版本号而不拒绝未知版本,v2 保留了原有读取方法顺序和 JSON 字段语义。新客户端仍应增加明确的版本范围检查,并且只有 getProtocolVersion() >= 2 时才调用 writeProperty()。

15. 当前已知边界

15.1 属性配置不能指定来源

配置只有 propertyId 和 areaId,没有 source。填写某个属性 ID 不代表强制从车机 AIDL 读取。

初始主动读取优先级:

VHAL -> APVP -> AIDL -> CAPI -> AdaptAPI -> SOA

实时阶段同时监听五个来源。如果不同来源存在相同的 propertyId + areaId,任何来源都可能更新同一个快照项目。

读取逻辑:app/src/main/java/com/kooo/evcc/miniprogram/MiniProgramPropertyHelper.kt

15.2 写入不能指定来源

writeProperty() 同样没有 source 参数,并按固定优先级寻找精确匹配的属性。一旦命中某个来源,即使写入失败也不会回退到下一来源。这能避免同一指令被多个数据源重复执行,但调用端无法用该接口强制选择某个来源。

写入逻辑:app/src/main/java/com/kooo/evcc/miniprogram/MiniProgramPropertyHelper.kt

15.3 没有快照序号

协议只有时间戳,没有单调递增的 sequence。客户端当前也不会根据顶层 updatedAt 丢弃过期快照。

由于首次注册时同时存在回调推送和主动拉取,若握手期间恰好发生属性更新,理论上可能短暂交错处理新旧快照。

后续可在顶层增加单调 sequence,客户端忽略不大于当前序号的快照。

15.4 协议版本未强制检查

getProtocolVersion() 和 JSON 中的 version 均存在,但旧版客户端没有拒绝未知版本。它们目前主要用于日志和人工识别;v2 写入客户端必须主动检查版本。

15.5 配置错误时 enabled 语义不严谨

当配置解析失败时,当前实现会:

  • 停止属性监听并将内部 running 设为 false;
  • 通过状态回调发送“配置错误”;
  • 但生成的 JSON 快照仍可能是 enabled: true。

因此客户端不应仅依赖 enabled 判断链路健康度。后续宜将配置错误快照改为 enabled: false,或增加稳定的机器可读 state/errorCode。

15.6 多种联动方式独立运行

“米德宝 BLE 直发联动”“米德宝 AIDL 联动”和“旧版 AIDL 联动”是三个独立子开关:

  • BLE 直发和 AIDL 可以同时开启;
  • 米德宝 AIDL 与自定义 AIDL 可以同时开启;此时自定义出口开放读取同一份快照;
  • 关闭某个子开关只停止对应出口,不应被理解为清空公共 AIDL 数据配置;
  • 总开关关闭时所有联动出口一起停止。

运行调度:app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkageRuntime.kt

15.7 无分片与流量控制

快照使用单个 Binder String 传输,没有分片、压缩和背压。协议只适合少量车辆信号,不应用来传输大量历史数据或二进制内容。

16. 后续协议改进方向

如果后续继续升级协议,建议优先考虑:

  1. 顶层增加单调递增的 sequence;
  2. 增加机器可读的 state 和 errorCode;
  3. 客户端明确校验支持的协议版本范围;
  4. 将首次同步收敛为一条明确路径,避免回调与主动拉取重复;
  5. 如需强制属性数据源,在 EVCC 配置端增加 source,但是否对外传输需单独评估;
  6. 修正配置错误时的 enabled / 运行状态语义;
  7. 如项目数量显著增长,再评估增量快照或更紧凑的 Parcelable 协议。

17. 源码索引

内容 文件
主 AIDL shared/src/main/aidl/com/kooo/evcc/midebao/IMidebaoLinkBridge.aidl
回调 AIDL shared/src/main/aidl/com/kooo/evcc/midebao/IMidebaoLinkCallback.aidl
Binder Service app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkBridgeService.kt
快照生成与回调 app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkageManager.kt
协议版本与项目配置 app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkageConfig.kt
多出口运行调度 app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkageRuntime.kt
多数据源属性读取与写入 app/src/main/java/com/kooo/evcc/miniprogram/MiniProgramPropertyHelper.kt
AIDL 配置管理、导入与导出 app/src/main/java/com/kooo/evcc/midebao/MidebaoLinkageCardController.kt

银河悬浮歌词参考实现位于 Galaxy-Robot-Midebao-Car-Machine-Linkage 仓库的:

app/src/main/aidl/com/kooo/evcc/midebao/
app/src/main/java/com/galaxy/lyricsnext/GeelyCarDataPanelService.java