小程序管理蓝牙设备开发指北
小程序管理蓝牙设备开发记录
前段时间接到一个管理蓝牙设备的需求,要求能搜索并连接指定设备,并读取设备的信息,然后发送指令给设备,让设备运行起来。
允许连接多台同类型的设备,并对设备做分开管理
期间,遇到不少的坑,在此记录下来,希望能对大家有所帮助,有欠缺的地方,还请大家帮忙指正一下,谢谢
话不多说,接下来就进入开发:
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
为了让工具更加完善,需要增加错误处理,异常抛出,错误重试等操作,这里就不在此赘述了