3、GATT层避坑:Service/Characteristic设计、MTU大小协商、通知与指示的选择

GATT层,说白了就是BLE协议栈里最贴近应用的那一层。你写的业务逻辑,最终都要通过Service和Characteristic暴露给对端。这一层要是设计不好,后面联调起来会非常痛苦。我这些年踩过的坑,有一半都跟GATT有关。

咱们直接进入正题,聊聊三个最核心的问题:怎么设计Service和Characteristic、MTU怎么协商、通知和指示到底选哪个。

3.1 Service/Characteristic设计:别把结构搞复杂了

很多人一上来就喜欢设计一个巨大的Service,里面塞十几个Characteristic。嗯,我刚开始做项目时也这么干过。结果呢?对端设备扫描服务列表时,耗时特别长,而且有些低端手机直接解析失败。

核心原则:一个Service只干一件事。

举个例子,如果你要做个智能灯,那就拆成两个Service:

  • Light Control Service:开关、亮度、色温
  • Device Information Service:固件版本、序列号、电量

这样设计,对端可以按需读取,不用一次性拉完所有数据。

我的习惯:每个Service的UUID尽量用16位标准UUID,别用128位自定义的。除非你确定需要防冲突,否则16位UUID在广播包和扫描响应里能省不少空间。

Characteristic的设计也有讲究。我建议你遵循以下几条:

  • 一个Characteristic只代表一个属性。比如“亮度”和“色温”不要放在同一个Characteristic里,否则解析逻辑会变得很复杂。
  • 合理设置属性。Read、Write、Notify、Indicate这些属性别乱加。只读的就别给Write权限,否则对端写进来你还要做错误处理。
  • 长度控制。单个Characteristic的值长度,尽量控制在20字节以内。为什么?因为MTU协商之前,默认MTU只有23字节,扣掉3字节的ATT头,一次最多传20字节。超过这个长度,就要走分片,效率低还容易出错。
我曾经踩过的坑:有个项目,我把一个100字节的配置数据放在一个Characteristic里,结果对端设备每次读取都要分5次。后来发现,那个设备根本不支持长读取(Read Blob),直接返回错误。最后只能改成多个Characteristic分段存储。

3.2 MTU大小协商:别用默认值,但别贪心

MTU(Maximum Transmission Unit)是GATT层最重要的参数之一。默认MTU是23字节,其中ATT协议头占3字节,实际有效载荷只有20字节。如果你不做MTU协商,每次读写都只能传20字节,效率非常低。

MTU协商的流程:

  1. 客户端(通常是手机)发起MTU请求,告诉服务端“我能支持的最大MTU是X”。
  2. 服务端回复MTU响应,告诉客户端“我能支持的最大MTU是Y”。
  3. 双方取较小值作为本次连接的MTU。

举个例子:手机说我能支持512,设备说我能支持247,那最终MTU就是247。有效载荷变成244字节,比默认的20字节大了12倍。

MTU协商的关键点:

  • MTU协商必须在连接建立后尽快发起,最好在发现服务之前。
  • MTU值不是越大越好。MTU越大,单次传输的数据越多,但出错重传的代价也越大。
  • 不同芯片的MTU上限不同。比如Nordic nRF52系列最大支持247,TI CC2640最大支持255,有些老芯片只支持128。

我个人的建议是:MTU设到247就够了。为什么?因为BLE 4.0/4.1的规范里,MTU最大就是247。虽然BLE 4.2以后支持更大的MTU(比如512),但很多老设备不支持。设到247,兼容性最好。

避坑指南:我曾经在一个项目里,把MTU设成了512,结果对端是个BLE 4.0的旧设备,直接不响应MTU请求。后来查了协议栈日志才发现,那个设备根本不支持MTU协商,默认一直用23字节。从那以后,我每次都会先检查对端设备的BLE版本。

3.3 通知与指示的选择:别搞混了,后果很严重

通知(Notification)和指示(Indication)是GATT层最常用的两种数据推送方式。很多人觉得它们差不多,其实差别很大。

特性 通知(Notification) 指示(Indication)
是否需要确认 不需要 需要(对端必须回复确认)
传输速度 快(无确认,连续发送) 慢(必须等确认才能发下一条)
可靠性 低(可能丢包) 高(确认机制保证送达)
适用场景 实时数据流(心率、加速度计) 关键事件(报警、配置变更)
对端负载 低 高(需要处理确认逻辑)

你想想看,如果你在做心率监测,每秒需要发送10次数据,用指示的话,每次都要等确认,速度根本跟不上。这时候必须用通知。

反过来,如果你在做门锁报警,报警事件必须确保送达,丢了就出大事了。这时候必须用指示。

我曾经踩过的坑:有个智能门锁项目,我用通知来发送开锁指令。结果测试时发现,有5%的概率手机收不到通知,门锁开了但手机没显示。后来改成指示,问题解决了。但代价是,开锁响应时间从50ms变成了200ms。嗯,这就是取舍。

还有一个容易忽略的点:CCCD(Client Characteristic Configuration Descriptor)。无论是通知还是指示,对端都必须先写入CCCD来使能。很多新手忘了这一步,结果数据发不出去。

CCCD的写入值:

  • 使能通知:写入0x0001
  • 使能指示:写入0x0002
  • 禁用:写入0x0000

我建议你在协议栈初始化时,就把CCCD的初始值设为0x0000。等对端主动写入使能后,再开始发送数据。这样最安全。

3.4 知识体系总览

下面这张图,把GATT层这三个核心问题的关系梳理清楚了。你可以把它当作设计时的检查清单。

GATT层核心设计要素 Service/Characteristic MTU大小协商 通知与指示 一个Service只干一件事 一个Characteristic一个属性 值长度控制在20字节内 连接后尽快发起协商 取双方较小值 建议设247,兼容性最好 通知:快但不可靠 指示:可靠但慢 别忘了写CCCD使能 设计检查清单 1. Service是否按功能拆分? 2. Characteristic属性是否设置正确? 3. MTU协商是否在服务发现前完成? 4. 通知/指示的选择是否匹配业务可靠性要求?

说白了,GATT层的设计没有银弹。每个项目都要根据实际需求做取舍。但只要你把Service结构设计清晰、MTU协商做对、通知和指示选对场景,至少能避开80%的坑。