3. UUID 与属性类型:16-bit/32-bit/128-bit UUID 定义、属性类型与权限配置
聊到 GATT 服务开发,UUID 和属性类型是绕不开的基础。说白了,UUID 就是蓝牙服务的“身份证号”,属性类型决定了这个“身份证”能干什么事。我刚开始接触 BLE 时,总觉得 UUID 不就是一串数字嘛,随便写一个就行。结果有一次跟别人的设备配对死活连不上,查了半天才发现是 UUID 格式写错了。嗯,从那以后我再也不敢小看这串数字了。
3.1 UUID 的三种长度:16-bit、32-bit、128-bit
蓝牙规范定义了三种 UUID 长度。它们的关系很简单:越短越省电,越长越灵活。
| UUID 类型 | 长度 | 使用场景 | 示例 |
|---|---|---|---|
| 16-bit UUID | 2 字节 | 蓝牙 SIG 定义的标准服务/特征 | 0x180D(心率服务) |
| 32-bit UUID | 4 字节 | 蓝牙 SIG 预留,实际很少用 | 0x0000180D-0000-1000-8000-00805F9B34FB |
| 128-bit UUID | 16 字节 | 自定义服务/特征 | 6E400001-B5A3-F393-E0A9-E50E24DCCA9E |
核心原则:能用 16-bit 就用 16-bit,省电省带宽。只有 SIG 没定义的服务才用 128-bit。
16-bit UUID 是蓝牙 SIG 官方分配的。比如心率服务 0x180D、电池服务 0x180F、设备信息服务 0x180A。这些标准 UUID 在蓝牙芯片内部有硬件加速,传输时只需要发 2 个字节,效率很高。
128-bit UUID 是给开发者自定义用的。格式是 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。我个人的习惯是:用在线 UUID 生成器随机生成一个,然后把前 4 个字节改成自己的公司代码或产品代码,方便调试时识别。
注意:32-bit UUID 在蓝牙 4.0/4.1 中定义过,但实际产品中几乎没人用。你看到的大部分“32-bit UUID”其实都是 128-bit UUID 的缩写形式。别被文档搞晕了。
3.2 UUID 的 Base 与偏移量
128-bit UUID 有一个固定的 Base UUID:0000xxxx-0000-1000-8000-00805F9B34FB。16-bit 和 32-bit UUID 本质上就是把这个 Base 里的 xxxx 替换掉。
举个例子:
- 心率服务 16-bit UUID = 0x180D
- 对应的 128-bit UUID = 0000180D-0000-1000-8000-00805F9B34FB
这个 Base UUID 是蓝牙 SIG 规定的,所有标准服务都遵循这个规则。自定义服务则完全自由,你可以用任何 128-bit UUID,只要不跟标准冲突就行。
小技巧:调试时,我习惯把自定义 UUID 的中间段改成有意义的 ASCII 码。比如 6E400001-B5A3-F393-E0A9-E50E24DCCA9E,这样抓包时一眼就能认出是自己的服务。
3.3 属性类型:Service、Characteristic、Descriptor
GATT 的层级结构其实很简单:Service 包含 Characteristic,Characteristic 包含 Descriptor。我画了一张图,帮你理清这个关系。
3.4 属性类型详解
3.4.1 Service(服务)
Service 是 GATT 的最顶层容器。一个设备可以包含多个 Service,每个 Service 有唯一的 UUID。Service 本身不存数据,它只是把相关的 Characteristic 组织在一起。
举个例子:心率服务(0x180D)下面通常包含:
- 心率测量特征(0x2A37)
- 身体位置特征(0x2A38)
- 心率控制点特征(0x2A39)
3.4.2 Characteristic(特征)
Characteristic 是 GATT 的数据核心。它包含三个部分:
- 声明(Declaration): 固定格式,包含属性、值句柄、UUID
- 值(Value): 实际数据
- 描述符(Descriptor): 可选,描述值的属性
关键点:Characteristic 的“属性”和“权限”是两回事。属性决定“能做什么操作”,权限决定“谁可以做”。
3.4.3 Descriptor(描述符)
Descriptor 是特征的“说明书”。最常见的几个:
| UUID | 名称 | 用途 |
|---|---|---|
| 0x2900 | 特征扩展属性 | 描述特征是否支持可靠写入 |
| 0x2901 | 特征用户描述 | 给特征起个名字(比如“温度值”) |
| 0x2902 | 客户端特征配置(CCC) | 控制 Notify/Indicate 开关 |
| 0x2904 | 特征表示格式 | 描述数据的格式(单位、精度等) |
避坑指南:我曾经在 CCC 描述符上栽过跟头。如果你想让手机端接收 Notify,必须把 CCC 描述符的权限设为“读+写”,否则手机端无法订阅通知。很多新手只设了“读”,结果数据发不出去。
3.5 属性权限配置
权限配置是 GATT 开发中最容易出错的地方。我把它分成三类:
3.5.1 访问权限(Access Permissions)
决定主机(手机)能否读/写这个特征值:
| 权限 | 含义 | 典型场景 |
|---|---|---|
| 只读 | 主机可以读,不能写 | 设备版本号、序列号 |
| 只写 | 主机可以写,不能读 | 控制命令(写后不返回) |
| 读写 | 主机可以读也可以写 | 可配置的参数 |
| 无 | 主机不能读也不能写 | 仅用于 Notify 的特征 |
3.5.2 属性权限(Attribute Permissions)
决定特征支持哪些操作:
| 属性 | 值 | 说明 |
|---|---|---|
| Broadcast | 0x01 | 支持广播包中携带 |
| Read | 0x02 | 支持读操作 |
| Write Without Response | 0x04 | 支持无应答写 |
| Write | 0x08 | 支持有应答写 |
| Notify | 0x10 | 支持通知(无确认) |
| Indicate | 0x20 | 支持指示(有确认) |
| Authenticated Signed Writes | 0x40 | 支持签名写 |
| Extended Properties | 0x80 | 需要查扩展属性描述符 |
我的经验:Notify 和 Indicate 的区别在于可靠性。Notify 快但不保证送达,Indicate 慢但可靠。心率数据用 Notify 就够了,但固件升级的进度通知最好用 Indicate,确保手机端收到。
3.5.3 安全权限(Security Permissions)
决定是否需要加密或配对才能访问:
- 无安全要求: 任何人都可以读/写
- 加密: 需要连接加密(无需配对)
- 配对: 需要绑定后才能访问
- MITM 保护: 需要防中间人攻击的配对
3.6 实战:配置一个自定义特征
假设我们要做一个温度传感器,需要定义一个自定义特征。我会这样配置:
// 伪代码示例:温度特征配置
#define TEMP_SERVICE_UUID 0x180D // 使用标准心率服务?不,我们用自定义
#define TEMP_SERVICE_UUID_CUSTOM "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define TEMP_CHAR_UUID_CUSTOM "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
// 特征属性:支持 Read + Notify
// 访问权限:主机可以读,但不能写(温度只上报)
// 安全权限:无(方便调试)
ble_gatt_char_def_t temp_char = {
.uuid = TEMP_CHAR_UUID_CUSTOM,
.properties = BLE_GATT_CHAR_PROP_READ | BLE_GATT_CHAR_PROP_NOTIFY,
.permissions = BLE_GATT_PERM_READ,
.max_len = 4, // 4 字节,存放 float 温度值
.initial_value = {0x00, 0x00, 0x00, 0x00}
};
注意:如果你同时设置了 Read 和 Notify,记得把 CCC 描述符(0x2902)的权限设为可写。否则手机端无法订阅 Notify,你发的数据它收不到。
3.7 常见错误与避坑
- UUID 大小写搞混: 蓝牙规范里 UUID 不区分大小写,但有些 SDK 区分。我建议统一用小写。
- 属性与权限不匹配: 比如特征属性设了 Notify,但 CCC 描述符没加,或者 CCC 权限设成了只读。结果就是手机端无法订阅。
- 128-bit UUID 重复: 两个服务用了同一个 UUID,导致手机端只能发现一个。我习惯在 UUID 里加版本号,比如
6E400001是 V1,6E400002是 V2。 - 安全权限设太高: 开发阶段把安全权限设成“需要配对”,结果每次调试都要重新配对,浪费时间。我建议开发阶段先用无安全要求,上线前再改。
UUID 和属性配置是 GATT 开发的基础。说白了,你只要记住:16-bit 给标准用,128-bit 给自己用;属性决定能干什么,权限决定谁能干。这两条线理清了,后面的开发就顺了。