微信小程式 BLE 基礎業務介面封裝

莱布尼茨發表於2024-08-28

寫在前面:本文所述未必符合當前最新情形(包括藍芽技術發展、微信小程式介面迭代等)。

微信小程式為藍芽操作提供了很多介面,但在實際開發過程中,會發現隱藏了不少坑。目前主流藍芽應用都是基於低功耗藍芽(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 引數,此處不贅述。

相關文章