蓝牙特征读写配置: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 解析流程

  1. 调用 gatt.discoverServices()。
  2. 在 onServicesDiscovered() 回调中获取 gatt.getServices() 列表。
  3. 遍历 Service,通过 service.getUuid() 匹配目标服务。
  4. 从匹配的 Service 中获取 service.getCharacteristics(),遍历并按 UUID 匹配目标特征。
  5. 保存对应的 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(接收信号强度)动态调整。

调试与验证

  1. 使用 nRF Connect App扫描设备,手动查看 Service/Characteristic UUID,确认与代码一致。
  2. 在 onCharacteristicChanged 回调中打印数据,验证通知是否正常触发。
  3. 模拟弱信号场景(如金属盒屏蔽),测试超时恢复机制。
日志关键点:记录每次读/写操作的时间戳、操作类型、状态码、数据长度,便于定位超时是发生在协议栈还是设备端。

常见错误与解决方案

错误现象可能原因解决方案
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。
BLE GATT 层次与通知使能流程 Android 客户端 (GATT Client) 蓝牙设备 (GATT Server) Service (UUID: xxxx) Characteristic Descriptor (0x2902) setCharNotification(true) writeDescriptor(0x0001) 写入 CCCD Notify 数据 先注册,后写Descriptor 超时断路器:Handler延迟