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">