小程序管理蓝牙设备开发指北
发布于 3 年前 作者 fang12 289 次浏览 来自 分享

小程序管理蓝牙设备开发记录

前段时间接到一个管理蓝牙设备的需求,要求能搜索并连接指定设备,并读取设备的信息,然后发送指令给设备,让设备运行起来。

允许连接多台同类型的设备,并对设备做分开管理

期间,遇到不少的坑,在此记录下来,希望能对大家有所帮助,有欠缺的地方,还请大家帮忙指正一下,谢谢

话不多说,接下来就进入开发:

1. 初始化

考虑到需要在不同页面都要使用微信的蓝牙接口,并且还需要一些数据的互通,所以我建了一个单例,用于处理微信接口,和设备状态、信息管理

  constructor(config = {}) {
    if (!manager.instance) {
      manager.instance = this;
      this.connectPool = [];
      this.cachePool = [];
      this.discoveryPool = [];
      this.timeout = 5000;
      this._timer = null;
      this.adpterStatus = {open: false}
    }
    Object.assign(manager.instance, config);
    if (!this.adpterStatus.open) {
      this.initBluetoothAdapter()
    }
    return manager.instance;
  }

connectPool 设备连接池,用于存储正在连接的设备 <br>
cachePool 设备缓存池,用于存储连接过的设备 <br>
discoveryPool 设备发现池,用于存储扫描到的设备 <br>
timeout 超时时间 <br>
adpterStatus 蓝牙适配器状态 {open: '是否打开', available: '是否可用', discovering: '是否正在搜索设备'}

若适配器打开状态为false那么初始化适配器initBluetoothAdapter


  /**
  * 为了方便处理微信的回调,建了一个公共的callBack方法
  */
  commonCall(success = ()=>{}, fail = ()=>{}, complete = ()=>{}) {
    return {success, fail, complete}
  }

  initBluetoothAdapter() {
    const that = this;
    
    /**
    * 监听适配器状态,开启监听之前先关闭监听,防止状态重复
    * offBluetoothAdapterStateChange 关闭适配器状态监听
    * onBluetoothAdapterStateChange 开启适配器状态监听
    */
    wx.offBluetoothAdapterStateChange();
    wx.onBluetoothAdapterStateChange(res => {
      // 同步适配器状态,TODO做manager工具内的监听,可以参考下一步搜索状态的监听 3. 发现设备 中的 discoveryPoolDidUpdate 方法
      Object.assign(that.adpterStatus, res)
      // TODO 若适配器重新可获取时,重新开启适配器
      // 若适配器open = true,开始 -> 2. 设置监听
      that.setListener()
    });
    /**
    * 开启小程序蓝牙适配器,开启之前先关闭,防止状态重复
    * closeBluetoothAdapter 关闭蓝牙适配器
    * openBluetoothAdapter 开启蓝牙适配器
    */
    wx.closeBluetoothAdapter();
    wx.openBluetoothAdapter(that.commonCall(success => {
        // 蓝牙适配器初始化成功
    }));
  }

2. 设置监听

适配器初始化完成后,设置监听,统一处理数据和状态:

onBluetoothDeviceFound 蓝牙搜索监听

onBLEConnectionStateChange 蓝牙设备连接状态监听,并在连接成功的时候

onBLECharacteristicValueChange 蓝牙设备特征值变化监听,用户小程序和蓝牙的交互


    setListener() {
        wx.offBluetoothDeviceFound()
        wx.onBluetoothDeviceFound(res => {
            // 设备搜索监听,更新设备,详情请移步 -> 3. 发现设备
        })

        wx.offBLEConnectionStateChange()
        wx.onBLEConnectionStateChange(res => {
            /**
            * res = {
            *    errorCode: 0 成功
            *    errorMsg: 错误信息
            *    connected: 0 断开连接,1 连接成功
            *    deviceId:连接设备的deivceId
            * }
            * 
            *   设备连接状态更新,若连接成功,则开始针对设备进行数据监听,详情请移步 -> 设备交互
            */
        })

        wx.offBLECharacteristicValueChange()
        wx.onBLECharacteristicValueChange(res => {
            // 设备特征值发生变化,更新设备数据,详情请移步 -> 设备交互
        })
    }

