Merge branch 'wireless-proxy'

# Conflicts:
#	package-lock.json
#	package.json
#	set_env.sh
#	src/App.vue
#	src/api/index.ts
#	src/views/About.vue
#	src/views/Wifi.vue
#	src/views/navigation/NavBar.vue
This commit is contained in:
kerms 2025-05-30 14:57:28 +02:00
commit dccf5feaa8
45 changed files with 4520 additions and 241 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
dist

137
package-lock.json generated
View File

@ -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": {

View File

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

View File

@ -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(() => {
</script>
<template>
<div class="flex flex-col h-screen">
<div class="flex flex-col wt-h-100">
<header>
<nav-bar/>
</header>
<RouterView/>
</div>
</template>
<style>
.wt-h-100 {
height: 100vh;
}
</style>

104
src/api/apiDataFlow.ts Normal file
View File

@ -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);
}

61
src/api/apiOTA.ts Normal file
View File

@ -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);
}

45
src/api/apiSystem.ts Normal file
View File

@ -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);
}

110
src/api/apiUart.ts Normal file
View File

@ -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);
}

View File

@ -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) {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M480-360 280-560h400L480-360Z"/></svg>

After

(image error) Size: 127 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="m280-400 200-200 200 200H280Z"/></svg>

After

(image error) Size: 127 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>

After

(image error) Size: 411 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>

After

(image error) Size: 292 B

View File

@ -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");

View File

@ -1,3 +1,19 @@
export function isDevMode() {
return import.meta.env.VITE_APP_MODE === 'dev';
}
}
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站或者加入允斯群,新项目和更新都会在第一时间在这里发布. 使用愉快^_^";
}

View File

@ -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,

View File

@ -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: () => {},
});
}

View File

@ -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: () => {},
});
}

View File

@ -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: () => {
},
});
}

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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 = {

View File

@ -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;

View File

@ -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:
"<p>Timeout=-1: Disable timeout frame break</p>" +
"<p>Timeout=0: Immediate break, any received data is considered complete</p>" +
"<p>Match after break: Typical \\n scenario</p>" +
"<p>Match before break: For scenarios with special frame headers</p>" +
"<p>Fixed byte frame break: Useful for large data transfer, e.g., break frame every 1024 bytes for easy data viewing</p>",
value: "Value",
timeout: "Timeout",
match: "Match",
byte: "Byte",
begin: "b",
end: "b",
other: "Other",
decodeAnsiEscapeCodes: "Decode ANSI Escape Codes",
ansiTooltips:
"<p>ANSI escape codes have many uses for terminals and text, such as changing text colors, among other effects.</p>\n" +
"<p>\n Learn more ->\n <a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/ANSI_escape_code\">" +
"https://en.wikipedia.org/wiki/ANSI_escape_code\n </a></p>",
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: "<p>\n" +
"<el-textsize=\"small\">Smart Mode:</el-text>\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" +
"</p>\n" +
"<p>\n" +
"<el-textsize=\"small\">Coexistence Mode:</el-text>\n" +
"Convenient but impacts stability and increases power consumption.\n" +
"</p>\n" +
"<p>\n" +
"<el-textsize=\"small\">Hotspot-Only Mode Drawback:</el-text>\n" +
"No network connection.\n" +
"</p>",
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",
}
};

204
src/locales/fr.ts Normal file
View File

@ -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:
"<p>Délai d'expiration=-1 : Désactiver la coupure de trame par délai d'expiration</p>" +
"<p>Délai d'expiration=0 : Coupure immédiate, toutes données reçues sont considérées complètes</p>" +
"<p>Match après coupure : Scénario typique \\n</p>" +
"<p>Match avant coupure : Pour des scénarios avec en-têtes de trame spécifiques</p>" +
"<p>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</p>",
value: "Valeur",
timeout: "Timeout",
match: "Match",
byte: "Byte",
begin: "b",
end: "b",
other: "Autres",
decodeAnsiEscapeCodes: "Décode Échappement ANSI",
ansiTooltips:
"<p>Les codes d'échappement ANSI ont de nombreuses utilisations pour les terminaux et le texte, comme changer les couleurs du texte, entre autres effets.</p>" +
"<p>\n En savoir plus ->\n " +
"<a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/ANSI_escape_code\">\n" +
"https://en.wikipedia.org/wiki/ANSI_escape_code\n </a>\n</p>",
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: "<p>\n" +
"<el-textsize=\"small\">Mode intelligent :</el-text>\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" +
"</p>\n" +
"<p>\n" +
"<el-textsize=\"small\">Mode coexistence :</el-text>\n" +
"Pratique mais réduit la stabilité et augmente la consommation d'énergie.\n" +
"</p>\n" +
"<p>\n" +
"<el-textsize=\"small\">Inconvénient du mode point d'accès seul :</el-text>\n" +
"Pas de connexion réseau.\n" +
"</p>",
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é",
}
};

View File

@ -7,8 +7,8 @@ type NestedKeyOf<ObjectType extends object> = {
: `${Key}`
}[keyof ObjectType & (string | number)];
type TranslationKeys = NestedKeyOf<typeof zh>;
export type TranslationKeys = NestedKeyOf<typeof zh>;
export function translate<K extends TranslationKeys>(key: K | string): string {
return i18n.global.t(key.toLowerCase());
export function translate(key: TranslationKeys | string): string {
return i18n.global.t(key);
}

View File

@ -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:
"<p>超时=-1 禁用超时断帧</p>" +
"<p>超时=0 当机立断,收到任何数据都视为完整数据</p>" +
"<p>匹配断后:典型\\n的场景</p>" +
"<p>匹配断前:用于有特殊帧头的场景</p>" +
"<p>固定字节断帧传输大量数据比如可以每隔1024字节断帧方便查看数据</p>",
value: "值",
timeout: "超时",
match: "匹配",
byte: "字节",
begin: "断",
end: "断",
other: "其他",
decodeAnsiEscapeCodes: "解码ANSI转义码",
ansiTooltips:
"<p>ANSI转义码对终端和文本有很多作用比如改变文本颜色等。</p>\n" +
"<p>\n" +
" 简单了解->\n" +
" <a target=\"_blank\" href=\"https://yunsi.studio/wireless-debugger/docs/uart-webhost/ansi-escape-code\">\n" +
" https://yunsi.studio/wireless-debugger/docs/uart-webhost/ansi-escape-code\n" +
" </a>\n" +
"</p>",
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: "<p>\n" +
"<el-textsize=\"small\">智能模式:</el-text>\n" +
"成功连接至Wi-Fi后,如果此设备的热点未被其他设备连接,将在30秒后自动关闭热点;如果此设备与AP断开连接,将在5秒后自动开启热点\n" +
"</p>\n" +
"<p>\n" +
"<el-textsize=\"small\">热点+终端共存模式:</el-text>\n" +
"方便使用,但是影响稳定性,增加功耗\n" +
"</p>\n" +
"<p>\n" +
"<el-textsize=\"small\">单热点模式缺点:</el-text>\n" +
"无网络\n" +
"</p>",
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: "调试器未连接",
}
}

View File

@ -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')

View File

@ -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;

View File

@ -12,6 +12,9 @@ const moduleMap = new Map<number, IModuleCallback>();
export function registerModule(moduleId: number, moduleCallback: IModuleCallback): boolean {
if (moduleMap.has(moduleId)) {
if (isDevMode()) {
console.log("module ", moduleId, "already registered");
}
return false;
}

File diff suppressed because it is too large Load Diff

View File

@ -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<InstanceInfo[]> = ref([]);
return {
instanceList,
}
});

View File

@ -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,
}
});

View File

@ -0,0 +1,8 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
export const useUartStore = defineStore('uart', () => {
const uartNum = ref(1);
return { uartNum }
})

View File

@ -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,
}
})

View File

@ -1,22 +1,25 @@
<script setup lang="ts">
import {useSystemStore} from "@/stores/useSystemStore";
import {translate} from "@/locales";
const version = import.meta.env.VITE_APP_GIT_TAG || "v0.0.0";
const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
const sysStore = useSystemStore();
</script>
<template>
<div class="text-layout">
<el-divider></el-divider>
<el-divider>关于</el-divider>
<el-divider>{{ translate('page.about') }}</el-divider>
<el-divider></el-divider>
<el-collapse>
<el-collapse-item title="关于网页版上位机">
<el-collapse-item :title="translate('aboutWebHost')">
<el-descriptions border :column="1" class="mt-5 description-style">
<el-descriptions-item label="版本">{{ version }}</el-descriptions-item>
<el-descriptions-item label="发布时间">{{ compileTime }}</el-descriptions-item>
<el-descriptions-item label="许可证">MIT</el-descriptions-item>
<el-descriptions-item :label="translate('version')">{{ version }}</el-descriptions-item>
<el-descriptions-item :label="translate('releaseTime')">{{ compileTime }}</el-descriptions-item>
</el-descriptions>
<el-descriptions title="鸣谢" border :column="1" class="mt-5 description-style">
<el-descriptions :title="translate('credit')" border :column="1" class="mt-5 description-style">
<el-descriptions-item label="vuejs"><a target="_blank" href="https://github.com/vuejs/vue/blob/main/LICENSE">MIT</a>
</el-descriptions-item>
<el-descriptions-item label="typescript"><a
@ -40,13 +43,15 @@ const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
</el-descriptions-item>
</el-descriptions>
</el-collapse-item>
<el-collapse-item title="关于下位机">
<el-collapse-item :title="translate('aboutDebugger')">
<el-descriptions border :column="1" class="mt-5 description-style">
<el-descriptions-item label="官网"><a target="_blank" href="https://yunsi.studio/wireless-proxy">允斯工作室</a></el-descriptions-item>
<el-descriptions-item label="版本">-</el-descriptions-item>
<el-descriptions-item :label="translate('officialWebsite')"><a target="_blank" href="https://yunsi.studio/wireless-debugger">https://yunsi.studio/wireless-debugger</a></el-descriptions-item>
<el-descriptions-item :label="translate('version')">{{ sysStore.curFmInfo.ver }}</el-descriptions-item>
<el-descriptions-item :label="translate('releaseTime')">{{ sysStore.curFmInfo.date }}</el-descriptions-item>
<el-descriptions-item :label="translate('serialNumber')">{{ sysStore.sysInfo.sn }}</el-descriptions-item>
</el-descriptions>
<el-descriptions title="鸣谢" border :column="1" class="mt-5 description-style">
<el-descriptions :title="translate('credit')" border :column="1" class="mt-5 description-style">
<el-descriptions-item label="windowsair"><a target="_blank" href="https://github.com/windowsair/wireless-esp8266-dap">wireless-esp8266-dap</a>
</el-descriptions-item>
</el-descriptions>
@ -54,22 +59,22 @@ const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
</el-collapse>
<el-descriptions title="作者:空空(kerms)" border :column="1" class="mt-5 description-style">
<el-descriptions-item label="官网"><a target="_blank" href="https://yunsi.studio/">允斯工作室https://yunsi.studio/</a></el-descriptions-item>
<el-descriptions :title="translate('author') + ' :空空(kerms)'" border :column="1" class="mt-5 description-style">
<el-descriptions-item :label="translate('officialWebsite')"><a target="_blank" href="https://yunsi.studio/">https://yunsi.studio/</a></el-descriptions-item>
<el-descriptions-item label="github"><a target="_blank" href="https://github.com/kerms">https://github.com/kerms</a>
</el-descriptions-item>
<el-descriptions-item label="邮箱">kerms@niazo.org</el-descriptions-item>
<el-descriptions-item :label="translate('email')">kerms@niazo.org</el-descriptions-item>
<el-descriptions-item label="BiliBili"><a target="_blank" href="https://space.bilibili.com/3461571571353885">3461571571353885</a>
</el-descriptions-item>
<el-descriptions-item label="QQ群">642246000</el-descriptions-item>
<el-descriptions-item label="备注">欢迎大家来打扰啊</el-descriptions-item>
<el-descriptions-item :label="translate('TencentQQGroup')">642246000</el-descriptions-item>
<el-descriptions-item :label="translate('note')">{{ translate('welcomeMessage') }}</el-descriptions-item>
</el-descriptions>
</div>
<el-divider></el-divider>
</template>
<style scoped>
<style scoped lang="postcss">
.description-style :deep(.el-descriptions__label) {
@apply w-32
}

