9、BLE连接与通信:连接GATT服务器、发现服务、读写特征值、通知与指示

好,咱们接着聊。上一章我们把设备扫描搞定了,拿到了一个 BluetoothDevice 对象。但拿到设备只是第一步,真正的硬仗是建立连接、找到服务、读写数据。这一章,我带你走一遍完整的 GATT 通信流程。

说白了,BLE 通信的核心就是 GATT 协议。设备之间怎么交换数据?就是通过 GATT 的 Service(服务)和 Characteristic(特征值)。你想想看,一个蓝牙设备就像一栋楼,Service 是楼层,Characteristic 是房间号。你要找到正确的房间,才能拿到里面的数据。

9.1 连接GATT服务器

连接这一步,我习惯用 BluetoothDevice.connectGatt() 方法。这个方法会返回一个 BluetoothGatt 对象,后续所有操作都靠它。

BluetoothGatt bluetoothGatt = device.connectGatt(context, false, gattCallback);

第二个参数 autoConnect,我建议你设为 false。为什么?设为 true 的话,系统会尝试自动重连,但实际体验并不好。我在项目中遇到过,设为 true 后设备断开,系统一直在后台重连,导致用户手动点击连接时反而连不上。嗯,这里要注意。

第三个参数是 BluetoothGattCallback,这是整个通信的“消息中心”。所有连接状态变化、服务发现、数据读写,都会回调到这里。

注意:connectGatt() 必须在主线程调用。另外,一个 BluetoothDevice 只能有一个 BluetoothGatt 实例,重复调用会导致问题。

9.2 发现服务:onServicesDiscovered

连接成功后,系统会回调 onConnectionStateChange。当状态变为 BluetoothProfile.STATE_CONNECTED 时,就该调用 discoverServices() 了。

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        // 连接成功,开始发现服务
        gatt.discoverServices();
    } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
        // 断开连接,清理资源
    }
}

服务发现完成后,会回调 onServicesDiscovered。这时候你就可以拿到所有 Service 和 Characteristic 了。

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        List<BluetoothGattService> services = gatt.getServices();
        for (BluetoothGattService service : services) {
            // 遍历服务,找到你需要的那个
            List<BluetoothGattCharacteristic> characteristics = service.getCharacteristics();
            for (BluetoothGattCharacteristic characteristic : characteristics) {
                // 遍历特征值
            }
        }
    }
}

我个人习惯在发现服务后,把需要的 Service 和 Characteristic 缓存起来,避免每次都要遍历。比如用 Map 存一下 UUID 和对象的映射关系。

小技巧:如果你知道设备的 Service UUID,可以在发现服务后直接用 gatt.getService(UUID) 获取,不用遍历所有服务。

9.3 读写特征值

拿到 Characteristic 后,就可以读写数据了。读操作很简单:

bluetoothGatt.readCharacteristic(characteristic);

结果会回调到 onCharacteristicRead

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        byte[] data = characteristic.getValue();
        // 处理读取到的数据
    }
}

写操作稍微复杂一点。你需要先设置要写入的数据,然后调用 writeCharacteristic

characteristic.setValue(data);
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
bluetoothGatt.writeCharacteristic(characteristic);

写的结果会回调到 onCharacteristicWrite。这里有个坑:写操作有几种类型,WRITE_TYPE_DEFAULT 需要等待设备回应,WRITE_TYPE_NO_RESPONSE 则不需要。我曾经在项目中用错了类型,导致数据一直写不进去,排查了半天才发现是类型不匹配。

核心要点:读写操作都是异步的,不能连续调用。必须等上一个回调完成后,才能发起下一个操作。否则会失败。

9.4 通知与指示

通知(Notification)和指示(Indication)是 BLE 通信中最常用的方式。设备主动推送数据,不需要你轮询去读。

开启通知的步骤:

  1. 调用 setCharacteristicNotification(characteristic, true)
  2. 写入 CCCD(Client Characteristic Configuration Descriptor)
// 第一步:启用通知
bluetoothGatt.setCharacteristicNotification(characteristic, true);

// 第二步:写入 CCCD
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
);
if (descriptor != null) {
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    bluetoothGatt.writeDescriptor(descriptor);
}

通知数据会回调到 onCharacteristicChanged

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    byte[] data = characteristic.getValue();
    // 处理实时数据
}

通知和指示的区别:通知不需要设备确认,速度快但可能丢包;指示需要设备确认,更可靠但速度慢。我一般用通知,除非是传输关键数据。

避坑指南:我曾经在小米手机上遇到一个问题,调用 setCharacteristicNotification 后,设备没有收到 CCCD 写入。后来发现是某些国产 ROM 对 BLE 做了限制,需要延迟 50ms 再写入 descriptor。嗯,这种兼容性问题很头疼。

9.5 核心流程总结

整个 BLE 通信流程,我用一张图来总结:

BLE GATT 通信流程 1. connectGatt() 2. onConnectionStateChange 3. discoverServices() 4. onServicesDiscovered 读特征值 写特征值 开启通知/指示

这张图把整个流程串起来了。从连接开始,到服务发现,再到读写和通知。每一步都是异步的,必须等前一步完成才能进行下一步。

9.6 实战建议

最后,给你几个实战中的建议:

  • 线程管理:所有 GATT 操作都在同一个线程中执行,建议用 Handler 或 RxJava 来管理操作队列。
  • 超时处理:连接和操作都可能超时,建议加一个超时定时器,超时后主动断开重连。
  • 资源释放:断开连接后,记得调用 gatt.close() 释放资源,否则会内存泄漏。
  • 重连机制:设备断开后,不要立即重连,建议加一个延迟和重试次数限制。

嗯,这一章的内容就到这里。BLE 通信的核心就是这些,你只要把流程走通,剩下的就是业务逻辑的处理了。


公众号:蓝海资料掘金营,微信deep3321