3. 发现设备

    /**
    * services: 可以通过设备是否具备特定的服务UUID来筛选自己想要的设备
    * sCall: 扫描方法调用成功
    * fCall:扫描方法调用失败
    */
    discoveryBluetoothDevices(services, sCall, fCall) {
        const that = this;
        // 扫描前清空discoveryPool
        that.discoveryPool = [];
        const discoveryCall = that.commonCall(sCall, fCall);
        discoveryCall.services = services;
        wx.stopBluetoothDevicesDiscovery(that.commonCall(__=>__, __=>__, () => {
        wx.startBluetoothDevicesDiscovery(discoveryCall)
        }))
  }

若设备有新设备,则‘设置监听’中的onBluetoothDeviceFound会进行新设备上报

调用updateDevice方法进行设备过滤和保存

    updateDevice(device) {
        if (!device) return;
        if (!device.name) return;
        if (!device.advertisData) return;
        const that = this;
        // 判断设备是否是新设备
        if (that.discoveryPool.map(v => v.deviceId).indexOf(device.deviceId) === -1) {
            // ab2str 见下方备注
            device.advertisData = ab2str(device.advertisData)
            that.discoveryPool.push(device)
            // 给单例添加提供给外部监听状态的接口,当设备有更新的时候,触发接口回调
            that.discoveryPoolDidUpdate && that.discoveryPoolDidUpdate instanceof Function && that.discoveryPoolDidUpdate(that.discoveryPool)
            // 私有回调 -> 4.连接设备
            that._discoveryPoolDidUpdate && that._discoveryPoolDidUpdate instanceof Function && that._discoveryPoolDidUpdate(device)
            that._timer && clearTimeout(that._timer)
        }
        // 超时时间,超时后若无新设备,则关闭Discovery方法
        that._timer = setTimeout(() => {
            wx.stopBluetoothDevicesDiscovery()
            that._timer && clearTimeout(that._timer)
        }, that.timeout)
    }

备注:设备广播数据是ArrayBuffer的形式,所以通过ab2str的方法进行转换,方法见下:

    ab2str(buffer) {
        return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
    }

4. 连接设备

小程序连接设备的接口wx.createBLEConnection()接受的值是deviceId,作为设备的标识符。

但是这个设备Id在iOS和Android手机上,同一设备的值是不同的,所以如果我们要把deviceId保存给后台服务器,下次再拿到,不一定可以直接使用。

所以为了通用性,我们应该跟设备制造商统一一下,让设备广播出自己的特征码,亦或者直接通过服务UUID获取到设备的mac地址,用这些不变且唯一的字符串作为保存到后端的设备标识符。

如果以广播中的特征码为唯一标识符,搜索设备并向后台保存的过程中,无需跟设备进行连接操作,本文以这样的方式进行;

如果通过服务UUID获取到设备的mac,保存给后端,需要扫描到设备之后,连接设备,并通过获取设备mac地址的服务UUID,读取到设备的mac,保存到后台。

    
    connectBluetoothDevice(indentify, sCall, fCall) {
        const that = this;
        const connectCall = that.commonCall(sCall, fCall);
        if (that.adpterStatus.discovering) {
        for (let i=0; i<that.discoveryPool.length; i++) {
            if (that.discoveryPool[i].advertisData == indentify) {
                connectCall.deviceId = that.discoveryPool[i].deviceId
                connectCall.timeout = that.timeout
                wx.createBLEConnection(connectCall)
            }
        }
        } else {
        that.discoveryPoolDidUpdate = null
        that.discoveryBluetoothDevices([], s => {
            // 扫描到设备之后,用广播数据进行比对,若一样,获取该设备的deviceId,并连接
            that._discoveryPoolDidUpdate = res => {
            if (res.advertisData == indentify) {
                connectCall.deviceId = res.deviceId
                wx.createBLEConnection(connectCall)
                wx.stopBluetoothDevicesDiscovery()
            } else {
                // 检查是否已经停止扫描
                console.log('_discoveryPoolDidUpdate', that.adpterStatus)
                fCall && fCall instanceof Function && fCall("device not found !")
            }
            }
        })
        }
  }

