由于公司业务做的是物联网智能充电柜,刚好最近的需求涉及到调用小程序蓝牙功能连接获取电池信息。鉴于网上大部分资料都是基于蓝牙多个特征值来实现,本文案例基于一个特征值通过写入不同的指令来实现不同数据的读取,于是就出现了这篇文章。
一、业务需求
点击电池按钮进入电池信息页面,点击底部的按钮获取附近的蓝牙设备列表,选择匹配公司业务的蓝牙进行连接,连接成功返回对应的蓝牙信息数据;包括电池名称、电池电压等,这些数据会定时刷新,类似websocket的即时通讯功能。
二、硬件交互
这边硬件给我们反馈的信息比较特殊,整个蓝牙模块只有一个特征值,通过写入不同的指令区分数据类型;比如特征值AAAA,获取电池配置的写入指令为85,获取电池信息的指令为90。而不是一个特征值对应一个数据类型,如特征值BBBB,通过读取指令返回电池配置,特征值CCCC返回电池信息。
蓝牙返回的信息数据是一串二进制的字节,超过20字节会被拆包发送。返回的全部参数都拼接在一起,需要前端进行切割。如0080000060je0000015467978,最前面两个字节00代表电池号。
一般为了数据安全起见,不会开启读取的权限。前端一般不会通过read()方法来读取数据,而是通过write()写入指令的方法来实现。当然了,这少不了加密,这边用的是crc-8计算规则,由于网上找不到对应方法,因此通过接口来实现。
三、蓝牙硬件协议文档
emmm,这份协议文档对我前端来说,真的看不懂。后来经过和我们技术大佬沟通了N遍才明白了。
注:一个字节2个字符串
四、小程序蓝牙交互流程
- 初始化蓝牙
- 判断本机蓝牙适配器是否可用
- 开启蓝牙搜索
- 开始搜索周边蓝牙设备
- 连接蓝牙
- 获取蓝牙对应的服务列表
- 获取对应服务列表下的所有特征值(该蓝牙是否可读写可监听消息推送等信息)
- 开启消息监听
- 对蓝牙进行读写操作(写入数据需要把json格式转成2进制格式,再分包发送)
(因系统与蓝牙单次传输只能发送20个字节, 因此大于20个字节时需要手动分包发送。)
五、涉及知识点
- 十六进制转十进制
- 十六进制转二进制
- arraybuffer转十六进制
- 字符串转arraybuffer
- crc-8/itu校验
- 拆包
- 拼包
- 大端转小端(十六进制高低位置换)
- 开尔文(k)温度转摄氏温度(°c)
六、重点代码
// 监听寻找到新设备的事件
onBluetoothDeviceFound() {
uni.onBluetoothDeviceFound((res) => {
console.log("监听寻找到新设备的事件", res, res.devices);
//不重复 就添加到bluetoothList中
if (
!this.bluetoothList.some((item) => {
return item.deviceId === res.devices[0].deviceId;
})
) {
// this.bluetoothList.push(res.devices[0])
// 蓝牙前缀是:LSBMS_ 过滤掉即可
res.devices.forEach((item) => {
// 过滤为空的设备 mac没有localName这个字断
if ((item.name || item.localName).indexOf("LSBMS_") > -1) {
// this.bluetoothList.push(item);
console.log('item-LSBMS_', item, this.batteryName)
// 匹配已经连接的电池
if (item.name == this.batteryName) {
// 在个人中心点进来会携带一个当前租用的电池 列表中默认选中
this.bluetoothList.push({
...item,
isCheck: true
});
} else {
this.bluetoothList.push({
...item,
isCheck: false
});
}
}
});
}
});
},
// 开启消息监听 前提必须拿到设备ID、服务ID、特征值
notify() {
let params = {
deviceId: this.deviceId,
serviceId: this.bluetoothParams.serviceId,
characteristicId: this.bluetoothParams.characteristicId
};
console.log("params: ", params);
uni.notifyBLECharacteristicValueChange({
...params,
state: true, // 关键参数
success: (res) => {
console.log("res: ", res);
},
fail: (err) => {
console.error(err);
uni.hideLoading();
},
});
},
// 接受消息 只有推送了才会响应
// 拆包接收这边硬件给过来的参数是以aaaaaa6个a作为结尾 一次数据
listenValueChange() {
console.log("----");
uni.onBLECharacteristicValueChange((res) => {
console.log("res: 接收消息", res);
// ArrayBuffer类型
let resHex = this.ab2hex(res.value);
console.log('resHex-----------', resHex);
// 以aaaaaa6个a作为结尾 一次数据 再进行监听
if (resHex.indexOf('aaaaaa') != -1) {
this.str = this.str + resHex
this.resultStr = this.str
this.str = ""
} else {
this.str = this.str + resHex
}
// 这里才算读取蓝牙数据结束 如果没有数据会一直转圈加载中
uni.hideLoading();
});
},
async write() {
// let msg = '028555D8AAAAAA'
// 获取电池包状态 msgid(00-255)、cmd-type为80、playload为55、crc通过前面三个参数加密而成、endTag终止符AAAAAA
// let msgId = "00";
// let cmd = 80
// let playload = 55
this.str = ""
this.resultStr = ""
if (this.msgId == 255) {
this.msgId = "00"
}
// crc8校验网上找不到 因此调后端接口
let resultCrc = await this.$http.get('account/account/get-crc', {
packet: this.msgId + this.cmd + this.playload,
}).then((response) => {
console.log('response--------urc8', response)
return response.crc8_itu
});
// let endTage = 'AAAAAA'
let hex = this.msgId + this.cmd + this.playload + resultCrc + this.endTage
// 累加 不能大于255
let id = Number(this.msgId) + 1
this.msgId = id < 9 ? "0" + id : id
console.log('this.msgId累加后', this.msgId)
console.log('hex--------------------', hex)
// let hex = '01805524AAAAAA'
let arrayBuffer = new Uint8Array(hex.match(/[\da-f]{2}/gi).map(function(h) {
return parseInt(h, 16)
}))
arrayBuffer = arrayBuffer.buffer;
uni.writeBLECharacteristicValue({
deviceId: this.deviceId,
serviceId: this.bluetoothParams.serviceId,
characteristicId: this.bluetoothParams.characteristicId,
value: arrayBuffer,
success(res) {
console.log('写入成功', res)
},
fail(err) {
console.error(err)
}
})
},
七、效果图