微信小程序 BLE 蓝牙封装

2,228 阅读5分钟

为了简化微信小程序环境下的蓝牙接入流程,经过线上正式项目一年的运行,发现BLE这块API许多坑,且难以移植复用,所以将它封装出来提高可维护性以及可移植性。

具体项目地址:github.com/arsize/ble

如何使用

安装Eventenitter

npm install eventemitter2 --save

引入

在项目根目录utils文件夹下添加如下文件:ble.js、bleHandler.js、tools.js、error.js 完成上面步骤,就可以直接在小程序中使用蓝牙功能了。✨

const emitter = new EventEmitter2();
const ble = new BLE(blename, emitter)

ble.listen(res => {
  if (res.type == 'connect') {
    switch(res.data){
      case "未打开适配器"break
      case "蓝牙已连接"break
      case ""
        break
    }
  }else if (res.type == "response") {
     console.log('收到设备消息响应:', res)
    //TODO
  }
})

ble.init()

实现细节

使用方法如上,很简单,只需要维护一个全局的ble实例,则可以进行蓝牙的各种功能操作。第二部引入的那几个文件是用来干嘛的呢? 大体上将蓝牙的连接、通讯、维护过程按功能的复杂程度分为三层:BLE、BLEHandler、Tool,ble更偏向用户层,blehandler提供一些流程性控制,tool则完全是封装的微信API,隔离一些繁复的工作,使代码看起来简洁一些。

源码解析

BLE(提供面向用户的流程控制):

import BLEHandler from "./bleHandler"

class BLE extends BLEHandler {
    constructor(blename, emitter) {
        super(blename, emitter)
    }
    listen(callback) {
        // 蓝牙事件注册,打开channel
        this.emitter.removeAllListeners("channel")
        this.emitter.on("channel", callback)
    }
    removeListen() {
        // 移除所有蓝牙事件
        this.emitter.removeAllListeners("channel")
    }
    async init() {
        let flow = false
        // 打开蓝牙适配器状态监听
        this.onBLEConnectionStateChange()
        // 蓝牙适配器初始化
        await this.openAdapter()
        // 搜索蓝牙设备
        await this.startSearch()
        // 获取设备ID
        flow = await this.onBluetoothFound()
        // 停止搜索设备
        await this.stopSearchBluetooth()
        if (!flow) return
        // 连接蓝牙
        await this.connectBlue();
        // 获取serviceId
        await this.getBLEServices()
        // 设置特征值
        await this.getCharacteristics();
        // 订阅特征值
        await this.notifyBLECharacteristicValueChange()
        // 打开传输监听,等待设备反馈数据
        this.onBLECharacteristicValueChange()
    }
    // 发送指令
    async send(mudata, cmd) {
        let flow = await this.sentOrder(mudata, cmd)
        return flow
    }
    async close() {
        await this.closeBLEConnection()
        await this.closeBLEAdapter()
    }

}

export { BLE };

BLEHandler(promise的封装,及Eventenitter通信控制)

import * as t from "./tools"
import { HTTP } from "../server";

/**
 * 蓝牙工具类
 * 封装小程序蓝牙流程方法
 * 处理事件通信
 */
