第四节:蓝牙协议栈基础——GAP与GATT角色、Service与Characteristic定义、UUID分配策略
好,咱们进入蓝牙协议栈的核心部分。说实话,很多做牙刷数据采集的开发者,一开始都被蓝牙协议栈搞晕过。我当年第一次接触BLE时,看着GAP、GATT、Service、Characteristic这些术语,脑子里全是问号。别急,今天咱们把这些概念一个一个拆开讲清楚。
4.1 GAP与GATT:蓝牙的两大角色体系
蓝牙协议栈分两层,上层是GAP,下层是GATT。我习惯这么理解:GAP负责“谁跟谁说话”,GATT负责“说什么话”。
4.1.1 GAP(通用访问协议)
GAP定义了蓝牙设备的角色。在牙刷场景中,主要有两种角色:
- Central(中心设备):通常是手机App,负责扫描和发起连接。
- Peripheral(外围设备):就是咱们的智能牙刷,负责广播自己的存在。
嗯,这里要注意:一个设备可以同时扮演多个角色,但咱们的牙刷只需要做Peripheral就够了。
关键点:GAP定义了广播、扫描、连接建立等流程。牙刷上电后,会周期性地发送广播包,里面包含设备名称、服务UUID等信息。手机App扫描到广播包后,发起连接请求。
4.1.2 GATT(通用属性协议)
GATT才是真正传输数据的地方。它定义了一种客户端-服务器模型:
- GATT Server:牙刷端,存储数据并提供服务。
- GATT Client:手机App端,读取或写入数据。
我刚开始做项目时,总把GAP和GATT搞混。后来我给自己编了个口诀:“GAP管连接,GATT管数据”。你想想看,连接都没建立,谈什么数据传输呢?
4.2 Service与Characteristic:数据组织的骨架
GATT协议里,数据是以Service和Characteristic的形式组织的。说白了,这就是一个树形结构。
4.2.1 Service(服务)
Service是一组相关功能的集合。比如咱们的牙刷,可以定义:
- 电池服务:包含电量百分比、充电状态等。
- 刷牙数据服务:包含刷牙时长、压力值、区域覆盖等。
- 设备信息服务:包含固件版本、序列号等。
每个Service都有一个唯一的UUID来标识。
4.2.2 Characteristic(特征)
Characteristic是实际承载数据的单元。每个Characteristic包含:
- Value:实际的数据值,比如电量80%。
- Properties:读写权限,比如可读、可写、可通知。
- Descriptors:描述信息,比如单位、格式等。
我的经验:在定义Characteristic时,尽量把数据拆细。比如刷牙时长单独一个Characteristic,压力值单独一个。别把所有数据塞到一个Characteristic里,否则解析起来很麻烦。我曾经见过一个项目,把所有传感器数据打包成一个字节数组,结果调试时痛苦得要命。
4.3 UUID分配策略:别乱起名字
UUID是每个Service和Characteristic的唯一标识。蓝牙标准定义了一些通用的UUID,比如电池服务的UUID是0x180F。但咱们的牙刷数据是自定义的,需要自己分配UUID。
4.3.1 UUID的两种类型
| 类型 | 长度 | 示例 | 使用场景 |
|---|---|---|---|
| 16-bit UUID | 2字节 | 0x180F |
蓝牙标准定义的服务 |
| 128-bit UUID | 16字节 | 0000xxxx-0000-1000-8000-00805F9B34FB |
自定义服务/特征 |
嗯,这里要注意:自定义服务必须用128-bit UUID,否则会和标准服务冲突。我见过有人偷懒用16-bit UUID,结果手机端解析时把自定义服务当成了标准服务,数据全乱了。
4.3.2 我的UUID分配策略
我个人习惯用这样的格式:
// 基础UUID(固定部分)
#define BASE_UUID "0000XXXX-0000-1000-8000-00805F9B34FB"
// 服务UUID
#define SERVICE_BATTERY_UUID "0000AA01-0000-1000-8000-00805F9B34FB"
#define SERVICE_BRUSH_DATA_UUID "0000AA02-0000-1000-8000-00805F9B34FB"
#define SERVICE_DEVICE_INFO_UUID "0000AA03-0000-1000-8000-00805F9B34FB"
// 特征UUID
#define CHAR_BATTERY_LEVEL_UUID "0000BB01-0000-1000-8000-00805F9B34FB"
#define CHAR_BRUSH_DURATION_UUID "0000BB02-0000-1000-8000-00805F9B34FB"
#define CHAR_BRUSH_PRESSURE_UUID "0000BB03-0000-1000-8000-00805F9B34FB"
为什么这么分配?
- AA开头:表示Service。
- BB开头:表示Characteristic。
- 编号递增:方便扩展,比如以后加个温度传感器,直接加个
BB04就行。
避坑指南:我曾经在项目里用随机生成的UUID,结果不同设备之间UUID不一致,导致手机App无法识别。后来我统一用了一个固定的基础UUID,只改后四位,问题就解决了。记住:UUID一旦发布,尽量不要修改,否则所有已部署的设备都得升级固件。
4.4 实际案例:牙刷数据采集的GATT结构
咱们来设计一个实际的牙刷数据采集GATT结构。假设牙刷有三个核心数据:电量、刷牙时长、压力值。
// 服务定义
Service: 电池服务 (UUID: 0000AA01-...)
└── Characteristic: 电量百分比 (UUID: 0000BB01-...)
├── Properties: 读 + 通知
├── Value: 0-100 (uint8)
└── Descriptor: 客户端特征配置 (CCCD)
Service: 刷牙数据服务 (UUID: 0000AA02-...)
├── Characteristic: 刷牙时长 (UUID: 0000BB02-...)
│ ├── Properties: 读 + 通知
│ ├── Value: 0-600秒 (uint16)
│ └── Descriptor: CCCD
└── Characteristic: 压力值 (UUID: 0000BB03-...)
├── Properties: 读 + 通知
├── Value: 0-255 (uint8)
└── Descriptor: CCCD
Service: 设备信息服务 (UUID: 0000AA03-...)
└── Characteristic: 固件版本 (UUID: 0000BB04-...)
├── Properties: 读
└── Value: 字符串 (如 "1.2.3")
为什么每个Characteristic都要加CCCD?因为CCCD是开启通知功能的开关。手机App写入0x0001到CCCD,牙刷就会在数据变化时主动推送。这样手机就不用一直轮询了,省电又高效。
核心要点:
- GAP管连接,GATT管数据。
- Service是功能集合,Characteristic是数据单元。
- 自定义UUID用128-bit,别偷懒用16-bit。
- CCCD是通知开关,别忘了加。
好了,蓝牙协议栈的基础就讲到这里。下一节咱们会深入蓝牙配网流程,包括广播包的解析和连接参数的优化。到时候我会分享一个我踩过的坑——广播间隔设得太短,导致牙刷耗电飞快。嗯,到时候细说。