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,这是整个通信的“消息中心”。所有连接状态变化、服务发现、数据读写,都会回调到这里。
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 和对象的映射关系。
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 通信中最常用的方式。设备主动推送数据,不需要你轮询去读。
开启通知的步骤:
- 调用
setCharacteristicNotification(characteristic, true) - 写入 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();
// 处理实时数据
}
通知和指示的区别:通知不需要设备确认,速度快但可能丢包;指示需要设备确认,更可靠但速度慢。我一般用通知,除非是传输关键数据。
9.5 核心流程总结
整个 BLE 通信流程,我用一张图来总结:
这张图把整个流程串起来了。从连接开始,到服务发现,再到读写和通知。每一步都是异步的,必须等前一步完成才能进行下一步。
9.6 实战建议
最后,给你几个实战中的建议:
- 线程管理:所有 GATT 操作都在同一个线程中执行,建议用 Handler 或 RxJava 来管理操作队列。
- 超时处理:连接和操作都可能超时,建议加一个超时定时器,超时后主动断开重连。
- 资源释放:断开连接后,记得调用
gatt.close()释放资源,否则会内存泄漏。 - 重连机制:设备断开后,不要立即重连,建议加一个延迟和重试次数限制。
嗯,这一章的内容就到这里。BLE 通信的核心就是这些,你只要把流程走通,剩下的就是业务逻辑的处理了。