公司做的项目有需要连接第三方的蓝牙设备,要求Android、IOS都要连接这台蓝牙设备,然后去识别NFC标签。
那个设备是德科物联的,DK309的设备,在这记录一下开发的过程。
首先呢,去了解了一下蓝牙设备的一些基础知识,这个可以在很多网站上都可以查得到。大致过程就是:
1. 我这边首先拿到了第三方的设备,然后尝试用自己的手机的蓝牙去连接,一直连接不上,然后找到了厂家,他们给了测试用的APK。结合网络上查询到的知识,得到一个新的知识点。并不是所有蓝牙设备连接的时候都需要确认匹配的那个pin码,以前连耳机这些都是需要点击确认的。
2. 然后就是,厂家只有原生的Android和IOS的方法,并没有那种在uniapp上使用的那种。然后就是下载手机端测试蓝牙连接的软件,IOS和Android平台都有一个叫LightBlue的软件,可以测试和设备的通信。
3. 然后就是基于uniapp的蓝牙连接的过程,有很多可以参考的文档,我这边是参考的这些文章:
https://www.cnblogs.com/mlw1814011067/p/16708559.html
蓝牙设备有设备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">
<view
v-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 = i
let 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].name
if (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-00805F9B34FB
uni.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 exception
console.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 00
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);
},
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 = finalResult
console.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方法可以看到。