View File

@ -2,12 +2,13 @@
<div class="text-layout">
<el-divider></el-divider>
<el-divider>反馈</el-divider>
<el-divider>{{ translate('page.feedback') }}</el-divider>
<el-divider></el-divider>
<el-descriptions title="反馈/建议/需要新功能" border :column="1">
<el-descriptions-item label="QQ群">642246000</el-descriptions-item>
<el-descriptions-item label="作者邮箱">kerms@niazo.org</el-descriptions-item>
<el-descriptions :title="translate('page.feedback') + '/' + translate('suggestion') + '/' + translate('feature')" border :column="1">
<el-descriptions-item :label="translate('TencentQQGroup')">642246000</el-descriptions-item>
<el-descriptions-item :label="translate('authorEmail')">kerms@niazo.org</el-descriptions-item>
<!-- TODO: add discord + BiliBili / instagram ? -->
</el-descriptions>
</div>
@ -16,5 +17,5 @@
<script setup lang="ts">
import {translate} from "@/locales";
</script>

View File

@ -1,9 +1,483 @@
<template>
<div class="button-m-0 messages-container flex flex-grow overflow-hidden" :class="{'flex-col': store.winLayoutMode ==='col'}">
<div v-show="store.winLeft.show" ref="win1Ref" class="bg-gray-50 flex-shrink-0 overflow-auto"
:class="{
'max-w-60': store.winLayoutMode==='row', 'xl:max-w-80': store.winLayoutMode==='row',
'min-w-60': store.winLayoutMode==='row', 'xl:min-w-80': store.winLayoutMode==='row'
}"
>
<text-data-config></text-data-config>
</div>
<div v-show="store.winLeft.show && (winDataView.show || store.winRight.show)" ref="firstWinResizeRef"></div>
<div v-show="winDataView.show" class="flex flex-col flex-grow overflow-hidden p-2">
<textDataViewer></textDataViewer>
</div>
<div v-show="winDataView.show && store.winRight.show" ref="thirdWinResizeRef"></div>
<div v-show="store.winRight.show" ref="win2Ref" :class="{
'max-w-80': store.winLayoutMode==='row', 'xl:max-w-96': store.winLayoutMode==='row',
'min-w-80': store.winLayoutMode==='row', 'xl:min-w-96': store.winLayoutMode==='row'
}"
class="bg-gray-50 flex flex-col flex-shrink-0 min-h-32 overflow-auto p-2">
<TextDataMacro @winSizeRefresh="handleWinSizeRefresh"></TextDataMacro>
</div>
</div>
<teleport to="#page-spec-slot">
<div>
<el-popover
placement="bottom"
trigger="click"
:hide-after="0"
transition="none"
>
<div class="button-m-0 flex flex-col space-y-2">
<div class="custom-style flex justify-center">
<el-segmented v-model="store.winLayoutMode" :options="layoutOptions" size="small"/>
</div>
<el-checkbox v-model="store.winAutoLayout" border size="small"
:disabled="store.winLayoutMode==='col'">
{{ $t('uart.responsive') }}
</el-checkbox>
<el-checkbox v-model="store.winLeft.show" border size="small" :disabled="store.winAutoLayout">
{{ $t("uart.configPannel") }}
</el-checkbox>
<el-checkbox v-model="winDataView.show" border size="small" :disabled="store.winAutoLayout">
{{ $t('uart.displayPannel') }}
</el-checkbox>
<el-checkbox v-model="store.winRight.show" border size="small" :disabled="store.winAutoLayout">
{{ $t('uart.macroPannel') }}
</el-checkbox>
</div>
<template #reference>
<el-button class="min-h-full" type="primary" :size="layoutConf.isMedium ? 'small' : 'default'">
{{ $t('uart.layout') }}
</el-button>
</template>
</el-popover>
</div>
<div class="mx-1"></div>
</teleport>
</template>
<script setup lang="ts">
import {computed, onMounted, onUnmounted, reactive, type Ref, ref, type UnwrapRef, watch} from "vue";
import {breakpointsTailwind, useBreakpoints} from '@vueuse/core'
import {useDataViewerStore} from '@/stores/dataViewerStore';
import * as api from '@/api';
import {ControlEvent} from '@/api';
import {
type IUartMsgBaud,
type IUartMsgConfig,
type IUartMsgNum,
uart_get_baud,
uart_get_config,
uart_get_default_num,
WtUartCmd
} from '@/api/apiUart';
/* TODO: use https://antoniandre.github.io/splitpanes/ */
import {type ApiBinaryMsg} from '@/api/binDataDef';
import * as df from '@/api/apiDataFlow';
import textDataViewer from "@/views/text-data-viewer/textDataViewer.vue";
import textDataConfig from "@/views/text-data-viewer/textDataConfig.vue"
import {registerModule} from "@/router/msgRouter";
import {isDevMode} from "@/composables/buildMode";
import {useWsStore} from "@/stores/websocket";
import {useUartStore} from "@/stores/useUartStore";
import TextDataMacro from "@/views/text-data-viewer/textDataMacro.vue";
import {translate} from "@/locales";
const store = useDataViewerStore()
const wsStore = useWsStore()
const uartStore = useUartStore()
const firstWinResizeRef = ref(document.body);
const thirdWinResizeRef = ref(document.body);
const win1Ref = ref(document.body);
const win2Ref = ref(document.body);
const breakpoints = useBreakpoints(breakpointsTailwind)
const layoutConf = reactive({
isSmall: breakpoints.smaller("sm"),
isMedium: breakpoints.smaller("lg"),
});
const layoutOptions = computed(() => [{
label: translate("uart.landscape"),
value: 'row'
}, {
label: translate("uart.portrait"),
value: 'col'
}]);
interface WinProperty {
show: boolean;
width: string;
height: string;
borderSize: number;
}
const winDataView = reactive({
show: true,
})
const ctx = reactive({
curResizeTarget: "none",
curHeightOffset: 0,
});
function updateCursor(i: HTMLElement) {
if (store.winLayoutMode === 'row') {
i.style.cursor = "col-resize";
} else {
i.style.cursor = "row-resize";
}
}
function updateWin(r: Ref<HTMLElement>, p: UnwrapRef<WinProperty>) {
if (store.winLayoutMode === 'row') {
r.value.style.minHeight = "";
r.value.style.maxHeight = ""
if (winDataView.show) {
r.value.style.minWidth = p.width;
r.value.style.maxWidth = p.width;
}
} else {
r.value.style.minWidth = ""
r.value.style.maxWidth = ""
if (winDataView.show) {
r.value.style.minHeight = p.height;
r.value.style.maxHeight = p.height;
}
}
}
function updateCursors() {
updateCursor(firstWinResizeRef.value);
updateCursor(thirdWinResizeRef.value);
}
function updateResizer() {
updateCursors();
updateWin(win1Ref, store.winLeft);
updateWin(win2Ref, store.winRight);
}
function mouseResize(e: MouseEvent) {
const curTarget = e.target as HTMLElement
if (store.winLayoutMode === 'row') {
let f = e.clientX;
if (ctx.curResizeTarget === "first") {
win1Ref.value.style.minWidth = f + "px";
win1Ref.value.style.maxWidth = f + "px";
} else {
if (isDevMode()) {
console.log("Row clientX", e.clientX, "clientY", e.clientY,
"layerX", e.layerX, "layerY", e.layerY, "offsetX", e.offsetX, "offsetY", e.offsetY,
"pageX", e.pageX, "pageY", e.pageY, win2Ref.value.clientHeight);
}
win2Ref.value.style.minWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
win2Ref.value.style.maxWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
}
} else {
/* col mode */
let f = e.clientY;
if (ctx.curResizeTarget === "first") {
win1Ref.value.style.minHeight = f - ctx.curHeightOffset + "px";
win1Ref.value.style.maxHeight = f - ctx.curHeightOffset + "px";
} else {
if (isDevMode()) {
console.log("Col clientX", e.clientX, "clientY", e.clientY,
"layerX", e.layerX, "layerY", e.layerY, "offsetX", e.offsetX, "offsetY", e.offsetY,
"pageX", e.pageX, "pageY", e.pageY, curTarget.offsetWidth, ctx.curHeightOffset);
}
win2Ref.value.style.minHeight = ctx.curHeightOffset - f + "px";
win2Ref.value.style.maxHeight = ctx.curHeightOffset - f + "px";
}
}
}
function touchResize(e: TouchEvent) {
let t = e.touches[0];
let f: number;
if (store.winLayoutMode === 'row') {
f = t.clientX;
if (ctx.curResizeTarget === "first") {
win1Ref.value.style.minWidth = f + "px";
win1Ref.value.style.maxWidth = f + "px";
} else {
win2Ref.value.style.minWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
win2Ref.value.style.maxWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
}
} else {
/* column layout mode */
f = t.clientY;
if (ctx.curResizeTarget === "first") {
/* setting window */
win1Ref.value.style.minHeight = f - ctx.curHeightOffset + "px";
win1Ref.value.style.maxHeight = f - ctx.curHeightOffset + "px";
} else {
/* quick access window */
win2Ref.value.style.minHeight = document.body.scrollHeight - f - store.winRight.borderSize + "px";
win2Ref.value.style.maxHeight = document.body.scrollHeight - f - store.winRight.borderSize + "px";
}
}
}
function startResize(event: Event) {
// Normalize touch and mouse events
if (event.type.includes('touch')) {
ctx.curHeightOffset = (event as TouchEvent).touches[0].clientY;
} else {
ctx.curHeightOffset = (event as MouseEvent).clientY;
}
const divRef = event.target;
if (divRef === firstWinResizeRef.value) {
ctx.curResizeTarget = "first";
ctx.curHeightOffset -= win1Ref.value.clientHeight;
// ctx.curOffset = win1Ref.value.clientHeight;
} else if (divRef === thirdWinResizeRef.value) {
ctx.curResizeTarget = "third";
ctx.curHeightOffset += win2Ref.value.clientHeight;
}
win1Ref.value.style.transition = 'initial';
win2Ref.value.style.transition = 'initial';
document.addEventListener("mousemove", mouseResize, false);
document.addEventListener("touchmove", touchResize, false);
store.winAutoLayout = false;
}
function stopResize() {
if (win1Ref.value) {
win1Ref.value.style.transition = '';
if (store.winLayoutMode === "row") {
store.winLeft.width = win1Ref.value.style.minWidth;
} else {
store.winLeft.height = win1Ref.value.style.minHeight;
}
}
if (win2Ref.value) {
win2Ref.value.style.transition = '';
if (store.winLayoutMode === "row") {
store.winRight.width = win2Ref.value.style.minWidth;
} else {
store.winRight.height = win2Ref.value.style.minHeight;
}
}
document.body.style.cursor = '';
document.removeEventListener("mousemove", mouseResize, false);
document.removeEventListener("touchmove", touchResize, false);
}
watch(() => store.winLayoutMode, (value) => {
updateResizer();
if (value === "col") {
store.winAutoLayout = false;
}
});
watch([
() => layoutConf.isSmall,
() => store.winAutoLayout
], (value) => {
if (store.winAutoLayout) {
store.winRight.show = !value[0];
win1Ref.value.style.minWidth = "";
win1Ref.value.style.maxWidth = "";
}
}, {
immediate: true,
});
watch([
() => layoutConf.isMedium,
() => store.winAutoLayout
], (value) => {
if (store.winAutoLayout) {
store.winLeft.show = !value[0];
win1Ref.value.style.minWidth = "";
win1Ref.value.style.maxWidth = "";
win2Ref.value.style.minWidth = "";
win2Ref.value.style.maxWidth = "";
winDataView.show = true;
}
}, {
immediate: true
});
watch(() => winDataView.show, value => {
if (!value) {
win1Ref.value.style.minWidth = "";
win1Ref.value.style.maxWidth = "";
win1Ref.value.style.maxHeight = "";
win1Ref.value.style.maxHeight = "";
win2Ref.value.style.minWidth = "";
win2Ref.value.style.maxWidth = "";
win2Ref.value.style.maxHeight = "";
win2Ref.value.style.maxHeight = "";
}
});
watch(() => store.winRight.show, value => {
if (!value && !winDataView.show) {
win1Ref.value.style.maxHeight = "";
win1Ref.value.style.maxHeight = "";
win1Ref.value.style.maxWidth = "";
win1Ref.value.style.maxWidth = "";
}
});
const onUartJsonMsg = (msg: api.ApiJsonMsg) => {
switch (msg.cmd as WtUartCmd) {
case WtUartCmd.GET_BAUD:
case WtUartCmd.SET_BAUD:{
const uartMsg = msg as IUartMsgBaud;
if (uartMsg.baud) {
store.setUartBaud(uartMsg.baud)
}
break;
}
case WtUartCmd.GET_CONFIG:
case WtUartCmd.SET_CONFIG:{
const uartMsg = msg as IUartMsgConfig;
store.uartConfig.data_bits = uartMsg.data_bits;
store.uartConfig.stop_bits = uartMsg.stop_bits;
store.uartConfig.parity = uartMsg.parity;
break;
}
case WtUartCmd.GET_DEFAULT_NUM:
uartStore.uartNum = (msg as IUartMsgNum).num;
uart_get_baud(uartStore.uartNum);
uart_get_config(uartStore.uartNum);
break;
default:
if (isDevMode()) {
console.log("uart not treated", msg);
}
break
}
};
const onUartBinaryMsg = (msg: ApiBinaryMsg) => {
if (isDevMode()) {
console.log("uart", msg);
}
store.addSegment(new Uint8Array(msg.payload), true);
};
const onClientCtrl = (msg: api.ControlMsg) => {
if (msg.type !== api.ControlMsgType.WS_EVENT) {
return
}
if (msg.data === ControlEvent.DISCONNECTED) {
store.acceptIncomingData = false;
} else if (msg.data === ControlEvent.CONNECTED) {
updateUartData();
store.acceptIncomingData = true;
}
};
function updateUartData() {
/* TODO: hard code for the moment, 0 is UART instance id (can be changed in the future) */
uart_get_default_num();
df.wt_data_flow_attach_cur_to_sender(0);
}
function handleWinSizeRefresh() {
if (!store.winAutoLayout) {
if (win1Ref.value) {
if (store.winLayoutMode === "row") {
win1Ref.value.style.minWidth = store.winLeft.width;
} else {
win1Ref.value.style.minHeight = store.winLeft.height;
win1Ref.value.style.maxHeight = store.winRight.height;
}
}
if (win2Ref.value) {
if (store.winLayoutMode === "row") {
win2Ref.value.style.minWidth = store.winRight.width;
} else {
win2Ref.value.style.minHeight = store.winRight.height;
win2Ref.value.style.maxHeight = store.winRight.height;
}
}
}
}
onMounted(() => {
registerModule(api.WtModuleID.UART, {
ctrlCallback: onClientCtrl,
serverJsonMsgCallback: onUartJsonMsg,
serverBinMsgCallback: onUartBinaryMsg,
});
firstWinResizeRef.value.style.borderWidth = store.winLeft.borderSize + "px";
thirdWinResizeRef.value.style.borderWidth = store.winRight.borderSize + "px";
updateCursors()
if (firstWinResizeRef.value) {
firstWinResizeRef.value.addEventListener("mousedown", startResize, false);
firstWinResizeRef.value.addEventListener("touchstart", startResize, false);
}
if (thirdWinResizeRef.value) {
thirdWinResizeRef.value.addEventListener("mousedown", startResize, false);
thirdWinResizeRef.value.addEventListener("touchstart", startResize, false);
}
document.addEventListener("mouseup", stopResize, false);
document.addEventListener("touchend", stopResize, false);
updateUartData();
store.acceptIncomingData = wsStore.state === ControlEvent.CONNECTED;
handleWinSizeRefresh()
});
onUnmounted(() => {
if (firstWinResizeRef.value) {
firstWinResizeRef.value.removeEventListener("mousedown", startResize, false);
firstWinResizeRef.value.removeEventListener("touchstart", startResize, false);
}
if (thirdWinResizeRef.value) {
thirdWinResizeRef.value.removeEventListener("mousedown", startResize, false);
thirdWinResizeRef.value.removeEventListener("touchstart", startResize, false);
}
document.removeEventListener("mouseup", stopResize, false);
document.removeEventListener("touchend", stopResize, false);
});
</script>
<template>
<div class="text-layout">
<h2 class="page-title opacity-10">尽请期待</h2>
</div>
</template>
<style scoped>
.button-m-0 :deep(.el-button + .el-button) {
margin-left: 0;
}
.custom-style .el-segmented {
--el-segmented-item-selected-color: var(--el-text-color-primary);
--el-segmented-item-selected-bg-color: var(--el-color-primary);
--el-border-radius-base: 16px;
}
.button-m-0 :deep(.el-checkbox) {
margin-right: 0;
}
</style>

