写在前面:本文所述未必符合当前最新情形(包括蓝牙技术发展、微信小程序接口迭代等)。

微信小程序为蓝牙操作提供了很多接口,但在实际开发过程中,会发现隐藏了不少坑。目前主流蓝牙应用都是基于
低功耗蓝牙(BLE)
的,本文介绍相关的几个基础接口,并对其进行封装,便于业务层调用。

蓝牙发展

在开发蓝牙应用程序之前,有必要对蓝牙这项技术做大致了解。

经典蓝牙

一种短距离无线通信标准,运行在 2.4GHz 频段,主要用于两个设备之间的数据传输。

一般将蓝牙 4.0 之前的版本称为
经典蓝牙
,其传输速率在 1-3Mbps 之间。虽然有着不错的传输速率,但由于功耗较大,难以满足移动终端和物联网的需求,逐渐被更先进的版本所取代。‌

低功耗蓝牙(BLE)

蓝牙 4.0‌ 引入了
低功耗蓝牙(BLE)
技术,其最大数据吞吐量仅为1Mbps,但相对经典蓝牙,BLE 拥有超低的运行功耗和待机功耗。

BLE 的低功耗是如何做到的呢?主要是缩减广播通道数量(由经典蓝牙的 16-32个,缩减为 3 个)、缩短广播射频开启时间(由经典蓝牙的 22.5ms,减少到 0.6-1.2ms)、深度睡眠模式及针对低功耗场景优化了协议栈等,此处不赘述。

当前最新版本

‌当前大版本是蓝牙 5.0,传输速度达到了 24Mbps,是 4.2 版本的两倍,有效工作距离可达 300 米,是 4.2 版本的四倍。低功耗模式下的传输速度上限为 2Mbps,适合于影音级应用,如高清晰度音频解码协议的应用。

蓝牙特征值

GATT(Generic Attribute Profile)协议
定义了蓝牙设备之间的通信方式,其中单个
服务(Service)
可以包含多个
特征值(Characteristic)
,每个服务和特征值都有特定的‌ UUID 来唯一标识。特征值是蓝牙设备中用于存储和传输数据的基本单元,每个特征值都有其特定的
属性和值

属性协议(ATT)
定义数据的检索,允许设备暴露数据给其他设备,这些数据被称为
属性(attribute)

通过属性可以设置特征值操作类型,如
读取、写入、通知
等,操作对象即为特征值的
值(value)
。一个特征值可以同时拥有多种操作类型。

为了实现数据的传输,服务需要暴露两个主要的特征值:
write
和‌
notify 或 indication
。write 特征值用于接收数据,而 notify 特征值用于发送数据。这些特征值类型为 bytes,并且一次传输的数据长度可以根据不同的特征值类型有所不同。

小程序接口封装

需要知道的是,虽然蓝牙是开放协议,但由于苹果 IOS 系统的封闭设计,目前苹果设备无法与 Android 及其它平台设备通过蓝牙相连。

本文描述皆基于 Android 平台。

关键接口

使用蓝牙传输数据都会涉及以下步骤及接口:

  1. 激活设备蓝牙(如在手机上点按蓝牙图标);
  2. wx.openBluetoothAdapter
    :初始化小程序蓝牙模块;
  3. 搜索外围设备
    1. wx.onBluetoothDeviceFound
      :监听搜索到新设备的事件;
    2. wx.startBluetoothDevicesDiscovery
      :开始搜索附近设备;
    3. wx.stopBluetoothDevicesDiscovery
      :找到待连的对手设备后停止搜索;
  4. wx.createBLEConnection
    :连接 BLE 设备;
  5. 接收数据
    1. wx.notifyBLECharacteristicValueChange
      :为下一步骤做铺垫(注意:必须对手设备的特征支持 notify 或者 indicate 才可以成功调用);
    2. wx.onBLECharacteristicValueChange
      :监听对手设备特征值变化事件,可以获得变化后的特征 value,如此数据就从对手设备传递过来了;
  6. wx.writeBLECharacteristicValue
    :向对手设备特征值中写入二进制数据(注意:必须对手设备的特征支持 write 才可以成功调用);
  7. wx.closeBLEConnection
    :断开连接;
  8. wx.closeBluetoothAdapter
    :关闭小程序蓝牙模块;
  9. 关闭设备蓝牙。

