2. BLE协议栈基础:GAP与GATT角色模型、Attribute Protocol、Service与Characteristic结构、UUID与Handle
好,咱们正式开始啃协议栈。很多初学者一上来就被各种缩写砸晕——GAP、GATT、ATT、UUID、Handle……说实话,我当年第一次看BLE协议栈文档时,也是翻来覆去看了好几遍才理清脉络。别急,这一章咱们就把这些概念一个一个拆开,用实际项目里的例子讲明白。
你想想看,两个设备要通信,总得先知道对方是谁、能干什么吧?这就是GAP和GATT要解决的事。GAP管“怎么找到你、怎么连上你”,GATT管“连上之后能交换什么数据”。
2.1 GAP角色模型:谁在广播,谁在扫描
GAP(Generic Access Profile)定义了设备在链路层的四种角色。我在做智能灯项目时,一开始就把角色搞混了,结果手机死活搜不到设备……后来才意识到,广播者和观察者是不建立连接的,而外设和中心才会建立连接。
| 角色 | 方向 | 典型设备 | 是否建立连接 |
|---|---|---|---|
| Broadcaster(广播者) | 只发不收 | 信标、温湿度传感器 | 否 |
| Observer(观察者) | 只收不发 | 扫描仪、网关 | 否 |
| Peripheral(外设) | 可发可收,响应连接 | 手环、心率带 | 是 |
| Central(中心) | 可发可收,发起连接 | 手机、平板 | 是 |
在ESP32上,你可以同时让设备扮演多个角色。比如我做过一个网关,它一边作为Central连接多个传感器,一边作为Peripheral让手机来连接它。但要注意,角色切换时内存和时序都要规划好,否则容易丢包。
2.2 GATT角色模型:服务端与客户端
GATT(Generic Attribute Profile)是在连接建立之后才起作用的。它定义了两种角色:GATT Server和GATT Client。说白了,Server就是提供数据的一方,Client就是读取或写入数据的一方。
举个例子:你做一个心率带,心率带本身是GATT Server,它把心率数据放在某个Characteristic里;手机App是GATT Client,它去读这个Characteristic。但注意,同一个设备可以同时是Server和Client——比如心率带从手机读取配置参数时,它就变成了Client。
2.3 Attribute Protocol:数据交换的最小单元
ATT(Attribute Protocol)是GATT的底层传输协议。它定义了一个个的“属性”(Attribute),每个属性由四部分组成:Attribute Handle、Attribute Type(UUID)、Attribute Permissions、Attribute Value。
嗯,这里要注意:Handle是属性的唯一编号,UUID是属性的类型标识。我刚开始做时,总把Handle和UUID搞混——Handle是“第几个”,UUID是“是什么”。
// 一个典型的Attribute结构
Attribute {
Handle: 0x0003 // 第3个属性
Type (UUID): 0x2A37 // 心率测量特征
Permissions: Read | Notify // 可读、可通知
Value: [0x06, 0x58] // 心率值:88 bpm
}
ATT定义了六种操作:Request、Response、Command、Notification、Indication、Confirmation。其中Notification和Indication的区别在于——Notification不需要确认,Indication需要对方回复Confirmation。我建议:对实时性要求高但可以容忍偶尔丢包的数据(比如心率),用Notification;对必须可靠传输的数据(比如固件升级包),用Indication。
2.4 Service与Characteristic结构
Service和Characteristic是GATT层对Attribute的进一步封装。一个Service包含一个或多个Characteristic,每个Characteristic又包含一个Value和若干个Descriptor。
我画了一张图,帮你理清这个层级关系:
每个Characteristic至少有一个Value,用来存放实际数据。Descriptor则用来描述这个Value的属性——最常用的是CCCD(Client Characteristic Configuration Descriptor),它控制着Server是否主动向Client推送数据。
2.5 UUID与Handle:如何唯一标识一切
UUID(Universally Unique Identifier)用来标识Service、Characteristic和Descriptor的类型。BLE规范定义了两类UUID:
- 16-bit UUID:由Bluetooth SIG标准化,比如心率服务0x180D、电池服务0x180F。这些是官方定义的,可以直接用。
- 128-bit UUID:自定义时使用,格式如
0000XXXX-0000-1000-8000-00805F9B34FB。其中XXXX是自定义的16位值。
Handle则是Attribute在GATT Server中的索引号,从0x0001开始递增。我习惯在代码里用宏定义把Handle和UUID对应起来,方便调试时对照。
// ESP32中定义自定义Service和Characteristic的示例
#define CUSTOM_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CUSTOM_CHAR_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CUSTOM_CHAR_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
// 注册Service
esp_ble_gatts_create_attr_tab(attr_tab, ...);
// 启动Service
esp_ble_gatts_start_service(service_handle);
这里有个细节:Handle是由协议栈自动分配的,但你可以在回调函数里拿到它。我建议在初始化时把Handle打印出来,方便后续对照调试。
2.6 实战中的角色选择建议
根据我做过的大大小小十几个BLE项目,给你一些角色选择的参考:
| 应用场景 | GAP角色 | GATT角色 | 说明 |
|---|---|---|---|
| 温湿度传感器 | Peripheral | Server | 只上报数据,手机来读 |
| 智能灯 | Peripheral | Server | 手机写入开关/亮度 |
| 数据采集网关 | Central | Client | 读取多个传感器数据 |
| 双向手环 | Peripheral | Server + Client | 上报心率,接收配置 |
| 蓝牙信标 | Broadcaster | 无 | 只广播,不连接 |