微信小程序连接低功耗蓝牙总结

3,198 阅读7分钟

前言

最近做完了一个项目(uniapp + vue2--uniapp中的蓝牙api与原生微信小程序api相同), 用微信小程序连接低功耗蓝牙(BLE)实现与硬件设备通信。期间根据微信开发文档进行蓝牙搜索,连接和通信并非一帆风顺,遇到了不少坑,闲下来时做一个总结。


注意:各API使用时,手机兼容问题和api下方要求的基础库版本

安卓

  • 打开蓝牙
  • 打开手机定位(才能搜索到蓝牙)

苹果手机

  • 打开蓝牙
  • 打开手机定位
文件目录结构

蓝牙初始化+搜索:【JR-Bluetooth.js】;

                                                                                JR-Bluetooth.js
export default class JRBluetooth {
    constructor(isIOS) {
        this.isIOS = isIOS; // 是否是iphone
        this.available = false; // 蓝牙是否可用/开启
        this.discovering = false; // 是否正在搜索设备
        this.discoveringTimer = null; // 搜索无设备提示延时器
        this.isDiscovery = false; // 是否搜索到设备
        this.discoveringInterval = 20000; // 搜索时长20s
    }
    // TODO 下面实现的方法
    // ...
}

1、蓝牙初始化和搜索蓝牙设备

初始化成功之后才能监听蓝牙适配器状态变化事件,两个回调函数根据自身项目的实现逻辑来。

/**                                                                            JR-Bluetooth.js
 * 初始化蓝牙模块
 * @param {function} discoveryCallback 搜索到设备后的回调
 * @param {function} stateChangeCallback 状态变化的回调
 */
initBluetooth(discoveryCallback,stateChangeCallback) {
    return new Promise((resolve, reject) => {
        uni.openBluetoothAdapter({
            success: res => {
                console.log('初始化蓝牙模块成功!');
                // 监听蓝牙适配器状态变化事件
                this.onBluetoothStateChange(stateChangeCallback);
                // 开始搜寻附近的蓝牙外围设备
                this.discoverBluetooth(discoveryCallback);
                resolve();
            },
            fail: errMsg => {
                console.log('初始化蓝牙模块失败');
                // 监听蓝牙适配器状态变化事件
                this.onBluetoothStateChange(stateChangeCallback);
                reject();
            }
        })
    })
}

搜索蓝牙设备,上报设备时间间隔设置之后,出现了拿到的设备列表混乱,不建议添加

坑1: 此项目中因为禁止搜索到其它的蓝牙,所以 services 中添加了主服务 UUID,这里UUID 必须写全才能只搜索此类型号的蓝牙设备。

/**                                                                           JR-Bluetooth.js
 * 搜寻蓝牙设备
 * @param {function} cb 搜索到设备后的回调
 */
discoverBluetooth(cb) {
    if (!cb || typeof cb !== 'function') return;
    uni.showLoading({
        title: '蓝牙搜索中'
    });
    const self = this;
    uni.startBluetoothDevicesDiscovery({
        services: [00001812], // 只搜索主服务 UUID 为 00001812 的设备
        allowDuplicatesKey: false, // 不允许重复上报同一设备
        success: () => {
            self.discoveringTimer = setTimeout(() => {
                // TODO
                // 进行超过指定时间还没搜到设备的处理
            }, self.discoveringInterval);
        },
        // interval: 200, // 上报设备的间隔
    })
    // 监听搜索蓝牙设备事件
    self.onBluetoothDeviceFound().then(res => {
            cb && cb(res);
    })
}

下边的方法根据需要增删

/**                                                                             JR-Bluetooth.js
 * 停止搜寻蓝牙设备
 */
stopDiscovery() {
    uni.stopBluetoothDevicesDiscovery();
    clearTimeout(this.discoveringTimer);
}
/**
 * close 本机蓝牙适配器
 */
closeBluetooth() {
    // console.log('关闭蓝牙适配器');
    uni.closeBluetoothAdapter();
    clearTimeout(this.discoveringTimer);
}
/**
 * 卸载本机蓝牙的事件
 */
