第四节:蓝牙协议栈基础——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协议里,数据是以ServiceCharacteristic的形式组织的。说白了,这就是一个树形结构。

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是通知开关,别忘了加。

好了,蓝牙协议栈的基础就讲到这里。下一节咱们会深入蓝牙配网流程,包括广播包的解析和连接参数的优化。到时候我会分享一个我踩过的坑——广播间隔设得太短,导致牙刷耗电飞快。嗯,到时候细说。