uniapp连接蓝牙设备
公司做的项目有需要连接第三方的蓝牙设备,要求Android、IOS都要连接这台蓝牙设备,然后去识别NFC标签。
那个设备是德科物联的,DK309的设备,在这记录一下开发的过程。
首先呢,去了解了一下蓝牙设备的一些基础知识,这个可以在很多网站上都可以查得到。大致过程就是:
1. 我这边首先拿到了第三方的设备,然后尝试用自己的手机的蓝牙去连接,一直连接不上,然后找到了厂家,他们给了测试用的APK。结合网络上查询到的知识,得到一个新的知识点。并不是所有蓝牙设备连接的时候都需要确认匹配的那个pin码,以前连耳机这些都是需要点击确认的。
2. 然后就是,厂家只有原生的Android和IOS的方法,并没有那种在uniapp上使用的那种。然后就是下载手机端测试蓝牙连接的软件,IOS和Android平台都有一个叫LightBlue的软件,可以测试和设备的通信。
3. 然后就是基于uniapp的蓝牙连接的过程,有很多可以参考的文档,我这边是参考的这些文章:
https://www.cnblogs.com/mlw1814011067/p/16708559.html
UNI-APP实现物联网中BLE蓝牙的数据交互 - 知乎
蓝牙设备有设备ID,服务值,特征值。uniapp也提供了蓝牙连接的方法,在官网上就有。大致步骤在文章中也写了思路。
主要踩坑的点是,uniapp的那些关于蓝牙的方法,最好最好最好用settimeout异步一下,不然返回的数据结果很有可能是空的。让你误以为没连接上没数据。
然后就是根据第三方提供的底层协议,向蓝牙设备发送指令,然后解析返回的结果,这个就要根据具体设备进行定制了。
<template><view class="content"><button type="default" v-show="!shows" @click="initBle">初始化蓝牙模块</button><scroll-view scroll-y="true" show-scrollbar="true"><radio-group @change="checkboxChange"><viewv-for="(item, index) in bleDevs":key="index"v-show="item.name.length > 0 && !shows"style="padding: 10rpx 20rpx; border-bottom: 1rpx solid #ececec"><view style="font-size: 32rpx; color: #333"><checkbox-group@change="checkboxChange":data-name="item.name":data-deviceId="item.deviceId"><label><checkbox :value="item.deviceId">{{ item.name }}</checkbox></label></checkbox-group></view><view style="font-size: 20rpx; padding: 10rpx 0">deviceId: {{ item.deviceId }} 信号强度: {{ item.RSSI }}dBm ({{Math.max(100 + item.RSSI, 0)}}%)</view></view></radio-group><view class="dis"><view @tap="connectBle" v-if="!shows" class="pl"> 连接 </view><view @tap="close" v-if="shows" class="pl"> 断开 </view><text v-if="shows" class="tipText">已成功连接蓝牙设备</text><view @tap="listen" v-if="shows && !submitDevice" class="pl"> 监听 </view><text v-if="submitDevice" class="tipText">与蓝牙设备建立通信成功</text><view @tap="writeDateTo" v-if="shows && !isStartFindCard && submitDevice" class="pl"> 开始识别 </view><text v-if="isStartFindCard" class="tipText">{{ findCardText }}</text><view @tap="stopRead" v-if="shows && isFindCard && !readyReadNFC" class="pl"> 停止识别 </view><view @tap="getDataNFC" v-if="shows && readyReadNFC" class="pl"> 获取NFC内容 </view><view @tap="goBack" v-if="shows && readyReadNFC && nfcDataASCI" class="pl"> 返回巡检页面 </view><text class="tipText">{{nfcDataASCI}}</text></view></scroll-view></view>
</template><script>
export default {data() {return {config: {color: "#333",backgroundColor: [1, "#fff"],title: "多设备蓝牙连接",back: false,},title: "Hello",bleDevs: [],deviceId: "",serviceId: "",characteristicId: "",deviceIds: [],totalList: [], // 全部已连接的设备shows: false,currentRadioIndex: -1,nfcDataIndex: -1,nfcDataResult: '',nfcDataASCI: '',findCardText: '无法识别的非NFC标签',submitDevice: false,isStartFindCard: false,isFindCard: false,readyReadNFC: false};},computed: {},destroyed() {},beforeDestroy() {if(this.shows){this.justClose()}this.stopBluetoothDevicesDiscovery()},onLoad: function (option) {let vm = this;vm.eventChannel = this.getOpenerEventChannel();vm.eventChannel.on('acceptDataFromOpenerPage',function(data) {console.log(data)})},onBackPress: function (event) {let vm = this;if (event.from == "backbutton") {// 不对图片进行改动,原样返回vm.eventChannel.emit('getNFCCompleteCode', {data: vm.nfcDataASCI});}},mounted() {this.onBLEConnectionStateChange();},methods: {goBack () {let vm = this;vm.eventChannel.emit('getNFCCompleteCode', {data: vm.nfcDataASCI});// 返回页面uni.navigateBack()},checkboxChange(e) {console.log(e)for (let i = 0; i < this.bleDevs.length; i++) {if (e.target.value[0] && e.target.dataset.name) {this.currentRadioIndex = ilet item = {deviceId: e.target.value[0],name: e.target.dataset.name}this.deviceIds[0] = item}}// if (e.target.value[0] && e.target.dataset.name) {// let item = {// deviceId: e.target.value[0],// name: e.target.dataset.name,// };// this.deviceIds[0] = item// }},hextoString(hexCharCodeStr) {try{var trimedStr = hexCharCodeStr.trim();var rawStr = trimedStr.substr(0, 2).toLowerCase() === "0x" ? trimedStr.substr(2) : trimedStr;var len = rawStr.length;if (len % 2 !== 0) {alert("存在非法字符!");return "";}var curCharCode;var resultStr = [];for (var i = 0; i < len; i = i + 2) {curCharCode = parseInt(rawStr.substr(i, 2), 16);resultStr.push(String.fromCharCode(curCharCode));}console.log(resultStr)}catch(e){console.log(e)return ''}return resultStr.join("");},// ArrayBuffer转16进度字符串示例ab2hex(buffer) {const hexArr = Array.prototype.map.call(new Uint8Array(buffer),function (bit) {return ("00" + bit.toString(16)).slice(-2);});return hexArr.join("");},onBLEConnectionStateChange() {uni.onBLEConnectionStateChange((res) => {// 该方法回调中可以用于处理连接意外断开等异常情况if (res.connected == false) {uni.hideLoading();for (let i = 0; i < this.deviceIds.length; i++) {if (res.deviceId == this.deviceIds[i].deviceId) {uni.showToast({title: this.deviceIds[i].name + " 蓝牙设备断开连接",icon: "none",});}}}});},//初始化蓝牙initBle() {console.log("初始化蓝牙>>>");this.bleDevs = [];this.deviceIds = [];uni.openBluetoothAdapter({success: (res) => {//已打开uni.getBluetoothAdapterState({//蓝牙的匹配状态success: (res1) => {console.log(res1, "“本机设备的蓝牙已打开”");// 开始搜索蓝牙设备this.startBluetoothDeviceDiscovery();},fail(error) {uni.showToast({ icon: "none", title: "查看手机蓝牙是否打开" });},});},fail: (err) => {//未打开uni.showToast({ icon: "none", title: "查看手机蓝牙是否打开" });},});},// 开始搜索蓝牙设备startBluetoothDeviceDiscovery() {console.log('开始搜索蓝牙设备')uni.startBluetoothDevicesDiscovery({success: (res) => {console.log("启动成功", res);// 发现外围设备this.onBluetoothDeviceFound();},fail: (err) => {console.log(err, "错误信息");},});},// 发现外围设备onBluetoothDeviceFound() {console.log("执行到这--发现外围设备")uni.onBluetoothDeviceFound((res) => {// 吧搜索到的设备存储起来,方便我们在页面上展示var bleName = res.devices[0].nameif (this.bleDevs.findIndex(item => item.deviceId == res.devices[0].deviceId) == -1 && bleName == "BLE_NFC") {this.bleDevs.push(res.devices[0]);}console.log("蓝牙列表", res);});},// 多选然后连接connectBle() {if (this.deviceIds.length == 0) {uni.showToast({ title: "请选择连接的设备", icon: "none" });return;}console.log(this.deviceIds)// this.deviceIds.forEach((item) => {// // this.createBLEConnection(item);// this.nowLinkLis(item);// });this.nowLinkLis(this.deviceIds[0]);},//选择设备连接吧deviceId传进来createBLEConnection(item) {uni.showLoading({title: "连接中,请稍等",mask: true,});let that = this;//连接蓝牙uni.createBLEConnection({deviceId: item.deviceId,success(res) {that.shows = true;that.stopBluetoothDevicesDiscovery();that.getBLEDeviceServices(2, item);console.log('createBLEConnection',res)},fail(res) {console.log("蓝牙连接失败", res);uni.showToast({title: items.name + "蓝牙连接失败",icon: "none",});uni.hideLoading()}});},// 停止搜寻蓝牙设备stopBluetoothDevicesDiscovery() {uni.stopBluetoothDevicesDiscovery({success: (e) => {this.loading = false;console.log("停止搜索蓝牙设备:" + e.errMsg);},fail: (e) => {console.log("停止搜索蓝牙设备失败,错误码:" + e.errCode);},});},//获取蓝牙的所有服务getBLEDeviceServices(index, items) {setTimeout(() => {uni.getBLEDeviceServices({// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接deviceId: items.deviceId,success: (res) => {// console.log("成功",res)console.log("device services:", res);//这里会获取到好多个services uuid 我们只存储我们需要用到的就行,这个uuid一般硬件厂家会给我们提供console.log("services", res.services);res.services.forEach((item) => {if (item.uuid.indexOf("0000FFF0-0000-1000-8000-00805F9B34FB") == 0) {items["serviceId"] = item.uuid;//进入特征this.getBLEDeviceCharacteristics(index, items);}});},});}, 2000);},//获取蓝牙特征getBLEDeviceCharacteristics(index, items) {console.log("进入特征");setTimeout(() => {uni.getBLEDeviceCharacteristics({// 这里的 deviceId 需要已经通过 createBLEConnection 与对应设备建立链接deviceId: items.deviceId,// 这里的 serviceId 需要在 getBLEDeviceServices 接口中获取serviceId: items.serviceId,success: (res) => {console.log("characteristics", res);res.characteristics.forEach((item) => {});},fail: (res) => {console.log(res);},});}, 1000);},close() {let that = this;uni.showModal({title: "提示",content: "将断开蓝牙连接",success: function (res) {if (res.confirm) {let item = that.deviceIds[0];uni.closeBLEConnection({deviceId: item.deviceId,success(res) {console.log("断开蓝牙成功", res);that.shows = false;that.totalList = [];that.nfcDataIndex = -1;that.nfcDataResult = '';that.submitDevice= false;that.isStartFindCard= false;that.isFindCard= false;that.readyReadNFC= false;that.nfcDataASCI = '';uni.showToast({title: "断开蓝牙成功",});},fail(res) {console.log("断开蓝牙失败", res);},});}},});},justClose() {let that = this;let item = that.deviceIds[0];uni.closeBLEConnection({deviceId: item.deviceId,success(res) {console.log("断开蓝牙成功", res);that.shows = false;that.totalList = [];that.nfcDataIndex = -1;that.nfcDataResult = '';that.submitDevice= false;that.isStartFindCard= false;that.isFindCard= false;that.readyReadNFC= false;that.nfcDataASCI = '';uni.showToast({title: "断开蓝牙成功",});},fail(res) {console.log("断开蓝牙失败", res);},});},// 直接启用监听功能nowLinkLis(items) {let that = this;console.log("items", items);uni.showLoading({title: "连接中,请稍等",mask: true,});//连接蓝牙uni.createBLEConnection({deviceId: items.deviceId,success(res) {that.stopBluetoothDevicesDiscovery(); // 停止搜索蓝牙setTimeout(() => {// serviceId 0000FFF0-0000-1000-8000-00805F9B34FB// characteristicId 0000FFF2-0000-1000-8000-00805F9B34FBuni.notifyBLECharacteristicValueChange({state: true, // 启用 notify 功能deviceId: items.deviceId,serviceId: "0000FFF0-0000-1000-8000-00805F9B34FB",characteristicId: "0000FFF2-0000-1000-8000-00805F9B34FB",success: (res) => {console.log("启用notify监听了", res);that.shows = true;uni.hideLoading();that.totalList.push(items);},fail: (res) => {console.log("启用 notify 功能失败", res);uni.hideLoading();uni.showToast({ title: "连接失败", icon: "none" });},});}, 2000);},fail(res) {console.log("蓝牙连接失败", res);uni.showToast({title: items.name + "连接失败",icon: "none",});},});},listen() {setTimeout(() => {this.submitDevice = true;console.log('开始监听特征值的变化')uni.onBLECharacteristicValueChange((res) => {try{let resHex = this.ab2hex(res.value)this.formatResultCode(resHex)}catch(e){//TODO handle the exceptionconsole.log(e)}});},2000)},writeDateTo() {const items = this.deviceIds[0]// const msg = '0063FF0A02'const openRead = [0,99,255,10,1]const buffer = new ArrayBuffer(5)const dataView = new DataView(buffer)for (let i = 0; i < openRead.length; i++) {dataView.setUint8(i, openRead[i])}// for (var i = 0; i < msg.length; i++) {// dataView.setUint8(i, msg.charAt(i).charCodeAt())// }console.log(buffer)setTimeout(() => {uni.writeBLECharacteristicValue({deviceId: items.deviceId,serviceId: "0000FFF0-0000-1000-8000-00805F9B34FB",characteristicId: "0000FFF2-0000-1000-8000-00805F9B34FB",value: buffer,writeType: "write",success: function (res) {uni.hideLoading();console.log('已发送指令')},fail: function (res) {uni.hideLoading();uni.showToast({title: "发送失败,可能蓝牙目前不支持写入",icon: "none",});},});}, 2000);},stopRead() {const items = this.deviceIds[0]// const msg = '0063FF0A02'const stopRead = [0,99,0,10,1]const buffer = new ArrayBuffer(stopRead.length)const dataView = new DataView(buffer)for (let i = 0; i < stopRead.length; i++) {dataView.setUint8(i, stopRead[i])}// for (var i = 0; i < msg.length; i++) {// dataView.setUint8(i, msg.charAt(i).charCodeAt())// }console.log(buffer)setTimeout(() => {uni.writeBLECharacteristicValue({deviceId: items.deviceId,serviceId: "0000FFF0-0000-1000-8000-00805F9B34FB",characteristicId: "0000FFF2-0000-1000-8000-00805F9B34FB",value: buffer,writeType: "write",success: function (res) {uni.hideLoading();console.log('已发送指令')},fail: function (res) {uni.hideLoading();uni.showToast({title: "发送失败,可能蓝牙目前不支持写入",icon: "none",});},});}, 2000);},getDataNFC() {const items = this.deviceIds[0]const readNFC = [0,209,5,8]const buffer = new ArrayBuffer(readNFC.length)const dataView = new DataView(buffer)for (let i = 0; i < readNFC.length; i++) {dataView.setUint8(i, readNFC[i])}// [0,209,0,16]// 03d29000 04a2e4ca fa437080 4948ffff e1101200// c1 0313d101 0f540265 6e545049 4d303030 31303134// c2 31ef0000 00000000 00000000 00000000 000000// c3 00000000 00000000 00console.log(buffer)setTimeout(() => {uni.writeBLECharacteristicValue({deviceId: items.deviceId,serviceId: "0000FFF0-0000-1000-8000-00805F9B34FB",characteristicId: "0000FFF2-0000-1000-8000-00805F9B34FB",value: buffer,writeType: "write",success: function (res) {uni.hideLoading();console.log('已发送指令')},fail: function (res) {uni.hideLoading();uni.showToast({title: "发送失败,可能蓝牙目前不支持写入",icon: "none",});},});}, 2000);},formatResultCode(result) {// result以转换成十六进制数据,取前四位特征码const startFindCardPattern = /^00649000/const endFindCardPattern = /^00646e81/const findNFCCardPattern = /^0063900006/const readNFCMsgPattern = /^..d29000/// 失败的正则表达式const errorFindNFCPattern = /^00636e81/const errorReadNFCMsgPattern = /^00d26e81/const resultStr = String(result)if (startFindCardPattern.test(resultStr)){console.log('已打开自动寻卡指令')this.isStartFindCard = true;}else if (endFindCardPattern.test(resultStr)) {this.readyReadNFC = true;console.log('已关闭自动寻卡指令')}else if (findNFCCardPattern.test(resultStr)) {console.log('已成功识别到卡片')this.findCardText = '已成功识别到NFC标签,请不要挪动设备'this.isFindCard = true;}else if (readNFCMsgPattern.test(resultStr)){console.log('开始读取NFC标签内容数据')this.nfcDataResult = resultStr.slice(8)this.nfcDataIndex = Number(resultStr[1])}else if (/^c/.test(resultStr)){console.log('后续读取的字节内容')const startIndex = Number(resultStr[1])if (startIndex <= this.nfcDataIndex){this.nfcDataResult = this.nfcDataResult + resultStr.slice(2)}if (startIndex == this.nfcDataIndex) {// 所有数据已全部读取到,输出结果console.log(this.nfcDataResult)let finalResult = this.hexToAscii(this.nfcDataResult)finalResult = finalResult.split('en')[1]this.nfcDataASCI = finalResultconsole.log(finalResult)}}else if (errorFindNFCPattern.test(resultStr)){this.isFindCard = false;this.findCardText = '无法识别的非NFC标签'}else if (errorReadNFCMsgPattern.test(resultStr)){this.nfcDataASCI = '获取NFC标签内容出现错误,请重试'}},hexToAscii(hexStr) {let asciiStr = "";for (let i = 0; i < hexStr.length; i += 2) {if(hexStr.substr(i,2) == 'ef'){return asciiStr}asciiStr += String.fromCharCode(parseInt(hexStr.substr(i, 2), 16));}return asciiStr;}},
};
</script><style lang="scss" scoped>
.input3 {display: flex;justify-content: space-around;input {border: 1rpx solid #ccc;margin: 20rpx;text-align: center;height: 60rpx;border-radius: 10rpx;font-size: 50rpx;}input:first-child,input:last-child {width: 200rpx;}
}
.bakBlue {background-color: #007aff !important;
}
.appItems {padding: 30rpx 0 30rpx 4rpx;display: flex;flex-wrap: wrap;.item {color: #333;width: 160rpx;height: 160rpx;border-radius: 50%;border: 1rpx solid #ececec;margin: 10rpx 15rpx;position: relative;.txt {position: absolute;font-size: 26rpx;top: 56rpx;width: 100%;color: #fff;z-index: 10;text-align: center;}.name {position: absolute;width: 80%;left: 10%;bottom: 30rpx;font-size: 20rpx;text-align: center;}}
}
.timers {text-align: center;margin-top: 30rpx;.time {margin-bottom: 40rpx;width: 100%;font-size: 80rpx;font-weight: bold;}.btns {display: flex;justify-content: space-around;view {width: 200rpx;height: 60rpx;background-color: #007aff;color: #fff;line-height: 60rpx;border-radius: 10rpx;}view:active {background-color: #2990ff;}}
}
.items {width: 100%;font-size: 32rpx;overflow-y: scroll;height: 300rpx;background-color: #ccc;margin: 40rpx 0;.item {padding: 4rpx 20rpx 0 20rpx;}
}
.pl {margin: 20rpx;background-color: #007aff;padding: 10rpx;
}
.classText {width: 94%;padding: 10rpx;margin: 3%;border: 1rpx solid #ececec;
}
.tipText {padding-left: 20rpx;padding-right: 20rpx;
}
.send {background-color: #ff3e3e;color: #fff;
}
.dis {display: flex;justify-content: space-between;color: #fff;text-align: center;flex-wrap: wrap;view {width: 100%;border-radius: 8rpx;font-size: 32rpx;}
}
.barItems {width: 100%;.barItem {display: flex;justify-content: space-around;// border: 1rpx solid #ececec;height: 100rpx;padding-top: 20rpx;align-items: center;.bar {width: 300rpx;display: flex;justify-content: space-around;view {border: 1rpx solid #ececec;width: 50rpx;height: 50rpx;text-align: center;}input {width: 100rpx;text-align: center;}}}
}
</style>
<style>
page {background-color: #fff;
}
</style>
这个代码里面添加了对应的指令以及解析对应的数据。需要一定的NFC标签卡的数据存储结构的知识,可以参考这篇文章。
浅谈Mifare ultralight原理_liujianhua1989的博客-CSDN博客
大致就是,NFC卡都有16个块存储数据,除了卡片自身的UID 外,还有存储的写入数据。
代码里面加了两个页面的跳转通信,在需要跳转的页面,用这种方法来进行读取设备识别到的NFC标签数据内容。A页面,跳转到蓝牙连接的页面。则在A页面加上:
uni.navigateTo({url: '页面的路径',events: {acceptDataFromOpenedPage: function(data) {console.log(data)},getNFCCompleteCode: function(NFCData) {console.log(NFCData)}},success:function(res){res.eventChannel.emit('acceptDataFromOpenerPage', { data: '从巡检列表过来的' })}})
难点就在于,向设备发送数据,uniapp提供的方法只示例发送0x00的16进制数据。
因为第三方的通行协议上写的指令都是十六进制,所以我这边把指令都转换为10进制对应的值,然后再通过ArrayBuffer和DataView进行转换成数据流。这个在代码的writeDateTo方法可以看到。