藍牙在小程序中的應用

1. 背景介紹前端

  藍牙是愛立信公司創立的一種無線技術標準,爲短距離的硬件設備提供低成本的通訊規範。藍牙規範由藍牙技術聯盟(Bluetooth Special Interest Group,簡稱SIG)管理,在計算機,手機,傳真機,耳機,汽車,家用電器等等不少場景普遍使用。藍牙具備如下一些特色:小程序

      (1) 無償使用:使用的工做頻段在2.4GHz的工科醫(ISM)頻段,無需申請許可證。安全

      (2) 功耗低:BLE4.0包含了一個低功耗標準(Bluetooth Low Energy),可讓藍牙的功耗顯著下降微信

      (3) 安全性高:藍牙規範提供了一套安全加密機制和受權機制,能夠有效防範數據被竊取網絡

      (4) 傳輸率高:目前最新BLE4.0版本,理論傳輸速率可達3Mbit/s(實際確定達不到),理論覆蓋範圍可達100米。異步

         

 

2.小程序藍牙介紹ide

    小程序API提供了一套藍牙操做接口,因此做爲咱們前端開發人員能夠更加方便的進行藍牙設備開發,而無需瞭解安卓和IOS的各類藍牙底層概念。小程序的藍牙操做大多都是經過異步調用來處理的,這裏面就存在着一些坑,後面會詳細介紹。在使用小程序藍牙API以前有幾個概念或者說術語須要預先了解:測試

    (1) 藍牙終端:咱們常說的硬件設備,包括手機,電腦等等。ui

    (2) UUID:是由子母和數字組成的40個字符串的序號,根據硬件設備有關聯的惟一ID。this

    (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);
})
View Code

 

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交互仍是有很大的差異的。

相關文章
相關標籤/搜索