<script setup>
|
import { ref, onMounted, inject, watch, reactive } from "vue";
|
import { useRouter } from "vue-router";
|
|
import { BluetoothLE } from "@ionic-native/bluetooth-le";
|
|
import { Buffer } from "buffer";
|
|
import CRC from "crc";
|
|
|
const router = useRouter();
|
const service = ref("00FF");
|
const characteristic = ref("FF01");
|
const key = ref([]);
|
const MAC = ref('A0:DD:6C:23:26:16');
|
|
function setMac(mac) {
|
MAC.value = mac.toUpperCase();
|
}
|
|
// 请求蓝牙权限
|
function _initBL() {
|
BluetoothLE.hasPermission().then(
|
(hasPermission) => {
|
console.log("hasPermission", hasPermission);
|
if (!hasPermission.isEnabled) {
|
BluetoothLE.requestPermission().then(
|
() => {
|
console.log("蓝牙权限已授予");
|
initBL();
|
},
|
(error) => {
|
console.log("蓝牙权限未授予", error);
|
}
|
);
|
} else {
|
console.log("蓝牙权限已授予");
|
initBL();
|
}
|
},
|
(error) => {
|
console.log("蓝牙权限未授予", error);
|
}
|
);
|
}
|
|
// 计算开锁密钥 data 从蓝牙读取到的数据 数组 8个字节
|
function calcKey(data) {
|
// 计算出crc 校验码 结果要低字节在前
|
// let res = CRC.crc16modbus(data).toString(16).split("");
|
// let _res = ["0X" + (res[2] + res[3]), "0X" + (res[0] + res[1])];
|
|
let res = CRC.crc16modbus(data);
|
let _res = [res & 0xff, (res >> 8) & 0xff];
|
// console.log("res", res, CRC.crc16modbus(data), _res, "=============");
|
let __res = [..._res, data[6], data[7]];
|
// 遇到0 变成128
|
// 然后再前两字节相乘 结果高字节放前 后两字节相乘 高字节在前
|
// console.log("__res", __res, "=============");
|
__res = __res.map((v) => (v > 0 ? v : 128));
|
let a = __res[0] * __res[1];
|
let b = __res[2] * __res[3];
|
// console.log("a, b", a, b, "=============");
|
|
return [a >> 8, a & 0xff, b >> 8, b & 0xff];
|
}
|
|
// 检查蓝牙是否可用
|
async function checkBluetoothEnabled() {
|
try {
|
const isEnabled = await BluetoothLE.isEnabled();
|
console.log("蓝牙是否可用", isEnabled, "=============");
|
|
if (!isEnabled.isEnabled) {
|
console.log("蓝牙不可用 请求开启", "=============");
|
}
|
console.log("return true", "=============");
|
|
return true;
|
} catch (error) {
|
console.log("蓝牙不可用", error);
|
return false;
|
}
|
}
|
|
// 初始化蓝牙
|
async function initBL(params) {
|
const isBluetoothEnabled = await checkBluetoothEnabled();
|
if (!isBluetoothEnabled) {
|
console.log("蓝牙不可用,无法初始化");
|
return;
|
}
|
// 初始化蓝牙
|
BluetoothLE.initialize().subscribe(
|
(result) => {
|
console.log("蓝牙初始化成功", result);
|
|
// scan();
|
},
|
(error) => {
|
console.log("蓝牙初始化失败", error);
|
}
|
);
|
}
|
|
const connect = async () => {
|
if (!MAC.value) {
|
console.log("MAC 为空");
|
return false;
|
}
|
// return new Promise((resolve, reject) => {
|
BluetoothLE.connect(
|
{
|
address: MAC.value,
|
},
|
(device) => {
|
console.log("连接成功", device);
|
},
|
(error) => {
|
console.log("连接失败", error);
|
}
|
).subscribe({
|
next: (device) => {
|
console.log("连接 next", device);
|
if (device.status === "connected") {
|
// connected.value = true;
|
getServer();
|
// resolve(true);
|
// } else {
|
// resolve(false);
|
}
|
},
|
error: (error) => {
|
console.error("连接失败2", error);
|
// resolve(false);
|
},
|
complete: () => {
|
console.log("连接完成");
|
// resolve(true);
|
},
|
});
|
// });
|
};
|
|
const disconnect = async () => {
|
if (!MAC.value) {
|
console.log("MAC 为空");
|
return false;
|
}
|
try {
|
await BluetoothLE.disconnect({
|
address: MAC.value,
|
});
|
console.log("断开成功");
|
// connected.value = false;
|
} catch (error) {
|
console.log("断开失败", error);
|
}
|
};
|
|
const scan = async () => {
|
if (!MAC.value) {
|
console.log("MAC 为空");
|
return false;
|
}
|
try {
|
await BluetoothLE.startScan().subscribe({
|
next: (device) => {
|
console.log("发现设备", device);
|
if (device.address && device.address.toUpperCase() === MAC.value) {
|
BluetoothLE.stopScan();
|
connect();
|
}
|
},
|
error: (error) => {
|
console.error("扫描设备失败", error);
|
},
|
complete: () => {
|
console.log("扫描设备完成");
|
},
|
});
|
} catch (error) {
|
console.log("扫描失败", error);
|
}
|
};
|
|
// 发现服务和 发现特征
|
function getServer() {
|
if (!MAC.value) {
|
console.log("MAC 为空");
|
return false;
|
}
|
console.log("getServer", "发现服务", "=============");
|
|
BluetoothLE.discover({
|
address: MAC.value,
|
})
|
.then((res) => {
|
console.log("res", res, "=============");
|
|
const { services } = res;
|
console.log("发现服务", services, JSON.stringify(services));
|
})
|
.catch((error) => {
|
console.log("发现服务失败", error);
|
});
|
}
|
|
// 读取数据
|
async function read() {
|
if (!MAC.value) {
|
console.log("MAC 为空");
|
return false;
|
}
|
console.log("read", service.value, characteristic.value, "=============");
|
return BluetoothLE.read({
|
address: MAC.value,
|
service: service.value,
|
characteristic: characteristic.value,
|
})
|
.then((data) => {
|
console.log("读取数据成功", data);
|
let str = data.value;
|
|
let bytes = BluetoothLE.encodedStringToBytes(data.value);
|
let readStr = Array.from(bytes)
|
.map((v) => v.toString(16))
|
.join(" ");
|
let res = calcKey(Array.from(bytes));
|
key.value = res;
|
console.log(
|
"str:",
|
str,
|
"=======readStr:",
|
readStr,
|
"=======bytes:",
|
bytes,
|
"=======res:",
|
res,
|
"============="
|
);
|
})
|
.catch((error) => {
|
console.log("读取数据失败", error);
|
});
|
}
|
|
// 写入数据
|
function write() {
|
if (!MAC.value) {
|
console.log("MAC 为空");
|
return false;
|
}
|
console.log(
|
"write",
|
service.value,
|
characteristic.value,
|
"=============",
|
key.value
|
);
|
|
return BluetoothLE.write({
|
address: MAC.value,
|
service: service.value,
|
characteristic: characteristic.value,
|
// value: new Uint8Array([0xff, 0x05, 0x00, 0x01, 0xff, 0x00, 0xc8, 0x24]),
|
value: BluetoothLE.bytesToEncodedString(new Uint8Array(key.value)),
|
})
|
.then(() => {
|
console.log("写入数据成功");
|
})
|
.catch((error) => {
|
console.log("写入数据失败", error);
|
});
|
}
|
|
function close() {
|
if (!MAC.value) {
|
console.log("MAC 为空");
|
return false;
|
}
|
try {
|
BluetoothLE.close({
|
address: MAC.value,
|
});
|
console.log('close 成功', '=============');
|
|
} catch (error) {
|
console.log('close 失败', error, '=============');
|
|
}
|
}
|
onMounted(() => {
|
_initBL();
|
});
|
|
</script>
|
|
<template>
|
<div class="page">
|
<!-- 头部 -->
|
<div class="card header">
|
<div class="title">鸿蒙电子智能锁</div>
|
|
<div class="">
|
<span>安全智能</span>
|
<span>操作便捷</span>
|
<el-button @click="scan">扫描</el-button>
|
<el-button @click="disconnect">断开</el-button>
|
<el-button @click="close">关闭</el-button>
|
<el-button @click="connect">连接</el-button>
|
<el-button @click="read">读取</el-button>
|
<el-button @click="write">写入</el-button>
|
</div>
|
<div class="">助力智慧化机房、机柜的安全管理</div>
|
<!-- 扫码 -->
|
</div>
|
<!-- 统计 -->
|
<div class="card disc">
|
<div class="dis-item">
|
<div class="icon house"></div>
|
<div class="name">管理机房</div>
|
<div class="num">4</div>
|
</div>
|
<div class="dis-item">
|
<div class="icon lock-ai"></div>
|
<div class="name">管理锁具</div>
|
<div class="num">4</div>
|
</div>
|
<div class="dis-item open">
|
<div class="icon lock-open"></div>
|
<div class="name">当前开启</div>
|
<div class="num">4</div>
|
</div>
|
<div class="dis-item close">
|
<div class="icon lock-close"></div>
|
<div class="name">当前关闭</div>
|
<div class="num">4</div>
|
</div>
|
</div>
|
<!-- 锁具告警 -->
|
<div class="card alarm">
|
<div class="title-level1">锁具告警</div>
|
<!-- 滚动区 -->
|
<div class="scroll-wraper posR">
|
<div class="scroll pos-full">
|
<div class="alarm-item">
|
<div class="date">2024-11-06</div>
|
<div class="time">17:25:10</div>
|
<div class="id">ID2020000226514</div>
|
<div class="lockName">蓝牙机柜锁1</div>
|
<div class="state">开锁异常</div>
|
</div>
|
<div class="alarm-item">
|
<div class="date">2024-11-06</div>
|
<div class="time">17:25:10</div>
|
<div class="id">ID2020000226514</div>
|
<div class="lockName">蓝牙机柜锁1</div>
|
<div class="state">开锁异常</div>
|
</div>
|
<div class="alarm-item">
|
<div class="date">2024-11-06</div>
|
<div class="time">17:25:10</div>
|
<div class="id">ID2020000226514</div>
|
<div class="lockName">蓝牙机柜锁1</div>
|
<div class="state">开锁异常</div>
|
</div>
|
<div class="alarm-item">
|
<div class="date">2024-11-06</div>
|
<div class="time">17:25:10</div>
|
<div class="id">ID2020000226514</div>
|
<div class="lockName">蓝牙机柜锁1</div>
|
<div class="state">开锁异常</div>
|
</div>
|
</div>
|
</div>
|
<!-- 更多告警 -->
|
<a href="javascript:void(0);" class="">更多告警</a>
|
</div>
|
<!-- 实时动态 -->
|
<div class="card dynamic">
|
<div class="title-level1">实时动态</div>
|
<div class="scroll-wraper posR">
|
<div class="scroll pos-full">
|
<div class="rt-item">
|
<div class="date">2024-11-06</div>
|
<div class="time">17:25:10</div>
|
<div class="id">ID2020000226514</div>
|
<div class="lockName">蓝牙机柜锁1</div>
|
<div class="state">开锁成功</div>
|
</div>
|
<div class="rt-item">
|
<div class="date">2024-11-06</div>
|
<div class="time">17:25:10</div>
|
<div class="id">ID2020000226514</div>
|
<div class="lockName">蓝牙机柜锁1</div>
|
<div class="state">开锁成功</div>
|
</div>
|
<div class="rt-item">
|
<div class="date">2024-11-06</div>
|
<div class="time">17:25:10</div>
|
<div class="id">ID2020000226514</div>
|
<div class="lockName">蓝牙机柜锁1</div>
|
<div class="state">闭锁成功</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<style scoped lang="less">
|
.page {
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
.card {
|
margin-left: 6px;
|
margin-right: 6px;
|
margin-bottom: 12px;
|
background: #fff;
|
border-radius: 6px;
|
display: flex;
|
flex-direction: column;
|
&.header {
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
padding-left: 3em;
|
font-size: 12px;
|
color: #4f98f6;
|
background: #81d3f8;
|
border-radius: 0;
|
margin-left: 0;
|
margin-right: 0;
|
flex: 36;
|
.title {
|
font-weight: bold;
|
font-size: 20px;
|
}
|
span {
|
display: inline-block;
|
margin-right: 2em;
|
}
|
}
|
&.disc {
|
flex: 19.6;
|
display: grid;
|
grid-template-columns: repeat(4, 1fr);
|
gap: 10px;
|
.dis-item {
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
align-items: center;
|
.icon {
|
background: #81d3f8;
|
background-repeat: no-repeat;
|
background-position: center center;
|
background-size: 60% auto;
|
width: 18vw;
|
height: 18vw;
|
border-radius: 50%;
|
&.house {
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3e%3cpath fill='currentColor' d='m12 5.15l-8 3.2V19h2v-6q0-.825.588-1.412T8 11h8q.825 0 1.413.588T18 13v6h2V8.35zM2 19V8.35q0-.625.338-1.125T3.25 6.5l8-3.2q.35-.15.75-.15t.75.15l8 3.2q.575.225.913.725T22 8.35V19q0 .825-.587 1.413T20 21h-4v-8H8v8H4q-.825 0-1.412-.587T2 19m7 2v-2h2v2zm2-3v-2h2v2zm2 3v-2h2v2zM8 11h8z'%3e%3c/path%3e%3c/svg%3e");
|
}
|
&.lock-ai {
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24' %3e %3cg fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' color='currentColor' %3e %3cpath d='m12.308 18l-1.461-4.521a.72.72 0 0 0-.693-.479a.72.72 0 0 0-.693.479L8 18m7-5v5m-6.462-1.5h3.231' %3e%3c/path%3e %3cpath d='M4.268 18.845c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345' %3e%3c/path%3e %3cpath d='M7.5 9V6.5a4.5 4.5 0 0 1 9 0V9'%3e%3c/path%3e %3c/g%3e %3c/svg%3e");
|
}
|
&.lock-open {
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3e%3cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='1.5' d='M14 10V7c0-2.21 1.79-4 4-4s4 1.79 4 4v3M4.6 10h10.8c.88 0 1.6.72 1.6 1.6v7c0 1.32-1.08 2.4-2.4 2.4H5.4C4.08 21 3 19.92 3 18.6v-7c0-.88.72-1.6 1.6-1.6'%3e%3c/path%3e%3c/svg%3e");
|
}
|
&.lock-close {
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='1em' height='1em' viewBox='0 0 24 24'%3e%3cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-miterlimit='10' stroke-width='1.5' d='M8 10V7c0-2.21 1.79-4 4-4s4 1.79 4 4v3m-9.4 0h10.8c.88 0 1.6.72 1.6 1.6v7c0 1.32-1.08 2.4-2.4 2.4H7.4C6.08 21 5 19.92 5 18.6v-7c0-.88.72-1.6 1.6-1.6'%3e%3c/path%3e%3c/svg%3e");
|
}
|
}
|
}
|
}
|
&.alarm {
|
flex: 29;
|
}
|
&.dynamic {
|
flex: 29;
|
}
|
.scroll-wraper {
|
flex: 1;
|
}
|
.scroll {
|
// -webkit-overflow-scrolling: touch;
|
overflow-y: auto;
|
}
|
}
|
}
|
</style>
|