unloadBluetooth() {
    this.closeBluetooth();
    this.discovering && this.stopDiscovery();
}
/**
 * 获取本机蓝牙适配器状态
 */
getBluetoothState() {
    return new Promise((resolve, reject) => {
        uni.getBluetoothAdapterState({
            success: res => {
                this.updateBluetoothState(res); // 更新蓝牙适配器状态
                resolve(res);
            }
        })
    })
}
/**
 * 监听蓝牙适配器状态变化事件
 * @param {function} cb 处理状态变化的函数
 */
onBluetoothStateChange(cb) {
    uni.onBluetoothAdapterStateChange(res => {
        const { available, discovering } = res;
        this.updateBluetoothState(res); // 更新蓝牙适配器状态
        if (!available && !discovering) { // 蓝牙断开
                this.unloadBluetooth();
        }
        cb && cb(res);
    })
}
/**
 * 更新蓝牙适配器状态
 */
updateBluetoothState(state) {
    // 是否可用 是否处于搜索状态
    // boolean
    const { available, discovering } = state;
    this.available = available;
    this.discovering = discovering;
}

重点:处理搜索到的蓝牙列表

坑2: 大家都知道,iPhone很安全,做过iPhone兼容的小伙伴应该深有体会,由于本项目中需要用到蓝牙mac地址,安卓手机可以直接获取,然而iphone手机只能拿到经过iphone处理后的 uuid,经过测试发现:IOS不同手机连接同一个蓝牙获取到的 UUID 是不一样的 。这里iphone如何得到蓝牙 mac地址呢:与硬件开发伙伴约定,他们将mac放入广播中,如下 小程序通过 解析 device.advertisData 拿到mac地址。

/**                                                                             JR-Bluetooth.js
 * 监听搜索蓝牙设备事件
 * @param {Array} devicesList 设备列表  
 */
onBluetoothDeviceFound(devicesList = []) {
    return new Promise((resolve, reject) => {
        uni.onBluetoothDeviceFound(res => {
            const devices = res.devices;
            const device = devices[0];
            // 筛选有名称的设备
            if (Utils.has(device['name'])) {
                if (this.isIOS) { // 如果是 iphone
                    //重点 根据advertisData 取出mac进行拼接
                    if (device.advertisData) {
                        // 此处0~6只针对本司低功耗蓝牙,各产品可能不同,需要经过测试确定
                        let bf = device.advertisData.slice(0, 6);
                        let mac = Array.prototype.map.call(new Uint8Array(bf), x => ('00' + x.toString(16)).slice(-2)).join(':');
                        mac = mac.toUpperCase();
                        // 保存mac地址
                        device.advertisMacData = mac;
                    }
                }
                devicesList.push(device);
                this.isDiscovery = true;
            }
            // 这里resolve只执行一次,不管监听到多少设备
            resolve(devicesList);
        })
    })
}

蓝牙连接:【JR-BLE.js】 serviceId 用于蓝牙连接成功后 向蓝牙写入(write)和订阅消息(notify).通过下方的 getBLEServices 方法获取

export default class JRBLE {
    constructor(deviceId, serviceId) {
        this.deviceId = deviceId; // 设备id
        this.serviceId = serviceId; // 主服务 uuid
        this.writeCharacteristicId = ""; // write 特征值uuid
        this.notifyCharacteristicId = ""; // notify  特征值uuid
        this.connectionTimeout = 20000; // 连接BLE timeout 时长
    }
    // TODO 下面实现的方法
    // ...
}

2、低功耗蓝牙的连接

重点是获取到特征值(notifyCharacteristicId, writeCharacteristicId)

/**                                                                            JR-BLE.js

 * 处理特征值,获取notifyCharacteristicId和writeCharacteristicId
 * @param {array} characteristics
 * @return {object} [notifyCharacteristicId, writeCharacteristicId] 
 */
