GATT服务开发与蓝牙网关联调 - 第2节
2. GATT 数据模型:Service、Characteristic、Descriptor 的定义与层级关系
GATT 这个词,刚接触蓝牙开发的朋友可能会觉得有点抽象。说白了,它就是蓝牙设备之间交换数据的一套规则。我当年第一次看 BLE 协议栈的时候,也被 Service、Characteristic 这些概念绕得晕头转向。后来亲手调了几个项目,才真正理解它们是怎么配合的。
咱们先看一张整体结构图,把这三个东西的层级关系搞清楚。
2.1 Service(服务)—— 功能的逻辑分组
Service 是什么?你可以把它理解成一个功能模块。比如一个智能手环,它有「电池服务」、「心率服务」、「设备信息服务」。每个 Service 负责一类数据的读写或通知。
我记得第一次做蓝牙心率计的时候,就踩了一个坑。当时我把心率和步数放在同一个 Service 里,结果手机 App 解析数据时总是乱掉。后来才明白,每个独立功能应该拆成独立的 Service。这样客户端按需订阅,互不干扰。
Service 的特点:
- 由 UUID 唯一标识
- 可以包含多个 Characteristic
- 可以包含其他 Service(称为 Include Service)
- 分为 Primary Service(主要服务)和 Secondary Service(次要服务)
2.2 Characteristic(特征)—— 真正的数据载体
Characteristic 是 GATT 里最核心的概念。Service 只是个容器,真正读写数据靠的是 Characteristic。每个 Characteristic 包含三个部分:
- Value:实际的数据值,比如心率数值、电池电量百分比
- Properties:操作权限,比如可读、可写、可通知
- Descriptors:描述这个 Value 的额外信息
我建议你在设计 Characteristic 时,Properties 一定要想清楚。举个例子,如果你只允许客户端读,就不要加上「可写」属性。我曾经见过一个产品,因为误加了可写属性,导致手机端误操作把设备配置给改了,现场排查了半天。
实战小技巧:
Properties 常用的组合:
- Read + Notify:传感器数据上报,客户端也能主动查询
- Write + Write Without Response:控制指令下发,不需要确认
- Indicate:需要确认的通知,适合重要数据
2.3 Descriptor(描述符)—— 特征的说明书
Descriptor 很多人容易忽略,但它其实很重要。它用来描述 Characteristic 的配置信息或附加属性。最常见的两个 Descriptor:
| Descriptor 名称 | UUID | 作用 |
|---|---|---|
| Client Characteristic Configuration | 0x2902 | 开启/关闭 Notify 或 Indicate |
| Characteristic User Description | 0x2901 | 给特征取个名字,比如"心率值" |
| Characteristic Presentation Format | 0x2904 | 描述数据格式,比如单位、精度 |
嗯,这里要注意。CCCD(0x2902)是每个支持 Notify 或 Indicate 的 Characteristic 必须带的。如果你忘了加,手机端就收不到数据。我刚开始做的时候,就因为这个折腾了两天,最后发现是 Descriptor 没配全。
UUID 的概念与类型
UUID 是 GATT 世界里每个 Service、Characteristic、Descriptor 的身份证。没有 UUID,设备之间就不知道对方在说什么。
2.4 16-bit UUID
16-bit UUID 是蓝牙 SIG 组织定义的标准 UUID。比如:
| UUID | 含义 |
|---|---|
| 0x1800 | Generic Access Service |
| 0x180A | Device Information Service |
| 0x180D | Heart Rate Service |
| 0x2A37 | Heart Rate Measurement Characteristic |
| 0x2A19 | Battery Level Characteristic |
这些标准 UUID 的好处是兼容性好。手机端的系统蓝牙框架(比如 iOS 的 CoreBluetooth)直接就能识别,不需要额外解析。我个人习惯是,能用标准 UUID 的尽量用,省事。
2.5 32-bit UUID
32-bit UUID 其实很少见。它是 SIG 保留给特定供应商用的,但实际项目中几乎没人用。你想想看,16-bit 不够用就用 128-bit,32-bit 卡在中间,两头不讨好。我做了这么多年蓝牙开发,只在一些老旧的私有协议里见过一次。
2.6 128-bit UUID
128-bit UUID 是自定义 UUID,格式是 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx。当你需要定义私有 Service 或 Characteristic 时,就用它。
举个例子,我做过一个智能门锁项目,需要自定义一个「开锁服务」。标准 UUID 里没有这个,所以我自己生成了一个 128-bit UUID:
// 自定义 Service UUID
#define LOCK_SERVICE_UUID "0000FFE0-0000-1000-8000-00805F9B34FB"
// 自定义 Characteristic UUID
#define LOCK_CONTROL_CHAR_UUID "0000FFE1-0000-1000-8000-00805F9B34FB"
#define LOCK_STATUS_CHAR_UUID "0000FFE2-0000-1000-8000-00805F9B34FB"
避坑指南:
我曾经见过有人直接把 16-bit UUID 前面补零当成 128-bit 用,比如把 0x180D 写成 0000180D-0000-1000-8000-00805F9B34FB。这样做虽然能工作,但会和标准 UUID 冲突。正确的做法是:自定义 UUID 一定要用随机生成器,不要自己编。
2.7 三种 UUID 的对比
| 类型 | 长度 | 用途 | 举例 |
|---|---|---|---|
| 16-bit | 2 字节 | SIG 标准服务/特征 | 0x180D(心率服务) |
| 32-bit | 4 字节 | 极少使用 | 几乎见不到 |
| 128-bit | 16 字节 | 自定义私有服务/特征 | 厂商自定义 |
你可能会问,为什么不用 16-bit 省空间?因为 BLE 协议在传输 UUID 时,如果检测到是 16-bit 标准 UUID,会自动压缩成 2 字节传输。128-bit 则要传 16 字节,确实多占一些空口时间。但如果你做的是私有协议,这点开销完全可以接受。
好了,这一节的内容就这些。Service、Characteristic、Descriptor 的层级关系,加上 UUID 的类型选择,是 GATT 开发的基础。把这些概念吃透,后面写代码的时候思路会清晰很多。