藍牙在日常生活中廣泛使用的一項技術(shù),小程序給了我們前端工程師一個控制藍牙的方法,帶上你的設(shè)備,來看看怎么控制你的藍牙設(shè)備吧。
藍牙是愛立信公司創(chuàng)立的一種無線技術(shù)標準,為短距離的硬件設(shè)備提供低成本的通信規(guī)范。藍牙規(guī)范由藍牙技術(shù)聯(lián)盟(Bluetooth Special Interest Group,簡稱SIG)管理,在計算機,手機,傳真機,耳機,汽車,家用電器等等很多場景廣泛使用。藍牙具有以下一些特點:
(1) 免費使用:使用的工作頻段在2.4GHz的工科醫(yī)(ISM)頻段,無需申請許可證。
(2) 功耗低:BLE4.0包含了一個低功耗標準(Bluetooth Low Energy),可以讓藍牙的功耗顯著降低
(3) 安全性高:藍牙規(guī)范提供了一套安全加密機制和授權(quán)機制,可以有效防范數(shù)據(jù)被竊取
(4) 傳輸率高:目前最新BLE4.0版本,理論傳輸速率可達3Mbit/s(實際肯定達不到),理論覆蓋范圍可達100米。


小程序API提供了一套藍牙操作接口,所以作為我們前端開發(fā)人員可以更加方便的進行藍牙設(shè)備開發(fā),而無需了解安卓和IOS的各種藍牙底層概念。小程序的藍牙操作大多都是通過異步調(diào)用來處理的,這里面就存在著一些坑,后面會詳細介紹。在使用小程序藍牙API之前有幾個概念或者說術(shù)語需要預(yù)先了解:
(1) 藍牙終端:我們常說的硬件設(shè)備,包括手機,電腦等等。
(2) UUID:是由子母和數(shù)字組成的40個字符串的序號,根據(jù)硬件設(shè)備有關(guān)聯(lián)的唯一ID。
(3) 設(shè)備地址:每個藍牙設(shè)備都有一個設(shè)備地址deviceId,但是安卓和IOS差別很大,安卓下設(shè)備地址就是mac地址,但是IOS無法獲取mac地址,所以設(shè)備地址是針對本機范圍有效的UUID,所以這里需要注意,后面會介紹。
(4) 設(shè)備服務(wù)列表:每個設(shè)備都存在一些服務(wù)列表,可以跟不同的設(shè)備進行通信,服務(wù)有一個serviceId來維護,每個服務(wù)包含了一組特征值。
(5) 服務(wù)特征值:包含一個單獨的value值和0 –n個用來描述characteristic 值(value)的descriptors。一個characteristics可以被認為是一種類型的,類似于一個類。
(6) ArrayBuffer:小程序中對藍牙數(shù)據(jù)的傳遞是使用ArrayBuffer的二進制類型來的,所以在我們的使用過程中需要進行轉(zhuǎn)碼。