108
src/views/Update.vue Normal file
View File

@ -0,0 +1,108 @@
<template>
<div class="text-layout description-style">
<h1 class="page-title">
固件更新
</h1>
<p class="text-center">(需联网)</p>
<el-divider></el-divider>
<el-descriptions title="当前版本" border :column="1">
<el-descriptions-item label="硬件版本">{{ sysStore.hwInfo.ver }}</el-descriptions-item>
<el-descriptions-item label="固件版本">{{ sysStore.curFmInfo.ver }}</el-descriptions-item>
<el-descriptions-item label="固件日期">{{ sysStore.curFmInfo.date }}</el-descriptions-item>
</el-descriptions>
<el-divider></el-divider>
<el-descriptions title="最新版本" border :column="1">
<template #extra>
<div class="flex">
<el-tooltip placement="top" effect="light">
<template #content>
<p>2秒延迟后重启</p>
</template>
<el-button @click="doReboot" type="warning" :disabled="sysStore.rebootInProgress">
重启{{ sysStore.rebootInProgress ? '中' : ''}}
</el-button>
</el-tooltip>
<el-button @click="doUpdate" type="primary" :disabled="!updateStore.canUpdate">
更新
</el-button>
</div>
</template>
<el-descriptions-item label="固件版本">{{ updateStore.newFmInfo.fm_ver }}</el-descriptions-item>
<el-descriptions-item label="更新日期">{{ updateStore.newFmInfo.upd_date }}</el-descriptions-item>
<el-descriptions-item label="固件大小">{{ updateStore.newFmInfo.fm_size }}</el-descriptions-item>
<el-descriptions-item label="更新进度">
<el-alert v-if="updateStore.updateStatus === 'OK'" title="更新已完成,重启后,刷新网页生效" type="success" show-icon :closable="false" />
<el-progress v-else :percentage="updateStore.updateProgress" :format="format" :status="updateStore.progressBarStatus"/>
</el-descriptions-item>
<el-descriptions-item label="更新内容">
<pre>{{ updateStore.newFmInfo.upd_note }}</pre>
</el-descriptions-item>
</el-descriptions>
<el-divider @click="showHidden = !showHidden">底部</el-divider>
<div v-if="showHidden">
<p>直链更新仅用于测试请勿使用</p>
<el-input placeholder="https://..." v-model="directLinkUpdate"></el-input>
<el-button type="primary" @click="doDirectLinkUpdate">更新</el-button>
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted, onUnmounted, ref} from "vue";
import {
wt_ota_do_update, wt_ota_do_url_update,
wt_ota_get_progress, wt_ota_get_update_info,
} from "@/api/apiOTA";
import {useSystemStore} from "@/stores/useSystemStore";
import {wt_sys_reboot} from "@/api/apiSystem";
import {useUpdateStore} from "@/stores/useUpdateStore";
const sysStore = useSystemStore();
const updateStore = useUpdateStore();
const showHidden = ref(false)
const directLinkUpdate = ref("");
const format = (percentage: number) => (percentage.toFixed(2) + '%')
function doUpdate() {
wt_ota_do_update();
updateStore.setProgressInterval();
}
function doReboot() {
wt_sys_reboot();
}
function doDirectLinkUpdate() {
if (directLinkUpdate.value.length === 0) {
return;
}
updateStore.setProgressInterval();
wt_ota_do_url_update(directLinkUpdate.value);
}
onMounted(() => {
wt_ota_get_update_info();
wt_ota_get_progress();
});
onUnmounted(() => {
updateStore.clearProgressInterval();
});
</script>
<style scoped>
.description-style :deep(.el-descriptions__label) {
@apply w-32
}
</style>

