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让手机来连接它。但要注意,角色切换时内存和时序都要规划好,否则容易丢包。

我的习惯: 开发初期先用ESP32的官方示例把Peripheral和Central跑通,确认广播和扫描参数没问题,再往上叠业务逻辑。这样排查问题时能快速定位是协议层还是应用层的问题。

2.2 GATT角色模型:服务端与客户端

GATT(Generic Attribute Profile)是在连接建立之后才起作用的。它定义了两种角色:GATT Server和GATT Client。说白了,Server就是提供数据的一方,Client就是读取或写入数据的一方。

举个例子:你做一个心率带,心率带本身是GATT Server,它把心率数据放在某个Characteristic里;手机App是GATT Client,它去读这个Characteristic。但注意,同一个设备可以同时是Server和Client——比如心率带从手机读取配置参数时,它就变成了Client。

关键点: GATT角色和GAP角色是正交的。一个Peripheral设备通常是GATT Server,但也可以同时是GATT Client。我在项目中就踩过这个坑——以为Peripheral只能当Server,结果想从手机读配置时发现接口没开。

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。

我画了一张图,帮你理清这个层级关系:

GATT 层级结构图 Profile(应用规范) Service(服务)— UUID: 0x180D(心率服务) Characteristic(特征)— UUID: 0x2A37(心率测量) Characteristic Value Descriptor: CCCD Descriptor: 其他 层级说明 • Profile:一个应用规范,包含多个Service • Service:一组相关功能的集合,用UUID标识 • Characteristic:一个具体的数据点,包含Value和Descriptor • Descriptor:描述Characteristic的元数据,CCCD用于控制通知/指示

每个Characteristic至少有一个Value,用来存放实际数据。Descriptor则用来描述这个Value的属性——最常用的是CCCD(Client Characteristic Configuration Descriptor),它控制着Server是否主动向Client推送数据。

我曾经踩过的坑: 在定义自定义Service时,忘了给Characteristic添加CCCD,结果手机端收不到通知。后来查了半天才发现,BLE规范要求:如果Characteristic支持Notify或Indicate,就必须包含CCCD。这个坑我至少见过三次。

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 无 只广播,不连接
我的建议: 如果你刚开始接触BLE,先从Peripheral + GATT Server的组合入手。这个模式最简单,ESP32官方示例也最全。等跑通了数据收发,再尝试Central模式或双角色模式。