📚 调试日记 系列
技术 蓝牙开发 跨平台开发

从'不支持任何操作'到完美运行:一次跌宕起伏的 BLE 调试记录

在使用 Tauri 框架开发 BLE 应用时,遇到了一个有趣的跨平台 bug:手机上所有蓝牙特征都显示'不支持任何操作',但电脑上完全正常。通过数据对比、权限检查、源代码审查,最终发现了 @mnlphlp/plugin-blec 库在 Android 端的硬编码 bug,并用基于描述符的推断机制完美解决了这个问题。

🤖 AI 协助生成内容

本文由 AI 协助生成,内容仅供参考。建议您仔细阅读,并根据自身知识判断其准确性。

Hako Chest
更新于
4,874 字
8 min read
从'不支持任何操作'到完美运行:一次跌宕起伏的 BLE 调试记录

序言

这是一次充满戏剧性转折的 BLE 调试之旅。从最初的”所有特征都不支持任何操作”,到发现是库的 Android 实现的硬编码 bug,再到最终用推断机制完美解决——每一步都充满了意外发现。

第一幕:神秘的”不支持任何操作”

现象

用户在手机上连接 BLE 设备后,所有特征都显示”不支持任何操作”(“不支持任何操作”),而同一个设备在电脑上完全正常

关键数据对比:

平台特征属性值状态
手机f10x00❌ NO OPS
手机f20x00❌ NO OPS
电脑f10x08✅ Write
电脑f20x12✅ 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

这确认了是库的硬编码问题,重试无法解决。

最终方案:基于描述符推断

既然无法从库获得真实属性,我们需要推断它们。

关键观察:

  1. CCCD 描述符 (0x2902) 表示支持 Notify
  2. SCCD 描述符 (0x2903) 表示支持 Indicate
  3. 标准 UUID 有已知的操作类型(如 Device Name 0x2a00 通常可读可写)
  4. 没有描述符 的特征通常支持基本的读写操作

实现的推断逻辑:

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

修复内容:

  1. 检测 Android 上的属性全 0 情况
  2. 自动基于描述符推断属性值
  3. 无需库更新即可使用
  4. 完全向后兼容

结语

这次调试经历教会了我:

  1. 数据永不说谎 - 直接对比原始数据是最直接的方法
  2. 查看源代码 - 当逻辑不通时,源代码会告诉你真相
  3. 平台差异很重要 - 跨平台 BLE 开发需要对每个平台的实现都有理解
  4. 推断和容错 - 有时候最好的解决方案不是完美修复,而是智能的 workaround

最后的胜利: 用户的手机 BLE 应用现在完美运行,所有特征的操作都正确显示了。🎉

🏷️ 标签

#BLE #GATT #Android #Tauri #调试 #跨平台 #TypeScript #Kotlin