class BLEHandler {
    constructor(blename, emitter) {
        this.blename = blename
        this.emitter = emitter
        this.readCharacteristicId = "";
        this.writeCharacteristicId = "";
        this.notifyCharacteristicId = "";
        this.deviceId = "";
        this.serviceId = "";
        this.lastDate = new Date().getTime()
    }
    async openAdapter() {
        let [err, res] = await t._openAdapter.call(this);
        if (err != null) {
            this.emitter.emit("channel", {
                type: "connect",
                data: "未打开适配器"
            })
            return;
        }
        return true
    }
    async startSearch() {
        let [err, res] = await t._startSearch.call(this);
        if (err != null) {
            return;
        }
        this.emitter.emit("channel", {
            type: "connect",
            data: "蓝牙搜索中"
        })

    }
    async onBluetoothFound() {
        let [err, res] = await t._onBluetoothFound.call(this);
        if (err != null) {
            this.emitter.emit("channel", {
                type: "connect",
                data: "未找到设备"
            })
            // 取消适配器
            this.closeBLEAdapter()
            wx.setStorageSync("bluestatus", "");
            return;
        }
        this.emitter.emit("channel", {
            type: "connect",
            data: "正在连接中"
        })
        return true
    }
    async stopSearchBluetooth() {
        let [err, res] = await t._stopSearchBluetooth.call(this);
        if (err != null) {
            return;
        }
    }
    async connectBlue() {
        let [err, res] = await t._connectBlue.call(this);
        if (err != null) {
            return;
        }
    }
    async getBLEServices() {
        let [err, res] = await t._getBLEServices.call(this);
        if (err != null) {
            return;
        }
    }
    async getCharacteristics() {
        let [err, res] = await t._getCharacteristics.call(this);
        if (err != null) {
            this.emitter.emit("channel", {
                type: "connect",
                data: "无法订阅特征值"
            })
            // 取消连接
            this.closeBLEConnection()
            this.closeBLEAdapter()
            wx.setStorageSync("bluestatus", "");
            return;
        }
        return true
    }
    async notifyBLECharacteristicValueChange() {
        let [err, res] = await t._notifyBLECharacteristicValueChange.call(this);
        if (err != null) {
            // 取消连接
            this.emitter.emit("channel", {
                type: "connect",
                data: "无法订阅特征值"
            })
            this.closeBLEConnection()
            this.closeBLEAdapter()
            wx.setStorageSync("bluestatus", "");
            return;
        }
        this.emitter.emit("channel", {
            type: "connect",
            data: "蓝牙已连接"
        })
        wx.setStorageSync("bluestatus", "on");
        return true
    }
    async closeBLEConnection() {
        let [err, res] = await t._closeBLEConnection.call(this);
        if (err != null) {
            return;
        }
    }
    async closeBLEAdapter() {
        let [err, res] = await t._closeBLEAdapter.call(this);
        if (err != null) {
            return;
        }
    }
    async sentOrder(mudata, cmd) {
        let data = t._sentOrder(mudata, cmd)
        console.log("-- 发送数据:", data)
        let arrayBuffer = new Uint8Array(data).buffer;
        let [err, res] = await t._writeBLECharacteristicValue.call(this, arrayBuffer)
        if (err != null) {
            return
        }
        return true

    }

    // 打开蓝牙适配器状态监听
    onBLEConnectionStateChange() {
        wx.onBLEConnectionStateChange(res => {
            // 该方法回调中可以用于处理连接意外断开等异常情况
            if (!res.connected) {
                this.closeBLEAdapter()
                wx.setStorageSync("bluestatus", "");
                this.emitter.emit("channel", {
                    type: "connect",
                    data: "蓝牙已断开"
                })
            }
        }, err => {
            console.log('err', err)
        })
    }

    // 收到设备推送的notification
    onBLECharacteristicValueChange() {
        wx.onBLECharacteristicValueChange(res => {
            let arrbf = new Uint8Array(res.value)
            console.log("收到上传数据:",arrbf)
            console.log("时间戳",new Date().getTime())
            arrbf.map(res=>{
                console.log(res)
            })
            if (this._checkData(arrbf)) {
                if (arrbf[3] != 0x00) {
                    let nowDate = new Date().getTime()
                    if ((nowDate - this.lastDate) > 900) {
                        console.log('-- 节流900ms,Lock!')
                        this.lastDate = nowDate
                        this._uploadInfo(arrbf)
                        this.emitter.emit("channel", {
                            type: "response",
                            data: arrbf
                        })
                    }
                }
            }
        })
    }
    _uploadInfo(message) {
        console.log("-- 准备数据同步!", this._mapToArray(message))
        let bleorder = wx.getStorageSync("bleorder");
        let blecabinet = wx.getStorageSync("blecabinet")
        HTTP({
            url: "cabinet/uploadBlueData",
            methods: "post",
            data: {
                cabinetQrCode: blecabinet,
                order: bleorder,
                message: this._mapToArray(message)
            }
        }).then(res => {
            console.log("✔ 数据同步成功!")

        }, err => {
            console.log('✘ 数据同步失败', err)
        })
    }
    _mapToArray(arrbf) {
        let arr = []
        arrbf.map(item => {
            arr.push(item)
        })
        return arr
    }
    // 校验数据正确性
    _checkData(arrbf) {
        // 校验帧头帧尾
        if (arrbf[0] != 0xEE || arrbf[1] != 0xFA || arrbf[arrbf.length - 1] != 0xFF || arrbf[arrbf.length - 2] != 0xFC) {
            console.log('✘ 帧头帧尾不匹配,请重发')
            console.log('帧头:', arrbf[0])
            console.log('帧头:', arrbf[1])
            console.log('帧尾:', arrbf[arrbf.length - 1])
            console.log('帧尾:', arrbf[arrbf.length - 2])
            return false
        }
        // 校验CRC
        let crc = t._modBusCRC16(arrbf, 2, arrbf.length - 5)
        if (arrbf[arrbf.length - 3] != crc & 0xff && arrbf[arrbf.length - 4] != (crc >> 8) & 0xff) {
            console.log('✘ crc校验错误,请重发')
            return false
        }
        let time = new Date().toLocaleTimeString()
        console.log(`✔ CRC数据校验成功!${arrbf[3] == 0 ? '❤' : '命令码:' + arrbf[3]},time:${time}`)
        return true
    }

}
export default BLEHandler

