feat(uart proxy)

- use vuetify virtual scroll to handle large message

known issues:
- very slow addItem when frequent add 1 item, become short time normal after a batch item add
 -> batch add every 20ms
This commit is contained in:
kerms 2024-05-20 10:40:22 +08:00
parent c2b8f6ba09
commit 15c1143b25
16 changed files with 2115 additions and 97 deletions

309
package-lock.json generated
View File

@ -8,12 +8,15 @@
"name": "vue-project",
"version": "0.0.0",
"dependencies": {
"element-plus": "^2.6.1",
"@vueuse/core": "^10.9.0",
"ansi_up": "^6.0.2",
"element-plus": "^2.7.3",
"mitt": "^3.0.1",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"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",
@ -38,6 +41,7 @@
"vite-plugin-css-injected-by-js": "^3.5.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-singlefile": "^2.0.1",
"vite-plugin-vuetify": "^2.0.3",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^2.0.6"
}
@ -106,7 +110,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
@ -122,7 +125,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -138,7 +140,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -154,7 +155,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -170,7 +170,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -186,7 +185,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -202,7 +200,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@ -218,7 +215,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@ -234,7 +230,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -250,7 +245,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -266,7 +260,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -282,7 +275,6 @@
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -298,7 +290,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -314,7 +305,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -330,7 +320,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -346,7 +335,6 @@
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -362,7 +350,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -378,7 +365,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
@ -394,7 +380,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
@ -410,7 +395,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
@ -426,7 +410,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -442,7 +425,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -458,7 +440,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -711,7 +692,7 @@
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@ -725,7 +706,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=6.0.0"
}
@ -734,7 +715,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=6.0.0"
}
@ -743,7 +724,7 @@
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@ -758,7 +739,7 @@
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@ -860,7 +841,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -873,7 +853,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
@ -886,7 +865,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -899,7 +877,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -912,7 +889,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -925,7 +901,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -938,7 +913,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -951,7 +925,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -964,7 +937,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -977,7 +949,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -990,7 +961,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -1003,7 +973,6 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -1016,7 +985,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -1047,7 +1015,7 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true
"devOptional": true
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
@ -1072,7 +1040,7 @@
"version": "20.11.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
"dev": true,
"devOptional": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@ -1084,9 +1052,9 @@
"dev": true
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
"version": "0.0.20",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.21.0",
@ -1487,15 +1455,28 @@
"integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==",
"dev": true
},
"node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"node_modules/@vuetify/loader-shared": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-2.0.3.tgz",
"integrity": "sha512-Ss3GC7eJYkp2SF6xVzsT7FAruEmdihmn4OCk2+UocREerlXKWgOKKzTN5PN3ZVN5q05jHHrsNhTuWbhN61Bpdg==",
"devOptional": true,
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
"upath": "^2.0.1"
},
"peerDependencies": {
"vue": "^3.0.0",
"vuetify": "^3.0.0"
}
},
"node_modules/@vueuse/core": {
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz",
"integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.9.0",
"@vueuse/shared": "10.9.0",
"vue-demi": ">=0.14.7"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@ -1527,19 +1508,19 @@
}
},
"node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz",
"integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.9.0.tgz",
"integrity": "sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==",
"dependencies": {
"vue-demi": "*"
"vue-demi": ">=0.14.7"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
@ -1574,7 +1555,7 @@
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"dev": true,
"devOptional": true,
"bin": {
"acorn": "bin/acorn"
},
@ -1607,6 +1588,14 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi_up": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/ansi_up/-/ansi_up-6.0.2.tgz",
"integrity": "sha512-3G3vKvl1ilEp7J1u6BmULpMA0xVoW/f4Ekqhl8RTrJrhEBkonKn5k3bUc5Xt+qDayA6iDX0jyUh3AbZjB/l0tw==",
"engines": {
"node": "*"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -1800,7 +1789,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true
"devOptional": true
},
"node_modules/callsites": {
"version": "3.1.0",
@ -2094,7 +2083,7 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"ms": "2.1.2"
},
@ -2117,7 +2106,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"devOptional": true,
"bin": {
"detect-libc": "bin/detect-libc.js"
},
@ -2275,9 +2264,9 @@
"dev": true
},
"node_modules/element-plus": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.6.1.tgz",
"integrity": "sha512-6VRpLjwtIVdtUuITJPPKtpOH1NM6nuAkRE3q5O4Lrx0N1bYMhTkiqb2Jy7zfQuDPbOIkkF2OABTzegpNnzgsnQ==",
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz",
"integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1",
@ -2299,6 +2288,94 @@
"vue": "^3.2.0"
}
},
"node_modules/element-plus/node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ=="
},
"node_modules/element-plus/node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz",
"integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/element-plus/node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/element-plus/node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/element-plus/node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.7.tgz",
"integrity": "sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@ -2320,7 +2397,7 @@
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
"integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
"dev": true,
"devOptional": true,
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
@ -2807,7 +2884,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@ -3265,7 +3341,7 @@
"version": "1.24.1",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.24.1.tgz",
"integrity": "sha512-kUpHOLiH5GB0ERSv4pxqlL0RYKnOXtgGtVe7shDGfhS0AZ4D1ouKFYAcLcZhql8aMspDNzaUCumGHZ78tb2fTg==",
"dev": true,
"devOptional": true,
"dependencies": {
"detect-libc": "^1.0.3"
},
@ -3295,7 +3371,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -3315,7 +3390,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@ -3335,7 +3409,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@ -3355,7 +3428,6 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -3375,7 +3447,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -3395,7 +3466,6 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -3415,7 +3485,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -3435,7 +3504,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@ -3455,7 +3523,6 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@ -3659,7 +3726,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"devOptional": true
},
"node_modules/muggle-string": {
"version": "0.4.1",
@ -4480,7 +4547,7 @@
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
"integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==",
"dev": true,
"devOptional": true,
"dependencies": {
"@types/estree": "1.0.5"
},
@ -4607,7 +4674,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=0.10.0"
}
@ -4624,7 +4691,7 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"devOptional": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@ -4903,7 +4970,7 @@
"version": "5.29.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.29.2.tgz",
"integrity": "sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw==",
"dev": true,
"devOptional": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@ -4921,7 +4988,7 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
"devOptional": true
},
"node_modules/text-table": {
"version": "0.2.0",
@ -5033,7 +5100,7 @@
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
"devOptional": true
},
"node_modules/unimport": {
"version": "3.7.1",
@ -5184,6 +5251,16 @@
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/upath": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",
"integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==",
"devOptional": true,
"engines": {
"node": ">=4",
"yarn": "*"
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
@ -5233,7 +5310,7 @@
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.2.tgz",
"integrity": "sha512-FWZbz0oSdLq5snUI0b6sULbz58iXFXdvkZfZWR/F0ZJuKTSPO7v72QPXt6KqYeMFb0yytNp6kZosxJ96Nr/wDQ==",
"dev": true,
"devOptional": true,
"dependencies": {
"esbuild": "^0.20.1",
"postcss": "^8.4.36",
@ -5351,6 +5428,25 @@
"vite": "^5.1.4"
}
},
"node_modules/vite-plugin-vuetify": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-2.0.3.tgz",
"integrity": "sha512-HbYajgGgb/noaVKNRhnnXIiQZrNXfNIeanUGAwXgOxL6h/KULS40Uf51Kyz8hNmdegF+DwjgXXI/8J1PNS83xw==",
"devOptional": true,
"dependencies": {
"@vuetify/loader-shared": "^2.0.3",
"debug": "^4.3.3",
"upath": "^2.0.1"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": ">=5",
"vue": "^3.0.0",
"vuetify": "^3.0.0"
}
},
"node_modules/vite-svg-loader": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/vite-svg-loader/-/vite-svg-loader-5.1.0.tgz",
@ -5467,6 +5563,39 @@
"typescript": "*"
}
},
"node_modules/vuetify": {
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.6.5.tgz",
"integrity": "sha512-YrHTM1vb7UllAtfH9tWfTo1wYMjyCSybu4WtXrfMRpMwAaZWgfrMmqD/4Tc+0KqDsDsYMXaYs0nJ6HtdMJZbyA==",
"engines": {
"node": "^12.20 || >=14.13"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/johnleider"
},
"peerDependencies": {
"typescript": ">=4.7",
"vite-plugin-vuetify": ">=1.0.0",
"vue": "^3.3.0",
"vue-i18n": "^9.0.0",
"webpack-plugin-vuetify": ">=2.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
},
"vite-plugin-vuetify": {
"optional": true
},
"vue-i18n": {
"optional": true
},
"webpack-plugin-vuetify": {
"optional": true
}
}
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",