5. 设备交互

设备交互有三种形式:

read 程序读取设备的信息

write 向设备发指令

notify 订阅设备的上报

蓝牙设备出厂的时候,就设置了一些接口,并定义好访问它的服务ID和特征值ID以及访问方式,通过这些可以跟设备做到交互

用前端跟后端交互的方式理解,跟设备进行交互的时候,服务ID和特征值ID 就相当于我们访问接口的api接口,read相当于get接口,获取到数据,write相当于post接口,数据发送给后台,后台就对应数据逻辑做相应变更,notify相当于与服务器建立websocket连接,实时获取服务器发来的数据(单方向)

由于服务ID特征值ID都是这样00002A23-0000-1000-8000-008BF9B054F3难以记住的串,所以我们建立一个服务适配器(services-adpter),它负责配置我们需要用到的服务,如下

    export const serviceAdapter = [
        { // 开始设备
            serviceName: 'start',
            serviceUUID: '服务ID',
            characterUUID: '特征值ID',
            inFormatter: '入参格式化方法',
            outFormatter: '出参格式化方法',
            type: 'write'
        }
    ]

我们传给设备的数据需要转换二进制数据异或操作,所以在这里进行配置入参格式化方法和出参格式化方法

接下来就是交互,在设备连接上之后,处理serviceAdapter中的type = read 和 type = notify的任务

处理serviceAdapter任务的顺序为 :处理read任务,对设备进行属性的初始化 -> 处理notify任务,对设备属性进行监听,并设置callBack -> write任务需要主动触发

    // 处理read任务,对设备进行属性的初始化
    const readServices = that.getServicesBy('read')
    readServices.forEach(rs => {
        console.log('will start read servce:', rs);
        const call = that.commonCall(success => {
            console.log('readBLECharacteristicValue success:', success)
        })
        call.deviceId = device.deviceId
        call.serviceId = rs.serviceUUID
        call.characteristicId = rs.characterUUID
        wx.readBLECharacteristicValue(call)
    })
    // 处理notify任务,对设备属性进行监听,并设置callBack
    const notifyServices = that.getServicesBy('notify')
    notifyServices.forEach(ns => {
        console.log('will start notify servce:', ns);
        const call = that.commonCall(success => {
            console.log('notifyBLECharacteristicValueChange success:', success)
        })
        call.deviceId = device.deviceId
        call.serviceId = ns.serviceUUID
        call.characteristicId = ns.characterUUID
        wx.notifyBLECharacteristicValueChange(call)
    })

write方式,需要用户主动触发

    beginService(indentify, serviceName, params, sCall, fCall) {
        const that = this
        for (let i=0; i<serviceAdapter.length; i++) {
            let adapter = serviceAdapter[i]
            if (adapter.serviceName == serviceName) {
                if (adapter.inFormatter && adapter.inFormatter instanceof Function) {
                    const device = this.deviceBy(indentify, 'connect')
                    const call = that.commonCall(sCall, fCall)
                    call.deviceId = device.deviceId
                    call.serviceId = adapter.serviceUUID
                    call.characteristicId = adapter.characterUUID
                    call.value = adapter.inFormatter(params)
                    wx.writeBLECharacteristicValue(call)
                }
                return
            }
        }
    }

indentify 设备的唯一标志符 <br>
serviceName 需要访问的服务名称 <br>
params 发送给设备的数据 <br>
发送数据给用户,并在监听中获取最新的设备信息和状态

自此我们就初步的完成了设备搜索到连接到交互的过程

6. TODOS

为了让工具更加完善,需要增加错误处理,异常抛出,错误重试等操作,这里就不在此赘述了

回到顶部