4. 特征读取实现与数据解析:onCharacteristicRead 回调、byte[] 转类型、错误码判断
4.1 原理概述
GATT 特征读取是蓝牙 GATT 客户端主动获取服务器端特征值的过程。当客户端调用 readCharacteristic(characteristic) 后,BLE 协议栈会发起属性协议 (ATT) 的 Read Request,服务器响应并返回特征值数据。读取结果通过 onCharacteristicRead() 回调返回,包含原始字节数组和状态码。
数据解析的核心是将 byte[] 按特征值定义的数据类型(如 uint8、uint16、float、字符串、数组等)转换为应用层可用的 Java/Kotlin 类型。错误码判断用于识别读取是否成功(如 GATT_SUCCESS)或通信异常(如 GATT_READ_NOT_PERMITTED、GATT_INSUFFICIENT_AUTHENTICATION 等)。
4.2 实现流程
特征读取全流程(简化步骤)
- 发起读取请求:BluetoothGatt 实例调用
readCharacteristic(BluetoothGattCharacteristic)。 - 等待回调:系统将请求排入 BLE 协议栈,服务器响应后调用客户端注册的
BluetoothGattCallback.onCharacteristicRead()。 - 解析 byte[]:根据特征声明格式,将
byte[]转换为目标类型(如 int、String、byte[] 原样等)。 - 判断错误码:检查
status参数,若不为 GATT_SUCCESS 则处理错误。 - 更新本地缓存/UI:将解析结果保存到内存或本地数据层,并通知 UI 刷新。
4.3 BLE 特征读取逻辑流程(内联 SVG 图)
4.4 代码实现:Android (Kotlin)
以下示例展示在 BluetoothGattCallback 中处理特征读取并解析数据。
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val value: ByteArray = characteristic.value ?: return
// 根据特征 UUID 或配置决定解析方式
when (characteristic.uuid) {
UUID_HEART_RATE_MEASUREMENT -> {
// 格式: uint8 HR 值,直接取第一个字节
val heartRate = value[0].toInt() and 0xFF
updateHeartRateUI(heartRate)
}
UUID_DEVICE_NAME -> {
// 字符串 (UTF-8)
val deviceName = String(value, Charsets.UTF_8)
updateDeviceNameUI(deviceName)
}
UUID_BATTERY_LEVEL -> {
// 无符号 uint8
val battery = value[0].toInt() and 0xFF
updateBatteryUI(battery)
}
UUID_TEMPERATURE -> {
// 小端 int16 再转 float (例如 0.1°C 单位)
if (value.size >= 2) {
val raw = (value[1].toInt() shl 8) or (value[0].toInt() and 0xFF)
val temperature = raw.toFloat() / 10.0f
updateTemperatureUI(temperature)
}
}
else -> {
// 默认将 byte[] 按十六进制显示
val hexStr = value.joinToString("") { "%02X".format(it) }
log("Raw data: $hexStr")
}
}
} else {
handleReadError(status, characteristic.uuid)
}
}
提示:Android 8.0+ 中
characteristic.value 在回调内直接可用,无需额外读取;但若在回调外使用需注意线程安全。
4.5 常见数据类型的 byte[] 转换方法
| 目标类型 | 转换方法(Kotlin) |
|---|---|
| uint8 (无符号8位) | value[0].toInt() and 0xFF |
| uint16 (小端) | (value[1].toInt() shl 8) or (value[0].toInt() and 0xFF) |
| int32 (小端) | ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).int |
| float (IEEE 754 小端) | ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN).float |
| UTF-8 字符串 | String(value, Charsets.UTF_8) |
| 十六进制字符串 | value.joinToString("") { "%02X".format(it) } |
注意:特征声明可能采用大端序。务必阅读 BLE 设备的数据规范文档,或从 GATT 服务的 Descriptor(如 Characteristic Presentation Format)推断字节序和对齐方式。
4.6 错误码判断
Android BluetoothGatt 中 status 的常用常量及含义:
| 错误码(int) | 常量名 | 含义 |
|---|---|---|
| 0 | GATT_SUCCESS | 操作成功 |
| 2 | GATT_READ_NOT_PERMITTED | 特征不具可读权限 |
| 5 | GATT_INSUFFICIENT_AUTHENTICATION | 需要配对/认证 |
| 8 | GATT_INSUFFICIENT_ENCRYPTION | 需要加密链路 |
| 15 | GATT_CONNECTION_TIMEOUT | 连接超时 |
| 133 | GATT_ERROR | 通用内部错误 |
建议对非 GATT_SUCCESS 状态进行分级处理:
- 临时错误(如超时、忙碌):延迟重试,最多 3 次。
- 权限错误(如未认证):发起配对流程或提示用户。
- 致命错误(如设备断开):关闭 GATT 连接并重新扫描。
4.7 调试与日志输出
调试时建议打印完整的 byte[] 内容及状态:
fun logReadResult(uuid: UUID, value: ByteArray?, status: Int) {
val hex = value?.joinToString("") { "%02X".format(it) } ?: "null"
Log.d("BLE_GATT", "Read [$uuid] status=$status value=$hex")
}
可通过 nRF Connect 或 LightBlue 工具对比验证原始数据是否正确。
4.8 性能与稳定性优化
- 批量读取:如果同一 service 下有多个特征需要读取,使用
onServicesDiscovered()后依次调用readCharacteristic(),避免并发读取。 - 缓存策略:若特征值变化频率低(如设备名称、序列号),在本地缓存一次读取结果,后续不再重复读取。
- 线程安全:回调在 Binder 线程,解析后的数据需通过 Handler 或 LiveData 切换到 UI 线程更新。
- 超时处理:为每次读取请求设置 5 秒超时定时器,若未收到回调则触发重试。
4.9 常见错误与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 读取返回 byte[] 全为零 | 设备未准备好、特征未被通知使能、读取时机过早 | 等待设备就绪信号或先写配置描述符使能通知 |
| 转换后数值异常 | 字节序错误或解析类型与声明不符 | 核对特征数据格式文档,使用 ByteBuffer 显式指定字节序 |
| 始终收到 GATT_READ_NOT_PERMITTED | 特征属性的“读”位未置位 | 检查设备固件或更换特征;若无读取权限可改用通知接收 |
| 回调从未触发 | 未正确注册 BluetoothGattCallback 或 GATT 实例被释放 | 确保 gatt 对象有效且 callback 非匿名内部类(防止 GC) |
4.10 部署注意事项
- 生产代码中不要直接使用
value.size做假设,应检查长度后再解析,防止数组越界。 - 对于敏感数据(如健康测量值),建议在解析后做范围校验(0~255 等),防止 UI 显示异常值。
- 若特征值超过 MTU 限制导致分段读取(目前 BLE 4.2+ 支持长读取),需处理多次回调并拼接数据。