Compare commits
68 Commits
Author | SHA1 | Date |
---|---|---|
|
9c6db50de2 | |
|
fc9648ea8b | |
|
12b56e80d6 | |
|
dccf5feaa8 | |
|
94d8061436 | |
|
aaea2c50a7 | |
|
d0325e5b44 | |
|
28eb1cedbb | |
|
b06b040e5c | |
|
e96037a073 | |
|
2f4790a57c | |
|
b7590d8997 | |
|
2082f5b60c | |
|
d7d7c94f53 | |
|
dd1b9adc0d | |
|
8e0d8234e5 | |
|
6b2d195faf | |
|
aa4e83937a | |
|
85864823af | |
|
6a7ec54ce8 | |
|
ff3b8bc9c7 | |
|
545d02e57b | |
|
dc861ab4ef | |
|
5f002865f1 | |
|
f785745c79 | |
|
d3fd661ecf | |
|
5c70211154 | |
|
6ff82d7fe4 | |
|
d45dde9261 | |
|
794c02b94c | |
|
70cf85aba5 | |
|
af73147bc5 | |
|
e53b0ed883 | |
|
72079d0cf9 | |
|
d5a9bee947 | |
|
1d5adbf14e | |
|
42624fb58c | |
|
80bbaf8f54 | |
|
4e809cde3f | |
|
4900caeda4 | |
|
c4bf06f043 | |
|
a444813a1e | |
|
d534fd4ec8 | |
|
8a84b215a7 | |
|
5b9d6f878f | |
|
3b6c48f1d0 | |
|
bc92656e20 | |
|
981b0fbfed | |
|
949ef16e7f | |
|
418a31ce6d | |
|
99b84b416d | |
|
94ae6e44ef | |
|
8d52ff6690 | |
|
4f50883e8c | |
|
6a64f861ba | |
|
021627caf8 | |
|
99fada4c85 | |
|
be07db9414 | |
|
11b53e7531 | |
|
fb833e6af7 | |
|
f0f11c0646 | |
|
a2b7026f54 | |
|
f72d117d90 | |
|
3fbb21aa1d | |
|
a7758ac69a | |
|
51783612bc | |
|
15c1143b25 | |
|
c2b8f6ba09 |
.eslintignore.gitattributesLICENCE.txtREADME.cn.mdREADME.md
assets
index.htmlpackage-lock.jsonpackage.jsonscripts
set_env.shsrc
App.vue
vite.config.tsapi
assets/icon
arrow_drop_down.svgarrow_drop_up.svgclose.svghelp.svglock.svglock_open.svgplay.svgrepeat.svgtext-data.svgtranslate.svgtrash.svg
composables
broadcastChannelDef.tsbuildMode.tsnotification.tsuseCommandLoopManager.tsuseDataFlowModule.tsuseSequentialUart.tsuseSystemModule.tsuseUartModule.tsuseUpdateModule.ts
i18n.tswebsocket
locales
main.tsrouter
stores
dataViewerStore.tsuseDataFlowStore.tsuseSystemStore.tsuseUartStore.tsuseUpdateStore.tsuseWidgetStore.ts
types
views
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
|
@ -0,0 +1 @@
|
|||
*.sh eol=lf
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2024 kerms
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,85 @@
|
|||
# wireless-esp32-tools 网页用户界面
|
||||
|
||||
本项目是 [wireless-esp32-tools](https://github.com/kerms/wireless-esp32-tools) 项目的网页用户界面。
|
||||
|
||||
`wireless-esp32-tools` 项目为多种ESP芯片提供了兼容CMSIS-DAP的无线调试工具,可将ESP芯片变为一个功能强大的无线调试器。
|
||||
|
||||
## 关于此网页界面
|
||||
|
||||
此Web应用程序提供了一个用户友好的仪表板,用于管理您的 `wireless-esp32-tools` 设备并与之交互。它采用 Vue.js 3、Vite 和 Element Plus 构建,并设计为直接托管在 ESP32 上。
|
||||
|
||||
通过此界面,您可以构建一个自定义的仪表板来监控目标设备并与之交互。
|
||||
|
||||
### 主要功能
|
||||
|
||||
* **Wi-Fi配置**:包括终端模式(STA)和热点模式(AP),支持静态/动态IP地址分配和DNS设置。
|
||||
* **动态组件仪表板**:一个完全灵活的基于网格的面板。
|
||||
* **实时数据可视化**:包括一个用于监控目标设备串行通信的UART数据查看器。
|
||||
* **嵌入式优先设计**:部署为单个.html文件以减少http连接数。
|
||||
* **适用于小屏幕设计**:响应式布局适配移动设备和平板电脑,具有触摸友好的控件和针对小屏幕优化的UI元素。
|
||||
|
||||
## 界面截图
|
||||
|
||||
**组件面板**
|
||||

|
||||
|
||||
**UART数据显示**
|
||||

|
||||
|
||||
## 使用说明
|
||||
|
||||
### 最终用户
|
||||
|
||||
1. 将您的计算机或移动设备连接到运行 `wireless-esp32-tools` 的ESP32所承载的Wi-Fi网络。
|
||||
2. 打开Web浏览器并导航到ESP32设备的IP地址(例如 `http://dap.local` 或 `http://192.168.1.1`(连接AP的情况下))。
|
||||
|
||||
### 开发人员 (针对此Web UI)
|
||||
|
||||
请按照以下步骤设置项目以进行本地开发或构建以进行部署。
|
||||
|
||||
#### 环境准备
|
||||
|
||||
* [Node.js](https://nodejs.org/) (v16 或更高版本)
|
||||
* [npm](https://www.npmjs.com/)
|
||||
|
||||
#### 本地开发
|
||||
|
||||
1. **安装依赖:**
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
2. **运行开发服务器:**
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
这将在本地启动一个服务器,通常地址为 `http://localhost:5173`。
|
||||
|
||||
3. **为移动/远程调试运行:**
|
||||
要从同一网络上的其他设备(如手机)访问开发服务器,请使用:
|
||||
```sh
|
||||
npm run devh
|
||||
```
|
||||
这将把服务器暴露给您的本地网络(例如 `http://192.168.X.X:5173`)。
|
||||
|
||||
#### 构建生产版本 (部署到ESP32)
|
||||
|
||||
1. **构建项目:**
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
此命令将编译和打包应用程序到 `dist/` 目录,生成 `index.html` 和 `ws.sharedworker.js`。
|
||||
|
||||
2. **压缩输出文件:**
|
||||
进入 `dist/` 目录并使用gzip压缩构建产物。
|
||||
```sh
|
||||
cd dist
|
||||
gzip *
|
||||
```
|
||||
这将生成 `index.html.gz` 和 `ws.sharedworker.js.gz`。
|
||||
|
||||
3. **部署到ESP32:**
|
||||
将压缩后的 `.gz` 文件复制到您的 `wireless-esp32-tools` ESP-IDF项目中的相应目录(例如 `project_components/html/`),然后将固件刷入您的设备。
|
||||
|
||||
## 许可证
|
||||
|
||||
根据MIT许可证分发。更多信息请参见 `LICENCE.txt` 文件。
|
86
README.md
86
README.md
|
@ -1,3 +1,87 @@
|
|||
# 允斯无线透传器的内嵌网页版上位机
|
||||
# Web UI for wireless-esp32-tools
|
||||
|
||||
This is the web user interface for the [wireless-esp32-tools](https://github.com/kerms/wireless-esp32-tools) project.
|
||||
|
||||
The `wireless-esp32-tools` project provides CMSIS-DAP compatible wireless debugging tools for various ESP chips, turning them into powerful wireless debug probes.
|
||||
|
||||
## About This Web Interface
|
||||
|
||||
This web application provides a user-friendly dashboard to manage and interact with your `wireless-esp32-tools` device. It is built with Vue.js 3, Vite, and Element Plus, and is designed to be hosted directly on the ESP32.
|
||||
|
||||
From this interface, you can build a customized dashboard to monitor and interact with your target device.
|
||||
|
||||
### Key Features
|
||||
|
||||
* **Wi-Fi Configuration**: Easily configure Wi-Fi settings including station mode (STA) and access point mode (AP), with support for static/dynamic IP addressing and DNS settings.
|
||||
* **Dynamic Widget Dashboard**: A fully flexible grid-based panel.
|
||||
* **Real-time Data Visualization**: Includes a UART data viewer for monitoring serial communication from your target device.
|
||||
* **Embedded-First Design**: Deploy in single .html file to reduce the number of http connection.
|
||||
* **Designed to be usable on small screen**: Responsive layout that adapts to mobile devices and tablets, with touch-friendly controls and optimized UI elements for small displays.
|
||||
|
||||
## Screenshots
|
||||
|
||||
**Widget Panel**
|
||||

|
||||
|
||||
**UART Data Viewer**
|
||||

|
||||
|
||||
## Getting Started
|
||||
|
||||
### For End-Users
|
||||
|
||||
1. Connect your computer or mobile device to the Wi-Fi network hosted by the ESP32 running `wireless-esp32-tools`.
|
||||
2. Open a web browser and navigate to the IP address of the ESP32 device (e.g., `http://dap.local` or `http://192.168.1.1`(when connected to the AP)).
|
||||
|
||||
### For Developers (of this Web UI)
|
||||
|
||||
Follow these steps to set up the project for local development or to build it for deployment.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
* [Node.js](https://nodejs.org/) (v16 or later)
|
||||
* [npm](https://www.npmjs.com/)
|
||||
|
||||
#### Local Development
|
||||
|
||||
1. **Install dependencies:**
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
2. **Run the development server:**
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
This will start a local server, typically at `http://localhost:5173`.
|
||||
|
||||
3. **Run for mobile/remote debugging:**
|
||||
To access the development server from other devices on the same network (like a mobile phone), use:
|
||||
```sh
|
||||
npm run devh
|
||||
```
|
||||
This will expose the server to your local network (e.g., `http://192.168.X.X:5173`).
|
||||
|
||||
#### Building for Production (Deploying to ESP32)
|
||||
|
||||
1. **Build the project:**
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
This command compiles and bundles the application into the `dist/` directory, creating `index.html` and `ws.sharedworker.js`.
|
||||
|
||||
There is some bugs on Firefox or Vue3 when embbeding sharedWorker in single file mode. So at the time, the sharedWorker are build separately.
|
||||
|
||||
2. **Compress the output files:**
|
||||
Navigate into the `dist/` directory and compress the build artifacts using gzip.
|
||||
```sh
|
||||
cd dist
|
||||
gzip *
|
||||
```
|
||||
This will generate `index.html.gz` and `ws.sharedworker.js.gz`.
|
||||
|
||||
3. **Deploy to ESP32:**
|
||||
Copy the compressed `.gz` files to the appropriate directory in your `wireless-esp32-tools` ESP-IDF project (e.g., `project_components/html/`) and flash the firmware to your device.
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT License. See `LICENCE.txt` for more information.
|
||||
|
|
Binary file not shown.
After ![]() (image error) Size: 103 KiB |
Binary file not shown.
After ![]() (image error) Size: 38 KiB |
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<link id="favicon" rel="icon" href="data:,">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
<title>yunsi.studio project</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
|
@ -4,24 +4,29 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": ". ./set_env.sh && vite",
|
||||
"devh": ". ./set_env.sh && vite --host",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": ". ./set_env.sh && vite preview",
|
||||
"build-only": ". ./set_env.sh && vite build",
|
||||
"build:dev": ". ./set_env.sh && vite build --mode development",
|
||||
"dev": "node scripts/gen-env.mjs",
|
||||
"devh": "node scripts/gen-env.mjs --host",
|
||||
"build": "run-p type-check build-only --",
|
||||
"preview": "node scripts/gen-env.mjs preview",
|
||||
"previewh": "node scripts/gen-env.mjs preview --host",
|
||||
"build-only": "node scripts/gen-env.mjs build",
|
||||
"build:dev": "NODE_ENV=development node scripts/gen-env.mjs build",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.ts,.tsx --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"element-plus": "^2.7.3",
|
||||
"ansi_up": "^6.0.2",
|
||||
"element-plus": "^2.8.1",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue": "^3.5.16",
|
||||
"vue-draggable-plus": "^0.4.1",
|
||||
"vue-grid-layout-v3": "^3.1.2",
|
||||
"vue-i18n": "^9.10.2",
|
||||
"vue-router": "^4.3.0"
|
||||
"vue-router": "^4.3.0",
|
||||
"vuetify": "^3.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
|
@ -42,10 +47,11 @@
|
|||
"typescript": "~5.4.0",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.1.6",
|
||||
"vite": "^5.3.3",
|
||||
"vite-plugin-css-injected-by-js": "^3.5.0",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vite-plugin-singlefile": "^2.0.1",
|
||||
"vite-plugin-singlefile": "^2.0.2",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vite-svg-loader": "^5.1.0",
|
||||
"vue-tsc": "^2.0.6"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { execSync, spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const gitTag = execSync('git describe --tags')
|
||||
.toString().trim().split('-').slice(0, 2).join('-');
|
||||
|
||||
const lastCommit = execSync('git log -1 --format=%cd')
|
||||
.toString().trim();
|
||||
|
||||
// run whatever came after “npm run dev …”
|
||||
const argsFromNpm = process.argv.slice(2); // e.g. ["--host"]
|
||||
|
||||
spawn(
|
||||
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
||||
['vite', ...argsFromNpm],
|
||||
{
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
VITE_APP_GIT_TAG: gitTag,
|
||||
VITE_APP_LAST_COMMIT: lastCommit
|
||||
}
|
||||
}
|
||||
).on('exit', code => process.exit(code));
|
10
set_env.sh
10
set_env.sh
|
@ -1,3 +1,7 @@
|
|||
#!/bin/bash
|
||||
export VITE_APP_GIT_TAG=$(git describe --tags | cut -d'-' -f1,2)
|
||||
export VITE_APP_LAST_COMMIT=$(git log -1 --format=%cd)
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VITE_APP_GIT_TAG=$(git describe --tags | cut -d'-' -f1,2)
|
||||
VITE_APP_LAST_COMMIT=$(git log -1 --format=%cd)
|
||||
|
||||
export VITE_APP_GIT_TAG
|
||||
export VITE_APP_LAST_COMMIT
|
100
src/App.vue
100
src/App.vue
|
@ -1,64 +1,94 @@
|
|||
<script setup lang="ts">
|
||||
import {useWsStore} from "@/stores/websocket";
|
||||
import type {IWebsocketService} from "@/composables/websocket/websocketService";
|
||||
import {getWebsocketService} from "@/composables/websocket/websocketService";
|
||||
import {onMounted, onUnmounted} from "vue";
|
||||
import {changeFavicon} from "@/composables/importFavicon";
|
||||
import {logHelloMessage} from "@/composables/logConsoleMsg";
|
||||
import NavBar from "@/views/navigation/NavBar.vue";
|
||||
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 { useWsStore } from '@/stores/websocket'
|
||||
import type { IWebsocketService } from '@/composables/websocket/websocketService'
|
||||
import { getWebsocketService } from '@/composables/websocket/websocketService'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { changeFavicon } from '@/composables/importFavicon'
|
||||
import { logHelloMessage } from '@/composables/logConsoleMsg'
|
||||
import NavBar from '@/views/navigation/NavBar.vue'
|
||||
import type { ControlMsg, ServerMsg } from '@/api'
|
||||
import { ControlEvent, ControlMsgType } from '@/api'
|
||||
import { routeCtrlMsg, routeModuleServerMsg } from '@/router/msgRouter'
|
||||
import { globalNotify } from '@/composables/notification'
|
||||
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'
|
||||
import { translate } from '@/locales'
|
||||
|
||||
const wsState = useWsStore();
|
||||
const wsState = useWsStore()
|
||||
|
||||
const onClientCtrl = (msg: ControlMsg) => {
|
||||
if (isDevMode()) {
|
||||
console.log("App.vue:", msg);
|
||||
console.log('App.vue:', msg)
|
||||
}
|
||||
if (msg.type === ControlMsgType.WS_EVENT) {
|
||||
wsState.$patch({state: msg.data as ControlEvent})
|
||||
routeCtrlMsg(msg);
|
||||
wsState.$patch({ state: msg.data as ControlEvent })
|
||||
routeCtrlMsg(msg)
|
||||
if (msg.data === ControlEvent.CONNECTED) {
|
||||
globalNotify("调试器已连接", "success");
|
||||
globalNotify(translate('common.debuggerConnected'), 'success')
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const onServerMsg = (msg: ServerMsg) => {
|
||||
if (isDevMode()) {
|
||||
console.log("App.vue:", msg);
|
||||
console.log('App.vue:', msg)
|
||||
}
|
||||
routeModuleServerMsg(msg);
|
||||
};
|
||||
routeModuleServerMsg(msg)
|
||||
}
|
||||
|
||||
let websocketService: IWebsocketService;
|
||||
let websocketService: IWebsocketService
|
||||
onMounted(() => {
|
||||
|
||||
logHelloMessage();
|
||||
let host = "";
|
||||
logHelloMessage()
|
||||
let host: string
|
||||
if (isDevMode()) {
|
||||
host = import.meta.env.VITE_DEVICE_HOST_NAME || "dap.local";
|
||||
host = import.meta.env.VITE_DEVICE_HOST_NAME || 'dap.local'
|
||||
} else {
|
||||
host = window.location.host
|
||||
}
|
||||
websocketService = getWebsocketService();
|
||||
websocketService.init(host, onServerMsg, onClientCtrl);
|
||||
changeFavicon();
|
||||
});
|
||||
if (import.meta.env.VITE_DISABLE_CONNECTION !== 'true') {
|
||||
websocketService = getWebsocketService()
|
||||
websocketService.init(host, onServerMsg, onClientCtrl)
|
||||
websocketService.getSocketStatus()
|
||||
}
|
||||
changeFavicon()
|
||||
|
||||
onUnmounted(() => {
|
||||
useSystemModule()
|
||||
useDataFlowModule()
|
||||
|
||||
});
|
||||
if (isOTAEnabled()) {
|
||||
useUpdateModule()
|
||||
}
|
||||
|
||||
if (isTrialMode()) {
|
||||
ElMessageBox.alert(getTrialMsg(), getTrialDate(), {
|
||||
confirmButtonText: translate('common.ok')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-screen">
|
||||
<div class="flex flex-col wt-h-100">
|
||||
<header>
|
||||
<nav-bar/>
|
||||
<nav-bar />
|
||||
</header>
|
||||
<RouterView/>
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.wt-h-100 {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -2,20 +2,49 @@ import {type ApiJsonMsg, sendJsonMsg, WtModuleID} from '@/api'
|
|||
|
||||
export enum WifiCmd {
|
||||
UNKNOWN = 0,
|
||||
WIFI_API_JSON_STA_GET_AP_INFO,
|
||||
WIFI_API_JSON_CONNECT,
|
||||
WIFI_API_JSON_GET_SCAN,
|
||||
WIFI_API_JSON_DISCONNECT,
|
||||
WIFI_API_JSON_AP_GET_INFO,
|
||||
WIFI_API_JSON_STA_GET_AP_INFO = 1,
|
||||
WIFI_API_JSON_CONNECT = 2,
|
||||
WIFI_API_JSON_GET_SCAN = 3,
|
||||
WIFI_API_JSON_DISCONNECT = 4,
|
||||
WIFI_API_JSON_AP_GET_INFO = 5,
|
||||
WIFI_API_JSON_GET_MODE = 6,
|
||||
WIFI_API_JSON_SET_MODE = 7,
|
||||
WIFI_API_JSON_AP_SET_CRED = 8,
|
||||
WIFI_API_JSON_STA_GET_STATIC_INFO = 9,
|
||||
WIFI_API_JSON_STA_SET_STATIC_CONF = 10,
|
||||
}
|
||||
|
||||
interface WifiMsgOut extends ApiJsonMsg {
|
||||
export enum WifiMode {
|
||||
/* permanent */
|
||||
WIFI_AP_AUTO_STA_ON = 0,
|
||||
|
||||
WIFI_AP_STA_OFF = 4, /* 100 */
|
||||
WIFI_AP_OFF_STA_ON = 5, /* 101 */
|
||||
WIFI_AP_ON_STA_OFF = 6, /* 110 */
|
||||
WIFI_AP_STA_ON = 7, /* 111 */
|
||||
|
||||
/* temporary */
|
||||
WIFI_AP_STOP = 8,
|
||||
WIFI_AP_START = 9,
|
||||
WIFI_STA_STOP = 10,
|
||||
WIFI_STA_START = 11,
|
||||
}
|
||||
|
||||
export enum WifiStatus {
|
||||
WIFI_MODE_NULL = 0, /**< null mode */
|
||||
WIFI_MODE_STA, /**< WiFi station mode */
|
||||
WIFI_MODE_AP, /**< WiFi soft-AP mode */
|
||||
WIFI_MODE_APSTA, /**< WiFi station + soft-AP mode */
|
||||
}
|
||||
|
||||
export interface WiFiCredential extends ApiJsonMsg {
|
||||
ssid?: string;
|
||||
password?: string;
|
||||
err?: string;
|
||||
}
|
||||
|
||||
export function wifi_get_scan_list() {
|
||||
const msg : WifiMsgOut = {
|
||||
const msg : WiFiCredential = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_GET_SCAN,
|
||||
}
|
||||
|
@ -23,7 +52,7 @@ export function wifi_get_scan_list() {
|
|||
}
|
||||
|
||||
export function wifi_sta_get_ap_info() {
|
||||
const msg : WifiMsgOut = {
|
||||
const msg : WiFiCredential = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_STA_GET_AP_INFO,
|
||||
}
|
||||
|
@ -31,7 +60,7 @@ export function wifi_sta_get_ap_info() {
|
|||
}
|
||||
|
||||
export function wifi_ap_get_info() {
|
||||
const msg : WifiMsgOut = {
|
||||
const msg : WiFiCredential = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_AP_GET_INFO,
|
||||
}
|
||||
|
@ -39,7 +68,7 @@ export function wifi_ap_get_info() {
|
|||
}
|
||||
|
||||
export function wifi_connect_to(ssid: string, password: string) {
|
||||
const msg: WifiMsgOut = {
|
||||
const msg: WiFiCredential = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_CONNECT,
|
||||
ssid: ssid,
|
||||
|
@ -49,6 +78,19 @@ export function wifi_connect_to(ssid: string, password: string) {
|
|||
}
|
||||
|
||||
export interface WifiInfo extends ApiJsonMsg {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
password: string;
|
||||
gateway: string;
|
||||
ip: string;
|
||||
mac: string;
|
||||
netmask: string;
|
||||
dns_main: string;
|
||||
dns_backup: string;
|
||||
wifiLogo?: string;
|
||||
}
|
||||
|
||||
export interface WifiScanInfo extends ApiJsonMsg {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
gateway: string;
|
||||
|
@ -59,5 +101,86 @@ export interface WifiInfo extends ApiJsonMsg {
|
|||
}
|
||||
|
||||
export interface WifiList extends ApiJsonMsg {
|
||||
scan_list: Array<WifiInfo>;
|
||||
scan_list: Array<WifiScanInfo>;
|
||||
}
|
||||
|
||||
export interface IWifiMode extends ApiJsonMsg {
|
||||
mode?: WifiMode;
|
||||
status?: WifiStatus;
|
||||
ap_on_delay?: number;
|
||||
ap_off_delay?: number;
|
||||
err?: string;
|
||||
}
|
||||
|
||||
export function wifi_set_mode(req_mode: WifiMode, ap_on_delay = -1, ap_off_delay = -1) {
|
||||
let msg: IWifiMode;
|
||||
if (req_mode === WifiMode.WIFI_AP_AUTO_STA_ON && ap_on_delay !== -1 && ap_off_delay !== -1) {
|
||||
msg = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_SET_MODE,
|
||||
mode: req_mode,
|
||||
ap_on_delay: ap_on_delay,
|
||||
ap_off_delay: ap_off_delay,
|
||||
};
|
||||
} else {
|
||||
msg = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_SET_MODE,
|
||||
mode: req_mode,
|
||||
};
|
||||
}
|
||||
sendJsonMsg(msg);
|
||||
}
|
||||
|
||||
export function wifi_get_mode() {
|
||||
const msg: IWifiMode = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_GET_MODE,
|
||||
};
|
||||
|
||||
sendJsonMsg(msg);
|
||||
}
|
||||
|
||||
export function wifi_ap_set_credential(ssid: string, password: string) {
|
||||
const msg : WiFiCredential = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_AP_SET_CRED,
|
||||
ssid: ssid,
|
||||
password: password,
|
||||
}
|
||||
sendJsonMsg(msg);
|
||||
}
|
||||
|
||||
export interface IWifiStaStaticInfo {
|
||||
static_ip_en: number;
|
||||
static_dns_en: number;
|
||||
ip: string;
|
||||
gateway: string;
|
||||
netmask: string;
|
||||
dns_main: string;
|
||||
dns_backup: string;
|
||||
}
|
||||
export function wifi_sta_get_static_info() {
|
||||
const msg: ApiJsonMsg = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_STA_GET_STATIC_INFO,
|
||||
}
|
||||
sendJsonMsg(msg);
|
||||
}
|
||||
|
||||
export function wifi_sta_set_static_conf(static_info: IWifiStaStaticInfo) {
|
||||
const msg: IWifiStaStaticInfo & ApiJsonMsg = {
|
||||
module: WtModuleID.WIFI,
|
||||
cmd: WifiCmd.WIFI_API_JSON_STA_SET_STATIC_CONF,
|
||||
static_dns_en: static_info.static_dns_en,
|
||||
static_ip_en: static_info.static_ip_en,
|
||||
ip: static_info.ip,
|
||||
gateway: static_info.gateway,
|
||||
netmask: static_info.netmask,
|
||||
dns_main: static_info.dns_main,
|
||||
dns_backup: static_info.dns_backup,
|
||||
}
|
||||
sendJsonMsg(msg);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
After (image error) Size: 222 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M478-240q21 0 35.5-14.5T528-290q0-21-14.5-35.5T478-340q-21 0-35.5 14.5T428-290q0 21 14.5 35.5T478-240Zm-36-154h74q0-33 7.5-52t42.5-52q26-26 41-49.5t15-56.5q0-56-41-86t-97-30q-57 0-92.5 30T342-618l66 26q5-18 22.5-39t53.5-21q32 0 48 17.5t16 38.5q0 20-12 37.5T506-526q-44 39-54 59t-10 73Zm38 314q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
After (image error) Size: 668 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-80q-33 0-56.5-23.5T160-160v-400q0-33 23.5-56.5T240-640h40v-80q0-83 58.5-141.5T480-920q83 0 141.5 58.5T680-720v80h40q33 0 56.5 23.5T800-560v400q0 33-23.5 56.5T720-80H240Zm0-80h480v-400H240v400Zm240-120q33 0 56.5-23.5T560-360q0-33-23.5-56.5T480-440q-33 0-56.5 23.5T400-360q0 33 23.5 56.5T480-280ZM360-640h240v-80q0-50-35-85t-85-35q-50 0-85 35t-35 85v80ZM240-160v-400 400Z"/></svg>
|
After (image error) Size: 499 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-160h480v-400H240v400Zm240-120q33 0 56.5-23.5T560-360q0-33-23.5-56.5T480-440q-33 0-56.5 23.5T400-360q0 33 23.5 56.5T480-280ZM240-160v-400 400Zm0 80q-33 0-56.5-23.5T160-160v-400q0-33 23.5-56.5T240-640h280v-80q0-83 58.5-141.5T720-920q83 0 141.5 58.5T920-720h-80q0-50-35-85t-85-35q-50 0-85 35t-35 85v80h120q33 0 56.5 23.5T800-560v400q0 33-23.5 56.5T720-80H240Z"/></svg>
|
After (image error) Size: 486 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z"/></svg>
|
After (image error) Size: 190 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M280-80 120-240l160-160 56 58-62 62h406v-160h80v240H274l62 62-56 58Zm-80-440v-240h486l-62-62 56-58 160 160-160 160-56-58 62-62H280v160h-80Z"/></svg>
|
After (image error) Size: 264 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-320h320v-80H240v80Zm400 0h80v-80h-80v80ZM240-480h80v-80h-80v80Zm160 0h320v-80H400v80ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"/></svg>
|
After (image error) Size: 370 B |
|
@ -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 |
|
@ -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 |
|
@ -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");
|
|
@ -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站或者加入允斯群,新项目和更新都会在第一时间在这里发布. 使用愉快^_^";
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
/**
|
||||
* @file Composable for managing a task scheduler that executes callbacks at specified intervals.
|
||||
* This scheduler avoids time drift and handles race conditions from stale timers.
|
||||
*/
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { isDevMode } from './buildMode'
|
||||
|
||||
interface ScheduledTask {
|
||||
id: string
|
||||
intervalMS: number
|
||||
nextExecutionTime: number
|
||||
executeCallback: () => Promise<void> | void
|
||||
oneTime: boolean
|
||||
version: number
|
||||
}
|
||||
|
||||
// --- Module-level state (Singleton pattern) ---
|
||||
|
||||
/** A sorted array of tasks to be executed. The task at index 0 is always the next one. */
|
||||
const scheduledTasks = ref<ScheduledTask[]>([])
|
||||
/** Tracks the latest version for a given task ID to prevent stale timers from running. */
|
||||
const taskVersions = new Map<string, number>()
|
||||
/** The ID of the currently active `setTimeout` instance. */
|
||||
let currentTimerId: number | null = null
|
||||
/** A reactive flag indicating if the scheduler has any pending tasks. */
|
||||
export const isSchedulerRunning = ref(false)
|
||||
/** A lock to prevent concurrent task executions */
|
||||
let isTaskExecuting = false
|
||||
|
||||
export function useCommandLoopManager() {
|
||||
// --- Private Scheduler Core ---
|
||||
|
||||
/**
|
||||
* Executes the task at the front of the queue and reschedules it if it's recurring.
|
||||
*/
|
||||
const executeNextTask = async () => {
|
||||
// If a task is already running, wait before trying to execute the next one.
|
||||
if (isTaskExecuting) {
|
||||
if (isDevMode()) console.log('[Scheduler] Delaying task execution: another task is already running.')
|
||||
// This is a simple back-off strategy.
|
||||
currentTimerId = window.setTimeout(executeNextTask, 50)
|
||||
return
|
||||
}
|
||||
|
||||
if (scheduledTasks.value.length === 0) {
|
||||
isSchedulerRunning.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const taskToExecute = scheduledTasks.value.shift()!
|
||||
|
||||
// Stale check
|
||||
const latestVersion = taskVersions.get(taskToExecute.id)
|
||||
if (taskToExecute.version !== latestVersion) {
|
||||
if (isDevMode()) console.log(`[Scheduler] Discarding stale task '${taskToExecute.id}' v${taskToExecute.version}.`)
|
||||
scheduleNextExecution() // The queue has changed, so recalculate.
|
||||
return
|
||||
}
|
||||
|
||||
// Acquire the lock and execute the callback.
|
||||
try {
|
||||
isTaskExecuting = true
|
||||
if (isDevMode()) console.log(`[Scheduler] Executing task '${taskToExecute.id}' v${taskToExecute.version}`)
|
||||
await taskToExecute.executeCallback()
|
||||
} catch (error) {
|
||||
console.error(`[Scheduler] Error in task '${taskToExecute.id}':`, error)
|
||||
} finally {
|
||||
isTaskExecuting = false // ALWAYS release the lock
|
||||
}
|
||||
|
||||
// If it's a recurring task, reschedule it.
|
||||
if (!taskToExecute.oneTime) {
|
||||
taskToExecute.nextExecutionTime = Date.now() + taskToExecute.intervalMS
|
||||
// Re-insert the task and re-sort the queue.
|
||||
scheduledTasks.value.push(taskToExecute)
|
||||
scheduledTasks.value.sort((a, b) => a.nextExecutionTime - b.nextExecutionTime)
|
||||
} else {
|
||||
taskVersions.delete(taskToExecute.id)
|
||||
}
|
||||
|
||||
// Set the timer for the next task in the queue.
|
||||
scheduleNextExecution()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a single master timer for the next task in the queue.
|
||||
*/
|
||||
const scheduleNextExecution = () => {
|
||||
if (currentTimerId !== null) {
|
||||
window.clearTimeout(currentTimerId)
|
||||
currentTimerId = null
|
||||
}
|
||||
|
||||
if (scheduledTasks.value.length === 0) {
|
||||
isSchedulerRunning.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const nextTask = scheduledTasks.value[0]
|
||||
const timeout = Math.max(0, nextTask.nextExecutionTime - Date.now())
|
||||
|
||||
currentTimerId = window.setTimeout(executeNextTask, timeout)
|
||||
isSchedulerRunning.value = true
|
||||
|
||||
if (isDevMode()) {
|
||||
console.log(`[Scheduler] Next task '${nextTask.id}' v${nextTask.version} scheduled in ${timeout}ms.`)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
/**
|
||||
* Registers a new task, updates an existing one, or removes a task.
|
||||
* This is the single entry point for all scheduling changes.
|
||||
* @param id A unique identifier for the task.
|
||||
* @param intervalMS The interval in milliseconds. Pass 0 or an invalid value to unregister.
|
||||
* @param executeCallback The function to execute.
|
||||
* @param oneTime If true, the task runs once and is not rescheduled.
|
||||
*/
|
||||
const registerLoop = (
|
||||
id: string,
|
||||
intervalMS: number | string,
|
||||
executeCallback: () => Promise<void> | void,
|
||||
oneTime = false,
|
||||
) => {
|
||||
scheduledTasks.value = scheduledTasks.value.filter((task) => task.id !== id)
|
||||
|
||||
const interval = typeof intervalMS === 'string' ? parseInt(intervalMS, 10) : intervalMS
|
||||
if (!interval || isNaN(interval) || interval <= 0) {
|
||||
if (isDevMode()) console.log(`[Scheduler] Unregistered task '${id}'.`)
|
||||
taskVersions.delete(id)
|
||||
scheduleNextExecution()
|
||||
return
|
||||
}
|
||||
|
||||
const newVersion = (taskVersions.get(id) || 0) + 1
|
||||
taskVersions.set(id, newVersion)
|
||||
|
||||
const newTask: ScheduledTask = {
|
||||
id,
|
||||
intervalMS: interval,
|
||||
nextExecutionTime: Date.now() + interval,
|
||||
executeCallback,
|
||||
oneTime,
|
||||
version: newVersion,
|
||||
}
|
||||
|
||||
scheduledTasks.value.push(newTask)
|
||||
scheduledTasks.value.sort((a, b) => a.nextExecutionTime - b.nextExecutionTime)
|
||||
|
||||
if (isDevMode()) {
|
||||
console.log(`[Scheduler] Registered task '${id}' v${newVersion} with interval ${interval}ms.`)
|
||||
}
|
||||
|
||||
scheduleNextExecution()
|
||||
}
|
||||
|
||||
/**
|
||||
* A convenience helper to explicitly remove a task from the scheduler.
|
||||
* @param id The identifier of the task to remove.
|
||||
*/
|
||||
const unregisterLoop = (id: string) => {
|
||||
registerLoop(id, -1, () => {})
|
||||
}
|
||||
|
||||
// --- Lifecycle Hook ---
|
||||
|
||||
onUnmounted(() => {
|
||||
if (currentTimerId !== null) window.clearTimeout(currentTimerId)
|
||||
scheduledTasks.value = []
|
||||
taskVersions.clear()
|
||||
isSchedulerRunning.value = false
|
||||
})
|
||||
|
||||
return { registerLoop, unregisterLoop, isRunning: isSchedulerRunning }
|
||||
}
|
|
@ -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: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { useDataViewerStore } from '@/stores/dataViewerStore'
|
||||
import { watch } from 'vue'
|
||||
import type { IDataBuf } from '@/stores/dataViewerStore'
|
||||
import { isDevMode } from '@/composables/buildMode'
|
||||
|
||||
function decodeUtf8(u8Arr: Uint8Array) {
|
||||
try {
|
||||
const decoder = new TextDecoder()
|
||||
const decodedText = decoder.decode(u8Arr) // Attempt to decode
|
||||
return decodedText.replace(/\uFFFD/g, '') // Remove all <20> characters
|
||||
} catch (error) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function useSequentialUart() {
|
||||
const dataViewerStore = useDataViewerStore() as any
|
||||
|
||||
async function sendCommand(command: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
let responseBuffer = ''
|
||||
let responseTimeout: number | null = null
|
||||
const RESPONSE_WAIT_TIME = 10 // Wait 500ms after last response before resolving
|
||||
|
||||
// Index to track which messages we've already processed
|
||||
let lastProcessedIndex = dataViewerStore.dataFiltered.length - 1
|
||||
|
||||
const stopWatch = watch(
|
||||
() => dataViewerStore.dataFiltered,
|
||||
(newBuf: IDataBuf[]) => {
|
||||
if (isDevMode()) {
|
||||
console.log('watch data', newBuf)
|
||||
}
|
||||
|
||||
// Process only new messages
|
||||
for (let i = lastProcessedIndex + 1; i < newBuf.length; i++) {
|
||||
const message = newBuf[i]
|
||||
if (message.isRX) {
|
||||
// Add to response buffer
|
||||
responseBuffer += decodeUtf8(message.data)
|
||||
|
||||
// Reset timeout to wait for more responses
|
||||
if (responseTimeout) {
|
||||
clearTimeout(responseTimeout)
|
||||
}
|
||||
|
||||
responseTimeout = window.setTimeout(() => {
|
||||
stopWatch()
|
||||
resolve(responseBuffer)
|
||||
}, RESPONSE_WAIT_TIME)
|
||||
}
|
||||
}
|
||||
|
||||
// Update last processed index
|
||||
lastProcessedIndex = newBuf.length - 1
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
dataViewerStore.addString(command, false, true)
|
||||
|
||||
// Set a maximum timeout in case no response is received
|
||||
const maxTimeout = window.setTimeout(() => {
|
||||
stopWatch()
|
||||
resolve(responseBuffer)
|
||||
}, 1000) // 5 seconds maximum wait time
|
||||
})
|
||||
}
|
||||
|
||||
async function sendCommands(commands: string[]): Promise<string[]> {
|
||||
const responses: string[] = []
|
||||
for (const command of commands) {
|
||||
const response = await sendCommand(command)
|
||||
responses.push(response)
|
||||
}
|
||||
if (isDevMode()) {
|
||||
console.log('sendCommands', responses)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
return { sendCommands }
|
||||
}
|
|
@ -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: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import type { IUartMsgConfig, IUartMsgNum } from '@/api/apiUart'
|
||||
import type { IUartMsgBaud } from '@/api/apiUart'
|
||||
import { uart_get_baud, uart_get_config, WtUartCmd } from '@/api/apiUart'
|
||||
import { useUartStore } from '@/stores/useUartStore'
|
||||
import { registerModule } from '@/router/msgRouter'
|
||||
import * as api from '@/api'
|
||||
import { isDevMode } from './buildMode'
|
||||
import { useDataViewerStore } from '@/stores/dataViewerStore'
|
||||
import type { ApiBinaryMsg } from '@/api/binDataDef'
|
||||
import { ControlEvent } from '@/api'
|
||||
import * as df from '@/api/apiDataFlow'
|
||||
import { uart_get_default_num } from '@/api/apiUart'
|
||||
|
||||
export function useUartModule() {
|
||||
const dataViewerStore = useDataViewerStore()
|
||||
const uartStore = useUartStore()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
dataViewerStore.setUartBaud(uartMsg.baud)
|
||||
}
|
||||
break
|
||||
}
|
||||
case WtUartCmd.GET_CONFIG:
|
||||
case WtUartCmd.SET_CONFIG: {
|
||||
const uartMsg = msg as IUartMsgConfig
|
||||
dataViewerStore.uartConfig.data_bits = uartMsg.data_bits
|
||||
dataViewerStore.uartConfig.stop_bits = uartMsg.stop_bits
|
||||
dataViewerStore.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)
|
||||
}
|
||||
|
||||
dataViewerStore.addSegment(new Uint8Array(msg.payload), true)
|
||||
}
|
||||
|
||||
const onClientCtrl = (msg: api.ControlMsg) => {
|
||||
if (msg.type !== api.ControlMsgType.WS_EVENT) {
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.data === ControlEvent.DISCONNECTED) {
|
||||
dataViewerStore.acceptIncomingData = false
|
||||
} else if (msg.data === ControlEvent.CONNECTED) {
|
||||
updateUartData()
|
||||
dataViewerStore.acceptIncomingData = true
|
||||
}
|
||||
}
|
||||
|
||||
registerModule(api.WtModuleID.UART, {
|
||||
ctrlCallback: onClientCtrl,
|
||||
serverJsonMsgCallback: onUartJsonMsg,
|
||||
serverBinMsgCallback: onUartBinaryMsg
|
||||
})
|
||||
}
|
|
@ -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: () => {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
47
src/i18n.ts
47
src/i18n.ts
|
@ -1,19 +1,52 @@
|
|||
import { createI18n } from 'vue-i18n';
|
||||
import {createI18n} from 'vue-i18n';
|
||||
import zh from '@/locales/zh'
|
||||
import en from '@/locales/en'
|
||||
import fr from '@/locales/fr'
|
||||
|
||||
// const locale = localStorage.getItem('lang') || 'zh';
|
||||
export const locale = 'zh';
|
||||
const userLanguage = navigator.language || 'en';
|
||||
|
||||
// Get the language code (e.g., 'en' from 'en-US')
|
||||
export const locale = userLanguage.split('-')[0];
|
||||
const messages = {
|
||||
zh,
|
||||
en,
|
||||
fr,
|
||||
} as const;
|
||||
|
||||
type Locale = keyof typeof messages;
|
||||
|
||||
export const availableLanguages = Object.keys(messages);
|
||||
|
||||
// export const locale = 'zh';
|
||||
console.log(userLanguage, locale, availableLanguages)
|
||||
|
||||
const i18n = createI18n({
|
||||
globalInjection: true,
|
||||
legacy: false,
|
||||
locale: locale,
|
||||
fallbackLocale: 'zh',
|
||||
messages: {
|
||||
zh,
|
||||
// en,
|
||||
}
|
||||
messages: messages
|
||||
});
|
||||
|
||||
export function getFlagFromLang(lang: string) {
|
||||
if (lang === 'zh') {
|
||||
return '🇨🇳';
|
||||
} else if (lang === 'en') {
|
||||
return '🇺🇸';
|
||||
} else if (lang === 'fr') {
|
||||
return '🇫🇷';
|
||||
}
|
||||
return '🏳️';
|
||||
}
|
||||
|
||||
export function setLang(lang: string): void {
|
||||
if (availableLanguages.includes(lang)) {
|
||||
i18n.global.locale.value = lang as Locale;
|
||||
}
|
||||
}
|
||||
|
||||
export function getLang() {
|
||||
return i18n.global.locale;
|
||||
}
|
||||
|
||||
export default i18n;
|
||||
|
|
|
@ -1,3 +1,235 @@
|
|||
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",
|
||||
widget: "Widget",
|
||||
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",
|
||||
},
|
||||
|
||||
widget: {
|
||||
editGrid: 'Edit Grid',
|
||||
editCell: 'Edit Cell',
|
||||
loopWidget: 'Loop Widget',
|
||||
loopWidgetDesc: 'Container for command sequences.',
|
||||
addGrid: 'Add to Grid',
|
||||
dataViewer: 'Data Viewer',
|
||||
dataViewerDesc: 'Displays raw text data from UART.',
|
||||
exportSettings: 'Export Settings',
|
||||
importSettings: 'Import Settings',
|
||||
resetToDefault: 'Reset to Default',
|
||||
gridItemName: 'Widget name',
|
||||
dropHere: 'Drop here',
|
||||
run: 'Run',
|
||||
loop: 'Loop',
|
||||
delay: 'Delay',
|
||||
addCommand: 'Add Command',
|
||||
loopInterval: 'Loop Interval',
|
||||
uartViewOnce: 'UART View Widget can only be added once.'
|
||||
},
|
||||
|
||||
common: {
|
||||
debuggerConnected: 'Debugger connected',
|
||||
ok: 'OK'
|
||||
},
|
||||
|
||||
navbar: {
|
||||
navigationSidebar: 'Navigation Sidebar',
|
||||
getSomeFries: "Let's go to the dock and get some fries",
|
||||
},
|
||||
};
|
|
@ -0,0 +1,236 @@
|
|||
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",
|
||||
widget: "Widget",
|
||||
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é",
|
||||
},
|
||||
|
||||
widget: {
|
||||
editGrid: 'Modifier la grille',
|
||||
editCell: 'Modifier les cellules',
|
||||
loopWidget: 'Widget Boucle',
|
||||
loopWidgetDesc: 'Conteneur pour les séquences de commandes.',
|
||||
addGrid: 'Ajouter à la grille',
|
||||
dataViewer: 'Visualiseur de données',
|
||||
dataViewerDesc: "Affiche les données texte brutes de l'UART.",
|
||||
exportSettings: 'Exporter les paramètres',
|
||||
importSettings: 'Importer les paramètres',
|
||||
resetToDefault: 'Réinitialiser par défaut',
|
||||
gridItemName: "Nom",
|
||||
dropHere: 'Déposer ici',
|
||||
run: 'Exécuter',
|
||||
loop: 'Boucle',
|
||||
delay: 'Délai',
|
||||
addCommand: 'Ajouter une commande',
|
||||
loopInterval: 'Intervalle de répétition',
|
||||
uartViewOnce: "Le widget de vue UART ne peut être ajouté qu'une seule fois."
|
||||
},
|
||||
|
||||
common: {
|
||||
debuggerConnected: 'Débogueur connecté',
|
||||
ok: 'OK'
|
||||
},
|
||||
|
||||
navbar: {
|
||||
navigationSidebar: 'Barre latérale de navigation',
|
||||
getSomeFries: 'Allons au quai prendre des frites',
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,204 @@ export default {
|
|||
home: "主页",
|
||||
wifi: "Wi-Fi",
|
||||
about: "关于",
|
||||
uart: "UART透传",
|
||||
uart: "UART",
|
||||
widget: "组件",
|
||||
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: "调试器未连接",
|
||||
},
|
||||
|
||||
widget: {
|
||||
editGrid: '编辑网格',
|
||||
editCell: '编辑单元',
|
||||
loopWidget: '循环小部件',
|
||||
loopWidgetDesc: '用于命令序列的容器。',
|
||||
addGrid: '添加到网格',
|
||||
dataViewer: '数据显示器',
|
||||
dataViewerDesc: '显示来自UART的原始文本数据。',
|
||||
exportSettings: '导出设置',
|
||||
importSettings: '导入设置',
|
||||
resetToDefault: '重置',
|
||||
gridItemName: '组件名称',
|
||||
dropHere: '在此处放置',
|
||||
run: '运行',
|
||||
loop: '循环',
|
||||
delay: '延迟',
|
||||
addCommand: '添加命令',
|
||||
loopInterval: '循环间隔',
|
||||
uartViewOnce: 'UART视图组件只能添加一次。'
|
||||
},
|
||||
|
||||
common: {
|
||||
debuggerConnected: '调试器已连接',
|
||||
ok: '好的'
|
||||
},
|
||||
|
||||
navbar: {
|
||||
navigationSidebar: '导航侧栏',
|
||||
getSomeFries: '走,去码头整点薯条',
|
||||
},
|
||||
}
|
|
@ -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')
|
||||
|
|
|
@ -1,54 +1,101 @@
|
|||
import {createRouter, createWebHistory} from 'vue-router'
|
||||
import Home from '@/views/Home.vue'
|
||||
import {createRouter, createWebHashHistory, 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 WidgetPannel from '@/views/WidgetPannel.vue'
|
||||
import {translate} from "@/locales";
|
||||
import {isOTAEnabled} from "@/composables/buildMode";
|
||||
import {reactive, watch} from "vue";
|
||||
import {getLang} from "@/i18n";
|
||||
|
||||
const languageState = reactive({
|
||||
lang: getLang()
|
||||
});
|
||||
|
||||
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.lang, () => {
|
||||
// Recompute all route meta titles
|
||||
updateMetaTitles();
|
||||
updateDocumentTitle(router.currentRoute.value);
|
||||
}, {deep: true});
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
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: '/widget:ext(.*)',
|
||||
meta: { titleKey: 'page.widget' },
|
||||
component: WidgetPannel,
|
||||
}, {
|
||||
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;
|
|
@ -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
|
@ -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,
|
||||
}
|
||||
});
|
|
@ -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,
|
||||
}
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useUartStore = defineStore('uart', () => {
|
||||
const uartNum = ref(1);
|
||||
|
||||
return { uartNum }
|
||||
})
|
|
@ -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,
|
||||
}
|
||||
})
|
|
@ -0,0 +1,424 @@
|
|||
import { computed, markRaw, ref, toRaw, watch } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import type { WidgetItem } from '@/types/grid'
|
||||
import { globalNotify } from '@/composables/notification'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import UartAtCommand from '@/views/widgets/uartAtCommand.vue'
|
||||
import WidgetLoop from '@/views/widgets/widgetLoop.vue'
|
||||
import textDataViewer from '@/views/text-data-viewer/textDataViewer.vue'
|
||||
|
||||
const componentMap: { [key: string]: any } = {
|
||||
WidgetLoop,
|
||||
textDataViewer,
|
||||
UartAtCommand
|
||||
}
|
||||
|
||||
const getComponentName = (component: any): string | null => {
|
||||
const rawComponent = toRaw(component)
|
||||
for (const name in componentMap) {
|
||||
if (componentMap[name] === rawComponent) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const getDefaultLayout = (): WidgetItem[] => [
|
||||
{
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 10,
|
||||
h: 10,
|
||||
i: 0,
|
||||
name: 'Widget A',
|
||||
static: false,
|
||||
widget: markRaw(WidgetLoop),
|
||||
widgetProps: [
|
||||
{
|
||||
id: 1,
|
||||
componentType: markRaw(UartAtCommand),
|
||||
props: { label: 'Device ID', command: 'AT+ID?', response: 'ID:xxxx' }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
componentType: markRaw(UartAtCommand),
|
||||
props: { label: 'Version', command: 'AT+VER?', response: 'V1.0.0' }
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
componentType: markRaw(UartAtCommand),
|
||||
props: { label: 'Reset', command: 'AT+RESET', response: 'OK' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
x: 10,
|
||||
y: 0,
|
||||
w: 10,
|
||||
h: 10,
|
||||
i: 1,
|
||||
name: 'Widget B',
|
||||
static: false,
|
||||
widget: markRaw(WidgetLoop),
|
||||
widgetProps: [
|
||||
{
|
||||
id: 1,
|
||||
componentType: markRaw(UartAtCommand),
|
||||
props: {
|
||||
label: 'Scan WiFi',
|
||||
command: 'AT+WSCANasdfasdfasdf',
|
||||
response: 'SCAN OKasd fsdaf asdf asdf asdf asdf '
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
componentType: markRaw(UartAtCommand),
|
||||
props: {
|
||||
label: 'Connect WiFi',
|
||||
command: 'AT+WCONN=ssid,pwd',
|
||||
response: 'CONN OK'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
x: 0,
|
||||
y: 10,
|
||||
w: 10,
|
||||
h: 10,
|
||||
i: 2,
|
||||
name: 'Widget C',
|
||||
static: false,
|
||||
widget: markRaw(WidgetLoop),
|
||||
widgetProps: [
|
||||
{
|
||||
id: 1,
|
||||
componentType: markRaw(UartAtCommand),
|
||||
props: {
|
||||
label: 'Ping Test',
|
||||
command: 'AT+PING=google.com',
|
||||
response: 'PING OK'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
x: 10,
|
||||
y: 10,
|
||||
w: 10,
|
||||
h: 10,
|
||||
i: 3,
|
||||
name: 'Widget D',
|
||||
static: false,
|
||||
widget: markRaw(textDataViewer),
|
||||
widgetProps: []
|
||||
}
|
||||
]
|
||||
|
||||
const throttle = (fn: Function, wait: number) => {
|
||||
let inThrottle: boolean, lastFn: number, lastTime: number
|
||||
return function (this: any, ...args: any[]) {
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
fn.apply(context, args)
|
||||
lastTime = Date.now()
|
||||
inThrottle = true
|
||||
} else {
|
||||
clearTimeout(lastFn)
|
||||
lastFn = window.setTimeout(() => {
|
||||
if (Date.now() - lastTime >= wait) {
|
||||
fn.apply(context, args)
|
||||
lastTime = Date.now()
|
||||
}
|
||||
}, Math.max(wait - (Date.now() - lastTime), 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const useWidgetStore = defineStore('widget', () => {
|
||||
const layout = ref<WidgetItem[]>(getDefaultLayout())
|
||||
|
||||
const editCell = ref(false)
|
||||
const editGrid = ref(false)
|
||||
const showOptions = ref(true)
|
||||
|
||||
const isUartViewAdded = computed(() =>
|
||||
layout.value.some((item) => toRaw(item.widget) === textDataViewer)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => editGrid.value,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
editCell.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => editCell.value,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
editGrid.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const getNextId = (): number => {
|
||||
const numericIds = layout.value.map((item) => Number(item.i)).filter((id) => !isNaN(id))
|
||||
if (numericIds.length === 0) {
|
||||
return 0
|
||||
}
|
||||
return Math.max(...numericIds) + 1
|
||||
}
|
||||
|
||||
const addLoopWidget = () => {
|
||||
const nextId = getNextId()
|
||||
let y = 0
|
||||
if (layout.value.length > 0) {
|
||||
y = Math.max(...layout.value.map((item) => item.y + item.h))
|
||||
}
|
||||
const newWidget: WidgetItem = {
|
||||
x: 0,
|
||||
y: y,
|
||||
w: 10,
|
||||
h: 5,
|
||||
i: nextId,
|
||||
name: `New Loop Widget`,
|
||||
static: false,
|
||||
widget: markRaw(WidgetLoop),
|
||||
widgetProps: []
|
||||
}
|
||||
layout.value.push(newWidget)
|
||||
}
|
||||
|
||||
const addUartViewWidget = () => {
|
||||
if (isUartViewAdded.value) {
|
||||
globalNotify('UART View Widget can only be added once.', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
const nextId = getNextId()
|
||||
let y = 0
|
||||
if (layout.value.length > 0) {
|
||||
y = Math.max(...layout.value.map((item) => item.y + item.h))
|
||||
}
|
||||
|
||||
const newWidget: WidgetItem = {
|
||||
x: 0,
|
||||
y: y,
|
||||
w: 10,
|
||||
h: 10,
|
||||
i: nextId,
|
||||
name: 'UART Data Viewer',
|
||||
static: false,
|
||||
widget: markRaw(textDataViewer),
|
||||
widgetProps: []
|
||||
}
|
||||
layout.value.push(newWidget)
|
||||
}
|
||||
|
||||
const deleteWidget = (index: number) => {
|
||||
layout.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const resetToDefault = () => {
|
||||
ElMessageBox.confirm(
|
||||
'This will reset your layout to the default settings. Are you sure?',
|
||||
'Warning',
|
||||
{
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
layout.value = getDefaultLayout()
|
||||
globalNotify('Layout reset to default.', 'success')
|
||||
})
|
||||
.catch(() => {
|
||||
globalNotify('Layout reset cancelled.', 'info')
|
||||
})
|
||||
}
|
||||
|
||||
const exportSettings = () => {
|
||||
try {
|
||||
const layoutToSave = toRaw(layout.value).map((item) => {
|
||||
const rawItem = toRaw(item)
|
||||
return {
|
||||
...rawItem,
|
||||
widget: getComponentName(rawItem.widget),
|
||||
widgetProps: rawItem.widgetProps?.map((prop: any) => {
|
||||
const rawProp = toRaw(prop)
|
||||
return {
|
||||
...rawProp,
|
||||
componentType: getComponentName(rawProp.componentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const dataStr = JSON.stringify(layoutToSave, null, 2)
|
||||
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
|
||||
|
||||
const exportFileDefaultName = 'at-command-settings.json'
|
||||
|
||||
const linkElement = document.createElement('a')
|
||||
linkElement.setAttribute('href', dataUri)
|
||||
linkElement.setAttribute('download', exportFileDefaultName)
|
||||
linkElement.click()
|
||||
globalNotify('Settings exported successfully.', 'success')
|
||||
} catch (error) {
|
||||
console.error('Failed to export settings:', error)
|
||||
globalNotify('Failed to export settings.', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const importSettings = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = '.json'
|
||||
input.onchange = (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
try {
|
||||
const fileContent = event.target?.result as string
|
||||
const parsedLayout = JSON.parse(fileContent)
|
||||
|
||||
// Basic validation
|
||||
if (!Array.isArray(parsedLayout)) {
|
||||
throw new Error('Invalid format: expected an array of widgets.')
|
||||
}
|
||||
|
||||
const newLayout = parsedLayout
|
||||
.map((item: any) => {
|
||||
if (item.widget && componentMap[item.widget]) {
|
||||
item.widget = markRaw(componentMap[item.widget])
|
||||
} else {
|
||||
console.warn(`Unknown widget type "${item.widget}" during import. Skipping item.`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (item.widgetProps) {
|
||||
item.widgetProps.forEach((prop: any) => {
|
||||
if (prop.componentType && componentMap[prop.componentType]) {
|
||||
prop.componentType = markRaw(componentMap[prop.componentType])
|
||||
} else if (prop.componentType) {
|
||||
console.warn(
|
||||
`Unknown componentType "${prop.componentType}" for widget "${item.name}". It will be ignored.`
|
||||
)
|
||||
prop.componentType = null
|
||||
}
|
||||
})
|
||||
item.widgetProps = item.widgetProps.filter((prop: any) => prop.componentType)
|
||||
}
|
||||
return item
|
||||
})
|
||||
.filter(Boolean) // remove null items
|
||||
|
||||
layout.value = newLayout
|
||||
globalNotify('Settings imported successfully.', 'success')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to import settings:', error)
|
||||
globalNotify(`Failed to import settings: ${error.message}`, 'error')
|
||||
}
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const saveLayoutToLocalStorage = () => {
|
||||
try {
|
||||
const layoutToSave = toRaw(layout.value).map((item) => {
|
||||
const rawItem = toRaw(item)
|
||||
return {
|
||||
...rawItem,
|
||||
widget: getComponentName(rawItem.widget),
|
||||
widgetProps: rawItem.widgetProps?.map((prop: any) => {
|
||||
const rawProp = toRaw(prop)
|
||||
return {
|
||||
...rawProp,
|
||||
componentType: getComponentName(rawProp.componentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
localStorage.setItem('at-command-layout', JSON.stringify(layoutToSave))
|
||||
} catch (error) {
|
||||
console.error('Failed to save layout to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadLayoutFromLocalStorage = () => {
|
||||
const savedLayoutJSON = localStorage.getItem('at-command-layout')
|
||||
if (!savedLayoutJSON) return
|
||||
|
||||
try {
|
||||
const parsedLayout = JSON.parse(savedLayoutJSON)
|
||||
|
||||
if (!Array.isArray(parsedLayout)) {
|
||||
throw new Error('Invalid format in localStorage: expected an array of widgets.')
|
||||
}
|
||||
|
||||
const newLayout = parsedLayout
|
||||
.map((item: any) => {
|
||||
if (item.widget && componentMap[item.widget]) {
|
||||
item.widget = markRaw(componentMap[item.widget])
|
||||
} else {
|
||||
console.warn(`Unknown widget type "${item.widget}" from localStorage. Skipping item.`)
|
||||
return null
|
||||
}
|
||||
|
||||
if (item.widgetProps) {
|
||||
item.widgetProps.forEach((prop: any) => {
|
||||
if (prop.componentType && componentMap[prop.componentType]) {
|
||||
prop.componentType = markRaw(componentMap[prop.componentType])
|
||||
} else if (prop.componentType) {
|
||||
console.warn(
|
||||
`Unknown componentType "${prop.componentType}" for widget "${item.name}" from localStorage. It will be ignored.`
|
||||
)
|
||||
prop.componentType = null
|
||||
}
|
||||
})
|
||||
item.widgetProps = item.widgetProps.filter((prop: any) => prop.componentType)
|
||||
}
|
||||
return item
|
||||
})
|
||||
.filter(Boolean) // remove null items
|
||||
|
||||
layout.value = newLayout
|
||||
globalNotify('Settings restored from last session.', 'success')
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load layout from localStorage:', error)
|
||||
globalNotify(`Failed to load settings from localStorage: ${error.message}`, 'error')
|
||||
localStorage.removeItem('at-command-layout')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
layout,
|
||||
throttle(() => saveLayoutToLocalStorage(), 1000),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
loadLayoutFromLocalStorage()
|
||||
|
||||
return {
|
||||
layout,
|
||||
editGrid,
|
||||
editCell,
|
||||
showOptions,
|
||||
isUartViewAdded,
|
||||
addLoopWidget,
|
||||
addUartViewWidget,
|
||||
deleteWidget,
|
||||
resetToDefault,
|
||||
exportSettings,
|
||||
importSettings,
|
||||
getNextId
|
||||
}
|
||||
})
|
|
@ -0,0 +1,26 @@
|
|||
import type { Component } from 'vue'
|
||||
|
||||
export interface UartCommandData {
|
||||
command: string
|
||||
label: string
|
||||
response: string
|
||||
}
|
||||
|
||||
export interface DraggableComponent<T = Record<string, any>> {
|
||||
id: number
|
||||
componentType: Component | string
|
||||
props: T
|
||||
}
|
||||
|
||||
export interface WidgetItem {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
i: number
|
||||
name: string
|
||||
static: boolean
|
||||
widget: Component | string
|
||||
widgetIconName?: string
|
||||
widgetProps: DraggableComponent[]
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,9 +1,480 @@
|
|||
<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 :showDataConfig="store.winLeft.show"></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'
|
||||
import { useUartModule } from '@/composables/useUartModule'
|
||||
|
||||
|
||||
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(() => {
|
||||
useUartModule()
|
||||
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,345 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-screen">
|
||||
<div v-show="widgetStore.showOptions" class="flex h-32 overflow-y-auto m-2">
|
||||
<div class="flex gap-4">
|
||||
<div class="flex flex-col gap-2 border-r pr-4">
|
||||
<el-checkbox v-model="widgetStore.editGrid" border class="w-full">{{
|
||||
translate('widget.editGrid')
|
||||
}}</el-checkbox>
|
||||
<el-checkbox v-model="widgetStore.editCell" border class="w-full">{{
|
||||
translate('widget.editCell')
|
||||
}}</el-checkbox>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="w-40 h-24 bg-gray-200 border-2 rounded-md p-2 flex flex-col justify-center items-center text-center cursor-move"
|
||||
draggable="true"
|
||||
@dragstart="dragStart('loop', $event)"
|
||||
@drag="drag"
|
||||
@dragend="dragEnd"
|
||||
>
|
||||
<span class="font-bold">{{ translate('widget.loopWidget') }}</span>
|
||||
<p class="text-xs">{{ translate('widget.loopWidgetDesc') }}</p>
|
||||
</div>
|
||||
<el-button @click="widgetStore.addLoopWidget" size="small" class="w-full mt-1">{{
|
||||
translate('widget.addGrid')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<div
|
||||
class="w-40 h-24 bg-gray-200 border-2 rounded-md p-2 flex flex-col justify-center items-center text-center"
|
||||
:class="{
|
||||
'cursor-move': !widgetStore.isUartViewAdded,
|
||||
'cursor-not-allowed opacity-50': widgetStore.isUartViewAdded
|
||||
}"
|
||||
:draggable="!widgetStore.isUartViewAdded"
|
||||
@dragstart="dragStart('uart', $event)"
|
||||
@drag="drag"
|
||||
@dragend="dragEnd"
|
||||
>
|
||||
<span class="font-bold">{{ translate('widget.dataViewer') }}</span>
|
||||
<p class="text-xs">{{ translate('widget.dataViewerDesc') }}</p>
|
||||
</div>
|
||||
<el-button
|
||||
@click="widgetStore.addUartViewWidget"
|
||||
size="small"
|
||||
class="w-full mt-1"
|
||||
:disabled="widgetStore.isUartViewAdded"
|
||||
>{{ translate('widget.addGrid') }}</el-button
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-start gap-2 border-l pl-4">
|
||||
<div>
|
||||
<el-button @click="widgetStore.exportSettings" size="small">{{
|
||||
translate('widget.exportSettings')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="widgetStore.importSettings" size="small">{{
|
||||
translate('widget.importSettings')
|
||||
}}</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="danger" @click="widgetStore.resetToDefault" size="small">{{
|
||||
translate('widget.resetToDefault')
|
||||
}}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="gridWrapper" class="flex-1 flex flex-col w-full min-h-0">
|
||||
<div class="flex-1 bg-gray-100 overflow-y-auto min-h-0">
|
||||
<GridLayout
|
||||
ref="gridlayout"
|
||||
v-model:layout="widgetStore.layout"
|
||||
:col-num="20"
|
||||
:row-height="30"
|
||||
:is-draggable="widgetStore.editGrid"
|
||||
:is-resizable="widgetStore.editGrid"
|
||||
:auto-size="false"
|
||||
:compact-type="null"
|
||||
:vertical-compact="false"
|
||||
>
|
||||
<grid-item
|
||||
v-for="(item, index) in widgetStore.layout"
|
||||
:key="item.i"
|
||||
v-bind="item"
|
||||
class="rounded-md flex flex-col text-xs p-1"
|
||||
:class="{
|
||||
'bg-blue-300': String(item.i) !== dropId,
|
||||
'bg-gray-400 opacity-50': String(item.i) === dropId
|
||||
}"
|
||||
>
|
||||
<template v-if="String(item.i) !== dropId">
|
||||
<div class="flex justify-between pb-0.5">
|
||||
<InlineSvg :name="getWidgetIconName(item)" width="20"></InlineSvg>
|
||||
<el-button
|
||||
v-show="widgetStore.editGrid"
|
||||
type="danger"
|
||||
size="small"
|
||||
class="self-center px-1"
|
||||
@click="widgetStore.deleteWidget(index)"
|
||||
>
|
||||
<InlineSvg name="close" width="20"></InlineSvg>
|
||||
</el-button>
|
||||
<div :id="`tp-widget-before-${item.i}`"></div>
|
||||
<div v-if="widgetStore.editGrid" class="w-full">
|
||||
<el-input
|
||||
v-model="item.name"
|
||||
size="small"
|
||||
:placeholder="translate('widget.gridItemName')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="item.name"
|
||||
class="truncate font-bold self-center text-center text-sm w-full"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<p v-else></p>
|
||||
<!-- empty space to align the check tag -->
|
||||
<el-check-tag
|
||||
v-if="widgetStore.editGrid"
|
||||
:checked="item.static"
|
||||
type="danger"
|
||||
class="self-center px-1"
|
||||
@click="item.static = !item.static"
|
||||
>
|
||||
<InlineSvg v-show="item.static" name="lock" width="20"></InlineSvg>
|
||||
<InlineSvg v-show="!item.static" name="lock_open" width="20"></InlineSvg>
|
||||
</el-check-tag>
|
||||
<div v-show="!widgetStore.editGrid" :id="`tp-widget-${item.i}`"></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-y-auto flex flex-col flex-grow">
|
||||
<component
|
||||
:is="item.widget"
|
||||
v-model="widgetStore.layout[index]"
|
||||
:editCell="widgetStore.editCell"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex justify-center items-center h-full">
|
||||
<p class="font-bold text-white">{{ translate('widget.dropHere') }}</p>
|
||||
</div>
|
||||
</grid-item>
|
||||
</GridLayout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<teleport to="#nav-right-slot">
|
||||
<ElCheckTag
|
||||
:checked="widgetStore.showOptions"
|
||||
type="primary"
|
||||
@click="widgetStore.showOptions = !widgetStore.showOptions"
|
||||
>{{ translate('widget.editGrid') }}</ElCheckTag
|
||||
>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { markRaw, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { GridLayout, GridItem } from 'vue-grid-layout-v3'
|
||||
import { ElInput, ElCheckbox, ElCheckTag, ElButton } from 'element-plus'
|
||||
import UartAtCommand from './widgets/uartAtCommand.vue'
|
||||
import WidgetLoop from './widgets/widgetLoop.vue'
|
||||
import textDataViewer from '@/views/text-data-viewer/textDataViewer.vue'
|
||||
import { useUartModule } from '@/composables/useUartModule'
|
||||
import { globalNotify } from '@/composables/notification'
|
||||
import { useWidgetStore } from '@/stores/useWidgetStore'
|
||||
import type { WidgetItem } from '@/types/grid'
|
||||
import { translate } from '@/locales'
|
||||
|
||||
const widgetStore = useWidgetStore()
|
||||
|
||||
const gridlayout = ref<InstanceType<typeof GridLayout> | null>(null)
|
||||
const gridWrapper = ref<HTMLElement | null>(null)
|
||||
|
||||
const getWidgetIconName = (item: WidgetItem) => {
|
||||
if (typeof item.widget === 'object' && item.widget !== null && 'widgetIconName' in item.widget) {
|
||||
return (item.widget as any).widgetIconName
|
||||
}
|
||||
return 'default-icon' // or some other default
|
||||
}
|
||||
|
||||
const throttle = (fn: Function, wait: number) => {
|
||||
let inThrottle: boolean, lastFn: number, lastTime: number
|
||||
return function (this: any, ...args: any[]) {
|
||||
const context = this
|
||||
if (!inThrottle) {
|
||||
fn.apply(context, args)
|
||||
lastTime = Date.now()
|
||||
inThrottle = true
|
||||
} else {
|
||||
clearTimeout(lastFn)
|
||||
lastFn = window.setTimeout(() => {
|
||||
if (Date.now() - lastTime >= wait) {
|
||||
fn.apply(context, args)
|
||||
lastTime = Date.now()
|
||||
}
|
||||
}, Math.max(wait - (Date.now() - lastTime), 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mouseAt = { x: -1, y: -1 }
|
||||
function syncMousePosition(event: MouseEvent) {
|
||||
mouseAt.x = event.clientX
|
||||
mouseAt.y = event.clientY
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
useUartModule()
|
||||
document.addEventListener('dragover', syncMousePosition)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('dragover', syncMousePosition)
|
||||
})
|
||||
|
||||
const dropId = 'drop-placeholder'
|
||||
const dragging = ref<{ type: string; w: number; h: number } | null>(null)
|
||||
|
||||
function dragStart(itemType: string, event: DragEvent) {
|
||||
if (itemType === 'loop') {
|
||||
dragging.value = { type: 'loop', w: 10, h: 5 }
|
||||
} else if (itemType === 'uart') {
|
||||
dragging.value = { type: 'uart', w: 10, h: 10 }
|
||||
}
|
||||
event.dataTransfer?.setData('text/plain', itemType)
|
||||
}
|
||||
|
||||
const drag = throttle(() => {
|
||||
if (!dragging.value || !gridWrapper.value || !gridlayout.value) return
|
||||
|
||||
const parentRect = gridWrapper.value.getBoundingClientRect()
|
||||
const mouseInGrid =
|
||||
mouseAt.x > parentRect.left &&
|
||||
mouseAt.x < parentRect.right &&
|
||||
mouseAt.y > parentRect.top &&
|
||||
mouseAt.y < parentRect.bottom
|
||||
|
||||
const placeholderIndex = widgetStore.layout.findIndex((item) => String(item.i) === dropId)
|
||||
|
||||
if (mouseInGrid) {
|
||||
const gLayout = gridlayout.value as any
|
||||
const colNum = 20
|
||||
const rowHeight = 30
|
||||
const margin = [10, 10]
|
||||
const scrollContainer = gLayout.$el.parentElement
|
||||
if (!scrollContainer) return
|
||||
const colWidth =
|
||||
(gLayout.$el.offsetWidth -
|
||||
margin[0] * (colNum - 1) -
|
||||
(gLayout.containerPadding?.[0] || margin[0]) * 2) /
|
||||
colNum
|
||||
|
||||
const xInGrid = mouseAt.x - parentRect.left + scrollContainer.scrollLeft
|
||||
const yInGrid = mouseAt.y - parentRect.top + scrollContainer.scrollTop
|
||||
|
||||
let gridX = Math.round(xInGrid / (colWidth + margin[0]) - dragging.value.w / 2)
|
||||
let gridY = Math.round(yInGrid / (rowHeight + margin[1]) - dragging.value.h / 2)
|
||||
|
||||
gridX = Math.max(0, Math.min(gridX, colNum - dragging.value.w))
|
||||
gridY = Math.max(0, gridY)
|
||||
|
||||
if (placeholderIndex === -1) {
|
||||
widgetStore.layout.push({
|
||||
x: gridX,
|
||||
y: gridY,
|
||||
w: dragging.value.w,
|
||||
h: dragging.value.h,
|
||||
i: dropId
|
||||
} as any)
|
||||
} else {
|
||||
widgetStore.layout[placeholderIndex].x = gridX
|
||||
widgetStore.layout[placeholderIndex].y = gridY
|
||||
}
|
||||
} else {
|
||||
if (placeholderIndex !== -1) {
|
||||
widgetStore.layout.splice(placeholderIndex, 1)
|
||||
}
|
||||
}
|
||||
}, 50)
|
||||
|
||||
function dragEnd() {
|
||||
if (!dragging.value) return
|
||||
|
||||
const parentRect = gridWrapper.value?.getBoundingClientRect()
|
||||
|
||||
const placeholderIndex = widgetStore.layout.findIndex((item) => String(item.i) === dropId)
|
||||
|
||||
if (placeholderIndex === -1) {
|
||||
dragging.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const placeholder = widgetStore.layout[placeholderIndex]
|
||||
const mouseInGrid =
|
||||
parentRect &&
|
||||
mouseAt.x > parentRect.left &&
|
||||
mouseAt.x < parentRect.right &&
|
||||
mouseAt.y > parentRect.top &&
|
||||
mouseAt.y < parentRect.bottom
|
||||
|
||||
if (mouseInGrid) {
|
||||
if (dragging.value.type === 'uart') {
|
||||
const uartViewExists = widgetStore.layout.some(
|
||||
(item) => item.widget === textDataViewer && String(item.i) !== dropId
|
||||
)
|
||||
if (uartViewExists) {
|
||||
globalNotify(translate('widget.uartViewOnce'), 'warning')
|
||||
widgetStore.layout.splice(placeholderIndex, 1) // remove placeholder
|
||||
dragging.value = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const newWidget = {
|
||||
x: placeholder.x,
|
||||
y: placeholder.y,
|
||||
w: dragging.value.w,
|
||||
h: dragging.value.h,
|
||||
i: widgetStore.getNextId(),
|
||||
name: dragging.value.type === 'loop' ? 'New Loop Widget' : 'UART Data Viewer',
|
||||
static: false,
|
||||
widget: markRaw(dragging.value.type === 'loop' ? WidgetLoop : textDataViewer),
|
||||
widgetProps: []
|
||||
}
|
||||
|
||||
widgetStore.layout.splice(placeholderIndex, 1, newWidget as any)
|
||||
} else {
|
||||
// Not in grid, just remove placeholder
|
||||
widgetStore.layout.splice(placeholderIndex, 1)
|
||||
}
|
||||
|
||||
dragging.value = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-check-tag) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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,40 +46,71 @@
|
|||
</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 {{ translate('wifi.mode') }}</h5>
|
||||
<div class="flex shrink-0">
|
||||
<el-tooltip effect="light">
|
||||
<template #content>
|
||||
<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>
|
||||
</div>
|
||||
<el-select v-model="wifiMode" :disabled="wsStore.state != ControlEvent.CONNECTED">
|
||||
<el-option
|
||||
v-for="item in wifiModeOptions"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
:label="item.label"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button type="primary" @click="wifiChangeMode" :loading="wifiMode_loading">{{ translate('wifi.save') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
|
||||
<el-descriptions
|
||||
title="Wi-Fi终端信息"
|
||||
:column="1"
|
||||
border
|
||||
class="description-style"
|
||||
>
|
||||
<el-descriptions-item label="asd">
|
||||
<template #label >
|
||||
<div>
|
||||
信号强度
|
||||
</div>
|
||||
<template #title>
|
||||
Wi-Fi {{ translate('wifi.stationInfo') }}
|
||||
<el-tag v-if="!isConnected" type="danger">{{ translate('wifi.disconnected') }}</el-tag>
|
||||
</template>
|
||||
<template #default >
|
||||
{{ wifiStaApInfo.rssi }}
|
||||
<template #extra>
|
||||
<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>
|
||||
<el-descriptions-item span="1">
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
SSID
|
||||
{{ translate('wifi.signalStrength') }}
|
||||
</div>
|
||||
</template>
|
||||
{{ wifiStaApInfo.ssid }}
|
||||
<template #default>
|
||||
<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)
|
||||
</div>
|
||||
</template>
|
||||
<p>{{ wifiStaApInfo.ssid }}</p>
|
||||
</el-descriptions-item>
|
||||
<!-- <el-descriptions-item span="6" >-->
|
||||
<!-- <template #label>-->
|
||||
|
@ -87,67 +118,164 @@
|
|||
<!-- 密码-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <password-viewer password="asdasdasd"></password-viewer>-->
|
||||
<!-- <password-viewer :password="wifiStaApInfo.password"></password-viewer>-->
|
||||
<!-- </el-descriptions-item>-->
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
IP
|
||||
</div>
|
||||
</template>
|
||||
{{ wifiStaApInfo.ip }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
MAC
|
||||
</div>
|
||||
</template>
|
||||
{{ wifiStaApInfo.mac }}
|
||||
<p>{{ wifiStaApInfo.mac }}</p>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<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>
|
||||
{{ wifiStaApInfo.gateway }}
|
||||
<p>{{ wifiStaApInfo.gateway }}</p>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
{{ translate('wifi.netmask') }}
|
||||
</div>
|
||||
</template>
|
||||
<p>{{ wifiStaApInfo.netmask }}</p>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
{{ translate('wifi.primaryDNS') }}
|
||||
</div>
|
||||
</template>
|
||||
<p>{{ wifiStaApInfo.dns_main }}</p>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
{{ translate('wifi.backupDNS') }}
|
||||
</div>
|
||||
</template>
|
||||
<p>{{ wifiStaApInfo.dns_backup }}</p>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
掩码
|
||||
{{ translate('wifi.IPmode') }}
|
||||
</div>
|
||||
</template>
|
||||
{{ wifiStaApInfo.netmask }}
|
||||
<el-select v-model="wifiStaticInfo.static_ip_en" :disabled="!isConnected">
|
||||
<el-option
|
||||
v-for="item in staIPModeOptions"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
:label="item.label"
|
||||
/>
|
||||
</el-select>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
|
||||
<template #label>
|
||||
<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>
|
||||
</el-descriptions-item>
|
||||
<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>
|
||||
</el-descriptions-item>
|
||||
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
{{ translate('wifi.DNSmode') }}
|
||||
</div>
|
||||
</template>
|
||||
<el-select v-model="wifiStaticInfo.static_dns_en" :disabled="!isConnected">
|
||||
<el-option
|
||||
v-for="item in staDNSModeOptions"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
:label="item.label"
|
||||
/>
|
||||
</el-select>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
|
||||
<template #label>
|
||||
<div>
|
||||
{{ translate('wifi.primaryDNS') }}
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="wifiStaticInfo.dns_main"></el-input>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
|
||||
<template #label>
|
||||
<div>
|
||||
{{ 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">{{ translate('wifi.save') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-descriptions
|
||||
title="Wi-Fi热点信息"
|
||||
: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="!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>
|
||||
SSID
|
||||
Wi-Fi(SSID)
|
||||
</div>
|
||||
</template>
|
||||
{{ wifiApInfo.ssid }}
|
||||
<div class="flex">
|
||||
<el-input v-model="wifiApInfo.ssid"></el-input>
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item span="6">
|
||||
<template #label>
|
||||
<div>
|
||||
{{ translate('wifi.password') }}
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model="wifiApInfo.password"></el-input>
|
||||
</el-descriptions-item>
|
||||
<!-- <el-descriptions-item span="6">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div>-->
|
||||
<!-- 密码-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <password-viewer password="asdasdasd"></password-viewer>-->
|
||||
<!-- </el-descriptions-item>-->
|
||||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
|
@ -169,7 +297,7 @@
|
|||
<el-descriptions-item span="4">
|
||||
<template #label>
|
||||
<div>
|
||||
网关
|
||||
{{ translate('wifi.gateway') }}
|
||||
</div>
|
||||
</template>
|
||||
{{ wifiApInfo.gateway }}
|
||||
|
@ -178,67 +306,137 @@
|
|||
<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">{{ translate('wifi.save') }}</el-button>
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {computed, type ComputedRef, onMounted, onUnmounted, reactive, ref} from "vue";
|
||||
import {
|
||||
wifi_sta_get_ap_info,
|
||||
type IWifiMode,
|
||||
wifi_ap_get_info,
|
||||
wifi_ap_set_credential,
|
||||
wifi_connect_to,
|
||||
wifi_get_mode,
|
||||
wifi_get_scan_list,
|
||||
wifi_set_mode,
|
||||
wifi_sta_get_ap_info,
|
||||
WifiCmd,
|
||||
type WifiInfo,
|
||||
type WifiList,
|
||||
wifi_ap_get_info, wifi_connect_to
|
||||
WifiMode,
|
||||
type WifiScanInfo,
|
||||
type WiFiCredential,
|
||||
WifiStatus, wifi_sta_get_static_info,
|
||||
type IWifiStaStaticInfo, wifi_sta_set_static_conf,
|
||||
} from "@/api/apiWifi";
|
||||
import type {FormInstance} from "element-plus";
|
||||
|
||||
import InlineSvg from "@/components/InlineSvg.vue";
|
||||
import type {ApiJsonMsg, ControlMsg, ServerMsg} from "@/api";
|
||||
import type {ApiJsonMsg, ControlMsg} from "@/api";
|
||||
import {ControlEvent, ControlMsgType, WtModuleID} from "@/api";
|
||||
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: "",
|
||||
})
|
||||
|
||||
let wifiSta_On = ref(false);
|
||||
const wifiMode_loading = ref(false)
|
||||
|
||||
let wifiAp_On = ref(false);
|
||||
|
||||
let wifiMode = ref(-1);
|
||||
|
||||
let wifiModeOptions = computed( () => [
|
||||
{
|
||||
label: translate('wifi.APauto_STA'),
|
||||
key: WifiMode.WIFI_AP_AUTO_STA_ON,
|
||||
}, {
|
||||
label: translate('wifi.APonly'),
|
||||
key: WifiMode.WIFI_AP_ON_STA_OFF,
|
||||
}, {
|
||||
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: "未连接",
|
||||
gateway: "-",
|
||||
ip: "-",
|
||||
mac: "-",
|
||||
dns_main: "-",
|
||||
dns_backup: "-",
|
||||
rssi: 0,
|
||||
netmask: "未连接",
|
||||
ssid: "未连接",
|
||||
netmask: "-",
|
||||
ssid: "-",
|
||||
password: "",
|
||||
}
|
||||
|
||||
const staIPModeOptions = [
|
||||
{
|
||||
label: translate('wifi.autoIP'),
|
||||
key: 0,
|
||||
}, {
|
||||
label: translate('wifi.staticIP'),
|
||||
key: 1,
|
||||
},
|
||||
]
|
||||
|
||||
const staDNSModeOptions = [
|
||||
{
|
||||
label: translate('wifi.autoDNS'),
|
||||
key: 0,
|
||||
}, {
|
||||
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>({
|
||||
dns_backup: "0.0.0.0",
|
||||
dns_main: "0.0.0.0",
|
||||
gateway: "0.0.0.0",
|
||||
ip: "0.0.0.0",
|
||||
netmask: "0.0.0.0",
|
||||
static_dns_en: 0,
|
||||
static_ip_en: 0,
|
||||
});
|
||||
|
||||
let scanning = ref(false);
|
||||
let scan_cb: any;
|
||||
let connectBtnClicked = 0;
|
||||
let options: Array<WifiInfo> = [];
|
||||
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) => {
|
||||
|
@ -262,7 +460,8 @@ 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;
|
||||
}
|
||||
|
@ -285,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:
|
||||
|
@ -295,6 +494,60 @@ const onClientMsg = (msg: ApiJsonMsg) => {
|
|||
Object.assign(wifiApInfo, info);
|
||||
break;
|
||||
}
|
||||
case WifiCmd.WIFI_API_JSON_SET_MODE:
|
||||
wifi_get_mode();
|
||||
/* falls through */
|
||||
case WifiCmd.WIFI_API_JSON_GET_MODE: {
|
||||
const modeInfo = msg as IWifiMode;
|
||||
wifiMode_loading.value = false;
|
||||
if (modeInfo.err !== undefined) {
|
||||
globalNotifyRightSide(translate('wifi.setFailed'), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (modeInfo.status !== undefined) {
|
||||
wifiAp_On.value = modeInfo.status === WifiStatus.WIFI_MODE_AP || modeInfo.status === WifiStatus.WIFI_MODE_APSTA;
|
||||
wifiSta_On.value = modeInfo.status === WifiStatus.WIFI_MODE_STA || modeInfo.status === WifiStatus.WIFI_MODE_APSTA;
|
||||
}
|
||||
if (modeInfo.mode !== undefined) {
|
||||
if (modeInfo.mode < WifiMode.WIFI_AP_STOP) {
|
||||
wifiMode.value = modeInfo.mode;
|
||||
} else if (modeInfo.mode === WifiMode.WIFI_AP_START) {
|
||||
wifiAp_On.value = true;
|
||||
} else if (modeInfo.mode === WifiMode.WIFI_AP_STOP) {
|
||||
wifiAp_On.value = false;
|
||||
} else if (modeInfo.mode === WifiMode.WIFI_STA_START) {
|
||||
wifiSta_On.value = true;
|
||||
} else if (modeInfo.mode === WifiMode.WIFI_STA_STOP) {
|
||||
wifiSta_On.value = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WifiCmd.WIFI_API_JSON_AP_SET_CRED: {
|
||||
const wifiCred = msg as WiFiCredential;
|
||||
if (wifiCred.err !== undefined) {
|
||||
globalNotifyRightSide(wifiCred.err, "error");
|
||||
} else {
|
||||
globalNotifyRightSide(translate('wifi.setSuccess'), "success");
|
||||
}
|
||||
wifiMode_loading.value = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case WifiCmd.WIFI_API_JSON_STA_GET_STATIC_INFO: {
|
||||
const staticInfo = msg as IWifiStaStaticInfo & ApiJsonMsg;
|
||||
Object.assign(wifiStaticInfo, staticInfo);
|
||||
break;
|
||||
}
|
||||
case WifiCmd.WIFI_API_JSON_STA_SET_STATIC_CONF:
|
||||
wifiMode_loading.value = false;
|
||||
break;
|
||||
default:
|
||||
if (isDevMode()) {
|
||||
console.log(msg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -311,12 +564,14 @@ const onClientCtrl = (msg: ControlMsg) => {
|
|||
if (msg.data === ControlEvent.CONNECTED) {
|
||||
wifi_sta_get_ap_info();
|
||||
wifi_ap_get_info();
|
||||
wifi_get_mode();
|
||||
wifi_sta_get_static_info();
|
||||
}
|
||||
};
|
||||
|
||||
function onScanClick() {
|
||||
if (wsStore.state !== ControlEvent.CONNECTED) {
|
||||
globalNotify("调试器未连接", 'error');
|
||||
if (!isConnected.value) {
|
||||
globalNotify(translate('wifi.debuggerNotConnected'), 'error');
|
||||
return;
|
||||
}
|
||||
scanning.value = true;
|
||||
|
@ -324,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 !== "") {
|
||||
|
@ -334,6 +589,49 @@ function onConnectClick() {
|
|||
}
|
||||
}
|
||||
|
||||
function beforeWifiModeChange(ap_sta: "AP" | "STA" = "AP") {
|
||||
if (ap_sta === "AP") {
|
||||
wifiMode_loading.value = true;
|
||||
wifi_set_mode(wifiAp_On.value ? WifiMode.WIFI_AP_STOP : WifiMode.WIFI_AP_START);
|
||||
} else {
|
||||
wifiMode_loading.value = true;
|
||||
wifi_set_mode(wifiSta_On.value ? WifiMode.WIFI_STA_STOP : WifiMode.WIFI_STA_START);
|
||||
}
|
||||
wifi_sta_get_ap_info();
|
||||
return false;
|
||||
}
|
||||
|
||||
function wifiChangeMode() {
|
||||
wifiMode_loading.value = true;
|
||||
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(translate('wifi.enterAPName'), "error");
|
||||
return;
|
||||
}
|
||||
wifiMode_loading.value = true;
|
||||
wifi_ap_set_credential(wifiApInfo.ssid, wifiApInfo.password);
|
||||
}
|
||||
|
||||
function wifiStaSetStaticInfo() {
|
||||
wifiMode_loading.value = true;
|
||||
wifi_sta_set_static_conf(wifiStaticInfo);
|
||||
wifi_sta_get_ap_info();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
registerModule(WtModuleID.WIFI, {
|
||||
ctrlCallback: onClientCtrl,
|
||||
|
@ -342,17 +640,18 @@ onMounted(() => {
|
|||
});
|
||||
wifi_sta_get_ap_info();
|
||||
wifi_ap_get_info();
|
||||
wifi_get_mode();
|
||||
wifi_sta_get_static_info();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
unregisterModule(WtModuleID.WIFI);
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="postcss">
|
||||
.description-style :deep(.el-descriptions__label) {
|
||||
@apply w-32
|
||||
}
|
||||
|
|
|
@ -1,26 +1,22 @@
|
|||
<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>
|
||||
<title>{{ translate('navbar.navigationSidebar') }}</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="走,去码头整点薯条">
|
||||
<router-link to="/" class="text-3xl px-4 font-bold leading-none hidden items-center sm:flex" :title="translate('navbar.getSomeFries')">
|
||||
<InlineSvg name="favicon" class="h-5 lg:h-8"></InlineSvg>
|
||||
</router-link>
|
||||
|
||||
<div id="nav-right-slot"></div>
|
||||
|
||||
<!-- <a class="text-3xl px-4 font-bold leading-none" href="/">-->
|
||||
<!-- <InlineSvg name="home" class="h-10"></InlineSvg>-->
|
||||
<!-- </a>-->
|
||||
<!-- <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 +31,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 +77,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 +90,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 +118,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,22 +163,40 @@ 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.feedback"),
|
||||
href: "/feedback",
|
||||
},
|
||||
]));
|
||||
|
||||
const sideBarItems: ComputedRef<Item[]> = computed(() => {
|
||||
const items: Item[] = [
|
||||
{
|
||||
name: translate("page.wifi"),
|
||||
href: "/wifi",
|
||||
}, {
|
||||
name: translate("page.uart"),
|
||||
href: "/uart",
|
||||
}, {
|
||||
name: translate("page.widget"),
|
||||
href: "/widget",
|
||||
}, {
|
||||
name: translate("page.about"),
|
||||
href: "/about",
|
||||
|
@ -160,7 +204,18 @@ const menuItems: Item[] = ([
|
|||
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 +243,9 @@ const menuItems: Item[] = ([
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
.el-select :deep(.el-select__wrapper) {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
|
@ -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>-->
|
||||
|
||||
<!-- <!– <div>专有协议–>-->
|
||||
<!-- <!– <el-button size="small">输入格式</el-button>–>-->
|
||||
<!-- <!– <el-button size="small">输出格式</el-button>–>-->
|
||||
<!-- <!– </div>–>-->
|
||||
<!-- </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>
|
|
@ -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>
|
|
@ -0,0 +1,457 @@
|
|||
<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="showDataConfig" class="h-[40vh] overflow-auto">
|
||||
<text-data-config></text-data-config>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-link v-show="showDataConfig" 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";
|
||||
import type { DraggableComponent } from '@/types/grid'
|
||||
|
||||
defineOptions({
|
||||
name: 'TextDataViewer',
|
||||
widgetIconName: 'text-data'
|
||||
})
|
||||
|
||||
withDefaults(defineProps<{
|
||||
editCell?: boolean
|
||||
showDataConfig?: boolean
|
||||
}>(), {
|
||||
editCell: false,
|
||||
showDataConfig: true,
|
||||
})
|
||||
|
||||
const modelValue = defineModel<DraggableComponent>({ required: false })
|
||||
|
||||
|
||||
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>
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<div ref="rootEl" class="p-1 bg-white text-xs border-b-2">
|
||||
<div v-if="props.editCell">
|
||||
<div>
|
||||
<div class="mb-1">
|
||||
<el-input v-model="model.label" placeholder="Label" size="small">
|
||||
<template #prepend>Label</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
<el-input v-model="model.command" placeholder="UART Command" size="small">
|
||||
<template #prepend>Command</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="width < 250" class="">
|
||||
<div class="truncate flex justify-between">
|
||||
<el-text class="font-bold">{{ model.label }}</el-text>
|
||||
<p class="ml-1 font-mono text-gray-500 text-[10px]">{{ model.command }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<strong>R:</strong>
|
||||
<el-text class="ml-1 font-mono">{{ model.response }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex text-gray-700">
|
||||
<div class="truncate min-h-8 w-32">
|
||||
<p class="font-bold">{{ model.label }}</p>
|
||||
<p class="font-mono text-gray-500 text-[10px]">{{ model.command }}</p>
|
||||
</div>
|
||||
<div class="pt-0.5">
|
||||
<p class="font-mono text-sm">{{ model.response }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineModel, type PropType, ref } from 'vue'
|
||||
import { ElInput, ElText } from 'element-plus'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
interface UartCommandData {
|
||||
command: string
|
||||
label: string
|
||||
response: string
|
||||
group?: string
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
editCell: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const model = defineModel<UartCommandData>({ required: true })
|
||||
|
||||
const rootEl = ref(null)
|
||||
const { width } = useElementSize(rootEl)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-input :deep(.el-input-group__prepend) {
|
||||
padding: 0 5px;
|
||||
font-size: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,233 @@
|
|||
<script setup lang="ts">
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import type { DraggableComponent, WidgetItem } from '../../types/grid'
|
||||
import { ElButton, ElIcon } from 'element-plus'
|
||||
import { markRaw, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { debouncedWatch } from '@vueuse/core'
|
||||
import type { UartCommandData } from '@/types/grid'
|
||||
import UartAtCommand from '@/views/widgets/uartAtCommand.vue'
|
||||
import { useWsStore } from '@/stores/websocket'
|
||||
import { globalNotify } from '@/composables/notification'
|
||||
import { isDevMode } from '@/composables/buildMode'
|
||||
import { useSequentialUart } from '@/composables/useSequentialUart'
|
||||
import { useCommandLoopManager } from '@/composables/useCommandLoopManager'
|
||||
import { translate } from '@/locales'
|
||||
|
||||
/* ---------------- props & model ----------------------------------- */
|
||||
const modelValue = defineModel<WidgetItem>({ required: true })
|
||||
const editCell = defineModel<boolean>('editCell', { required: true })
|
||||
|
||||
defineOptions({
|
||||
name: 'WidgetLoop',
|
||||
widgetIconName: 'repeat'
|
||||
})
|
||||
|
||||
const intervalMS = ref(0)
|
||||
const active = ref(false)
|
||||
|
||||
// Get command loop manager
|
||||
const commandLoopManager = useCommandLoopManager()
|
||||
const { sendCommands } = useSequentialUart()
|
||||
const widgetId = ref(`widget-${modelValue.value.i}`)
|
||||
|
||||
// This watcher handles enabling or disabling the recurring task.
|
||||
watch(active, (isActive) => {
|
||||
if (isActive) {
|
||||
// When activated, register the loop if the interval is valid.
|
||||
const intervalValue = typeof intervalMS.value === 'string' ? parseInt(intervalMS.value, 10) : intervalMS.value;
|
||||
if (intervalValue > 0) {
|
||||
commandLoopManager.registerLoop(widgetId.value, intervalValue, runCommands);
|
||||
}
|
||||
} else {
|
||||
// When deactivated, always unregister the loop.
|
||||
commandLoopManager.unregisterLoop(widgetId.value);
|
||||
}
|
||||
});
|
||||
|
||||
// This watcher handles changes to the interval, but only if the loop is active.
|
||||
debouncedWatch(intervalMS, (newInterval) => {
|
||||
// If the loop isn't active, do nothing. The `active` watcher handles state.
|
||||
if (!active.value) {
|
||||
// If the interval is cleared while inactive, ensure it's unregistered.
|
||||
if (!newInterval || newInterval <= 0) {
|
||||
commandLoopManager.unregisterLoop(widgetId.value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalValue = typeof newInterval === 'string' ? parseInt(newInterval, 10) : newInterval;
|
||||
|
||||
// The registerLoop function internally handles unregistering the old task.
|
||||
// It will also handle unregistering if the new interval is invalid (e.g., 0).
|
||||
commandLoopManager.registerLoop(widgetId.value, intervalValue, runCommands);
|
||||
|
||||
}, { debounce: 500 });
|
||||
|
||||
const handleAddItem = () => {
|
||||
const newId =
|
||||
Math.max(
|
||||
0,
|
||||
...modelValue.value.widgetProps.map((item) => item.id)
|
||||
) + 1
|
||||
const newItem: DraggableComponent<UartCommandData> = {
|
||||
id: newId,
|
||||
componentType: markRaw(UartAtCommand),
|
||||
props: {
|
||||
label: 'New Command',
|
||||
command: 'AT+CMD',
|
||||
response: ''
|
||||
}
|
||||
}
|
||||
modelValue.value.widgetProps.push(newItem)
|
||||
}
|
||||
|
||||
function deleteItem(id: number) {
|
||||
const index = modelValue.value.widgetProps.findIndex((item) => item.id === id)
|
||||
if (index !== -1) {
|
||||
modelValue.value.widgetProps.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
/* optional helper if you still need cloning */
|
||||
function rawClone(item: DraggableComponent): DraggableComponent {
|
||||
return { ...item } // already plain in parent
|
||||
}
|
||||
|
||||
function ensureUniqueId(evt: any) {
|
||||
const arr = modelValue.value.widgetProps
|
||||
const moved = arr[evt.newIndex] // item that just arrived
|
||||
const hasDuplicate = arr.filter(i => i.id === moved.id).length > 1
|
||||
if (hasDuplicate) {
|
||||
// e.g. give it the next free integer
|
||||
const max = Math.max(...arr.map(i => i.id))
|
||||
moved.id = max + 1
|
||||
}
|
||||
}
|
||||
|
||||
const executeOnce = () => {
|
||||
// Register a one-time execution with the command loop manager
|
||||
// Set as highest priority by using a very small interval (1ms)
|
||||
commandLoopManager.registerLoop(
|
||||
`${widgetId.value}-once-${Date.now()}`, // Unique ID
|
||||
1, // 1ms interval (will be executed immediately)
|
||||
runCommands,
|
||||
true // oneTime = true
|
||||
)
|
||||
}
|
||||
|
||||
const runCommands = async () => {
|
||||
if (useWsStore().state !== 'CONNECTED') {
|
||||
globalNotify('Device not connected', 'error');
|
||||
return
|
||||
}
|
||||
|
||||
const commandsToRun = modelValue.value.widgetProps
|
||||
if (!commandsToRun || commandsToRun.length === 0) return
|
||||
|
||||
// Extract command strings
|
||||
const commandStrings = commandsToRun.map(cmd => cmd.props.command)
|
||||
|
||||
if (isDevMode()) {
|
||||
console.log('Running commands:', commandStrings)
|
||||
}
|
||||
|
||||
// Execute all commands at once
|
||||
const responses = await sendCommands(commandStrings)
|
||||
|
||||
// Update responses in UI
|
||||
commandsToRun.forEach((command, index) => {
|
||||
if (index < responses.length) {
|
||||
command.props.response = responses[index] || 'No response'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
commandLoopManager.unregisterLoop(widgetId.value)
|
||||
})
|
||||
|
||||
// Initialize on mount
|
||||
onMounted(() => {
|
||||
// On component mount, only register if it's explicitly set to active and has an interval.
|
||||
if (active.value && intervalMS.value > 0) {
|
||||
commandLoopManager.registerLoop(
|
||||
widgetId.value,
|
||||
intervalMS.value,
|
||||
runCommands
|
||||
)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full p-1">
|
||||
<VueDraggable
|
||||
v-model="modelValue.widgetProps"
|
||||
item-key="id"
|
||||
class="flex-1 min-h-0 overflow-y-auto"
|
||||
group="people"
|
||||
:clone="rawClone"
|
||||
:animation="100"
|
||||
direction="vertical"
|
||||
handle=".drag-handle"
|
||||
@add="ensureUniqueId"
|
||||
>
|
||||
<div v-for="row in modelValue.widgetProps" :key="row.id" class="flex flex-row items-center">
|
||||
<el-tag v-if="editCell" size="large" type="success" class="drag-handle cursor-move">
|
||||
=
|
||||
</el-tag>
|
||||
<component
|
||||
:is="row.componentType"
|
||||
v-model:modelValue="row.props"
|
||||
:editCell="editCell"
|
||||
class="flex-1"
|
||||
/>
|
||||
<el-button
|
||||
v-if="editCell"
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="deleteItem(row.id)"
|
||||
circle
|
||||
>
|
||||
<InlineSvg name="trash" width="20"></InlineSvg>
|
||||
</el-button>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
<div v-if="editCell" class="bg-gray-50 flex gap-1">
|
||||
<el-button type="primary" size="small" @click="handleAddItem">{{ translate('widget.addCommand') }}</el-button>
|
||||
<div>
|
||||
<el-popover
|
||||
placement="top-start"
|
||||
trigger="hover"
|
||||
:show-after="1000"
|
||||
:content="translate('widget.loopInterval')"
|
||||
>
|
||||
<template #reference>
|
||||
<el-input
|
||||
v-model="intervalMS"
|
||||
size="small"
|
||||
type="number"
|
||||
:min="0"
|
||||
:max="2147483647"
|
||||
>
|
||||
<template #prepend>
|
||||
<InlineSvg name="repeat" width="20"></InlineSvg>
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<teleport defer :to="`#tp-widget-before-${modelValue.i}`">
|
||||
<el-button plain size="small" @click="active = !active" :type="active ? 'success' : 'info'">
|
||||
{{ intervalMS }}ms
|
||||
</el-button>
|
||||
</teleport>
|
||||
<teleport defer :to="`#tp-widget-${modelValue.i}`">
|
||||
<el-button text bg size="small" @click="executeOnce">
|
||||
<InlineSvg name="play" width="20"></InlineSvg>
|
||||
</el-button>
|
||||
</teleport>
|
||||
</template>
|
|
@ -0,0 +1,37 @@
|
|||
<!-- CommandWidget.vue -->
|
||||
<script setup lang="ts">
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import type { DraggableComponent } from '../../types/grid'
|
||||
|
||||
/* ---------------- props & model ----------------------------------- */
|
||||
const modelValue = defineModel<DraggableComponent[]>({ required: true })
|
||||
defineProps<{
|
||||
editCell: boolean
|
||||
}>()
|
||||
|
||||
/* optional helper if you still need cloning */
|
||||
function rawClone(item: DraggableComponent): DraggableComponent {
|
||||
return { ...item } // already plain in parent
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueDraggable
|
||||
v-model="modelValue"
|
||||
item-key="id"
|
||||
class="h-full flex flex-col"
|
||||
group="people"
|
||||
:clone="rawClone"
|
||||
:animation="100"
|
||||
direction="vertical"
|
||||
handle=".drag-handle"
|
||||
>
|
||||
<component
|
||||
v-for="row in modelValue"
|
||||
:key="row.id"
|
||||
:is="row.componentType"
|
||||
v-model:modelValue="row.props"
|
||||
:is-editing-cell="editCell"
|
||||
/>
|
||||
</VueDraggable>
|
||||
</template>
|
|
@ -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: {
|
||||
|
|
Loading…
Reference in New Issue