GATT服务器端开发:BluetoothGattServer、BluetoothGattServerCallback、自定义Service
好,咱们今天聊聊 BLE 开发中另一个重要角色——GATT 服务器端。说白了,就是让你的 Android 设备变成一个「被连接」的设备,而不是主动去扫描连接别人。
我刚开始做 BLE 项目时,大部分时间都在写客户端(Central),也就是手机去连手环、连传感器。直到有一次,产品经理说:「咱们能不能让手机当体温计,让平板来读数据?」嗯,这时候就得写 GATT 服务器端了。
你想想看,一个 BLE 设备要想被别人访问,它得先把自己的数据组织好,暴露出去。这个组织方式就是 GATT 协议规定的——Service、Characteristic、Descriptor 这三层结构。今天我们就来手把手搭一个自定义的 GATT 服务器。
GATT 服务器架构概览
先看一张图,帮你快速建立整体认知。GATT 服务器端的核心组件就三个:
- BluetoothGattServer:服务器本体,负责接收连接、处理请求
- BluetoothGattServerCallback:回调接口,所有事件都在这里处理
- 自定义 Service/Characteristic:你暴露给外界的数据结构
第一步:获取 BluetoothGattServer
要创建 GATT 服务器,得先拿到 BluetoothManager,然后调用 openGattServer()。这个 API 会返回一个 BluetoothGattServer 实例,或者 null(如果设备不支持 BLE 服务器模式)。
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val bluetoothAdapter = bluetoothManager.adapter
// 注意:需要先确保蓝牙已开启
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
// 处理蓝牙未开启的情况
return
}
val gattServer = bluetoothManager.openGattServer(
this, // Context
gattServerCallback // BluetoothGattServerCallback
)
第二步:实现 BluetoothGattServerCallback
这个回调接口是 GATT 服务器的心脏。所有客户端发起的操作,都会在这里通知你。你需要重写以下几个关键方法:
| 回调方法 | 触发时机 | 你需要做的事 |
|---|---|---|
| onConnectionStateChange() | 客户端连接/断开 | 记录连接状态,更新 UI |
| onCharacteristicReadRequest() | 客户端读特征值 | 准备数据,调用 sendResponse() |
| onCharacteristicWriteRequest() | 客户端写特征值 | 解析数据,执行逻辑,调用 sendResponse() |
| onDescriptorReadRequest() | 客户端读描述符 | 通常用于 CCCD(客户端特征配置描述符) |
| onDescriptorWriteRequest() | 客户端写描述符 | 处理通知/指示的启用/禁用 |
| onMtuChanged() | MTU 协商完成 | 记录新的 MTU 值,用于后续大包传输 |
来看一个完整的回调实现示例:
private val gattServerCallback = object : BluetoothGattServerCallback() {
override fun onConnectionStateChange(
device: BluetoothDevice,
status: Int,
newState: Int
) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
Log.d(TAG, "设备已连接: ${device.address}")
// 记录连接设备,用于后续发送通知
connectedDevice = device
}
BluetoothProfile.STATE_DISCONNECTED -> {
Log.d(TAG, "设备已断开: ${device.address}")
connectedDevice = null
}
}
}
override fun onCharacteristicReadRequest(
device: BluetoothDevice,
requestId: Int,
offset: Int,
characteristic: BluetoothGattCharacteristic
) {
// 根据特征值 UUID 返回不同数据
val value = when (characteristic.uuid) {
TEMPERATURE_CHAR_UUID -> {
// 读取当前温度值
val temp = readCurrentTemperature()
floatToBytes(temp)
}
BATTERY_LEVEL_CHAR_UUID -> {
byteArrayOf(currentBatteryLevel.toByte())
}
else -> {
// 未知特征值,返回错误
gattServer?.sendResponse(device, requestId,
BluetoothGatt.GATT_INVALID_HANDLE, offset, null)
return
}
}
gattServer?.sendResponse(device, requestId,
BluetoothGatt.GATT_SUCCESS, offset, value)
}
override fun onCharacteristicWriteRequest(
device: BluetoothDevice,
requestId: Int,
characteristic: BluetoothGattCharacteristic,
preparedWrite: Boolean,
responseNeeded: Boolean,
offset: Int,
value: ByteArray
) {
// 处理写入请求
when (characteristic.uuid) {
COMMAND_CHAR_UUID -> {
handleCommand(value)
}
TARGET_TEMP_CHAR_UUID -> {
val targetTemp = bytesToFloat(value)
setTargetTemperature(targetTemp)
}
}
if (responseNeeded) {
gattServer?.sendResponse(device, requestId,
BluetoothGatt.GATT_SUCCESS, offset, null)
}
}
override fun onMtuChanged(device: BluetoothDevice, mtu: Int) {
Log.d(TAG, "MTU 更新为: $mtu")
currentMtu = mtu
}
}
第三步:自定义 Service 和 Characteristic
现在我们来定义自己的 GATT 服务。假设我们要做一个「智能温度计」,暴露两个特征值:当前温度(只读)和目标温度(可读写)。
// 定义 UUID
val SERVICE_UUID = UUID.fromString("00001809-0000-1000-8000-00805F9B34FB")
val TEMPERATURE_CHAR_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805F9B34FB")
val TARGET_TEMP_CHAR_UUID = UUID.fromString("00002A1D-0000-1000-8000-00805F9B34FB")
// 创建 Service
val tempService = BluetoothGattService(
SERVICE_UUID,
BluetoothGattService.SERVICE_TYPE_PRIMARY
)
// 创建只读特征值:当前温度
val temperatureChar = BluetoothGattCharacteristic(
TEMPERATURE_CHAR_UUID,
BluetoothGattCharacteristic.PROPERTY_READ,
BluetoothGattCharacteristic.PERMISSION_READ
)
// 创建可读写特征值:目标温度
val targetTempChar = BluetoothGattCharacteristic(
TARGET_TEMP_CHAR_UUID,
BluetoothGattCharacteristic.PROPERTY_READ or
BluetoothGattCharacteristic.PROPERTY_WRITE,
BluetoothGattCharacteristic.PERMISSION_READ or
BluetoothGattCharacteristic.PERMISSION_WRITE
)
// 添加特征值到 Service
tempService.addCharacteristic(temperatureChar)
tempService.addCharacteristic(targetTempChar)
// 添加 Service 到 GATT 服务器
gattServer?.addService(tempService)
🔑 关键点:
- PROPERTY 决定客户端能做什么(读、写、通知等)
- PERMISSION 决定是否需要加密连接才能操作
- UUID 建议使用标准 SIG UUID 或自定义 UUID(格式:0000xxxx-0000-1000-8000-00805F9B34FB)
第四步:发送通知(Notification)
有时候服务器需要主动通知客户端数据变化,比如温度值更新了。这时候就需要用到通知机制。通知需要客户端先订阅(通过写 CCCD 描述符),然后服务器才能发送。
// 在 Service 中添加 CCCD 描述符
val cccdDescriptor = BluetoothGattDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"),
BluetoothGattDescriptor.PERMISSION_READ or
BluetoothGattDescriptor.PERMISSION_WRITE
)
temperatureChar.addDescriptor(cccdDescriptor)
// 在回调中处理 CCCD 写入
override fun onDescriptorWriteRequest(
device: BluetoothDevice,
requestId: Int,
descriptor: BluetoothGattDescriptor,
preparedWrite: Boolean,
responseNeeded: Boolean,
offset: Int,
value: ByteArray
) {
if (descriptor.uuid == UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")) {
// 解析 CCCD 值
val enableNotification = value[0].toInt() == 0x01
Log.d(TAG, "通知已${if (enableNotification) "启用" else "禁用"}")
}
if (responseNeeded) {
gattServer?.sendResponse(device, requestId,
BluetoothGatt.GATT_SUCCESS, offset, null)
}
}
// 发送通知
fun sendTemperatureNotification(temperature: Float) {
val device = connectedDevice ?: return
val value = floatToBytes(temperature)
// 设置特征值数据
temperatureChar.value = value
// 发送通知
gattServer?.notifyCharacteristicChanged(
device,
temperatureChar,
false // false 表示通知,true 表示指示
)
}
完整流程串联
把上面所有步骤串起来,一个完整的 GATT 服务器启动流程如下:
- 获取 BluetoothManager 和 BluetoothAdapter
- 调用 openGattServer() 创建服务器实例
- 构建自定义 Service 和 Characteristic
- 调用 addService() 注册到服务器
- 在回调中处理客户端的读写请求
- 需要时调用 notifyCharacteristicChanged() 发送通知
- 页面销毁时调用 close() 关闭服务器
override fun onDestroy() {
super.onDestroy()
gattServer?.close() // 关闭服务器,释放资源
}
好了,GATT 服务器端开发的核心内容就这些。说白了就是三步:建服务器、写回调、定义数据。你想想看,其实和写一个 HTTP 服务器很像——监听端口、处理请求、返回数据。只不过 BLE 的协议栈帮你封装了底层的蓝牙通信细节。
在实际项目中,我建议你把 Service 的定义和回调处理封装成一个独立的类,方便复用和测试。另外,多设备连接时要注意管理好每个设备的连接状态和通知订阅状态,这块容易出 bug。