蓝牙特征读写配置:Service/Characteristic 解析、通知使能、读写超时
蓝牙低功耗(BLE)通信的核心是 GATT(Generic Attribute Profile)协议。本章讲解如何解析设备端暴露的 Service 和 Characteristic,启用通知(Notification)机制,并配置读写超时策略,确保 OkHttp 上层数据上传的稳定低层链路。
GATT 层次结构
每个 BLE 设备作为 GATT Server 暴露一个或多个 Service,每个 Service 包含多个 Characteristic,每个 Characteristic 可能带 Descriptor(用于配置属性,如通知开关)。
| 层级 | 描述 | UUID 示例 |
|---|---|---|
| Service | 逻辑功能分组,如电池服务、设备信息服务 | 0x180F(电池) |
| Characteristic | 具体数据单元,包含 value 和属性(read/write/notify) | 0x2A19(电池电量) |
| Descriptor | 描述 Characteristic 的配置,常用 CCCD(Client Characteristic Configuration Descriptor) | 0x2902(通知使能) |
开发要点:必须通过
BluetoothGatt.discoverServices() 触发服务发现,完成后再遍历 Service 及 Characteristic,不要假设设备提前暴露。Service / Characteristic 解析流程
- 调用
gatt.discoverServices()。 - 在
onServicesDiscovered()回调中获取gatt.getServices()列表。 - 遍历 Service,通过
service.getUuid()匹配目标服务。 - 从匹配的 Service 中获取
service.getCharacteristics(),遍历并按 UUID 匹配目标特征。 - 保存对应的
BluetoothGattCharacteristic引用,用于后续读写和通知操作。
// 示例:解析服务和特征
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
for (BluetoothGattService service : gatt.getServices()) {
if (service.getUuid().equals(MY_SERVICE_UUID)) {
for (BluetoothGattCharacteristic charac : service.getCharacteristics()) {
if (charac.getUuid().equals(MY_CHAR_UUID)) {
targetCharacteristic = charac;
// 后续执行使能通知等操作
}
}
}
}
}
}
通知使能(Enable Notification)
只有 Characteristic 属性包含 PROPERTY_NOTIFY 或 PROPERTY_INDICATE 时才能接收主动数据推送。启用通知需修改其 CCCD Descriptor(UUID: 0x2902),写入对应值:
- 通知(Notification): 写入
0x0001 - 指示(Indicate): 写入
0x0002
注意:必须先调用
gatt.setCharacteristicNotification(characteristic, true),再写入 Descriptor。部分国产芯片对写入顺序敏感,建议间隔 100ms 以上。// 使能通知的标准步骤
public void enableNotification(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
boolean registered = gatt.setCharacteristicNotification(characteristic, true);
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(MY_CCCD_UUID);
if (descriptor != null) {
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);
} else {
Log.e("BLE", "CCCD descriptor not found");
}
}
在 onDescriptorWrite() 回调中确认写入成功,再订阅 OkHttp 数据流。
读写超时配置
BLE 操作(readCharacteristic、writeCharacteristic、executeReliableWrite)可能因设备繁忙、信号衰减或固件异常而超时。建议实现应用层超时:
- 方案一:Handler + Runnable。发起操作时发送延迟消息,若在超时前收到对应回调则移除消息,否则触发重试或错误通知。
- 方案二:CompletableFuture + 超时。更适用于 OkHttp 异步链路,但需注意线程切换。
// 利用 Handler 实现读超时(基础版)
private Handler timeoutHandler = new Handler(Looper.getMainLooper());
private Runnable readTimeoutRunnable = () -> {
Log.e("BLE", "Read characteristic timeout");
// 断开重连或上报错误
disconnectAndReconnect();
};
public void readWithTimeout(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, long timeoutMs) {
timeoutHandler.postDelayed(readTimeoutRunnable, timeoutMs);
gatt.readCharacteristic(characteristic);
}
// 在 onCharacteristicRead 回调中取消超时
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
timeoutHandler.removeCallbacks(readTimeoutRunnable);
// 处理数据
}
推荐超时基值:读操作 5~10s,写操作 3~5s,通知使能操作 10s。可根据设备 RSSI(接收信号强度)动态调整。
调试与验证
- 使用 nRF Connect App扫描设备,手动查看 Service/Characteristic UUID,确认与代码一致。
- 在
onCharacteristicChanged回调中打印数据,验证通知是否正常触发。 - 模拟弱信号场景(如金属盒屏蔽),测试超时恢复机制。
日志关键点:记录每次读/写操作的时间戳、操作类型、状态码、数据长度,便于定位超时是发生在协议栈还是设备端。
常见错误与解决方案
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| onServicesDiscovered 未触发 | 未调用 discoverServices() 或设备已断开 | 检查连接状态,重新调用 |
| onCharacteristicRead 返回 128(GATT_INSUFFICIENT_AUTHENTICATION) | 特征需要配对/加密 | 先创建 Bone(安全连接)或设置 MTU |
| 通知未收到数据 | CCCD 未写入或属性不含 NOTIFY | 检查 Characteristic 属性;确认写入顺序 |
| 读写超时频繁 | 设备响应慢或连接间隔过大 | 调整连接参数(Connection Interval)到 30~50ms |
| OkHttp 层数据积压 | 上层处理太慢,导致 BLE 回调堆积 | 使用线程池或事件队列,异步处理 |
性能与稳定性优化
- 批量写入:对于大数据分包,使用
BluetoothGatt.executeReliableWrite()或队列逐个写入,避免同时发起多个请求。 - 超时处理分级:轻度超时重试 1~2 次,连续超时则断开重连并清除缓存。
- 状态机保护:维护一个操作状态(IDLE / READING / WRITING / NOTIFYING),防止回调乱序。
- OkHttp 侧的背压:在
onCharacteristicChanged中将数据放入阻塞队列,由 OkHttp 工作线程拉取,避免 UI 线程处理 IO。