蓝牙在小程序中的应用

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

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

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

朱胜的专栏

1 篇文章3 人订阅

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏安恒信息

思科ASA SSL VPN爆权限提升漏洞

日前,国外安全公司Trustwave研究员Jonathan Claudius发现Cisco ASA 5500系列平台存在一个权限提升漏洞,Cis...

3165
来自专栏晓晨的专栏

Docker实用技巧之更改软件包源提升构建速度

地球,中国,成都市,某小区的阳台上,一青年负手而立,闭目沉思,阵阵的凉风吹得他衣衫呼呼的飘。忽然,他抬起头,刹那间,睁开了双眼,好似一到精光射向星空,只见这夜空...

560
来自专栏安恒网络空间安全讲武堂

WIFI干扰器制作

WIFI干扰器制作 ? emmmm 不能瞎玩啊 被隔壁邻居举报了我不负责的 Esp8266的工作原理 Esp8266的工作原理,知乎有位大佬的回答: 解析一键配...

5699
来自专栏漏斗社区

关于HID攻击介绍

HID(Human InterfaceDevice,是计算机直接与人交互的设备,例如键盘、鼠标等)攻击的一种。攻击者通过将USB设备模拟成为键盘,让电脑识别成为...

1205
来自专栏一“技”之长

iOS使用VOIP与CallKit实现体验优质的网络通讯功能

    VOIP是Apple提供给开发者的网络电话功能接口。简单来说,其可以让你的应用程序在完全杀死的情况下被服务端唤醒。CallKit是iOS10引入的新框架...

862
来自专栏DeveWork

总结:如何加速你的 WordPress 站点?

这篇文章英文原文发表于Smashing Magazine,感谢小影 的为我们带来的全文翻译。内容上讲解比较通俗易懂,非常适合初学者。 几个月前,我做了一个实验,...

2327
来自专栏FreeBuf

树莓派随身工具箱:中间人劫持获取控制权

上文讲解了树莓派随身工具箱的环境搭建,这段时间又对其进行了一些优化,主要是从便携美观上面改进。同时,在实际使用中发现了一些问题,并做了小小的改动。

1043
来自专栏FreeBuf

5分钟教程:如何通过UART获得root权限

写在前面的话 你知道物联网设备以及其他硬件制造商是如何调试和测试自家设备的吗?没错,绝大多数情况下,他们都会留下一个串行接口,这样就可以利用这个接口并通过she...

2926
来自专栏進无尽的文章

视频直播| 搭建一个本地nginx服务器以及实现推流和拉流

原想用mac中自带的Apache搭建,但是naginx是轻量级的,同样起web 服务,也比apache 占用更少的内存及资源,nginx 处理请求是异步非阻塞的...

2301
来自专栏杨建荣的学习笔记

关于ORA-12801,ORA-27090的简单分析(r4笔记第58天)

今天下午收到客户的邮件,说有一个job在运行的时候报错了,希望我们能帮忙看看是什么原因。 ERROR: Caught en exception: ORA-128...

2757

扫码关注云+社区