3. GATT 与属性协议:角色、属性与服务结构
GATT 层,说白了就是 BLE 设备之间「怎么聊天、聊什么」的规矩。我刚开始接触 BLE 时,总觉得 GATT 和 ATT 是一回事,后来踩了坑才明白——ATT 是底层传输机制,GATT 是上层框架。今天咱们就把这层关系彻底理清。
3.1 GATT 角色:Server 与 Client
GATT 定义了两种角色:Server 和 Client。一个设备可以同时扮演两种角色,但在实际产品中,绝大多数场景是单向的。
| 角色 | 职责 | 典型设备 |
|---|---|---|
| GATT Server | 存储属性数据,响应读写请求,主动通知 | 传感器、心率带、温湿度计 |
| GATT Client | 发起读写请求,接收通知/指示 | 手机 App、网关、手表 |
关键点:Server 是数据提供方,Client 是数据消费方。但 Server 也可以主动「推」数据给 Client——这就是后面要讲的 Notification 和 Indication。
我记得有个项目,客户非要把传感器做成 Client,手机做成 Server。理由是「手机更强大,应该存数据」。结果发现传感器每次都要主动去手机拉数据,功耗根本压不住。后来我建议他们换回来——传感器做 Server,手机做 Client,功耗直接降了 60%。
3.2 Attribute:属性的最小单元
GATT 的一切都建立在 Attribute 之上。每个 Attribute 就是一个数据条目,包含四个字段:
- Handle:属性的唯一编号,16 位,从 0x0001 开始递增
- Type (UUID):标识属性是什么类型的数据
- Permissions:读/写/加密等访问权限
- Value:实际数据内容
你想想看,整个 GATT 数据库其实就是一张属性表。Client 通过 Handle 或 UUID 来定位属性,然后执行读写操作。
我的习惯:在项目初期,我会先画一张属性表,把每个 Handle、UUID、权限、初始值都列清楚。这比直接写代码要高效得多——至少能避免一半的调试时间。
3.3 Service 与 Characteristic
光有属性还不够,得把它们组织起来。GATT 用两层结构来管理:
- Service:一组相关功能的集合。比如「电池服务」包含电量百分比、电量状态等特征。
- Characteristic:一个具体的数据点。每个 Characteristic 包含一个 Value 属性和若干描述符(Descriptor)。
举个例子,一个温湿度计的服务结构:
Service: 环境监测 (UUID: 0x181A)
├── Characteristic: 温度 (UUID: 0x2A6E)
│ ├── Value: 当前温度值 (2 bytes, int16)
│ ├── CCC Descriptor: 使能通知
│ └── 用户描述: "Temperature"
└── Characteristic: 湿度 (UUID: 0x2A6F)
├── Value: 当前湿度值 (2 bytes, uint16)
├── CCC Descriptor: 使能通知
└── 用户描述: "Humidity"
注意:每个 Characteristic 必须包含一个声明属性(Declaration)和一个值属性(Value)。CCC 是可选的,但如果你需要通知功能,就必须加。
3.4 UUID:区分属性的身份证
UUID 就是属性的身份证。BLE 用了两种:
- 16-bit UUID:蓝牙 SIG 定义的标准服务/特征,比如电池服务 0x180F、电量百分比 0x2A19
- 128-bit UUID:自定义服务/特征,格式为 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
我见过不少团队在自定义 UUID 时随便写,结果不同产品之间 UUID 冲突了。嗯,这里要注意——自定义 UUID 必须用随机生成器,别自己编。
经验之谈:能用标准 UUID 就别自己造。标准服务在 iOS 和 Android 上都有系统级支持,省很多事。我做过一个项目,用了自定义的电池服务,结果 iOS 不识别,还得自己写解析逻辑——纯粹给自己找麻烦。
3.5 Handle:属性的物理地址
Handle 是属性在 GATT 数据库中的索引,从 0x0001 开始顺序分配。Client 通过 Handle 直接访问属性,比用 UUID 查找快得多。
但 Handle 有个坑——它是动态分配的。每次设备重启,Handle 可能变化。所以你不能在代码里硬编码 Handle 值。
我的做法:在固件里用 UUID 来发现服务/特征,拿到 Handle 后再缓存起来。这样既保证了兼容性,又提高了后续访问速度。
3.6 CCC:Client Characteristic Configuration
CCC 是 GATT 里最常用的描述符,没有之一。它控制着两个关键功能:
- Notification:Server 主动发数据给 Client,不需要 Client 确认
- Indication:Server 主动发数据,Client 必须确认收到
CCC 的值只有两个字节:
| 值 | 含义 |
|---|---|
| 0x0000 | 禁用通知和指示 |
| 0x0001 | 使能通知 |
| 0x0002 | 使能指示 |
| 0x0003 | 同时使能通知和指示(极少用) |
曾经踩过的坑:有一次产品量产了,发现部分手机连上后收不到数据。查了半天,原来是 CCC 的初始值设成了 0x0001,但手机端没有主动写 CCC。后来改成默认使能通知,问题解决。记住——CCC 的初始值由 Server 决定,但最终状态由 Client 控制。
3.7 整体结构图
下面这张图把 GATT 的层级关系画清楚了:
3.8 实战建议
最后给几个实在的建议:
- 服务数量别太多:一个设备 3-5 个服务就够了,多了影响发现速度
- CCC 一定要持久化:设备掉电后要记住 CCC 状态,不然每次重连都要重新订阅
- Handle 分配留余量:我习惯在服务之间留 2-3 个空 Handle,方便后期加特征
- 标准 UUID 优先:能省去很多兼容性测试的麻烦
一个小技巧:调试时可以用 nRF Connect 或 LightBlue 扫描你的设备,看看 GATT 数据库是不是按预期组织的。我每次写完服务都会扫一遍,确认 Handle 顺序、UUID、权限都没问题。