uni-app低功耗蓝牙开发

uni-app低功耗蓝牙开发

2020年08月16日 阅读:565 字数:2099 阅读时长:5 分钟

uni-app 低功耗蓝牙扫描、连接、获取主服务特征值,向蓝牙设备发送数据,订阅蓝牙设备收到的通知

Uniapp蓝牙模块功能开发笔记。

功能主要是初始化蓝牙模块、搜索蓝牙设备、连接蓝牙设备、获取蓝牙设备所有服务、获取蓝牙设备某个服务中所有特征值、向蓝牙设备特征值中写入二进制数据、读取或监听低功耗蓝牙设备的特征值变化。

1、初始化蓝牙模块

由于Uniapp的初始化蓝牙模块api并不会拉起系统打开蓝牙开关,需要手动开启蓝牙后再进行初始化。

但是可以使用html5plus来实现请求打开蓝牙开关,适用于Andriod

pages/scan/index.vue

// 安卓系统请求开启蓝牙
initBluetoothAdapter() {
  let BluetoothAdapter;
  this.main = plus.android.runtimeMainActivity();
  BluetoothAdapter = plus.android.importClass("android.bluetooth.BluetoothAdapter");
  this.BAdapter = BluetoothAdapter.getDefaultAdapter();
  if(this.BAdapter && !this.BAdapter.isEnabled()) {
    this.BAdapter.enable();
  }
},
scopeBluetoothAdapter() {
  return new Promise(resolve => {
    this.BAdapter.enable();
    const receiver = plus.android.implements('io.dcloud.android.content.BroadcastReceiver', {
      onReceive: function(context, intent) { //实现onReceiver回调函数
        plus.android.importClass(intent);
        this.main.unregisterReceiver(receiver);
      }
    });
    const IntentFilter = plus.android.importClass('android.content.IntentFilter');
    const filter = new IntentFilter();
    filter.addAction(BAdapter.ACTION_STATE_CHANGED); //监听蓝牙开关
    this.main.registerReceiver(receiver, filter); //注册监听
    resolve()
  })
},
// 初始化蓝牙设备
openBluetoothAdapter() {
  return new Promise((resolve, reject) => {
    uni.openBluetoothAdapter({
      success: e => {
        resolve()
      },
      fail: e => {
        reject(e)
      }
    });
  })
},
// 关闭蓝牙模块。调用该方法将断开所有已建立的连接并释放系统资源。
closeBluetoothAdapter() {
  return new Promise((resolve, reject) => {
    uni.closeBluetoothAdapter({
      success: e => {
        resolve()
      },
      fail: e => {
        reject(e)
      }
    });
  })
}

2、扫描蓝牙设备

只搜索广播包有对应 uuid 的主服务的蓝牙设备:使用services属性,或者自己写逻辑过滤掉周边不需要处理的其他蓝牙设备

可以使用vuex存储扫描到的蓝牙设备。

store/modules/bluetooth.js

uni.startBluetoothDevicesDiscovery({
  allowDuplicatesKey: true, //是否允许重复上报同一设备
  interval: 200,
  // services: ['FFF0', 'FFB0'],
  success: e => {
    this.searchLoad = true;
    !this.isListenFound && this.onBluetoothDeviceFound(); // 是否已经监听发现外围设备事件
  },
  fail: e => {
    console.log('搜索蓝牙设备失败,错误码:' + e.errCode);、
  }
});
uni.onBluetoothDeviceFound(({ devices }) => {
  const { deviceId, RSSI, advertisServiceUUIDs } = devices[0]
    const source = JSON.parse(JSON.stringify(state.bluetoothList))
    const advertisServiceUUIDsStr = advertisServiceUUIDs.join('~')
    if(advertisServiceUUIDsStr.includes('FFF0') && advertisServiceUUIDsStr.includes('FFB0')){ // 过滤主服务
      const row = source.find(element => element.deviceId === deviceId)
      if(row) {
        row.RSSI = RSSI
      } else {
        source.push(lists)
      }
      commit('SET_BLELIST', source)
    }
});
uni.stopBluetoothDevicesDiscovery({
  success: e => {
    console.log('停止搜索蓝牙设备:' + e.errMsg);
  },
  fail: e => {
    console.log('停止搜索蓝牙设备失败,错误码:' + e.errCode);
  }
});

3、连接蓝牙设备

使用vuex存储已连接的蓝牙设备uuid,断开连接失败可以尝试重新初始化蓝牙模块~~

store/modules/bluetooth.js

/**
 * 连接低功耗蓝牙
 * @param {Object} device 设备uuid/名称
 */
createBLEConnection({ commit, dispatch }, deviceId){
  return new Promise((resolve, reject) => {
    uni.createBLEConnection({
      // 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
      deviceId,
      timeout: 10000, // 连接超时时间
      success: res => {
        commit('SET_DEVICE', deviceId)
        resolve()
      },
      fail: error => {
        commit('SET_DEVICE', '')
        reject(error)
      }
    });
  })
},

