BLE GATT接口设计:UUID、数据格式、通知与版本兼容
BLE连接成功,并不代表产品集成已经成功。应用可能发现错误的特征,把有符号测量值解析错,或在固件升级后收不到更新。可交付的GATT接口需要书面约定设备与接口标识、字节格式、交付语义、安全要求和版本变化规则。本文以自定义传感与控制接口为例,说明如何建立这份约定。
先定义接口,再开发应用
记录每个服务和特征的UUID、属性、访问权限、最大长度以及错误响应。只有当产品的含义和数据格式符合标准定义时,才复用Bluetooth SIG服务。私有语义应分配稳定的128位UUID,不要自行占用尚未分配的短UUID。SIG的数据类型规范规定,短服务UUID只能使用SIG已分配的值。
将UUID统一保存在受版本控制的注册表中,供固件、应用和测试工具共同使用。不要只用设备名称判断身份:名称可能变化,多台设备也可能使用相同名称广播。同样,不要跨固件版本硬编码属性句柄。应先发现服务,选择目标实例,再在该服务内解析所需特征。
| 特征 | 建议操作 | 需要约定的内容 |
|---|---|---|
| 协议信息 | 读取 | 数据格式主版本和次版本、能力集及最大应用帧长度 |
| 测量数据流 | 通知 | 单位、序号、时间基准和溢出策略 |
| 配置 | 读取及有响应写入 | 取值范围、授权、校验和持久化时机 |
| 命令结果 | 指示,或配合应用层确认的通知 | 事务ID、结果码及“完成”的确切含义 |
定义可以直接测试的字节格式
下面是一个12字节测量帧示例,并非蓝牙标准配置文件。所有多字节字段均采用小端序。定点温度表示可以避免依赖某种语言的浮点序列化方式。单独读取协议信息特征,即可获取次版本及支持的能力。
| 偏移 | 字段 | 定义 |
|---|---|---|
| 0 | major | 无符号8位数据格式主版本,初始为1 |
| 1 | flags | 位0表示温度有效;其他位保留并置零 |
| 2–3 | sequence | 无符号16位序号,按65536取模 |
| 4–7 | uptime_ms | 无符号32位采样时间,单位为启动后的毫秒数,按2³²回绕 |
| 8–9 | temperature | 有符号16位,每计数0.01 °C;无效时忽略 |
| 10–11 | battery_mV | 无符号16位毫伏值;65535表示不可用 |
本示例中,序号和运行时间在重启后清零;会话初始化时读取启动标识,可区分重启与计数器回绕。负温度、无效标志、截断帧和计数器回绕都应加入双方共用的标准测试向量。下面的Python解析器展示了显式校验方式:
import struct
def decode_v1(payload):
if len(payload) != 12:
raise ValueError("expected 12 bytes")
major, flags, seq, ms, temp, mv = struct.unpack("<BBHIhH", payload)
if major != 1 or flags & 0xFE:
raise ValueError("unsupported schema or flags")
return {
"sequence": seq, "uptime_ms": ms,
"temperature_C": temp / 100 if flags & 1 else None,
"battery_mV": None if mv == 65535 else mv,
}
标准向量示例为01 01 2A 00 E8 03 00 00 2E FB E4 0C:主版本1,温度有效,序号42,运行时间1000 ms,温度−12.34 °C,电压3300 mV。连接真实硬件前,应先在两端分别验证该向量。
由于严格检查了保留位,这个解析器不能静默接受新的标志语义。应保留v1帧,协商双方支持的扩展,或引入新的主版本。只有原有解析器明确允许尾部扩展时,追加字节才具有向后兼容性。

