一、问题背景
项目基于 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 + 主动分包 + 帧结构自描述