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)客户端

通知,说白了就是服务端主动给客户端发数据。比如心率监测,每秒钟都要上报一次心率值,总不能让客户端一直轮询吧?

实现通知其实就两步:

  1. 检查客户端是否已经订阅了通知(也就是CCCD配置)
  2. 调用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就够了,效率更高。

核心要点:通知的前提是客户端已经写入了CCCD(Client Characteristic Configuration Descriptor)。你需要在onDescriptorWriteRequest回调里处理这个逻辑,把客户端的订阅状态保存下来。

5.5 整体流程梳理

为了让你看得更清楚,我画了一张图,把整个GATT服务端的生命周期串起来:

GATT服务端开发流程 1. 获取BluetoothGattServer 2. 创建Service & Characteristic 3. 添加Service到Server 4. 等待客户端连接 5. 处理读写请求(回调) 6. 主动Notify客户端 循环 关键回调方法: • onConnectionStateChange — 客户端连接/断开 • onCharacteristicReadRequest — 客户端读取特征值 • onCharacteristicWriteRequest — 客户端写入特征值 • onDescriptorWriteRequest — 客户端订阅/取消通知

5.6 避坑指南

最后,分享几个我踩过的坑:

  • Service添加失败:同一个UUID的Service不能重复添加。如果你在调试时反复调用addService,记得先调用clearServices清空一下。
  • 通知发不出去:检查一下客户端是否真的订阅了。我曾经遇到一个情况,客户端订阅了,但CCCD的值没写对,导致通知一直发不出去。你可以在onDescriptorWriteRequest里打印一下value,看看是不是0x0001(启用通知)。
  • 多设备连接:一个GATT Server可以同时连接多个客户端。通知的时候,记得遍历所有已订阅的设备,别只发给第一个。

好了,关于GATT服务端开发的核心内容就这些。你想想看,其实流程并不复杂,关键是把回调里的逻辑处理好。下次我们聊聊更进阶的话题——如何管理多个连接和并发请求。