2、GATT与属性协议:GATT角色定义、Service与Characteristic、UUID分配规则
好,咱们接着聊。上一章我们把蓝牙协议栈的底子打好了,这一章要进入真正干活的部分——GATT。说白了,GATT就是蓝牙设备之间怎么“聊天”的规矩。我当年第一次做蓝牙项目时,被GATT的角色定义绕得晕头转向,后来踩了几个坑才明白,其实没那么复杂。
2.1 GATT角色定义:谁说了算?
GATT定义了两种角色:GATT Server 和 GATT Client。你想想看,就像医生和病人——医生(Server)有你的病历数据,病人(Client)来查询或修改这些数据。
- GATT Server:持有数据的一方。比如按摩仪,它存储了力度、模式、电量等信息。
- GATT Client:发起请求的一方。比如手机App,它读取按摩仪的状态,或者下发控制指令。
嗯,这里要注意:一个设备可以同时是Server和Client。我在项目中就遇到过这种情况——按摩仪既是Server(给手机上报数据),又是Client(从另一个传感器读取温度)。但大多数场景下,按摩仪只做Server,手机只做Client。
核心原则:谁提供数据,谁就是Server。谁消费数据,谁就是Client。
2.2 Service与Characteristic:数据的骨架
GATT的数据组织方式,我习惯把它想象成一个文件柜:
- Service(服务):相当于一个抽屉。每个抽屉代表一类功能,比如“电池服务”、“设备信息服务”。
- Characteristic(特征):相当于抽屉里的文件夹。每个文件夹存放具体的数据项,比如“电池电量”、“设备名称”。
- Descriptor(描述符):相当于文件夹上的标签。它描述了这个特征怎么用,比如“是否支持通知”、“单位是什么”。
举个例子,按摩仪的“力度控制服务”可以这样设计:
Service: 力度控制服务 (UUID: 0xFFE0)
├── Characteristic: 当前力度 (UUID: 0xFFE1)
│ ├── Properties: Read, Write
│ └── Value: 0-100 (表示力度百分比)
│ └── Descriptor: 用户描述 (UUID: 0x2901)
│ └── Value: "当前按摩力度"
└── Characteristic: 力度档位 (UUID: 0xFFE2)
├── Properties: Read, Write, Notify
└── Value: 1-5 (表示档位)
我个人习惯,每个Service只放3-5个Characteristic。太多了,手机端解析起来麻烦,而且容易出错。我曾经见过一个项目,一个Service里塞了20多个Characteristic,调试时简直想骂人。
2.3 UUID分配规则:别撞车!
UUID就是每个Service和Characteristic的身份证号。蓝牙规范里有两类UUID:
| 类型 | 长度 | 用途 | 示例 |
|---|---|---|---|
| 16-bit UUID | 2字节 | 蓝牙联盟定义的标准服务 | 0x180A (设备信息服务) |
| 128-bit UUID | 16字节 | 厂商自定义服务 | 0000FFE0-0000-1000-8000-00805F9B34FB |
这里有个坑:千万别用16-bit UUID做自定义服务。为什么?因为16-bit UUID是蓝牙联盟分配好的,你随便用可能会跟别人的标准服务冲突。我刚开始做时图省事,用了0xFFE0这种看起来“没人用”的16-bit UUID,结果跟某款手机的蓝牙库冲突了,查了两天才发现是UUID撞车。
避坑指南:自定义Service和Characteristic,一律用128-bit UUID。别偷懒,否则后面有你哭的。
那128-bit UUID怎么生成?我建议用在线UUID生成器,或者直接用固定模板:
// 我常用的自定义UUID模板
// 基础UUID: 0000XXXX-0000-1000-8000-00805F9B34FB
// 其中XXXX替换为自定义的16位值
#define SERVICE_UUID "0000FFE0-0000-1000-8000-00805F9B34FB"
#define CHAR_UUID_LEVEL "0000FFE1-0000-1000-8000-00805F9B34FB"
#define CHAR_UUID_MODE "0000FFE2-0000-1000-8000-00805F9B34FB"
嗯,这里要注意:UUID的字节序问题。蓝牙协议里UUID是小端存储,但很多SDK帮你处理好了。如果你要手动拼数据包,记得检查一下字节序,否则会出大问题。
2.4 实战经验:按摩仪的GATT设计
好了,理论说完了,咱们看看实际怎么做。以下是我给按摩仪设计的GATT结构:
Service: 设备信息服务 (UUID: 0x180A) // 标准服务
├── Characteristic: 设备名称 (UUID: 0x2A00)
│ └── Properties: Read
└── Characteristic: 序列号 (UUID: 0x2A25)
└── Properties: Read
Service: 按摩控制服务 (UUID: 0000FFF0-...)
├── Characteristic: 开关控制 (UUID: 0000FFF1-...)
│ ├── Properties: Write, Notify
│ └── Value: 0x00(关) / 0x01(开)
├── Characteristic: 力度设置 (UUID: 0000FFF2-...)
│ ├── Properties: Read, Write
│ └── Value: 0-100
└── Characteristic: 模式选择 (UUID: 0000FFF3-...)
├── Properties: Read, Write, Notify
└── Value: 0x00(揉捏) / 0x01(敲打) / 0x02(推拿)
Service: 电池服务 (UUID: 0x180F) // 标准服务
└── Characteristic: 电池电量 (UUID: 0x2A19)
├── Properties: Read, Notify
└── Value: 0-100
我个人习惯,把标准服务和自定义服务分开。标准服务用蓝牙联盟的UUID,兼容性好。自定义服务用128-bit UUID,灵活度高。这样手机端用系统蓝牙API就能直接读取电池信息,不用写额外代码。
小技巧:给Characteristic加上Notify属性,这样数据变化时能主动推送给手机,不用手机轮询。省电又高效。
最后说一句,GATT设计没有绝对的对错,但一定要保持一致性。我见过最头疼的项目,同一个产品不同批次用的UUID都不一样,导致App要维护多个版本。所以,设计好之后,写进文档,团队所有人都按这个来。
下一章,咱们聊聊怎么用代码实现这些Service和Characteristic。到时候我会把完整的代码贴出来,一步步带你跑通。