Tools(对微信蓝牙API的封装,改造,以及一些偏底层接口的封装)

import errToString from "./error";

let PRINT_SHOW = true //是否开启蓝牙调试

function _openAdapter() {
    print(`—————— —————— ——————`);
    print(`准备初始化蓝牙适配器...`);
    return wx.openBluetoothAdapter().then(
        (res) => {
            print(`✔ 适配器初始化成功!`);
            return [null, res]
        },
        (err) => {
            print(`✘ 初始化失败!${errToString(err)}`);
            return [errToString(err), null]
        }
    );
}

/**
 * @param {Array<string>} services
 * @param { Int } interval
 */
function _startSearch() {
    print(`准备搜寻附近的蓝牙外围设备...`);
    return promisify(wx.startBluetoothDevicesDiscovery, {
        interval: 1000
    }).then(
        (res) => {
            print(`✔ 搜索成功!`);
            return [null, res]

        },
        (err) => {
            print(`✘ 搜索蓝牙设备失败!${errToString(err)}`);
            return [errToString(err), null]
        }
    );
}

/**
 *@param {Array<string>} devices
 *@deviceId 设备ID
 */
function _onBluetoothFound() {
    print(`监听搜寻新设备事件...`);
    return _onBluetoothFound_promise.call(this).then(res => {
        print(`✔ 设备ID找到成功!`);
        return [null, res]
    }, err => {
        print(`✘ 设备ID找到失败!`);
        return [errToString(err), null]

    })
}

/**
 * @param {Array} devices 查找到设备数组
 * @param {int} count 计数器-嗅探2次
 */
function _onBluetoothFound_promise() {
    let devices = []
    let count = 0
    print(`blename:${this.blename}`)
    return new Promise((resolve, reject) => {
        wx.onBluetoothDeviceFound(res => {
            devices.push(...res.devices)
            count++
            if (count > 1) {
                devices.forEach(element => {
                    if ((element.name && element.name == this.blename) || (element.localName && element.localName == this.blename)) {
                        this.deviceId = element.deviceId
                        resolve(res)
                    }
                });
                reject('device not found')
            }
            print(`已嗅探蓝牙设备数:${devices.length}...`)
        }, err => {
            reject(err)
        })
    })
}

function _stopSearchBluetooth() {
    print(`停止查找新设备...`);
    return wx.stopBluetoothDevicesDiscovery().then(
        (res) => {
            print(`✔ 停止查找设备成功!`);
            return [null, res]
        },
        (err) => {
            print(`✘ 停止查询设备失败!${errToString(err)}`);
            return [errToString(err), null]
        }
    );
}

function _connectBlue() {
    print(`准备连接设备...`);
    return promisify(wx.createBLEConnection, {
        deviceId: this.deviceId,
    }).then(
        (res) => {
            print(`✔ 连接蓝牙成功!`);
            return [null, res]
        },
        (err) => {
            print(`✘ 连接蓝牙失败!${errToString(err)}`);
            return [errToString(err), null]
        }
    );
}

function _closeBLEConnection() {
    print(`断开蓝牙连接...`)
    return promisify(wx.closeBLEConnection, {
        deviceId: this.deviceId,
    }).then(
        (res) => {
            print(`✔ 断开蓝牙成功!`);
            return [null, res]
        },
        (err) => {
            print(`✘ 断开蓝牙连接失败!${errToString(err)}`);
            return [errToString(err), null]
        }
    );
}

function _closeBLEAdapter() {
    print(`释放蓝牙适配器...`)
    return wx.closeBluetoothAdapter().then(res => {
        print(`✔ 释放适配器成功!`)
        return [null, res]
    }, err => {
        print(`✘ 释放适配器失败!${errToString(err)}`)
        return [errToString(err), null]
    })
}

function _getBLEServices() {
    print(`获取蓝牙设备所有服务...`)
    return promisify(wx.getBLEDeviceServices, {
        deviceId: this.deviceId
    }).then(res => {
        print(`✔ 获取service成功!`)
        return [null, res]
    }, err => {
        print(`✘ 获取service失败!${errToString(err)}`)
        return [errToString(err), null]
    })
}

