寫在前面:本文所述未必符合當前最新情形(包括藍芽技術發展、微信小程式介面迭代等)。
微信小程式為藍芽操作提供了很多介面,但在實際開發過程中,會發現隱藏了不少坑。目前主流藍芽應用都是基於低功耗藍芽(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 平臺。
關鍵介面
使用藍芽傳輸資料都會涉及以下步驟及介面:
- 啟用裝置藍芽(如在手機上點按藍芽圖示);
wx.openBluetoothAdapter
:初始化小程式藍芽模組;- 搜尋外圍裝置
wx.onBluetoothDeviceFound
:監聽搜尋到新裝置的事件;wx.startBluetoothDevicesDiscovery
:開始搜尋附近裝置;wx.stopBluetoothDevicesDiscovery
:找到待連的對手裝置後停止搜尋;
wx.createBLEConnection
:連線 BLE 裝置;- 接收資料
wx.notifyBLECharacteristicValueChange
:為下一步驟做鋪墊(注意:必須對手裝置的特徵支援 notify 或者 indicate 才可以成功呼叫);wx.onBLECharacteristicValueChange
:監聽對手裝置特徵值變化事件,可以獲得變化後的特徵 value,如此資料就從對手裝置傳遞過來了;
wx.writeBLECharacteristicValue
:向對手裝置特徵值中寫入二進位制資料(注意:必須對手裝置的特徵支援 write 才可以成功呼叫);wx.closeBLEConnection
:斷開連線;wx.closeBluetoothAdapter
:關閉小程式藍芽模組;- 關閉裝置藍芽。
坑及注意點(僅限於筆者基於開發過程使用到的機型觀察記錄,未必有普遍性):
- 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 引數,此處不贅述。