一渊之隔头像
uniapp BLE 开发踩坑:小米手机数据接收截断问题排查全记录封面图

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 + 主动分包 + 帧结构自描述

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_37263478/article/details/168865794

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--