一、BLE与GATT概述:先搞懂协议骨架
做 BLE 开发,绕不开 GATT。说白了,GATT 就是蓝牙低功耗设备之间交换数据的规则。你想想看,一台心率带、一个温度传感器、一个智能门锁,它们怎么跟手机 App 对话?就是靠 GATT 这个“语言框架”。
我在项目里见过不少新手直接上手写 BLE 代码,结果发现连 Service 和 Characteristic 都分不清——嗯,那后面调试的时候肯定寸步难行。所以第一节课,我们先稳稳地把 GATT 的核心概念吃透。
1.1 GATT 是什么?
GATT(Generic Attribute Profile)是蓝牙协议栈中负责数据交互的层。它建立在 ATT(Attribute Protocol)之上,定义了 服务(Service)、特征(Characteristic)、描述符(Descriptor) 这三个核心元素。
核心要点:
- GATT 是 C/S(客户端/服务器)模型。手机通常是客户端,设备端是服务器。
- 数据以“属性”的形式存储,每个属性都有 UUID、权限和值。
- GATT 负责把属性组织成有意义的业务单元。
我个人习惯把 GATT 比作一个文件系统:
- 属性 = 文件块
- 服务 = 文件夹
- 特征 = 里面的具体文件
- 描述符 = 文件的元信息(作者、日期、权限等)
你想想看,这样理解是不是清晰多了?
1.2 服务(Service)—— 功能的容器
一个 BLE 设备可以包含多个 Service。每个 Service 代表一类独立功能。比如:
电池服务(0x180F)、设备信息服务(0x180A)、心率服务(0x180D)。
服务本身不存数据,它只是把相关的特征“打包”在一起。你可以理解成它就是一个“组”。
我记得有一次调试一款智能手环,厂商自定义了一个 Service,UUID 长长一串。结果我读不到特征值,后来才发现他们 Service 的 UUID 写错了——花了两天才定位到。所以 UUID 必须严格匹配,这是避坑第一课。
1.3 特征(Characteristic)—— 真正的数据接口
Characteristic(特征)才是我们读写数据的实际手柄。一个 Service 下面可以有一个或多个 Characteristic。每个 Characteristic 包含:
- 声明(Declaration):固定属性,描述特征本身
- 值(Value):数据本体,比如电量百分比 85%
- 描述符(Descriptor):可选的附加信息
特征还带了 属性权限:
| 属性名 | 含义 | 举例 |
|---|---|---|
| Read | 客户端可以读取值 | 读取固件版本 |
| Write | 客户端可以写入值 | 写入目标温度 |
| Notify | 服务端主动推送值(需要客户端开启CCC描述符) | 实时心率数据 |
| Indicate | 类似Notify,但需要确认 | 高精度传感器校准 |
这里要注意:Notify 和 Indicate 不要求客户端一直轮询,省电的关键就在这里。我在做运动手环时,一开始用了 Read 轮询,结果电量一天就耗光。换成 Notify 后,续航直接翻倍——这是血的教训啊。
1.4 描述符(Descriptor)—— 特征的“说明书”
描述符是附着在特征上的附加属性。最经典的就是 客户端特征配置描述符(CCCD),UUID 固定为 0x2902。它用来控制是否开启 Notify 或 Indicate。
其他常见描述符:
- 用户描述描述符(
0x2901):给人看的名称,比如“电量百分比” - 表示格式描述符(
0x2904):描述数据类型、单位、指数 - 范围描述符(
0x2906):限制值的最大最小值
我曾经遇到一个问题:读心率值时返回了一个很大的负数,查了半天才发现是表示格式描述符里的指数字段没解析正确。所以说,描述符不仅仅是摆设,它直接影响数据解析。
1.5 一个完整的例子:电池服务
拿最常用的电池服务(Battery Service,UUID = 0x180F)来说:
Service: 0x180F (Battery Service)
├── Characteristic: 0x2A19 (Battery Level)
│ ├── Value: 0-100 百分比
│ ├── Properties: Read, Notify
│ └── Descriptor: 0x2902 (CCCD)
│ └── Value: 0x0001 开启 Notify
│ └── Descriptor: 0x2901 (User Description)
│ └── Value: "Battery Percentage"
解读一下:
- Service 声明了这是一个电池相关的功能组。
- Characteristic 0x2A19 存的是电量百分比,可读也可通知。
- CCCD 描述符控制是否让设备主动上报电量变化。
- 用户描述描述符是个可选的信息。
当你用手机连接一个支持电池服务的蓝牙耳机时,App 只需要读取 Battery Level 的值就能显示电量。如果开启了 Notify,耳机电量从 80% 降到 79%,它会自动告诉手机——不需要手机一直问。
1.6 GATT 结构图:一张图看清三层关系
下面这张 SVG 图把 GATT 的层级结构画出来了。我建议你把它存下来,后面写代码时随时对照。
1.7 核心概念速查表
| 概念 | 英文 | 作用 | 类比 |
|---|---|---|---|
| 服务 | Service | 将相关特征分组,代表一个功能模块 | 文件夹 |
| 特征 | Characteristic | 数据读写的实际接口 | 文件 |
| 描述符 | Descriptor | 附加的元数据或配置 | 文件属性 |
| 属性 | Attribute | GATT的最小数据单元,包含UUID、权限、值 | 磁盘上的字节块 |
| UUID | UUID | 唯一标识一个服务、特征或描述符 | 文件名 |
我的小技巧:
在 Android 开发中,我习惯先用手册或 nRF Connect 扫描设备,把 Service 和 Characteristic 的 UUID 全部记录成常量类。这样后面写 Kotlin 代码时直接引用,不会写错。
曾经踩过的坑:
我第一次写 BLE 读写代码时,忽略了特征属性权限。明明设备支持 Notify,我却用了 Read 去轮询,结果设备返回了空值。后来发现:每次操作前先用 BluetoothGattCharacteristic.getProperties() 检查权限。这句话,建议你记下来。