handleCharacteristics(characteristics) {
    const len = characteristics.length;
    for (let i = 0; i < len; i++) {
        const characteristic = characteristics[i];
        // 如果根据 serviceId 匹配到 所用的蓝牙产品,直接把特征值写死,因为是唯一的。
        if (this.serviceId == '6E400001-B5A3-F393-E0A9-E50E24DCCA9E' || this.serviceId.toLocaleUpperCase() == '6E400001-B5A3-F393-E0A9-E50E24DCCA9E') {
            this.notifyCharacteristicId = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E'
            this.writeCharacteristicId = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E'
        } else {
            // 获取notifyCharacteristicId
            if (characteristic.properties.notify) {
                 this.notifyCharacteristicId = characteristic.uuid;
            }
            // 获取writeCharacteristicId
            if (characteristic.properties.write) {
                 this.writeCharacteristicId = characteristic.uuid;
            }
        }
    }
    return {
        notifyCharacteristicId: this.notifyCharacteristicId,
        writeCharacteristicId: this.writeCharacteristicId
    }
}

坑3: 注意 getBLEServices() 方法中一定要 延迟一段时间再调用 uni.getBLEDeviceServices() 方法。 为什么呢? 当调用完 createBLEConnection 方法,即BLE初始化成功后,不能立即去获取(即调用uni.getBLEDeviceServices API) BLE设备服务列表, 立即调用在有些手机中会获取服务失败

/**                                                                           JR-BLE.js
/**
 * 连接BLE
 * @param {function} cb 弹框回调
 */
createBLEConnection() {
    return new Promise((resolve, reject) => {
        uni.createBLEConnection({
            deviceId: this.deviceId,
            timeout: this.connectionTimeout,
            success: res => {
                // console.log("res:createBLEConnection " + JSON.stringify(res));
                resolve(res);
            },
            fail: err => {
                console.log(err);
                reject(err)
                // err.errCode == 10003 ? this.createBLEConnection() : reject(err)
            },
        })
    })
}

/**
 * 获取某个型号BLE设备服务表
 * 这里获取到的是一个列表,具体选取哪个serviceId 需要拿到每个serviceId后,
 * 去获取蓝牙设备对应服务中所有特征值(characteristic), 获取到的特征值能成功写入(write)和订阅(notify)
 * 消息, 就用这个serviceId, 此时serviceId 就可以写死到项目中***(只针对同型号的蓝牙).***
 */
getBLEServices() {
    return new Promise((resolve, reject) => {
        // 连接成功之后需要延时,继续操作才不会出问题
        setTimeout(() => {
            uni.getBLEDeviceServices({
                deviceId: this.deviceId,
                success: res => {
                    const serviceList = [];
                    const services = res.services;
                    const len = services.length;
                    for (let i = 0; i < len; i++) {
                        if (services[i].isPrimary) {
                            serviceList.push(services[i]);
                        }
                    }
                    // console.log(serviceList);
                    resolve(serviceList);
                },
                fail: err => {
                    console.log(err);
                    reject(err);
                }
            })
        }, 600)
    })
}

/**
 * 获取蓝牙设备某个服务中所有特征值(characteristic)
 * 
 */
getBLEDeviceCharacteristics() {
    return new Promise((resolve, reject) => {
        uni.getBLEDeviceCharacteristics({
            deviceId: this.deviceId,
            serviceId: this.serviceId,
            success: res => {
                // 处理特征值,获取notifyCharacteristicId和writeCharacteristicId
                const result = this.handleCharacteristics(res.characteristics);
                resolve(result);
            }
        })
    })
}

/**
 * 向低功耗蓝牙设备特征值中写入二进制数据
 * @param {Object} buffer
 */
writeBLECharacteristicValue(buffer) {
    return new Promise((resolve, reject) => {
        uni.writeBLECharacteristicValue({
            deviceId: this.deviceId,
            serviceId: this.serviceId,
            characteristicId: this.writeCharacteristicId,
            value: buffer,
            success: res => {
                resolve(res);
            },
            fail: err => {
                console.log('message发送失败', JSON.stringify(err));
                reject(err);
            }
        });
    });
}

/**
 * 启用低功耗蓝牙设备特征值变化时的 notify 功能,订阅特征值
 */
