1. 蓝牙工作模式与通信协议
1.1 角色/工作模式
微信官方文档,这个官方的文档一定要看
蓝牙低功耗协议给设备定义了若干角色,或称工作模式,我这里是手机作为中心设备/主机 (Central)
,第三方设备作为外围设备/从机 (Peripheral)
。
- 中心设备扫描外围设备,与外围设备建立连接,使用外围设备提供的服务(Service)
- 外围设备一直处于广播状态,等待中心设备的连接,外围设备不可主动发起搜索
1.2 通信协议
主要的概念:
-
配置文件 (Profile)
,Profile 是被蓝牙标准预先定义的一些 Service 的集合; -
服务 (Service)
,蓝牙设备对外提供的服务,可以使多个。wx.getBLEDeviceServices
api获取; -
特征 (Characteristic)
,每个 Service 包含 0 至多个 Characteristic。wx.getBLEDeviceCharacteristics
api获取; -
描述符 (Descriptor)
,Descriptor 是描述特征值的已定义属性,每个 Descriptor 由一个 UUID 唯一标识。下面代码中的可写特征writeCharacteristicId、可读特征readCharacteristicId、可监听通知特征notifyCharacteristicId,这3个就是
总结:每个蓝牙设备可能提供多个 Service,每个 Service 可能有多个 Characteristic,我们根据蓝牙设备的协议对对应 Characteristic 的值进行读写即可达到与其通信的目的
2. 前置准备
-
手机要打开蓝牙功能;
-
android6.0以上版本,需要在手机上打开位置信息(GPS)功能,否则无法进行设备搜索;
-
小程序隐私协议中要开通蓝牙功能,并且说明使用蓝牙的目的(本地开发不需要,提交审核发版后需要);
-
在隐私协议中开通蓝牙功能后,然后在小程序中才可以使用
wx.openBluetoothAdapter
等api; -
目标设备的蓝牙名称要知道,就和要连接蓝牙耳机一样,不然怎么连接。
3. 部分代码片段
3.1 检查蓝牙是否已打开并授权
可以在页面onLoad、onShow中调用
async function handleCheckBluetooth() {
try {
// 考虑到蓝牙功能可以间接进行定位,安卓 6.0 及以上版本,无定位权限或定位开关未打开时,无法进行设备搜索
const systemInfo = await wx.getSystemInfo();
if(!systemInfo) return Promise.reject();
const {platform, bluetoothEnabled, locationEnabled} = systemInfo;
if(!bluetoothEnabled) {
platform === "android"
? wx.showModal({
title: "提示",
content: "请打开手机蓝牙开关,否则无法搜索蓝牙设备,点击确定,前去打开(部分安卓机如果蓝牙开关已打开,则点击“允许新连接”)",
success: (res) => {
if(res.confirm) {
// 安卓机直接跳转蓝牙设置页
wx.openSystemBluetoothSetting();
} else if(res.cancel) {
wx.showToast({
title: "已取消打开手机蓝牙",
icon: "none"
});
}
}
})
: wx.showModal({
title: "提示",
content: "请打开手机的蓝牙开关,否则无法搜索蓝牙设备",
showCancel: false
});
return Promise.reject();
}
if(platform === "android" && !locationEnabled) {
wx.showModal({
title: "提示",
content: "请打开手机的位置(GPS)开关,否则无法搜索蓝牙设备,请自行到手机系统设置中打开",
showCancel: false
});
return Promise.reject();
}
const settingRes = await wx.getSetting();
if (!settingRes.authSetting['scope.bluetooth']) {
// 一旦用户明确同意或拒绝过授权,其授权关系会记录在后台,直到用户主动删除小程序。
const authorizeRes = await wx.authorize({
scope: 'scope.bluetooth'
});
console.log("authorizeRes:", authorizeRes);
if(authorizeRes.errMsg && authorizeRes.endsWith("ok")) {
return Promise.resolve();
}
} else {
return Promise.resolve();
}
} catch (error) {
console.error(error);
if(error && error.errMsg && error.errMsg.endsWith("deny")) {
wx.showModal({
title: "提示",
content: "当前小程序未授权使用蓝牙,点击确定,前往设置页面允许小程序使用蓝牙",
success: (res) => {
if(res.confirm) {
wx.openSetting();
}
if(res.cancel) {
wx.showToast({
title: "您已拒绝蓝牙授权,无法使用蓝牙功能",
icon: "none"
});
}
}
});
}
return Promise.reject();
}
}
3.2 连接目标蓝牙设备
const targetDevicesName = "xxx"; // 目标蓝牙设备名称
const TARGET_DEVICE_SERVICES_UUID_55535343 = "55535343"; // 目标蓝牙设备某服务uuid的起始部分
let targetDeviceId = ""; // 目标蓝牙设备id
let timer = null; // 连接成功后向目标设备发送心跳包的定时器
let tryReconnectNum = 3; // 连接成功后,蓝牙突然断开,重连次数
let targetDevicesServices = []; // 目标蓝牙设备所有服务 + 特征
function sleep(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
}
3.2.1 蓝牙关闭连接、停止搜索
退出页面,销毁时使用
// 销毁
async function handleDestroy() {
timer && clearTimeout(timer);
timer = null;
wx.stopBluetoothDevicesDiscovery();
if(targetDeviceId) {
wx.closeBLEConnection({
deviceId: targetDeviceId
});
}
wx.closeBluetoothAdapter();
}
3.2.2 初始化蓝牙模块
async function handleOpenBluetoothAdapter() {
try {
const systemInfo = await wx.getSystemInfo();
console.log(systemInfo);
systemInfo.platform === "ios"
? await wx.openBluetoothAdapter({
mode: "central"
})
: await wx.openBluetoothAdapter();
return Promise.resolve();
} catch (error) {
console.error(error);
if(error && error.errMsg === "openBluetoothAdapter:fail already opened") {
return Promise.resolve();
}
return Promise.reject(error);
}
}
3.2.3 扫描发现目标蓝牙设备
// 默认30s内未发现目标蓝牙设备,就停止扫描
function handleSerchTargetDevices() {
let flag = true;
return async (maxWaitTime = 30) => {
if (maxWaitTime <= 0) {
flag = false;
return "";
}
console.log(`蓝牙搜索还剩${maxWaitTime}s`)
if(flag) {
const bluetoothDeviceRes = await wx.getBluetoothDevices();
const bluetoothDeviceList = bluetoothDeviceRes.devices || [];
if(Array.isArray(bluetoothDeviceList) && bluetoothDeviceList.length > 0) {
const r = bluetoothDeviceList.find(item => item.name === targetDevicesName);
if(r) {
// 一旦发现目标设备,就停止扫描
await wx.stopBluetoothDevicesDiscovery();
flag = false;
return r.deviceId;
} else {
await sleep(1000);
maxWaitTime--;
return await handleSerchTargetDevices()(maxWaitTime);
}
} else {
await sleep(1000);
maxWaitTime--;
return await handleSerchTargetDevices()(maxWaitTime);
}
}
}
}
3.2.4 连接目标蓝牙设备
async handleConnectBluetooth() {
try {
await handleDestroy();
await handleOpenBluetoothAdapter();
// 监听蓝牙适配器状态变化事件
wx.onBluetoothAdapterStateChange((adapterStateRes) => {
console.log('adapterState changed, now is', adapterStateRes);
if(!adapterStateRes.available) {
wx.showToast({
title: "蓝牙被关闭,请打开蓝牙",
icon: "none",
duration: 3500,
});
timer && clearTimeout(timer);
}
});
const systemInfo = await wx.getSystemInfo();
const bluetoothAdapterStateRes = await wx.getBluetoothAdapterState();
const f = systemInfo.platform === "devtools"
? (!bluetoothAdapterStateRes.adapterState.discovering && bluetoothAdapterStateRes.adapterState.available)
: (!bluetoothAdapterStateRes.discovering && bluetoothAdapterStateRes.available);
if(f) {
const bluetoothDevicesDiscoveryRes = await wx.startBluetoothDevicesDiscovery(); // 开始搜寻附近的
}
// 扫描发现目标蓝牙设备
const deviceId = await handleSerchTargetDevices()(30);
if(!deviceId) {
wx.showToast({
title: "未搜索到目标设备",
icon: "none"
});
await handleDestroy();
return Promise.reject("未搜索到目标设备");
}
targetDeviceId = deviceId;
// 进行连接
const BLEConnectionRes = await wx.createBLEConnection({
deviceId: targetDeviceId,
});
console.log("BLEConnectionRes:", BLEConnectionRes);
if(!BLEConnectionRes || (BLEConnectionRes && BLEConnectionRes.errCode !== 0)) {
wx.showToast({
title: "蓝牙设备连接失败",
icon: "none"
});
return;
} else {
console.log("蓝牙连接成功");
// 蓝牙断开后重新连接成功
if(this.tryReconnectState === 1) {
tryReconnectNum = 3;
await handleSendConnectOrder(TARGET_DEVICE_SERVICES_UUID_55535343);
}
await handleGetBLEDeviceRSSI();
}
// 监听蓝牙连接状态
wx.onBLEConnectionStateChange(async (res) => {
if(!res.connected) {
if(tryReconnectNum < 0) {
wx.showToast({
title: "蓝牙断开",
icon: "error"
});
tryReconnectNum = 3;
timer && clearTimeout(timer);
wx.offBLEConnectionStateChange();
} else {
tryReconnectNum-=1;
console.warn("蓝牙断开,正在尝试重新连接...");
await handleConnectBluetooth();
}
}
await handleGetBLEDeviceRSSI();
});
} catch (error) {
console.error(error);
return Promise.reject(error);
}
}
3.3 获取目标设备蓝牙信号强度
async function handleGetBLEDeviceRSSI() {
// https://developers.weixin.qq.com/community/develop/doc/0004a223bd8aa0b945aa302fc5b400?highLine=wx.getBLEDeviceRSSI
// 单位是dbm,取值范围是[-127, 126]
if(!!!targetDeviceId) return;
try {
const BLEDeviceRSSIRes = await wx.getBLEDeviceRSSI({
deviceId: targetDeviceId,
});
const deviceRSSI = BLEDeviceRSSIRes.RSSI;
// ...
} catch (error) {
console.error(error);
}
}
3.4 获取目标蓝牙低功耗设备所有服务 (service)
async function handleGetBLEServices() {
const BLEDeviceServicesRes = await wx.getBLEDeviceServices({
deviceId: targetDeviceId,
});
console.log("目标设备所有服务", BLEDeviceServicesRes);
if(Array.isArray(BLEDeviceServicesRes.services)) {
targetDevicesServices = BLEDeviceServicesRes.services.map(item => ({
uuid: item.uuid,
characteristics: [] // 目标服务所有特征,见下面
}));
}
}
3.5 获取服务中的所有特征 (characteristic)
async function handleCharacteristic() {
if(!Array.isArray(targetDevicesServices) || targetDevicesServices.length<= 0) return Promise.resolve();
const iterator = targetDevicesServices[Symbol.iterator]();
const run = async function() {
const {value, done} = iterator.next();
if(done) {
return Promise.resolve();
}
const BLEDeviceServiceCharacteristicsRes = await wx.getBLEDeviceCharacteristics({
deviceId: targetDeviceId,
serviceId: value.uuid
});
const index = targetDevicesServices.findIndex(item => item.uuid === value.uuid);
// 将每个服务下的特征添加到对应的服务下,方便页面中展示
targetDevicesServices.splice(index, 1, {
...targetDevicesServices[index],
characteristics: BLEDeviceServiceCharacteristicsRes.characteristics.map(item => item)
});
await run();
}.bind(this);
return await run();
}
3.6 向目标设备发送指令(根据你的实际业务修改)
这里是可以向目标设备发送操控指令的,且只有目标设备指令解析成功后,才可以下发下一个指令,不能连续发送指令,否则会导致指令解析失败
// 心跳指令
const heartUint8Array = new Uint8Array([0xFD, ..., 0x1E]);
// 下发数据指令
const sendDataUint8Array = new Uint8Array([0xFD, ..., 0xBB]);
// 下发开始指令
const sendDataStartUint8Array = new Uint8Array([0xFD, ..., 0xBA]);
// 下发停止指令
const sendDataStopUint8Array = new Uint8Array([0xFD, ..., 0xB9]);
// 发送连接指令
const sendDataConnectUint8Array = new Uint8Array([0xFC, ..., 0xBE]);
const deviceState = null; // 当前操作的指令
const ORDER_CONF = {
connect: "connect", // 连接
heart: "heart", // 心跳
start: "start", // 开始
stop: "stop", // 停止
data: "data", // 下发数据
}
// ArrayBuffer转16进制字符串示例
function ab2hex(buffer) {
let hexArr = Array.prototype.map.call(new Uint8Array(buffer), function(bit) {
return ('00' + bit.toString(16)).slice(-2);
});
return hexArr.join('');
}
// 10进制转16进制
function num2Hex(num) {
const r = Number(num).toString(16).padStart(2, "0").toUpperCase();
return Number(`0x${r}`);
}
// 16进制异或值计算
function hexXor(arr) {
const crc = arr.reduce((prev, cur) => prev ^ cur, 0);
return crc.toString(16).padStart(2, '0').toUpperCase();
}
import chunk from "lodash/chunk";
async handleSendConnectOrder(TARGET_DEVICE_SERVICES_UUID = TARGET_DEVICE_SERVICES_UUID_55535343) {
// 根据uuid获取目标服务
const obj = targetDevicesServices.find(item => item.uuid.startsWith(TARGET_DEVICE_SERVICES_UUID));
const {uuid: targetServiceUUID, characteristics} = obj;
if(!Array.isArray(characteristics) || (Array.isArray(characteristics) && characteristics.length <= 0)) {
console.log("该服务暂无特征可处理")
return;
}
// 可写特征
const writeCharacteristicId = characteristics.find(({properties}) => properties.write && !properties.read && !properties.notify).uuid;
// 可读特征
const readCharacteristicId = characteristics.find(({properties}) => !properties.write && properties.read).uuid;
// 可监听特征
const notifyCharacteristicId = characteristics.find(({properties}) => !properties.write && (properties.notify || properties.indicate)).uuid;
await wx.notifyBLECharacteristicValueChange({
deviceId: targetDeviceId,
serviceId: targetServiceUUID,
characteristicId: notifyCharacteristicId,
state: true,
success:(res)=> {
console.log("订阅监听特征值:", res.errMsg);
},
});
// 必须在这里的回调才能获取
wx.onBLECharacteristicValueChange(async (res) => {
console.log(`characteristic ${res.characteristicId} has changed, now is:`, ab2hex(res.value));
const val = ab2hex(res.value);
if(res.characteristicId === readCharacteristicId) {
switch(val) {
case "fc xxx 41":
console.log("[连接指令]设备解析成功,准备执行下发数据指令");
await sleep(1500);
// 下发数据指令
await wx.writeBLECharacteristicValue({
deviceId: targetDeviceId,
serviceId: targetServiceUUID,
characteristicId: writeCharacteristicId,
value: sendDataUint8Array.buffer,
success: (res) => {
console.log("[下发数据指令]发送成功:", res);
deviceState = ORDER_CONF.data;
},
fail:(e) => {
console.error("[下发数据指令]发送失败:", e);
}
});
timer && clearTimeout(timer);
timer = setTimeout(() => {
// heartUint8Array
wx.writeBLECharacteristicValue({
deviceId: targetDeviceId,
serviceId: targetServiceUUID,
characteristicId: writeCharacteristicId,
value: heartUint8Array.buffer,
success: (res) => {
console.log("[心跳指令]发送成功:", res);
deviceState = ORDER_CONF.heart;
handleGetBLEDeviceRSSI();
},
fail:(e) => {
console.error("[心跳指令]发送失败:", e);
wx.showToast({
title: "蓝牙连接中断,请检查",
icon: "none"
});
}
});
}, 30 * 1000);
break;
case "fd xxx fe":
switch(deviceState) {
case ORDER_CONF.data:
console.log(`[下发数据指令]设备解析成功`);
// wx.writeBLECharacteristicValue({
// deviceId: targetDeviceId,
// serviceId: targetServiceUUID,
// characteristicId: writeCharacteristicId,
// value: sendDataStartUint8Array.buffer,
// success: (res) => {
// console.log("[下发开始指令]发送成功:", res);
// deviceState = ORDER_CONF.start;
// },
// fail:(e) => {
// console.error("[下发开始指令]发送失败:", e);
// }
// });
break;
case ORDER_CONF.start:
console.log(`[下发开始指令]设备解析成功`);
break;
case ORDER_CONF.stop:
console.log(`[下发停止指令]设备解析成功`);
break;
case ORDER_CONF.heart:
console.log(`[心跳指令]设备解析成功`);
timer && clearTimeout(timer);
timer = setTimeout(() => {
wx.writeBLECharacteristicValue({
deviceId: targetDeviceId,
serviceId: targetServiceUUID,
characteristicId: writeCharacteristicId,
value: heartUint8Array.buffer,
success: (res) => {
console.log("[心跳指令]发送成功:", res);
deviceState = ORDER_CONF.heart;
handleGetBLEDeviceRSSI();
},
fail:(e) => {
console.error("[心跳指令]发送失败:", e);
}
});
}, 30 * 1000);
break;
default:
break;
}
break;
default:
// 检测设备当前返回的是不是历史记录
if(val.startsWith("fd") && val.length > 8) {
// chunk
const arr = chunk(val.split(""), 2);
if(arr[8] && arr[8].join("") === "03") {
console.log("[设备返回历史记录]", val);
// 对设备返回的数据进行解析...
return;
}
}
console.warn(`[${deviceState}]指令设备解析失败`);
// ...
break;
}
}
});
// 读取设备特征值
await wx.readBLECharacteristicValue({
deviceId: targetDeviceId,
serviceId: targetServiceUUID,
characteristicId: readCharacteristicId,
success:(res) => {
console.log('readBLECharacteristicValue==>',res)
},
fail:(err)=> {
console.error('readBLECharacteristicValue err==>',err)
}
});
await sleep(1500);
// 发送连接指令
await wx.writeBLECharacteristicValue({
deviceId: targetDeviceId,
serviceId: targetServiceUUID,
characteristicId: writeCharacteristicId,
value: sendDataConnectUint8Array.buffer,
success: (res) => {
console.log("[连接指令]发送成功:", res);
deviceState = ORDER_CONF.connect;
},
fail:(e) => {
console.error("[连接指令]发送失败:", e);
}
});
}
3.7 初始化调用顺序
async init() {
try {
await handleCheckBluetooth();
await handleConnectBluetooth();
await handleGetBLEServices();
await handleCharacteristic();
await handleSendConnectOrder(TARGET_DEVICE_SERVICES_UUID_55535343);
// await handleSendConnectOrder(TARGET_DEVICE_SERVICES_UUID_55535344);
} catch (error) {
console.error(error);
}
}
4. 参考资料
5. 最后
如果文章对您有帮助,可以关注我的个人公众号半个柠檬2020
,偶尔也会在公众号上面更新一些自己的学习笔记。