1
0
Fork 0
mirror of https://github.com/silenty4ng/k5web synced 2025-04-06 16:47:24 +00:00
This commit is contained in:
Silent YANG 2024-01-25 21:41:55 +08:00
parent 57baa3566f
commit 5d8ae7b6de
18 changed files with 316 additions and 124 deletions

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="zh-cmn">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico">

View file

@ -1,5 +1,7 @@
<template>
<a-layout-footer class="footer">Arco Pro</a-layout-footer>
<a-layout-footer class="footer">
<a href="https://github.com/silenty4ng/k5web" target="_blank">K5Web</a>
</a-layout-footer>
</template>
<script lang="ts" setup></script>

View file

@ -24,42 +24,7 @@
</div>
<ul class="right-side">
<li>
<a-tooltip :content="$t('settings.search')">
<a-button class="nav-btn" type="outline" :shape="'circle'">
<template #icon>
<icon-search />
</template>
</a-button>
</a-tooltip>
</li>
<li>
<a-tooltip :content="$t('settings.language')">
<a-button
class="nav-btn"
type="outline"
:shape="'circle'"
@click="setDropDownVisible"
>
<template #icon>
<icon-language />
</template>
</a-button>
</a-tooltip>
<a-dropdown trigger="click" @select="changeLocale as any">
<div ref="triggerBtn" class="trigger-btn"></div>
<template #content>
<a-doption
v-for="item in locales"
:key="item.value"
:value="item.value"
>
<template #icon>
<icon-check v-show="item.value === currentLocale" />
</template>
{{ item.label }}
</a-doption>
</template>
</a-dropdown>
<a-button type="primary" @click="connectIt">{{ appStore.connectState ? '断开' : '连接' }}</a-button>
</li>
<li>
<a-tooltip
@ -116,6 +81,7 @@
import useLocale from '@/hooks/locale';
import useUser from '@/hooks/user';
import Menu from '@/components/menu/index.vue';
import { connect, disconnect, sendPacket, readPacket } from '@/utils/serial.js';
const appStore = useAppStore();
const userStore = useUserStore();
@ -162,6 +128,32 @@
Message.success(res as string);
};
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
const connectIt = async () => {
if(appStore.connectState == false){
const _connect = await connect();
if(!_connect){
alert('连接失败');
return;
}
const version = await eeprom_init(_connect);
appStore.updateSettings({ connectState: true, connectPort: _connect, firmwareVersion: version });
}else{
disconnect(appStore.connectPort);
appStore.updateSettings({ connectState: false, connectPort: null, firmwareVersion: "" });
}
}
const eeprom_init = async (port: any) => {
const packet = new Uint8Array([0x14, 0x05, 0x04, 0x00, 0xff, 0xff, 0xff, 0xff]);
await sendPacket(port, packet);
const response = await readPacket(port, 0x15);
const decoder = new TextDecoder();
const version = new Uint8Array(response.slice(4, 4+16));
return decoder.decode(version.slice(0, version.indexOf(0)));
}
</script>
<style scoped lang="less">

View file

@ -13,5 +13,8 @@
"device": "desktop",
"tabBar": false,
"menuFromServer": false,
"serverMenu": []
"serverMenu": [],
"connectState": false,
"firmwareVersion": "",
"connectPort": null
}

View file

@ -1,43 +1,9 @@
import type { Router, LocationQueryRaw } from 'vue-router';
import NProgress from 'nprogress'; // progress bar
import { useUserStore } from '@/store';
import { isLogin } from '@/utils/auth';
export default function setupUserLoginInfoGuard(router: Router) {
router.beforeEach(async (to, from, next) => {
NProgress.start();
const userStore = useUserStore();
if (isLogin()) {
if (userStore.role) {
next();
} else {
try {
await userStore.info();
next();
} catch (error) {
await userStore.logout();
next({
name: 'login',
query: {
redirect: to.name,
...to.query,
} as LocationQueryRaw,
});
}
}
} else {
if (to.name === 'login') {
next();
return;
}
next({
name: 'login',
query: {
redirect: to.name,
...to.query,
} as LocationQueryRaw,
});
}
next();
});
}

