26_微信小程序蓝牙-蓝牙通信篇
26_微信小程序蓝牙-蓝牙通信篇
注: 示例代码使用uniapp编写,下面通过uni.调用的API侧重于微信侧,等同于wx.,
上一篇文章: 25_微信小程序蓝牙-蓝牙连接篇 中已经完成了蓝牙设备的连接与断开。
本文续接上一篇文章,实现终端设备(手机)与低功耗蓝牙设备间的通信,包括读取和写入数据。
一.通用流程
要实现终端设备与低功耗蓝牙蓝牙设备的通信,首先需要获取低功耗蓝牙设备所有可用的服务,找到每一个服务对应的所有特征,从这些特征中找到对应类型的特征,当然,我们也没有必要找到所有的特征,只需找到第一个满足条件的特征即可。
- 读取数据找到
indicate、notify或read的特征,indicate和notify用于实时读取。 - 写入数据找到
write的特征。
最后通过找到的特征实现读或者写操作。
注: ios端,断开设备连接,下次重新连接后,前一次连接查找到的特征失效
二.读取数据
-
获取设备所有可用的服务
调用
uni.getBLEDeviceServices,传入已连接设备的设备id即可。注:ios平台 && wxwork(企业微信端),需待蓝牙连接成功后,延迟1秒后调用
uni.getBLEDeviceServices<!-- index.vue --> <template> <view class="index-page"> <view class="bluetooth-status">蓝牙状态: {{bluetoothStatus}}</view> <scroll-view scroll-y class="device-list"> <view v-for="(item, index) in devices" class="device-item"> <view class="device-info-box"> <view class="device-info">设备名称: {{item.localName || item.name || item.deviceId}}</view> <view class="device-info">设备ID: {{item.deviceId}}</view> </view> <view class="btn-change-connect" @click="toChangeConnect($event, index)">{{item.connected ? "断开连接":"连接"}}</view> </view> </scroll-view> <view class="index__test-btn" @click="toReadBluetoothData($event)">读取数据</view> </view> </template> <script> export default { ... computed: { connectedIds() { let devices = this.devices || [] let connectedDevices = devices.filter((item) => item.connected) return connectedDevices.map((item) => item.deviceId) } }, methods: { ... toReadBluetoothData: function(event) { if(this.connectedIds.length <= 0) { uni.showToast({ title: "请先连接蓝牙设备" }) return } //这里默认取第一个连接上的设备id,如果连接了多个设备,请自行适配 let deviceId = this.connectedIds[0] new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] console.log("toReadBluetoothData", services) }) } } } </script> <style scoped> ... .index__test-btn { height: 88rpx; border-radius: 12rpx; box-sizing: border-box; background: chocolate; color: white; font-size: 28rpx; display: flex; flex-direction: row; align-items: center; justify-content: center; margin: 20rpx 20rpx constant(safe-area-inset-bottom); margin: 20rpx 20rpx env(safe-area-inset-bottom); } </style> -
调用
getBLEDeviceCharacteristics传入deviceId和serviceId获取对应的特征列表,找到第一个indicate、notify或read的特征<script> export default { data() { return { bluetoothStatus: "BLUETOOTH_AUTHOR_PENDING", devices: [], characteristicMap: {} } }, ... methods: { ... /** * 找到第一个满足type条件的特征 * @param {Array<any>} services - 所有可用的蓝牙服务 * @param {string} deviceId - 设备id * @param {number} searchIndex - 从第几个服务开始查找 * @param {string} type - 查找的特征类型, * type取值: read、write、notify、indicate、writeNoResponse、writeDefault,默认: read */ seekFirstCharacteristic: function(services = [], deviceId = "", searchIndex = 0, type = "indicate") { if(searchIndex < services.length) { let serviceId = services[searchIndex].uuid let characteristicKey = `${deviceId}-${serviceId}-${type}` if(this.characteristicMap[characteristicKey]) { return Promise.resolve(this.characteristicMap[characteristicKey]) } return uni.getBLEDeviceCharacteristics({ deviceId: deviceId, serviceId: serviceId }).then((res) => { let characteristics = (res || {}).characteristics || [] let targetCharacteristics = undefined for (let i = 0; i < characteristics.length && !targetCharacteristics; ++i) { let properties = (characteristics[i] || {}).properties || {} if (properties[type]) { targetCharacteristics = { serviceId: serviceId, deviceId: deviceId, type: type, characteristicId: characteristics[i].uuid } } } if(targetCharacteristics) { this.characteristicMap[characteristicKey] = targetCharacteristics return Promise.resolve(this.characteristicMap[characteristicKey]) } else { searchIndex += 1 return this.seekFirstCharacteristic(services, deviceId, searchIndex, type) } }).catch(() => { searchIndex += 1 return this.seekFirstCharacteristic(services, deviceId, searchIndex, type) }) } else { return Promise.reject({ "errMsg": `seekFirstCharacteristic:fail not found ${type} characteristic`, "errno": -2 }) } }, toReadBluetoothData: function(event) { if(this.connectedIds.length <= 0) { uni.showToast({ title: "请先连接蓝牙设备" }) return } //这里默认取第一个连接上的设备id,如果连接了多个设备,请自行适配 let deviceId = this.connectedIds[0] new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] return this.seekFirstCharacteristic(services, deviceId, 0, "notify") }).then((res) => { console.log("firstNotifyCharacteristic", res) }) } } } </script> -
同样可以把
seekFirstCharacteristic封装到BluetoothManager中... function seekFirstCharacteristic(services = [], deviceId = "", searchIndex = 0, type = "read") { if(searchIndex < services.length) { let serviceId = services[searchIndex].uuid let characteristicKey = `${deviceId}-${serviceId}-${type}` if(this.characteristicMap[characteristicKey]) { return Promise.resolve(this.characteristicMap[characteristicKey]) } return uni.getBLEDeviceCharacteristics({ deviceId: deviceId, serviceId: serviceId }).then((res) => { let characteristics = (res || {}).characteristics || [] let targetCharacteristics = undefined for (let i = 0; i < characteristics.length && !targetCharacteristics; ++i) { let properties = (characteristics[i] || {}).properties || {} if (properties[type]) { targetCharacteristics = { serviceId: serviceId, deviceId: deviceId, type: type, characteristicId: characteristics[i].uuid } } } if(targetCharacteristics) { this.characteristicMap[characteristicKey] = targetCharacteristics return Promise.resolve(this.characteristicMap[characteristicKey]) } else { searchIndex += 1 return this.getFirstCharacteristic(services, deviceId, searchIndex, type) } }).catch(() => { searchIndex += 1 return this.getFirstCharacteristic(services, deviceId, searchIndex, type) }) } else { return Promise.reject({ "errMsg": `seekFirstCharacteristic:fail not found ${type} characteristic`, "errno": -2 }) } } class BluetoothManagerInstance implements BluetoothManager { ... private characteristicMap: Object = {}; private onBLEConnectionStateChange: (result: any) => void = (result) => { let connected = result.connected || false let deviceId = result.deviceId || "" let existsIndex = this.connectedDeviceIds.findIndex((connectedId) => connectedId == deviceId) if(connected) { if(existsIndex == -1) { this.connectedDeviceIds.push(deviceId) } } else { //设备断开连接后,清除当前设备的特征值 for(let key in this.characteristicMap) { if(key.indexOf(deviceId) != -1) { this.characteristicMap[key] = undefined } } if(existsIndex != -1) { this.connectedDeviceIds.splice(existsIndex, 1) } } getBluetoothDevices.call(this) } /** * 找到第一个满足type条件的特征 * @param {Array<any>} services - 所有可用的蓝牙服务 * @param {string} deviceId - 设备id * @param {number} searchIndex - 从第几个服务开始查找 * @param {string} type - 查找的特征类型, * type取值: read、write、notify、indicate、writeNoResponse、writeDefault,默认: read */ getFirstCharacteristic(services = [], deviceId = "", searchIndex = 0, type = "read") { return seekFirstCharacteristic.call(this, services, deviceId, searchIndex, type) } ... }<script> export default { ... methods: { ... toReadBluetoothData: function(event) { if(this.connectedIds.length <= 0) { uni.showToast({ title: "请先连接蓝牙设备" }) return } //这里默认取第一个连接上的设备id,如果连接了多个设备,请自行适配 let deviceId = this.connectedIds[0] new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] let bluetoothManager = getApp().globalData.bluetoothManager return bluetoothManager.getFirstCharacteristic(services, deviceId, 0, "notify") }).then((res) => { console.log("firstNotifyCharacteristic", res) }) } } } </script> -
读取蓝牙设备发送的数据
- 如果是
notify或者indicate,首先调用uni.onBLECharacteristicValueChange注册设备的特征值变化的事件回调,然后调用notifyBLECharacteristicValueChange启用监听,然后在设备的特征值变化的事件回调方法中,就可以获取到蓝牙设备发送的数据了。 - 如果是
read,则需要手动调用uni.readBLECharacteristicValue读取蓝牙设备发送的数据 - 蓝牙设备发送的数据是一个字节数组,我们将字节数组,转换为16进制后,即为传输数据的
ascii码数组,在将这个ascii码数组转换为字符串数组,即得到传输的数据了
注:
indicate操作成功后需要设备主动更新特征的 value,才会触发uni.onBLECharacteristicValueChange回调注: 安卓平台上,在
uni.notifyBLECharacteristicValueChange接口调用成功后立即调用uni.writeBLECharacteristicValue接口,在部分机型上会发生 10008 系统错误以
notify为例:<script> export default { ... onLoad() { uni.onBLECharacteristicValueChange(this.characteristicValueChange) getApp().globalData.bluetoothManager.on("statusChange", this.onBluetoothStatusChanged) getApp().globalData.bluetoothManager.on("deviceFound", this.onDeviceFound) }, onUnload() { getApp().globalData.bluetoothManager.off("statusChange", this.onBluetoothStatusChanged) getApp().globalData.bluetoothManager.off("deviceFound", this.onDeviceFound) uni.offBLECharacteristicValueChange(this.characteristicValueChange) }, ... methods: { ... ab2hex(buffer) { let hexArr = Array.prototype.map.call( new Uint8Array(buffer), function(bit) { return ('00' + bit.toString(16)).slice(-2) } ) let validate = true for(let i=0; i<hexArr.length; i++) { if(!this.valueMap[hexArr[i]]) { validate = false break } } return validate ? hexArr:[]; }, characteristicValueChange: function(result) { let buffer = (result || {}).value || [] let asciiArr = this.ab2hex(buffer) //ascii码数组转字符串 ... }, toReadBluetoothData: function(event) { if(this.connectedIds.length <= 0) { uni.showToast({ title: "请先连接蓝牙设备" }) return } //这里默认取第一个连接上的设备id,如果连接了多个设备,请自行适配 uni.showLoading() let deviceId = this.connectedIds[0] new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] let bluetoothManager = getApp().globalData.bluetoothManager return bluetoothManager.getFirstCharacteristic(services, deviceId, 0, "notify") }).then((res) => { return uni.notifyBLECharacteristicValueChange({ deviceId: res.deviceId, serviceId: res.serviceId, characteristicId: res.characteristicId, state: true }) }).finally(() => { uni.hideLoading() }) } } } </script> - 如果是
三.写入数据
以蓝牙打印机为例,向设备发送tspl指令,关于tspl指令可以参考这篇文章: 打印机TSPL指令集
由于蓝牙之间传输的数据都是
ArrayBuffer,所以需要把需要发送的数据预处理成ArrayBuffer。获取设备所有可用的服务
调用
uni.getBLEDeviceCharacteristics传入deviceId和serviceId获取对应的特征列表,找到第一个write的特征最后调用
uni.writeBLECharacteristicValue将ArrayBuffer写入注: 小程序不会对写入数据包大小做限制,但系统与蓝牙设备会限制蓝牙 4.0 单次传输的数据大小,超过最大字节数后会发生写入错误,建议每次写入不超过 20 字节
注: 若单次写入数据过长,iOS 上存在系统不会有任何回调的情况(包括错误回调)
因此,我们需要将数据以20字节的大小分包写入。
-
编写tspl指令
<script> export default { ... methods: { ... getTSPLCommand: function() { let tsplCommand = `SIZE 60 mm,40 mm\r\n` tsplCommand += `DIRECTION 0,0\r\n` tsplCommand += `REFERENCE 0,0\r\n` tsplCommand += `OFFSET 0 mm\r\n` tsplCommand += `DENSITY 15\r\n` tsplCommand += `AUTODETECT\r\n` tsplCommand += `CLS\r\n` tsplCommand += `TEXT 0,16,"TSS24.BF2",0,1,1,"左上"\r\n` tsplCommand += `TEXT 384,16,"TSS24.BF2",0,1,1,"右上"\r\n` tsplCommand += `TEXT 0,280,"TSS24.BF2",0,1,1,"左下"\r\n` tsplCommand += `TEXT 384,280,"TSS24.BF2",0,1,1,"右下"\r\n` tsplCommand += `TEXT 192,136,"TSS24.BF2",0,1,1,"中间"\r\n` tsplCommand += `PRINT 1,1\r\n` return tsplCommand }, ... } } </script> -
将编写好的tspl指令转成GBK编码的ArrayBuffer,并按照指定字节的大小分包,默认按20字节分
注: javascript内置的TextEncoder和TextDecoder在微信小程序环境下被阉割了
所以,转换GBK编码的ArrayBuffer,可以使用如下两个库:
gbk.js,大小约50kb左右,适合在小程序中使用
text-encoding,大小约600kb左右,小程序中慎用,否则很容易超包<script> import { encode } from "@/bluetooth/gbk.js" export default { ... methods: { ... getArrayBufferList: function(tsplCommand = "", subpackage = 20) { const string2ArrayBuffer = (data) => { var data = encode(data) var byteArr = new Array(); for (var i = 0; i < data.length; i++) { var ch = data.charAt(i); if (ch == '%') { var num = data.charAt(i + 1) + data.charAt(i + 2); num = parseInt(num, 16); num = num | (-1 << 8); byteArr.push(num); i += 2; } else { byteArr.push(ch.charCodeAt()); } } return Uint8Array.from(byteArr).buffer; } const splitBuffer = (buffer, len) => { let byteLength = buffer.byteLength; let result = [] for(let i = 0; i < byteLength; i += len) { result.push(buffer.slice(i, i + len)) } return result; } let buffer = string2ArrayBuffer(tsplCommand) return splitBuffer(buffer, subpackage) }, ... } } </script> -
写入数据
<script> import { encode } from "@/bluetooth/gbk.js" export default { ... methods: { ... toWriteBluetoothData: function() { if(this.connectedIds.length <= 0) { uni.showToast({ title: "请先连接蓝牙设备" }) return } let sendloop = this.getArrayBufferList(this.getTSPLCommand()) //这里默认取第一个连接上的设备id,如果连接了多个设备,请自行适配 uni.showLoading() let deviceId = this.connectedIds[0] new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] let bluetoothManager = getApp().globalData.bluetoothManager return bluetoothManager.getFirstCharacteristic(services, deviceId, 0, "write") }).then((characteristic) => { return this.writeCharacteristicValue(characteristic, sendloop) }).finally(() => { uni.hideLoading() }) } ... } } </script> -
整个写入蓝牙数据的过程,同样可以封装到
BluetoothManager中import { encode } from "./gbk.js" ... function getArrayBufferList(content = "", subpackage = 20) { const string2ArrayBuffer = (data) => { var data = encode(data) var byteArr = new Array(); for (var i = 0; i < data.length; i++) { var ch = data.charAt(i); if (ch == '%') { var num = data.charAt(i + 1) + data.charAt(i + 2); num = parseInt(num, 16); num = num | (-1 << 8); byteArr.push(num); i += 2; } else { byteArr.push(ch.charCodeAt()); } } return Uint8Array.from(byteArr).buffer; } const splitBuffer = (buffer, len) => { let byteLength = buffer.byteLength; let result = [] for(let i = 0; i < byteLength; i += len) { result.push(buffer.slice(i, i + len)) } return result; } let buffer = string2ArrayBuffer(content) return splitBuffer(buffer, subpackage) } function realWriteCharacteristicValue(characteristic, sendloop) { const writeSingle = (index = 0) => { if (index >= sendloop.length) { return Promise.resolve() } let buffer = sendloop[index]; return uni.writeBLECharacteristicValue({ deviceId: characteristic.deviceId, serviceId: characteristic.serviceId, characteristicId: characteristic.characteristicId, value: buffer }).then(() => { index += 1 return writeSingle(index) }) } return writeSingle() } class BluetoothManagerInstance implements BluetoothManager { ... /** * 向蓝牙设备发送数据 * @param {string} deviceId - 设备id * @param {string} content - 数据内容 * @param {number} subpackage - 分包大小 */ writeCharacteristicValue(deviceId: string = "", content: string = "", subpackage: number = 20) { if(!deviceId) { return Promise.reject({ "errMsg": `请先连接蓝牙设备`, "errno": -2 }) } if(!content) { return Promise.reject({ "errMsg": `数据不能为空`, "errno": -2 }) } let sendloop = getArrayBufferList.call(this, content, 20) return new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] return this.getFirstCharacteristic(services, deviceId, 0, "write") }).then((characteristic) => { return realWriteCharacteristicValue.call(this, characteristic, sendloop) }) } ... } -
最后来一张打印的效果图

四.完整代码
-
gbk.js可以从这里下载 -
bluetooth-manager.tsimport { encode } from "./gbk.js" /** * 蓝牙权限待申请 */ const BLUETOOTH_AUTHOR_PENDING:string = "BLUETOOTH_AUTHOR_PENDING" /** * 蓝牙权限被拒绝 */ const BLUETOOTH_AUTHOR_REJECT:string = "BLUETOOTH_AUTHOR_REJECT" /** * BluetoothAdapter不可用 */ const BLUETOOTH_ADAPTER_UNAVAILABLE:string = "BLUETOOTH_ADAPTER_UNAVAILABLE" /** * 蓝牙适配器可用 */ const BLUETOOTH_ADAPTER_AVAILABLE:string = "BLUETOOTH_ADAPTER_AVAILABLE" export interface BluetoothManager { on(type: string, callback: (status: string) => void): void; off(type: string, callback: (status: string) => void): void; destroy(): void; } function dispatchDeviceFound(devices: Array<any>) { this.devicesCallbacks.forEach((func) => { func && func(devices) }) } function dispatchBluetoothStatus(status: string): void { this.statusCallbacks.forEach((func) => { func && func(status) }) } function startScanBluetoothDevice(): Promise<any> { return uni.startBluetoothDevicesDiscovery({ allowDuplicatesKey: false, interval: 1000 }).catch((err) => { return Promise.reject() }).then(() => { //添加搜索到新设备的事件的监听函数 uni.offBluetoothDeviceFound(this.onBluetoothDeviceFound) uni.onBluetoothDeviceFound(this.onBluetoothDeviceFound) return Promise.resolve() }) } function stopScanBluetoothDevice():Promise<any> { //移除搜索到新设备的事件的监听函数 uni.offBluetoothDeviceFound(this.onBluetoothDeviceFound) return uni.stopBluetoothDevicesDiscovery().catch(() => undefined) } function getBluetoothDevices(): void { uni.getBluetoothDevices().then((res) => { let repeatDevices = (res || {}).devices || [] //去重 let devices = [] repeatDevices.forEach((item) => { let deviceId = item.deviceId let existsIndex = devices.findIndex((existsItem) => existsItem.deviceId == deviceId) if(existsIndex == -1) { devices.push(item) } }) //排序 devices.sort((item1, item2) => { let name1 = (item1.localName || item1.name).toLowerCase() let name2 = (item2.localName || item2.name).toLowerCase() return name1.localeCompare(name2) }) //判断连接状态 devices.forEach((item) => { let deviceId = item.deviceId let existsIndex = this.connectedDeviceIds.findIndex((connectedId) => connectedId == deviceId) item.connected = existsIndex != -1 }) dispatchDeviceFound.call(this, devices) }) } function initializeBluetooth(): Promise<any> { this.status = BLUETOOTH_AUTHOR_PENDING dispatchBluetoothStatus.call(this, this.status) const scope:string = "scope.bluetooth" const openBluetoothAdapter = ():Promise<any> => { return uni.openBluetoothAdapter().catch((err) => { this.status = BLUETOOTH_ADAPTER_UNAVAILABLE dispatchBluetoothStatus.call(this, this.status) return Promise.reject() }).then((res) => { this.status = BLUETOOTH_ADAPTER_AVAILABLE dispatchBluetoothStatus.call(this, this.status) return Promise.resolve() }).finally(() => { //关闭蓝牙适配器状态监听 uni.offBluetoothAdapterStateChange(this.adapterStateChange) //开启蓝牙适配器状态监听 uni.onBluetoothAdapterStateChange(this.adapterStateChange) }) } const openSetting = () => { return uni.showModal({ content: "您需要在设置中开启\"蓝牙\"选项,才可以使用蓝牙哦!", cancelText: "任性不去", confirmText: "去设置", }).then((res) => { if (res.confirm) { return new Promise((resolve, reject) => { this.openSettingResolves.push(resolve) uni.openSetting().catch((error) => { reject({ "errMsg": "openSetting:fail system error", "errno": -2 }) }) }).then((res) => { let authSetting = res.authSetting || {} if (authSetting[scope]) { return Promise.resolve() } else { return openSetting() } }) } else { return Promise.reject({ "errMsg": "authorize:fail auth deny", "errno": 103 }) } }) } const getSetting = () => { return uni.getSetting().catch((error) => { return Promise.reject({ "errMsg": "authorize:fail system error", "errno": -2 }) }).then((res) => { let authSetting = res.authSetting || {} let keys = Object.keys(authSetting) if (keys.indexOf(scope) != -1) { if(!authSetting[scope]) { return openSetting() } else { return Promise.resolve() } } else { return Promise.reject({ "errMsg": "authorize:fail auth canceled", "errno": -3 }) } }) } return uni.authorize({scope: scope}).catch((err) => { return getSetting() }).catch((err) => { this.status = BLUETOOTH_AUTHOR_REJECT dispatchBluetoothStatus.call(this, this.status) return Promise.reject() }).then((res) => { return uni.closeBluetoothAdapter().catch((err) => Promise.resolve()) }).then((res) => { return openBluetoothAdapter() }).then((res) => { return stopScanBluetoothDevice.call(this) }).then((res) => { return startScanBluetoothDevice.call(this) }).then((res) => { //关闭蓝牙连接状态监听 uni.offBLEConnectionStateChange(this.onBLEConnectionStateChange) //开启蓝牙连接状态监听 uni.onBLEConnectionStateChange(this.onBLEConnectionStateChange) }) } function seekFirstCharacteristic(services = [], deviceId = "", searchIndex = 0, type = "read") { if(searchIndex < services.length) { let serviceId = services[searchIndex].uuid let characteristicKey = `${deviceId}-${serviceId}-${type}` if(this.characteristicMap[characteristicKey]) { return Promise.resolve(this.characteristicMap[characteristicKey]) } return uni.getBLEDeviceCharacteristics({ deviceId: deviceId, serviceId: serviceId }).then((res) => { let characteristics = (res || {}).characteristics || [] let targetCharacteristics = undefined for (let i = 0; i < characteristics.length && !targetCharacteristics; ++i) { let properties = (characteristics[i] || {}).properties || {} if (properties[type]) { targetCharacteristics = { serviceId: serviceId, deviceId: deviceId, type: type, characteristicId: characteristics[i].uuid } } } if(targetCharacteristics) { this.characteristicMap[characteristicKey] = targetCharacteristics return Promise.resolve(this.characteristicMap[characteristicKey]) } else { searchIndex += 1 return this.getFirstCharacteristic(services, deviceId, searchIndex, type) } }).catch(() => { searchIndex += 1 return this.getFirstCharacteristic(services, deviceId, searchIndex, type) }) } else { return Promise.reject({ "errMsg": `seekFirstCharacteristic:fail not found ${type} characteristic`, "errno": -2 }) } } function getArrayBufferList(content = "", subpackage = 20) { const string2ArrayBuffer = (data) => { var data = encode(data) var byteArr = new Array(); for (var i = 0; i < data.length; i++) { var ch = data.charAt(i); if (ch == '%') { var num = data.charAt(i + 1) + data.charAt(i + 2); num = parseInt(num, 16); num = num | (-1 << 8); byteArr.push(num); i += 2; } else { byteArr.push(ch.charCodeAt()); } } return Uint8Array.from(byteArr).buffer; } const splitBuffer = (buffer, len) => { let byteLength = buffer.byteLength; let result = [] for(let i = 0; i < byteLength; i += len) { result.push(buffer.slice(i, i + len)) } return result; } let buffer = string2ArrayBuffer(content) return splitBuffer(buffer, subpackage) } function realWriteCharacteristicValue(characteristic, sendloop) { const writeSingle = (index = 0) => { if (index >= sendloop.length) { return Promise.resolve() } let buffer = sendloop[index]; return uni.writeBLECharacteristicValue({ deviceId: characteristic.deviceId, serviceId: characteristic.serviceId, characteristicId: characteristic.characteristicId, value: buffer }).then(() => { index += 1 return writeSingle(index) }) } return writeSingle() } class BluetoothManagerInstance implements BluetoothManager { private status:string; private statusCallbacks: Array<(status: string) => void> = []; private openSettingResolves: Array<(result:any) => void> = []; private devicesCallbacks: Array<(devices: Array<any>) => void> = []; private onAppShow: (options: any) => void = (options): void => { uni.getSetting().then((res) => { if(this.openSettingResolves.length > 0) { this.openSettingResolves.forEach((resolveFunc) => resolveFunc(res)) this.openSettingResolves = [] } else { let authSetting = res.authSetting || {} let keys = Object.keys(authSetting) if(keys.indexOf("scope.bluetooth") != -1) { if (!authSetting["scope.bluetooth"]) { if(this.status != BLUETOOTH_AUTHOR_PENDING) { this.status = BLUETOOTH_AUTHOR_REJECT dispatchBluetoothStatus.call(this, this.status) } } else { if(this.status == BLUETOOTH_AUTHOR_REJECT) { initializeBluetooth.call(this); } } } } return Promise.resolve() }).catch((err) => { return Promise.resolve() }) } private adapterStateChange: (result: any) => void = (res) => { let available = (res || {}).available || false let discovering = (res || {}).discovering || false let bluetoothStatus = BLUETOOTH_ADAPTER_UNAVAILABLE if(available) { bluetoothStatus = BLUETOOTH_ADAPTER_AVAILABLE } if(this.status != bluetoothStatus) { //蓝牙开关状态改变 if(bluetoothStatus == BLUETOOTH_ADAPTER_AVAILABLE) { //搜索设备 startScanBluetoothDevice.call(this) } else { //停止搜索设备 stopScanBluetoothDevice.call(this) } } this.status = bluetoothStatus } private onBluetoothDeviceFound: (result: any) => void = (result) => { getBluetoothDevices.call(this) } private connectedDeviceIds: Array<string> = []; private onBLEConnectionStateChange: (result: any) => void = (result) => { let connected = result.connected || false let deviceId = result.deviceId || "" let existsIndex = this.connectedDeviceIds.findIndex((connectedId) => connectedId == deviceId) if(connected) { if(existsIndex == -1) { this.connectedDeviceIds.push(deviceId) } } else { //设备断开连接后,清除当前设备的特征值 for(let key in this.characteristicMap) { if(key.indexOf(deviceId) != -1) { this.characteristicMap[key] = undefined } } if(existsIndex != -1) { this.connectedDeviceIds.splice(existsIndex, 1) } } getBluetoothDevices.call(this) } private characteristicMap: Object = {}; constructor() { uni.onAppShow(this.onAppShow); initializeBluetooth.call(this); } on(type: string, callback: (result: any) => void): void { if(type == "statusChange") { this.statusCallbacks.push(callback) } else if(type == "deviceFound") { this.devicesCallbacks.push(callback) } } off(type: string, callback: (result: any) => void): void { if(type == "statusChange") { let callbackIndex = this.statusCallbacks.findIndex((item) => item == callback) if(callbackIndex == -1) { return } this.statusCallbacks.splice(callbackIndex, 1) } else if(type == "deviceFound") { let callbackIndex = this.devicesCallbacks.findIndex((item) => item == callback) if(callbackIndex == -1) { return } this.devicesCallbacks.splice(callbackIndex, 1) } } /** * 找到第一个满足type条件的特征 * @param {Array<any>} services - 所有可用的蓝牙服务 * @param {string} deviceId - 设备id * @param {number} searchIndex - 从第几个服务开始查找 * @param {string} type - 查找的特征类型, * type取值: read、write、notify、indicate、writeNoResponse、writeDefault,默认: read */ getFirstCharacteristic(services = [], deviceId = "", searchIndex = 0, type = "read") { return seekFirstCharacteristic.call(this, services, deviceId, searchIndex, type) } /** * 向蓝牙设备发送数据 * @param {string} deviceId - 设备id * @param {string} content - 数据内容 * @param {number} subpackage - 分包大小 */ writeCharacteristicValue(deviceId: string = "", content: string = "", subpackage: number = 20) { if(!deviceId) { return Promise.reject({ "errMsg": `请先连接蓝牙设备`, "errno": -2 }) } if(!content) { return Promise.reject({ "errMsg": `数据不能为空`, "errno": -2 }) } let sendloop = getArrayBufferList.call(this, content, 20) return new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] return this.getFirstCharacteristic(services, deviceId, 0, "write") }).then((characteristic) => { return realWriteCharacteristicValue.call(this, characteristic, sendloop) }) } destroy(): void { this.status = BLUETOOTH_AUTHOR_PENDING //关闭蓝牙连接状态监听 uni.offBLEConnectionStateChange(this.onBLEConnectionStateChange) stopScanBluetoothDevice.call(this) //关闭蓝牙适配器状态监听 uni.offBluetoothAdapterStateChange(this.adapterStateChange) uni.closeBluetoothAdapter().catch(() => undefined) this.statusCallbacks = [] this.devicesCallbacks = [] this.connectedDeviceIds = [] uni.offAppShow(this.onAppShow) } } export const createBluetoothManager = ():BluetoothManager => { return new BluetoothManagerInstance(); } -
index.vue<template> <view class="index-page"> <view class="bluetooth-status">蓝牙状态: {{bluetoothStatus}}</view> <scroll-view scroll-y class="device-list"> <view v-for="(item, index) in devices" class="device-item"> <view class="device-info-box"> <view class="device-info">设备名称: {{item.localName || item.name || item.deviceId}}</view> <view class="device-info">设备ID: {{item.deviceId}}</view> </view> <view class="btn-change-connect" @click="toChangeConnect($event, index)">{{item.connected ? "断开连接":"连接"}}</view> </view> </scroll-view> <view class="index__test-btn" @click="toWriteBluetoothData($event)">写入数据</view> </view> </template> <script> export default { data() { return { bluetoothStatus: "BLUETOOTH_AUTHOR_PENDING", devices: [] } }, onLoad() { getApp().globalData.bluetoothManager.on("statusChange", this.onBluetoothStatusChanged) getApp().globalData.bluetoothManager.on("deviceFound", this.onDeviceFound) }, onUnload() { getApp().globalData.bluetoothManager.off("statusChange", this.onBluetoothStatusChanged) getApp().globalData.bluetoothManager.off("deviceFound", this.onDeviceFound) }, computed: { connectedIds() { let devices = this.devices || [] let connectedDevices = devices.filter((item) => item.connected) return connectedDevices.map((item) => item.deviceId) } }, methods: { onBluetoothStatusChanged: function(status) { this.bluetoothStatus = status }, onDeviceFound: function(devices) { this.devices = devices }, toChangeConnect: function(event, index) { let item = this.devices[index] uni.showLoading() let changeTask = undefined if(!item.connected) { changeTask = uni.createBLEConnection({ deviceId: item.deviceId }) } else { changeTask = uni.closeBLEConnection({ deviceId: item.deviceId }) } changeTask.finally(() => { uni.hideLoading() }) }, getTSPLCommand: function() { let tsplCommand = `SIZE 60 mm,40 mm\r\n` tsplCommand += `DIRECTION 0,0\r\n` tsplCommand += `REFERENCE 0,0\r\n` tsplCommand += `OFFSET 0 mm\r\n` tsplCommand += `DENSITY 15\r\n` tsplCommand += `AUTODETECT\r\n` tsplCommand += `CLS\r\n` tsplCommand += `TEXT 0,16,"TSS24.BF2",0,1,1,"左上"\r\n` tsplCommand += `TEXT 384,16,"TSS24.BF2",0,1,1,"右上"\r\n` tsplCommand += `TEXT 0,280,"TSS24.BF2",0,1,1,"左下"\r\n` tsplCommand += `TEXT 384,280,"TSS24.BF2",0,1,1,"右下"\r\n` tsplCommand += `TEXT 192,136,"TSS24.BF2",0,1,1,"中间"\r\n` tsplCommand += `PRINT 1,1\r\n` return tsplCommand }, toWriteBluetoothData: function() { //这里默认取第一个连接上的设备id,如果连接了多个设备,请自行适配 uni.showLoading() let deviceId = this.connectedIds[0] let bluetoothManager = getApp().globalData.bluetoothManager let content = this.getTSPLCommand() bluetoothManager.writeCharacteristicValue(deviceId, content).finally(() => { uni.hideLoading() }) } } } </script> <style scoped> .index-page { width: 100%; height: 100%; display: flex; flex-direction: column; align-items: stretch; justify-content: center; } .bluetooth-status { font-size: 24rpx; padding: 20rpx 0; margin: 0 20rpx; box-sizing: border-box; flex-shrink: 0; border-bottom: thin solid #f2f2f2; } .device-list { flex: 1; overflow: hidden; } .device-item { padding: 20rpx 0; margin: 0 20rpx; box-sizing: border-box; background: white; border-bottom: thin solid #f2f2f2; display: flex; flex-direction: row; align-items: center; justify-content: center; } .device-info { font-size: 28rpx; margin-top: 20rpx; } .device-info:first-child { margin-top: 0; } .device-info-box { flex: 1; overflow: hidden; display: flex; flex-direction: column; align-items: stretch; justify-content: center; } .btn-change-connect { padding: 20rpx 60rpx; box-sizing: border-box; background: #2BA3FF; color: white; font-size: 28rpx; border-radius: 10rpx; } .index__test-btn { height: 88rpx; border-radius: 12rpx; box-sizing: border-box; background: chocolate; color: white; font-size: 28rpx; display: flex; flex-direction: row; align-items: center; justify-content: center; margin: 20rpx 20rpx constant(safe-area-inset-bottom); margin: 20rpx 20rpx env(safe-area-inset-bottom); } </style>
五.补充:将读取到蓝牙数据的ArrayBuffer解析成字符串
-
gbk.js注: re的值太长,请自行下载gbk.js,然后从下载好的文件中复制过来
var U2Ghash; var G2Uhash; function data() { //re的值太长,请自行下载gbk.js,然后从下载好的文件中复制过来 var re = '' .replace(/#(\d+)\$/g, function(a, b) { return Array(+b + 3).join('#'); }) .replace(/#/g, '####') .replace(/(\w\w):([\w#]+)(?:,|$)/g, function(a, hd, dt) { return dt.replace(/../g, function(a) { if (a != '##') { return hd + a; } else { return a; } }); }); U2Ghash = {}, G2Uhash = {}; return re; } function initUsChash(data) { var k = 0; data = data.match(/..../g); for (var i = 0x81; i <= 0xfe; i++) { for (var j = 0x40; j <= 0xFE; j++) { U2Ghash[data[k++]] = ('%' + i.toString(16) + '%' + j.toString(16)) .toUpperCase(); } } for (var key in U2Ghash) { G2Uhash[U2Ghash[key]] = key; } } function isAscii(unicode) { return ((unicode == 0x20AC) || (unicode <= 0x007F && unicode >= 0x0000)); } export const encode = function(str) { if (U2Ghash == undefined) { initUsChash(data()); } return str.replace(/./g, function(a) { var code = a.charCodeAt(0); if (isAscii(code)) { return encodeURIComponent(a); } else { var key = code.toString(16); if (key.length != 4) key = ('000' + key).match(/....$/)[0]; return U2Ghash[key] || a; } }); } export const decode = function(str) { if (G2Uhash == undefined) { initUsChash(data()); } return str.replace(/%[\w]{2}/g, function(a) { let unicode = parseInt(a.replace(/%/g, ""), 16); if(isAscii(unicode)) { return decodeURIComponent(a); } return a }).replace(/%[0-9A-F]{2}%[0-9A-F]{2}/g, function(a) { if (a in G2Uhash) { return String.fromCharCode('0x' + G2Uhash[a]); } else { return a; } }) } -
在收到蓝牙数据的回调中解析数据
<script> import { decode } from "@/bluetooth/gbk.js" export default { ... onLoad() { getApp().globalData.bluetoothManager.on("statusChange", this.onBluetoothStatusChanged) getApp().globalData.bluetoothManager.on("deviceFound", this.onDeviceFound) uni.onBLECharacteristicValueChange(this.characteristicValueChange) }, onUnload() { uni.offBLECharacteristicValueChange(this.characteristicValueChange) getApp().globalData.bluetoothManager.off("statusChange", this.onBluetoothStatusChanged) getApp().globalData.bluetoothManager.off("deviceFound", this.onDeviceFound) }, ... methods: { ... characteristicValueChange: function(event) { let value = event.value let uint8Value = new Uint8Array(value) let result = Array.prototype.map.call(uint8Value, (unicode) => { if((unicode == 0x20AC) || (unicode <= 0x007F && unicode >= 0x0000)) { return encodeURIComponent(String.fromCharCode(unicode)) } else { return "%" + ("00" + unicode.toString(16)).slice(-2).toUpperCase() } }).join("") console.log("收到蓝牙发送的数据:", decode(result)) }, toWriteBluetoothData: function() { // //这里默认取第一个连接上的设备id,如果连接了多个设备,请自行适配 // uni.showLoading() // let deviceId = this.connectedIds[0] // let bluetoothManager = getApp().globalData.bluetoothManager // let content = this.getTSPLCommand() // bluetoothManager.writeCharacteristicValue(deviceId, content).finally(() => { // uni.hideLoading() // }) uni.showLoading() let deviceId = this.connectedIds[0] return new Promise((resolve, reject) => { let timer = setTimeout(() => { clearTimeout(timer) resolve() }, 1000) }).then(() => { return uni.getBLEDeviceServices({ deviceId: deviceId }) }).then((res) => { let services = (res || {}).services || [] let bluetoothManager = getApp().globalData.bluetoothManager return bluetoothManager.getFirstCharacteristic(services, deviceId, 0, "notify") }).then((res) => { return uni.notifyBLECharacteristicValueChange({ deviceId: res.deviceId, serviceId: res.serviceId, characteristicId: res.characteristicId, state: true }) }).then((res) => { let bluetoothManager = getApp().globalData.bluetoothManager //向打印机发送这个指令,后打印机会通过"蓝牙数据回调方法"回复我们设备的型号 let content = `~!T` return bluetoothManager.writeCharacteristicValue(deviceId, content) }).finally(() => { uni.hideLoading() }) } } } </script>
微信小程序-蓝牙篇完结。
更多推荐


所有评论(0)