function _getCharacteristics() {
    print(`开始获取特征值...`);
    return promisify(wx.getBLEDeviceCharacteristics, {
        deviceId: this.deviceId,
        serviceId: this.serviceId,
    }).then(
        (res) => {
            print(`✔ 获取特征值成功!`);
            for (let i = 0; i < res.characteristics.length; i++) {
                let item = res.characteristics[i];
                if (item.properties.read) {
                    this.readCharacteristicId = item.uuid;
                }
                if (item.properties.write && !item.properties.read) {
                    this.writeCharacteristicId = item.uuid;
                }
                if (item.properties.notify || item.properties.indicate) {
                    this.notifyCharacteristicId = item.uuid;
                }
            }
            return [null, res]
        },
        (err) => {
            print(`✘ 获取特征值失败!${errToString(err)}`);
            return [errToString(err), null]
        }
    );
}

// 订阅特征值
function _notifyBLECharacteristicValueChange() {
    return promisify(wx.notifyBLECharacteristicValueChange, {
        deviceId: this.deviceId,
        serviceId: this.serviceId,
        characteristicId: this.notifyCharacteristicId,
        state: true
    }).then(res => {
        print(`✔ 订阅notify成功!`)
        return [null, res]
    }, err => {
        print(`✘ 订阅notify失败!${errToString(err)}`)
        return [errToString(err), null]
    })
}

/**
 * 指令封装
 * @param {Array} mudata 
 */
function _sentOrder(mudata, cmd) {
    print(`开始封装指令...`)
    let uarr = new Array(mudata.length + 8)
    uarr[0] = 0xEE //帧头
    uarr[1] = 0xFA //帧头
    uarr[2] = mudata.length + 1
    uarr[3] = cmd //命令码
    mudata.map((item, index) => {
        uarr[index + 4] = item
    })
    let crc = _modBusCRC16(uarr, 2, mudata.length + 3)
    uarr[uarr.length - 4] = (crc >> 8) & 0xff
    uarr[uarr.length - 3] = crc & 0xff
    uarr[uarr.length - 2] = 0xFC //帧尾
    uarr[uarr.length - 1] = 0xFF //帧尾
    print(`✔ 封装成功!`)
    return uarr
}

// CRC16 校验算法
function _modBusCRC16(data, startIdx, endIdx) {
    var crc = 0xffff;
    do {
        if (endIdx <= startIdx) {
            break;
        }
        if (data.length <= endIdx) {
            break;
        }
        for (var i = startIdx; i <= endIdx; i++) {
            var byte = data[i] & 0xffff;
            for (var j = 0; j < 8; j++) {
                crc = (byte ^ crc) & 0x01 ? (crc >> 1) ^ 0xa001 : crc >> 1;
                byte >>= 1;
            }
        }
    } while (0);
    return ((crc << 8) | (crc >> 8)) & 0xffff;
}

function _writeBLECharacteristicValue(mudata) {
    return promisify(wx.writeBLECharacteristicValue, {
        deviceId: this.deviceId,
        serviceId: this.serviceId,
        characteristicId: this.writeCharacteristicId,
        value: mudata,
    }).then(res => {
        print(`✔ 写入数据成功!`)
        return [null, res]
    }, err => {
        print(`✘ 写入数据失败!${errToString(err)}`)
        return [errToString(err), null]
    })
}

/**
 * 对微信接口的promise封装
 * @param {function} fn 
 * @param {object} args 
 */
function promisify(fn, args) {
    return new Promise((resolve, reject) => {
        fn({
            ...(args || {}),
            success: (res) => resolve(res),
            fail: (err) => reject(err),
        });
    });
}

/**
 * 对微信接口回调函数的封装
 * @param {function} fn 
 */
function promisify_callback(fn) {
    return new Promise((resolve, reject) => {
        fn(
            (res) => {
                resolve(res);
            },
            (rej) => {
                reject(rej);
            }
        );
    });
}

function print(str) {
    PRINT_SHOW ? console.log(str) : null;
}

export {
    print,
    _getCharacteristics,
    _connectBlue,
    _getBLEServices,
    _closeBLEConnection,
    _closeBLEAdapter,
    _stopSearchBluetooth,
    _notifyBLECharacteristicValueChange,
    _onBluetoothFound,
    _startSearch,
    _openAdapter,
    _sentOrder,
    _writeBLECharacteristicValue,
    _modBusCRC16,
    promisify,
    promisify_callback,
};