View File

@ -5,6 +5,7 @@
"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",
@ -14,11 +15,14 @@
"format": "prettier --write src/"
},
"dependencies": {
"element-plus": "^2.6.1",
"@vueuse/core": "^10.9.0",
"ansi_up": "^6.0.2",
"element-plus": "^2.7.3",
"mitt": "^3.0.1",
"pinia": "^2.1.7",
"vue": "^3.4.21",
"vue-i18n": "^9.10.2",
"vuetify": "^3.6.5",
"vue-router": "^4.3.0"
},
"devDependencies": {
@ -44,6 +48,7 @@
"vite-plugin-css-injected-by-js": "^3.5.0",
"vite-plugin-html": "^3.2.2",
"vite-plugin-singlefile": "^2.0.1",
"vite-plugin-vuetify": "^2.0.3",
"vite-svg-loader": "^5.1.0",
"vue-tsc": "^2.0.6"
}

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

@ -0,0 +1,47 @@
import {type ApiJsonMsg} from '@/api'
import * as api from "@/api/index";
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 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);
}

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

@ -0,0 +1,96 @@
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
}
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 function uart_send_msg(payload: Uint8Array, sub_mod: number = 1) {
/* 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 = 1) {
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 = 1) {
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 = 1) {
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 = 1) {
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);
}

View File

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

After

(image error) Size: 127 B

View File

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

After

(image error) Size: 127 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="m136-80-56-56 264-264H160v-80h320v320h-80v-184L136-80Zm344-400v-320h80v184l264-264 56 56-264 264h184v80H480Z"/></svg>

After

(image error) Size: 206 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M120-120v-320h80v184l504-504H520v-80h320v320h-80v-184L256-200h184v80H120Z"/></svg>

After

(image error) Size: 171 B

View File

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

After

(image error) Size: 292 B

View File

@ -4,6 +4,8 @@ import '@/assets/page.css'
import '@/assets/navigation.css'
import 'element-plus/dist/index.css';
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
@ -17,5 +19,6 @@ const app = createApp(App)
app.use(createPinia())
app.use(i18n);
app.use(router)
app.use(createVuetify())
app.mount('#app')

View File

@ -0,0 +1,553 @@
import {defineStore} from "pinia";
import {computed, type Ref, ref, shallowReactive} from "vue";
import {AnsiUp} from 'ansi_up'
import {debouncedWatch} from "@vueuse/core";
import {type IUartConfig, uart_send_msg} from "@/api/apiUart";
interface IDataArchive {
time: number;
isRX: boolean;
data: Uint8Array;
}
export interface IDataBuf {
time: string;
isRX: boolean;
data: Uint8Array;
str: string;
hex: string;
hexdump: string;
}
function decodeUtf8(u8Arr: Uint8Array) {
try {
const decoder = new TextDecoder();
const decodedText = decoder.decode(u8Arr); // Attempt to decode
return decodedText.replace(/\uFFFD/g, ''); // Remove all <20> characters
} catch (error) {
return "";
}
}
function escapeHTML(text: string) {
const element = document.createElement('p');
element.textContent = text;
return element.innerHTML;
}
function unescapeString(str: string) {
return str.replace(/\\(u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0-7]{1,3}|.)/g, (_, escapeChar) => {
switch (escapeChar[0]) {
case 'n':
return '\n';
case 'r':
return '\r';
case 't':
return '\t';
case 'b':
return '\b';
case 'v':
return '\v';
case '0':
return '\0';
case 'x':
return String.fromCharCode(parseInt(escapeChar.slice(1), 16));
case 'u':
return String.fromCharCode(parseInt(escapeChar.slice(1), 16));
default:
if (/^[0-7]{1,3}$/.test(escapeChar)) {
return String.fromCharCode(parseInt(escapeChar, 8));
}
return escapeChar;
}
});
}
const zeroPad = (num: number, places: number) => String(num).padStart(places, '0');
const ansi_up = new AnsiUp();
ansi_up.escape_html = false;
/* quick HEX lookup table */
const byteToHex: string[] = new Array(256);
for (let n = 0; n <= 0xff; ++n) {
byteToHex[n] = n.toString(16).padStart(2, "0").toUpperCase();
}
function u8toHexString(buff: Uint8Array) {
const hexOctets = []; // new Array(buff.length) is even faster (preallocates necessary array size), then use hexOctets[i] instead of .push()
if (buff.length === 0) {
return ""
}
for (let i = 0; i < buff.length; ++i)
hexOctets.push(byteToHex[buff[i]]);
return hexOctets.join(" ");
}
function u8toHexdump(buffer: Uint8Array) {
const lines: string[] = [];
const bytesPerRow = 16;
for (let lineStart = 0; lineStart < buffer.length; lineStart += bytesPerRow) {
let result = lineStart.toString(16).padStart(4, '0') + ": ";
let ascii = "";
// Process each byte in the row
for (let i = 0; i < bytesPerRow; i++) {
const byteIndex = lineStart + i;
if (byteIndex >= buffer.length) {
// Pad the row if it's shorter than the full width
result += " ";
} else {
const byte = buffer[byteIndex];
result += byteToHex[byte] + " ";
// Prepare the ASCII representation, non-printable as '.'
if (byte >= 32 && byte <= 126) {
ascii += String.fromCharCode(byte);
} else {
ascii += ".";
}
}
if (i === 8) {
result += " "
}
}
result += "|" + ascii + "|" + " ".repeat(16 - ascii.length);
lines.push(result);
}
return strToHTML(escapeHTML(lines.join('\n')));
}
function strToHTML(str: string) {
return str.replace(/\n/g, '<br>') // Replace newline with <br> tag
.replace(/\t/g, '&emsp;') // Replace tab with spaces (or you could use '&nbsp;' for single spaces)
.replace(/ /g, '&nbsp;');
}
function isArrayContained(subArray: Uint8Array, mainArray: Uint8Array) {
if (subArray.length === 0) return -1;
outerLoop: for (let i = 0; i <= mainArray.length - subArray.length; i++) {
// Check if subArray is found starting at position i in mainArray
for (let j = 0; j < subArray.length; j++) {
if (mainArray[i + j] !== subArray[j]) {
continue outerLoop; // Continue the outer loop if mismatch found
}
}
return i; // subArray is found within mainArray
}
return -1;
}
const baudArr = [
{
start: 300,
count: 9,
}, {
start: 14400,
count: 9,
}
]
function generateBaudArr(results: { baud: number;}[]) {
for (let i = 0; i < baudArr.length; ++i) {
let start = baudArr[i].start;
for (let j = 0; j < baudArr[i].count; ++j) {
results.push({baud: start});
start += start;
}
}
results.sort((a, b) => {
return a.baud - b.baud;
});
}
export const useDataViewerStore = defineStore('text-viewer', () => {
/* private value */
const predefineColors = [
'#f0f9eb',
'#ecf4ff',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'rgba(85,155,197,0.44)',
]
const predefinedUartBaudFrequent = Object.freeze([
{
baud: 115200,
}, {
baud: 921600,
}, {
baud: 9600,
}
])
const uartBaudList: { baud: number; }[] = [];
generateBaudArr(uartBaudList);
/* public value */
const configPanelTab = ref("second");
const configPanelShow = ref(true);
const quickAccessPanelShow = ref(true);
const enableAnsiDecode = ref(true);
const frameBreakSequence = ref("\\n");
const frameBreakSequenceNormalized = ref(new Uint8Array(0));
const frameBreakDelay = ref(0);
const showText = ref(true);
const showHex = ref(false);
const showHexdump = ref(false);
const enableLineWrap = ref(true);
const showTimestamp = ref(true);
const pauseAutoRefresh = ref(false);
const RxHexdumpColor = ref("#f0f9eb");
const RxTotalByteCount = ref(0);
const RxByteCount = ref(0);
const TxHexdumpColor = ref("#ecf4ff");
const TxTotalByteCount = ref(0);
const TxByteCount = ref(0);
const enableFilter = ref(true);
const enableMatch = ref(false);
const forceToBottom = ref(true);
const filterChanged = ref(false);
const textSuffixValue = ref("")
const textPrefixValue = ref("")
const uartBaud = ref(115200);
const uartBaudReal = ref(115200);
const uartConfig: Ref<IUartConfig> = ref({
data_bits: 8,
parity: 0,
stop_bits: 1,
});
const filterValue = ref("");
const computedFilterValue = computed(() => {
const str = unescapeString(filterValue.value);
const encoder = new TextEncoder();
return encoder.encode(str);
})
const computedSuffixValue = computed(() => {
const str = unescapeString(textSuffixValue.value);
const encoder = new TextEncoder();
return encoder.encode(str);
})
const computedPrefixValue = computed(() => {
const str = unescapeString(textPrefixValue.value);
const encoder = new TextEncoder();
return encoder.encode(str);
})
debouncedWatch(() => frameBreakSequence.value, (newValue) => {
const unescapedStr = unescapeString(newValue);
const encoder = new TextEncoder();
frameBreakSequenceNormalized.value = encoder.encode(unescapedStr);
}, {debounce: 300, immediate: true});
debouncedWatch(() => frameBreakDelay.value, (newValue) => {
if (newValue < 0) {
frameBreakReady = false;
clearTimeout(frameBreakTimeoutID);
} else if (newValue === 0) {
frameBreakReady = true;
clearTimeout(frameBreakTimeoutID);
} else {
refreshTimeout();
}
}, {debounce: 300, immediate: true});
const dataArchive: IDataArchive[] = [];
const dataBuf: IDataBuf[] = [];
const dataBufLength = ref(0);
/* actual data shown on screen */
const dataFiltered: IDataBuf[] = shallowReactive([]);
const dataFilteredLength = ref(0);
let frameBreakReady = false;
let frameBreakTimeoutID = setTimeout(() => {
}, 0);
debouncedWatch(computedFilterValue, () => {
dataFiltered.length = 0; // Clear the array efficiently
if (computedFilterValue.value.length === 0) {
dataFiltered.push(...dataBuf);
filterChanged.value = true;
return;
}
const index = dataFiltered.length;
for (const item of dataBuf) {
const index = isArrayContained(computedFilterValue.value, item.data);
if (index >= 0) {
dataFiltered.push(item);
}
filterChanged.value = true;
}
}, {debounce: 300});
function addString(item: string, isRX: boolean = false, doSend: boolean = false) {
const encoder = new TextEncoder();
item = unescapeString(item);
const encodedStr = encoder.encode(item);
return addItem(encodedStr, isRX, doSend);
}
function addHexString(item: string, isRX: boolean = false, doSend: boolean = false){
if (item === "") {
return addItem(new Uint8Array(0), isRX);
}
const hexArray = item.split(' ');
// Map each hex value to a decimal (integer) and create a Uint8Array from these integers
const uint8Array = new Uint8Array(hexArray.map(hex => parseInt(hex, 16)));
return addItem(uint8Array, isRX, doSend);
}
function addItem(item: Uint8Array, isRX: boolean, doSend: boolean = false){
const t = new Date();
// dataArchive.push({
// time: t.getMilliseconds(),
// isRX: isRX,
// data: u8arr,
// });
if (isRX) {
RxTotalByteCount.value += item.length;
RxByteCount.value = item.length;
} else {
/* append prefix and suffix */
if (computedPrefixValue.value.length || computedSuffixValue.value.length) {
const newArr = new Uint8Array(computedPrefixValue.value.length +
computedSuffixValue.value.length + item.length);
newArr.set(computedPrefixValue.value);
newArr.set(item, computedPrefixValue.value.length);
newArr.set(computedSuffixValue.value, computedPrefixValue.value.length + item.length);
item = newArr;
}
if (doSend) {
/* INFO: hard coded for the moment */
uart_send_msg(item);
}
TxTotalByteCount.value += item.length;
TxByteCount.value = item.length;
}
let str = decodeUtf8(item);
str = escapeHTML(str);
str = strToHTML(str);
/* unescape data \n */
if (enableAnsiDecode.value) {
/* ansi_to_html will escape HTML sequence */
str = ansi_up.ansi_to_html("\x1b[0m" + str);
}
dataBuf.push({
time: "["
+ zeroPad(t.getHours(), 2) + ":"
+ zeroPad(t.getMinutes(), 2) + ":"
+ zeroPad(t.getSeconds(), 2) + ":"
+ zeroPad(t.getMilliseconds(), 3)
+ "]",
data: item,
isRX: isRX,
str: str,
hex: u8toHexString(item),
hexdump: u8toHexdump(item),
});
if (dataBuf.length >= 20000) {
dataBuf.splice(0, 5000);
}
if (!enableFilter.value || computedFilterValue.value.length === 0) {
dataFiltered.push(dataBuf[dataBuf.length - 1]);
if (dataFiltered.length >= 20000) {
dataFiltered.splice(0, 5000);
}
} else if (enableFilter.value && isArrayContained(computedFilterValue.value, dataBuf[dataBuf.length - 1].data) >= 0) {
dataFiltered.push(dataBuf[dataBuf.length - 1]);
if (dataFiltered.length >= 20000) {
dataFiltered.splice(0, 5000);
}
}
dataBufLength.value = dataBuf.length;
}
function popItem() {
dataBuf.pop();
dataFiltered.pop();
}
function doFrameBreak() {
frameBreakReady = true;
}
function refreshTimeout() {
/* always break */
// if (frameBreakDelay.value === 0) {
// frameBreakReady = true;
// }
if (!frameBreakReady && frameBreakDelay.value > 0) {
clearTimeout(frameBreakTimeoutID);
frameBreakTimeoutID = setTimeout(doFrameBreak, frameBreakDelay.value);
}
}
function addChunk(item: Uint8Array, isRX: boolean) {
let newArray: Uint8Array;
if (frameBreakSequence.value === "") {
if (frameBreakReady || dataBuf.length === 0 || dataBuf[dataBuf.length - 1].isRX != isRX) {
addItem(item, isRX);
frameBreakReady = false;
} else {
/* TODO: append item to last */
newArray = new Uint8Array(dataBuf[dataBuf.length - 1].data.length + item.length + 1);
newArray.set(dataBuf[dataBuf.length - 1].data);
newArray.set(item, dataBuf[dataBuf.length - 1].data.length);
popItem();
addItem(newArray, isRX);
}
refreshTimeout();
return;
}
if (frameBreakReady) {
newArray = item;
} else {
if (dataBuf.length) {
newArray = new Uint8Array(dataBuf[dataBuf.length - 1].data.length + item.length + 1);
newArray.set(dataBuf[dataBuf.length - 1].data);
newArray.set(item, dataBuf[dataBuf.length - 1].data.length);
popItem();
} else {
newArray = item;
}
}
console.log(newArray)
console.log(frameBreakSequenceNormalized.value)
/* break frame at sequence match */
let matchIndex = isArrayContained(frameBreakSequenceNormalized.value, newArray);
while (matchIndex < 0) {
console.log(matchIndex)
/* update last buf item */
addItem(newArray.slice(0, matchIndex + frameBreakSequenceNormalized.value.length), isRX);
newArray = newArray.slice(matchIndex + frameBreakSequenceNormalized.value.length);
matchIndex = isArrayContained(frameBreakSequenceNormalized.value, newArray);
}
addItem(newArray.slice(0, matchIndex + frameBreakSequenceNormalized.value.length), isRX);
}
function clearByteCount(isRX: boolean) {
if (isRX) {
RxTotalByteCount.value = 0;
} else {
TxTotalByteCount.value = 0;
}
}
function clearDataBuff() {
dataBuf.length = 0;
dataFiltered.length = 0;
dataBufLength.value = 0;
RxByteCount.value = 0;
RxTotalByteCount.value = 0;
TxByteCount.value = 0;
TxTotalByteCount.value = 0;
}
function clearFilteredBuff() {
dataFiltered.length = 0;
}
function refreshFilteredBuff() {
const oldValue = filterValue.value;
filterValue.value += "66";
filterValue.value = oldValue;
}
function setUartBaud(baud: number) {
uartBaudReal.value = baud;
for (let i = 0; i < uartBaudList.length; i++) {
const difference = Math.abs(uartBaudList[i].baud - baud);
const percentageDifference = (difference / baud);
if (percentageDifference !== 0 && percentageDifference < 0.001) {
uartBaud.value = uartBaudList[i].baud;
return;
}
}
uartBaud.value = baud;
}
return {
addItem,
addChunk,
addString,
addHexString,
clearFilteredBuff,
clearDataBuff,
refreshFilteredBuff,
textSuffixValue,
textPrefixValue,
clearByteCount,
dataBufLength,
configPanelTab,
configPanelShow,
pauseAutoRefresh,
quickAccessPanelShow,
dataFiltered,
filterValue,
enableAnsiDecode,
showHex,
showHexdump,
showText,
showTimestamp,
enableLineWrap,
RxHexdumpColor,
TxHexdumpColor,
predefineColors,
RxByteCount,
RxTotalByteCount,
TxByteCount,
TxTotalByteCount,
forceToBottom,
frameBreakSequence,
frameBreakDelay,
filterChanged,
/* UART */
predefinedUartBaudFrequent,
uartBaudList,
uartBaud,
uartConfig,
uartBaudReal,
setUartBaud,
}
});

View File

@ -1,9 +1,497 @@
<template>
<div class="button-m-0 messages-container flex flex-grow overflow-hidden" :class="{'flex-col': layoutMode==='col'}">
<div v-show="store.configPanelShow" ref="win1Ref" class="bg-gray-50 flex-shrink-0 overflow-auto"
:class="{
'max-w-60': layoutMode==='row', 'xl:max-w-80': layoutMode==='row',
'min-w-60': layoutMode==='row', 'xl:min-w-80': layoutMode==='row'
}"
>
<text-data-config></text-data-config>
</div>
<div v-show="store.configPanelShow && (winDataView.show || win2.show)" ref="firstWinResizeRef"></div>
<div v-show="winDataView.show" class="flex flex-col flex-grow overflow-hidden p-2">
<textDataViewer></textDataViewer>
</div>
<div v-show="winDataView.show && win2.show" ref="thirdWinResizeRef"></div>
<div v-show="win2.show" ref="win2Ref" :class="{
'max-w-80': layoutMode==='row', 'xl:max-w-96': layoutMode==='row',
'min-w-80': layoutMode==='row', 'xl:min-w-96': layoutMode==='row'
}"
class="bg-gray-50 flex flex-col flex-shrink-0 min-h-32 overflow-auto p-2">
<div class="flex flex-col gap-5">
<el-text type="primary">快捷发送</el-text>
<el-text size="small">努力施工中</el-text>
</div>
</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="layoutMode" :options="layoutOptions" size="small"/>
</div>
<el-checkbox label="自适应" v-model="layoutConf.isAutoHide" border size="small"
:disabled="layoutMode==='col'"/>
<el-checkbox label="设置窗" v-model="store.configPanelShow" border size="small" :disabled="layoutConf.isAutoHide"/>
<el-checkbox label="数据窗" v-model="winDataView.show" border size="small" :disabled="layoutConf.isAutoHide"/>
<el-checkbox label="快捷窗" v-model="win2.show" border size="small" :disabled="layoutConf.isAutoHide"/>
</div>
<template #reference>
<el-button class="min-h-full" type="primary" :size="layoutConf.isMedium ? 'small' : 'default'">布局
</el-button>
</template>
</el-popover>
</div>
<div class="mx-1"></div>
</teleport>
</template>
<script setup lang="ts">
import {onMounted, onUnmounted, reactive, type Ref, ref, type UnwrapRef, watch} from "vue";
import {breakpointsTailwind, useBreakpoints, useWindowSize} from '@vueuse/core'
import {useDataViewerStore} from '@/stores/dataViewerStore';
import * as api from '@/api';
import * as uart from '@/api/apiUart';
import * as bin from '@/api/binDataDef';
import * as data_flow 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 {type ApiBinaryMsg} from "@/api/binDataDef";
import {
type IUartMsgBaud,
type IUartMsgConfig,
uart_get_baud,
uart_get_config,
uart_set_baud,
uart_set_config,
WtUartCmd
} from "@/api/apiUart";
import {isDevMode} from "@/composables/buildMode";
import {ControlEvent} from "@/api";
import {wt_data_flow_attach_cur_to_sender} from "@/api/apiDataFlow";
const store = useDataViewerStore()
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 windowSize = useWindowSize()
const layoutConf = reactive({
isSmall: breakpoints.smaller("sm"),
isMedium: breakpoints.smaller("lg"),
isAutoHide: ref(true)
});
const layoutOptions = [{
label: '横/行',
value: 'row'
}, {
label: '竖/列',
value: 'col'
}]
const layoutMode = ref(layoutOptions[0].value);
const displayOptions = [{
label: '默认显示',
value: 'auto'
}, {
label: '手动显示',
value: 'manual'
}]
const displayMode = ref(displayOptions[0].value);
interface WinProperty {
show: boolean;
width: string;
height: string;
borderSize: number;
}
const win1 = reactive<WinProperty>({
show: true,
width: "100px",
height: "100px",
borderSize: 3,
});
const win2 = reactive<WinProperty>({
show: true,
width: "100px",
height: "100px",
borderSize: 3,
});
const winDataView = reactive({
show: true,
})
const ctx = reactive({
curResizeTarget: "none",
curHeightOffset: 0,
});
function updateCursor(i: HTMLElement) {
if (layoutMode.value === 'row') {
i.style.cursor = "col-resize";
} else {
i.style.cursor = "row-resize";
}
}
function updateWin(r: Ref<HTMLElement>, p: UnwrapRef<WinProperty>) {
if (layoutMode.value === '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, win1);
updateWin(win2Ref, win2);
}
function mouseResize(e: MouseEvent) {
const curTarget = e.target as HTMLElement
if (layoutMode.value === 'row') {
let f = e.clientX;
if (ctx.curResizeTarget === "first") {
win1Ref.value.style.minWidth = f + "px";
win1Ref.value.style.maxWidth = f + "px";
} else {
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 - win2.borderSize + "px";
win2Ref.value.style.maxWidth = document.body.scrollWidth - f - win2.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 {
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 = 0;
if (layoutMode.value === '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 - win2.borderSize + "px";
win2Ref.value.style.maxWidth = document.body.scrollWidth - f - win2.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 - win2.borderSize + "px";
win2Ref.value.style.maxHeight = document.body.scrollHeight - f - win2.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);
layoutConf.isAutoHide = false;
}
function stopResize() {
if (win1Ref.value) {
win1Ref.value.style.transition = '';
if (layoutMode.value === "row") {
win1.width = win1Ref.value.style.minWidth;
} else {
win1.height = win1Ref.value.style.minHeight;
}
}
if (win2Ref.value) {
win2Ref.value.style.transition = '';
if (layoutMode.value === "row") {
win2.width = win1Ref.value.style.minWidth;
} else {
win2.height = win1Ref.value.style.minHeight;
}
}
document.body.style.cursor = '';
document.removeEventListener("mousemove", mouseResize, false);
document.removeEventListener("touchmove", touchResize, false);
}
watch(() => layoutMode.value, (value) => {
updateResizer();
if (value === "col") {
layoutConf.isAutoHide = false;
}
});
watch([
() => layoutConf.isSmall,
() => layoutConf.isAutoHide
], (value) => {
if (layoutConf.isAutoHide) {
win2.show = !value[0];
win1Ref.value.style.minWidth = "";
win1Ref.value.style.maxWidth = "";
}
}, {
immediate: true,
});
watch([
() => layoutConf.isMedium,
() => layoutConf.isAutoHide
], (value) => {
if (layoutConf.isAutoHide) {
store.configPanelShow = !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(() => win2.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;
break;
}
default:
if (isDevMode()) {
console.log("uart not treated", msg);
}
break
}
};
const onUartBinaryMsg = (msg: ApiBinaryMsg) => {
console.log("uart", msg);
if (msg.sub_mod !== 1) {
/* ignore other num for the moment */
return;
}
/* UART_NUM_1 msg */
store.addItem(new Uint8Array(msg.payload), true);
};
const onDataFlowJsonMsg = (msg: api.ApiJsonMsg) => {
if (isDevMode()) {
console.log("Dflow Json", msg);
}
};
const onDataFlowBinaryMsg = (msg: ApiBinaryMsg) => {
if (isDevMode()) {
console.log("Dflow Bin", msg);
}
};
const onClientCtrl = (msg: api.ControlMsg) => {
if (msg.type !== api.ControlMsgType.WS_EVENT) {
return
}
if (msg.data === ControlEvent.DISCONNECTED) {
} else if (msg.data === ControlEvent.CONNECTED) {
updateUartData();
}
};
function updateUartData() {
/* TODO: hard code for the moment, 0 is UART instance id (can be changed in the future) */
wt_data_flow_attach_cur_to_sender(0);
uart_get_baud();
uart_get_config();
}
watch(() => store.uartBaud, value => {
uart_set_baud(value);
});
watch(() => store.uartConfig, value => {
uart_set_config(value);
}, {deep: true});
onMounted(() => {
registerModule(api.WtModuleID.UART, {
ctrlCallback: onClientCtrl,
serverJsonMsgCallback: onUartJsonMsg,
serverBinMsgCallback: onUartBinaryMsg,
});
registerModule(api.WtModuleID.DATA_FLOW, {
ctrlCallback: () => {},
serverJsonMsgCallback: onDataFlowJsonMsg,
serverBinMsgCallback: onDataFlowBinaryMsg,
});
firstWinResizeRef.value.style.borderWidth = win1.borderSize + "px";
thirdWinResizeRef.value.style.borderWidth = win2.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();
});
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>

View File

@ -0,0 +1,304 @@
<template>
<div>
<el-tabs v-model="store.configPanelTab" class="mx-2 custom-tabs fit">
<el-tab-pane label="接口" name="first" class="min-h-80">
<div class="flex flex-col gap-2">
<el-form :size="store.configPanelShow ? '' : 'small'">
<el-form-item
label="波特率"
class="mb-2"
>
<div class="flex w-full">
<el-select v-model="store.uartBaud" :teleported="false">
<template #header>
<div class="overflow-auto max-h-40">
<div class="flex gap-0">
<el-input-number
v-model="uartCustomBaud"
placeholder="自定义波特率"
size="small"
:controls="false"
:min="110"
class="flex-grow"
></el-input-number>
<el-button size="small" @click="onUseCustomUartBaud">使用</el-button>
<!-- <el-button size="small" @click="onConfirm" class="ml-0">增加</el-button>-->
</div>
<el-option-group label="常用">
<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="其他">
<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">实际波特率:{{store.uartBaudReal}}</p>
<el-form-item label="数据位" class="mb-2">
<el-select v-model="store.uartConfig.data_bits" :teleported="false" placeholder="Select">
<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="校验位" class="mb-2">
<el-select v-model="store.uartConfig.parity" :teleported="false" placeholder="Select">
<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="停止位">
<el-select v-model="store.uartConfig.stop_bits" :teleported="false" placeholder="Select">
<el-option
v-for="item in uartStopBitsOptions"
:key="item.key"
:value="item.key"
:label="item.label"
/>
</el-select>
</el-form-item>
<el-form-item label=" ">
<div class="flex">
<el-button type="primary">连接</el-button>
</div>
</el-form-item>
</el-form>
</div>
</el-tab-pane>
<!-- ///////////////////////////////////////////////////////////////// -->
<el-tab-pane label="显示框" name="second">
<div class="flex flex-col">
<el-collapse v-model="collapseActiveName">
<el-collapse-item name="1">
<template #title>
显示选项
</template>
<template #default>
<div class="flex flex-col gap-2">
<div class="flex flex-col">
<el-checkbox border v-model="store.showText" label="显示文本"/>
</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">显示时间戳</el-checkbox>
</div>
<div class="flex flex-col">
<el-checkbox border v-model="store.enableLineWrap" label="启用换行"/>
</div>
<el-tag type="success">
<el-text type="success">RX HEXDUMP高亮选色</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高亮选色</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">
<template #title>
其他
</template>
<template #default>
<div class="flex flex-col gap-2">
<el-tooltip
class="box-item"
effect="light"
placement="right-start"
>
<template #content>
<p>ANSI转义码对终端和文本有很多作用比如改变文本颜色等</p>
<p>
简单了解->
<el-link target="_blank" href="https://zhuanlan.zhihu.com/p/390666800">
https://zhuanlan.zhihu.com/p/390666800
</el-link>
</p>
</template>
<el-checkbox border v-model="store.enableAnsiDecode">解析ANSI转义码</el-checkbox>
</el-tooltip>
<el-input v-model="store.filterValue" placeholder="文本;支持\n\x" clearable>
<template #prepend>
过滤
</template>
</el-input>
</div>
</template>
</el-collapse-item>
</el-collapse>
<!-- <div class="flex flex-col">-->
<!-- <el-text type="success">断帧设置</el-text>-->
<!-- <el-input v-model="store.frameBreakSequence" class="max-w-52">-->
<!-- <template #prepend>-->
<!-- 文本匹配断帧-->
<!-- </template>-->
<!-- </el-input>-->
<!-- <el-input v-model="store.frameBreakDelay" type="number" class="max-w-52">-->
<!-- <template #prepend>-->
<!-- 超时断帧-->
<!-- </template>-->
<!-- </el-input>-->
<!-- </div>-->
<!-- <div class="flex flex-col flex-wrap">-->
<!-- <el-button size="small">滚动到底</el-button>-->
<!-- <div>显示-->
<!-- <el-checkbox size="small" border>数据差异高亮</el-checkbox>-->
<!-- <el-checkbox size="small" border>TX高亮</el-checkbox>-->
<!-- <el-checkbox size="small" border>显示RX</el-checkbox>-->
<!-- <el-checkbox size="small" border>显示TX</el-checkbox>-->
<!-- <el-checkbox size="small" border>RX右对齐</el-checkbox>-->
<!-- </div>-->
<!-- &lt;!&ndash; <div>专有协议&ndash;&gt;-->
<!-- &lt;!&ndash; <el-button size="small">输入格式</el-button>&ndash;&gt;-->
<!-- &lt;!&ndash; <el-button size="small">输出格式</el-button>&ndash;&gt;-->
<!-- &lt;!&ndash; </div>&ndash;&gt;-->
<!-- </div>-->
</div>
</el-tab-pane>
<!-- ///////////////////////////////////////////////////////////// -->
<el-tab-pane label="发送" name="third">
<div class="flex flex-col gap-2">
<el-input v-model="store.textPrefixValue" placeholder="支持\n\x" clearable>
<template #prepend>
{{ `添加帧头►` }}
</template>
</el-input>
<el-input v-model="store.textSuffixValue" placeholder="支持\n\x" clearable>
<template #append>
添加帧尾
</template>
</el-input>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
import {useDataViewerStore} from "@/stores/dataViewerStore";
import {globalNotify} from "@/composables/notification";
const store = useDataViewerStore()
const collapseActiveName = ref(["1", "2"])
const uartCustomBaud = ref(9600)
const uartDataBitsOptions = [
{
key: 5,
label: "5 bits",
}, {
key: 6,
label: "6 bits",
}, {
key: 7,
label: "7 bits",
}, {
key: 8,
label: "8 bits",
}
]
const uartParityOptions = [
{
key: 0,
label: "无none",
}, {
key: 1,
label: "奇odd",
}, {
key: 2,
label: "偶even",
}
]
const uartStopBitsOptions = [
{
key: 1,
label: "1",
}, {
key: 15,
label: "1.5",
}, {
key: 2,
label: "2",
}
]
const onUseCustomUartBaud = () => {
if (uartCustomBaud.value) {
store.uartBaud = uartCustomBaud.value;
} else {
globalNotify("波特率格式错误", "warning")
}
}
</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 */
}
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<p></p>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,376 @@
<template>
<div class="flex min-h-7 overflow-auto">
<el-popover
placement="bottom"
trigger="click"
:hide-after="0"
transition="none"
width="300"
>
<div v-if="!store.configPanelShow" class="h-[40vh] overflow-auto">
<text-data-config></text-data-config>
</div>
<template #reference>
<el-link v-show="!store.configPanelShow" 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="强制滚动至底部" border/>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>仅清除显示区域可用刷新恢复</p>
</template>
<el-button size="small" @click="store.clearFilteredBuff">
<InlineSvg class="h-5" name="trash"></InlineSvg>
清屏
</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="light"
placement="top"
>
<template #content>
<p>与缓存同步</p>
</template>
<el-button size="small" @click="store.refreshFilteredBuff">
刷新
</el-button>
</el-tooltip>
<!-- <el-checkbox class="hover:bg-blue-200" size="small" v-model="store.pauseAutoRefresh" label="暂停数据刷新" border/>-->
</div>
<div class="flex">
<el-button size="small" @click="addItem(1)">add1</el-button>
<el-button size="small" @click="addItem(10)">add10</el-button>
<el-button size="small" @click="addItem(100)">add100</el-button>
<el-button size="small" @click="addItem(1000)">add1000</el-button>
<el-button size="small" @click="scrollToBottom">scrollToBottom</el-button>
<!-- <el-button @click="toggleAutoBottom">autoBot: {{ forceToBottom }}</el-button>-->
<!-- <el-checkbox size="small" v-model="store.forceToBottom" :label="'autoBot:' + store.forceToBottom" border></el-checkbox>-->
<!-- <el-button>{{ count }}, {{ items.length }}, {{ vuetifyVirtualScrollBarRef.scrollTop }},-->
<!-- {{ vuetifyVirtualScrollBarRef.clientHeight }}, {{ vuetifyVirtualScrollBarRef.scrollHeight }}-->
<!-- </el-button>-->
<!-- <el-button @click="updateScroll">{{ scrollTop }}, {{ clientHeight }}, {{ scrollHeight }}</el-button>-->
</div>
<!-- <div>-->
<!-- <el-popover-->
<!-- placement="bottom"-->
<!-- trigger="click"-->
<!-- :hide-after="0"-->
<!-- transition="none"-->
<!-- width="300"-->
<!-- >-->
<!-- <div id="quick-access-panel-tp" class="bg-amber-200"></div>-->
<!-- <template #reference>-->
<!-- <el-link class="content-center" v-show="!store.quickAccessPanelShow">-->
<!-- <InlineSvg name="arrow_drop_down" class="h-7"></InlineSvg>-->
<!-- 666-->
<!-- </el-link>-->
<!-- </template>-->
<!-- </el-popover>-->
<!-- </div>-->
</div>
<div class="flex flex-grow overflow-hidden border-2 scroll-m-2">
<v-virtual-scroll
: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>
<div class="flex">
<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 type="primary" v-show="store.showTimestamp"><span>{{ item.time }}</span>TX-|</p>
<p v-show="store.showText"
v-html="item.str"></p>
</div>
<div class="flex text-wrap">
<p v-show="store.showHex" class="">{{ item.hex }}</p>
</div>
<div class="flex">
<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 min-h-6 flex gap-2 justify-between overflow-y-scroll">
<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>
<div class="flex align-center">
<el-checkbox v-model="enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
循环发送(ms)
</el-checkbox>
<el-input-number
v-model="loopSendFreq"
class="h-5"
size="small"
:step="10"
>
</el-input-number>
</div>
<el-link @click="isSendTextFormat = !isSendTextFormat">
<el-tag class="font-mono font-bold" size="small">发送格式{{ isSendTextFormat ? "文本" : "HEX" }}</el-tag>
</el-link>
</div>
<div class="flex gap-2">
<el-link @click="showTxTotalByte = !showTxTotalByte">
<el-tag class="font-mono font-bold" size="small">
{{ showTxTotalByte ? `TX统计:${store.TxTotalByteCount}B`: `上个TX帧:${store.TxByteCount}B` }}
</el-tag>
</el-link>
<el-link type="success" @click="showRxTotalByte = !showRxTotalByte">
<el-tag class="font-mono font-bold" size="small" type="success">
{{ showRxTotalByte ? `RX统计:${store.RxTotalByteCount}B`: `上个RX帧:${store.RxByteCount}B` }}
</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">缓存帧数: {{ store.dataBufLength }}/20000</span>
</el-tag>
</div>
</div>
</div>
<div class="flex flex-row font-mono">
<el-input type="textarea" :autosize="{ minRows: 1, maxRows: 6}" v-model="uartInputTextBox" clearable
:placeholder="isSendTextFormat ?
'输入文本,支持\\n\\x转义' :
'输入HEX格式'"
@keydown="handleTextboxKeydown"
></el-input>
<el-tooltip content="Ctrl+回车" placement="top" :auto-close="500">
<el-button type="primary"
@click="onSendClick">
{{ (isSendTextFormat || isHexStringValid) ? "发送" : "格式化" }}
</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";
const count = ref(0);
const showTxTotalByte = ref(false);
const showRxTotalByte = ref(false);
const vuetifyVirtualScrollRef = ref(document.body);
const vuetifyVirtualScrollBarRef = ref(document.body);
const vuetifyVirtualScrollContainerRef = ref(document.body);
const enableLoopSend = ref(false);
const loopSendFreq = ref(1000);
let loopSendIntervalID: number;
const isSendTextFormat = ref(true)
const isHexStringValid = ref(false);
const uartInputTextBox = ref("")
const store = useDataViewerStore();
const mutationObserver = new MutationObserver(() => {
if (store.forceToBottom) {
scrollToBottom();
}
});
onMounted(() => {
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)
}
})
onUnmounted(() => {
mutationObserver.disconnect();
});
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, true);
}
}
function scrollToBottom() {
nextTick(() => {
const scrollerElement = vuetifyVirtualScrollBarRef.value; // Adjust according to your setup
// scrollerElement.scrollTop = scrollerElement.scrollHeight;
scrollerElement.scrollTo(scrollerElement.scrollLeft, scrollerElement.scrollHeight);
});
}
function scrollToTop() {
nextTick(() => {
vuetifyVirtualScrollBarRef.value.scrollTop = vuetifyVirtualScrollBarRef.value.scrollHeight;
// vuetifyVirtualScrollBarRef.value.scrollTo(0, 0);
});
}
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() {
isHexStringValid.value = uartInputTextBox.value.toUpperCase() === formatHexInput(uartInputTextBox.value);
}
watch(isSendTextFormat, (value) => {
if (!value) {
checkHexTextValid()
}
});
watch(() => uartInputTextBox.value, (newValue) => {
if (!isSendTextFormat.value) {
checkHexTextValid()
}
})
watch(enableLoopSend, (newValue) => {
if (newValue) {
clearInterval(loopSendIntervalID);
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
} else {
clearInterval(loopSendIntervalID);
}
});
watch(loopSendFreq, (value) => {
if (enableLoopSend.value && value) {
/* update interval with new value */
clearInterval(loopSendIntervalID);
loopSendIntervalID = setInterval(onSendClick, loopSendFreq.value);
}
})
/* patch scroll container does not update clear filter */
watch(() => store.filterChanged, (value) => {
if (value) {
scrollToBottom();
scrollToTop()
store.filterChanged = false;
}
})
const handleScroll = (ev: Event) => {
if (store.forceToBottom) {
setTimeout(scrollToBottom, 0);
}
};
function clearSendInput() {
uartInputTextBox.value = ""
}
function handleTextboxKeydown(ev: KeyboardEvent) {
if (ev.ctrlKey && ev.key === 'Enter') {
onSendClick();
}
}
function onSendClick() {
if (isSendTextFormat.value) {
store.addString(uartInputTextBox.value, false, true);
} else if (!isHexStringValid.value) {
uartInputTextBox.value = formatHexInput(uartInputTextBox.value);
} else {
store.addHexString(uartInputTextBox.value, false, true);
}
}
</script>

View File

@ -7,7 +7,7 @@ import vue from '@vitejs/plugin-vue'
import svgLoader from "vite-svg-loader";
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
import { viteSingleFile } from 'vite-plugin-singlefile'
import vuetify from 'vite-plugin-vuetify'
// https://vitejs.dev/config/
export default ({mode}: ConfigEnv) => {
@ -25,6 +25,7 @@ export default ({mode}: ConfigEnv) => {
svgLoader(),
cssInjectedByJsPlugin(),
viteSingleFile(),
vuetify(),
],
define: {},
resolve: {