GATT 数据模型深度解析:Service、Characteristic、Descriptor 的层级关系
好,咱们今天来啃 GATT 数据模型这块硬骨头。说实话,我刚接触 BLE 时,被 Service、Characteristic、Descriptor 这三个东西绕得晕头转向。后来在做一个心率监测项目时,因为把 Characteristic 的属性配错了,设备死活连不上手机——嗯,从那以后我才真正搞懂它们之间的关系。
说白了,GATT 的数据模型就是一个树形结构。你想想看,它就像你电脑里的文件夹系统:最顶层是 Service(文件夹),里面放着 Characteristic(文件),而 Descriptor(文件属性)则描述这个文件的具体信息。
1. 层级关系:从 Service 到 Descriptor
咱们先看一张整体结构图,我习惯用这种方式来理解复杂概念:
这张图把层级关系讲得很清楚了。从上到下依次是:Profile → Service → Characteristic → Descriptor。我刚开始做项目时,总以为 Profile 是必须的,其实它只是蓝牙 SIG 定义的一个规范文档,真正在设备上跑的是 Service。
2. Service:服务的容器
Service 说白了就是一组相关功能的集合。比如你做一个智能灯泡,就需要一个「灯控服务」;做心率带,就需要一个「心率服务」。每个 Service 都有一个唯一的 UUID 来标识。
我记得有一次调试一个温湿度计,发现手机扫描不到设备,折腾了半天——原来是 Service 的 UUID 写错了。所以这里要特别提醒:
3. Characteristic:数据的载体
Characteristic 才是真正干活的东西。它包含三个部分:
- 声明(Declaration):固定格式,包含属性、句柄和 UUID
- 值(Value):实际的数据内容
- 描述符(Descriptor):对值的补充说明
这里有个关键点——Characteristic 的属性(Properties)决定了你能对它做什么。我见过太多新手把 Read 和 Notify 搞混,结果数据读不出来。
| 属性 | 含义 | 典型场景 |
|---|---|---|
| Read | 客户端可以读取值 | 读取设备型号 |
| Write | 客户端可以写入值 | 发送控制指令 |
| Notify | 设备主动通知客户端(无需确认) | 实时心率数据 |
| Indicate | 设备主动通知客户端(需要确认) | 关键告警信息 |
4. Descriptor:数据的说明书
Descriptor 是很多人容易忽略的部分。它用来描述 Characteristic 的额外信息,比如:
- Client Characteristic Configuration Descriptor (CCCD):最常用的描述符,用来开启/关闭 Notify 或 Indicate
- Characteristic User Description:给用户看的文字说明
- Characteristic Presentation Format:描述数据的格式(比如单位、精度)
我曾经在一个项目中,因为忘记配置 CCCD,导致手机收不到 Notify 数据。排查了整整两天才发现——嗯,从那以后我每次写代码都会检查 CCCD 是否配置正确。
5. UUID 的定义与类型
UUID 就是每个 Service、Characteristic、Descriptor 的身份证。BLE 协议定义了三种 UUID 长度:
| 类型 | 长度 | 使用场景 | 示例 |
|---|---|---|---|
| 16-bit UUID | 2 字节 | 蓝牙 SIG 标准服务/特征 | 0x180D(心率服务) |
| 32-bit UUID | 4 字节 | 蓝牙 SIG 预留,实际很少用 | 0x0000180D |
| 128-bit UUID | 16 字节 | 自定义服务/特征 | 0000xxxx-0000-1000-8000-00805F9B34FB |
这里有个坑:很多人以为 16-bit UUID 和 128-bit UUID 可以随便混用。其实不行。标准服务必须用 16-bit,自定义服务必须用 128-bit。我见过有人把自定义服务写成 16-bit,结果手机端解析出错。
0000xxxx-0000-1000-8000-00805F9B34FB。其中 xxxx 是你的自定义部分。蓝牙 SIG 规定,自定义 UUID 必须使用这个基址,不能自己乱改。
6. Attribute Protocol (ATT) 基础
ATT 是 GATT 的底层传输协议。说白了,GATT 定义的是「数据长什么样」,ATT 定义的是「数据怎么传」。ATT 的核心概念是属性(Attribute),每个属性包含:
- 句柄(Handle):属性的唯一编号,从 0x0001 开始递增
- 类型(Type):就是 UUID
- 权限(Permissions):读、写、加密等
- 值(Value):实际数据
ATT 定义了 6 种 PDU(协议数据单元)类型,最常用的几个:
| PDU 类型 | 方向 | 作用 |
|---|---|---|
| Read Request / Response | Client → Server → Client | 读取属性值 |
| Write Request / Response | Client → Server → Client | 写入属性值(需要确认) |
| Write Command | Client → Server | 写入属性值(无需确认) |
| Handle Value Notification | Server → Client | Notify 数据推送 |
| Handle Value Indication | Server → Client → Server | Indicate 数据推送(需要确认) |
我刚开始做 BLE 时,总搞不清 Write Request 和 Write Command 的区别。后来在做一个固件升级项目时,因为用了 Write Command 来传输固件包,结果丢包导致升级失败——Write Command 不保证送达,而 Write Request 会返回确认。
7. 实战中的层级关系示例
咱们用一个实际的心率监测设备来串一遍整个模型:
Profile: Heart Rate Profile (HRP)
└── Service: Heart Rate Service (UUID: 0x180D)
├── Characteristic: Heart Rate Measurement (UUID: 0x2A37)
│ ├── Properties: Notify
│ ├── Value: [心率数据]
│ └── Descriptor: CCCD (UUID: 0x2902)
│ └── Value: 0x0001 (开启 Notify)
├── Characteristic: Body Sensor Location (UUID: 0x2A38)
│ ├── Properties: Read
│ └── Value: 0x01 (胸部)
└── Characteristic: Heart Rate Control Point (UUID: 0x2A39)
├── Properties: Write
└── Value: [控制指令]
你看,这个结构非常清晰。每个 Characteristic 都有明确的用途,Descriptor 为它们提供额外的配置能力。我在实际项目中,都是先画好这个树形图,再开始写代码——这样能避免很多低级错误。