View file

@ -13,7 +13,7 @@ const router = createRouter({
routes: [
{
path: '/',
redirect: 'login',
redirect: 'dashboard/workplace',
},
{
path: '/login',

View file

@ -1,10 +0,0 @@
export default {
path: 'https://arco.design',
name: 'arcoWebsite',
meta: {
locale: 'menu.arcoWebsite',
icon: 'icon-link',
requiresAuth: true,
order: 8,
},
};

View file

@ -1,10 +1,9 @@
export default {
path: 'https://arco.design/vue/docs/pro/faq',
name: 'faq',
path: 'https://www.vicicode.com/',
name: '作者BD8DFN',
meta: {
locale: 'menu.faq',
icon: 'icon-question-circle',
locale: '作者BD8DFN',
requiresAuth: true,
order: 9,
order: 8,
},
};

View file

@ -23,16 +23,16 @@ const DASHBOARD: AppRouteRecordRaw = {
},
},
{
path: 'monitor',
name: 'Monitor',
component: () => import('@/views/dashboard/monitor/index.vue'),
meta: {
locale: 'menu.dashboard.monitor',
requiresAuth: true,
roles: ['admin'],
},
},
// {
// path: 'monitor',
// name: 'Monitor',
// component: () => import('@/views/dashboard/monitor/index.vue'),
// meta: {
// locale: 'menu.dashboard.monitor',
// requiresAuth: true,
// roles: ['*'],
// },
// },
],
};

View file

@ -19,7 +19,7 @@ const EXCEPTION: AppRouteRecordRaw = {
meta: {
locale: 'menu.exception.403',
requiresAuth: true,
roles: ['admin'],
roles: ['*'],
},
},
{
@ -45,4 +45,4 @@ const EXCEPTION: AppRouteRecordRaw = {
],
};
export default EXCEPTION;
// export default EXCEPTION;

View file

@ -19,7 +19,7 @@ const FORM: AppRouteRecordRaw = {
meta: {
locale: 'menu.form.step',
requiresAuth: true,
roles: ['admin'],
roles: ['*'],
},
},
{
@ -29,10 +29,10 @@ const FORM: AppRouteRecordRaw = {
meta: {
locale: 'menu.form.group',
requiresAuth: true,
roles: ['admin'],
roles: ['*'],
},
},
],
};
export default FORM;
// export default FORM;

View file

@ -35,4 +35,4 @@ const LIST: AppRouteRecordRaw = {
],
};
export default LIST;
// export default LIST;

View file

@ -19,10 +19,10 @@ const PROFILE: AppRouteRecordRaw = {
meta: {
locale: 'menu.profile.basic',
requiresAuth: true,
roles: ['admin'],
roles: ['*'],
},
},
],
};
export default PROFILE;
// export default PROFILE;

View file

@ -19,7 +19,7 @@ const RESULT: AppRouteRecordRaw = {
meta: {
locale: 'menu.result.success',
requiresAuth: true,
roles: ['admin'],
roles: ['*'],
},
},
{
@ -35,4 +35,4 @@ const RESULT: AppRouteRecordRaw = {
],
};
export default RESULT;
// export default RESULT;

View file

@ -35,4 +35,4 @@ const USER: AppRouteRecordRaw = {
],
};
export default USER;
// export default USER;

View file

@ -19,7 +19,7 @@ const VISUALIZATION: AppRouteRecordRaw = {
meta: {
locale: 'menu.visualization.dataAnalysis',
requiresAuth: true,
roles: ['admin'],
roles: ['*'],
},
},
{
@ -30,10 +30,10 @@ const VISUALIZATION: AppRouteRecordRaw = {
meta: {
locale: 'menu.visualization.multiDimensionDataAnalysis',
requiresAuth: true,
roles: ['admin'],
roles: ['*'],
},
},
],
};
export default VISUALIZATION;
// export default VISUALIZATION;

243
src/utils/serial.js Normal file
View file

