diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..76add87 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 170a7d3..fe03b36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@vueuse/core": "^10.9.0", "ansi_up": "^6.0.2", - "element-plus": "^2.7.3", + "element-plus": "^2.8.1", "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.21", @@ -979,31 +979,29 @@ } }, "node_modules/@volar/language-core": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.4.tgz", - "integrity": "sha512-ROfPepDxZ5Eq+Unbx3M9QcHT7MoE9tYdbkuzLTtxG5rfkEi5RwsDPncjANMOq/gHhIIDlWgqWwS2nXWMGsuj4w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0.tgz", + "integrity": "sha512-FTla+khE+sYK0qJP+6hwPAAUwiNHVMph4RUXpxf/FIPKUP61NFrVZorml4mjFShnueR2y9/j8/vnh09YwVdH7A==", "dev": true, "dependencies": { - "@volar/source-map": "2.1.4" + "@volar/source-map": "2.4.0" } }, "node_modules/@volar/source-map": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.4.tgz", - "integrity": "sha512-mCg8IiPZmHZVzqL4Owg+BzQ5ZTG1cVwATxrkrFPZpcAin97Xa3MbchxVhHtHTWTT8ER8bJh5xVjeVxsSN++FUA==", - "dev": true, - "dependencies": { - "muggle-string": "^0.4.0" - } + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0.tgz", + "integrity": "sha512-2ceY8/NEZvN6F44TXw2qRP6AQsvCYhV2bxaBPWxV9HqIfkbRydSksTFObCF1DBDNBfKiZTS8G/4vqV6cvjdOIQ==", + "dev": true }, "node_modules/@volar/typescript": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.4.tgz", - "integrity": "sha512-Mt7wOLPkomFnUfVpb5IHlPhSpD7FJAn+FHSsovePmqFNQzFLz16wrpHjAkorPiAnP0847w71NL5fIJyWbAsR8Q==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0.tgz", + "integrity": "sha512-9zx3lQWgHmVd+JRRAHUSRiEhe4TlzL7U7e6ulWXOxHH/WNYxzKwCvZD7WYWEZFdw4dHfTD9vUR0yPQO6GilCaQ==", "dev": true, "dependencies": { - "@volar/language-core": "2.1.4", - "path-browserify": "^1.0.1" + "@volar/language-core": "2.4.0", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { @@ -1052,6 +1050,16 @@ "@vue/shared": "3.4.21" } }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "node_modules/@vue/devtools-api": { "version": "6.6.1", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz", @@ -1096,18 +1104,19 @@ } }, "node_modules/@vue/language-core": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.7.tgz", - "integrity": "sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==", + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.29.tgz", + "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==", "dev": true, "dependencies": { - "@volar/language-core": "~2.1.3", + "@volar/language-core": "~2.4.0-alpha.18", "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.4.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", - "path-browserify": "^1.0.1", - "vue-template-compiler": "^2.7.14" + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" @@ -1533,9 +1542,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001599", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz", - "integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==", + "version": "1.0.30001667", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", + "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", "dev": true, "funding": [ { @@ -1793,12 +1802,12 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "devOptional": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1977,9 +1986,9 @@ "dev": true }, "node_modules/element-plus": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz", - "integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.8.1.tgz", + "integrity": "sha512-p11/6w/O0+hGvPhiN3jrcgh+XG+eg5jZlLdQVYvcPHZYhhCh3J3YeZWW1JO/REPES1vevkboT6VAi+9wHA8Dsg==", "dependencies": { "@ctrl/tinycolor": "^3.4.1", "@element-plus/icons-vue": "^2.3.1", @@ -3249,9 +3258,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -3303,9 +3312,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "devOptional": true }, "node_modules/muggle-string": { @@ -3709,9 +3718,9 @@ "dev": true }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -3817,9 +3826,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -3836,8 +3845,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4263,9 +4272,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -5042,6 +5051,12 @@ "vue": ">=3.2.13" } }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "node_modules/vue": { "version": "3.4.21", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz", @@ -5132,31 +5147,21 @@ "vue": "^3.2.0" } }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", - "dev": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, "node_modules/vue-tsc": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.7.tgz", - "integrity": "sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==", + "version": "2.0.29", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz", + "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==", "dev": true, "dependencies": { - "@volar/typescript": "~2.1.3", - "@vue/language-core": "2.0.7", + "@volar/typescript": "~2.4.0-alpha.18", + "@vue/language-core": "2.0.29", "semver": "^7.5.4" }, "bin": { "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "typescript": "*" + "typescript": ">=5.0.0" } }, "node_modules/vuetify": { diff --git a/package.json b/package.json index 2a0690d..de3bf70 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "dependencies": { "@vueuse/core": "^10.9.0", "ansi_up": "^6.0.2", - "element-plus": "^2.7.3", + "element-plus": "^2.8.1", "mitt": "^3.0.1", "pinia": "^2.1.7", "vue": "^3.4.21", diff --git a/src/App.vue b/src/App.vue index 746b801..627b695 100644 --- a/src/App.vue +++ b/src/App.vue @@ -10,7 +10,11 @@ import type {ControlMsg, ServerMsg} from "@/api"; import {ControlEvent, ControlMsgType} from "@/api"; import {routeCtrlMsg, routeModuleServerMsg} from "@/router/msgRouter"; import {globalNotify} from "@/composables/notification"; -import {isDevMode} from "@/composables/buildMode"; +import {getTrialDate, getTrialMsg, isDevMode, isOTAEnabled, isTrialMode} from "@/composables/buildMode"; +import {useSystemModule} from "@/composables/useSystemModule"; +import {useDataFlowModule} from "@/composables/useDataFlowModule"; +import {useUpdateModule} from "@/composables/useUpdateModule"; +import {ElMessageBox} from "element-plus"; const wsState = useWsStore(); @@ -38,7 +42,7 @@ let websocketService: IWebsocketService; onMounted(() => { logHelloMessage(); - let host = ""; + let host: string; if (isDevMode()) { host = import.meta.env.VITE_DEVICE_HOST_NAME || "dap.local"; } else { @@ -46,7 +50,21 @@ onMounted(() => { } websocketService = getWebsocketService(); websocketService.init(host, onServerMsg, onClientCtrl); + websocketService.getSocketStatus(); changeFavicon(); + + useSystemModule(); + useDataFlowModule(); + + if (isOTAEnabled()) { + useUpdateModule(); + } + + if (isTrialMode()) { + ElMessageBox.alert(getTrialMsg(), getTrialDate(), { + confirmButtonText: '好的', + }); + } }); onUnmounted(() => { @@ -55,10 +73,16 @@ onUnmounted(() => { + + diff --git a/src/api/apiDataFlow.ts b/src/api/apiDataFlow.ts new file mode 100644 index 0000000..1b8613a --- /dev/null +++ b/src/api/apiDataFlow.ts @@ -0,0 +1,104 @@ +import {type ApiJsonMsg} from '@/api' +import * as api from "@/api/index"; + +export enum WtDataFlowType { + NONE = 0, + SOCKET = 0x10, + WS_SERVER = 0x11, + WS_CLIENT, + WSS_SERVER, + WSS_CLIENT, + TCP_SERVER, + TCP_CLIENT, + TCP_TLS_SERVER, + TCP_TLS_CLIENT, + UDP_SERVER, + UDP_CLIENT, + PERIPHERAL = 0x80, + GPIO = 0x81, + UART = 0x82, + I2C, + I3C, + SPI, + I2S, + CAN, + RMT, + USB, +} + +export enum WtDataFlowCmd { + UNKNOWN = 0, + GET_INS_LIST = 1, + GET_CUR_INS = 2, + GET_CUR_ATTACH_LIST = 3, + GET_ATTACH_LIST = 4, + ATTACH = 5, + ATTACH_CUR_TO_RECVER = 6, + ATTACH_CUR_TO_SENDER = 7, + DETACH_SINGLE = 8, + DETACH_CUR_FROM = 9, + SET_DATA_TYPE = 10, +} + +export interface IWtDataFlowJsonMsg extends ApiJsonMsg { + data_type?: 3 | 4, + ins_idx?: number, +} + +export interface IPeriphInfo { + periph_num: number; +} + +export interface ISocketInfo { + foreign_port: number; + foreign_ip: string; + local_port: number; +} + +export interface InstanceInfo { + ins_idx: number, + mod_idx: number, + mod_type: number, + port_info: ISocketInfo | IPeriphInfo; +} + +export interface IInstanceList extends ApiJsonMsg { + instances: InstanceInfo[], +} + +export interface AttachInfo { + attach_idx: number, + s_ins_idx: number, + r_ins_idx: number, + data_type: 3 | 4, +} + +export interface IAttachList extends ApiJsonMsg { + attaches: AttachInfo[], +} + +export function wt_data_flow_get_instance_list() { + const jsonMsg: IWtDataFlowJsonMsg = { + cmd: WtDataFlowCmd.GET_INS_LIST, + module: api.WtModuleID.DATA_FLOW, + } + api.sendJsonMsg(jsonMsg); +} + +export function wt_data_flow_attach_cur_to_sender(instance_index: number) { + const jsonMsg: IWtDataFlowJsonMsg = { + cmd: WtDataFlowCmd.ATTACH_CUR_TO_SENDER, + module: api.WtModuleID.DATA_FLOW, + data_type: 3, + ins_idx: instance_index, + } + api.sendJsonMsg(jsonMsg); +} + +export function wt_data_flow_get_attach_list() { + const jsonMsg: IWtDataFlowJsonMsg = { + cmd: WtDataFlowCmd.GET_ATTACH_LIST, + module: api.WtModuleID.DATA_FLOW, + } + api.sendJsonMsg(jsonMsg); +} diff --git a/src/api/apiOTA.ts b/src/api/apiOTA.ts new file mode 100644 index 0000000..9004545 --- /dev/null +++ b/src/api/apiOTA.ts @@ -0,0 +1,61 @@ +import {type ApiJsonMsg, sendJsonMsg, WtModuleID} from '@/api' + +export enum WtOTACmd { + WT_OTA_GET_UPDATE_INFO = 1, /* total_size, ver */ + WT_OTA_DO_UPDATE = 2, /* returns OK, chunk of remaining bytes and total length -> wt_event_manager */ + WT_OTA_GET_PROGRESS = 3, /* returns chunk of remaining bytes and total length */ + WT_OTA_DO_URL_UPDATE = 4, /* force update { url: "https://" } */ +} + +export enum WtOTAProgressStatus { + OK = "OK", + IDLE = "IDLE", + IN_PROGRESS = "IN_PROGRESS", + FAILED = "FAILED", +} + +export interface IOTAProgress extends ApiJsonMsg { + progress: number; + total_size: number; + status: string; +} + +export interface IOTAFmInfo extends ApiJsonMsg { + fm_size: number; + fm_ver: string; + upd_date: string; + upd_note: string; +} + +export function wt_ota_get_update_info() { + const msg: ApiJsonMsg = { + module: WtModuleID.OTA, + cmd: WtOTACmd.WT_OTA_GET_UPDATE_INFO, + }; + sendJsonMsg(msg); +} + +export function wt_ota_do_update() { + const msg: ApiJsonMsg = { + module: WtModuleID.OTA, + cmd: WtOTACmd.WT_OTA_DO_UPDATE, + }; + sendJsonMsg(msg); +} + +export function wt_ota_get_progress() { + const msg: ApiJsonMsg = { + module: WtModuleID.OTA, + cmd: WtOTACmd.WT_OTA_GET_PROGRESS, + }; + sendJsonMsg(msg); +} + +export function wt_ota_do_url_update(url: string) { + const msg: ApiJsonMsg & {url: string} = { + module: WtModuleID.OTA, + cmd: WtOTACmd.WT_OTA_DO_URL_UPDATE, + url: url, + }; + sendJsonMsg(msg); +} diff --git a/src/api/apiSystem.ts b/src/api/apiSystem.ts new file mode 100644 index 0000000..0ac8227 --- /dev/null +++ b/src/api/apiSystem.ts @@ -0,0 +1,45 @@ +import {type ApiJsonMsg, sendJsonMsg, WtModuleID} from '@/api' + +export enum WtSytemCmd { + WT_SYS_GET_FM_INFO = 1, + WT_SYS_REBOOT = 2, + WT_SYS_GET_SYS_INFO = 3, +} + +export interface ISysFmInfo extends ApiJsonMsg { + fm_ver: string; + upd_date: string; +} + +export interface ISysHwInfo extends ApiJsonMsg { + hw_ver: string; + mf_date: string; +} + +export interface ISysInfo { + sn: string; +} + +export function wt_sys_get_fm_info() { + const msg: ApiJsonMsg = { + module: WtModuleID.SYSTEM, + cmd: WtSytemCmd.WT_SYS_GET_FM_INFO, + }; + sendJsonMsg(msg); +} + +export function wt_sys_reboot() { + const msg: ApiJsonMsg = { + module: WtModuleID.SYSTEM, + cmd: WtSytemCmd.WT_SYS_REBOOT, + }; + sendJsonMsg(msg); +} + +export function wt_sys_get_sys_info() { + const msg: ApiJsonMsg = { + module: WtModuleID.SYSTEM, + cmd: WtSytemCmd.WT_SYS_GET_SYS_INFO, + }; + sendJsonMsg(msg); +} diff --git a/src/api/apiUart.ts b/src/api/apiUart.ts new file mode 100644 index 0000000..789532c --- /dev/null +++ b/src/api/apiUart.ts @@ -0,0 +1,110 @@ +import type {ApiBinaryMsg} from "@/api/binDataDef"; +import {WtDataType} from "@/api/binDataDef"; +import {type ApiJsonMsg, sendBinMsg, sendJsonMsg, WtModuleID} from "@/api/index"; + +export enum WtUartCmd { + UNKNOWN = 0, + + /* UART PERIPHERAL */ + GET_AVAILABLE_NUMS = 1, + GET_BAUD = 4, + SET_BAUD = 5, + GET_CONFIG = 6, /* data bits, parity and stop bits */ + SET_CONFIG = 7, + GET_FLOW_CTRL, /* flow control function RTS/CTS*/ + SET_FLOW_CTRL, + GET_PINS_NUM, /* not implemented change pinout function */ + SET_PINS_NUM, /* not implemented */ + GET_MODE, /* not implemented UART/RS485/IrDA */ + SET_MODE, /* not implemented UART/RS485/IrDA */ + + GET_STATUS = 20, /* is uart enabled and other information */ + SET_STATUS, /* set specific uart port disable */ + GET_DATA_TYPE = 22, // 0x03 or 0x04 + SET_DATA_TYPE = 23, // 0x03 or 0x04 + + GET_DEFAULT_NUM = 24, +} + +enum ANSI_ESCAPE_CODE { + REFRESH_WINDOW = '\x1b[7t', + CLEAR_WINDOW = '\x1b[2J' +} + +export interface IUartConfig { + data_bits: 5 | 6 | 7 | 8; + parity : 0 | 1 | 2; + stop_bits: 1 | 15 | 2; +} + +export interface IUartMsgConfig extends ApiJsonMsg, IUartConfig { + sub_mod: number; +} + +export interface IUartMsgBaud extends ApiJsonMsg { + sub_mod: number; + baud: number; +} + +export interface IUartMsgNum extends ApiJsonMsg { + num: number; +} + +export function uart_send_msg(payload: Uint8Array, sub_mod: number) { + /* hard code uart num for now */ + const msg: ApiBinaryMsg = { + sub_mod: sub_mod, + data_type: WtDataType.RAW, + module: WtModuleID.UART, + payload: payload, + } + sendBinMsg(msg); +} + +export function uart_get_baud(uart_num: number) { + const cmd = { + cmd: WtUartCmd.GET_BAUD, + module: WtModuleID.UART, + sub_mod: uart_num, + } + sendJsonMsg(cmd); +} + +export function uart_set_baud(baud: number, uart_num: number) { + const cmd: IUartMsgBaud = { + cmd: WtUartCmd.SET_BAUD, + module: WtModuleID.UART, + baud: baud, + sub_mod: uart_num, + } + sendJsonMsg(cmd); +} + +export function uart_get_config(uart_num: number) { + const cmd = { + cmd: WtUartCmd.GET_CONFIG, + module: WtModuleID.UART, + sub_mod: uart_num, + } + sendJsonMsg(cmd); +} + +export function uart_set_config(uart_config: IUartConfig, uart_num: number) { + const cmd: IUartMsgConfig = { + cmd: WtUartCmd.SET_CONFIG, + module: WtModuleID.UART, + sub_mod: uart_num, + data_bits: uart_config.data_bits, + parity: uart_config.parity, + stop_bits: uart_config.stop_bits, + } + sendJsonMsg(cmd); +} + +export function uart_get_default_num() { + const cmd = { + cmd: WtUartCmd.GET_DEFAULT_NUM, + module: WtModuleID.UART, + } + sendJsonMsg(cmd); +} diff --git a/src/api/index.ts b/src/api/index.ts index be2fa89..cc8b218 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -31,9 +31,11 @@ export interface ServerMsg { } export enum WtModuleID { + SYSTEM = 0, WIFI = 1, DATA_FLOW = 2, UART = 4, + OTA = 5, } export function sendJsonMsg(apiJsonMsg: ApiJsonMsg) { diff --git a/src/assets/icon/arrow_drop_down.svg b/src/assets/icon/arrow_drop_down.svg new file mode 100644 index 0000000..88e7bf1 --- /dev/null +++ b/src/assets/icon/arrow_drop_down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icon/arrow_drop_up.svg b/src/assets/icon/arrow_drop_up.svg new file mode 100644 index 0000000..10c71f5 --- /dev/null +++ b/src/assets/icon/arrow_drop_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icon/translate.svg b/src/assets/icon/translate.svg new file mode 100644 index 0000000..a8471d8 --- /dev/null +++ b/src/assets/icon/translate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icon/trash.svg b/src/assets/icon/trash.svg new file mode 100644 index 0000000..d1d3899 --- /dev/null +++ b/src/assets/icon/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/composables/broadcastChannelDef.ts b/src/composables/broadcastChannelDef.ts index 48cc7af..0f4dbd9 100644 --- a/src/composables/broadcastChannelDef.ts +++ b/src/composables/broadcastChannelDef.ts @@ -1,4 +1,17 @@ -export const toServer = new BroadcastChannel("toServer"); -export const toClient = new BroadcastChannel("toClient"); -export const toWebsocketCtrl = new BroadcastChannel("toWebsocketCtrl"); -export const toClientCtrl = new BroadcastChannel("toClientCtrl"); +// Define a fallback mock class only if BroadcastChannel is undefined +const BC: typeof BroadcastChannel = typeof BroadcastChannel !== 'undefined' + ? BroadcastChannel + : class { + constructor(name: string) { + // no-op + } + postMessage(_: any) {} + close() {} + addEventListener(_: string, __: any) {} + removeEventListener(_: string, __: any) {} + } as unknown as typeof BroadcastChannel; + +export const toServer = new BC("toServer"); +export const toClient = new BC("toClient"); +export const toWebsocketCtrl = new BC("toWebsocketCtrl"); +export const toClientCtrl = new BC("toClientCtrl"); \ No newline at end of file diff --git a/src/composables/buildMode.ts b/src/composables/buildMode.ts index 840a858..19cecdc 100644 --- a/src/composables/buildMode.ts +++ b/src/composables/buildMode.ts @@ -1,3 +1,19 @@ export function isDevMode() { return import.meta.env.VITE_APP_MODE === 'dev'; -} \ No newline at end of file +} + +export function isOTAEnabled() { + return import.meta.env.VITE_ENABLE_OTA === 'true' || false; +} + +export function isTrialMode() { + return import.meta.env.VITE_TRIAL_MODE === "true" || false; +} + +export function getTrialDate() { + return import.meta.env.VITE_TRIAL_DATE || "1970-01-01"; +} + +export function getTrialMsg() { + return import.meta.env.VITE_TRIAL_MSG || "感谢您试用允斯开放固件,若您喜欢,欢迎关注我的B站或者加入允斯群,新项目和更新都会在第一时间在这里发布. 使用愉快^_^"; +} diff --git a/src/composables/notification.ts b/src/composables/notification.ts index 0ffe27a..3c1cc76 100644 --- a/src/composables/notification.ts +++ b/src/composables/notification.ts @@ -2,7 +2,7 @@ import {ElMessage, ElNotification} from "element-plus"; type NotificationType = 'error' | 'warning' | 'info' | 'success' ; -export function globalNotify(msg: string, type: NotificationType) { +export function globalNotify(msg: string, type: NotificationType = "info") { ElMessage({ message: msg, grouping: true, @@ -13,7 +13,7 @@ export function globalNotify(msg: string, type: NotificationType) { }) } -export function globalNotifyRightSide(msg: string, type: NotificationType) { +export function globalNotifyRightSide(msg: string, type: NotificationType = "info") { ElNotification({ message: msg, type: type, diff --git a/src/composables/useDataFlowModule.ts b/src/composables/useDataFlowModule.ts new file mode 100644 index 0000000..665924c --- /dev/null +++ b/src/composables/useDataFlowModule.ts @@ -0,0 +1,44 @@ +import {registerModule} from "@/router/msgRouter"; +import {type ApiJsonMsg, type ControlMsg, ControlMsgType, WtModuleID} from "@/api"; +import {isDevMode} from "@/composables/buildMode"; +import {useDataFlowStore} from "@/stores/useDataFlowStore"; +import {type IInstanceList, WtDataFlowCmd} from "@/api/apiDataFlow"; + + +export function useDataFlowModule() { + const dfStore = useDataFlowStore() + + function onClientCtrl(msg: ControlMsg) { + if (msg.type !== ControlMsgType.WS_EVENT) { + return + } + } + + function onClientMsg(msg: ApiJsonMsg) { + switch (msg.cmd as WtDataFlowCmd) { + case WtDataFlowCmd.GET_INS_LIST: { + const insList = msg as IInstanceList; + dfStore.instanceList = insList.instances; + break; + } + case WtDataFlowCmd.GET_ATTACH_LIST: { + break; + } + default: + break; + } + if (isDevMode()) { + console.log(msg); + } + } + + registerModule(WtModuleID.DATA_FLOW, { + ctrlCallback: onClientCtrl, + serverJsonMsgCallback: onClientMsg, + serverBinMsgCallback: () => {}, + }); +} + + + + diff --git a/src/composables/useSystemModule.ts b/src/composables/useSystemModule.ts new file mode 100644 index 0000000..52a07ce --- /dev/null +++ b/src/composables/useSystemModule.ts @@ -0,0 +1,54 @@ +import {useSystemStore} from "@/stores/useSystemStore"; +import {registerModule} from "@/router/msgRouter"; +import {type ApiJsonMsg, ControlEvent, type ControlMsg, ControlMsgType, WtModuleID} from "@/api"; +import {type ISysFmInfo, type ISysInfo, wt_sys_get_fm_info, wt_sys_get_sys_info, WtSytemCmd} from "@/api/apiSystem"; +import {isDevMode} from "@/composables/buildMode"; + + +export function useSystemModule() { + const sysStore = useSystemStore() + + function onClientCtrl(msg: ControlMsg) { + if (msg.type !== ControlMsgType.WS_EVENT) { + return + } + + if (msg.data === ControlEvent.CONNECTED) { + wt_sys_get_fm_info(); + wt_sys_get_sys_info(); + sysStore.rebootInProgress = false; + } + } + + function onClientMsg(msg: ApiJsonMsg) { + switch (msg.cmd as WtSytemCmd) { + case WtSytemCmd.WT_SYS_REBOOT: + sysStore.rebootInProgress = true; + break; + case WtSytemCmd.WT_SYS_GET_FM_INFO: { + const fm_info = msg as ISysFmInfo; + sysStore.curFmInfo.date = fm_info.upd_date; + sysStore.curFmInfo.ver = fm_info.fm_ver; + break; + } + case WtSytemCmd.WT_SYS_GET_SYS_INFO: { + const sysInfo: ISysInfo = msg as ISysInfo & ApiJsonMsg; + Object.assign(sysStore.sysInfo, sysInfo); + break; + } + } + if (isDevMode()) { + console.log(msg); + } + } + + registerModule(WtModuleID.SYSTEM, { + ctrlCallback: onClientCtrl, + serverJsonMsgCallback: onClientMsg, + serverBinMsgCallback: () => {}, + }); +} + + + + diff --git a/src/composables/useUpdateModule.ts b/src/composables/useUpdateModule.ts new file mode 100644 index 0000000..350e06f --- /dev/null +++ b/src/composables/useUpdateModule.ts @@ -0,0 +1,99 @@ +import {registerModule} from "@/router/msgRouter"; +import {type ApiJsonMsg, ControlEvent, type ControlMsg, ControlMsgType, WtModuleID} from "@/api"; +import {useUpdateStore} from "@/stores/useUpdateStore"; +import { + type IOTAFmInfo, + type IOTAProgress, + WtOTACmd, + WtOTAProgressStatus, + wt_ota_get_progress, + wt_ota_get_update_info, +} from "@/api/apiOTA"; +import {isDevMode} from "@/composables/buildMode"; +import {useSystemStore} from "@/stores/useSystemStore"; + +export function useUpdateModule() { + const updateStore = useUpdateStore() + const sysStore = useSystemStore() + + function onClientCtrl(msg: ControlMsg) { + if (msg.type !== ControlMsgType.WS_EVENT) { + return + } + + if (msg.data === ControlEvent.CONNECTED) { + wt_ota_get_update_info(); + wt_ota_get_progress(); + } + } + + function onClientMsg(msg: ApiJsonMsg) { + switch (msg.cmd as WtOTACmd) { + case WtOTACmd.WT_OTA_GET_UPDATE_INFO: { + const info = msg as IOTAFmInfo; + Object.assign(updateStore.newFmInfo, info); + if (updateStore.newFmInfo.fm_ver !== sysStore.curFmInfo.ver && updateStore.newFmInfo.fm_ver[0] !== '-' + && (updateStore.updateStatus === 'IDLE' || updateStore.updateStatus === 'FAILED')) { + updateStore.canUpdate = true; + } else { + updateStore.canUpdate = false; + } + break; + } + case WtOTACmd.WT_OTA_DO_UPDATE: + break; + case WtOTACmd.WT_OTA_GET_PROGRESS: { + const progress = msg as IOTAProgress; + updateStore.updateStatus = progress.status; + if (progress.total_size !== 0) { + updateStore.updateProgress = (progress.progress / progress.total_size) * 100; + } else { + updateStore.updateProgress = 0; + } + if (progress.status === WtOTAProgressStatus.IDLE) { + if (updateStore.newFmInfo.fm_ver !== sysStore.curFmInfo.ver && updateStore.newFmInfo.fm_ver[0] !== '-') { + updateStore.canUpdate = true; + } else { + updateStore.canUpdate = false; + } + updateStore.clearProgressInterval(); + updateStore.progressBarStatus = ''; + } else if (progress.status === WtOTAProgressStatus.FAILED) { + if (updateStore.newFmInfo.fm_ver !== sysStore.curFmInfo.ver && updateStore.newFmInfo.fm_ver[0] !== '-') { + updateStore.canUpdate = true; + } else { + updateStore.canUpdate = false; + } + updateStore.clearProgressInterval(); + updateStore.progressBarStatus = 'exception'; + } else if (progress.status === WtOTAProgressStatus.IN_PROGRESS) { + updateStore.setProgressInterval(); + updateStore.progressBarStatus = ''; + updateStore.canUpdate = false; + } else if (progress.status === WtOTAProgressStatus.OK) { + updateStore.clearProgressInterval(); + updateStore.canUpdate = false; + updateStore.progressBarStatus = 'success'; + } + break; + } + default: + break; + } + + if (isDevMode()) { + console.log(msg); + } + } + + registerModule(WtModuleID.OTA, { + ctrlCallback: onClientCtrl, + serverJsonMsgCallback: onClientMsg, + serverBinMsgCallback: () => { + }, + }); +} + + + + diff --git a/src/composables/websocket/websocketService.ts b/src/composables/websocket/websocketService.ts index 910f7bc..a21feae 100644 --- a/src/composables/websocket/websocketService.ts +++ b/src/composables/websocket/websocketService.ts @@ -2,7 +2,7 @@ import MyWorker from '@/composables/websocket/ws.sharedworker?sharedworker' import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper"; import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef"; import type {ControlMsg, ServerMsg} from "@/api"; -import {ControlEvent, ControlMsgType} from "@/api"; +import {ControlMsgType} from "@/api"; import {isDevMode} from "@/composables/buildMode"; export interface IWebsocketService { @@ -14,12 +14,13 @@ export interface IWebsocketService { deinit(): void; send(msg: ServerMsg): void; + getSocketStatus(): void; } /** * Websocket that run in a shared worker, shared across tabs */ -class WebsocketShared implements IWebsocketService{ +class WebsocketShared implements IWebsocketService { private static instance: IWebsocketService; private worker: SharedWorker; @@ -82,6 +83,10 @@ class WebsocketShared implements IWebsocketService{ this.ctrlCallback(ev.data); } + + getSocketStatus() { + this.worker.port.postMessage({type: ControlMsgType.WS_GET_STATE} as ControlMsg) + } } class WebsocketClassic implements IWebsocketService{ @@ -115,10 +120,14 @@ class WebsocketClassic implements IWebsocketService{ send(msg: ServerMsg): void { this.socket.send(msg); } + + getSocketStatus(): void { + this.socket.getSocketStatus(); + } } export function getWebsocketService(): IWebsocketService { - if (typeof SharedWorker !== 'undefined') { + if (typeof SharedWorker !== 'undefined' && typeof localStorage !== 'undefined') { return WebsocketShared.getInstance(); } else { return WebsocketClassic.getInstance(); diff --git a/src/composables/websocket/websocketWrapper.ts b/src/composables/websocket/websocketWrapper.ts index 8efede7..3c2828f 100644 --- a/src/composables/websocket/websocketWrapper.ts +++ b/src/composables/websocket/websocketWrapper.ts @@ -1,5 +1,4 @@ - -import type {ApiJsonMsg, ControlMsg, ServerMsg} from "@/api"; +import type {ControlMsg, ServerMsg} from "@/api"; import {ControlEvent, ControlMsgType} from "@/api"; import {isDevMode} from "@/composables/buildMode"; @@ -9,6 +8,8 @@ interface IWebsocket { close(): void; send(msg: ServerMsg): void; + + getSocketStatus(): void; } class WebsocketDummy implements IWebsocket { @@ -20,6 +21,9 @@ class WebsocketDummy implements IWebsocket { send(msg: ServerMsg) { } + + getSocketStatus(): void { + } } class OneTimeWebsocket implements IWebsocket { @@ -61,6 +65,8 @@ class OneTimeWebsocket implements IWebsocket { console.log("No heart beat, break connection"); this.close(); this.clear(); + // } else if (this.socket.readyState === this.socket.CONNECTING) { + // this.close(); } if (isDevMode()) { console.log("interval: ", this.heartBeatTimeCount, "state: ", this.socket.readyState); @@ -159,6 +165,26 @@ class OneTimeWebsocket implements IWebsocket { this.ctrlCallback(msg); this.closeCallback(); } + + getSocketStatus() { + let type: ControlEvent; + switch (this.socket.readyState) { + case WebSocket.CONNECTING: + type = ControlEvent.CONNECTING; + break; + case WebSocket.OPEN: + type = ControlEvent.CONNECTED; + break; + default: + type = ControlEvent.DISCONNECTED; + break; + } + const msg: ControlMsg = { + type: ControlMsgType.WS_EVENT, + data: type, + }; + this.ctrlCallback(msg); + } } export class WebsocketWrapper { @@ -219,4 +245,8 @@ export class WebsocketWrapper { send(msg: ServerMsg) { this.socket.send(msg) } + + getSocketStatus() { + this.socket.getSocketStatus(); + } } diff --git a/src/composables/websocket/ws.sharedworker.ts b/src/composables/websocket/ws.sharedworker.ts index b303a9e..1a106d1 100644 --- a/src/composables/websocket/ws.sharedworker.ts +++ b/src/composables/websocket/ws.sharedworker.ts @@ -1,12 +1,11 @@ import type {ControlMsg, ServerMsg} from "@/api"; - -declare const self: SharedWorkerGlobalScope; - +import {ControlEvent, ControlMsgType} from "@/api"; import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper"; import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef"; -import {ControlEvent, ControlMsgType} from "@/api"; import {isDevMode} from "@/composables/buildMode"; +declare const self: SharedWorkerGlobalScope; + const websocket = new WebsocketWrapper(); let host = ""; @@ -30,6 +29,8 @@ self.onconnect = function(event) { host = e.data.data; websocket.init(host, msgBroadcast, ctrlBroadcast); } + } else if (e.data.type === ControlMsgType.WS_GET_STATE) { + websocket.getSocketStatus(); } }; const msg: ControlMsg = { diff --git a/src/i18n.ts b/src/i18n.ts index 45a36b8..200e587 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -1,19 +1,52 @@ -import { createI18n } from 'vue-i18n'; +import {createI18n} from 'vue-i18n'; import zh from '@/locales/zh' import en from '@/locales/en' +import fr from '@/locales/fr' -// const locale = localStorage.getItem('lang') || 'zh'; -export const locale = 'zh'; +const userLanguage = navigator.language || 'en'; + +// Get the language code (e.g., 'en' from 'en-US') +export const locale = userLanguage.split('-')[0]; +const messages = { + zh, + en, + fr, +} as const; + +type Locale = keyof typeof messages; + +export const availableLanguages = Object.keys(messages); + +// export const locale = 'zh'; +console.log(userLanguage, locale, availableLanguages) const i18n = createI18n({ globalInjection: true, legacy: false, locale: locale, fallbackLocale: 'zh', - messages: { - zh, - // en, - } + messages: messages }); +export function getFlagFromLang(lang: string) { + if (lang === 'zh') { + return '🇨🇳'; + } else if (lang === 'en') { + return '🇺🇸'; + } else if (lang === 'fr') { + return '🇫🇷'; + } + return '🏳️'; +} + +export function setLang(lang: string): void { + if (availableLanguages.includes(lang)) { + i18n.global.locale.value = lang as Locale; + } +} + +export function getLang() { + return i18n.global.locale; +} + export default i18n; diff --git a/src/locales/en.ts b/src/locales/en.ts index 9715b27..fab8bfc 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1,3 +1,204 @@ export default { - disconnected: "disconnected" + emoji: { + flag: "🇺🇸", + }, + disconnected: "Disconnected", + connected: "Connected", + connecting: "Connecting", + use: "use", + author: "author", + studioYunSi: "Yunsi Studio", + authorEmail: "Author email", + TencentQQGroup: "QQ Group", + Discord: "Discord", + BiliBili: "BiliBili", + + suggestion: "suggestion", + feature: "feature", + version: "Version", + releaseTime: "Release Time", + credit: "Credit", + aboutWebHost: "About the Web Host Application", + aboutDebugger: "About the Debugger", + officialWebsite: "Official Website", + email: "Email", + note: "Note", + welcomeMessage: "Welcome to reach out anytime", + serialNumber: "Serial Number", + + ws: { + disconnected: "Disconnected", + connected: "Connected", + connecting: "Connecting", + }, + + page: { + home: "Home", + wifi: "Wi-Fi", + about: "About", + uart: "Uart", + feedback: "Feedback", + close: "Close", + update: "Update", + fullscreen: "Fullscreen", + windowed: "Windowed" + }, + + uart: { + port: "Port", + startCommunication: "Start Communication", + stopCommunication: "Stop Communication", + commonlyUsed: "Common", + baudrate: "Baud Rate", + customBaud: "Custom Baud", + use: "Use", + actual: "Actual", + dataBits: "Data Bits", + stopBits: "Stop Bits", + parity: "Parity", + parityNone: "None", + parityOdd: "Odd", + parityEven: "Even", + flowControl: "Flow Control", + send: "Send", + clear: "Clear", + clearTooltip: "Only clears the display area, can be restored with refresh.", + updateTooltip: "Sync with cache + filter", + autoUpdateTooltip: "Only stop refreshing the display area; the background continues to receive data.", + receive: "Receive", + + displayOptions: "Display Options", + display: "Display", + show: "Show", + text: "Text", + timestamp: "Timestamp", + enable: "Enable", + lineWrap: "Line Wrap", + highlight: "Highlight", + + frameBreakStrategy: "Frame Break Strategy", + priority: "Priority", + rule: "Rule", + ruleTips: + "

Timeout=-1: Disable timeout frame break

" + + "

Timeout=0: Immediate break, any received data is considered complete

" + + "

Match after break: Typical \\n scenario

" + + "

Match before break: For scenarios with special frame headers

" + + "

Fixed byte frame break: Useful for large data transfer, e.g., break frame every 1024 bytes for easy data viewing

", + value: "Value", + timeout: "Timeout", + match: "Match", + byte: "Byte", + begin: "b", + end: "b", + + other: "Other", + decodeAnsiEscapeCodes: "Decode ANSI Escape Codes", + ansiTooltips: + "

ANSI escape codes have many uses for terminals and text, such as changing text colors, among other effects.

\n" + + "

\n Learn more ->\n " + + "https://en.wikipedia.org/wiki/ANSI_escape_code\n

", + filter: "Filter", + textAndEscape: "Text with \\n\\x support", + autoUpdateNewData: "Auto-refresh new data", + updateFrequency: "Data Display Update Interval (ms)", + updateFrequencyTooltip: "Increasing the interval can reduce CPU usage.", + + addHeader: "Add Header", + addFooter: "Add Footer", + + passthrough: "Passthrough", + proxy: "Proxy", + serverPort: "Server Port", + connectedClient: "Connected Client", + refresh: "Refresh", + interface: "Interface", + noClientConnected: "No Client Connected", + + import: "Import", + export: "Export", + reset: "Reset", + resetTooltip: "Takes effect after refreshing the page.", + saveToLocal: "Save to Local", + saveToLocalTooltip: "If multiple pages exist, they will overwrite each other.", + add: "Add", + edit: "Edit", + drag: "Drag", + ipChangeAlert: "Changing the IP address will cause the configuration to be lost.", + + layout: "Layout", + landscape: "Landscape", + portrait: "Portrait", + responsive: "Responsive", + configPannel: "Config", + displayPannel: "Display", + macroPannel: "Quick Send", + autoScrollToBottom: "Auto Scroll", + clearScreen: "Clear", + autoUpdate: "Auto Update", + tempDisplayTooltip: "Data that does not meet the frame-break rules (e.g., not timed out) is temporarily displayed in real-time in this area. If it exceeds 8192 bytes, it will automatically break frames.", + loopSend: "Loop Send", + loopSendTooltip: "The actual frequency is affected by the interface refresh rate. For more accuracy, you can try turning off 'auto-refresh'.", + sendFormat: "Send Format", + cachedFrame: "Cached", + format: "Format", + }, + + wifi: { + settings: "Settings", + setFailed: "Settings failed to set", + setSuccess: "Settings saved", + connection: "Connection", + scanning: "Scanning", + scan: "Scan", + scanDone: "Scan done", + warnWifiName: "Enter Wi-Fi Name", + password: "Password", + connectInfoHTML: "Changing Wi-Fi will disconnect this interface from the passthrough device if not connected through its hotspot.", + connect: "Connect", + mode: "Mode", + save: "Save", + station: "Station", + intelligent: "Smart", + APOnly: "Hotspot Only", + disconnected: "Disconnected", + modeTipsHtml: "

\n" + + "Smart Mode:\n" + + "After connecting to Wi-Fi, the hotspot will turn off automatically after 30 seconds if no device is connected. It will turn on after 5 seconds if disconnected from AP.\n" + + "

\n" + + "

\n" + + "Coexistence Mode:\n" + + "Convenient but impacts stability and increases power consumption.\n" + + "

\n" + + "

\n" + + "Hotspot-Only Mode Drawback:\n" + + "No network connection.\n" + + "

", + enabled: "Enabled", + disabled: "Disabled", + + stationInfo: "Terminal (STA)", + hotspotInfo: "Hotspot (AP)", + signalStrength: "Signal Strength", + gateway: "Gateway", + netmask: "Netmask", + primaryDNS: "Primary DNS", + backupDNS: "Backup DNS", + IPmode: "IP Allocation Mode", + DNSmode: "DNS Mode", + internalAddress: "Internal Address", + + autoIP: "Automatic (DHCP)", + staticIP: "Static IP", + autoDNS: "Automatic (Use Gateway)", + staticDNS: "Static DNS", + APauto_STA: "Smart Hotspot + Persistent Terminal (AP+STA)", + APonly: "Hotspot Only (AP)", + AP_STA: "Persistent Hotspot + Persistent Terminal (AP+STA)", + + connectionSuccess: "Connection Successful", + enterAPName: "Entre the AP name", + debuggerNotConnected: "Debugger not connected", + } + }; \ No newline at end of file diff --git a/src/locales/fr.ts b/src/locales/fr.ts new file mode 100644 index 0000000..d3d43fb --- /dev/null +++ b/src/locales/fr.ts @@ -0,0 +1,204 @@ +export default { + emoji: { + flag: "🇫🇷", + }, + disconnected: "Déconnecté", + connected: "Connecté", + connecting: "Connexion..", + use: "utiliser", + author: "Auteur", + studioYunSi: "Studio Yunsi", + authorEmail: "Email de l'auteur", + TencentQQGroup: "Groupe QQ", + Discord: "Discord", + BiliBili: "BiliBili", + + suggestion: "suggestion", + feature: "fonctionnalité", + version: "Version", + releaseTime: "Date de Publication", + credit: "Remerciements", + aboutWebHost: "À propos de l'Hôte Web", + aboutDebugger: "À propos du Débogueur", + officialWebsite: "Site Officiel", + email: "E-mail", + note: "Remarque", + welcomeMessage: "N'hésitez pas à venir nous solliciter.", + serialNumber: "Numéro de série", + + ws: { + disconnected: "Déconnecté", + connected: "Connecté", + connecting: "Connexion..", + }, + + page: { + home: "Accueil", + wifi: "Wi-Fi", + about: "À propos", + uart: "Uart", + feedback: "Feedback", + close: "Fermer", + update: "Mise à jour", + fullscreen: "Plein écran", + windowed: "Fenêtré", + }, + + uart: { + port: "Port", + startCommunication: "Démarrer la communication", + stopCommunication: "Arrêter la communication", + commonlyUsed: "Fréquemment utilisé", + baudrate: "Taux de Baud", + customBaud: "Baud", + use: "Utiliser", + actual: "Actuel", + dataBits: "Bits de Données", + stopBits: "Bits d'Arrêt", + parity: "Parité", + parityNone: "Aucune", + parityOdd: "Impair(Odd)", + parityEven: "Pair(Even)", + flowControl: "Contrôle de Flux", + send: "Envoyer", + clear: "Effacer", + clearTooltip: "Ne supprime que la zone d'affichage, peut être restaurée en actualisant.", + updateTooltip: "Synchroniser avec le cache + filtrer", + autoUpdateTooltip: "Arrête uniquement le rafraîchissement de la zone d'affichage ; l'arrière-plan continue de recevoir des données.", + receive: "Recevoir", + + displayOptions: "Options d'Affichage", + display: "Affichage", + show: "Afficher", + text: "Texte", + timestamp: "Horodatage", + enable: "Activer", + lineWrap: "Retour à la Ligne", + highlight: "Surligner", + + frameBreakStrategy: "Stratégie de Coupure de Trame", + priority: "Priorité", + rule: "Règle", + ruleTips: + "

Délai d'expiration=-1 : Désactiver la coupure de trame par délai d'expiration

" + + "

Délai d'expiration=0 : Coupure immédiate, toutes données reçues sont considérées complètes

" + + "

Match après coupure : Scénario typique \\n

" + + "

Match avant coupure : Pour des scénarios avec en-têtes de trame spécifiques

" + + "

Coupure de trame par octets fixes : Utile pour le transfert de grandes quantités de données, par exemple, couper la trame tous les 1024 octets pour faciliter la visualisation des données

", + value: "Valeur", + timeout: "Timeout", + match: "Match", + byte: "Byte", + begin: "b", + end: "b", + + other: "Autres", + decodeAnsiEscapeCodes: "Décode Échappement ANSI", + ansiTooltips: + "

Les codes d'échappement ANSI ont de nombreuses utilisations pour les terminaux et le texte, comme changer les couleurs du texte, entre autres effets.

" + + "

\n En savoir plus ->\n " + + "\n" + + "https://en.wikipedia.org/wiki/ANSI_escape_code\n \n

", + filter: "Filtrer", + textAndEscape: "Texte;supporte\\n\\x", + autoUpdateNewData: "Auto-update nouvelles données", + updateFrequency: "Délais rafraîchissement des Données (ms)", + updateFrequencyTooltip: "Augmenter l'intervalle peut réduire l'utilisation des ressources CPU.", + + addHeader: "Ajouter un En-tête", + addFooter: "Ajouter un Pied de page", + + passthrough: "Transmission", + proxy: "Proxy", + serverPort: "Port Serveur", + connectedClient: "Client Connecté", + refresh: "Rafraîchir", + interface: "Interface", + noClientConnected: "Aucun Client Connecté", + + import: "Importer", + export: "Exporter", + reset: "Réinitialiser", + resetTooltip: "Prend effet après le rafraîchissement de la page.", + saveToLocal: "Enregistrer Localement", + saveToLocalTooltip: "S'il existe plusieurs pages, elles se chevaucheront mutuellement.", + add: "Ajouter", + edit: "Éditer", + drag: "Glisser", + ipChangeAlert: "Le changement d'adresse IP entraînera la perte de la configuration.", + + layout: "Disposition", + landscape: "Paysage", + portrait: "Portrait", + responsive: "Résponsive", + configPannel: "Configuration", + displayPannel: "Données", + macroPannel: "Envoie Rapide", + autoScrollToBottom: "Auto Scroll", + clearScreen: "Effacer", + autoUpdate: "Auto Update", + tempDisplayTooltip: "es données qui ne respectent pas les règles de rupture de trame (par exemple : non expirées) s'affichent temporairement en temps réel dans cette zone. Au-delà de 8192 octets, une rupture de trame est automatique.", + loopSend: "Envoi en Boucle", + loopSendTooltip: "La fréquence réelle est influencée par le taux de rafraîchissement de l'interface. Pour plus de précision, vous pouvez essayer de désactiver 'l'actualisation automatique'.", + sendFormat: "Format d'Envoi", + cachedFrame: "Cache", + format: "Format", + }, + + wifi: { + settings: "Paramètres", + setFailed: "Echec d'enregistrement de paramètres", + setSuccess: "Paramètres enregistrés", + connection: "Connexion", + scanning: "Recherche en cours", + scan: "Rechercher", + scanDone: "Fin recherche de Wi-Fi", + warnWifiName: "Entrez le nom du Wi-Fi", + password: "Mot de passe", + connectInfoHTML: "Changer de Wi-Fi déconnectera cette interface du dispositif de transmission s'il ne passe pas par le point d'accès.", + connect: "Connecter", + mode: "Mode", + save: "Enregistrer", + station: "Station", + intelligent: "Intelligent", + APOnly: "Point d'accès uniquement", + disconnected: "Déconnecté", + modeTipsHtml: "

\n" + + "Mode intelligent :\n" + + "Après la connexion au Wi-Fi, le point d'accès s'éteindra automatiquement après 30 secondes si aucun appareil n'est connecté. Il s'allumera après 5 secondes si la connexion AP est perdue.\n" + + "

\n" + + "

\n" + + "Mode coexistence :\n" + + "Pratique mais réduit la stabilité et augmente la consommation d'énergie.\n" + + "

\n" + + "

\n" + + "Inconvénient du mode point d'accès seul :\n" + + "Pas de connexion réseau.\n" + + "

", + enabled: "Activé", + disabled: "Désactivé", + + stationInfo: "Terminal(STA)", + hotspotInfo: "Point d'Accès(AP)", + signalStrength: "Puissance du Signal", + gateway: "Passerelle", + netmask: "Masque de Sous-réseau", + primaryDNS: "DNS Primaire", + backupDNS: "DNS Secondaire", + IPmode: "Mode d'Attribution IP", + DNSmode: "Mode DNS", + internalAddress: "Adresse Interne", + + autoIP: "Automatique (DHCP)", + staticIP: "IP Statique", + autoDNS: "Automatique (gateway)", + staticDNS: "DNS Statique", + APauto_STA: "Point d'Accès Intelligent + Terminal Permanent (AP+STA)", + APonly: "Point d'Accès Seul (AP)", + AP_STA: "Point d'Accès Permanent + Terminal Permanent (AP+STA)", + + connectionSuccess: "Connexion Réussie", + enterAPName: "Entrez le nom du AP", + debuggerNotConnected: "Debugger non connecté", + } +}; \ No newline at end of file diff --git a/src/locales/index.ts b/src/locales/index.ts index 0d4b347..7bff647 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -7,8 +7,8 @@ type NestedKeyOf = { : `${Key}` }[keyof ObjectType & (string | number)]; -type TranslationKeys = NestedKeyOf; +export type TranslationKeys = NestedKeyOf; -export function translate(key: K | string): string { - return i18n.global.t(key.toLowerCase()); +export function translate(key: TranslationKeys | string): string { + return i18n.global.t(key); } diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 17be1d1..74d2c1b 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -1,7 +1,30 @@ export default { + emoji: { + flag: "🇨🇳", + }, disconnected: "未连接", connected: "已连接", connecting: "连接中", + use: "使用", + author: "作者", + studioYunSi: "允斯工作室", + authorEmail: "作者邮箱", + TencentQQGroup: "QQ群", + Discord: "Discord", + BiliBili: "哔哩哔哩", + + suggestion: "建议", + feature: "需求", + version: "版本", + releaseTime: "发布时间", + credit: "鸣谢", + aboutWebHost: "关于网页版上位机", + aboutDebugger: "关于调试器", + officialWebsite: "官网", + email: "邮箱", + note: "备注", + welcomeMessage: "欢迎来打扰啊~", + serialNumber: "序列号", ws: { disconnected: "未连接", @@ -13,8 +36,172 @@ export default { home: "主页", wifi: "Wi-Fi", about: "关于", - uart: "UART透传", + uart: "UART", feedback: "反馈", close: "关闭", + update: "更新", + fullscreen: "全屏", + windowed: "窗口", }, + + uart: { + port: "接口", + startCommunication: "开始数据收发", + stopCommunication: "停止数据收发", + commonlyUsed: "常用", + baudrate: "波特率", + customBaud: "自定义波特率", + use: "使用", + actual: "实际", + dataBits: "数据位", + stopBits: "停止位", + parity: "校验位", + parityNone: "无(None)", + parityOdd: "奇(Odd)", + parityEven: "偶(Even)", + flowControl: "流控制", + send: "发送", + clear: "清空", + clearTooltip: "仅清除显示区域,可用刷新恢复", + updateTooltip: "与缓存同步+过滤", + autoUpdateTooltip: "仅停止刷新显示区,后台继续接收数据", + receive: "接收", + + displayOptions: "显示选项", + display: "显示框", + show: "显示", + text: "文本", + timestamp: "时间戳", + enable: "启用", + lineWrap: "换行", + highlight: "高亮", + + frameBreakStrategy: "断帧策略", + priority: "优先级", + rule: "规则", + ruleTips: + "

超时=-1: 禁用超时断帧

" + + "

超时=0: 当机立断,收到任何数据都视为完整数据

" + + "

匹配断后:典型\\n的场景

" + + "

匹配断前:用于有特殊帧头的场景

" + + "

固定字节断帧:传输大量数据,比如可以每隔1024字节断帧,方便查看数据

", + value: "值", + timeout: "超时", + match: "匹配", + byte: "字节", + begin: "断", + end: "断", + + other: "其他", + decodeAnsiEscapeCodes: "解码ANSI转义码", + ansiTooltips: + "

ANSI转义码对终端和文本有很多作用,比如改变文本颜色等。

\n" + + "

\n" + + " 简单了解->\n" + + " \n" + + " https://yunsi.studio/wireless-debugger/docs/uart-webhost/ansi-escape-code\n" + + " \n" + + "

", + filter: "过滤", + textAndEscape: "文本,支持\\n\\x", + autoUpdateNewData: "新数据自动刷新", + updateFrequency: "数据显示刷新间隔(ms)", + updateFrequencyTooltip: "提高间隔可减少CPU资源的使用", + + addHeader: "增加帧头", + addFooter: "增加帧尾", + + passthrough: "透传", + proxy: "透传", + serverPort: "服务器端口", + connectedClient: "已连接的客户端", + refresh: "刷新", + interface: "接口", + noClientConnected: "无客户端连接", + + import: "导入", + export: "导出", + reset: "重置", + resetTooltip: "刷新页面后生效", + saveToLocal: "保存到本地", + saveToLocalTooltip: "若存在多个页面,会相互覆盖", + add: "添加", + edit: "编辑", + drag: "拖拽", + ipChangeAlert: "IP地址改变会导致配置丢失", + + layout: "布局", + landscape: "横/行", + portrait: "竖/列", + responsive: "自适应", + configPannel: "设置窗", + displayPannel: "数据窗", + macroPannel: "快捷窗", + autoScrollToBottom: "自动滚动到底部", + clearScreen: "清屏", + autoUpdate: "自动刷新", + tempDisplayTooltip: "未满足断帧规则的数据(如:未超时),暂时实时显示在此区域。超过8192字节,自动断帧;", + loopSend: "循环发送", + loopSendTooltip: "实际频率受界面刷新率影响,如需要更精确,可以尝试关闭‘自动刷新’", + sendFormat: "发送格式", + cachedFrame: "缓存帧数", + format: "格式化", + }, + + wifi: { + settings: "配置", + setFailed: "设置失败", + setSuccess: "配置成功", + connection: "连接", + scanning: "扫描中", + scan: "扫描", + scanDone: "扫描成功", + warnWifiName: "请输入WIFI名", + password: "密码", + connectInfoHTML: "如果不是通过透传器的热点连接,更换Wi-Fi将导致此界面与透传器断开连接。", + connect: "连接", + mode: "模式", + save: "保存", + station: "终端", + intelligent: "智能", + APOnly: "仅开启热点", + disconnected: "未连接", + modeTipsHtml: "

\n" + + "智能模式:\n" + + "成功连接至Wi-Fi后,如果此设备的热点未被其他设备连接,将在30秒后自动关闭热点;如果此设备与AP断开连接,将在5秒后自动开启热点\n" + + "

\n" + + "

\n" + + "热点+终端共存模式:\n" + + "方便使用,但是影响稳定性,增加功耗\n" + + "

\n" + + "

\n" + + "单热点模式缺点:\n" + + "无网络\n" + + "

", + enabled: "已开启", + disabled: "未开启", + + stationInfo: "终端(STA)", + hotspotInfo: "自发热点(AP)", + signalStrength: "信号强度", + gateway: "网关", + netmask: "掩码", + primaryDNS: "首选DNS", + backupDNS: "备用DNS", + IPmode: "IP分配模式", + DNSmode: "DNS模式", + internalAddress: "内网地址", + + autoIP: "自动 (DHCP)", + staticIP: "静态IP", + autoDNS: "自动 (使用网关)", + staticDNS: "静态DNS", + APauto_STA: "智能热点+常开终端 (AP+STA)", + APonly: "仅开启热点 (AP)", + AP_STA: "常开热点+常开终端 (AP+STA)", + + connectionSuccess: "连接成功", + enterAPName: "请输入AP名", + debuggerNotConnected: "调试器未连接", + } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 5841e0b..5689295 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,8 @@ import '@/assets/page.css' import '@/assets/navigation.css' import 'element-plus/dist/index.css'; +import 'vuetify/styles' +import { createVuetify } from 'vuetify' import { createApp } from 'vue' import { createPinia } from 'pinia' @@ -17,5 +19,6 @@ const app = createApp(App) app.use(createPinia()) app.use(i18n); app.use(router) +app.use(createVuetify()) app.mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts index 72ca4a5..06b7b01 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,12 +1,46 @@ -import {createRouter, createWebHistory} from 'vue-router' -import Home from '@/views/Home.vue' +import {createRouter, createWebHistory, type RouteLocationNormalizedLoaded} from 'vue-router' import Wifi from '@/views/Wifi.vue' import Feedback from '@/views/Feedback.vue' import About from '@/views/About.vue' import Uart from '@/views/Uart.vue' import Page404 from '@/views/404.vue' +import Update from '@/views/Update.vue' import {translate} from "@/locales"; +import {isOTAEnabled} from "@/composables/buildMode"; +import {reactive, watch} from "vue"; +import {getLang} from "@/i18n"; +const languageState = reactive({ + currentLanguage: getLang(), // Get the current language from your i18n setup +}); + +interface AppRouteMeta { + title?: string; + titleKey?: string; +} + +const updateMetaTitles = () => { + router.getRoutes().forEach(route => { + const meta = route.meta as AppRouteMeta; + if (meta.titleKey) { + meta.title = translate(meta.titleKey); + } + }); +}; + +function updateDocumentTitle(route: RouteLocationNormalizedLoaded) { + const meta = route.meta as AppRouteMeta; + document.title = typeof route.meta.title === 'string' + ? `${translate(meta.titleKey || "")} | ${translate('studioYunSi')}` + : '允斯调试器'; +} + +// Watch for language changes to update the titles dynamically +watch(() => languageState.currentLanguage, () => { + // Recompute all route meta titles + updateMetaTitles(); + updateDocumentTitle(router.currentRoute.value); +}, {deep: true}); const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -14,41 +48,49 @@ const router = createRouter({ { path: '/', name: 'home', - meta: {title: translate("page.home")}, - // component: Wifi - redirect: () => '/wifi', + meta: { titleKey: 'page.home' }, + redirect: () => '/uart', }, { path: '/home:ext(.*)', - meta: {title: translate("page.home")}, + meta: { titleKey: 'page.home' }, redirect: () => '/', }, { path: '/wifi:ext(.*)', - meta: {title: translate('page.wifi')}, + meta: { titleKey: 'page.wifi' }, component: Wifi, }, { path: '/about:ext(.*)', - meta: {title: translate('page.about')}, + meta: { titleKey: 'page.about' }, component: About, }, { path: '/uart:ext(.*)', - meta: {title: translate('page.uart')}, + meta: { titleKey: 'page.uart' }, component: Uart, }, { path: '/feedback:ext(.*)', - meta: {title: translate('page.feedback')}, + meta: { titleKey: 'page.feedback' }, name: 'feedback', component: Feedback, - }, { - path: '/:catchAll(.*)', // This will match all paths that aren't matched by above routes + }, { + path: '/update:ext(.*)', + meta: { titleKey: 'page.update' }, + name: 'update', + component: isOTAEnabled() ? Update : Page404, + }, { + path: '/:catchAll(.*)', // Catch-all route for 404 name: 'NotFound', component: Page404, }, ] }) +// Update document title dynamically router.beforeEach((to, from, next) => { - document.title = typeof to.meta.title === 'string' ? to.meta.title + " | 允斯工作室" : '允斯调试器'; + updateDocumentTitle(to); next(); }); -export default router +// Initialize titles on load +updateMetaTitles(); + +export default router; \ No newline at end of file diff --git a/src/router/msgRouter.ts b/src/router/msgRouter.ts index 368df05..0dbffa0 100644 --- a/src/router/msgRouter.ts +++ b/src/router/msgRouter.ts @@ -12,6 +12,9 @@ const moduleMap = new Map(); export function registerModule(moduleId: number, moduleCallback: IModuleCallback): boolean { if (moduleMap.has(moduleId)) { + if (isDevMode()) { + console.log("module ", moduleId, "already registered"); + } return false; } diff --git a/src/stores/dataViewerStore.ts b/src/stores/dataViewerStore.ts new file mode 100644 index 0000000..3785750 --- /dev/null +++ b/src/stores/dataViewerStore.ts @@ -0,0 +1,1155 @@ +import {defineStore} from "pinia"; +import { + computed, + type Ref, + type ShallowReactive, + ref, + shallowReactive, + watch, +} from "vue"; +import {AnsiUp} from 'ansi_up' +import {debouncedWatch} from "@vueuse/core"; +import {type IUartConfig, uart_send_msg} from "@/api/apiUart"; +import {isDevMode} from "@/composables/buildMode"; +import {useUartStore} from "@/stores/useUartStore"; + +interface IDataArchive { + time: number; + isRX: boolean; + data: Uint8Array; +} + +export interface IDataBuf { + time: string; + isRX: boolean; + type: number; + data: Uint8Array; + str: string; + hex: string; + hexdump: string; +} + +function decodeUtf8(u8Arr: Uint8Array) { + try { + const decoder = new TextDecoder(); + const decodedText = decoder.decode(u8Arr); // Attempt to decode + return decodedText.replace(/\uFFFD/g, ''); // Remove all � characters + } catch (error) { + return ""; + } +} + +function escapeHTML(text: string) { + const element = document.createElement('p'); + element.textContent = text; + return element.innerHTML; +} + +function unescapeString(str: string): Uint8Array { + const resultArray = []; + let i = 0; + + while (i < str.length) { + if (str[i] === '\\' && i + 1 < str.length) { + i++; + switch (str[i]) { + case 'n': + resultArray.push(0x0A); // LF + break; + case 'r': + resultArray.push(0x0D); // CR + break; + case 't': + resultArray.push(0x09); // Tab + break; + case 'b': + resultArray.push(0x08); // Backspace + break; + case 'v': + resultArray.push(0x0B); // Vertical Tab + break; + case '0': + resultArray.push(0x00); // Null + break; + case 'x': + if (i + 2 < str.length) { + resultArray.push(parseInt(str.substr(i + 1, 2), 16)); + i += 2; + } + break; + case 'u': + if (i + 4 < str.length) { + const codePoint = parseInt(str.substr(i + 1, 4), 16); + if (codePoint < 0x80) { + resultArray.push(codePoint); + } else if (codePoint < 0x800) { + resultArray.push(192 | (codePoint >> 6)); + resultArray.push(128 | (codePoint & 63)); + } else if (codePoint < 0x10000) { + resultArray.push(224 | (codePoint >> 12)); + resultArray.push(128 | ((codePoint >> 6) & 63)); + resultArray.push(128 | (codePoint & 63)); + } else { + resultArray.push(240 | (codePoint >> 18)); + resultArray.push(128 | ((codePoint >> 12) & 63)); + resultArray.push(128 | ((codePoint >> 6) & 63)); + resultArray.push(128 | (codePoint & 63)); + } + i += 4; + } + break; + default: + // This handles any escaped character that is not a special character + resultArray.push(str[i].charCodeAt(0)); + break; + } + } else { + // This section now properly converts a direct character to UTF-8 when it's beyond the ASCII range + const code = str.codePointAt(i); + if (code == undefined) { + continue + } + if (code < 0x80) { + resultArray.push(code); + } else if (code < 0x800) { + resultArray.push(192 | (code >> 6)); + resultArray.push(128 | (code & 63)); + } else if (code < 0x10000) { + resultArray.push(224 | (code >> 12)); + resultArray.push(128 | ((code >> 6) & 63)); + resultArray.push(128 | (code & 63)); + } else { + resultArray.push(240 | (code >> 18)); + resultArray.push(128 | ((code >> 12) & 63)); + resultArray.push(128 | ((code >> 6) & 63)); + resultArray.push(128 | (code & 63)); + i++; // Move past the second part of the surrogate pair + } + } + i++; + } + + return new Uint8Array(resultArray); +} + +const zeroPad = (num: number, places: number) => String(num).padStart(places, '0'); +const ansi_up = new AnsiUp(); +ansi_up.escape_html = false; + +const ANSI_REFRESH_WINDOW = new Uint8Array([0x1B, 0x5B, 0x37, 0x74]); +const ANSI_CLEAR_WINDOW = new Uint8Array([0x1B, 0x5B, 0x32, 0x4A]); + +/* quick HEX lookup table */ +const byteToHex: string[] = new Array(256); +for (let n = 0; n <= 0xff; ++n) { + byteToHex[n] = n.toString(16).padStart(2, "0").toUpperCase(); +} + +function u8toHexString(buff: Uint8Array) { + const hexOctets = []; // new Array(buff.length) is even faster (preallocates necessary array size), then use hexOctets[i] instead of .push() + if (buff.length === 0) { + return "" + } + + for (let i = 0; i < buff.length; ++i) + hexOctets.push(byteToHex[buff[i]]); + + return hexOctets.join(" "); +} + +function u8toHexdump(buffer: Uint8Array) { + const lines: string[] = []; + const bytesPerRow = 16; + + for (let lineStart = 0; lineStart < buffer.length; lineStart += bytesPerRow) { + let result = lineStart.toString(16).padStart(4, '0') + ": "; + let ascii = ""; + + // Process each byte in the row + for (let i = 0; i < bytesPerRow; i++) { + const byteIndex = lineStart + i; + if (byteIndex >= buffer.length) { + // Pad the row if it's shorter than the full width + result += " "; + } else { + const byte = buffer[byteIndex]; + result += byteToHex[byte] + " "; + + // Prepare the ASCII representation, non-printable as '.' + if (byte >= 32 && byte <= 126) { + ascii += String.fromCharCode(byte); + } else { + ascii += "."; + } + } + if (i === 8) { + result += " " + } + } + + result += "|" + ascii + "|" + " ".repeat(16 - ascii.length); + lines.push(result); + } + + return strToHTML(escapeHTML(lines.join('\n'))); +} + +function strToHTML(str: string) { + return str.replace(/\n/g, '
') // Replace newline with
tag + .replace(/\t/g, ' ') // Replace tab with spaces (or you could use ' ' for single spaces) + .replace(/\r/, '') + .replace(/ /g, ' '); +} + +function isArrayContained(subArray: Uint8Array, mainArray: Uint8Array, + sub_start: number = 0, main_start: number = 0) { + if (subArray.length === 0) return -1; + outerLoop: for (let i = main_start; i <= mainArray.length - subArray.length; i++) { + // Check if subArray is found starting at position i in mainArray + for (let j = sub_start; j < subArray.length; j++) { + if (mainArray[i + j] !== subArray[j]) { + continue outerLoop; // Continue the outer loop if mismatch found + } + } + return i; // subArray is found within mainArray + } + return -1; +} + +const baudArr = [ + { + start: 300, + count: 9, + }, { + start: 14400, + count: 9, + } +] + +function generateBaudArr(results: { baud: number; }[]) { + for (let i = 0; i < baudArr.length; ++i) { + let start = baudArr[i].start; + for (let j = 0; j < baudArr[i].count; ++j) { + results.push({baud: start}); + start += start; + } + } + + results.sort((a, b) => { + return a.baud - b.baud; + }); +} + +export interface ISettings { + testNumber: number; + configTab: string; + showText: boolean; + showHex: boolean; + showHexdump: boolean; + showTimestamp: boolean; + lineWrap: boolean; + RxHexdumpColor: string; + TxHexdumpColor: string; + frameBreakSequence: string; + frameBreakAfterSequence: boolean; + frameBreakDelay: number; + frameBreakSize: number; + frameBreak: any[]; // Specify more precise types + decodeANSI: boolean; + frameFilterValue: string; + autoUpdate: boolean; + updateInterval: number; + sendFramePrefix: string; + sendFrameSuffix: string; + loopSendInterval: number; + sendBoxIsText: boolean; + sendBox: string; + winLeft: any; + winRight: any; + winAutoLayout: boolean; + winLayoutMode: string; + macroButtons: macroItem[]; + ipChangeAlert: boolean; +} + +interface macroItem { + value: string; + label: string; + id: number; +} + +export const useDataViewerStore = defineStore('text-viewer', () => { + const uartStore = useUartStore() + + /* private value */ + const predefineColors = [ + '#f0f9eb', + '#ecf4ff', + 'rgba(255, 69, 0, 0.68)', + 'rgb(255, 120, 0)', + 'hsv(51, 100, 98)', + 'hsva(120, 40, 94, 0.5)', + 'hsl(181, 100%, 37%)', + 'hsla(209, 100%, 56%, 0.73)', + 'rgba(85,155,197,0.44)', + ] + + const predefinedUartBaudFrequent = Object.freeze([ + { + baud: 115200, + }, { + baud: 921600, + }, { + baud: 9600, + } + ]) + + const uartBaudList: { baud: number; }[] = []; + generateBaudArr(uartBaudList); + + /* public value */ + const configPanelTab = ref("second"); + const enableAnsiDecode = ref(true); + + + /* + * FRAME BREAK STUFS + * */ + + let RxSegment: Uint8Array = new Uint8Array(0); + const RxRemainHexdump = ref(""); + + const frameBreakSequence = ref("\\n"); + const frameBreakAfterSequence = ref(true); + const frameBreakSequenceNormalized = computed(() => { + return unescapeString(frameBreakSequence.value); + }); + const frameBreakDelay = ref(0); + let frameBreakDelayTimeoutID: number = -1; + + function frameBreakFlush() { + if (RxSegment.length) { + addItem(RxSegment, true); + RxSegment = new Uint8Array(); + RxRemainHexdump.value = ""; + } + } + + function frameBreakRefreshTimout() { + if (frameBreakDelay.value > 0) { + if (frameBreakDelayTimeoutID >= 0) { + clearTimeout(frameBreakDelayTimeoutID); + frameBreakDelayTimeoutID = -1; + } + frameBreakDelayTimeoutID = setTimeout(() => { + frameBreakFlush() + }, frameBreakDelay.value); + } else { + if (frameBreakDelayTimeoutID >= 0) { + clearTimeout(frameBreakDelayTimeoutID); + frameBreakDelayTimeoutID = -1; + } + if (frameBreakDelay.value === 0) { + frameBreakFlush(); + } + } + } + + debouncedWatch(() => frameBreakDelay.value, () => { + frameBreakRefreshTimout(); + console.log("timeout called"); + }, {debounce: 300}); + + + + const frameBreakSize = ref(0); + const frameBreakRules = ref([{ + name: 'timeout', + type: 'number', + min: -1, + draggable: false, + transformData: breakDelay, + }, { + name: 'match', + type: 'text', + draggable: true, + transformData: breakSequence, + }, { + name: 'byte', + type: 'number', + min: 0, + draggable: true, + transformData: breakSize, + },]) + + function breakDelay(inputArray: Uint8Array[]) { + return {result: [] as Uint8Array[], remain: true}; + } + + function breakSequence(inputArray: Uint8Array[]) { + if (frameBreakSequenceNormalized.value.length <= 0) { + return {result: inputArray, remain: true}; + } + const result: Uint8Array[] = []; + /* if split after, the matched array is appended to the previous */ + const appendedLength = frameBreakAfterSequence.value ? frameBreakSequenceNormalized.value.length : 0; + /* else after the first match, skip the matchArr at the beginning of array in subsequent match */ + const skipLength = frameBreakAfterSequence.value ? 0 : frameBreakSequenceNormalized.value.length; + let startIndex = 0; + + inputArray.forEach(array => { + startIndex = 0; + let matchIndex = isArrayContained(frameBreakSequenceNormalized.value, array, 0, startIndex); + + while (matchIndex !== -1) { + const endIndex = matchIndex + appendedLength; + if (startIndex !== endIndex) { + result.push(array.subarray(startIndex, endIndex)); + } + startIndex = endIndex; + matchIndex = isArrayContained(frameBreakSequenceNormalized.value, array, + 0, startIndex + skipLength); + } + // Add the last segment if there's any remaining part of the array + if (startIndex < array.length) { + result.push(array.subarray(startIndex, array.length)); + } + }); + const remain = startIndex < inputArray[inputArray.length - 1].length; + return {result, remain}; + } + + function breakSize(inputArray: Uint8Array[]) { + if (frameBreakSize.value <= 0) { + return {result: inputArray, remain: true}; + } + const result: Uint8Array[] = []; + inputArray.forEach(item => { + for (let start = 0; start < item.length; start += frameBreakSize.value) { + const end = Math.min(start + frameBreakSize.value, item.length); + result.push(item.subarray(start, end)); + } + }); + const remain = result[result.length - 1].length < frameBreakSize.value; + return {result, remain}; + } + + function reloadFrameBreak() { + /* function and ref can not be stored in localStorage */ + for (let i = 0; i < frameBreakRules.value.length; i++) { + if (frameBreakRules.value[i].name === "timeout") { + frameBreakRules.value[i].transformData = breakDelay; + } else if (frameBreakRules.value[i].name === "match") { + frameBreakRules.value[i].transformData = breakSequence; + } else { + frameBreakRules.value[i].transformData = breakSize; + } + } + } + + function addStringMessage(input: string, isRX: boolean, doSend: boolean = false) { + const unescaped = unescapeString(input); + addSegment(unescaped, isRX, doSend); + } + + function addSegment(input: Uint8Array, isRX: boolean, doSend: boolean = false) { + if (input.length <= 0) { + if (isDevMode()) { + console.log("input size =0"); + } + return; + } + + let frames: Uint8Array[] = [] + const data= new Uint8Array(RxSegment.length + input.length); + let remain = true; + data.set(RxSegment); + data.set(input, RxSegment.length); + RxSegment = data; + + frames.push(RxSegment); + /* ready for adding new items */ + for (let i = 1; i < frameBreakRules.value.length; i++) { + const ret: {result: Uint8Array[], remain: boolean} = frameBreakRules.value[i].transformData(frames); + /* check if last item changed */ + if (!ret.remain || ret.result[ret.result.length - 1].length !== frames[frames.length - 1].length) { + remain = ret.remain; + } + frames = ret.result; + } + + if (frameBreakDelay.value !== 0 && remain) { + RxSegment = frames.pop() || new Uint8Array(); + if (frameBreakDelay.value > 0) { + frameBreakRefreshTimout(); + } + } else { + RxSegment = new Uint8Array(); + } + + for (let i = 0; i < frames.length; i++) { + addItem(frames[i], isRX, doSend); + } + + if (RxSegment.length > 8192) { + addItem(RxSegment, isRX, doSend); + RxSegment = new Uint8Array(); + } + } + + const frameBreakRet = { + frameBreakSequence, + frameBreakAfterSequence, + frameBreakDelay, + frameBreakSize, + frameBreakRules, + RxRemainHexdump, + addStringMessage, + addSegment, + } + + const showText = ref(true); + const showHex = ref(false); + const showHexdump = ref(false); + const enableLineWrap = ref(false); + const showTimestamp = ref(true); + const showVirtualScroll = ref(true); + + const RxHexdumpColor = ref("#f0f9eb"); + const RxTotalByteCount = ref(0); + const RxByteCount = ref(0); + + const TxHexdumpColor = ref("#ecf4ff"); + const TxTotalByteCount = ref(0); + const TxByteCount = ref(0); + + let TxByteCountLocal = 0; + let TxTotalByteCountLocal = 0; + let RxByteCountLocal = 0; + let RxTotalByteCountLocal = 0; + + function clearRxCounter() { + RxByteCountLocal = 0; + RxTotalByteCountLocal = 0; + } + + function clearTxCounter() { + TxByteCountLocal = 0; + TxTotalByteCountLocal = 0; + } + + const enableFilter = ref(true); + const forceToBottom = ref(true); + const filterChanged = ref(false); + + const textPrefixValue = ref(""); + const textSuffixValue = ref("\\r\\n"); + const hasAddedText = computed(() => { + return textPrefixValue.value.length > 0 || textSuffixValue.value.length > 0; + }); + + const uartBaud = ref(115200); + const uartBaudReal = ref(115200); + const uartConfig: Ref = ref({ + data_bits: 8, + parity: 0, + stop_bits: 1, + }); + + const filterValue = ref(''); + const computedFilterValue = computed(() => { + return unescapeString(filterValue.value); + }) + + const computedSuffixValue = computed(() => { + return unescapeString(textSuffixValue.value); + }) + + const computedPrefixValue = computed(() => { + return unescapeString(textPrefixValue.value); + }) + + const dataBuf: IDataBuf[] = []; + const dataBufLength = ref(0); + + /* actual data shown on screen */ + const dataFiltered: ShallowReactive = shallowReactive([]); + const dataFilterAutoUpdate = ref(true); + const acceptIncomingData = ref(false); + + // let frameBreakReady = false; + // let frameBreakTimeoutID = setTimeout(() => { + // }, 0); + + debouncedWatch(computedFilterValue, () => { + refreshFilteredBuff() + }, {debounce: 300}); + + let batchDataUpdateIntervalID: number = -1; + const batchUpdateTime = ref(80); /* ms */ + let batchStartIndex: number = 0; + + watch(batchUpdateTime, () => { + if (batchDataUpdateIntervalID >= 0) { + clearInterval(batchDataUpdateIntervalID); + batchDataUpdateIntervalID = -1; + } + batchUpdate(); + if (dataFilterAutoUpdate.value && batchDataUpdateIntervalID < 0) { + batchDataUpdateIntervalID = setInterval(batchUpdate, batchUpdateTime.value); + } + }, {immediate: true}); + + /* delayed value update, prevent quick unnecessary update */ + setInterval(() => { + dataBufLength.value = dataBuf.length; + TxByteCount.value = TxByteCountLocal; + TxTotalByteCount.value = TxTotalByteCountLocal; + RxByteCount.value = RxByteCountLocal; + RxTotalByteCount.value = RxTotalByteCountLocal; + if (RxSegment.length) { + RxRemainHexdump.value = u8toHexdump(RxSegment); + } else { + RxRemainHexdump.value = ""; + } + }, 200); + + function addString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0) { + console.log(item); + const unescaped = unescapeString(item); + return addItem(unescaped, isRX, doSend, type); + } + + function addHexString(item: string, isRX: boolean = false, doSend: boolean = false, type: number = 0) { + if (item === "") { + return addItem(new Uint8Array(0), isRX); + } + const hexArray = item.split(' '); + // Map each hex value to a decimal (integer) and create a Uint8Array from these integers + const uint8Array = new Uint8Array(hexArray.map(hex => parseInt(hex, 16))); + return addItem(uint8Array, isRX, doSend, type); + } + + function batchUpdate() { + if (batchStartIndex >= dataBuf.length) { + return; + } + + /* handle data buf array */ + if (dataBuf.length >= 30000) { + /* make array size to 15000 */ + const deleteCount = dataBuf.length - 30000 + 5000; + batchStartIndex -= deleteCount; + dataBuf.splice(0, deleteCount); + } + + if (!dataFilterAutoUpdate.value) { + return; + } + softRefreshFilterBuf(); + } + + function softRefreshFilterBuf() { + /* handle filtered buf array */ + const totalBufLength = dataBuf.length - batchStartIndex + dataFiltered.length; + + if (batchStartIndex < 0) { + dataFiltered.length = 1; + dataFiltered.pop(); + } else if (totalBufLength >= 30000) { + dataFiltered.splice(0, totalBufLength - 30000 + 5000); + } + + if (!enableFilter.value || computedFilterValue.value.length === 0) { + + /* no filter, do normal push */ + if (batchStartIndex < 0) { + dataFiltered.push(...dataBuf); + } else { + dataFiltered.push(...dataBuf.slice(batchStartIndex)); + } + batchStartIndex = dataBuf.length; + } else if (enableFilter.value && computedFilterValue.value.length !== 0) { + for (let i = batchStartIndex; i < dataBuf.length; i++) { + if (isArrayContained(computedFilterValue.value, dataBuf[i].data) >= 0) { + dataFiltered.push(dataBuf[i]); + } + } + batchStartIndex = dataBuf.length; + } + } + + function addItem(item: Uint8Array, isRX: boolean, doSend: boolean = false, type: number = 0) { + if (!acceptIncomingData.value && isRX) { + return; + } + const t = new Date(); + + // dataArchive.push({ + // time: t.getMilliseconds(), + // isRX: isRX, + // data: u8arr, + // }); + + if (isRX) { + RxTotalByteCountLocal += item.length; + RxByteCountLocal = item.length; + } else { + /* append prefix and suffix */ + if (computedPrefixValue.value.length || computedSuffixValue.value.length) { + const newArr = new Uint8Array(computedPrefixValue.value.length + + computedSuffixValue.value.length + item.length); + newArr.set(computedPrefixValue.value); + newArr.set(item, computedPrefixValue.value.length); + newArr.set(computedSuffixValue.value, computedPrefixValue.value.length + item.length); + item = newArr; + } + if (acceptIncomingData.value) { + if (doSend) { + /* INFO: hard coded for the moment */ + uart_send_msg(item, uartStore.uartNum); + } + } else { + type = 1; + } + TxTotalByteCountLocal += item.length; + TxByteCountLocal = item.length; + } + + let str: string; + str = decodeUtf8(item); + str = escapeHTML(str); + str = strToHTML(str); + + /* unescape data \n */ + if (enableAnsiDecode.value) { + if (isArrayContained(ANSI_CLEAR_WINDOW, item) >= 0) { + clearFilteredBuff(); + } + if (isArrayContained(ANSI_REFRESH_WINDOW, item) >= 0) { + batchUpdate() + softRefreshFilterBuf(); + } + /* ansi_to_html will escape HTML sequence */ + str = ansi_up.ansi_to_html("\x1b[0m" + str); + } + + dataBuf.push({ + time: + "[" + + zeroPad(t.getHours(), 2) + ":" + + zeroPad(t.getMinutes(), 2) + ":" + + zeroPad(t.getSeconds(), 2) + ":" + + zeroPad(t.getMilliseconds(), 3) + + "]", + type: type, + data: item, + isRX: isRX, + str: str, + hex: u8toHexString(item), + hexdump: u8toHexdump(item), + }); + } + + function clearByteCount(isRX: boolean) { + if (isRX) { + RxTotalByteCountLocal = 0; + } else { + TxTotalByteCountLocal = 0; + } + } + + function clearDataBuff() { + clearFilteredBuff(); + dataBuf.length = 0; + dataBufLength.value = 0; + batchStartIndex = 0; + + RxByteCountLocal = 0; + RxTotalByteCountLocal = 0; + TxByteCountLocal = 0; + TxTotalByteCountLocal = 0; + + RxSegment = new Uint8Array(); + RxRemainHexdump.value = ""; + } + + function clearFilteredBuff() { + /* prevent virtual scroll not displaying new data */ + showVirtualScroll.value = !showVirtualScroll.value; + /* the actual clear buff clear */ + dataFiltered.length = 0; + } + + function refreshFilteredBuff() { + clearFilteredBuff() + for (const item of dataBuf) { + const index = isArrayContained(computedFilterValue.value, item.data); + if (index >= 0 || filterValue.value.length === 0) { + dataFiltered.push(item); + } + } + filterChanged.value = true; + } + + function setUartBaud(baud: number) { + uartBaudReal.value = baud; + for (let i = 0; i < uartBaudList.length; i++) { + const difference = Math.abs(uartBaudList[i].baud - baud); + const percentageDifference = (difference / baud); + if (percentageDifference !== 0 && percentageDifference < 0.001) { + uartBaud.value = uartBaudList[i].baud; + return; + } + } + uartBaud.value = baud; + } + + + const enableLoopSend = ref(false); + const loopSendFreq = ref(1000); + let loopSendIntervalID: number = -1; + const sendBox = ref(""); + const sendBoxIsText = ref(true); + const isHexStringValid = ref(false); + + function loopSend() { + if (!acceptIncomingData.value) { + enableLoopSend.value = false; + } + + if (sendBoxIsText.value) { + addString(sendBox.value, false, true); + } else { + if (!isHexStringValid.value) { + addString("HEX格式错误", false, false, 1); + addHexString(sendBox.value, false, false, 1); + } else { + addHexString(sendBox.value, false, true); + } + } + } + + watch(enableLoopSend, (newValue) => { + if (newValue) { + if (loopSendIntervalID !== -1) { + clearInterval(loopSendIntervalID); + } + loopSendIntervalID = setInterval(loopSend, loopSendFreq.value); + } else { + clearInterval(loopSendIntervalID); + loopSendIntervalID = -1; + } + }); + + watch(loopSendFreq, (value) => { + if (enableLoopSend.value && value) { + /* update interval with new value */ + if (loopSendIntervalID !== -1) { + clearInterval(loopSendIntervalID); + } + loopSendIntervalID = setInterval(loopSend, loopSendFreq.value); + } + }) + + const loopSendRet = { + enableLoopSend, + loopSendFreq, + loopSendIntervalID, + isSendTextFormat: sendBoxIsText, + uartInputTextBox: sendBox, + isHexStringValid, + } + + interface WinProperty { + show: boolean; + width: string; + height: string; + borderSize: number; + } + + /* window layout */ + const winLeft = ref({ + show: true, + width: "100px", + height: "100px", + borderSize: 3, + }); + + /* window layout */ + const winRight = ref({ + show: true, + width: "100px", + height: "100px", + borderSize: 3, + }); + + const winAutoLayout = ref(true); + const winLayoutMode = ref('row'); + + + const winLayoutRet = { + winLeft, + winRight, + winAutoLayout, + winLayoutMode, + } + + /* macro buttons */ + + const macroData: Ref = ref([ + { + value: 'AT', + label: '测试AT', + id: 1, + },{ + value: 'AT+CSQ', + label: '询信号强度', + id: 2, + },{ + value: 'AT+CGSN', + label: '询序列号', + id: 3, + }, { + value: 'AT+CGMR', + label: '询固件版本', + id: 4, + }, { + value: 'AT+CMEE', + label: '询终端报错', + id: 5, + }, { + value: 'AT+NRB', + label: '重启', + id: 6, + }, { + value: 'AT+CGATT', + label: '询网络激活状态', + id: 7, + }, { + value: 'AT+CEREG', + label: '询网络注册状态', + id: 8, + }, { + value: 'AT+CSCON', + label: '询网络连接状态', + id: 9, + } + ]); + const macroId = ref(10); + + const macroDataRet = { + macroData, + macroId, + } + + + const autoSaveSettings = ref(true); + /* save to localStorage */ + const testNumber = ref(0); + const ipChangeAlert = ref(true); + + const settings: Ref = ref({ + testNumber, + macroButtons: macroData, + + configTab: configPanelTab, + showHex, + showText, + showHexdump, + showTimestamp, + lineWrap: enableLineWrap, + RxHexdumpColor, + TxHexdumpColor, + + frameBreak: frameBreakRules, + frameBreakSequence, + frameBreakSize, + frameBreakDelay, + frameBreakAfterSequence, + frameFilterValue: filterValue, + decodeANSI: enableAnsiDecode, + autoUpdate: dataFilterAutoUpdate, + updateInterval: batchUpdateTime, + sendFramePrefix: textPrefixValue, + sendFrameSuffix: textSuffixValue, + loopSendInterval: loopSendFreq, + sendBoxIsText, + sendBox, + winRight, + winLeft, + winAutoLayout, + winLayoutMode, + ipChangeAlert, + }); + + const settingsName = "uart-settings"; + + watch(() => settings, () => { + if (autoSaveSettings.value) { + localStorage.setItem(settingsName, JSON.stringify(settings.value)); + } + }, {deep: true}); + + function loadSettings(settingsData: string = "") { + let data: string | null = settingsData; + if (settingsData === "") { + data = localStorage.getItem(settingsName); + } + if (data) { + // Assuming the stored data is a JSON string + const parsedData = JSON.parse(data); + if (parsedData.macroButtons !== undefined) { + macroData.value = parsedData.macroButtons; + } + if (parsedData.configTab !== undefined) { + configPanelTab.value = parsedData.configTab; + } + if (parsedData.showText !== undefined) { + showText.value = parsedData.showText; + } + if (parsedData.showHex !== undefined) { + showHex.value = parsedData.showHex; + } + if (parsedData.showHexdump !== undefined) { + showHexdump.value = parsedData.showHexdump; + } + if (parsedData.showTimestamp !== undefined) { + showTimestamp.value = parsedData.showTimestamp; + } + if (parsedData.lineWrap !== undefined) { + enableLineWrap.value = parsedData.lineWrap; + } + if (parsedData.RxHexdumpColor !== undefined) { + RxHexdumpColor.value = parsedData.RxHexdumpColor; + } + if (parsedData.TxHexdumpColor !== undefined) { + TxHexdumpColor.value = parsedData.TxHexdumpColor; + } + if (parsedData.frameBreakSequence !== undefined) { + frameBreakSequence.value = parsedData.frameBreakSequence; + } + if (parsedData.frameBreakAfterSequence !== undefined) { + frameBreakAfterSequence.value = parsedData.frameBreakAfterSequence; + } + if (parsedData.frameBreakDelay !== undefined) { + frameBreakDelay.value = parsedData.frameBreakDelay; + } + if (parsedData.frameBreakSize !== undefined) { + frameBreakSize.value = parsedData.frameBreakSize; + } + + if (parsedData.frameBreak !== undefined) { + frameBreakRules.value = parsedData.frameBreak; + } + if (parsedData.decodeANSI !== undefined) { + enableAnsiDecode.value = parsedData.decodeANSI; + } + if (parsedData.frameFilterValue !== undefined) { + filterValue.value = parsedData.frameFilterValue; + } + if (parsedData.autoUpdate !== undefined) { + dataFilterAutoUpdate.value = parsedData.autoUpdate; + } + if (parsedData.updateInterval !== undefined) { + batchUpdateTime.value = parsedData.updateInterval; + } + + if (parsedData.sendFramePrefix !== undefined) { + textPrefixValue.value = parsedData.sendFramePrefix; + } + if (parsedData.sendFrameSuffix !== undefined) { + textSuffixValue.value = parsedData.sendFrameSuffix; + } + + if (parsedData.loopSendInterval !== undefined) { + loopSendFreq.value = parsedData.loopSendInterval; + } + if (parsedData.sendBoxIsText !== undefined) { + sendBoxIsText.value = parsedData.sendBoxIsText; + } + if (parsedData.sendBox !== undefined) { + sendBox.value = parsedData.sendBox; + } + + if (parsedData.winLeft !== undefined) { + winLeft.value = parsedData.winLeft; + } + if (parsedData.winRight !== undefined) { + winRight.value = parsedData.winRight; + } + if (parsedData.winAutoLayout !== undefined) { + winAutoLayout.value = parsedData.winAutoLayout; + } + if (parsedData.winLayoutMode !== undefined) { + winLayoutMode.value = parsedData.winLayoutMode; + } + if (parsedData.ipChangeAlert !== undefined) { + ipChangeAlert.value = parsedData.ipChangeAlert; + } + + reloadFrameBreak(); + macroId.value = macroData.value.reduce((max, item) => Math.max(max, item.id), 0) + 1; + } + } + + return { + addItem, + addString, + addHexString, + clearFilteredBuff, + clearDataBuff, + refreshFilteredBuff, + softRefreshFilterBuf, + textSuffixValue, + textPrefixValue, + hasAddedText, + clearByteCount, + dataBufLength, + configPanelTab, + dataFiltered, + dataFilterAutoUpdate, + filterValue, + batchUpdateTime, + acceptIncomingData, + + showVirtualScroll, + + enableAnsiDecode, + showHex, + showHexdump, + showText, + showTimestamp, + enableLineWrap, + RxHexdumpColor, + TxHexdumpColor, + predefineColors, + RxByteCount, + RxTotalByteCount, + TxByteCount, + TxTotalByteCount, + clearRxCounter, + clearTxCounter, + forceToBottom, + filterChanged, + + ...frameBreakRet, + + /* UART */ + predefinedUartBaudFrequent, + uartBaudList, + uartBaud, + uartConfig, + uartBaudReal, + setUartBaud, + ...loopSendRet, + ...winLayoutRet, + ...macroDataRet, + + autoSaveSettings, + settings, + testNumber, + loadSettings, + ipChangeAlert, + } +}); diff --git a/src/stores/useDataFlowStore.ts b/src/stores/useDataFlowStore.ts new file mode 100644 index 0000000..70b648a --- /dev/null +++ b/src/stores/useDataFlowStore.ts @@ -0,0 +1,11 @@ +import {defineStore} from "pinia"; +import {type Ref, ref} from "vue"; +import type {InstanceInfo} from "@/api/apiDataFlow"; + +export const useDataFlowStore = defineStore('data_flow', () => { + const instanceList: Ref = ref([]); + + return { + instanceList, + } +}); diff --git a/src/stores/useSystemStore.ts b/src/stores/useSystemStore.ts new file mode 100644 index 0000000..44974d3 --- /dev/null +++ b/src/stores/useSystemStore.ts @@ -0,0 +1,28 @@ +import {defineStore} from "pinia"; +import {ref} from "vue"; + +export const useSystemStore = defineStore('system', () => { + + const curFmInfo = ref({ + ver: "-", + date: "-", + }); + + const hwInfo = ref({ + ver: "-", + date: "-", + }) + + const sys_info = ref({ + sn: "-", + }); + + const rebootInProgress = ref(false); + + return { + curFmInfo, + hwInfo, + sysInfo: sys_info, + rebootInProgress, + } +}); diff --git a/src/stores/useUartStore.ts b/src/stores/useUartStore.ts new file mode 100644 index 0000000..c8faff2 --- /dev/null +++ b/src/stores/useUartStore.ts @@ -0,0 +1,8 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' + +export const useUartStore = defineStore('uart', () => { + const uartNum = ref(1); + + return { uartNum } +}) diff --git a/src/stores/useUpdateStore.ts b/src/stores/useUpdateStore.ts new file mode 100644 index 0000000..fed62ca --- /dev/null +++ b/src/stores/useUpdateStore.ts @@ -0,0 +1,45 @@ +import { ref } from 'vue' +import { defineStore } from 'pinia' +import {wt_ota_get_progress} from "@/api/apiOTA"; + +export const useUpdateStore = defineStore('update', () => { + const canUpdate = ref(false); + const updateProgress = ref(0); + const updateStatus = ref(''); + + const progressBarStatus = ref(''); + + let progressIntervalID = -1; + + const newFmInfo = ref({ + fm_size: 0, + fm_ver: "-", + upd_date: "-", + upd_note: "-", + }) + + function setProgressInterval() { + if (progressIntervalID < 0) { + progressIntervalID = setInterval(() => { + wt_ota_get_progress(); + }, 1000); + } + } + + function clearProgressInterval() { + if (progressIntervalID >= 0) { + clearInterval(progressIntervalID); + progressIntervalID = -1; + } + } + + return { + canUpdate, + updateProgress, + updateStatus, + progressBarStatus, + newFmInfo, + setProgressInterval, + clearProgressInterval, + } +}) diff --git a/src/views/About.vue b/src/views/About.vue index 1b1d149..2f89f39 100644 --- a/src/views/About.vue +++ b/src/views/About.vue @@ -1,22 +1,25 @@ - diff --git a/src/views/Update.vue b/src/views/Update.vue new file mode 100644 index 0000000..fc900d6 --- /dev/null +++ b/src/views/Update.vue @@ -0,0 +1,108 @@ + + + + + diff --git a/src/views/Wifi.vue b/src/views/Wifi.vue index b5ef150..5e93681 100644 --- a/src/views/Wifi.vue +++ b/src/views/Wifi.vue @@ -1,17 +1,17 @@ - \ No newline at end of file diff --git a/src/views/text-data-viewer/textDataConfig.vue b/src/views/text-data-viewer/textDataConfig.vue new file mode 100644 index 0000000..11716ba --- /dev/null +++ b/src/views/text-data-viewer/textDataConfig.vue @@ -0,0 +1,457 @@ + + + + + \ No newline at end of file diff --git a/src/views/text-data-viewer/textDataMacro.vue b/src/views/text-data-viewer/textDataMacro.vue new file mode 100644 index 0000000..4035d0e --- /dev/null +++ b/src/views/text-data-viewer/textDataMacro.vue @@ -0,0 +1,182 @@ + + + + + + \ No newline at end of file diff --git a/src/views/text-data-viewer/textDataViewer.vue b/src/views/text-data-viewer/textDataViewer.vue new file mode 100644 index 0000000..0cd4aa9 --- /dev/null +++ b/src/views/text-data-viewer/textDataViewer.vue @@ -0,0 +1,440 @@ + + + diff --git a/vite.config.ts b/vite.config.ts index 26066a8..73291a7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -7,7 +7,7 @@ import vue from '@vitejs/plugin-vue' import svgLoader from "vite-svg-loader"; import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"; import { viteSingleFile } from 'vite-plugin-singlefile' - +import vuetify from 'vite-plugin-vuetify' // https://vitejs.dev/config/ export default ({mode}: ConfigEnv) => { @@ -25,6 +25,7 @@ export default ({mode}: ConfigEnv) => { svgLoader(), cssInjectedByJsPlugin(), viteSingleFile(), + vuetify(), ], define: {}, resolve: {