欢迎大家前往腾讯云技术社区,获取更多腾讯海量技术实践干货哦~
作者:朱胜

导语

蓝牙在日常生活中广泛使用的一项技术,小程序给了我们前端工程师一个控制蓝牙的方法,带上你的设备,来看看怎么控制你的蓝牙设备吧。

1. 背景介绍

蓝牙是爱立信公司创立的一种无线技术标准,为短距离的硬件设备提供低成本的通信规范。蓝牙规范由蓝牙技术联盟(Bluetooth Special Interest Group,简称SIG)管理,在计算机,手机,传真机,耳机,汽车,家用电器等等很多场景广泛使用。蓝牙具有以下一些特点:
(1) 免费使用:使用的工作频段在2.4GHz的工科医(ISM)频段,无需申请许可证。
(2) 功耗低:BLE4.0包含了一个低功耗标准(Bluetooth Low Energy),可以让蓝牙的功耗显著降低
(3) 安全性高:蓝牙规范提供了一套安全加密机制和授权机制,可以有效防范数据被窃取
(4) 传输率高:目前最新BLE4.0版本,理论传输速率可达3Mbit/s(实际肯定达不到),理论覆盖范围可达100米。
1508314661423_3343_1508314685332.jpg
1508314674485_6520_1508314698318.png

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的二进制类型来的,所以在我们的使用过程中需要进行转码。
1508314805462_3727_1508314829406.png

3. API总览

小程序对蓝牙设备的操作有18个API
[td]
API名称说明
openBluetoothAdapter初始化蓝牙适配器,在此可用判断蓝牙是否可用
closeBluetoothAdapter关闭蓝牙连接,释放资源
getBluetoothAdapterState获取蓝牙适配器状态,如果蓝牙未开或不可用,这里可用检测到
onBluetoothAdapterStateChange蓝牙适配器状态发生变化事件,这里可用监控蓝牙的关闭和打开动作
startBluetoothDevicesDiscovery开始搜索设备,蓝牙初始化成功后就可以搜索设备
stopBluetoothDevicesDiscovery当找到目标设备以后需要停止搜索,因为搜索设备是比较消耗资源的操作
getBluetoothDevices获取已经搜索到的设备列表
onBluetoothDeviceFound当搜索到一个设备时的事件,在此可用过滤目标设备
getConnectedBluetoothDevices获取已连接的设备
createBLEConnection创建BLE连接
closeBLEConnection关闭BLE连接
getBLEDeviceServices获取设备的服务列表,每个蓝牙设备都有一些服务
getBLEDeviceCharacteristics获取蓝牙设备某个服务的特征值列表
readBLECharacteristicValue读取低功耗蓝牙设备的特征值的二进制数据值
writeBLECharacteristicValue向蓝牙设备写入数据
notifyBLECharacteristicValueChange开启蓝牙设备notify提醒功能,只有开启这个功能才能接受到蓝牙推送的数据
onBLEConnectionStateChange监听蓝牙设备错误事件,包括异常断开等等
onBLECharacteristicValueChange监听蓝牙推送的数据,也就是notify数据

4. 主要流程

蓝牙通信的一个正常流程是下面的图示
1508314916535_7138_1508314940423.png
(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
    1508314941142_8213_1508314965035.png
    由于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参数包含了以下属性:
    1508315210401_5391_1508315234216.png
    characteristics包含了一组特征值列表
    1508315221637_8594_1508315245508.png
    通过遍历特征值对象来获取想要的特征值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通知模式,同时开启监听特征值变化消息
    1508315245679_1026_1508315269498.png
    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交互还是有很大的差别的。

    此文已由作者授权腾讯云技术社区发布,转载请注明文章出处
    原文链接:https://cloud.tencent.com/community/article/827097