区分订阅成功和应用数据可靠交付
通知没有ATT层确认,指示则有。但这两者都不能证明测量值已写入云端数据库,或电机已经完成动作。BLE连接链路有自身的重传机制,断连、软件队列满和应用重启仍然需要端到端处理策略。用序号检测缺失样本,用事务ID匹配命令结果。底层过程见ATT规范。
通过平台API进行订阅,并检查完成结果后再将数据流标记为就绪。在GATT层,客户端特征配置描述符(CCCD,UUID为0x2902)使用0x0001启用通知,使用0x0002启用指示。订阅状态按客户端分别维护。绑定和非绑定设备的持久化规则不同,因此重连逻辑必须恢复本地回调,并确认订阅已经有效。具体规则见GATT规范。
设置有界队列,并写清过载行为:丢弃最旧样本、只保留最新值,或在产品允许的情况下暂停采集。对外提供丢样计数。命令应区分“已接收”“执行中”和“已完成”。断连后重试同一事务,不能意外让执行器动作两次。
显式处理MTU及平台差异
常规单句柄通知的值长度不能超过ATT_MTU减3字节;属性值还有独立的512字节上限。因此,在默认LE ATT MTU为23时,这类通知最多携带20字节。更大的值需要双方约定的应用层分片方案,或其他合适的传输过程。增大MTU本身不会自动选择更快的PHY,也不会自动增大链路层数据长度。参见ATT报文格式。
从Android 14开始,首个GATT客户端发起MTU请求时,协议栈会请求517,后续请求被忽略。应按协商完成回调中的结果确定帧长度,不能按请求值推断。在Apple平台,应针对具体写入类型查询maximumWriteValueLength(for:),不要套用Android的假设。参见Android BluetoothGatt参考和Apple写入长度参考。
除非所选协议栈明确支持所需并发方式,否则应顺序执行异步初始化操作。函数调用成功通常只表示请求已入队,最终结果仍在回调中。日志应同时记录协商MTU、目标特征UUID、安全状态、订阅结果和解析器版本。
规划固件和应用的跨版本兼容
数据格式版本、固件版本和GATT数据库布局是三个不同问题。对受支持的数据格式,应保持现有字段含义稳定。显式公布能力,而不是让客户端从固件版本字符串猜测功能。主版本不受支持时,应在发送控制命令前拒绝使用,并给出有效诊断信息。
若升级改变了服务数据库,就要实现适用的Service Changed及缓存处理;支持Database Hash时,可用它检测数据库变化。这些机制不会转换已经改变的应用数据格式。升级和回退测试既要覆盖已有绑定客户端,也要覆盖全新安装。数据库层要求见GATT缓存规则。
用发布测试矩阵主动暴露问题
| 测试 | 主动触发的问题 | 保留的证据 |
|---|---|---|
| 旧应用配新固件;新应用配旧固件 | 未知主版本、缺少可选字段或布局变化 | 能力读取结果及明确的接受或拒绝结果 |
| MTU为23及更大的协商MTU | 载荷过长、分片长度错误或截断 | 接收长度及标准向量解析结果 |
| 在命令执行或订阅时断连 | 重复动作或数据流静默 | 事务ID、订阅完成状态和首个有效样本 |
| 保留绑定关系后升级及回退 | 过期句柄缓存或访问错误特征 | 发现与缓存变化过程及UUID映射 |
| 慢速消费者及队列满 | 内存增长或无法解释的数据丢失 | 队列峰值和丢样统计 |
| 未经认证或授权的客户端 | 在错误安全级别接受配置 | 预期错误及配置未被更改的证据 |
数据流静默时,依次检查通知属性、订阅完成状态、权限和数据生产端是否运行。数值异常时,先将原始字节与有符号规则、倍率和字节序对照,再考虑调整射频参数。只有升级设备出错时,优先检查数据库缓存和数据格式协商。
准备可审核的集成资料
固件评审应提供UUID注册表、报文规范、标准测试向量、受支持的手机与操作系统清单,以及兼容性矩阵。欧蓓特的固件与BSP诊断服务可作为项目沟通入口。已交付的WS63无线模组集成案例展示了硬件与无线模式的范围定义背景,但不代表本文的GATT示例已在该模组上实现或验证。