實戰分享,教你藍牙在小程序中的應用
歡迎大家前往騰訊雲技術社區,獲取更多騰訊海量技術實踐幹貨哦~
作者:朱勝
導語
藍牙在日常生活中廣泛使用的一項技術,小程序給了我們前端工程師一個控制藍牙的方法,帶上你的設備,來看看怎麽控制你的藍牙設備吧。
1. 背景介紹
藍牙是愛立信公司創立的一種無線技術標準,為短距離的硬件設備提供低成本的通信規範。藍牙規範由藍牙技術聯盟(Bluetooth Special Interest Group,簡稱SIG)管理,在計算機,手機,傳真機,耳機,汽車,家用電器等等很多場景廣泛使用。藍牙具有以下一些特點:
(1) 免費使用:使用的工作頻段在2.4GHz的工科醫(ISM)頻段,無需申請許可證。
(2) 功耗低:BLE4.0包含了一個低功耗標準(Bluetooth Low Energy),可以讓藍牙的功耗顯著降低
(3) 安全性高:藍牙規範提供了一套安全加密機制和授權機制,可以有效防範數據被竊取
(4) 傳輸率高:目前最新BLE4.0版本,理論傳輸速率可達3Mbit/s(實際肯定達不到),理論覆蓋範圍可達100米。
2.小程序藍牙介紹
小程序API提供了一套藍牙操作接口,所以作為我們前端開發人員可以更加方便的進行藍牙設備開發,而無需了解安卓和IOS的各種藍牙底層概念。小程序的藍牙操作大多都是通過異步調用來處理的,這裏面就存在著一些坑,後面會詳細介紹。在使用小程序藍牙API之前有幾個概念或者說術語需要預先了解:
(1) 藍牙終端:我們常說的硬件設備,包括手機,電腦等等。
(2) UUID:是由子母和數字組成的40個字符串的序號,根據硬件設備有關聯的唯一ID。
(3) 設備地址:每個藍牙設備都有一個設備地址deviceId,但是安卓和IOS差別很大,安卓下設備地址就是mac地址,但是IOS無法獲取mac地址,所以設備地址是針對本機範圍有效的UUID,所以這裏需要註意,後面會介紹。
(4) 設備服務列表:每個設備都存在一些服務列表,可以跟不同的設備進行通信,服務有一個serviceId來維護,每個服務包含了一組特征值。
(5) 服務特征值:包含一個單獨的value值和0 –n個用來描述characteristic 值(value)的descriptors。一個characteristics可以被認為是一種類型的,類似於一個類。
(6) ArrayBuffer:小程序中對藍牙數據的傳遞是使用ArrayBuffer的二進制類型來的,所以在我們的使用過程中需要進行轉碼。
3. API總覽
小程序對藍牙設備的操作有18個API
API名稱 | 說明 |
---|---|
openBluetoothAdapter | 初始化藍牙適配器,在此可用判斷藍牙是否可用 |
closeBluetoothAdapter | 關閉藍牙連接,釋放資源 |
getBluetoothAdapterState | 獲取藍牙適配器狀態,如果藍牙未開或不可用,這裏可用檢測到 |
onBluetoothAdapterStateChange | 藍牙適配器狀態發生變化事件,這裏可用監控藍牙的關閉和打開動作 |
startBluetoothDevicesDiscovery | 開始搜索設備,藍牙初始化成功後就可以搜索設備 |
stopBluetoothDevicesDiscovery | 當找到目標設備以後需要停止搜索,因為搜索設備是比較消耗資源的操作 |
getBluetoothDevices | 獲取已經搜索到的設備列表 |
onBluetoothDeviceFound | 當搜索到一個設備時的事件,在此可用過濾目標設備 |
getConnectedBluetoothDevices | 獲取已連接的設備 |
createBLEConnection | 創建BLE連接 |
closeBLEConnection | 關閉BLE連接 |
getBLEDeviceServices | 獲取設備的服務列表,每個藍牙設備都有一些服務 |
getBLEDeviceCharacteristics | 獲取藍牙設備某個服務的特征值列表 |
readBLECharacteristicValue | 讀取低功耗藍牙設備的特征值的二進制數據值 |
writeBLECharacteristicValue | 向藍牙設備寫入數據 |
notifyBLECharacteristicValueChange | 開啟藍牙設備notify提醒功能,只有開啟這個功能才能接受到藍牙推送的數據 |
onBLEConnectionStateChange | 監聽藍牙設備錯誤事件,包括異常斷開等等 |
onBLECharacteristicValueChange | 監聽藍牙推送的數據,也就是notify數據 |
4. 主要流程
藍牙通信的一個正常流程是下面的圖示
(1) 開啟藍牙:調用openBluetoothAdapter來開啟和初始化藍牙,這個時候可以根據狀態判斷用戶設備是否支持藍牙
(2) 檢查藍牙狀態:調用getBluetoothAdapterState來檢查藍牙是否開啟,如果沒有開啟可以在這裏提醒用戶開啟藍牙,並且能在開啟後自動啟動下面的步驟
這裏有一個坑:IOS裏面藍牙狀態變化以後不能馬上開始搜索,否則會搜索不到設備,必須要等待2秒以上。
function connect(){ wx.openBluetoothAdapter({ success: function (res) { }, fail(res){ }, complete(res){ wx.onBluetoothAdapterStateChange(function(res) { if(res.available){ setTimeout(function(){ connect(); },2000); } }) //開始搜索 } }) }
(3) 搜索設備:startBluetoothDevicesDiscovery開始搜索設備,當發現一個設備會觸發onBluetoothDeviceFound事件,首先看下標準API
由於IOS無法獲取Mac地址所以這裏需要區分兩個場景
a) 安卓:安卓下可以根據Mac地址來搜索設備,或者跳過此步直接連接到設備。當搜索到一個設備以後,可以在onBluetoothDeviceFound事件回調中判斷當前設備的deviceID是否為指定的Mac地址
let mac = "XXXXXXXXXXXXXXX"; wx.startBluetoothDevicesDiscovery({ services:[], success(res) { wx.onBluetoothDeviceFound(res=>{ let devices = res.devices; for(let i = 0;i<devices.length;i++){ if(devices[i].deviceId = mac){ console.log("find"); wx.stopBluetoothDevicesDiscovery({ success:res=>console.log(res), fail:res=>console.log(res), }) } } }); }, fail(res){ console.log(res); } })
b) IOS:IOS下獲取設備Mac地址的方法已經被屏蔽,所以不存在mac地址,此時只能通過其他方式來判斷,比如在藍牙設備advertisData字段添加一些特別的信息來判斷等等,可以轉字符串來判斷,也可以直接用二進制來判斷。
let id = "XXXXXXXXXXXXXXX",//設備標識符 deviceId = ""; wx.startBluetoothDevicesDiscovery({ services:[], success(res) { wx.onBluetoothDeviceFound(res=>{ var devices = res.devices; for(let i = 0;i<devices.length;i++){ let advertisData = devices[i].advertisData; var data = arrayBufferToHexString(advertisData);//二進制轉字符串 if (!!data && data.indexOf(id) > -1) { console.log("find"); deviceId = devices[i].deviceId; } } }); }, fail(res){ console.log(res); } }); function arrayBufferToHexString(buffer) { let bufferType = Object.prototype.toString.call(buffer) if (buffer != ‘[object ArrayBuffer]‘) { return } let dataView = new DataView(buffer) var hexStr = ‘‘; for (var i = 0; i < dataView.byteLength; i++) { var str = dataView.getUint8(i); var hex = (str & 0xff).toString(16); hex = (hex.length === 1) ? ‘0‘ + hex : hex; hexStr += hex; } **** return hexStr.toUpperCase(); }
這裏需要註意的是:如果知道mac地址在安卓下可以直接略過搜索過程直接連接,如果不知道mac地址或者是IOS場景下需要開啟搜索,由於搜索是比較消耗資源的動作,所以發現目標設備以後一定要及時關閉搜索,以節省系統消耗。
(4) 搜索到設備以後,就是連接設備createBLEConnection:
(5) 連接成功以後就開始查詢設備的服務列表:getBLEDeviceServices,然後根據目標服務ID或者標識符來找到指定的服務ID
let deviceId = "XXXX"; wx.getBLEDeviceServices({ deviceId: device_id, success: function (res) { let service_id = ""; for(let i = 0;i<res.services.length;i++){ if(services[i].uuid.toUpperCase().indexOf("TEST") != -1){ service_id = services[i].uuid; break; } } return service_id; }, fail(res){ console.log(res); } })
這裏有個坑的地方:如果是安卓下如果你知道設備的服務ID,你可以省去getBLEDeviceServices的過程,但是IOS下即使你知道了服務ID,也不能省去getBLEDeviceServices的過程,這是小程序裏面需要註意的一點。
(6) 獲取服務特征值:每個服務都包含了一組特征值用來描述服務的一些屬性,比如是否可讀,是否可寫,是否可以開啟notify通知等等,當你跟藍牙通信時需要這些特征值ID來傳遞數據。
getBLEDeviceCharacteristics方法返回了res參數包含了以下屬性:
characteristics包含了一組特征值列表
通過遍歷特征值對象來獲取想要的特征值ID
wx.getBLEDeviceCharacteristics({ deviceId: device_id, serviceId: service_id, success: function (res) { let notify_id,write_id,read_id; for (let i = 0; i < res.characteristics.length; i++) { let charc = res.characteristics[i]; if (charc.properties.notify) { notify_id = charc.uuid; } if(charc.properties.write){ write_id = charc.uuid; } if(charc.properties.write){ read_id = charc.uuid; } } }, fail(res){ console.log(res); } })
這個例子就通過搜索特征值取到了 notify特征值ID,寫ID和讀取ID
(7) 獲取特征值ID以後就可以開啟notify通知模式,同時開啟監聽特征值變化消息
wx.notifyBLECharacteristicValueChange({ state: true, deviceId: device_id, serviceId: service_id, characteristicId:notify_id, complete(res) { wx.onBLECharacteristicValueChange(function (res) { console.log(arrayBufferToHexString(res.value)); }) }, fail(res){ console.log(res); } })
(8) 一切都準備好以後,就可以開始給藍牙發送消息,一旦藍牙有響應,就可以在onBLECharacteristicValueChange事件中得到消息並打印出來。
這裏面有個坑:開啟notify以後並不能馬上發送消息,藍牙設備有個準備的過程,需要在setTimeout中延遲1秒以上才能發送,否則會發送失敗
let buf = hexStringToArrayBuffer("test"); wx.writeBLECharacteristicValue({ deviceId: device_id, serviceId: service_id, characteristicId:write_id, value: buf, success: function (res) { console.log(buf); }, fail(res){ console.log(res); } }) function hexStringToArrayBuffer(str) { if (!str) { return new ArrayBuffer(0); } var buffer = new ArrayBuffer(str.length); let dataView = new DataView(buffer) let ind = 0; for (var i = 0, len = str.length; i < len; i += 2) { let code = parseInt(str.substr(i, 2), 16) dataView.setUint8(ind, code) ind++ } return buffer; }
(9) 所有都通信完畢後可以斷開連接:
wx.closeBLEConnection({
deviceId: device_id,
success(res) {
console.log(res)
},
fail(res) {
console.log(res)
}
})
wx.closeBluetoothAdapter({
success: function (res) {
console.log(res)
}
})
5. 完整例子
這裏為了簡潔,把fail等異常處理已經省去,主要流程就是設置設備ID和服務ID的過濾值,在開啟notify之後寫入測試消息,然後監聽藍牙發送過來的消息,整個過程采用簡化處理,沒有使用事件通信來驅動,僅做參考。
let blueApi = { cfg:{ device_info:"AAA", server_info:"BBB", onOpenNotify:null }, blue_data:{ device_id:"", service_id:"", write_id:"" }, setCfg(obj){ this.cfg = Object.assign({},this.cfg,obj); }, connect(){ if(!wx.openBluetoothAdapter){ this.showError("當前微信版本過低,無法使用該功能,請升級到最新微信版本後重試。"); return; } var _this = this; wx.openBluetoothAdapter({ success: function (res) { }, complete(res){ wx.onBluetoothAdapterStateChange(function(res) { if(res.available){ setTimeout(function(){ _this.connect(); },2000); } }) _this.getBlueState(); } }) }, //發送消息 sendMsg(msg,toArrayBuf = true) { let _this = this; let buf = toArrayBuf ? this.hexStringToArrayBuffer(msg) : msg; wx.writeBLECharacteristicValue({ deviceId: _this.blue_data.device_id, serviceId: _this.blue_data.service_id, characteristicId:_this.blue_data.write_id, value: buf, success: function (res) { console.log(res); } }) }, //監聽消息 onNotifyChange(callback){ var _this = this; wx.onBLECharacteristicValueChange(function (res) { let msg = _this.arrayBufferToHexString(res.value); callback && callback(msg); console.log(msg); }) }, disconnect(){ var _this = this; wx.closeBLEConnection({ deviceId: _this.blue_data.device_id, success(res) { } }) }, /*事件通信模塊*/ /*連接設備模塊*/ getBlueState() { var _this = this; if(_this.blue_data.device_id != ""){ _this.connectDevice(); return; } wx.getBluetoothAdapterState({ success: function (res) { if (!!res && res.available) {//藍牙可用 _this.startSearch(); } } }) }, startSearch(){ var _this = this; wx.startBluetoothDevicesDiscovery({ services:[], success(res) { wx.onBluetoothDeviceFound(function(res){ var device = _this.filterDevice(res.devices); if(device){ _this.blue_data.device_id = device.deviceId; _this.stopSearch(); _this.connectDevice(); } }); } }) }, //連接到設備 connectDevice(){ var _this = this; wx.createBLEConnection({ deviceId: _this.blue_data.device_id, success(res) { _this.getDeviceService(); } }) }, //搜索設備服務 getDeviceService(){ var _this = this; wx.getBLEDeviceServices({ deviceId: _this.blue_data.device_id, success: function (res) { var service_id = _this.filterService(res.services); if(service_id != ""){ _this.blue_data.service_id = service_id; _this.getDeviceCharacter(); } } }) }, //獲取連接設備的所有特征值 getDeviceCharacter() { let _this = this; wx.getBLEDeviceCharacteristics({ deviceId: _this.blue_data.device_id, serviceId: _this.blue_data.service_id, success: function (res) { let notify_id,write_id,read_id; for (let i = 0; i < res.characteristics.length; i++) { let charc = res.characteristics[i]; if (charc.properties.notify) { notify_id = charc.uuid; } if(charc.properties.write){ write_id = charc.uuid; } if(charc.properties.write){ read_id = charc.uuid; } } if(notify_id != null && write_id != null){ _this.blue_data.notify_id = notify_id; _this.blue_data.write_id = write_id; _this.blue_data.read_id = read_id; _this.openNotify(); } } }) }, openNotify(){ var _this = this; wx.notifyBLECharacteristicValueChange({ state: true, deviceId: _this.blue_data.device_id, serviceId: _this.blue_data.service_id, characteristicId: _this.blue_data.notify_id, complete(res) { setTimeout(function(){ _this.onOpenNotify && _this.onOpenNotify(); },1000); _this.onNotifyChange();//接受消息 } }) }, /*連接設備模塊*/ /*其他輔助模塊*/ //停止搜索周邊設備 stopSearch() { var _this = this; wx.stopBluetoothDevicesDiscovery({ success: function (res) { } }) }, arrayBufferToHexString(buffer) { let bufferType = Object.prototype.toString.call(buffer) if (buffer != ‘[object ArrayBuffer]‘) { return } let dataView = new DataView(buffer) var hexStr = ‘‘; for (var i = 0; i < dataView.byteLength; i++) { var str = dataView.getUint8(i); var hex = (str & 0xff).toString(16); hex = (hex.length === 1) ? ‘0‘ + hex : hex; hexStr += hex; } return hexStr.toUpperCase(); }, hexStringToArrayBuffer(str) { if (!str) { return new ArrayBuffer(0); } var buffer = new ArrayBuffer(str.length); let dataView = new DataView(buffer) let ind = 0; for (var i = 0, len = str.length; i < len; i += 2) { let code = parseInt(str.substr(i, 2), 16) dataView.setUint8(ind, code) ind++ } return buffer; } //過濾目標設備 filterDevice(device){ var data = blueApi.arrayBufferToHexString(device.advertisData); if (data && data.indexOf(this.device_info.substr(4).toUpperCase()) > -1) { var obj = { name: device.name, deviceId: device.deviceId } return obj } else{ return null; } }, //過濾主服務 filterService(services){ let service_id = ""; for(let i = 0;i<services.length;i++){ if(services[i].uuid.toUpperCase().indexOf(this.server_info) != -1){ service_id = services[i].uuid; break; } } return service_id; } /*其他輔助模塊*/ } blueApi.setCfg({ device_info:"AAA", server_info:"BBB", onOpenNotify:function(){ blueApi.sendMsg("test"); } }) blueApi.connect(); blueApi.onNotifyChange(function(msg){ console.log(msg); })
6. 跳坑總結
(1) 等待響應:很多情況下需要等待設備響應,尤其在IOS環境下,比如
監聽到藍牙開啟後,不能馬上開始搜索,需要等待2秒
開啟notify以後,不能馬上發送消息,需要等待1秒
(2) Mac和UUID:安卓的mac地址是可以獲取到的所以設備的ID是固定的,但是IOS是獲取不到MAC地址的,只能獲取設備的UUID,而且是動態的,所以需要使用其他方法來查詢。
(3) IOS下只有搜索可以省略,如果你知道了設備的ID,服務ID和各種特征值ID,在安卓下可以直接連接,然後發送消息,省去搜索設備,搜索服務和搜索特征值的過程,但是在IOS下,只能指定設備ID連接,後面的過程是不能省略的。
(4) 監聽到的消息要進行過濾處理,有些設備會抽風一樣的發送同樣的消息,需要在處理邏輯裏面去重。
(5) 操作完成後要及時關閉連接,同時也要關閉藍牙設備,否則安卓下再次進入會搜索不到設備除非關閉小程序進程再進才可以,IOS不受影響。
wx.closeBLEConnection({
deviceId: _this.blue_data.device_id,
success(res) {
},
fail(res) {
}
})
wx.closeBluetoothAdapter({
success(res){
},
fail(res){
}
})
除了以上的常見問題,你還需要處理很多異常情況,比如藍牙中途關閉,網絡斷開,GPS未開啟等等場景,總之和硬件設備打交道跟純UI交互還是有很大的差別的。
相關閱讀
微信+WeTest:小程序雲端測試系統上線
更穩更快:深大的樹洞小程序版本叠代與優化記錄
菊花綻放:微信是如何識別小程序碼的?
此文已由作者授權騰訊雲技術社區發布,轉載請註明文章出處
原文鏈接:https://cloud.tencent.com/community/article/827097
實戰分享,教你藍牙在小程序中的應用