notifyBLECharacteristicValue() {
    return new Promise((resolve, reject) => {
        uni.notifyBLECharacteristicValueChange({
            state: true, // 启用 notify 功能
            deviceId: this.deviceId,
            serviceId: this.serviceId,
            characteristicId: this.notifyCharacteristicId,
            success: res => {
                resolve(res)
            },
            fail: err => {
                reject(err)
                console.log('notifyBLECharacteristicValueChange failed:' + err.errMsg);
            }
        });
    })
}

/**
 * 接收设备推送的 notification
 * @param {function} cb 处理回调
 */
onBLECharacteristicValueChange(cb) {
    uni.onBLECharacteristicValueChange(function (res) {
      cb && cb(res.value)
    })
}

/**
 * 监听低功耗蓝牙连接状态的改变事件。
 * 包括开发者主动连接或断开连接,设备丢失,连接异常断开等等
 */
onBLEConnectionStateChange() {
    return new Promise(resolve=>{
        uni.onBLEConnectionStateChange(function (res) {
          // 该方法回调中可以用于处理连接意外断开等异常情况
          // console.log(`device ${res.deviceId} state has changed, connected: ${res.connected}`)
          if (!res.connected) {
              resolve(res)
          }
        })
    })

}

/**
 * 断开联链接
 */
closeBLEConnection() {
    uni.closeBLEConnection({
        deviceId: this.deviceId,
        success: res => {
            // console.log(res)
        }
    })
}

调用: 【 test.vue 】。

import BluetoothJR from './JR-Bluetooth.js';
import BLEJR from './JR-BLE.js';
// serviceId 每种蓝牙产品不一样, 这里只是个例子,具体获取方法 JR-BLE.js中有说明
const serviceID = ['55535343-ad7d-2bc5-8fa9-9fafd205e455'];
let JRBluetooth = new BluetoothJR();
// 初始化时的传入的回调函数根据自身需求实现.
JRBluetooth.initBluetooth()
    .then(res => {
        // 初始化成功
        createBLEConnect(deviceId); // 连接BLE
    })
    .catch(() => {
        // 初始化失败
    })
function createBLEConnect(deviceId) {
    JRBLE = new BLEJR(deviceId, serviceId);
    // 停止本机的蓝牙搜索
    JRBluetooth.stopDiscovery();
    // 等待连接BLE设备
    JRBLE.createBLEConnection()
        .then(res => {
            JRBLE.getBLEServices()
                .then(res => {
                    // 处理获取到的 service 列表
                    handleBLEDeviceServices(res);
                    // 监听BLE连接状态
                    JRBLE.onBLEConnectionStateChange().then(res => {
                        // 处理异常
                        // this.handleBLEStateChange(res);
                    });
                })
                .catch(error => {
                    // this.handleBLEConnectError(error, cb);
                });
        })
        .catch(error => {
            // this.handleBLEConnectError(error, cb);
        });
}
 /**
 * 处理BLE services
 * @param {Object} list
 */
function handleBLEDeviceServices(list) {
    if (!Array.isArray(list)) {
        return;
    }
    list.forEach(async item => {
        if (serviceId.indexOf(item.uuid) !== -1 || serviceId.indexOf(item.uuid.toLocaleUpperCase()) !== -1 || serviceId.indexOf(item.uuid.toLocaleLowerCase()) !== -1) {
            // 把当前服务 uuid 更新一下
            JRBLE.serviceId = item.uuid;
            // 获取某个服务特征值
            await JRBLE.getBLEDeviceCharacteristics();
            // 启用 notify
            await JRBLE.notifyBLECharacteristicValue();
            // 监听特征变化 这里接收BLE发来的数据,可传入一个函数(handleReceiveData)去处理
            JRBLE.onBLECharacteristicValueChange(handleReceiveData);
        }
    });
}

结束语

wx小程序连接低功耗蓝牙基本就这些流程, 里边有不少坑, 从初始化到连接到写入数据, 比较麻烦的点是获取 serviceId, 即主服务 service, 还有主服务对应的 write 与 notify 特征值,会耗费一点时间去测试. 下一篇实现一下分包发送.