GATT服务开发与蓝牙网关联调 - 第2节

2. GATT 数据模型:Service、Characteristic、Descriptor 的定义与层级关系

GATT 这个词,刚接触蓝牙开发的朋友可能会觉得有点抽象。说白了,它就是蓝牙设备之间交换数据的一套规则。我当年第一次看 BLE 协议栈的时候,也被 Service、Characteristic 这些概念绕得晕头转向。后来亲手调了几个项目,才真正理解它们是怎么配合的。

咱们先看一张整体结构图,把这三个东西的层级关系搞清楚。

GATT 数据模型层级结构 GATT 数据模型层级结构 Profile(应用规范) Service(服务) Characteristic(特征) Descriptor(描述符) 说明: 1. 一个 Profile 包含多个 Service 2. 一个 Service 包含多个 Characteristic 3. 一个 Characteristic 包含多个 Descriptor

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 开发的基础。把这些概念吃透,后面写代码的时候思路会清晰很多。