View File

@ -1,17 +1,17 @@
<template>
<div class="text-layout">
<h1 class="page-title">
Wi-Fi 配置
Wi-Fi {{ translate('wifi.settings') }}
</h1>
<el-divider></el-divider>
<h2 class="mb-4 text-xl font-bold tracking-tight md:text-2xl lg:text-3xl">连接Wi-Fi</h2>
<h2 class="mb-4 text-xl font-bold tracking-tight md:text-2xl lg:text-3xl">{{ translate('wifi.connection') }} Wi-Fi</h2>
<el-form label-width="auto" ref="formRef" :model="ssidValidateForm" class="m-auto">
<el-form-item
label="Wi-Fi"
label="Wi-Fi"
prop="wifiSsid"
:rules="[
{ required: true, message: '请输入WIFI名'},
{ required: true, message: translate('wifi.warnWifiName')},
]"
>
<div class="flex w-full">
@ -36,7 +36,7 @@
</div>
</div>
</el-form-item>
<el-form-item label="密码">
<el-form-item :label="translate('wifi.password')">
<el-input
v-model="ssidValidateForm.password"
show-password
@ -46,33 +46,21 @@
</el-form-item>
<div class="mb-2">
<el-alert type="info" show-icon>
如果不是通过透传器的热点连接更换Wi-Fi将导致此界面与透传器断开连接
{{ translate("wifi.connectInfoHTML")}}
</el-alert>
</div>
<div class="flex justify-center">
<el-button @click="onConnectClick" type="primary">连接</el-button>
<el-button @click="onConnectClick" type="primary">{{ translate('wifi.connect') }}</el-button>
</div>
</el-form>
<el-divider></el-divider>
<div class="flex items-center">
<h5 class="text-md font-bold text-gray-800 w-32">Wi-Fi模式</h5>
<h5 class="text-md font-bold text-gray-800 w-32">Wi-Fi {{ translate('wifi.mode') }}</h5>
<div class="flex shrink-0">
<el-tooltip effect="light">
<template #content>
<p>热点+终端模式并存会影响稳定性且保持热点开启会增加功耗</p>
<p>
<el-text size="small">智能模式</el-text>
成功连接Wi-Fi30秒后自动关闭热点断开连接5秒后自动打开热点
</p>
<p>
<el-text size="small">热点+终端共存模式</el-text>
方便使用但是影响稳定性
</p>
<p>
<el-text size="small">单热点模式缺点</el-text>
无网络
</p>
<div v-html="translate('wifi.modeTipsHtml')"></div>
</template>
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip>
@ -85,38 +73,41 @@
:label="item.label"
/>
</el-select>
<el-button type="primary" @click="wifiChangeMode" :loading="wifiMode_loading">保存</el-button>
<el-button type="primary" @click="wifiChangeMode" :loading="wifiMode_loading">{{ translate('wifi.save') }}</el-button>
</div>
<el-divider></el-divider>
<el-descriptions
title="Wi-Fi终端(STA)信息"
:column="1"
border
class="description-style"
>
<template #title>
Wi-Fi {{ translate('wifi.stationInfo') }}
<el-tag v-if="!isConnected" type="danger">{{ translate('wifi.disconnected') }}</el-tag>
</template>
<template #extra>
<el-switch v-model="wifiSta_On" :disabled="wsStore.state != ControlEvent.CONNECTED || !wifiAp_On"
active-text="已开启" inactive-text="未开启" :loading="wifiMode_loading"
<el-switch v-model="wifiSta_On" :disabled="!isConnected || !wifiAp_On"
:active-text="translate('wifi.enabled')" :inactive-text="translate('wifi.disabled')" :loading="wifiMode_loading"
:before-change="()=>beforeWifiModeChange('STA')"
/>
</template>
<el-descriptions-item span="4">
<template #label>
<div>
信号强度
{{ translate('wifi.signalStrength') }}
</div>
</template>
<template #default>
<p>{{ wifiStaApInfo.rssi }}</p>
<p> {{ wifi_rssi_to_percent(wifiStaApInfo.rssi) }} % ({{ wifiStaApInfo.rssi }} dBm)</p>
</template>
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
Wi-Fi(SSID)
Wi-Fi(SSID)
</div>
</template>
<p>{{ wifiStaApInfo.ssid }}</p>
@ -139,14 +130,14 @@
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>IP(内网地址)</div>
<div>IP({{ translate('wifi.internalAddress') }})</div>
</template>
<p>{{ wifiStaApInfo.ip }}</p>
</el-descriptions-item>
<el-descriptions-item span="4">
<template #label>
<div>
网关
{{ translate('wifi.gateway') }}
</div>
</template>
<p>{{ wifiStaApInfo.gateway }}</p>
@ -154,7 +145,7 @@
<el-descriptions-item span="4">
<template #label>
<div>
掩码
{{ translate('wifi.netmask') }}
</div>
</template>
<p>{{ wifiStaApInfo.netmask }}</p>
@ -162,7 +153,7 @@
<el-descriptions-item span="4">
<template #label>
<div>
首选DNS
{{ translate('wifi.primaryDNS') }}
</div>
</template>
<p>{{ wifiStaApInfo.dns_main }}</p>
@ -170,7 +161,7 @@
<el-descriptions-item span="4">
<template #label>
<div>
备用DNS
{{ translate('wifi.backupDNS') }}
</div>
</template>
<p>{{ wifiStaApInfo.dns_backup }}</p>
@ -179,10 +170,10 @@
<el-descriptions-item span="4">
<template #label>
<div>
IP分配模式
{{ translate('wifi.IPmode') }}
</div>
</template>
<el-select v-model="wifiStaticInfo.static_ip_en" :disabled="wsStore.state != ControlEvent.CONNECTED">
<el-select v-model="wifiStaticInfo.static_ip_en" :disabled="!isConnected">
<el-option
v-for="item in staIPModeOptions"
:key="item.key"
@ -193,14 +184,14 @@
</el-descriptions-item>
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
<template #label>
<div>IP(内网地址)</div>
<div>IP({{ translate('wifi.internalAddress') }})</div>
</template>
<el-input v-model="wifiStaticInfo.ip"></el-input>
</el-descriptions-item>
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
<template #label>
<div>
网关
{{ translate('wifi.gateway') }}
</div>
</template>
<el-input v-model="wifiStaticInfo.gateway"></el-input>
@ -208,7 +199,7 @@
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
<template #label>
<div>
掩码
{{ translate('wifi.netmask') }}
</div>
</template>
<el-input v-model="wifiStaticInfo.netmask"></el-input>
@ -217,10 +208,10 @@
<el-descriptions-item span="4">
<template #label>
<div>
DNS模式
{{ translate('wifi.DNSmode') }}
</div>
</template>
<el-select v-model="wifiStaticInfo.static_dns_en" :disabled="wsStore.state != ControlEvent.CONNECTED">
<el-select v-model="wifiStaticInfo.static_dns_en" :disabled="!isConnected">
<el-option
v-for="item in staDNSModeOptions"
:key="item.key"
@ -232,7 +223,7 @@
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
<template #label>
<div>
首选DNS
{{ translate('wifi.primaryDNS') }}
</div>
</template>
<el-input v-model="wifiStaticInfo.dns_main"></el-input>
@ -240,34 +231,37 @@
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
<template #label>
<div>
备用DNS
{{ translate('wifi.backupDNS') }}
</div>
</template>
<el-input v-model="wifiStaticInfo.dns_backup"></el-input>
</el-descriptions-item>
</el-descriptions>
<div class="flex justify-center mt-4">
<el-button type="primary" :loading="wifiMode_loading" @click="wifiStaSetStaticInfo">保存</el-button>
<el-button type="primary" :loading="wifiMode_loading" @click="wifiStaSetStaticInfo">{{ translate('wifi.save') }}</el-button>
</div>
<el-divider></el-divider>
<el-descriptions
title="Wi-Fi自发热点(AP)信息"
:column="1"
border
class="description-style"
>
<template #title>
Wi-Fi {{ translate('wifi.hotspotInfo') }}
<el-tag v-if="!isConnected" type="danger">{{ translate('wifi.disconnected') }}</el-tag>
</template>
<template #extra>
<el-switch v-model="wifiAp_On" :disabled="wsStore.state != ControlEvent.CONNECTED || !wifiSta_On"
:loading="wifiMode_loading" active-text="已开启" inactive-text="未开启"
<el-switch v-model="wifiAp_On" :disabled="!isConnected || !wifiSta_On"
:loading="wifiMode_loading" :active-text="translate('wifi.enabled')" :inactive-text="translate('wifi.disabled')"
:before-change="()=>beforeWifiModeChange('AP')"
/>
</template>
<el-descriptions-item span="6">
<template #label>
<div>
Wi-Fi(SSID)
Wi-Fi(SSID)
</div>
</template>
<div class="flex">
@ -277,7 +271,7 @@
<el-descriptions-item span="6">
<template #label>
<div>
密码
{{ translate('wifi.password') }}
</div>
</template>
<el-input v-model="wifiApInfo.password"></el-input>
@ -303,7 +297,7 @@
<el-descriptions-item span="4">
<template #label>
<div>
网关
{{ translate('wifi.gateway') }}
</div>
</template>
{{ wifiApInfo.gateway }}
@ -312,14 +306,14 @@
<el-descriptions-item span="4">
<template #label>
<div>
掩码
{{ translate('wifi.netmask') }}
</div>
</template>
{{ wifiApInfo.netmask }}
</el-descriptions-item>
</el-descriptions>
<div class="flex justify-center mt-4">
<el-button type="primary" :loading="wifiMode_loading" @click="wifiApChangeCredential">保存</el-button>
<el-button type="primary" :loading="wifiMode_loading" @click="wifiApChangeCredential">{{ translate('wifi.save') }}</el-button>
</div>
<el-divider></el-divider>
</div>
@ -327,7 +321,7 @@
</template>
<script setup lang="ts">
import {computed, onMounted, onUnmounted, reactive, ref} from "vue";
import {computed, type ComputedRef, onMounted, onUnmounted, reactive, ref} from "vue";
import {
type IWifiMode,
wifi_ap_get_info,
@ -355,9 +349,10 @@ import {registerModule, unregisterModule} from "@/router/msgRouter";
import {useWsStore} from "@/stores/websocket";
import {globalNotify, globalNotifyRightSide} from "@/composables/notification";
import {isDevMode} from "@/composables/buildMode";
import {translate} from "@/locales";
const formRef = ref<FormInstance>()
let wifiListPlaceholder = ref("我的WIFI")
let wifiListPlaceholder = ref("MY-WIFI")
let ssidValidateForm = reactive({
wifiSsid: "",
password: "",
@ -370,58 +365,60 @@ let wifiAp_On = ref(false);
let wifiMode = ref(-1);
let wifiModeOptions = [
let wifiModeOptions = computed( () => [
{
label: "智能热点+常开终端 (AP+STA)",
label: translate('wifi.APauto_STA'),
key: WifiMode.WIFI_AP_AUTO_STA_ON,
}, {
label: "仅开启热点 (AP)",
label: translate('wifi.APonly'),
key: WifiMode.WIFI_AP_ON_STA_OFF,
}, {
label: "[不推荐] 常开热点+常开终端 (AP+STA)",
label: translate('wifi.AP_STA'),
key: WifiMode.WIFI_AP_STA_ON,
}, /* {
}, /*
{
value: "仅开启终端STA",
key: 2,
},*/
]
])
let wsStore = useWsStore();
const defWifiInfo: WifiInfo = {
cmd: 1,
module: 1,
gateway: "未连接",
ip: "未连接",
mac: "未连接",
dns_main: "未连接",
dns_backup: "未连接",
gateway: "-",
ip: "-",
mac: "-",
dns_main: "-",
dns_backup: "-",
rssi: 0,
netmask: "未连接",
ssid: "未连接",
netmask: "-",
ssid: "-",
password: "",
}
const staIPModeOptions = [
{
label: "自动 (DHCP)",
label: translate('wifi.autoIP'),
key: 0,
}, {
label: "静态IP",
label: translate('wifi.staticIP'),
key: 1,
},
]
const staDNSModeOptions = [
{
label: "自动 (使用网关)",
label: translate('wifi.autoDNS'),
key: 0,
}, {
label: "静态DNS",
label: translate('wifi.staticDNS'),
key: 1,
},
]
const isConnected = computed(() => wsStore.state === ControlEvent.CONNECTED)
let wifiStaApInfo = reactive<WifiInfo>({...defWifiInfo});
let wifiApInfo = reactive<WifiInfo>({...defWifiInfo});
let wifiStaticInfo = reactive<IWifiStaStaticInfo>({
@ -439,7 +436,7 @@ let scan_cb: any;
let connectBtnClicked = 0;
let options: Array<WifiScanInfo> = [];
const scanText = computed(() => {
return scanning.value ? "扫描中" : "扫描";
return scanning.value ? translate("wifi.scanning") : translate("wifi.scan");
});
const querySearch = (queryString: string, cb: any) => {
@ -463,7 +460,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
}
if (connectBtnClicked) {
connectBtnClicked = 0;
globalNotifyRightSide(wifiStaApInfo.ssid + " 连接成功", "success");
globalNotifyRightSide(wifiStaApInfo.ssid + " " + translate('wifi.connectionSuccess'), "success");
wifi_sta_get_static_info();
}
break;
@ -487,7 +484,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
scan_cb(options);
scan_cb = null;
}
globalNotifyRightSide("扫描完成", "success");
globalNotifyRightSide(translate('wifi.scanDone'), "success");
break;
}
case WifiCmd.WIFI_API_JSON_DISCONNECT:
@ -504,7 +501,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
const modeInfo = msg as IWifiMode;
wifiMode_loading.value = false;
if (modeInfo.err !== undefined) {
globalNotifyRightSide("设置失败", "error");
globalNotifyRightSide(translate('wifi.setFailed'), "error");
return;
}
@ -532,7 +529,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
if (wifiCred.err !== undefined) {
globalNotifyRightSide(wifiCred.err, "error");
} else {
globalNotifyRightSide("已保存配置", "success");
globalNotifyRightSide(translate('wifi.setSuccess'), "success");
}
wifiMode_loading.value = false;
@ -540,7 +537,6 @@ const onClientMsg = (msg: ApiJsonMsg) => {
}
case WifiCmd.WIFI_API_JSON_STA_GET_STATIC_INFO: {
const staticInfo = msg as IWifiStaStaticInfo & ApiJsonMsg;
console.log("@@@", staticInfo);
Object.assign(wifiStaticInfo, staticInfo);
break;
}
@ -574,8 +570,8 @@ const onClientCtrl = (msg: ControlMsg) => {
};
function onScanClick() {
if (wsStore.state !== ControlEvent.CONNECTED) {
globalNotify("调试器未连接", 'error');
if (!isConnected.value) {
globalNotify(translate('wifi.debuggerNotConnected'), 'error');
return;
}
scanning.value = true;
@ -583,8 +579,8 @@ function onScanClick() {
}
function onConnectClick() {
if (wsStore.state !== ControlEvent.CONNECTED) {
globalNotify("调试器未连接", 'error');
if (!isConnected.value) {
globalNotify(translate('wifi.debuggerNotConnected'), 'error');
return;
}
if (ssidValidateForm.wifiSsid !== "") {
@ -610,9 +606,20 @@ function wifiChangeMode() {
wifi_set_mode(wifiMode.value);
}
function wifi_rssi_to_percent(rssi: number)
{
if (rssi <= -100) {
return 0;
} else if (rssi >= -50) {
return 100;
} else {
return 2 * (rssi + 100);
}
}
function wifiApChangeCredential() {
if (wifiApInfo.ssid === "") {
globalNotifyRightSide("请输入AP名称", "error");
globalNotifyRightSide(translate('wifi.enterAPName'), "error");
return;
}
wifiMode_loading.value = true;
@ -644,7 +651,7 @@ onUnmounted(() => {
</script>
<style scoped>
<style scoped lang="postcss">
.description-style :deep(.el-descriptions__label) {
@apply w-32
}

View File

@ -1,11 +1,12 @@
<template>
<nav class="relative px-2 py-0.5 sm:py-1 flex justify-between items-center border-b h-full">
<div class="flex">
<button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 pl-1 mx-4">
<button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 pl-1 mx-2 sm:mx-4">
<svg class="block h-3 lg:h-4 lg:w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<title>导航侧栏</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path>
</svg>
<el-badge v-if="updateStore.canUpdate" is-dot></el-badge>
</button>
<router-link to="/" class="text-3xl px-4 font-bold leading-none hidden items-center sm:flex" title="走,去码头整点薯条">
@ -19,8 +20,8 @@
<!-- <router-link to="/" class="flex items-center text-sm text-blue-600 font-bold">主页</router-link>-->
<!-- <a class="flex items-center text-sm text-blue-600 font-bold" href="/">主页6</a>-->
<div class="flex pt-0.5 sm:pt-1 ml-4 text-sm items-center sm:hidden">
<router-link :to="route.fullPath">{{ route.meta.title }}</router-link>
<div class="flex pt-0.5 sm:pt-1 ml-4 text-xs items-center sm:hidden">
<router-link :to="route.fullPath">{{ $route.meta.title }}</router-link>
</div>
</div>
@ -35,6 +36,20 @@
<!-- <a class="md:ml-auto md:mr-3"></a>-->
<div class="flex h-full">
<div id="page-spec-slot" class="content-center h-full flex flex-row"></div>
<div class="mr-2">
<el-select v-model="language" class="min-w-20 h-full" @change="handleLanguageChange">
<el-option value="en">🇺🇸 English</el-option>
<el-option value="zh">🇨🇳 简体中文</el-option>
<el-option value="fr">🇫🇷 Français</el-option>
<template #label>
<div class="flex">
<InlineSvg name="translate" class="w-4 mr-1"></InlineSvg>
{{ languageFlag }}
</div>
</template>
</el-select>
</div>
<div class="lg:hidden">
<el-button :type="wsColor" size="small" class="transition duration-1000 min-h-full">
<InlineSvg v-show="wsColor!=='success'" name="link-off" class="mr-2" width="20"></InlineSvg>
@ -67,8 +82,11 @@
<div class="flex flex-col justify-between m-4 mt-0">
<ul>
<li v-for="(item, index) in menuItems" class="mb-1" :key="index">
<router-link @click="sideMenuOpen=false" :title="item.name" :to="item.href" :class="[sideMenuItemClass, item?.class]">{{ item.name }}</router-link>
<li v-for="(item, index) in sideBarItems" class="mb-1" :key="index">
<router-link @click="sideMenuOpen=false" :title="item.name" :to="item.href" :class="[sideMenuItemClass, item?.class]">
{{ item.name }}
<el-badge v-if="item?.badge?.value" is-dot></el-badge>
</router-link>
</li>
</ul>
</div>
@ -77,9 +95,9 @@
<div>
<el-button @click="toggle">
<InlineSvg v-if="!isFullscreen" name="open-in-full" width="16px" fill="#000000"></InlineSvg>
<p v-if="!isFullscreen">全屏</p>
<p v-if="!isFullscreen">{{ translate('page.fullscreen') }}</p>
<InlineSvg v-if="isFullscreen" name="close-fullscreen" width="16px" fill="#000000"></InlineSvg>
<p v-if="isFullscreen">缩小</p>
<p v-if="isFullscreen">{{ translate('page.windowed') }}</p>
</el-button>
</div>
</template>
@ -105,18 +123,31 @@
<script lang="ts" setup>
import InlineSvg from "@/components/InlineSvg.vue";
import {computed, ref} from "vue";
import {computed, type ComputedRef, type Ref, ref} from "vue";
import {useWsStore} from "@/stores/websocket";
import {translate} from "@/locales";
import {ControlEvent} from "@/api";
import {useRoute} from "vue-router";
import { useFullscreen } from '@vueuse/core'
import {useUpdateStore} from "@/stores/useUpdateStore";
import {isOTAEnabled} from "@/composables/buildMode";
import {getFlagFromLang, locale, setLang} from "@/i18n"
const wsStore = useWsStore();
const updateStore = useUpdateStore();
const {isFullscreen, toggle} = useFullscreen();
const route = useRoute();
const language = ref(locale);
const sideMenuItemClass = "block p-4 text-sm font-semibold hover:bg-blue-50 hover:text-blue-600 rounded"
const languageFlag = computed(() => {
return getFlagFromLang(language.value);
});
function handleLanguageChange(lang: string) {
setLang(lang);
}
const sideMenuItemClass = "block p-4 text-sm font-semibold hover:bg-blue-50 hover:text-blue-600 rounded flex"
const sideMenuOpen = ref(false);
const stateMenuOpen = ref(false)
@ -137,30 +168,56 @@ const wsColor = computed(() => {
});
const wsState = computed(() => {
return translate(wsStore.state);
return translate(wsStore.state.toLocaleLowerCase());
});
type Item = {
name: string;
href: string;
class?: string;
badge?: Ref<boolean>;
};
const menuItems: Item[] = ([
/* {
name: translate("page.home"),
href: "/",
}, */{
const menuItems: ComputedRef<Item[]> = computed(() => ([
{
name: translate("page.uart"),
href: "/uart",
}, {
name: translate("page.wifi"),
href: "/wifi",
}, {
name: translate("page.about"),
href: "/about",
}, {
name: translate("page.feedback"),
href: "/feedback",
},
]);
]));
const sideBarItems: ComputedRef<Item[]> = computed(() => {
const items: Item[] = [
{
name: translate("page.uart"),
href: "/uart",
}, {
name: translate("page.wifi"),
href: "/wifi",
}, {
name: translate("page.about"),
href: "/about",
}, {
name: translate("page.feedback"),
href: "/feedback",
},
];
if (isOTAEnabled()) {
items.push({
name: translate("page.update"),
href: "/update",
badge: computed(() => updateStore.canUpdate),
})
}
return items;
});
</script>
@ -188,5 +245,9 @@ const menuItems: Item[] = ([
padding: 0;
}
.el-select :deep(.el-select__wrapper) {
@apply h-full;
}
</style>

View File

@ -0,0 +1,457 @@
<template>
<div>
<el-tabs v-model="store.configPanelTab" class="mx-2 custom-tabs fit">
<el-tab-pane name="first" class="min-h-80">
<template #label>{{ $t("uart.port") }}</template>
<div class="flex flex-col gap-2">
<el-form :size="store.winLeft.show ? '' : 'small'" label-position="left" label-width="auto">
<el-form-item
class="mb-2"
>
<template #label>{{ $t("uart.baudrate") }}</template>
<div class="flex w-full">
<el-select v-model="store.uartBaud" :teleported="false" @change="onUartBaudChange">
<template #header>
<div class="overflow-auto max-h-40">
<div class="flex gap-0">
<el-input-number
v-model="uartCustomBaud"
:placeholder="translate('uart.customBaud')"
size="small"
:controls="false"
:min="110"
class="flex-grow"
></el-input-number>
<el-button size="small" @click="onUseCustomUartBaud">{{ $t('uart.use') }}</el-button>
<!-- <el-button size="small" @click="onConfirm" class="ml-0">增加</el-button>-->
</div>
<el-option-group :label="translate('uart.commonlyUsed')">
<el-option
v-for="item in store.predefinedUartBaudFrequent"
:key="item.baud"
:value="item.baud"
class="border-b list-none"
/>
</el-option-group>
<el-option-group :label="translate('uart.other')">
<el-option
v-for="item in store.uartBaudList"
:key="item.baud"
:value="item.baud"
class="border-b list-none"
/>
</el-option-group>
</div>
</template>
</el-select>
</div>
</el-form-item>
<p class="text-xs">{{ $t('uart.actual') }} {{ $t('uart.baudrate') }}:{{ store.uartBaudReal }}</p>
<el-form-item :label="translate('uart.dataBits')" class="mb-2">
<el-select v-model="store.uartConfig.data_bits" :teleported="false"
placeholder="Select" @change="onUartConfigChange">
<el-option
v-for="item in uartDataBitsOptions"
:key="item.key"
:value="item.key"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item :label="translate('uart.parity')" class="mb-2">
<el-select v-model="store.uartConfig.parity" :teleported="false"
placeholder="Select" @change="onUartConfigChange">
<el-option
v-for="item in uartParityOptions"
:key="item.key"
:value="item.key"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item :label="translate('uart.stopBits')">
<el-select v-model="store.uartConfig.stop_bits" :teleported="false"
placeholder="Select" @change="onUartConfigChange">
<el-option
v-for="item in uartStopBitsOptions"
:key="item.key"
:value="item.key"
:label="item.label"
/>
</el-select>
</el-form-item>
<div class="flex justify-center">
<el-button :type="store.acceptIncomingData ? 'danger': 'success'"
:disabled="wsStore.state !== ControlEvent.CONNECTED"
@click="store.acceptIncomingData = !store.acceptIncomingData"
>
{{ store.acceptIncomingData ? $t("uart.stopCommunication") : $t("uart.startCommunication") }}
</el-button>
</div>
</el-form>
</div>
</el-tab-pane>
<!-- ///////////////////////////////////////////////////////////////// -->
<el-tab-pane name="second">
<template #label>{{ $t("uart.displayPannel") }}</template>
<div class="flex flex-col">
<el-collapse v-model="collapseActiveName">
<el-collapse-item name="1">
<template #title>
{{ $t('uart.displayOptions') }}
</template>
<template #default>
<div class="flex flex-col gap-2">
<div class="flex flex-col">
<el-checkbox border v-model="store.showText" :label="translate('uart.text')"/>
</div>
<div class="flex flex-col">
<el-checkbox border v-model="store.showHex" label="HEX"/>
</div>
<div class="flex flex-col">
<el-checkbox border v-model="store.showHexdump" label="HEXDUMP"/>
</div>
<div class="flex flex-col">
<el-checkbox border v-model="store.showTimestamp" :label="translate('uart.timestamp')"/>
</div>
<div class="flex flex-col">
<el-checkbox border v-model="store.enableLineWrap" :label="translate('uart.lineWrap')"/>
</div>
<el-tag type="success">
<el-text type="success">RX HEXDUMP {{ $t("uart.highlight") }}</el-text>
<el-color-picker v-model="store.RxHexdumpColor" show-alpha :predefine="store.predefineColors"
size="small"/>
</el-tag>
<el-tag type="primary">
<el-text type="primary">TX HEXDUMP {{ $t("uart.highlight") }}</el-text>
<el-color-picker v-model="store.TxHexdumpColor" show-alpha :predefine="store.predefineColors"
size="small"/>
</el-tag>
</div>
</template>
</el-collapse-item>
<el-collapse-item name="2" :title="translate('uart.frameBreakStrategy')">
<VueDraggable v-model="store.frameBreakRules" target="tbody" handle=".sort-target"
:animation="150"
:on-move="checkMove">
<table class="w-full bg-white">
<thead>
<tr class="text-sm h-7">
<th>{{ $t('uart.priority') }}</th>
<th>
<div class="flex justify-center">
{{ translate('uart.rule' as TranslationKeys) }}
<el-tooltip placement="top" effect="light">
<template #content>
<div v-html="translate('uart.ruleTips')"></div>
</template>
<InlineSvg name="help" class="w-4 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip>
</div>
</th>
<th>{{ translate('uart.value' as TranslationKeys) }}</th>
</tr>
</thead>
<tbody class="text-xs text-center">
<tr v-for="(item, index) in store.frameBreakRules" :key="index">
<td :class="item.draggable ? 'sort-target' : ''">
{{ item.draggable ? index : 'NaN' }}
</td>
<td :class="item.draggable ? 'sort-target' : ''">
{{ translate("uart." + item.name) }}
</td>
<td>
<div v-if="item.type === 'number'">
<el-input-number v-if="item.name === 'timeout'" v-model="store.frameBreakDelay" :min="item.min || 0" size="small" style="width: 100px"/>
<el-input-number v-else v-model="store.frameBreakSize" :min="item.min || 0" size="small" style="width: 100px"/>
</div>
<div v-else>
<el-input class="break-input" v-model="store.frameBreakSequence" :placeholder="translate('uart.textAndEscape')" size="small"
style="width: 100px">
<template #prepend>
<el-button size="small" @click="store.frameBreakAfterSequence = false">
<span
:class="store.frameBreakAfterSequence ? 'text-gray-400' : 'text-blue-400 font-bold'">
{{ translate("uart.begin") }}
</span>
</el-button>
</template>
<template #append>
<el-button size="small" @click="store.frameBreakAfterSequence = true">
<span
:class="store.frameBreakAfterSequence ? 'text-blue-400 font-bold' : 'text-gray-300'">
{{ translate("uart.end") }}
</span>
</el-button>
</template>
</el-input>
</div>
</td>
</tr>
</tbody>
</table>
</VueDraggable>
</el-collapse-item>
<el-collapse-item name="3" :title="translate('uart.other')">
<template #default>
<div class="flex flex-col gap-2">
<el-tooltip
class="box-item"
effect="light"
placement="right-start"
>
<template #content>
<div v-html="translate('uart.ansiTooltips')"></div>
</template>
<el-checkbox border v-model="store.enableAnsiDecode">{{ translate('uart.decodeAnsiEscapeCodes') }}</el-checkbox>
</el-tooltip>
<el-input v-model="store.filterValue" :placeholder="translate('uart.textAndEscape')" clearable>
<template #prepend>
{{ translate("uart.filter") }}
</template>
</el-input>
<div class="border rounded flex flex-col">
<el-checkbox border v-model="store.dataFilterAutoUpdate">{{ translate('uart.autoUpdateNewData') }}</el-checkbox>
<el-tooltip :content="translate('uart.updateFrequencyTooltip')" placement="right" effect="light"
:show-after="500">
<div class="flex gap-4 p-2">
<el-text>{{ translate('uart.updateFrequency') }}</el-text>
<el-input-number
:step="10"
:min="10"
size="small"
v-model="store.batchUpdateTime"
>
</el-input-number>
</div>
</el-tooltip>
</div>
</div>
</template>
</el-collapse-item>
</el-collapse>
<!-- <div class="flex flex-col">-->
<!-- <el-text type="success">断帧设置</el-text>-->
<!-- <el-input v-model="store.frameBreakSequence" class="max-w-52">-->
<!-- <template #prepend>-->
<!-- 文本匹配断帧-->
<!-- </template>-->
<!-- </el-input>-->
<!-- <el-input v-model="store.frameBreakDelay" type="number" class="max-w-52">-->
<!-- <template #prepend>-->
<!-- 超时断帧-->
<!-- </template>-->
<!-- </el-input>-->
<!-- </div>-->
<!-- <div class="flex flex-col flex-wrap">-->
<!-- <el-button size="small">滚动到底</el-button>-->
<!-- <div>显示-->
<!-- <el-checkbox size="small" border>数据差异高亮</el-checkbox>-->
<!-- <el-checkbox size="small" border>TX高亮</el-checkbox>-->
<!-- <el-checkbox size="small" border>显示RX</el-checkbox>-->
<!-- <el-checkbox size="small" border>显示TX</el-checkbox>-->
<!-- <el-checkbox size="small" border>RX右对齐</el-checkbox>-->
<!-- </div>-->
<!-- &lt;!&ndash; <div>专有协议&ndash;&gt;-->
<!-- &lt;!&ndash; <el-button size="small">输入格式</el-button>&ndash;&gt;-->
<!-- &lt;!&ndash; <el-button size="small">输出格式</el-button>&ndash;&gt;-->
<!-- &lt;!&ndash; </div>&ndash;&gt;-->
<!-- </div>-->
</div>
</el-tab-pane>
<!-- ///////////////////////////////////////////////////////////// -->
<el-tab-pane :label="translate('uart.send')" name="third">
<template #label>{{ $t("uart.send") }}</template>
<div class="flex flex-col gap-2">
<el-input v-model="store.textPrefixValue" :placeholder="translate('uart.textAndEscape')" clearable>
<template #prepend>
{{ translate('uart.addHeader') }}
</template>
</el-input>
<el-input v-model="store.textSuffixValue" :placeholder="translate('uart.textAndEscape')" clearable>
<template #append>
{{ translate('uart.addFooter') }}
</template>
</el-input>
</div>
</el-tab-pane>
<el-tab-pane :label="translate('uart.proxy')" name="fourth" class="min-h-80">
<template #label>{{ $t("uart.passthrough") }}</template>
<div class="flex flex-col gap-2">
<div class="border rounded bg-white p-2">
<span class="border-r px-2">TCP {{ translate('uart.serverPort') }}</span>
<span class="px-2 cursor-not-allowed">1346</span>
</div>
<div>
<p><el-button @click="refreshTCPClientList" size="small" type="primary" :plain="true">{{ translate('uart.refresh') }}</el-button> {{ translate('uart.connectedClient') }}</p>
<el-table :data="dfStore.instanceList.filter((item) => (item.port_info as ISocketInfo).local_port === 1346)" :empty-text="translate('uart.noClientConnected')">
<el-table-column label="IP" prop="port_info.foreign_ip" />
<el-table-column :label="translate('uart.port')" prop="port_info.foreign_port"/>
</el-table>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import {VueDraggable} from 'vue-draggable-plus'
import {computed, ref} from "vue";
import {useDataViewerStore} from "@/stores/dataViewerStore";
import {useWsStore} from "@/stores/websocket";
import {globalNotify} from "@/composables/notification";
import {ControlEvent} from "@/api";
import type {MoveEvent} from "sortablejs";
import InlineSvg from "@/components/InlineSvg.vue";
import {useDataFlowStore} from "@/stores/useDataFlowStore";
import {wt_data_flow_get_instance_list, type ISocketInfo} from "@/api/apiDataFlow";
import {uart_set_baud, uart_set_config} from "@/api/apiUart";
import {useUartStore} from "@/stores/useUartStore";
import {translate, type TranslationKeys} from "@/locales";
const store = useDataViewerStore()
const uartStore = useUartStore()
const wsStore = useWsStore()
const dfStore = useDataFlowStore()
const collapseActiveName = ref(["1", "2", "3"])
const uartCustomBaud = ref(1500000)
const uartDataBitsOptions = [
{
key: 5,
label: "5 bits",
}, {
key: 6,
label: "6 bits",
}, {
key: 7,
label: "7 bits",
}, {
key: 8,
label: "8 bits",
}
]
const uartParityOptions = computed(() => [
{
key: 0,
label: translate("uart.parityNone"),
}, {
key: 1,
label: translate("uart.parityOdd"),
}, {
key: 2,
label: translate("uart.parityEven"),
}
]);
const uartStopBitsOptions = [
{
key: 1,
label: "1",
}, {
key: 15,
label: "1.5",
}, {
key: 2,
label: "2",
}
]
const onUseCustomUartBaud = () => {
if (uartCustomBaud.value) {
store.uartBaud = uartCustomBaud.value;
onUartBaudChange();
} else {
globalNotify("波特率格式错误", "warning")
}
}
function onUartBaudChange() {
uart_set_baud(store.uartBaud, uartStore.uartNum);
}
function onUartConfigChange() {
uart_set_config(store.uartConfig, uartStore.uartNum);
}
function checkMove(event: MoveEvent) {
// Find index of related element
const toIndex: number = Array.from(event.to.children).indexOf(event.related);
return !!store.frameBreakRules[toIndex].draggable;
}
function refreshTCPClientList() {
dfStore.instanceList = [];
wt_data_flow_get_instance_list();
}
</script>
<style scoped>
.custom-tabs {
box-sizing: border-box;
}
.custom-tabs :deep(.el-tabs__item.is-top) {
padding: unset;
}
.custom-tabs :deep(.el-tabs__nav.is-top) {
@apply w-full flex justify-around
}
.custom-tabs :deep(.el-collapse-item__wrap) {
transition: all 0s; /* Customize the duration and easing */
}
.sortable-chosen {
background-color: var(--el-color-primary-light-9);
}
.sort-target {
cursor: move;
}
tr td {
@apply p-1;
}
.break-input :deep(.el-input-group__prepend), .break-input :deep(.el-input-group__append) {
background-color: unset;
@apply p-0 min-w-6
}
</style>

View File

@ -0,0 +1,182 @@
<template>
<div class="flex items-center mb-2 flex-wrap gap-2">
<el-button type="primary" @click="importSettings">{{ translate('uart.import') }}</el-button>
<el-button type="warning" @click="exportSettings">{{ translate('uart.export') }}</el-button>
<el-tooltip
effect="light"
placement="top"
:show-after="500"
>
<template #content>
<p>{{ translate('uart.resetTooltip') }}</p>
</template>
<el-button type="info" @click="resetSettings">{{ translate('uart.reset') }}</el-button>
</el-tooltip>
<el-tooltip
effect="light"
placement="top"
:show-after="500"
>
<template #content>
<p>{{ translate('uart.saveToLocalTooltip') }}</p>
</template>
<el-checkbox border v-model="store.autoSaveSettings">{{ translate('uart.saveToLocal') }}</el-checkbox>
</el-tooltip>
</div>
<div class="flex items-center mb-2 flex-wrap gap-2">
<el-button type="primary" @click="() => {
store.macroData.push({
value: '',
label: translate('uart.send'),
id: store.macroId,
})
store.macroId++;
}">{{ translate('uart.add') }}
</el-button>
<el-checkbox v-model="editMode" border>{{ translate('uart.edit') }}</el-checkbox>
<el-checkbox v-model="draggableEnabled" border>{{ translate('uart.drag') }}</el-checkbox>
</div>
<div>
<el-alert v-if="store.ipChangeAlert" @close="store.ipChangeAlert=false">{{ translate('uart.ipChangeAlert') }}</el-alert>
</div>
<VueDraggable v-model="store.macroData" handle=".sort-target"
:animation="150" class="break-input">
<div v-for="(item, index) in store.macroData" :key="item.id" class="w-full text-xs flex items-center py-0.5"
:class="editMode ? 'macroButtons' : ''">
<el-tag size="large" type="success" v-if="draggableEnabled" class="sort-target mr-1">
=
</el-tag>
<el-input v-model="item.value" class="font-mono">
<template #append>
<el-input v-if="editMode" v-model="item.label"></el-input>
<el-button v-else @click="onSendClick(item.value)" type="primary">{{ item.label }}</el-button>
</template>
</el-input>
<el-link :underline="false" @click="store.macroData.splice(index, 1);">
<el-tag size="large" type="danger" v-if="editMode" class="ml-1">
x
</el-tag>
</el-link>
</div>
</VueDraggable>
</template>
<script setup lang="ts">
import {VueDraggable} from "vue-draggable-plus";
import {onMounted, ref} from "vue";
import {globalNotify, globalNotifyRightSide} from "@/composables/notification";
import {useDataViewerStore} from "@/stores/dataViewerStore";
import {translate} from "@/locales";
const editMode = ref(false);
const draggableEnabled = ref(true);
const store = useDataViewerStore();
const emit = defineEmits(['winSizeRefresh'])
function onSendClick(val: string) {
if (!val && !store.hasAddedText) {
globalNotify("无帧头帧尾、发送框无数据发送")
return;
}
if (store.acceptIncomingData) {
store.addString(val, false, true);
} else {
store.addString(val, false, true, 1);
}
}
function importSettings() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.onchange = (event: Event) => {
const target = event.target as HTMLInputElement;
if (!target.files) return;
const file = target.files[0];
const reader = new FileReader();
reader.onload = (e: ProgressEvent<FileReader>) => {
const text = e.target?.result;
if (typeof text !== 'string') return;
try {
store.loadSettings(text);
emit('winSizeRefresh', '');
} catch (error) {
globalNotifyRightSide('导入失败', "error");
console.log("error", error);
}
};
reader.readAsText(file);
};
input.click();
}
function exportSettings() {
let obj = {
version: "v0.1.0",
/* Macro Window */
...store.settings
};
const dataStr = JSON.stringify(obj, null, 2);
const blob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = "settingsBackup.json";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
function resetSettings() {
localStorage.clear();
}
onMounted(() => {
store.loadSettings();
});
</script>
<style scoped>
.sortable-chosen {
background-color: var(--el-color-primary-light-7);
}
.sort-target {
cursor: move;
}
.macroButtons :deep(.el-input-group__append) {
padding: 0;
}
.break-input :deep(.el-input-group__append) {
background-color: unset;
border-color: unset;
color: unset;
}
.break-input :deep(.el-input-group__append button.el-button) {
background-color: var(--el-color-primary-light-9);
border-color: var(--el-border-color);
color: unset;
border-radius: 0 5px 5px 0;
}
.break-input :deep(.el-input-group__append button.el-button):hover {
background-color: var(--el-color-primary-light-7);
}
</style>

View File

@ -0,0 +1,440 @@
<template>
<div class="flex min-h-7 overflow-auto">
<el-popover
placement="bottom"
trigger="click"
:hide-after="0"
transition="none"
width="300"
>
<div v-if="!store.winLeft.show" class="h-[40vh] overflow-auto">
<text-data-config></text-data-config>
</div>
<template #reference>
<el-link v-show="!store.winLeft.show" type="primary">
<InlineSvg name="arrow_drop_down" class="h-6 mb-1 px-2"></InlineSvg>
</el-link>
</template>
</el-popover>
<div class="flex">
<el-checkbox size="small" v-model="store.forceToBottom" :label="translate('uart.autoScrollToBottom')" border/>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>{{ translate('uart.clearTooltip') }}</p>
</template>
<el-button size="small" @click="store.clearFilteredBuff">
<InlineSvg class="h-5" name="trash"></InlineSvg>
{{ $t('uart.clearScreen') }}
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>{{ translate('uart.clearTooltip') }}</p>
</template>
<el-button size="small" @click="store.refreshFilteredBuff">
{{ $t('page.update') }}
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>{{ translate('uart.autoUpdateTooltip') }}</p>
</template>
<el-checkbox size="small" border v-model="store.dataFilterAutoUpdate">
{{ $t('uart.autoUpdate') }}
</el-checkbox>
</el-tooltip>
</div>
</div>
<div class="flex flex-grow overflow-hidden border-2 rounded scroll-m-2">
<v-virtual-scroll
v-if="store.showVirtualScroll"
:items="store.dataFiltered"
id="myScrollerID"
ref="vuetifyVirtualScrollRef"
class="font-mono break-all text-sm"
:class="[store.enableLineWrap ? 'break-all' : 'text-nowrap']"
>
<template v-slot:default="{ item, }">
<div class="">
<div class="flex" :class="[store.enableLineWrap ? 'whitespace-pre-wrap' : 'whitespace-pre']">
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
<span>{{ item.time }}</span>-RX|</p>
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>TX-|</p>
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>NS-|</p>
<p v-show="store.showText"
v-html="item.str"></p>
</div>
<div class="flex">
<p v-show="store.showHex" class="">{{ item.hex }}</p>
</div>
<div class="flex whitespace-pre">
<p v-show="store.showHexdump"
class="text-nowrap"
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }"
v-html="item.hexdump"
></p>
</div>
</div>
</template>
</v-virtual-scroll>
<v-virtual-scroll
v-else
:items="store.dataFiltered"
id="myScrollerID"
ref="vuetifyVirtualScrollRef2"
class="font-mono break-all text-sm"
:class="[store.enableLineWrap ? 'break-all' : 'text-nowrap']"
>
<template v-slot:default="{ item, }">
<div>
<div class="flex" :class="[store.enableLineWrap ? 'whitespace-pre-wrap' : 'whitespace-pre']">
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
<span>{{ item.time }}</span>-RX|</p>
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>TX-|</p>
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
<span>{{ item.time }}</span>NS-|</p>
<p v-show="store.showText"
v-html="item.str"></p>
</div>
<div class="flex">
<p v-show="store.showHex" class="">{{ item.hex }}</p>
</div>
<div class="flex whitespace-pre">
<p v-show="store.showHexdump"
class="text-nowrap"
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }"
v-html="item.hexdump"
></p>
</div>
</div>
</template>
</v-virtual-scroll>
</div>
<div class="shrink-0 flex h-8 mt-0.5 text-xs">
<div class="flex shrink-0">
<el-tooltip :content="translate('uart.tempDisplayTooltip')" effect="light">
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
</el-tooltip>
<p></p>
</div>
<div ref="RxHexDumpRef" class="p-0.5 border-2 rounded w-full overflow-y-scroll font-mono text-nowrap">
<p v-html="store.RxRemainHexdump"></p>
</div>
</div>
<div class="shrink-0 min-h-6 flex gap-2 justify-between overflow-auto">
<div class="flex gap-2">
<el-link @click="clearSendInput">
<el-tag class="font-mono" size="small">
<div class="flex ">
<InlineSvg class="h-5" name="trash"></InlineSvg>
<span class="content-end text-xs"></span>
</div>
</el-tag>
</el-link>
<el-tooltip :content="translate('uart.loopSendTooltip')" placement="right" effect="light" :show-after="1000">
<div class="flex align-center">
<el-checkbox v-model="store.enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
{{ translate('uart.loopSend') }}(ms)
</el-checkbox>
<el-input-number
v-model="store.loopSendFreq"
class="h-5"
size="small"
:step="10"
:min="1"
>
</el-input-number>
</div>
</el-tooltip>
<el-link @click="store.isSendTextFormat = !store.isSendTextFormat">
<el-tag class="font-mono font-bold" size="small">{{ translate('uart.sendFormat') }}{{ store.isSendTextFormat ? translate("uart.text") : "HEX" }}</el-tag>
</el-link>
</div>
<div class="flex gap-2">
<el-link @click="store.clearTxCounter()">
<el-tag class="font-mono font-bold" size="small">
{{ `TX(B):${store.TxByteCount}/ ${store.TxTotalByteCount}` }}
</el-tag>
</el-link>
<el-link type="success" @click="store.clearRxCounter()">
<el-tag class="font-mono font-bold" size="small" type="success">
{{ `RX(B):${store.RxByteCount}/ ${store.RxTotalByteCount}` }}
</el-tag>
</el-link>
<div class="flex align-center">
<el-tag class="font-mono font-bold" size="small" type="info">
<el-link class="flex" @click="store.clearDataBuff" type="warning">
<InlineSvg class="h-5" name="trash"></InlineSvg>
</el-link>
<span class="align-text-bottom">{{ translate('uart.cachedFrame') }}: {{ store.dataBufLength }}/30000</span>
</el-tag>
</div>
</div>
</div>
<div class="flex flex-row font-mono">
<el-input type="textarea" :autosize="{ minRows: 1, maxRows: 6}" v-model="store.uartInputTextBox" clearable
:placeholder="store.isSendTextFormat ?
translate('uart.textAndEscape') :
'HEX'"
@keydown="handleTextboxKeydown"
></el-input>
<el-tooltip content="Ctrl+Enter" placement="top" :auto-close="500">
<el-button type="primary"
@click="onSendClick">
{{ (store.isSendTextFormat || store.isHexStringValid) ? translate("uart.send") : translate("格式化") }}
</el-button>
</el-tooltip>
</div>
</template>
<script setup lang="ts">
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
import {useDataViewerStore} from "@/stores/dataViewerStore";
import InlineSvg from "@/components/InlineSvg.vue";
import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue";
import {debouncedWatch} from "@vueuse/core";
import {globalNotify} from "@/composables/notification";
import {translate} from "@/locales";
const count = ref(0);
const vuetifyVirtualScrollBarRef = ref(document.body);
const vuetifyVirtualScrollContainerRef = ref(document.body);
const store = useDataViewerStore();
const RxHexDumpRef = ref(document.body);
let lastScrollHeight = 0;
const mutationObserver = new MutationObserver(() => {
if (store.forceToBottom) {
lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop;
scrollToBottom();
}
});
function attachScroll() {
const parent = document.getElementById('myScrollerID') || document.body;
// used to scroll to bottom
vuetifyVirtualScrollBarRef.value = parent || document.body;
// used to monitor height changes, so that one new Items are rendered, the height change -> scroll to bottom
vuetifyVirtualScrollContainerRef.value = parent.querySelector('.v-virtual-scroll__container') || document.body;
vuetifyVirtualScrollBarRef.value.onscroll = handleScroll;
if (vuetifyVirtualScrollContainerRef.value) {
const config = {childList: true, subtree: true, attributes: true};
mutationObserver.observe(vuetifyVirtualScrollBarRef.value, config)
}
if (store.forceToBottom) {
scrollToBottom();
}
}
onMounted(() => {
attachScroll();
})
onUnmounted(() => {
mutationObserver.disconnect();
if (!store.isHexStringValid) {
store.uartInputTextBox = formatHexInput(store.uartInputTextBox);
}
});
debouncedWatch(() => store.showVirtualScroll, () => {
lastScrollHeight = 0;
mutationObserver.disconnect();
attachScroll();
}, {debounce: 80});
function addItem(nr: number) {
let rawText = "";
let maxcount = count.value + nr;
for (; count.value < maxcount; count.value++) {
let text = "";
if ((count.value & 3) === 3) {
text = `<p class="border-4">${count.value}inputasdf<br/>${count.value}asdf <br/>${count.value}asdfasdf <br/>${count.value}asdf </p>`;
text += text;
} else if ((count.value & 2) === 2) {
text = `<p class="border-4">${count.value}inputas df2<br/>${count.value} asdf asd <br/>${count.value}fasdf asdf </p>`;
text += text;
} else if ((count.value & 1) === 1) {
text = `<p class="border-4">${count.value}inputas df2asdf asd <br/>${count.value}fasdf asdf ${count.value} </p>`;
text += text;
} else {
text = `<p class="border-4">${count.value}inputasa<br/>jdhfklasjdhfklasdhflasidfhilasdfhlasdiufhlasdkfhuasnlfcyerhfcibnkuaweghnfctiklaweuyrchnlaweirtucgnawertkcgyawertcnawelcrvnawgervcawencrgf${count.value} </p>`;
}
rawText = count.value + "<p class=\"border-4\"> 666666666b\n6666 666\x1b[33m6666666666666666666666666</p>b\n"
const encoder = new TextEncoder();
const arr = encoder.encode(rawText);
store.addItem(arr, false, false, 1);
}
}
function scrollToBottom() {
nextTick(() => {
const scrollerElement = vuetifyVirtualScrollBarRef.value; // Adjust according to your setup
scrollerElement.scrollTop = scrollerElement.scrollHeight;
});
}
function formatHexInput(input: string) {
// Split the input string on spaces to process each segment separately
let str;
// Remove any "0x" prefix and handle uppercase conversion
str = input.replace(/^0x/i, ' ').toUpperCase();
// Remove any non-hexadecimal characters
str = str.replace(/[^0-9A-F]/gi, ' ');
let segments = str.split(/\s+/);
let output: string[] = [];
segments.forEach(segment => {
// Check if segment length is odd and needs padding
if (segment.length % 2 !== 0) {
segment = '0' + segment; // Prepend '0' to make the length even
}
// Split segment into array of two-character chunks
let chunked = [];
for (let i = 0; i < segment.length; i += 2) {
chunked.push(segment.substring(i, i + 2));
}
// Concatenate chunked segments and add to output
output.push(chunked.join(' '));
});
// Join all processed segments with a space and return
return output.join(' ');
}
function checkHexTextValid() {
store.isHexStringValid = store.uartInputTextBox.toUpperCase() === formatHexInput(store.uartInputTextBox);
if (!store.isHexStringValid) {
store.enableLoopSend = false;
}
}
watch(() => store.isSendTextFormat, (value) => {
if (!value) {
checkHexTextValid()
}
});
watch(() => store.uartInputTextBox, () => {
if (!store.isSendTextFormat) {
checkHexTextValid()
}
})
/* patch scroll container does not update clear filter */
watch(() => store.filterChanged, (value) => {
if (value && store.forceToBottom) {
scrollToBottom();
}
store.filterChanged = false;
})
watch(() => store.RxRemainHexdump, value => {
if (value) {
RxHexDumpRef.value.scrollTop = RxHexDumpRef.value.scrollHeight;
}
})
watch(() => store.showVirtualScroll, () => {
if (store.forceToBottom) {
scrollToBottom();
}
});
const handleScroll = () => {
if (store.forceToBottom) {
if (vuetifyVirtualScrollBarRef.value.scrollTop - lastScrollHeight < 0) {
store.forceToBottom = false;
} else {
scrollToBottom();
}
} else if ((vuetifyVirtualScrollBarRef.value.scrollHeight -
vuetifyVirtualScrollBarRef.value.scrollTop) <= vuetifyVirtualScrollBarRef.value.clientHeight) {
store.forceToBottom = true;
}
lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop;
};
watch(() => store.forceToBottom, value => {
if (value) {
setTimeout(scrollToBottom, 0);
}
});
function clearSendInput() {
store.uartInputTextBox = ""
}
function handleTextboxKeydown(ev: KeyboardEvent) {
if (ev.ctrlKey && ev.key === 'Enter') {
onSendClick();
}
}
function onSendClick() {
if (!store.uartInputTextBox && !store.hasAddedText) {
globalNotify("无帧头帧尾、发送框无数据发送")
return;
}
if (store.acceptIncomingData) {
if (store.isSendTextFormat) {
store.addString(store.uartInputTextBox, false, true);
} else if (!store.isHexStringValid) {
store.uartInputTextBox = formatHexInput(store.uartInputTextBox);
} else {
store.addHexString(store.uartInputTextBox, false, true);
}
} else {
if (store.isSendTextFormat) {
store.addString(store.uartInputTextBox, false, true, 1);
} else if (!store.isHexStringValid) {
store.uartInputTextBox = formatHexInput(store.uartInputTextBox);
} else {
store.addHexString(store.uartInputTextBox, false, true, 1);
}
}
}
</script>

View File

@ -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: {