高通 GAIA v1/v2/v3 共存时,Android 端该怎么做协议分层

0 阅读9分钟

做高通耳机 App,如果只面对单一设备、单一协议版本,事情通常还没那么复杂。你知道设备支持什么命令,知道 characteristic 怎么收发,知道回包长什么样,按这套协议一路写下去,很多功能都能跑起来。

真正让项目开始变重的,往往不是 BLE 本身,而是协议版本开始共存。

一旦项目里同时要考虑 GAIA v1/v2/v3,Android 端就不能再把协议当成一份固定规则来写了。这个时候最怕的不是“多几个 if”,而是你没有意识到版本共存本质上是在逼 App 做协议分层。

如果这一步没做好,前期也许还能勉强跑,后面设备型号一多、功能一多、OTA 一接,整个代码就会迅速变成一团:命令发错版本、回包走错处理器、初始化顺序互相干扰,最后你看到的就不只是业务 bug,而是整套协议系统开始失控。

高通这类耳机项目最容易踩的坑,就是一开始把协议理解成“BLE 上面的一组命令码”。这种理解不算完全错,但只够支撑早期 demo。真到了多版本共存阶段,GAIA 更像一套完整的通信框架,而不是几条命令。

从你现在这个项目里,其实已经能很明显看出这种分层思路。

最底层是传输层。代码里有 LeGattClientRfcommClient,也就是 BLE 和 RFCOMM 两种不同链路。这个设计本身就说明,协议层不应该直接绑死在某一种蓝牙传输上。因为对于上层来说,重点不是“底下是 BLE 还是经典蓝牙”,而是“有没有一条稳定的数据通道能把 GAIA 包送上来、送下去”。

这一步如果不拆,后面协议逻辑就会和 BLE characteristic、RFCOMM socket 这些细节硬耦合在一起。短期看省事,长期看几乎必炸。

再往上一层是收发层。你项目里有 GaiaReaderGaiaSender,这层很关键。因为它把“底层收发字节流”变成了“上层读写 GAIA 包”。

这个抽象听起来普通,但实际上非常重要。协议系统如果没有独立的 reader / sender,后面所有版本逻辑、vendor 逻辑、plugin 逻辑都会被迫围着原始字节写。只要某一层直接碰底层字节,系统边界很快就会开始模糊。

协议分层真正开始体现价值的,是版本层。

你项目里同时有:

  • V1V2Vendor

  • V3Vendor

这个结构其实就已经给了答案。

GAIA v1/v2/v3 共存时,Android 端最重要的一件事,不是“兼容所有命令”,而是先把不同版本的协议处理入口拆开。

因为版本共存最怕的事情就是混用。

比如某个设备上报的是 GAIA_VERSION = 3,但你的某个初始化流程还在按 v1/v2 的处理逻辑走;或者某段业务代码根本不管当前版本,直接按老的 packet 格式去拼命令;再或者某些插件初始化时默认自己一定跑在某个版本之上,结果遇到另一种设备就开始状态错乱。

这种问题如果只是偶尔出一个,很难定位。因为表面现象可能只是:

  • 某个命令没响应

  • 某个设备状态不更新

  • 某个插件一直 ready 不起来

  • OTA 在某一步失败

但根因其实是版本路由错了。

所以协议分层的第一原则其实很简单:

先判断版本,再分派能力,不要让业务逻辑自己猜版本。

这一点你项目日志里已经有很直观的证据:


GT BLE device info: info=GAIA_VERSION, update=3

[onStarted] stopping V1V2Vendor, gaiaVersion=3

这两句说明当前实现做对了一件很重要的事:连接建立以后,不是让所有 vendor 都同时尝试接管协议,而是先识别版本,再决定谁该退出、谁该继续工作。

这个模式其实就是版本分层最核心的思想。

不是所有逻辑都支持所有版本,而是每个版本应该有自己的边界。

从架构上看,我觉得 v1/v2/v3 共存时,Android 端至少要拆成这几层。

第一层是 Transport Layer

这一层只负责链路本身,比如 BLE GATT 还是 RFCOMM,连接状态怎么回调,断开怎么恢复,底层字节怎么发出去。它不该知道什么是 v1/v2/v3,也不该关心某个命令是不是升级命令。

第二层是 Protocol IO Layer

这一层负责包的读取和发送,也就是类似 GaiaReaderGaiaSender 这种角色。它的职责是把 transport 提供的字节流变成可处理的协议输入,把上层生成的协议输出重新送回 transport。

第三层是 Version Routing Layer

这一层非常关键。它应该负责:

  • 当前设备 GAIA 版本识别

  • 不同版本实现的启停

  • 当前协议上下文归属

如果没有这层,版本判断很容易散落到各个业务类里,最后变成 everywhere 的 if (version == 3) ... else ...。这种代码后期会非常难维护。

