文章目录
前言
一、关于蓝牙协议
-
蓝牙协议分为三种模式:
- 高速蓝牙:主要用于数据交换与传输
- 传统蓝牙:以基础信息沟通,设备连接为重点
- 低功耗蓝牙:以不占用过多带宽的设备连接为主。在低功耗模式条件下,Bluetooth 4.0协议以上的蓝牙设备,传输距离可提升到100米以上(BLE)
-
蓝牙服务UUID:
- 通过蓝牙的UUID来标识 蓝牙服务与通讯访问的属性,不同的蓝牙服务和属性使用的是不同的方法,所以在获取到蓝牙服务时需要保持服务一致才能通信
- 蓝牙的read,write,notification特征属性,都有对应的特征服务字段(同样是UUID)。
- 厂商可以自定义蓝牙服务以及特征字段,因此实现蓝牙通信的前提是拿到确定的服务特征值
-
蓝牙广播:
- 蓝牙数据通过广播的形式实现通信,格式如长度+类型+内容,基内容可变,类型固定,长度由内容确定
二、关于微信小程序蓝牙模块API
主要用到API如下:
三、蓝牙业务模块封装
代码如下,篇幅有限进行了部分省略。
3.1 蓝牙基类
/**
* 蓝牙跳绳基类,用于蓝牙相关通信均可继承
*/
import {
inArray, // 展示搜索到的设备列表时去重
uuid2Mac, // 用于统一ios与android设备端展示的deviceId格式为:xx:xx:xx:xx:xx
deviceNameFilter,
utf8to16,
hexToString,
ab2hex,
str2ab,
} from "@/utils/util-BLE.js";
class BLEController {
// 自动关闭定时器
findTimer = null;
// 蓝牙适配器开启状态
static adapterOpend = false;
// 扫描设备状态
startDiscovery = false;
// 蓝牙连接状态
connectStatus = false;
// 蓝牙扫描自动结束时间2min
#timeout = 2 * 60 * 1000;
// 蓝牙通信超时时间5min
#notifyTimeout = 5 * 60 * 1000;
// 蓝牙搜索是否超时
deviceDiscoveryTimeout = false;
// 蓝牙设备ID,注意:ios设置对应deviceId为设备uuid,安卓及开发者工具上连接的蓝牙为mac地址
deviceId;
deviceName;
// 设备mac地址(统一编码处理)
deviceMac;
// 设备列表, [{deviceMac:设备mac地址, deviceId:设备ID,deviceName:设备名称,...}]
deviceList = [];
// 蓝牙服务特征队列
characteristicStack = [];
// 蓝牙消息队列
msgStack = [];
// 蓝牙通信serviceId
serviceId = "";
constructor(context) {
if (context) {
this.deviceId = context.deviceId;
this.deviceMac = context.deviceMac;
this.connectStatus = false;
}
}
/**
* 1.初始化蓝牙模块
*/
openBluetoothAdapter() {
const _this = this;
if (BLEController.adapterOpend) {
console.log("蓝牙适配器已打开,请勿重复操作------》");
return;
}
wx.openBluetoothAdapter({
mode: "central",
success(res) {
BLEController.adapterOpend = true;
console.log("蓝牙适配器打开成功-----------------》");
},
fail(res) {
BLEController.adapterOpend = false;
_this.BLEFail(res);
console.log("蓝牙适配器打开失败-----------------》", res.errMsg);
},
});
}
/**
* 2.扫描蓝牙设备(绑定蓝牙,连接蓝牙通用)
* @param {Array} options.keywords 蓝牙名称筛选关键字
* @param {string} options.deviceId 可选参数,蓝牙设备id,连接用
*/
startBluetoothDevicesDiscovery(options) {
// ---------省略---------------》
if (this.startDiscovery) {
console.log("已开启蓝牙扫描,勿重复开启-----------》");
return;
} else {
this.startDiscovery = true;
wx.startBluetoothDevicesDiscovery({
allowDuplicatesKey: false,
success: (res) => {
this.onBluetoothDeviceFound(options);
console.log("开始扫描蓝牙设备-----------------》");
},
fail: (res) => {
this.startDiscovery = false;
},
});
}
}
/**
* 2.1 监听搜索到新设备
* @param {Array} options.keywords 蓝牙名称筛选关键字,来自startBluetoothDevicesDiscovery调用
* @param {string} options.deviceId
*/
onBluetoothDeviceFound(options) {
let { keywords } = options;
// 超时自动结束
this.findTimer = setTimeout(() => {
clearTimeout(this.findTimer);
if (!this.connectStatus) {
console.log("蓝牙扫描超时,自动关闭任务-------------》");
this.deviceDiscoveryTimeout = true;
this.startDiscovery = false;
wx.stopBluetoothDevicesDiscovery();
}
}, this.#timeout);
// 监听扫描
wx.onBluetoothDeviceFound((res) => {
let devices = res.devices;
devices.forEach((device) => {
if (!device.name && !device.localName) {
return;
}
// 获取设备MAC地址,并根据关键字过滤
let systemInfo = wx.getSystemInfoSync();
let iosDevice = systemInfo.system.toLowerCase().indexOf("ios") > -1;
let deviceMac = iosDevice
? uuid2Mac(device.advertisData)
: device.deviceId;
if (keywords && keywords.length > 0) {
if (deviceNameFilter(device.name, keywords)) {
const foundDevices = this.deviceList;
const idx = inArray(foundDevices, "deviceMac", deviceMac);
let devicesInner = [...this.deviceList];
if (idx === -1) {
device.deviceMac = deviceMac;
devicesInner[foundDevices.length] = device;
this.deviceList = devicesInner;
console.log("发现蓝牙设备并更新列表-----------》", deviceMac);
}
}
}
});
});
}
/**
* 3. 直接连接蓝牙
* @param {string} options.deviceId 蓝牙设备id,连接用
*/
createBLEConnection(options) {
let { deviceId } = options;
this.deviceId = deviceId;
// 如果开启扫描时适配器还没启动,轮询等待
const _this = this;
if (!BLEController.adapterOpend) {
// ---------省略---------------》
}
if (this.connectStatus) {
wx.closeBLEConnection({
deviceId: deviceId,
});
}
let timeout = this.#timeout;
console.log("开始连接蓝牙------------》", deviceId);
this.stopBLEDevicesTask();
wx.createBLEConnection({
deviceId: deviceId,
timeout: timeout,
success: (res) => {
console.log("蓝牙连接成功----------》", deviceId);
this.connectStatus = true;
this.onBLEConnectionStateChange();
this.getBLEDeviceServices();
},
fail: (res) => {
this.connectStatus = false;
console.log("连接失败-------》", res.errMsg);
},
});
}
/**
* 3.1 获取蓝牙低功耗设备所有服务
* @param {string} deviceId 蓝牙设备Id,来自createBLEConnection调用
*/
getBLEDeviceServices() {
wx.getBLEDeviceServices({
deviceId: this.deviceId,
success: (res) => {
/**
* 16 位 UUID 从对接文档中获取(注意都是0000开头,接着的4位数字为16进制的uuid,所有服务只有4位uuid不一样)
* 跳绳主服务 0000xxxx-0000-1000-8000-00805f9b34fb (0xXXXX)
*/
// 注意有多个服务,不同服务的操作不一样,单个服务只能执行单个操作,所以这里需要建立多个连接
for (let i = 0; i < res.services.length; i++) {
// 注意uuid的大小写
if (
res.services[i].isPrimary &&
res.services[i].uuid == "0000xxxx-0000-1000-8000-00805F9B34FB"
) {
this.getBLEDeviceCharacteristics(res.services[i].uuid);
return;
}
}
},
fail: (res) => {
console.log("服务获取失败------------->", res.errMsg);
},
});
}
/**
* 3.2 获取蓝牙低功耗设备某个服务中所有特征 (characteristic)
* @param {string} uuid 服务UUID,来自getBLEDeviceServices调用
*/
getBLEDeviceCharacteristics(serviceId) {
wx.getBLEDeviceCharacteristics({
deviceId: this.deviceId,
serviceId,
success: (res) => {
// 设备特征列表
let characteristics = res.characteristics;
for (let i = 0; i < characteristics.length; i++) {
let item = characteristics[i];
// 该特征是否支持 read 操作
if (item.properties.read) {
wx.readBLECharacteristicValue({
deviceId: this.deviceId,
serviceId,
characteristicId: item.uuid,
});
}
// 该特征是否支持 write 操作
if (item.properties.write) {
this.serviceId = serviceId;
this.characteristicId = item.uuid;
this.onConnectSuccess();
}
// 该特征是否支持 notify ,indicate操作 ,开启监听订阅特征消息
if (item.properties.notify || item.properties.indicate) {
wx.notifyBLECharacteristicValueChange({
deviceId: this.deviceId,
serviceId,
characteristicId: item.uuid,
state: true,
});
this.onBLECharacteristicValueChange();
}
}
},
});
}
/**
* 3.4 监听蓝牙数据
* @param
*/
onBLECharacteristicValueChange() {
wx.onBLECharacteristicValueChange((characteristic) => {
let { characteristicId, value } = characteristic;
const idx = inArray(this.characteristicStack, "uuid", characteristicId);
let formatedValue = hexToString(ab2hex(value));
if (
this.msgStack.indexOf(formatedValue) < 0 ||
this.msgStack.length > 2
) {
this.msgStack.push(formatedValue);
if (formatedValue.indexOf("#") > -1) {
var dataValue = this.msgStack.join("");
this.msgStack = [];
// 消息事件
this.onMsgValueChange(dataValue);
}
}
if (idx === -1) {
this.characteristicStack.push({
uuid: characteristic.characteristicId,
value: value,
});
} else {
this.characteristicStack.splice(idx, 1, {
uuid: characteristic.characteristicId,
value: value,
});
}
});
}
/**
* 3.6 蓝牙状态变化监听
*/
onBLEConnectionStateChange() {
wx.onBLEConnectionStateChange((res) => {
let { deviceId, connected } = res;
this.connectStatus = connected;
console.log("蓝牙状态变化 -------------》", connected, deviceId);
});
}
/**
* 4. 发送蓝牙指令。蓝牙指令超出20字符时需要截断多次发送
* @param {string} cmdStr 蓝牙指令
* @param {string} cmdName 蓝牙指令名称——可选用于打印调试
*/
writeBLECharacteristicValue(cmdStr, cmdName) {
console.log("发送蓝牙指令------------》", cmdStr, cmdName);
var byteLen = cmdStr.length;
var pos = 0;
let loopCount = 0;
// 消息超长分批处理
for (let i = 0; i < byteLen; i += 20) {
let buffer = str2ab(cmdStr.slice(pos, pos + 20));
pos += 20;
loopCount += 1;
let param = {
deviceId: this.deviceId,
serviceId: this.serviceId,
characteristicId: this.characteristicId,
value: buffer,
};
console.log(`cyy:第${loopCount}次发送指令${cmdName}:`, param);
wx.writeBLECharacteristicValue({
...param,
success: function (res) {
console.log("发送指令成功", cmdName);
},
fail: function (res) {
console.warn("发送指令失败", cmdName, res);
},
});
}
}
/**
* 蓝牙错误拦截
*/
BLEFail(res) {
wx.hideLoading();
if (res.errno == 103) {
uni.showModal({
title: "提示",
content: "请先开启蓝牙权限",
showCancel: false,
success: function () {
wx.openSetting();
},
});
} else if (res.errCode === 10001) {
uni.showModal({
title: "提示",
content: "请先打开手机蓝牙!",
showCancel: false,
});
} else {
console.log("cyy: 蓝牙错误---------》", res.errCode);
}
this.errorHandler();
}
/**
* 停止蓝牙通信活动及监听
*/
stopBLEDevicesTask() {
// 停止扫描设备
this.startDiscovery = false;
wx.stopBluetoothDevicesDiscovery();
// 关闭扫描新设备监听
wx.offBluetoothDeviceFound();
// 关闭数据监听
wx.offBLECharacteristicValueChange();
// 移除蓝牙低功耗连接状态改变事件的监听函数
wx.offBLEConnectionStateChange();
}
/**
* 停止所有蓝牙活动
*/
closeBLE() {
// 关闭线程
if (this.findTimer) {
clearTimeout(this.findTimer);
}
// 停止扫描
this.stopBLEDevicesTask();
// 断开连接
if (this.deviceId) {
wx.closeBLEConnection({
deviceId: this.deviceId,
});
this.connectStatus = false;
}
// 关闭适配器
BLEController.adapterOpend = false;
wx.closeBluetoothAdapter();
}
/**
* @override 蓝牙报错后处理
*/
errorHandler() {}
/**
* @override 3.5 消息监听通知——实例化时重写
*/
onMsgValueChange(dataValue) {}
/**
* @override 3.3连接成功处理函数——可通过继承重写
*/
onConnectSuccess() {}
}
export default BLEController;
3.2 工具函数
3.2.1 uuid2Mac 统一安卓与IOS端deviceId展示
- 在安卓设备中,获取到的 deviceId 为设备 MAC 地址,iOS 上则为设备 uuid,因此为了展示一致需要将ios的展示进行输入(当然IOS的连接还是得用获取到的uuid)
export function uuid2Mac(advertisData) {
if (advertisData) {
let bf = advertisData.slice(3, 9);
let mac = Array.prototype.map
.call(new Uint8Array(bf), (x) => ("00" + x.toString(16)).slice(-2))
.join(":");
mac = mac.toUpperCase();
return mac;
}
}
3.2.2 新设备去重
export function inArray(arr, key, val) {
for (let i = 0; i < arr.length; i++) {
if (arr[i][key] === val) {
return i;
}
}
return -1;
}
3.2.3 字符串转ArrayBuffer
因通信数据是以arrayBuffer格式传输的,所以在小程序端向设备端下发数据时,需要进行指令处理
// 将字符串转为 ArrayBuffer
export function str2ab(str) {
var buf = new ArrayBuffer(str.length);
var bufView = new Uint8Array(buf);
for (var i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}
3.2.4 arrayBuffer转换为字符串
- 同样设备向小程序传输的数据也是arrayBuffer类型,所以需要解码。
// ArrayBuffer转16进度字符串
export function ab2hex(buffer) {
var hexArr = Array.prototype.map.call(new Uint8Array(buffer), function (bit) {
return ("00" + bit.toString(16)).slice(-2);
});
return hexArr.join("");
}
// 将16进制转为 字符串
export function hexToString(str) {
var val = "",
len = str.length / 2;
for (var i = 0; i < len; i++) {
val += String.fromCharCode(parseInt(str.substr(i * 2, 2), 16));
}
return utf8to16(val);
}
3.3 实例化应用
- 以蓝牙扫描为例(基于uniapp实现的小程序,所以还是vue2代码)
// 引入
import BLEController from "../controllers/BLE-controller"
export default {
data(){
return {
BLE:null
}
},
created() {
this.BLE = new BLEController()
this.BLE.openBluetoothAdapter()
this.BLE.startBluetoothDevicesDiscovery({keywords:['筛选蓝牙名(自己定)']})
},
onUnload() {
this.BLE && this.BLE.closeBLE()
},
}
- html部分
<div class="device-list" v-if="BLE&&BLE.deviceList.length > 0">
<div class="device-item flex-h-between" v-for="(device, index) in BLE.deviceList" :key="device.id">
<div class="left-info flex-h">
<image src="/static/sub-device/icon-rope.png" mode="widthFix" class="device-icon" />
<div class="device-info flex-v">
<div class="device-name">{{ device.name }}</div>
<div class="device-mac">{{ device.deviceMac }}</div>
</div>
</div>
<div class="right-control">
<button :disabled="bindingId == device.deviceId"
:class="['bind-btn', bindingId == device.deviceId && 'disabled']" :data-key="index" @click="bindThis">
<u-loading-icon mode="semicircle" color="#fff" size="12" v-if="bindingId == device.deviceId"></u-loading-icon>
<text>绑定</text>
</button>
</div>
</div>
</div>