小程序?qū)λ{牙設(shè)備的操作有18個API
| API名稱 | 說明 |
|---|---|
| openBluetoothAdapter | 初始化藍牙適配器,在此可用判斷藍牙是否可用 |
| closeBluetoothAdapter | 關(guān)閉藍牙連接,釋放資源 |
| getBluetoothAdapterState | 獲取藍牙適配器狀態(tài),如果藍牙未開或不可用,這里可用檢測到 |
| onBluetoothAdapterStateChange | 藍牙適配器狀態(tài)發(fā)生變化事件,這里可用監(jiān)控藍牙的關(guān)閉和打開動作 |
| startBluetoothDevicesDiscovery | 開始搜索設(shè)備,藍牙初始化成功后就可以搜索設(shè)備 |
| stopBluetoothDevicesDiscovery | 當找到目標設(shè)備以后需要停止搜索,因為搜索設(shè)備是比較消耗資源的操作 |
| getBluetoothDevices | 獲取已經(jīng)搜索到的設(shè)備列表 |
| onBluetoothDeviceFound | 當搜索到一個設(shè)備時的事件,在此可用過濾目標設(shè)備 |
| getConnectedBluetoothDevices | 獲取已連接的設(shè)備 |
| createBLEConnection | 創(chuàng)建BLE連接 |
| closeBLEConnection | 關(guān)閉BLE連接 |
| getBLEDeviceServices | 獲取設(shè)備的服務(wù)列表,每個藍牙設(shè)備都有一些服務(wù) |
| getBLEDeviceCharacteristics | 獲取藍牙設(shè)備某個服務(wù)的特征值列表 |
| readBLECharacteristicValue | 讀取低功耗藍牙設(shè)備的特征值的二進制數(shù)據(jù)值 |
| writeBLECharacteristicValue | 向藍牙設(shè)備寫入數(shù)據(jù) |
| notifyBLECharacteristicValueChange | 開啟藍牙設(shè)備notify提醒功能,只有開啟這個功能才能接受到藍牙推送的數(shù)據(jù) |
| onBLEConnectionStateChange | 監(jiān)聽藍牙設(shè)備錯誤事件,包括異常斷開等等 |
| onBLECharacteristicValueChange | 監(jiān)聽藍牙推送的數(shù)據(jù),也就是notify數(shù)據(jù) |
藍牙通信的一個正常流程是下面的圖示

(1) 開啟藍牙:調(diào)用openBluetoothAdapter來開啟和初始化藍牙,這個時候可以根據(jù)狀態(tài)判斷用戶設(shè)備是否支持藍牙
(2) 檢查藍牙狀態(tài):調(diào)用getBluetoothAdapterState來檢查藍牙是否開啟,如果沒有開啟可以在這里提醒用戶開啟藍牙,并且能在開啟后自動啟動下面的步驟
這里有一個坑:IOS里面藍牙狀態(tài)變化以后不能馬上開始搜索,否則會搜索不到設(shè)備,必須要等待2秒以上。
function connect(){ wx.openBluetoothAdapter({ success: function (res) { }, fail(res){ }, complete(res){ wx.onBluetoothAdapterStateChange(function(res) { if(res.available){ setTimeout(function(){ connect(); },2000); } }) //開始搜索 } }) }
(3) 搜索設(shè)備:startBluetoothDevicesDiscovery開始搜索設(shè)備,當發(fā)現(xiàn)一個設(shè)備會觸發(fā)onBluetoothDeviceFound事件,首先看下標準API

由于IOS無法獲取Mac地址所以這里需要區(qū)分兩個場景
a) 安卓:安卓下可以根據(jù)Mac地址來搜索設(shè)備,或者跳過此步直接連接到設(shè)備。當搜索到一個設(shè)備以后,可以在onBluetoothDeviceFound事件回調(diào)中判斷當前設(shè)備的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下獲取設(shè)備Mac地址的方法已經(jīng)被屏蔽,所以不存在mac地址,此時只能通過其他方式來判斷,比如在藍牙設(shè)備advertisData字段添加一些特別的信息來判斷等等,可以轉(zhuǎn)字符串來判斷,也可以直接用二進制來判斷。
let id = "XXXXXXXXXXXXXXX",//設(shè)備標識符 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);//二進制轉(zhuǎn)字符串 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(); }
這里需要注意的是:如果知道m(xù)ac地址在安卓下可以直接略過搜索過程直接連接,如果不知道m(xù)ac地址或者是IOS場景下需要開啟搜索,由于搜索是比較消耗資源的動作,所以發(fā)現(xiàn)目標設(shè)備以后一定要及時關(guān)閉搜索,以節(jié)省系統(tǒng)消耗。
(4) 搜索到設(shè)備以后,就是連接設(shè)備createBLEConnection:
(5) 連接成功以后就開始查詢設(shè)備的服務(wù)列表:getBLEDeviceServices,然后根據(jù)目標服務(wù)ID或者標識符來找到指定的服務(wù)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); } })
這里有個坑的地方:如果是安卓下如果你知道設(shè)備的服務(wù)ID,你可以省去getBLEDeviceServices的過程,但是IOS下即使你知道了服務(wù)ID,也不能省去getBLEDeviceServices的過程,這是小程序里面需要注意的一點。
(6) 獲取服務(wù)特征值:每個服務(wù)都包含了一組特征值用來描述服務(wù)的一些屬性,比如是否可讀,是否可寫,是否可以開啟notify通知等等,當你跟藍牙通信時需要這些特征值ID來傳遞數(shù)據(jù)。
getBLEDeviceCharacteristics方法返回了res參數(shù)包含了以下屬性:

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通知模式,同時開啟監(jiān)聽特征值變化消息

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) 一切都準備好以后,就可以開始給藍牙發(fā)送消息,一旦藍牙有響應(yīng),就可以在onBLECharacteristicValueChange事件中得到消息并打印出來。
這里面有個坑:開啟notify以后并不能馬上發(fā)送消息,藍牙設(shè)備有個準備的過程,需要在setTimeout中延遲1秒以上才能發(fā)送,否則會發(fā)送失敗
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)
}
})
這里為了簡潔,把fail等異常處理已經(jīng)省去,主要流程就是設(shè)置設(shè)備ID和服務(wù)ID的過濾值,在開啟notify之后寫入測試消息,然后監(jiān)聽藍牙發(fā)送過來的消息,整個過程采用簡化處理,沒有使用事件通信來驅(qū)動,僅做參考。
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(); } }) }, //發(fā)送消息 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); } }) }, //監(jiān)聽消息 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) { } }) }, /*事件通信模塊*/ /*連接設(shè)備模塊*/ 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(); } }); } }) }, //連接到設(shè)備 connectDevice(){ var _this = this; wx.createBLEConnection({ deviceId: _this.blue_data.device_id, success(res) { _this.getDeviceService(); } }) }, //搜索設(shè)備服務(wù) 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(); } } }) }, //獲取連接設(shè)備的所有特征值 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();//接受消息 } }) }, /*連接設(shè)備模塊*/ /*其他輔助模塊*/ //停止搜索周邊設(shè)備 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; } //過濾目標設(shè)備 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; } }, //過濾主服務(wù) 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); })
(1) 等待響應(yīng):很多情況下需要等待設(shè)備響應(yīng),尤其在IOS環(huán)境下,比如
監(jiān)聽到藍牙開啟后,不能馬上開始搜索,需要等待2秒
開啟notify以后,不能馬上發(fā)送消息,需要等待1秒
(2) Mac和UUID:安卓的mac地址是可以獲取到的所以設(shè)備的ID是固定的,但是IOS是獲取不到MAC地址的,只能獲取設(shè)備的UUID,而且是動態(tài)的,所以需要使用其他方法來查詢。
(3) IOS下只有搜索可以省略,如果你知道了設(shè)備的ID,服務(wù)ID和各種特征值ID,在安卓下可以直接連接,然后發(fā)送消息,省去搜索設(shè)備,搜索服務(wù)和搜索特征值的過程,但是在IOS下,只能指定設(shè)備ID連接,后面的過程是不能省略的。
(4) 監(jiān)聽到的消息要進行過濾處理,有些設(shè)備會抽風(fēng)一樣的發(fā)送同樣的消息,需要在處理邏輯里面去重。
(5) 操作完成后要及時關(guān)閉連接,同時也要關(guān)閉藍牙設(shè)備,否則安卓下再次進入會搜索不到設(shè)備除非關(guān)閉小程序進程再進才可以,IOS不受影響。
wx.closeBLEConnection({
deviceId: _this.blue_data.device_id,
success(res) {
},
fail(res) {
}
})
wx.closeBluetoothAdapter({
success(res){
},
fail(res){
}
})
除了以上的常見問題,你還需要處理很多異常情況,比如藍牙中途關(guān)閉,網(wǎng)絡(luò)斷開,GPS未開啟等等場景,總之和硬件設(shè)備打交道跟純UI交互還是有很大的差別的。