5. BLE GATT服务端开发:BluetoothGattServer的创建、添加Service与Characteristic、响应读写请求、通知(Notify)客户端
各位同学,今天我们来聊聊BLE开发中一个非常核心的角色——GATT服务端。
平时我们做手机端开发,大部分时间都在扮演GATT客户端的角色,也就是去扫描、连接、读写外设。但有时候,我们的Android设备需要反过来,作为一个“外设”去提供服务。比如,你的手机模拟一个心率计,或者做一个蓝牙键盘。这时候,你就得自己搭一个GATT服务端。
说白了,就是让你的Android设备“装”成一个BLE设备,让别人来连你。
5.1 创建BluetoothGattServer
第一步,得先拿到一个BluetoothGattServer的实例。这个东西不能直接new,得通过系统服务来获取。
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothGattServer gattServer = bluetoothManager.openGattServer(context, gattServerCallback);
这里有个关键点——openGattServer的第二个参数是一个回调。这个回调非常重要,所有客户端的连接、读写请求,都会通过它通知你。
openGattServer之前,请确保你的应用已经获取了BLUETOOTH_ADVERTISE权限(Android 12+)。我早期做项目时,就因为漏了这个权限,回调死活不触发,排查了半天。
5.2 添加Service与Characteristic
拿到GATT Server之后,下一步就是往里塞Service和Characteristic。这就像搭积木:一个Service里可以包含多个Characteristic,每个Characteristic又有自己的属性和值。
我个人习惯先把Service和Characteristic的UUID定义好,这样代码看起来清爽很多。
UUID SERVICE_UUID = UUID.fromString("0000180D-0000-1000-8000-00805F9B34FB"); // 心率服务
UUID CHAR_HEART_RATE_MEASUREMENT_UUID = UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB");
BluetoothGattService heartRateService = new BluetoothGattService(
SERVICE_UUID,
BluetoothGattService.SERVICE_TYPE_PRIMARY
);
BluetoothGattCharacteristic heartRateChar = new BluetoothGattCharacteristic(
CHAR_HEART_RATE_MEASUREMENT_UUID,
BluetoothGattCharacteristic.PROPERTY_READ | BluetoothGattCharacteristic.PROPERTY_NOTIFY,
BluetoothGattCharacteristic.PERMISSION_READ
);
heartRateService.addCharacteristic(heartRateChar);
gattServer.addService(heartRateService);
嗯,这里要注意一下Characteristic的属性。你想想看,如果你只设置了PROPERTY_READ,客户端是没法订阅通知的。必须把PROPERTY_NOTIFY也加上,客户端才能收到你主动推送的数据。
PROPERTY_WRITE和对应的PERMISSION_WRITE。我曾经遇到过一个坑:只加了属性,忘了加权限,结果客户端写数据一直失败。
5.3 响应读写请求
当客户端发起读写操作时,你的gattServerCallback就会被调用。这里我直接贴一个典型的回调实现:
private BluetoothGattServerCallback gattServerCallback = new BluetoothGattServerCallback() {
@Override
public void onCharacteristicReadRequest(BluetoothDevice device, int requestId, int offset, BluetoothGattCharacteristic characteristic) {
// 读取当前心率值
byte[] heartRateData = getCurrentHeartRateData();
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, heartRateData);
}
@Override
public void onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
// 处理写入的数据
if (responseNeeded) {
gattServer.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, offset, null);
}
}
};
这里有个细节:sendResponse的第四个参数是offset,第五个参数是数据。如果你的数据很长,客户端可能会分多次读取,每次带不同的offset。嗯,这种情况在心率服务里不常见,但如果你做OTA升级或者文件传输,就得处理这个逻辑。
5.4 通知(Notify)客户端
通知,说白了就是服务端主动给客户端发数据。比如心率监测,每秒钟都要上报一次心率值,总不能让客户端一直轮询吧?
实现通知其实就两步:
- 检查客户端是否已经订阅了通知(也就是CCCD配置)
- 调用
notifyCharacteristicChanged方法
public void sendHeartRateNotification(int heartRate) {
byte[] data = new byte[]{(byte) heartRate};
heartRateChar.setValue(data);
// 通知所有已订阅的客户端
List<BluetoothDevice> devices = getSubscribedDevices(); // 你需要自己维护这个列表
for (BluetoothDevice device : devices) {
gattServer.notifyCharacteristicChanged(device, heartRateChar, false);
}
}
第三个参数confirm,如果是true,就表示使用Indication(需要客户端确认);false就是普通的Notify。我个人建议,如果数据丢失影响不大,用Notify就够了,效率更高。
onDescriptorWriteRequest回调里处理这个逻辑,把客户端的订阅状态保存下来。
5.5 整体流程梳理
为了让你看得更清楚,我画了一张图,把整个GATT服务端的生命周期串起来:
5.6 避坑指南
最后,分享几个我踩过的坑:
- Service添加失败:同一个UUID的Service不能重复添加。如果你在调试时反复调用
addService,记得先调用clearServices清空一下。 - 通知发不出去:检查一下客户端是否真的订阅了。我曾经遇到一个情况,客户端订阅了,但CCCD的值没写对,导致通知一直发不出去。你可以在
onDescriptorWriteRequest里打印一下value,看看是不是0x0001(启用通知)。 - 多设备连接:一个GATT Server可以同时连接多个客户端。通知的时候,记得遍历所有已订阅的设备,别只发给第一个。
好了,关于GATT服务端开发的核心内容就这些。你想想看,其实流程并不复杂,关键是把回调里的逻辑处理好。下次我们聊聊更进阶的话题——如何管理多个连接和并发请求。