寫在前面:本文所述未必符合當前最新情形(包括藍牙技術發展、微信小程式介面迭代等)。 微信小程式為藍牙操作提供了很多介面,但在實際開發過程中,會發現隱藏了不少坑。目前主流藍牙應用都是基於低功耗藍牙(BLE)的,本文介紹相關的幾個基礎介面,並對其進行封裝,便於業務層調用。 藍牙發展 在開發藍牙應用程式之 ...
寫在前面:本文所述未必符合當前最新情形(包括藍牙技術發展、微信小程式介面迭代等)。
微信小程式為藍牙操作提供了很多介面,但在實際開發過程中,會發現隱藏了不少坑。目前主流藍牙應用都是基於低功耗藍牙(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 參數,此處不贅述。