前言
最近做完了一个项目(uniapp + vue2--uniapp中的蓝牙api与原生微信小程序api相同), 用微信小程序连接低功耗蓝牙(BLE)实现与硬件设备通信。期间根据微信开发文档进行蓝牙搜索,连接和通信并非一帆风顺,遇到了不少坑,闲下来时做一个总结。附:【微信开发者文档】。
安卓和苹果手机连接蓝牙的条件:
安卓
- 打开蓝牙
- 打开手机定位(才能搜索到蓝牙)
苹果手机
- 打开蓝牙
- 打开手机定位
实现连接及数据传输的步骤
- 蓝牙初始化:打开蓝牙模块
- 搜索:检测附近的蓝牙设备
- 连接:主动连接找到的目标设备
- 监听:开启监听,接收设备数据
- 发送数据:向目标设备发送数据
蓝牙初始化+搜索 【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、蓝牙初始化
首先,使用uni.openBluetoothAdapter进行蓝牙初始化初始化成功之后才能监听蓝牙适配器状态(传入一个回调函数,比如:进行断开连接后重连操作),初始化成功后搜寻附近的设备。
/** 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();
}
})
})
}
2、搜索设备
开启搜索uni.startBluetoothDevicesDiscovery,监听搜索到的设备uni.onBluetoothDeviceFound
- 如果只想搜索某个型号的蓝牙设备,可以在
services中添加主服务 UUID,这里UUID 必须写全才能只搜索此类型号的蓝牙设备。当然是在已经提前拿到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;
}
重点:处理搜索到的蓝牙列表
坑1: 大家都知道,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】
搜索到蓝牙设备后,接下来连接蓝牙设备
deviceId用于连接蓝牙
serviceId用于蓝牙连接成功后,获取写入(write)和订阅消息(notify)的特征值
JR-BLE.js
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 下面实现的方法
// ...
}
3、连接低功耗蓝牙
调用 uni.createBLEConnection进行连接
/** 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)
},
})
})
}
连接成功后,就可以通过uni.getBLEDeviceServices去获取蓝牙设备的服务列表了。
| 类型 | 说明 | |
|---|---|---|
| uuid | string | 蓝牙设备服务的 UUID |
| isPrimary | boolean | 该服务是否为主服务 |
服务项 Object 有两个参数
uuid第2步中提到过,用于搜索设备的 主服务 UUID 就是这个东东isPrimary一般选择主服务(true)- 注意 这里为什么要用
setTimeout,是因为连接成功之后需要延时,才能成功获取到服务。- 踩坑传送
- 华为手机鸿蒙安卓版无法获取服务的解决方案
- 针对华为手机,是将 setTimeout 的延迟时间改为 1.5 秒,也可以用上述链接中的解决方案
/** JR-BLE.js
* 获取某个型号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);
}
})
}, 1500)
})
}
我们会拿到多个主服务,如下图:
拿到了蓝牙模块的服务列表之后,该怎么做呢,接下来的工作就很反人类了。
- 我们接下来的操作,是实现数据收发的前提:
- 找到有效的服务A
- 找到服务A中有效的 notify 特征值
notifyCharacteristicId - 找到服务A中有效的 write 特征值
writeCharacteristicId
拿到服务列表的操作,具体代码见 test.vue 中的 handleBLEDeviceServices方法。
- 首先,遍历
serviceList中的每一项,代表一个服务;我们需要根据服务A中的uuid,调用uni.getBLEDeviceCharacteristics获取蓝牙低功耗设备服务A中所有特征 (characteristic)。 也就是下边这样子:
/**
* 获取蓝牙设备某个服务中所有特征值(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);
}
})
})
}
特征值格式:
2. 获取到所有特征值后
重点是拿到其中的订阅特征值
notifyCharacteristicId和写入特征值writeCharacteristicId
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];
// 获取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
}
}
拿到可用服务的两个特征值之后,我们就可以发送指令(接收和发送数据)了
- 首先,必须先调用 wx.notifyBLECharacteristicValueChange接口才能接收到设备推送的 notification。
- 在这里,我们就用到了前面拿到的订阅特征值id
notifyCharacteristicId - 订阅成功后,调用
uni.onBLECharacteristicValueChange就能接收到设备推送的数据了。
/** JR-BLE.js
* 启用低功耗蓝牙设备特征值变化时的 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)
})
}
如下图,可以看一下接收到的数据格式:
- 下发数据:用到了前面拿到的写入特征值id
writeCharacteristicId - 订阅成功后,调用
uni.writeBLECharacteristicValue就能发送数据了。
/** JR-BLE.js
* 向低功耗蓝牙设备特征值中写入二进制数据
* @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);
}
});
});
}
如下图,可以看一下发送的数据格式:
其它异常处理函数
/** JR-BLE.js
* 监听低功耗蓝牙连接状态的改变事件。
* 包括开发者主动连接或断开连接,设备丢失,连接异常断开等等
*/
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);
}
});
}
关于数据
ArrayBuffer数据DataView数据- 一些
16进制数据处理的方法
结束语
wx小程序连接低功耗蓝牙基本就这些流程, 里边有不少坑, 从初始化到连接到写入数据, 最开始比较麻烦的点是获取 serviceId, 即主服务 service, 还有主服务对应的 write 与 notify 特征值,会耗费一点时间去测试. 后来才知道,找主服务id,可以一开始就问清楚嵌入式开发的同学他要用哪个主服务uuid,就会节省很多时间。 下一篇实现一下分包发送.