uniapp BLE 开发踩坑:小米手机数据接收截断问题排查全记录

一、问题背景

项目基于 uniapp(APP 端) ​ 做跨平台蓝牙通信,设备端是串口透传 BLE 模块,业务层自定义二进制协议。

正常流程:

  • 手机连接设备 → 开启 notify → 设备主动推数据
  • 前端解析协议帧(帧头 AA AA F3 01 + 业务数据)

测试阶段发现一个诡异现象:

机型 表现
vivo 数据完整,版本解析
小米(HyperOS/MIUI) 超时5秒收不到23字节的查询信息

蓝牙设备端用抓包工具确认:数据确实发出去了,不是硬件没发。

二、排查过程

1. 先排除代码层问题

  • 监听只注册一次 ✓
  • notify 订阅在发现服务之后 ✓
  • 没并发调用 write/notify ✓
  • 用 nRF Connect 连同一设备验证

2. nRF Connect 对比验证(关键一步)

不手动改 MTU 时:

  • 小米默认 MTU = 23
  • 单包 notify 有效载荷 = 23 - 3(ATT 头)= 20 字节

设备发 23 字节:

  • ATT 层不自动分片
  • 超出的 3 字节被协议栈直接丢弃

vivo:

  • 无修改vivo厂商默认的mtu会更大一点

结论:不是 uniapp bug,是 BLE 默认 MTU 机制 + 透传模块不协商 MTU​ 的必然结果。


四、根因总结

1. BLE 协议层限制

  • 默认 ATT_MTU = 23
  • ATT 头占 3 字节(Opcode 1 + Handle 2)
  • 应用层单包最多 20 字节

而我跟主板通讯建立私有协议,23 字节是"应用帧总长",不是默认的20字节,因为透传模式就是不分割协议只传数据的。

2. 透传模式放大问题

透传模块:

  • 不解析协议
  • 不主动发 MTU 协商
  • MCU 给多少就一次 notify 丢出去

于是:

  • 通道够宽(vivo)→ 正常
  • 通道默认 20(小米)→ 静默截断,但是奇怪的是不影响发送,只影响接受,这里涉及到安卓底层与手机厂商以及uniapp三方的交互协议

3. 小米 /红米问题

  • vivo:部分 ROM 自动协商
  • 小米:默认不协商,setBLEMTU 返回 success 也不一定真生效

五、解决方案(生产可用)

方案核心原则

不信任默认 MTU,不信任协商返回值,永远按"可能只有 20 字节"做兜底。


1. 连接时序严格卡顺序

复制代码
createBLEConnection
 → onBLEConnectionStateChange(connected=true)
 → getBLEDeviceServices
 → getBLEDeviceCharacteristics
 → setBLEMTU(延迟 500~800ms 再调)
 → notifyBLECharacteristicValueChange(等 success)
 → 再发业务指令

小米上任何一步抢跑都会丢首包。


2. MTU 协商(测试后发现调整mtu值就可以但不能仅凭借uniappapi返回来确定,有时候设置不成功也没有反应,最终测试还是可以的,所以采用这种方法)

复制代码
setTimeout(() => {
  uni.setBLEMTU({
    deviceId,
    mtu: 247,
    success: () => console.log('MTU 协商请求成功'),
    fail: () => console.log('MTU 协商失败,走分包兜底')
  })
}, 600)

注意:

  • 不依赖 res 里的返回值
  • 真实可用 MTU 以"实际收到的字节数"为准

3. 发送端:按 MTU-3 串行分包

复制代码
function writeQueue(buffer, mtu = 23) {
  const chunkSize = Math.max(20, mtu - 3)
  let pos = 0

  return new Promise((resolve, reject) => {
    const next = () => {
      if (pos >= buffer.byteLength) return resolve()
      const slice = buffer.slice(pos, pos + chunkSize)
      uni.writeBLECharacteristicValue({
        deviceId, serviceId, characteristicId,
        value: slice,
        success: () => {
          pos += slice.byteLength
          setTimeout(next, 20) // 小米缓冲小,必须节流
        },
        fail: reject
      })
    }
    next()
  })
}

4. 接收端:永远做缓冲区

复制代码
let cache = new Uint8Array(0)

uni.onBLECharacteristicValueChange(res => {
  const chunk = new Uint8Array(res.value)
  const merged = new Uint8Array(cache.length + chunk.length)
  merged.set(cache)
  merged.set(chunk, cache.length)
  cache = merged

  // 按协议帧头 + 长度字段判断是否收齐
  const frame = parseFrame(cache)
  if (frame.complete) {
    handleBusiness(frame.data)
    cache = new Uint8Array(0)
  }
})

协议设计建议:

  • 帧头(2~4 字节)
  • 长度字段(1~2 字节)
  • 序号/分包标志
  • CRC 校验

六、避坑清单(小米专属)

  • 不假设系统自动协商 MTU
  • setBLEMTU 不在连接成功瞬间调,延 500ms+
  • notify 订阅必须等 success 再发指令
  • 接收端必须有等待缓冲区,不信任"一次回调=完整帧"
  • 用 nRF Connect 先验证,再写业务代码

七、复盘结论

这次问题表面是"小米收不到数据",本质是:

透传模式 + 默认 MTU 20 + 不协商 + 长帧直发 = 静默截断

vivo/只是被系统"偷偷兜底"了,不代表协议设计是对的。

真正稳的 BLE 通信,应该:

  • 手机端:协商 MTU + 串行分包 + 接收重组
  • 设备端:感知 MTU + 主动分包 + 帧结构自描述
相关推荐
阿俊-全栈开发2 小时前
LikeShop 单商户标准版 UniApp 端二开:页面组件复用与自定义主题适配
uni-app·likeshop开源商城·likeshop单商户标准版·多商户源码
换元不配限5 小时前
Android 休眠与唤醒完全指南:从手机到 IoT 设备的实战解析
android·物联网·智能手机·alarmmanager·wakelock·workmanager·休眠和唤醒
半生过往8 小时前
uni-app多端项目工作笔记
笔记·uni-app
wulechun8 小时前
基于深度学习的中文自然语言处理工具Jiagu:从原理到实战的开发者指南
智能手机
薛先生_0998 小时前
手机远程控制电脑哪个好用?2026安卓鸿蒙iOS手机远程控制电脑软件横测排名
智能手机·电脑
Henry-SAP9 小时前
阶跃星辰发布智能体手机STEPX Neo
人工智能·智能手机
yikegis9 小时前
手机拍视频太多,内存不够用了怎么办?(怎么压缩视频)
智能手机·音视频
00后程序员张9 小时前
无需 Mac 的 iOS App 上架工具,开心上架Appuploader 负责哪些环节
android·ios·小程序·https·uni-app·iphone·webview
Light609 小时前
豆包收回了那只“手“,Agent手机开始拼协议
人工智能·智能手机·mcp协议·权限边界·agent手机·appfunctions·工具投毒