// 断开与低功耗蓝牙设备的连接
closeBLEConnection({ commit, dispatch }){
  return new Promise((resolve, reject) => {
    if(!state.deviceId) {
      resolve()
      return
    }
    uni.closeBLEConnection({
      deviceId: state.deviceId,
      success: res => {
        console.log(state.deviceId + '断开连接')
        commit('SET_DEVICE', '')
        resolve()
      },
      fail: async(error) => {
        await dispatch('closeBluetoothAdapter').catch(() => {})
        await dispatch('openBluetoothAdapter').catch(() => {})
        reject(error)
      }
    });
  })
},

3.1、获取蓝牙设备服务及其特征值

需要获取到蓝牙设备可以读、写、订阅的服务及其对应的特征值,才能向设备发送或接收设备发来的信息

有可能读和订阅都在一个服务里面,也可能分开,一般蓝牙设备文档里面会有说明,如果没有那就只能去试每个服务的特征值是否可读、可写、可订阅咯ヾ§  ̄▽)ゞ2333333

像下面的示例,写服务为FFE5,特征值为FFE9。订阅服务为FFE0,特征值为FFE4。

store/modules/bluetooth.js

/**
 * 获取所有服务
 * @description 12秒内未获取所需的主服务及特征值则返回失败
 */
getBLEDeviceServices({ commit, dispatch }){
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      reject({errCode: 10004})
    }, 12000);
    // 使用定时器获取服务
    // 处理连接设备后获取服务,services列表为空的问题 @see https://ask.dcloud.net.cn/question/66347
    clearInterval(timer)
    const timer = setInterval(() => {
      uni.getBLEDeviceServices({
        // 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
        deviceId: state.deviceId,
        success: async(res) => {
          const { services } = res
          if(services.length){
            clearInterval(timer)
            const writeService = services.find(item => item.uuid.indexOf('FFE5') > -1)
            const notifyService = services.find(item => item.uuid.indexOf('FFE0') > -1)
            commit('SET_WRITESERVICE', writeService.uuid)
            commit('SET_NOTIFYSERVCE', notifyService.uuid)
            await dispatch('getBLEDeviceCharacteristics', { serviceId: writeService.uuid, type: 'write'}).catch(() => {})
            await dispatch('getBLEDeviceCharacteristics', { serviceId: notifyService.uuid, type: 'notify'}).catch(() => {})
            clearTimeout(timeout)
            if(state.writeCharacteristic && state.notifyCharacteristic) {
              resolve()
            } else {
              reject({errCode: 10005})
            }
          }
        },
        fail: error => {
          reject(error)
        }
      });
    }, 200);
  })
},

/**
 * 获取某个服务下的所有特征值
 * @param {String} serviceId 服务的uuid
 * @param {String} type 服务类型
 */
getBLEDeviceCharacteristics({ commit, dispatch }, data){
  const { serviceId, type } = data
  return new Promise((resolve, reject) => {
    uni.getBLEDeviceCharacteristics({
      // 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
      deviceId: state.deviceId,
      // 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
      serviceId,
      success: res => {
        const { characteristics } = res
        if(type === 'write') {
          const writeCharacteristic = characteristics.find(item => item.uuid.indexOf('FFE9') > -1)
          const writeCharacteristicId = writeCharacteristic ? writeCharacteristic.uuid : ''
          commit('SET_WRITECHARACTERISTIC', writeCharacteristicId)
        } else {
          const notifyCharacteristic = characteristics.find(item => item.uuid.indexOf('FFE4') > -1)
          const notifyCharacteristicId = notifyCharacteristic ? notifyCharacteristic.uuid : ''
          commit('SET_NOTIFYCHARACTERISTIC', notifyCharacteristicId)
        }
        resolve()
      },
      fail: error => {
        reject(error)
      }
    });
  })
}

3.2、断线重连机制

连接成功后,就监听低功耗蓝牙连接状态的改变事件了。包括开发者主动连接或断开连接,设备丢失,连接异常断开等等

store/modules/bluetooth.js

onBLEConnectionStateChange({ commit, dispatch }) {
  uni.onBLEConnectionStateChange(res => {
    // 断线重连
    if(!res.connected && res.deviceId === state.deviceId){
      uni.showModal({
        title: '提示',
        content: '连接断开,是否要重连',
        success: async(res) => {
          if (res.confirm) {
            uni.showLoading({
              title: '尝试重连中...',
              mask: true
            })
            try {
              await dispatch('createBLEConnection', state.deviceId)
              uni.showToast({
                title: '重连成功',
                icon: 'none'
              })
            } catch(error) {
              uni.hideLoading()
              dispatch('closeBLEConnection')
            }
          } else if (res.cancel) {
            uni.hideLoading()
            dispatch('closeBLEConnection')
          }
        }
      })
    }
  });
},

4、向蓝牙设备发送

蓝牙最大传输单元默认为20,所以需要把要发送的数据拆成1包20字节去发送,可以尝试使用uni.setBLEMTUapi与设备协商,把传输单元设置大一点,分包就可以传输更多数据。

