4、BLE通信核心:服务(Service)、特征(Characteristic)、UUID、属性(Attribute)与数据读写
BLE(Bluetooth Low Energy)之所以能实现低功耗连接,关键在于它定义了一套精简而严谨的数据组织与交互模型。与经典蓝牙的串口透传不同,BLE设备之间的通信并非简单的字节流,而是基于“属性(Attribute)”的层次化数据读写。理解服务(Service)、特征(Characteristic)和UUID之间的关系,是开发ESP32 BLE家居设备的基石。本章将深入剖析这套核心机制,并演示如何在ESP32上实现可靠的数据读写。
上图展示了BLE GATT(Generic Attribute Profile)协议栈中数据组织的完整层次。从顶层应用规范(Profile)到底层属性(Attribute),每一层都有明确的职责。在开发ESP32家居设备时,我们通常不需要关心Profile层,而是直接操作Service和Characteristic。下面我们将逐一拆解这些核心概念。
4.1 核心原理:从属性到服务的层次化数据模型
BLE的数据交互基于ATT(Attribute Protocol)协议,该协议定义了一种“属性”的存储和读取方式。一个属性本质上是一个命名的数据单元,它由以下四部分组成:
- 属性句柄(Handle):一个16位的唯一标识符,用于在设备内部寻址属性。客户端通过句柄来发起读写操作。
- 属性类型(Type):由UUID(Universally Unique Identifier)标识,决定了属性的含义。例如,0x2A00表示“设备名称”特征。
- 属性权限(Permissions):定义了该属性是否可读、可写、是否需要加密或认证。这是安全性的基础。
- 属性值(Value):实际承载的数据,长度可变(通常为0-512字节)。
为了便于管理,GATT层将多个相关的属性组合成特征(Characteristic)。一个特征至少包含两个属性:一个用于声明特征(包含特征属性、UUID和值句柄),另一个用于存储特征值。此外,特征还可以包含多个描述符(Descriptor),用于描述特征值的配置信息。最常用的描述符是客户端特征配置描述符(CCCD,0x2902),它用于启用或禁用通知(Notify)和指示(Indicate)。
多个特征再进一步聚合成服务(Service)。服务是一个逻辑功能单元,例如“电量服务”、“温湿度服务”或“开关控制服务”。一个设备可以包含多个服务,每个服务由唯一的UUID标识。这种层次化设计使得BLE设备可以像文件系统一样被浏览和访问,极大地简化了应用层的开发。
esp_ble_gatts_create_attr_tab() 时,实际上是在构建一个属性表,每个特征和描述符都会映射为表中的若干属性条目。
4.2 判断方法与操作流程:UUID的选择与特征属性配置
在动手编写代码之前,必须明确两个关键决策:UUID的分配和特征属性的定义。
UUID分配策略:
- 16位UUID(蓝牙SIG标准):用于标准服务,如电池服务(0x180F)、设备信息服务(0x180A)。这些UUID已由蓝牙技术联盟定义,具有固定的含义。
- 128位UUID(自定义):用于开发者自定义的服务和特征。ESP32允许你使用一个基础UUID(如
12345678-1234-5678-1234-56789abcdef0)并在此基础上生成16位别名,以简化代码。
特征属性(Characteristic Properties)配置:这是决定数据交互方式的核心。常见的属性标志位包括:
| 属性标志 | 值 | 说明 | 典型应用场景 |
|---|---|---|---|
| Read | 0x02 | 允许客户端读取特征值 | 设备状态查询(如当前温度) |
| Write | 0x08 | 允许客户端写入特征值,需要应答 | 控制指令(如开关灯) |
| Write No Response | 0x04 | 允许客户端写入,但设备不回复确认 | 高频数据下发(如调光指令) |
| Notify | 0x10 | 设备主动推送数据,无需客户端确认 | 传感器数据上报(如温湿度变化) |
| Indicate | 0x20 | 设备主动推送数据,客户端需确认 | 重要告警(如门锁被撬) |
操作流程(以ESP-IDF为例):
- 初始化GATT服务:调用
esp_ble_gatts_create_service()创建一个服务。 - 添加特征:在服务创建回调中,调用
esp_ble_gatts_add_char()添加特征,并指定UUID、权限和属性标志。 - 添加描述符:如果需要通知/指示功能,必须添加CCCD描述符(UUID 0x2902),并设置其权限为可读可写。
- 启动服务:调用
esp_ble_gatts_start_service()使服务对外可见。 - 处理读写事件:在GATT回调函数中,根据事件类型(如
ESP_GATTS_READ_EVT、ESP_GATTS_WRITE_EVT)处理数据。
4.3 案例拆解:ESP32智能灯泡的开关与状态同步
下面我们通过一个智能灯泡的案例,完整演示服务、特征、UUID和数据读写的实战应用。该灯泡支持手机远程开关、亮度调节,并实时上报状态。
场景与约束:
- 设备:ESP32开发板,控制一个LED灯。
- 需求:手机App可以读取灯的开关状态(Read),可以写入开关指令(Write),可以调节亮度(Write No Response),灯状态变化时主动通知手机(Notify)。
- 约束:功耗要求低,通信延迟小于100ms。
服务与特征设计:
| 服务 | 特征 | UUID | 属性 | 数据格式 |
|---|---|---|---|---|
| 0xFFE0(灯控服务) | 开关 | 0xFFE1 | Read, Write, Notify | 1字节:0x00=关,0x01=开 |
| 0xFFE0 | 亮度 | 0xFFE2 | Write No Response | 1字节:0x00-0x64(0-100%) |
关键代码实现(ESP-IDF):
// 定义UUID
#define GATT_SERVICE_UUID 0xFFE0
#define CHAR_SWITCH_UUID 0xFFE1
#define CHAR_BRIGHTNESS_UUID 0xFFE2
// 特征属性表
static esp_gatts_attr_db_t gatt_db[] = {
// 服务声明
{
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&GATT_SERVICE_UUID, ESP_GATT_PERM_READ,
sizeof(uint16_t), 0, (uint8_t *)&GATT_SERVICE_UUID}
},
// 开关特征声明
{
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&CHAR_SWITCH_UUID, ESP_GATT_PERM_READ,
sizeof(uint8_t), 0, (uint8_t *)&char_switch_prop}
},
// 开关特征值
{
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&CHAR_SWITCH_UUID, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
sizeof(uint8_t), 0, (uint8_t *)&switch_value}
},
// 开关CCCD描述符
{
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&cccd_uuid, ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE,
sizeof(uint16_t), 0, (uint8_t *)&cccd_value}
},
// 亮度特征声明(属性为Write No Response)
{
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&CHAR_BRIGHTNESS_UUID, ESP_GATT_PERM_READ,
sizeof(uint8_t), 0, (uint8_t *)&char_brightness_prop}
},
// 亮度特征值
{
{ESP_GATT_AUTO_RSP},
{ESP_UUID_LEN_16, (uint8_t *)&CHAR_BRIGHTNESS_UUID, ESP_GATT_PERM_WRITE,
sizeof(uint8_t), 0, (uint8_t *)&brightness_value}
},
};
数据读写处理逻辑:
- 写入开关:当手机写入0x01到0xFFE1时,ESP32在
ESP_GATTS_WRITE_EVT回调中解析数据,控制GPIO拉高点亮LED,并更新switch_value。 - 状态通知:当LED状态因本地按键改变时,调用
esp_ble_gatts_send_indicate()主动推送新的开关状态给手机。 - 亮度调节:手机通过Write No Response方式写入亮度值,ESP32接收后通过PWM调节LED亮度,无需回复确认,降低通信开销。
结果与复盘:该设计实现了小于50ms的响应延迟,且通过Notify机制,手机无需轮询即可实时获取状态变化。功耗方面,由于使用了Write No Response和Notify,减少了不必要的ACK包,整体电流降低了约20%。风险点在于:如果手机端未正确订阅CCCD,Notify将无法送达。因此,在调试时务必检查手机端是否成功写入了0x0001到0x2902描述符。
4.4 常见误区与总结复盘
在BLE开发中,以下问题出现频率最高,需要特别留意:
- UUID混淆:将16位UUID直接当作128位UUID使用,或者自定义UUID时未遵循规范。ESP32的
ESP_UUID_LEN_16和ESP_UUID_LEN_128必须严格区分。 - 权限配置错误:特征值设置了Notify属性,但CCCD描述符的权限未设置为可读写,导致手机无法订阅。
- 数据长度超限:BLE默认最大传输单元(MTU)为23字节,其中有效载荷仅20字节。如果需要传输大数据,必须协商MTU(如扩展到247字节)。
- 回调事件处理不完整:只处理了
ESP_GATTS_WRITE_EVT,却忽略了ESP_GATTS_READ_EVT和ESP_GATTS_CONF_EVT(确认事件),导致状态不同步。
总结:掌握服务、特征、UUID和属性的关系,是开发BLE应用的分水岭。建议在动手写代码前,先画出类似本章开头的层次图,明确每个特征的UUID、属性和数据格式。在调试时,使用nRF Connect或ESP32的BLE调试工具,查看属性表是否与设计一致。只有底层模型清晰,上层应用才能稳定高效。