注意:微信小程序蓝牙API与支付宝小程序蓝牙API有略微不同之处,注意闭坑,本文所讲的是自己开发低功耗蓝牙开锁的功能和全流程。所用技术为Taro,具体用法可以去查Taro官网(Taro:https://taro-docs.jd.com/taro/docs/README)。 有写的不好的或者意见和建议,请在评论区留言 。
蓝牙低功耗是从蓝牙 4.0 起支持的协议,与经典蓝牙相比,功耗极低、传输速度更快,但传输数据量较小。常用在对续航要求较高且只需小数据量传输的各种智能电子产品中,比如智能穿戴设备、智能家电、传感器等,应用场景广泛。
1.【第一步】:需要调用Taro.openBluetoothAdapter(),判断蓝牙是否开启(注意事项⚠️:Taro.openBluetoothAdapter支付宝和微信小程序返回的结果不一样,注意区分判断)
if (Taro.getEnv() === "ALIPAY") {
res = await Taro.openBluetoothAdapter();
} else {
const { errMsg } = await Taro.openBluetoothAdapter();
res = errMsg;
}
//微信小程序蓝牙未打开
if (Taro.getEnv() === "WEAPP" && res !== "openBluetoothAdapter:ok") {
// TODO 没有打开蓝牙,需要提示用户打开
}
//支付宝小程序 error: 12, errorMessage: "蓝牙未打开"
if (Taro.getEnv() === "ALIPAY" && res.error === 12) {
// TODO 没有打开蓝牙,需要提示用户打开
}
2.**【第二步】**开启蓝牙后,需要调用Taro.getLocation()开启地理位置,方便搜索到蓝牙,因android手机不兼容,所以必须要调用定位的方法,以更准确的查到附近的蓝牙设备
//获取当前的地理位置、速度。当用户离开小程序后,此接口无法调用
Taro.getLocation({})
.then(res => {
// TODO 通过后端接口获取你需要的设备信息,一般需要uuid,localKey 等等
})
.catch(err => {
console.log("err", err);
});
3.**【第三步】**通过后端接口获取你需要的设备信息,拿到对应的uuid,localKey,其中,uuid是为了与搜索到的众多的蓝牙设备相匹配
//TODO 调用接口拿到自己设备的信息
const { uuid, localKey } = await getBleLocksInfoData(deviceIds);
const loginKey = localKey.substr(0, 6);
Taro.setStorageSync("loginKey", loginKey);
//TODO 根据接口返回的uuid进行查找附近的设备并进行匹配
searchBle(uuid);
4.**【第四步】**开始搜寻附近的蓝牙外围设备。此操作比较耗费系统资源,请在搜索并连接到设备后调用 wx.stopBluetoothDevicesDiscovery
方法停止搜索。 另外,services:要搜索的蓝牙设备主 service 的 uuid 列表。某些蓝牙设备会广播自己的主 service 的 uuid。如果设置此参数,则只搜索广播包有对应 uuid 的主服务的蓝牙设备。建议主要通过该参数过滤掉周边不需要处理的其他蓝牙设备。
//因为我们的services是["A201"],所以这里直接参数写为["A201"]来查询
const data = await Taro.startBluetoothDevicesDiscovery({
services: ["A201"]
});
//此处,微信小程序和支付宝小程序api返回的data结果不一样,请注意区分
if (data.isDiscovering || data) {
//TODO:搜索蓝牙设备,并匹配UUID
onDiscoveryBLE(uuid)
//另注意:可以多次调用搜索蓝牙,但一定要及时关闭定时器
//多次调用搜索蓝牙
this.delayTimer = setInterval(() => {
this.onDiscoveryBLE(uuid);
}, 1000);
//关闭定时器
this.setTimer = setTimeout(() => {
// 关闭蓝牙
Taro.stopBluetoothDevicesDiscovery();
// 清理定时
clearInterval(this.delayTimer);
}, 15000);
}
5.**【第五步】**调用Taro.getBluetoothDevices获取附近蓝牙设备,并进行筛选匹配过滤(要注意微信和支付宝api返回的结果是不一样的,微信需要转为十六进制,支付宝需要转换为十六进制字符串数组)
// 搜索蓝牙 -- 微信 支付宝搜索蓝牙设备并连接
onDiscoveryBLE = async uuid => {
//获取在蓝牙模块生效期间所有已发现的蓝牙设备。包括已经和本机处于连接状态的设备。
let isALiPay = Taro.getEnv() === "ALIPAY";
let { devices } = await Taro.getBluetoothDevices();
let macAddress = "";
try {
// 过滤一些设备
devices = devices.filter(item => item.name && item.name !== "未知设备");
//这里打印出设备列表,方便查看设备信息
console.log("devices", devices);
// 匹配 uuid 找到对应的设备
if (devices.length) {
for (const item of devices) {
let {
advertisServiceUUIDs,
serviceData,
advertisData,
deviceId
} = item;
if (!advertisServiceUUIDs || !serviceData) return;
if (isALiPay) { //支付宝
//TODO hexStringToArray十六进制字符串转十六进制字符串数组(方法见下文)
serviceData = utils.hexStringToArray(serviceData[advertisServiceUUIDs]);
advertisData = utils.hexStringToArray(advertisData);
} else { //微信
//TODO ab2hex ArrayBuffer转十六进制(方法见下文)
serviceData = utils.ab2hex(serviceData[advertisServiceUUIDs]);
advertisData = utils.ab2hex(advertisData);
}
//TODO 这里需要根据advertisData, serviceData这两个参数解密出uuid,然后与后端接口返回的uuid匹配,
//为ture就表示是自己的设备
const uuids = utils.getUuid(advertisData, serviceData);
if (uuids === uuid) {
console.log("数据有了:", item);
macAddress = deviceId;
Taro.setStorageSync("macAddress", macAddress);
break;
}
}
if (macAddress) {
//TODO 找到自己的设备后,开始建立建立连接(【第六步】)
connectBle();
}
}
} catch (error) {
//TODO 处理失败的逻辑 可给予弹窗提示
}
};
6.**【第六步】**在connectBle()这个方法内处理建立连接逻辑,拿到macAddress后,调用Taro.createBLEConnection()进行连接
//TODO 要注意支付宝和微信的区别
const deviceId = macAddress;
if (Taro.getEnv() === "ALIPAY") {
BLEConnection = my.connectBLEDevice
}else{
BLEConnection = Taro.createBLEConnection
}
BLEConnection({ deviceId })
.then(connect => {
getBLEDeviceServices(connect,deviceId,resolve)
})
.catch(err => {
console.log("connect error", err);
//TODO 可以处理连接失败的操作,具体可以重连等
//思路:先关闭连接,然后再重新初始化蓝牙
Taro.closeBLEConnection({ deviceId })
.then(res => {
console.log("res", res);
initBleConnect();
})
.catch(error => {
console.log("error", error);
initBleConnect();
});
});
const initBleConnect = () => {
Taro.closeBluetoothAdapter()
.then(ddd => {
console.log("ddd", ddd);
// 一秒钟之后再去开启蓝牙
setTimeout(() => {
//初始化蓝牙
Taro.openBluetoothAdapter()
.then(res => {
console.log("蓝牙重启", res);
const { errMsg } = res;
if (errMsg === "openBluetoothAdapter:ok" || res.error !== 12) {
console.log("蓝牙重启 -----===========");
connectBluttoth(macAddress)
.then(connect => resolve(connect))
.catch(err => reject(err));
} else {
reject({ code: 1 });
}
})
.catch(err => {
reject({ code: 1 });
console.log("重启失败err", err);
});
}, 500);
})
.catch(rr => {
console.log("rr", rr);
});
};
7.**【第七步】**在getBLEDeviceServices()处理获取蓝牙低功耗设备所有服务 (service),另外,调用notifyBLECharacteristicValueChange必须先启用notifyBLECharacteristicValueChange 才能监听到设备 characteristicValueChange
事件,所以在页面加载完成后我们需要加载此方法
const getBLEDeviceServices =(connect,deviceId,resolve)=>{
//因为支付宝返回的{},所以在此做了兼容
if (Number(connect.errCode) === 0 || JSON.stringify(connect) === '{}') {
//处理获取蓝牙低功耗设备所有服务 (service)
Taro.getBLEDeviceServices({ deviceId }).then(({ services }) => {
console.log(services);
if (!services.length) {
console.log("未找到服务列表");
return;
}
if (services.length) {
const service = services[0];
// 获取蓝牙低功耗设备某个服务中所有特征 (characteristic)。
Taro.getBLEDeviceCharacteristics({
deviceId,
serviceId: service.uuid || service.serviceId
}).then(({ characteristics }) => {
// 获取设备数据
if (!characteristics.length) {
console.log("未找到设备特征值");
return;
}
characteristics.forEach(item => {
if (item.properties.notify) {
//启用蓝牙低功耗设备特征值变化时的 notify 功能,订阅特征。注意:必须设备的特征支持 notify 或者 indicate 才可以成功调用。
//另外,必须先启用 wx.notifyBLECharacteristicValueChange 才能监听到设备 characteristicValueChange 事件
Taro.notifyBLECharacteristicValueChange({
deviceId,
serviceId: service.uuid || service.serviceId,
characteristicId: item.uuid || item.characteristicId,
state: true
});
} else if (item.properties.write) {
clearTimeout(connectTimer);
resolve({
characteristicIdUuid: item.uuid || item.characteristicId,
serviceUuid: service.uuid || service.serviceId
});
} else {
console.log("该特征值属性: 其他");
}
});
});
}
});
}
}
import aesjs from "aes-js"; //需要引入aes-js
async componentDidMount() {
global["events"].on("onPairResult", this.onPairResult, this);
global["events"].on("openLockClick", this.openLockClick, this);
global["events"].emit("initLockState", 1);
//监听低功耗蓝牙设备的特征值变化事件。
//必须先启用 notifyBLECharacteristicValueChange 接口才能接收到设备推送的 notification。
Taro.onBLECharacteristicValueChange(res => {
// 在开锁中才接受数据
if (this.isOpenLock) {
const { value } = res;
let key = ''
if (Taro.getEnv() === "ALIPAY") {
key = utils.HexString2Bytes(value)
} else {
const str = utils.ab2hex(value).join("");
key = utils.HexString2Bytes(str);
}
console.log('parseDataReceived -- key:' + key);
//开锁的逻辑,需要解析数据 -- 根据自己的业务和需求进行解析
parseDataReceived.call(this, key);
}
});
//监听低功耗蓝牙连接状态的改变事件。包括开发者主动连接或断开连接,设备丢失,连接异常断开等等
if (Taro.getEnv() === "ALIPAY") {
my.onBLEConnectionStateChanged(res => {
// TODO 可以处理失败的弹窗提示等
});
} else {
Taro.onBLEConnectionStateChange(res => {
// 该方法回调中可以用于处理连接意外断开等异常情况
// TODO 可以处理失败的弹窗提示等
});
}
}
8.【工具类】 工具类
/**
* 字符串转 16 进制
* @param str 字符串
*/
function HexString2Bytes(str) {
var pos = 0;
var len = str.length;
if (len % 2 != 0) {
return null;
}
len /= 2;
var arrBytes = new Array();
for (var i = 0; i < len; i++) {
var s = str.substr(pos, 2);
var v = intToByte(parseInt(s, 16));
arrBytes.push(v);
pos += 2;
}
return arrBytes;
}
//十六进制字符串转换为数组
function hexStringToArray(str) {
var pos = 0;
var len = str.length;
if (len % 2 != 0) {
return null;
}
len /= 2;
var arrBytes = new Array();
for (var i = 0; i < len; i++) {
var s = str.substr(pos, 2);
arrBytes.push(s);
pos += 2;
}
return arrBytes;
}
/**
* 把 ArrayBuffer 转换成 16 进制内容
* @param buffer 二进制内容
*/
const ab2hex = buffer => {
const arr = [] as any;
Array.prototype.map.call(new Uint8Array(buffer), function(bit) {
let item = ("00" + bit.toString(16)).slice(-2);
arr.push(item);
});
return arr;
};
//通过advertisData, serviceData这两个参数获取uuid,与自己设备的uuid相匹配
const getUuid = (advertisData, serviceData) => {
serviceData = serviceData.slice(1, serviceData.length);
const pid = serviceData.join("");
const strByte = aesjs.utils.hex.toBytes(pid);
const md5Val = md5.array(strByte);
const uuidDataArr = advertisData.slice(8, advertisData.length);
const uuidDataStr = uuidDataArr.join("");
const encryptUuid = aesjs.utils.hex.toBytes(uuidDataStr);
const aesCbc = new aesjs.ModeOfOperation.cbc(md5Val, md5Val);
const decryptedBytes = aesCbc.decrypt(encryptUuid);
const uuid = aesjs.utils.utf8.fromBytes(decryptedBytes);
return uuid;
};