参考文档:https://uniapp.dcloud.io/api/system/ble?id=setblemtu

utils/bluetooth.js

/**
 * 向蓝牙设备发送数据
 * @param {Hex} cmd 命令
 * @param {String} sn 设备编号
 * @param {Object} data 信息内容
 * @param {Function} callback 回调函数
 */
export function writeBLECharacteristicValue(data , callback){
  return new Promise(async(resolve, reject) => {
    try{
      const msg = JSON.stringify(data)
      if(!store.getters.isNotify){
        await notifyBLECharacteristicValue().catch(error => {
          reject(error)
        })
        await sleep(1000) // 在调用 notifyBLECharacteristicValueChange 成功后立即调用 writeBLECharacteristicValue 接口,在部分机型上会发生 10008 系统错误
      }
      onBLECharacteristicValueChange().then(res => {
        resolve(res)
      })
      sendDataPacket(msg, 0, callback).catch(error => {
        reject(error)
      })
    }catch(e){
      //TODO handle the exception
      reject(e)
    }
  })
}

/**
 * 数据分包发送,每个包20字节
 * @param {Array} data 总的数据
 * @param {Number} index 当前分包数据起始下标
 * @param {Function} callback 回调函数
 */
const sendDataPacket = async(data, index, callback) => {
  const partArray = data.slice(index, index + 20)
  const buffer = new Uint8Array(partArray).buffer;

  return new Promise((resolve, reject) => {
    uni.writeBLECharacteristicValue({
      // 这里的 deviceId 需要在 getBluetoothDevices 或 onBluetoothDeviceFound 接口中获取
      deviceId: store.getters.deviceId,
      // 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
      serviceId: store.getters.writeService,
      // 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
      characteristicId: store.getters.writeCharacteristic,
      // 这里的value是ArrayBuffer类型
      value: buffer,
      success: res => {
        index += 20
        if(typeof callback === 'function'){
          const progress = index / data.length
          callback(progress)
        }
        if(index < data.length){
          sendDataPacket(data, index, callback)
        }else{
          resolve(res)
        }
        console.log('writeBLECharacteristicValue success', res.errMsg)
      },
      fail: error => {
        console.log(error)
        reject(error)
      }
    })
  })
}

/**
 * 字符串转二进制数组
 * @param {String} str 源字符串
 * @return {Array}
 */
export function str2hex(str) {
  const target = []
  for(let i=0; i<str.length; i++){
    target.push(str.charCodeAt(i))
  }
  return target
}

/**
 * 10进制转16进制字符串
 * @param {Number} dec 10进制
 * @param {Number} len 目标字符串长度
 * @return {String}
 */
export function dec2hex(dec, len){
  const hex = (dec).toString(16)
  return (Array(len).join(0) + hex).slice(-len);   // 16进制不及目标长度前导补0
}

5、接收蓝牙设备发来的数据

先启用 notifyBLECharacteristicValueChange 接口才能接收到设备推送的 notification。

如上一节所说,不协商蓝牙最大传输单元,接收到的数据也是1包20字节,所以需要将接收到的数据进行拼包,根据结束标识判断是完整的一条信息

如下示例,结束标识是'\n\r'

utils/bluetooth.js

/**
 * 订阅操作成功后需要设备主动更新特征值的 value,才会触发 uni.onBLECharacteristicValueChange 回调。
 */
notifyBLECharacteristicValue() {
  return new Promise((resolve, reject) => {
    uni.notifyBLECharacteristicValueChange({
      state: true, // 启用 notify 功能
      // 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接
      deviceId: state.deviceId,
      // 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取
      serviceId: state.notifyService,
      // 这里的 characteristicId 需要在 getBLEDeviceCharacteristics 接口中获取
      characteristicId: state.notifyCharacteristic,
      success: res => {
        resolve(res)
      },
      fail: error => {
        reject(error)
      }
    });
  })
}

/**
 * 监听低功耗蓝牙设备的特征值变化事件。必须先启用 notifyBLECharacteristicValueChange 接口才能接收到设备推送的 notification。
 */
const onBLECharacteristicValueChange = function(){
  let partData = ''
  return new Promise((resolve, reject) => {
    // 必须在这里的回调才能获取
    uni.onBLECharacteristicValueChange(characteristic => {
      console.log('监听低功耗蓝牙设备的特征值变化事件成功');
      const { value } = characteristic
      partData += ab2hex(value) + ','

      if(partData.indexOf(',13,10,') > -1){
        resolve(result )
      }
    });
  })
}

/**
 * ArrayBuffer转字符串
 * @param {ArrayBuffer} buffer
 * @return {String}
 */
export function ab2hex(buffer) {
  const dataView = new Uint8Array(buffer)
  const hexArr = dataView.map(item => item)
  return hexArr.join(',')
}

推荐阅读

恰饭区

评论区 (0)

0/500

还没有评论,快来抢第一吧