坑及注意点(仅限于笔者基于开发过程使用到的机型观察记录,未必有普遍性):

  • wx.onBluetoothDeviceFound 这个方法只能找到新的蓝牙设备,之前搜索过的在部分安卓机型上,不算做新的蓝牙设备,因此重新搜索不到。这种情况,要么重启小程序蓝牙模块或者重启小程序,或者使用
    wx.getBluetoothDevices
    获取在蓝牙模块生效期间所有搜索到的蓝牙设备。
  • 连接未必能一次成功,需要多连几次。
  • 每次连接最好能重启 BluetoothAdapter,否则在后续 wx.notifyBLECharacteristicValueChange 时容易报 10005-没有找到指定特征 错误。
  • 若小程序在之前已有搜索过某个蓝牙设备,并成功建立连接,可直接传入之前搜索获取的 deviceId 直接尝试连接该设备,无需再次进行搜索操作。
  • 系统与蓝牙设备会限制蓝牙 4.0 单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过 20 字节。
  • 一旦过程中出现任何异常,就必须断开连接重连,否则后续会一直报 notifyblecharacteristicValuechange:fail: no characteristic 错误

主要代码

注:本文代码块为笔者临时盲敲,仅作参考。

定义一个工具对象

const ble = {}

由于可能会遇到的各类问题,我们先全局定义运行时异常枚举和 throw/handle 方法,免得后面遇到异常处理各写各的。

const ble = {
  errors: {
    OPEN_ADAPTER: '开启蓝牙模块异常',
    CLOSE_ADAPTER: '关闭蓝牙模块异常',
    CONNECT: '蓝牙连接异常',
    NOTIFY_CHARACTERISTIC_VALUE_CHANGE: '注册特征值变化异常',
    WRITE: '发送数据异常',
    DISCONNECT: '断开蓝牙连接异常',
    //...
  },

  _throwError(title, err) {
    //... 可以考虑在这里调用 wx.closeBLEConnection

    if (err) {
      err.title = title
      throw err
    }
    throw new Error(title)
  },  

蓝牙连接。注意到这是个有限递归方法,且每次连接都先重启 BluetoothAdapter,原因请看上节。

/** 
   * @param {string} deviceId 设备号
   * @param {int} tryCount 已尝试次数
   */
  async connectBLE(deviceId, tryCount = 5) {
    await wx.closeBluetoothAdapter().catch(err => { ble._throwError(this.errors.CLOSE_ADAPTER, err) })
    await wx.openBluetoothAdapter().catch(err => { ble._throwError(this.errors.OPEN_ADAPTER, err) })
    await wx.createBLEConnection({
      deviceId: deviceId,
      timeout: 5000
    })
      .catch(async err => {
        if (err.errCode === -1) { //蓝牙已是连接状态
          // continue work
        } else {
          console.log(`第${6 - tryCount}次蓝牙连接出错`, err.errCode, err.errMsg)
          tryCount--
          if (tryCount === 0) {
            ble._throwError(this.errors.CONNECT, err)
          } else {
            await ble.connectBLE(deviceId, tryCount)
          }
        }
      })
      //蓝牙连接成功
  },

连接成功后,可能需要监听对手设备,用于接收其传过来的数据。

  async onDataReceive(deviceId, serviceId, characteristicId, callback) {
    await wx.notifyBLECharacteristicValueChange({
      deviceId: deviceId,
      serviceId: serviceId,
      characteristicId: characteristicId,
      state: true
    }).catch(err => { ble._throwError(this.errors.NOTIFY_CHARACTERISTIC_VALUE_CHANGE, err) })

    wx.onBLECharacteristicValueChange(res => {
      let data = new Uint8Array(res.value)
      callback(data)
    })
  },

发送数据,须切片,每次发送不多于 20字节。此处增加了在固定时长内的重试机制。

  /** 
   * @param {Uint8ClampedArray} data 待发送数据
   * @param {boolean} holdConnWhenDone 发送完毕后是否保持连接
   */
  async send(deviceId, serviceId, characteristicId, data, holdConnWhenDone = false) {
    let idx = 0 //已传输字节数
    let startTime = Date.now(),
      duration = 800 //发送失败重试持续时间  
    while (idx < data.byteLength) {
      await wx.writeBLECharacteristicValue({
        deviceId: deviceId,
        serviceId: serviceId,
        characteristicId: characteristicId,
        value: data.slice(idx, idx += 20).buffer
      })
        .then(_ => startTime = Date.now()) //成功则now重置
        .catch(err => {
          if (Date.now() - startTime >= duration) {
            ble._throwError(this.errors.WRITE, err)
          } else {
            //重试
            idx -= 20
          }
        })
    }
    if (!holdConnWhenDone)
      await wx.closeBLEConnection({ deviceId: deviceId }).catch(err => { ble._throwError(this.errors.DISCONNECT, err) })
  }

在实际项目中,可能需要在每次发送数据片之后得到对手设备响应后,根据响应决定重发(校验错误或响应超时等)、中止(设备繁忙)、还是接着发送下一个数据片。这种情况则需配合 onDataReceive 方法协同工作,向其传入合适的 callback 参数,此处不赘述。

标签: none

添加新评论