在蓝牙5.0 BLE(低功耗蓝牙)开发中,GATT(通用属性规范) 是应用层数据交互的绝对核心。很多开发者能连上设备,却在“找不到服务”或“收不到数据”上栽跟头。本文将深入服务发现流程与通知(Notify)机制这两大实战难点,助你打通BLE通信的“任督二脉”。
一、GATT服务发现:连接后的“地图加载”
BLE连接建立后,手机(Client)并不知道设备(Server)内部有什么能力。服务发现(Service Discovery) 就是Client主动去Server端“读取地图”的过程。这个过程必须严格按照ATT(属性协议)的顺序执行。
1. 发现流程状态机
在Android等平台,GATT操作是异步回调驱动的。一个标准的发现流程必须遵循以下状态迁移:
1. 连接成功:onConnectionStateChange回调状态变为STATE_CONNECTED。
2. 触发发现:立即调用gatt.discoverServices(),进入发现状态。
3. 发现完成:等待onServicesDiscovered回调,状态为GATT_SUCCESS。
4. 获取对象:只有在此之后,调用gatt.getService(UUID)才能拿到非空对象。
避坑指南:绝对不要在连接成功回调后立即进行读写操作,此时服务表尚未加载,必然失败。
2. 底层交互解析(ATT层)
服务发现在底层是通过一系列ATT指令完成的:
- MTU交换:协商双方能处理的最大数据包长度。
• 发现主服务:Client发送ATT_READ_BY_GROUP_TYPE_REQ,遍历Server的属性表,找出所有UUID=0x2800(主服务声明)的条目。
- 发现特征:针对每个服务,发送ATT_READ_BY_TYPE_REQ,查找UUID=0x2803(特征声明),获取特征的属性(Properties)和句柄(Handle)。
二、通知机制(Notify/Indicate):服务端主动“推数据”
BLE通信默认是Client主动请求(Read/Write)。若要让Server(如传感器)主动上报数据,必须使用通知(Notify) 或指示(Indicate) 机制。
1. Notify vs Indicate:一字之差,天壤之别
两者都用于Server主动发送数据,但可靠性机制完全不同:
特性 Notify(通知) Indicate(指示)
确认机制 ❌ 无ACK(发完即忘) ✅ 有ACK(必须确认)
可靠性 可能丢包(适合传感器流) 可靠送达(适合关键指令)
速度 快,无等待开销 慢,需等待Client回复
2. 使能通知的“三步走”代码(Android示例)
通知机制是双向握手的:Client必须先告诉Server“我要订阅”,Server才能推送。这通过写入CCCD(客户端特征配置描述符) 实现。
// 1. 获取目标特征(Characteristic)
val service = gatt.getService(UUID_SERVICE)
val char = service.getCharacteristic(UUID_CHAR_NOTIFY)
// 2. 本地开启监听(告诉Android系统准备接收)
gatt.setCharacteristicNotification(char, true)
// 3. 关键步骤:写入CCCD描述符,告诉设备端“开启通知”
val cccd = char.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
cccd.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccd) // 这一步才是真正的“订阅”
关键点:
• CCCD UUID:固定为00002902-0000-1000-8000-00805f9b34fb。
• 写入值:ENABLE_NOTIFICATION_VALUE(0x0001)开启通知;ENABLE_INDICATION_VALUE(0x0002)开启指示。
3. 服务端(嵌入式侧)发送逻辑
当Server端(如ESP32、nRF52)检测到CCCD被写入0x0001后,即可在数据更新时主动发送。
// 基于ESP-IDF的伪代码示例
// 检查客户端是否开启了通知
uint16_t cccd_value;
esp_ble_gatts_get_attr_value(cccd_handle, &cccd_value);
if (cccd_value & ESP_GATT_PERM_NOTIFY) {
// 发送通知(无确认,高效)
esp_ble_gatts_send_indicate(gatts_if, conn_id, char_handle, data_len, data, false);
// 若为Indicate,最后一个参数传 true
}
三、量产级调试与排错
1. 服务发现失败(onServicesDiscovered返回错误)
• 原因:MTU协商失败或ATT超时。
• 对策:在连接后、发现服务前,先调用gatt.requestMtu(247)申请更大的MTU(需设备端支持),减少分包次数。
2. 收不到通知(Notify不触发)
• 检查CCCD:90%的问题出在这里。确认Client端writeDescriptor是否成功,且Server端正确解析了CCCD的值。
• 连接参数:通知频率受Connection Interval限制。若间隔太长(如1秒),数据会堆积。建议在Android端调用gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)请求更短的间隔。
3. 数据截断
• 默认限制:BLE默认ATT_MTU为23字节,有效载荷仅20字节。长数据需进行MTU协商(如扩展到247字节)或应用层分包。
四、结语
GATT服务发现是BLE通信的“地基”,而通知机制是实时数据流的“桥梁”。掌握这两者,意味着你已突破了BLE开发中最容易卡壳的关卡。记住核心口诀:先发现,后操作;开通知,先写CCCD。
117