第四层是 Vendor / Plugin Layer

这一层才是真正承载设备功能的地方。你项目里已经有 V1V2VendorV3Vendor 以及对应的 plugin 体系,这说明它不是按“一个命令一个类”在组织,而是在按能力模块组织。这个方向是对的,因为耳机类项目的命令并不是孤立存在的。电量、ANC、EQ、设备信息、升级,这些都更适合作为模块管理,而不是散成一堆 util。

第五层是 Business Layer

也就是 ViewModel、UI、设置页、升级页这些面向用户的逻辑。它最不应该知道的,就是底层 packet 到底长什么样、当前 characteristic 是哪个 UUID、当前 GAIA 版本该怎么判断。业务层应该拿到的是“设备支持哪些能力”“当前状态是什么”“我能不能发这个操作”。

换句话说,业务层永远不该直接依赖协议版本判断。

这件事说起来很抽象,但在项目里其实特别具体。比如一个“读取电量”的需求,如果没有协议分层,最后就很容易变成这样:


if (gaiaVersion == 3) {

// send v3 packet

} else {

// send v1/v2 packet

}

看起来就两行判断,好像没什么问题。但这种写法只要一多,版本逻辑就开始污染业务层。后面再加一个“电池盒状态”“双耳电量”“佩戴状态”,代码只会越来越散。

更合理的思路是把差异留在版本层或者 plugin 层,业务层只拿统一接口:


interface BatteryFeature {

fun requestBattery()

}

然后由不同版本实现各自适配:


class BatteryFeatureV3(...) : BatteryFeature {

override fun requestBattery() {

// send v3 packet

}

}

class BatteryFeatureV1V2(...) : BatteryFeature {

override fun requestBattery() {

// send v1/v2 packet

}

}

最后由版本路由层决定给业务层注入哪个实现。

这样写的好处很明显。业务层不会被协议版本污染,版本差异被收敛在更靠下的地方,后面加设备、加功能、加版本时,扩展成本会低很多。

很多 GAIA 项目到后期不好维护,本质上都不是因为协议太复杂,而是因为一开始没把“版本共存”当成架构问题,而只是把它当成“多做几个兼容判断”。

这两个思路的差别非常大。

如果你把版本共存当成兼容判断,最终代码一定会往四处扩散。

如果你把版本共存当成协议分层问题,很多复杂度其实会被收敛起来。

除了版本层,插件初始化顺序也是这类项目里很重要的一层。

你现在项目日志里还有这种状态:


basicPluginReady=false, upgradePluginReady=false

这说明系统并没有把“连接成功”直接等同于“所有协议能力都 ready 了”。这是对的。

因为在多版本共存场景下,不同插件很可能依赖不同的前置条件:

  • 设备版本是否识别完成

  • 对应 vendor 是否已接管

  • 基础设备信息是否同步完成

  • 升级功能是否完成初始化

如果没有这些状态边界,业务层就很容易在错误的时间点发命令,最后看起来像“设备没响应”,其实只是协议系统还没准备好。

所以协议分层里还有一个容易被忽略的原则:

ready 也是分层的一部分。

不是只有连接状态才需要状态机,协议版本、插件能力、升级能力也都应该有自己的 ready 状态。

这一点对 OTA 尤其重要。因为 OTA 一旦介入,系统就不只是“普通控制协议”了,而是要进入一个更严格的升级上下文。你项目日志里已经能看到 UpgradeFail,这恰恰说明 OTA 并不是一个孤立功能,而是建立在版本识别、插件初始化、协议发送、连接稳定性之上的一条复合链路。

也正因为如此,GAIA v1/v2/v3 共存的 Android 端设计,最忌讳的是直接把 OTA 也揉进普通命令通道逻辑里。升级相关能力最好还是作为一个相对独立的 plugin 或模块,在统一的版本路由和连接状态之上工作,而不是四散在业务代码里。

如果把今天这篇文章压成一句更直白的话,我会这样说:

GAIA 多版本共存时,Android 端真正该做的不是“把所有版本都支持一遍”,而是“把不同版本的复杂度隔离到合适的层里”。

BLE 负责通道,Reader / Sender 负责收发,Version Router 负责判版本,Vendor / Plugin 负责功能实现,业务层只消费统一能力。

这个边界一旦清楚,项目虽然还是复杂,但复杂得有秩序。

反过来,如果这些层混在一起,哪怕一开始能跑,后面设备一多,协议一变,整个工程很快就会被版本差异拖垮。

高通耳机 App 这类项目为什么不好写,很多时候不是因为某个命令太难,而是因为一旦进入多版本共存阶段,Android 端实际上已经不再是在写“蓝牙功能”,而是在写一套长期可演进的协议框架。