@ -0,0 +1,243 @@
async function connect() {
if (!('serial' in navigator)) {
alert('当前浏览器不支持网页串口功能,请使用 Chrome, Edge, Opera 浏览器。');
return null;
}
try {
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 38400 });
return port;
} catch (error) {
console.error('Error connecting to the serial port:', error);
return null;
}
}
async function disconnect(port) {
try {
if (port && port.readable) {
// Close the port if it's open
await port.close();
console.log('Serial port disconnected.');
} else {
console.warn('Serial port is not open.');
}
} catch (error) {
console.error('Error closing the serial port:', error);
}
}
function xor(data) {
let data_xor = new Uint8Array(data); // prevent mutation of the original data
const k5_xor_array = new Uint8Array([
0x16, 0x6c, 0x14, 0xe6, 0x2e, 0x91, 0x0d, 0x40,
0x21, 0x35, 0xd5, 0x40, 0x13, 0x03, 0xe9, 0x80
]);
for (let i = 0; i < data_xor.length; i++) {
data_xor[i] ^= k5_xor_array[i % k5_xor_array.length];
}
return data_xor;
}
function crc16xmodem(data, crc = 0) {
const poly = 0x1021;
for (let i = 0; i < data.length; i++) {
crc ^= data[i] << 8;
for (let j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ poly;
} else {
crc <<= 1;
}
}
crc &= 0xffff;
}
return crc;
}
function packetize(data) {
const header = new Uint8Array([0xab, 0xcd]);
const length = new Uint8Array([data.length & 0xff, (data.length >> 8) & 0xff]);
const crc = new Uint8Array([crc16xmodem(data) & 0xff, (crc16xmodem(data) >> 8) & 0xff]);
const unobfuscatedData = new Uint8Array([...data, ...crc]); // crc is added before xor, and xor is applied to data and crc
const obfuscatedData = xor(unobfuscatedData);
const footer = new Uint8Array([0xdc, 0xba]);
const packet = new Uint8Array([...header, ...length, ...obfuscatedData, ...footer]);
return packet;
}
function unpacketize(packet) {
const length = new Uint8Array([packet[2], packet[3]]);
const obfuscatedData = packet.slice(4, packet.length - 4);
if (obfuscatedData.length !== length[0] + (length[1] << 8)) {
throw ('Packet length does not match the length field.');
}
return xor(obfuscatedData);
}
// Known commands:
// 0x30 - Present version information, seemingly ignored by the radio
// 0x19 - Flash block write request. Usually sent in blocks of 100 bytes.
// Known responses:
// 0x18 - Radio is in bootloader mode and ready to flash. This packet is spammed by the radio until flashing begins.
// 0x1a - FLash block was written successfully. This packet is sent after each 0x19 write request.
/**
* Waits for a packet from the radio. The packet data is returned as a Uint8Array.
* @param {SerialPort} port - The serial port to read from.
* @param {number} expectedData - The first byte of the expected packet. (just a byte, not uint8array)
* @param {number} timeout - The timeout in milliseconds.
* @returns {Promise<Uint8Array>} - A promise that resolves with the received packet or gets rejected on timeout.
*/
async function readPacket(port, expectedData, timeout = 1000) {
// Create a reader to read data from the serial port
const reader = port.readable.getReader();
let buffer = new Uint8Array();
let timeoutId; // Store the timeout ID to clear it later
try {
return await new Promise((resolve, reject) => {
// Event listener to handle incoming data
function handleData({ value, done }) {
if (done) {
// If `done` is true, then the reader has been cancelled
reject('Reader has been cancelled.');
console.log('Reader has been cancelled. Current Buffer:', buffer, uint8ArrayToHexString(buffer));
return;
}
// Append the new data to the buffer
buffer = new Uint8Array([...buffer, ...value]);
// Strip the beginning of the buffer until the first 0xAB byte
// This is done to ensure that the buffer does not contain any incomplete packets
while (buffer.length > 0 && buffer[0] !== 0xAB) {
buffer = buffer.slice(1);
}
// Process packets while there's enough data in the buffer
while (buffer.length >= 4 && buffer[0] === 0xAB && buffer[1] === 0xCD) {
const payloadLength = buffer[2] + (buffer[3] << 8);
const totalPacketLength = payloadLength + 8; // Packet length + header + footer
if (buffer.length >= totalPacketLength) {
// Extract the packet from the buffer
const packet = buffer.slice(0, totalPacketLength);
// Verify if the received data forms a valid packet
if (packet[payloadLength + 6] === 0xDC && packet[payloadLength + 7] === 0xBA) {
// Remove the processed packet from the buffer
buffer = buffer.slice(totalPacketLength);
// Continue if the packet is not the expected data
const deobfuscatedData = unpacketize(packet);
if (deobfuscatedData[0] !== expectedData) {
console.log('Unexpected packet received:', deobfuscatedData);
continue;
}
// Resolve with the deobfuscated data if it matches the expected data
resolve(deobfuscatedData);
return;
} else {
// If the packet is not valid, discard the first byte and try again
buffer = buffer.slice(1);
}
} else {
// Not enough data in the buffer to form a complete packet
// Break the loop and wait for more data
break;
}
}
// Continue reading data
reader.read().then(handleData).catch(error => {
console.error('Error reading data from the serial port:', error);
reject(error);
return;
});
}
// Subscribe to the data event to start listening for incoming data
reader.read().then(handleData).catch(error_1 => {
console.error('Error reading data from the serial port:', error_1);
reject(error_1);
return;
});
// Set the timeout to reject the Promise if the packet is not received within the specified time
timeoutId = setTimeout(() => {
reader.cancel().then(() => {
reject('Timeout: Packet not received within the specified time.');
return;
}).catch(error_2 => {
console.error('Error cancelling reader:', error_2);
reject(error_2);
return;
});
}, timeout);
});
} finally {
// Clear the timeout when the promise is settled (resolved or rejected)
clearTimeout(timeoutId);
// Release the reader in the finally block to ensure it is always released
reader.releaseLock();
}
}
/**
* Sends a packet to the radio.
* @param {SerialPort} port - The serial port to write to.
* @param {Uint8Array} data - The packet data to send.
* @returns {Promise<void>} - A promise that resolves when the packet is sent.
* @throws {Error} - If the packet could not be sent.
*/
async function sendPacket(port, data) {
try {
// create writer for port
const writer = port.writable.getWriter();
// prepare packet
const packet = packetize(data);
// send packet
//console.log('Sending packet:', packet);
await writer.write(packet);
// close writer
writer.releaseLock();
} catch (error) {
console.error('Error sending packet:', error);
console.log('Error sending packet. Aborting.');
return Promise.reject(error);
}
}
/**
* Converts a Uint8Array to a hexadecimal string, mostly for debugging purposes.
*
* @param {Uint8Array} uint8Array - The Uint8Array to convert.
* @returns {string} The hexadecimal representation of the Uint8Array without separators.
*/
function uint8ArrayToHexString(uint8Array) {
return Array.from(uint8Array)
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('');
}
export { connect, disconnect, sendPacket, readPacket, uint8ArrayToHexString }

View file

@ -1,24 +1,21 @@
<template>
<a-col class="banner">
<a-col :span="8">
<a-col>
<a-typography-title :heading="5" style="margin-top: 0">
欢迎你~
{{ appStore.connectState ? "欢迎你~,连接成功!" : "欢迎你~,点击右上角“连接”按钮连接手台。" }}
</a-typography-title>
</a-col>
<a-divider class="panel-border" />
<a-card v-show="appStore.connectState" :style="{ width: '360px', marginTop: '2em', marginBottom: '2em' }" title="手台信息">
当前固件版本{{ appStore.firmwareVersion }}
</a-card>
</a-col>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useUserStore } from '@/store';
import { useAppStore } from '@/store';
const userStore = useUserStore();
const userInfo = computed(() => {
return {
name: userStore.name,
};
});
const appStore = useAppStore();
</script>
<style scoped lang="less">