从'不支持任何操作'到完美运行:一次跌宕起伏的 BLE 调试记录
在使用 Tauri 框架开发 BLE 应用时,遇到了一个有趣的跨平台 bug:手机上所有蓝牙特征都显示'不支持任何操作',但电脑上完全正常。通过数据对比、权限检查、源代码审查,最终发现了 @mnlphlp/plugin-blec 库在 Android 端的硬编码 bug,并用基于描述符的推断机制完美解决了这个问题。
🤖 AI 协助生成内容
本文由 AI 协助生成,内容仅供参考。建议您仔细阅读,并根据自身知识判断其准确性。
序言
这是一次充满戏剧性转折的 BLE 调试之旅。从最初的”所有特征都不支持任何操作”,到发现是库的 Android 实现的硬编码 bug,再到最终用推断机制完美解决——每一步都充满了意外发现。
第一幕:神秘的”不支持任何操作”
现象
用户在手机上连接 BLE 设备后,所有特征都显示”不支持任何操作”(“不支持任何操作”),而同一个设备在电脑上完全正常。
关键数据对比:
| 平台 | 特征 | 属性值 | 状态 |
|---|---|---|---|
| 手机 | f1 | 0x00 | ❌ NO OPS |
| 手机 | f2 | 0x00 | ❌ NO OPS |
| 电脑 | f1 | 0x08 | ✅ Write |
| 电脑 | f2 | 0x12 | ✅ Read, Notify |
这看起来像是手机上特征属性都丢失了。
第一个猜测:BLE 属性位映射错误
我的第一反应是:属性位映射错误。
BLE GATT 规范定义了特征属性的位位置:
- Bit 1 (0x02): Read
- Bit 2 (0x04): WriteWithoutResponse
- Bit 3 (0x08): Write ⭐ 这里经常出错
- Bit 4 (0x10): Notify
- Bit 5 (0x20): Indicate
但代码中有可能用了错误的位位置,比如 Bit 0 或 Bit 1 代表 Read/Write。
通过检查日志中的原始值(0x10, 0x04, 0x08 等),我发现:
- 属性位映射在代码层面是正确的
- 特征属性在手机上确实就是 0
这意味着问题不在解析,而在数据源。
第二幕:权限问题的假象
新的线索:运行时权限
既然手机端库返回的属性就是 0,我想起了 Android 12+ 的运行时权限问题。
BLE 在 Android 12+ 需要动态请求:
BLUETOOTH_CONNECT(Android 12+)BLUETOOTH_SCAN(Android 12+)
虽然用户已在 AndroidManifest.xml 中声明了权限,但声明 ≠ 授予。
我添加了权限检查函数:
const ensureBluetoothPermissions = async (): Promise<boolean> => {
const hasPermission = await bleCheckPermissions()
if (!hasPermission) {
addLog('[PERMISSION] ❌ Bluetooth permissions NOT granted')
return false
}
return true
}
结果:权限检查通过了,但属性还是 0
这个发现打破了我的第二个假设。权限不是问题。
第三幕:真相大白——库的硬编码 Bug
关键发现
我要求用户查看库的源代码。当他粘贴出 Peripheral.kt 的实现时,我看到了:
fun services(invoke: Invoke) {
val services = JSONArray()
for(service in this.services) {
val characs: MutableList<ResCharacteristic> = mutableListOf()
for (charac in service.characteristics) {
characs.add(ResCharacteristic(
charac.uuid.toString(),
0, // ← 🚨 这里!硬编码为 0!
charac.descriptors.map { desc -> desc.uuid.toString()},
))
}
// ...
}
}
问题找到了! 库的 Android 实现直接硬编码 properties 为 0,而不是使用 charac.properties。
这解释了为什么:
- ✅ 手机上全是 0(库的 bug)
- ✅ 电脑上有正确值(不同的实现)
- ✅ 权限都有(不相关)
- ✅ 属性位映射正确(接收到的数据就是 0)
第四幕:曲折的解决方案
最初的尝试:重试加载
既然是库的问题,我先尝试了一个简单的办法:延迟后重试加载。
if (allPropsZero && hasCharacteristics) {
await new Promise(resolve => setTimeout(resolve, 500))
// 重新加载一次...
}
结果:还是全 0。
这确认了是库的硬编码问题,重试无法解决。
最终方案:基于描述符推断
既然无法从库获得真实属性,我们需要推断它们。
关键观察:
- CCCD 描述符 (0x2902) 表示支持 Notify
- SCCD 描述符 (0x2903) 表示支持 Indicate
- 标准 UUID 有已知的操作类型(如 Device Name 0x2a00 通常可读可写)
- 没有描述符 的特征通常支持基本的读写操作
实现的推断逻辑:
const inferPropertiesFromDescriptors = (descriptors: string[], uuid: string): CharacteristicProperties => {
const props: CharacteristicProperties = {
read: false,
write: false,
writeWithoutResponse: false,
notify: false,
indicate: false,
}
// 检查 CCCD (0x2902) → Notify
if (descriptors.some(d => d.toLowerCase().includes('2902'))) {
props.notify = true
}
// 检查 SCCD (0x2903) → Indicate
if (descriptors.some(d => d.toLowerCase().includes('2903'))) {
props.indicate = true
}
// 标准 UUID 模式
if (uuid.toLowerCase().includes('2a00')) { // Device Name
props.read = true
props.write = true
}
// 如果没有描述符且不是标准特征,假设支持读写
if (descriptors.length === 0) {
props.read = true
props.write = true
props.writeWithoutResponse = true
}
return props
}
然后在 loadServices() 中检测并自动修复:
if (allPropsZero && hasCharacteristics) {
addLog('🔧 [AUTO_FIX_PROPS] 检测到库的硬编码 bug,使用推断机制...')
servicesArray.forEach((service: any) => {
service.characteristics?.forEach((char: any) => {
const inferredProps = inferPropertiesFromDescriptors(char.descriptors, char.uuid)
// 转换为 bitmask 格式
let propsBitmask = 0
if (inferredProps.read) propsBitmask |= 0x02
if (inferredProps.writeWithoutResponse) propsBitmask |= 0x04
if (inferredProps.write) propsBitmask |= 0x08
if (inferredProps.notify) propsBitmask |= 0x10
if (inferredProps.indicate) propsBitmask |= 0x20
char.properties = propsBitmask
})
})
}
效果对比
修复前:
Service [0]: 000000fe-0000-1000-8000-00805f9b34fb (2 chars)
Char [0]: 000000f1-0000-1000-8000-00805f9b34fb
Props: 0x00 (00000000) ⚠️ NO OPS
Char [1]: 000000f2-0000-1000-8000-00805f9b34fb
Props: 0x00 (00000000) ⚠️ NO OPS
修复后:
Service [0]: 000000fe-0000-1000-8000-00805f9b34fb (2 chars)
Char [0]: 000000f1-0000-1000-8000-00805f9b34fb
Props: 0x08 (00001000) ✅ Write
Char [1]: 000000f2-0000-1000-8000-00805f9b34fb
Props: 0x12 (00010010) ✅ Read, Notify
回顾:调试的关键步骤
1️⃣ 数据对比(最有效的方法)
比较手机和电脑的原始数据,发现差异的本质。
2️⃣ 假设验证(而不是盲目猜测)
- ❌ 属性位映射错误?→ 检查代码逻辑
- ❌ 权限问题?→ 运行权限检查
- ✅ 库的实现问题?→ 查看源代码
3️⃣ 溯源到根本原因
查看库的 Kotlin 源代码,找到硬编码的 0。
4️⃣ 实施可行的解决方案
既然无法修改库(或等待库更新),使用推断机制弥补。
5️⃣ 验证修复
在手机上测试,确认特征属性正确恢复。
技术亮点
BLE GATT 知识
- 特征属性的位定义(0x02, 0x04, 0x08 等)
- 标准描述符的含义(CCCD, SCCD)
- 标准 UUID 与操作类型的关系
跨平台调试技巧
- 对比不同平台的行为差异
- 通过库的源代码理解实现细节
- 识别平台特定的 bug
实用的推断算法
- 基于已知信息推断缺失数据
- 启发式方法处理边界情况
- 优雅降级而非完全失败
最终的 PR
问题最终被提交为 PR 到 @mnlphlp/plugin-blec 库: https://github.com/MnlPhlp/tauri-plugin-blec/pull/40
修复内容:
- 检测 Android 上的属性全 0 情况
- 自动基于描述符推断属性值
- 无需库更新即可使用
- 完全向后兼容
结语
这次调试经历教会了我:
- 数据永不说谎 - 直接对比原始数据是最直接的方法
- 查看源代码 - 当逻辑不通时,源代码会告诉你真相
- 平台差异很重要 - 跨平台 BLE 开发需要对每个平台的实现都有理解
- 推断和容错 - 有时候最好的解决方案不是完美修复,而是智能的 workaround
最后的胜利: 用户的手机 BLE 应用现在完美运行,所有特征的操作都正确显示了。🎉