uniapp蓝牙业务指令 + 心跳包并发写入导致的粘包、乱序、通信异常。

8 阅读3分钟

1.需求场景

在开发蓝牙app的时候,通常是用户点击对于页面才会发送业务指令获取信息,如果页面的数据不需要实时更新,只用发送一次业务指令,或者进入的页面不需要发送业务指令,这个时候硬件要求,必须要每隔30秒就要发送一次数据通讯,如果不通讯,我这边会复位一次,【硬件逻辑:如果 30s 内没有收到任何有效数据 / 指令,就认为:主机(手机 APP)掉线了、通信挂了、程序跑飞了】 也就是会自动断开蓝牙连接,这个时候就需要一个心跳包定时30秒发送,但会出现一个问题,如果正好30秒,心跳包开始发送,但用户也是正好在切换页面或者设置的时候,也在发送业务指令,也就是同一个秒正好发送心跳包和业务指令,这个时候会导致出现多线程 / 并发向同一个 BLE 特征值写入数据, 会出现粘包、乱序、丢包、通信异常。

2.原因分析

BLE GATT 特征值写入有一个铁律:

同一时间,只能有一个写入请求在执行,必须等上一个写入完成(回调成功 / 失败),才能发起下一个。

这是 BLE 协议栈硬件 + 固件 + 系统层的限制,不是你代码写得好不好的问题。

当你同时做两件事:

  1. 发送业务指令(比如控制指令、数据帧)
  2. 发送心跳包(定时自动发)

两个写入操作并发抢占同一个特征值,就会触发:

  • 粘包:两个数据包被底层拼在一起发送
  • 乱序:心跳包插到业务指令中间
  • 写冲突:系统直接返回 busy /failed
  • 断开连接:协议栈崩溃,蓝牙直接断连

这不是 bug,是 BLE 的工作机制。

3.解决方案

串行写入队列 + 写入完成回调触发下一个

简单说:

  • 所有要发的包(业务指令、心跳包)不直接发
  • 全部丢进一个线程安全队列
  • 队列严格按顺序一个一个发
  • 必须等上一个写入成功 / 失败回调回来,再发下一个
<template>
  <view>
    <button @click="sendBusinessCmd">发送业务指令</button>
    <button @click="startHeartBeat">启动心跳</button>
    <button @click="stopHeartBeat">停止心跳</button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      // BLE 必要参数(你自己的设备)
      deviceId: '你的设备ID',
      serviceId: '你的服务UUID',
      characteristicId: '你的特征值UUID',

      // 发送队列(核心!线程安全数组)
      sendQueue: [],
      // 是否正在写入(防止并发)
      isWriting: false,
      // 心跳定时器
      heartTimer: null,
    };
  },

  methods: {
    // ======================================
    // 【全局唯一入口】所有发送都走这里
    // 业务指令、心跳包 全部入队
    // ======================================
    addToSendQueue(bufferData) {
      // 推入队列
      this.sendQueue.push(bufferData);
      // 如果当前空闲,立即执行发送
      if (!this.isWriting) {
        this.processQueue();
      }
    },

    // ======================================
    // 【串行发送核心】严格一个发完再发下一个
    // ======================================
    processQueue() {
      // 队列空了,结束
      if (this.sendQueue.length === 0) {
        this.isWriting = false;
        return;
      }

      this.isWriting = true;
      // 取出队首数据
      const data = this.sendQueue.shift();

      // uni-app BLE 唯一写入点
      uni.writeBLECharacteristicValue({
        deviceId: this.deviceId,
        serviceId: this.serviceId,
        characteristicId: this.characteristicId,
        value: data,
        success: () => {
          // 上一包发送成功 → 立刻发送下一包
          this.processQueue();
        },
        fail: (err) => {
          console.error('写入失败', err);
          // 失败也要继续发下一包,不能卡死队列
          this.processQueue();
        },
      });
    },

    // ======================================
    // 业务指令发送(示例)
    // ======================================
    sendBusinessCmd() {
      // 1. 构造你的业务指令 ArrayBuffer
      const cmd = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
      // 2. 丢进队列(不直接写!)
      this.addToSendQueue(cmd.buffer);
    },

    // ======================================
    // 心跳包发送(必须进队列!)
    // ======================================
    sendHeartBeat() {
      const heart = new Uint8Array([0xAA, 0xBB, 0xCC]);
      this.addToSendQueue(heart.buffer);
    },

    // ======================================
    // 启动心跳定时器
    // ======================================
    startHeartBeat() {
      if (this.heartTimer) return;
      this.heartTimer = setInterval(() => {
        this.sendHeartBeat(); // 心跳 → 入队
      }, 1000); // 1秒一次心跳
    },

    // ======================================
    // 停止心跳
    // ======================================
    stopHeartBeat() {
      clearInterval(this.heartTimer);
      this.heartTimer = null;
    },
  },

  // 页面销毁清空定时器
  beforeDestroy() {
    this.stopHeartBeat();
  },
};
</script>

4.总结

  • Vue2 + uni-app 中 BLE 写入不支持并发
  • 业务指令 + 心跳包必须共用同一个串行队列
  • 必须等写入回调完成才能发送下一包