From 5d8ae7b6dee4ddaaf795ce2c612198b5b90ae2b1 Mon Sep 17 00:00:00 2001 From: Silent YANG <yang@vicicode.com> Date: Thu, 25 Jan 2024 21:41:55 +0800 Subject: [PATCH] update --- index.html | 2 +- src/components/footer/index.vue | 4 +- src/components/navbar/index.vue | 64 ++--- src/config/settings.json | 5 +- src/router/guard/userLoginInfo.ts | 36 +-- src/router/index.ts | 2 +- src/router/routes/externalModules/arco.ts | 10 - src/router/routes/externalModules/faq.ts | 9 +- src/router/routes/modules/dashboard.ts | 20 +- src/router/routes/modules/exception.ts | 4 +- src/router/routes/modules/form.ts | 6 +- src/router/routes/modules/list.ts | 2 +- src/router/routes/modules/profile.ts | 4 +- src/router/routes/modules/result.ts | 4 +- src/router/routes/modules/user.ts | 2 +- src/router/routes/modules/visualization.ts | 6 +- src/utils/serial.js | 243 ++++++++++++++++++ .../dashboard/workplace/components/banner.vue | 17 +- 18 files changed, 316 insertions(+), 124 deletions(-) delete mode 100644 src/router/routes/externalModules/arco.ts create mode 100644 src/utils/serial.js diff --git a/index.html b/index.html index d094eab..dc84d3f 100644 --- a/index.html +++ b/index.html @@ -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"> diff --git a/src/components/footer/index.vue b/src/components/footer/index.vue index 9a250cc..f532505 100644 --- a/src/components/footer/index.vue +++ b/src/components/footer/index.vue @@ -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> diff --git a/src/components/navbar/index.vue b/src/components/navbar/index.vue index 20cf5bf..57b5548 100644 --- a/src/components/navbar/index.vue +++ b/src/components/navbar/index.vue @@ -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"> diff --git a/src/config/settings.json b/src/config/settings.json index ef20b23..b0cee59 100644 --- a/src/config/settings.json +++ b/src/config/settings.json @@ -13,5 +13,8 @@ "device": "desktop", "tabBar": false, "menuFromServer": false, - "serverMenu": [] + "serverMenu": [], + "connectState": false, + "firmwareVersion": "", + "connectPort": null } diff --git a/src/router/guard/userLoginInfo.ts b/src/router/guard/userLoginInfo.ts index 7a06895..d96f4a0 100644 --- a/src/router/guard/userLoginInfo.ts +++ b/src/router/guard/userLoginInfo.ts @@ -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(); }); } diff --git a/src/router/index.ts b/src/router/index.ts index e230a4b..01839b4 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -13,7 +13,7 @@ const router = createRouter({ routes: [ { path: '/', - redirect: 'login', + redirect: 'dashboard/workplace', }, { path: '/login', diff --git a/src/router/routes/externalModules/arco.ts b/src/router/routes/externalModules/arco.ts deleted file mode 100644 index d9a76eb..0000000 --- a/src/router/routes/externalModules/arco.ts +++ /dev/null @@ -1,10 +0,0 @@ -export default { - path: 'https://arco.design', - name: 'arcoWebsite', - meta: { - locale: 'menu.arcoWebsite', - icon: 'icon-link', - requiresAuth: true, - order: 8, - }, -}; diff --git a/src/router/routes/externalModules/faq.ts b/src/router/routes/externalModules/faq.ts index 232b81d..85797ef 100644 --- a/src/router/routes/externalModules/faq.ts +++ b/src/router/routes/externalModules/faq.ts @@ -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, }, }; diff --git a/src/router/routes/modules/dashboard.ts b/src/router/routes/modules/dashboard.ts index baeae09..6551b27 100644 --- a/src/router/routes/modules/dashboard.ts +++ b/src/router/routes/modules/dashboard.ts @@ -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: ['*'], + // }, + // }, ], }; diff --git a/src/router/routes/modules/exception.ts b/src/router/routes/modules/exception.ts index dac1ccc..c8dd20f 100644 --- a/src/router/routes/modules/exception.ts +++ b/src/router/routes/modules/exception.ts @@ -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; diff --git a/src/router/routes/modules/form.ts b/src/router/routes/modules/form.ts index 5c8682f..77a2260 100644 --- a/src/router/routes/modules/form.ts +++ b/src/router/routes/modules/form.ts @@ -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; diff --git a/src/router/routes/modules/list.ts b/src/router/routes/modules/list.ts index ba0bba6..6fa82e0 100644 --- a/src/router/routes/modules/list.ts +++ b/src/router/routes/modules/list.ts @@ -35,4 +35,4 @@ const LIST: AppRouteRecordRaw = { ], }; -export default LIST; +// export default LIST; diff --git a/src/router/routes/modules/profile.ts b/src/router/routes/modules/profile.ts index 4c396fc..4ae10f4 100644 --- a/src/router/routes/modules/profile.ts +++ b/src/router/routes/modules/profile.ts @@ -19,10 +19,10 @@ const PROFILE: AppRouteRecordRaw = { meta: { locale: 'menu.profile.basic', requiresAuth: true, - roles: ['admin'], + roles: ['*'], }, }, ], }; -export default PROFILE; +// export default PROFILE; diff --git a/src/router/routes/modules/result.ts b/src/router/routes/modules/result.ts index 52d281c..fa83e4b 100644 --- a/src/router/routes/modules/result.ts +++ b/src/router/routes/modules/result.ts @@ -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; diff --git a/src/router/routes/modules/user.ts b/src/router/routes/modules/user.ts index 6390474..600de04 100644 --- a/src/router/routes/modules/user.ts +++ b/src/router/routes/modules/user.ts @@ -35,4 +35,4 @@ const USER: AppRouteRecordRaw = { ], }; -export default USER; +// export default USER; diff --git a/src/router/routes/modules/visualization.ts b/src/router/routes/modules/visualization.ts index aefa2b1..ef111f2 100644 --- a/src/router/routes/modules/visualization.ts +++ b/src/router/routes/modules/visualization.ts @@ -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; diff --git a/src/utils/serial.js b/src/utils/serial.js new file mode 100644 index 0000000..0dceb35 --- /dev/null +++ b/src/utils/serial.js @@ -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 } \ No newline at end of file diff --git a/src/views/dashboard/workplace/components/banner.vue b/src/views/dashboard/workplace/components/banner.vue index e41ec9e..5adc43c 100644 --- a/src/views/dashboard/workplace/components/banner.vue +++ b/src/views/dashboard/workplace/components/banner.vue @@ -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">