前言
最近做完了一个项目(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 特征值,会耗费一点时间去测试. 下一篇实现一下分包发送.