Adds msgPack and update message protocol

This commit is contained in:
Rune Harlyk
2025-07-10 16:18:09 +02:00
committed by Rune Harlyk
parent 01d46f283b
commit a43c250ed1
9 changed files with 485 additions and 321 deletions
+2
View File
@@ -48,11 +48,13 @@
"@niku/vite-env-caster": "^1.0.2", "@niku/vite-env-caster": "^1.0.2",
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-auto": "^4.0.0",
"@tailwindcss/vite": "^4.0.12", "@tailwindcss/vite": "^4.0.12",
"@types/msgpack-lite": "^0.1.11",
"chart.js": "^4.4.2", "chart.js": "^4.4.2",
"compare-versions": "^6.1.0", "compare-versions": "^6.1.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"daisyui": "^5.0.0", "daisyui": "^5.0.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",
"msgpack-lite": "^0.1.26",
"nipplejs": "^0.10.1", "nipplejs": "^0.10.1",
"svelte-dnd-list": "^0.1.8", "svelte-dnd-list": "^0.1.8",
"svelte-modals": "^2.0.0", "svelte-modals": "^2.0.0",
+91 -32
View File
@@ -13,10 +13,13 @@ importers:
version: 1.1.2 version: 1.1.2
'@sveltejs/adapter-auto': '@sveltejs/adapter-auto':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))) version: 4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.0.12 specifier: ^4.0.12
version: 4.0.12(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) version: 4.0.12(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@types/msgpack-lite':
specifier: ^0.1.11
version: 0.1.11
chart.js: chart.js:
specifier: ^4.4.2 specifier: ^4.4.2
version: 4.4.2 version: 4.4.2
@@ -32,6 +35,9 @@ importers:
jwt-decode: jwt-decode:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
msgpack-lite:
specifier: ^0.1.26
version: 0.1.26
nipplejs: nipplejs:
specifier: ^0.10.1 specifier: ^0.10.1
version: 0.10.1 version: 0.10.1
@@ -65,13 +71,13 @@ importers:
version: 1.49.1 version: 1.49.1
'@sveltejs/adapter-static': '@sveltejs/adapter-static':
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))) version: 3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
'@sveltejs/kit': '@sveltejs/kit':
specifier: ^2.5.27 specifier: ^2.5.27
version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) version: 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@types/eslint': '@types/eslint':
specifier: ^8.56.0 specifier: ^8.56.0
version: 8.56.0 version: 8.56.0
@@ -128,10 +134,10 @@ importers:
version: 0.18.5 version: 0.18.5
vite: vite:
specifier: ^6.2.1 specifier: ^6.2.1
version: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2) version: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vitest: vitest:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0(jsdom@24.0.0)(lightningcss@1.29.2) version: 1.2.0(@types/node@24.0.12)(jsdom@24.0.0)(lightningcss@1.29.2)
packages: packages:
@@ -760,6 +766,12 @@ packages:
'@types/json-schema@7.0.15': '@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/msgpack-lite@0.1.11':
resolution: {integrity: sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ==}
'@types/node@24.0.12':
resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==}
'@types/semver@7.5.8': '@types/semver@7.5.8':
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
@@ -1190,6 +1202,9 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
event-lite@0.1.3:
resolution: {integrity: sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==}
execa@5.1.1: execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1335,6 +1350,9 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.1: ignore@5.3.1:
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@@ -1356,6 +1374,9 @@ packages:
inherits@2.0.4: inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
int64-buffer@0.1.10:
resolution: {integrity: sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==}
is-binary-path@2.1.0: is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -1394,6 +1415,9 @@ packages:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isexe@2.0.0: isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1592,6 +1616,10 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
msgpack-lite@0.1.26:
resolution: {integrity: sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==}
hasBin: true
nanoid@3.3.7: nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -2019,6 +2047,9 @@ packages:
ufo@1.5.3: ufo@1.5.3:
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
universalify@0.2.0: universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
@@ -2613,18 +2644,18 @@ snapshots:
'@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.27.8': {}
'@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))': '@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
dependencies: dependencies:
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) '@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
import-meta-resolve: 4.1.0 import-meta-resolve: 4.1.0
'@sveltejs/adapter-static@3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))': '@sveltejs/adapter-static@3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
dependencies: dependencies:
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) '@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))': '@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@types/cookie': 0.6.0 '@types/cookie': 0.6.0
cookie: 0.6.0 cookie: 0.6.0
devalue: 5.1.1 devalue: 5.1.1
@@ -2637,27 +2668,27 @@ snapshots:
set-cookie-parser: 2.6.0 set-cookie-parser: 2.6.0
sirv: 3.0.1 sirv: 3.0.1
svelte: 5.20.4 svelte: 5.20.4
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2) vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))': '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
debug: 4.4.0 debug: 4.4.0
svelte: 5.20.4 svelte: 5.20.4
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2) vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))': '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies: dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
debug: 4.4.0 debug: 4.4.0
deepmerge: 4.3.1 deepmerge: 4.3.1
kleur: 4.1.5 kleur: 4.1.5
magic-string: 0.30.17 magic-string: 0.30.17
svelte: 5.20.4 svelte: 5.20.4
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2) vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vitefu: 1.0.6(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) vitefu: 1.0.6(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -2714,13 +2745,13 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.0.12 '@tailwindcss/oxide-win32-arm64-msvc': 4.0.12
'@tailwindcss/oxide-win32-x64-msvc': 4.0.12 '@tailwindcss/oxide-win32-x64-msvc': 4.0.12
'@tailwindcss/vite@4.0.12(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))': '@tailwindcss/vite@4.0.12(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies: dependencies:
'@tailwindcss/node': 4.0.12 '@tailwindcss/node': 4.0.12
'@tailwindcss/oxide': 4.0.12 '@tailwindcss/oxide': 4.0.12
lightningcss: 1.29.2 lightningcss: 1.29.2
tailwindcss: 4.0.12 tailwindcss: 4.0.12
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2) vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
'@tweenjs/tween.js@23.1.2': {} '@tweenjs/tween.js@23.1.2': {}
@@ -2737,6 +2768,14 @@ snapshots:
'@types/json-schema@7.0.15': {} '@types/json-schema@7.0.15': {}
'@types/msgpack-lite@0.1.11':
dependencies:
'@types/node': 24.0.12
'@types/node@24.0.12':
dependencies:
undici-types: 7.8.0
'@types/semver@7.5.8': {} '@types/semver@7.5.8': {}
'@types/stats.js@0.17.3': {} '@types/stats.js@0.17.3': {}
@@ -3259,6 +3298,8 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
event-lite@0.1.3: {}
execa@5.1.1: execa@5.1.1:
dependencies: dependencies:
cross-spawn: 7.0.3 cross-spawn: 7.0.3
@@ -3414,6 +3455,8 @@ snapshots:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.1: {} ignore@5.3.1: {}
import-fresh@3.3.0: import-fresh@3.3.0:
@@ -3432,6 +3475,8 @@ snapshots:
inherits@2.0.4: {} inherits@2.0.4: {}
int64-buffer@0.1.10: {}
is-binary-path@2.1.0: is-binary-path@2.1.0:
dependencies: dependencies:
binary-extensions: 2.3.0 binary-extensions: 2.3.0
@@ -3458,6 +3503,8 @@ snapshots:
is-stream@3.0.0: {} is-stream@3.0.0: {}
isarray@1.0.0: {}
isexe@2.0.0: {} isexe@2.0.0: {}
jiti@2.4.2: {} jiti@2.4.2: {}
@@ -3635,6 +3682,13 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
msgpack-lite@0.1.26:
dependencies:
event-lite: 0.1.3
ieee754: 1.2.1
int64-buffer: 0.1.10
isarray: 1.0.0
nanoid@3.3.7: {} nanoid@3.3.7: {}
nanoid@3.3.8: {} nanoid@3.3.8: {}
@@ -4010,6 +4064,8 @@ snapshots:
ufo@1.5.3: {} ufo@1.5.3: {}
undici-types@7.8.0: {}
universalify@0.2.0: {} universalify@0.2.0: {}
unplugin-icons@0.18.5: unplugin-icons@0.18.5:
@@ -4054,13 +4110,13 @@ snapshots:
uzip@0.20201231.0: {} uzip@0.20201231.0: {}
vite-node@1.2.0(lightningcss@1.29.2): vite-node@1.2.0(@types/node@24.0.12)(lightningcss@1.29.2):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
debug: 4.4.0 debug: 4.4.0
pathe: 1.1.2 pathe: 1.1.2
picocolors: 1.0.1 picocolors: 1.0.1
vite: 5.4.14(lightningcss@1.29.2) vite: 5.4.14(@types/node@24.0.12)(lightningcss@1.29.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- less - less
@@ -4072,31 +4128,33 @@ snapshots:
- supports-color - supports-color
- terser - terser
vite@5.4.14(lightningcss@1.29.2): vite@5.4.14(@types/node@24.0.12)(lightningcss@1.29.2):
dependencies: dependencies:
esbuild: 0.21.5 esbuild: 0.21.5
postcss: 8.5.3 postcss: 8.5.3
rollup: 4.34.8 rollup: 4.34.8
optionalDependencies: optionalDependencies:
'@types/node': 24.0.12
fsevents: 2.3.3 fsevents: 2.3.3
lightningcss: 1.29.2 lightningcss: 1.29.2
vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2): vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2):
dependencies: dependencies:
esbuild: 0.25.0 esbuild: 0.25.0
postcss: 8.5.3 postcss: 8.5.3
rollup: 4.34.8 rollup: 4.34.8
optionalDependencies: optionalDependencies:
'@types/node': 24.0.12
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.4.2 jiti: 2.4.2
lightningcss: 1.29.2 lightningcss: 1.29.2
yaml: 2.4.2 yaml: 2.4.2
vitefu@1.0.6(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)): vitefu@1.0.6(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)):
optionalDependencies: optionalDependencies:
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2) vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vitest@1.2.0(jsdom@24.0.0)(lightningcss@1.29.2): vitest@1.2.0(@types/node@24.0.12)(jsdom@24.0.0)(lightningcss@1.29.2):
dependencies: dependencies:
'@vitest/expect': 1.2.0 '@vitest/expect': 1.2.0
'@vitest/runner': 1.2.0 '@vitest/runner': 1.2.0
@@ -4116,10 +4174,11 @@ snapshots:
strip-literal: 1.3.0 strip-literal: 1.3.0
tinybench: 2.8.0 tinybench: 2.8.0
tinypool: 0.8.4 tinypool: 0.8.4
vite: 5.4.14(lightningcss@1.29.2) vite: 5.4.14(@types/node@24.0.12)(lightningcss@1.29.2)
vite-node: 1.2.0(lightningcss@1.29.2) vite-node: 1.2.0(@types/node@24.0.12)(lightningcss@1.29.2)
why-is-node-running: 2.2.2 why-is-node-running: 2.2.2
optionalDependencies: optionalDependencies:
'@types/node': 24.0.12
jsdom: 24.0.0 jsdom: 24.0.0
transitivePeerDependencies: transitivePeerDependencies:
- less - less
+138 -100
View File
@@ -1,122 +1,160 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import msgpack from 'msgpack-lite';
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const; const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
type SocketEvent = (typeof socketEvents)[number]; type SocketEvent = (typeof socketEvents)[number];
type SocketMessage = [number, string?, unknown?];
let useBinary = false;
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
useBinary = data instanceof ArrayBuffer;
try {
if (useBinary) {
return msgpack.decode(new Uint8Array(data as ArrayBuffer));
}
return JSON.parse(data as string);
} catch (error) {
console.error(`Could not decode data: ${data} - ${error}`);
}
return null;
};
const encodeMessage = (data: unknown) => {
try {
return useBinary ? msgpack.encode(data) : JSON.stringify(data);
} catch (error) {
console.error(`Could not encode data: ${data} - ${error}`);
}
};
function createWebSocket() { function createWebSocket() {
let listeners = new Map<string, Set<(data?: unknown) => void>>(); const listeners = new Map<string, Set<(data?: unknown) => void>>();
const { subscribe, set } = writable(false); const { subscribe, set } = writable(false);
const reconnectTimeoutTime = 5000; const reconnectTimeoutTime = 5000;
let unresponsiveTimeoutId: number; let unresponsiveTimeoutId: ReturnType<typeof setTimeout>;
let reconnectTimeoutId: number; let reconnectTimeoutId: ReturnType<typeof setTimeout>;
let ws: WebSocket; let ws: WebSocket;
let socketUrl: string | URL; let socketUrl: string | URL;
function init(url: string | URL) { function init(url: string | URL) {
socketUrl = url; socketUrl = url;
connect(); connect();
} }
function disconnect(reason: SocketEvent, event?: Event) { function disconnect(reason: SocketEvent, event?: Event) {
ws.close(); ws.close();
set(false); set(false);
clearTimeout(unresponsiveTimeoutId); clearTimeout(unresponsiveTimeoutId);
clearTimeout(reconnectTimeoutId); clearTimeout(reconnectTimeoutId);
listeners.get(reason)?.forEach((listener) => listener(event)); listeners.get(reason)?.forEach(listener => listener(event));
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime); reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
} }
function connect() { function connect() {
ws = new WebSocket(socketUrl); ws = new WebSocket(socketUrl);
ws.onopen = (ev) => { ws.binaryType = 'arraybuffer';
set(true); ws.onopen = ev => {
clearTimeout(reconnectTimeoutId); ping();
listeners.get('open')?.forEach((listener) => listener(ev)); useBinary = true;
for (const event of listeners.keys()) { ping();
if (socketEvents.includes(event as SocketEvent)) continue; set(true);
subscribeToEvent(event); clearTimeout(reconnectTimeoutId);
} listeners.get('open')?.forEach(listener => listener(ev));
}; for (const event of listeners.keys()) {
ws.onmessage = (message) => { if (socketEvents.includes(event as SocketEvent)) continue;
resetUnresponsiveCheck(); subscribeToEvent(event);
let data = message.data; }
if (data instanceof ArrayBuffer) { };
listeners.get('binary')?.forEach((listener) => listener(data)); ws.onmessage = frame => {
return; resetUnresponsiveCheck();
} const message = decodeMessage(frame.data);
data = data.substring(1); if (!message) return;
const [_, event, payload = undefined] = message;
if (event) listeners.get(event)?.forEach(listener => listener(payload));
};
ws.onerror = ev => disconnect('error', ev);
ws.onclose = ev => disconnect('close', ev);
}
if (!data) return; function unsubscribe(event: string, listener?: (data: unknown) => void) {
const eventListeners = listeners.get(event);
if (!eventListeners) return;
let event = data.substring(data.indexOf('/') + 1, data.indexOf('[')); if (!eventListeners.size) {
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']')); unsubscribeToEvent(event);
}
if (listener) {
eventListeners?.delete(listener);
} else {
listeners.delete(event);
}
}
try { function resetUnresponsiveCheck() {
payload = JSON.parse(payload); clearTimeout(unresponsiveTimeoutId);
} catch (error) {} unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
if (event) listeners.get(event)?.forEach((listener) => listener(payload)); }
};
ws.onerror = (ev) => disconnect('error', ev);
ws.onclose = (ev) => disconnect('close', ev);
}
function unsubscribe(event: string, listener?: (data: any) => void) { function sendEvent(event: string, data: unknown) {
let eventListeners = listeners.get(event); if (!ws || ws.readyState !== WebSocket.OPEN) return;
if (!eventListeners) return; send([2, event, data]);
}
if (!eventListeners.size) { function unsubscribeToEvent(event: string) {
unsubscribeToEvent(event); if (!ws || ws.readyState !== WebSocket.OPEN) return;
} send([1, event]);
if (listener) { }
eventListeners?.delete(listener);
} else {
listeners.delete(event);
}
}
function resetUnresponsiveCheck() { function subscribeToEvent(event: string) {
clearTimeout(unresponsiveTimeoutId); if (!ws || ws.readyState !== WebSocket.OPEN) return;
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime); send([0, event]);
} }
function sendEvent(event: string, data: unknown) { function send(data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(`2/${event}[${JSON.stringify(data)}]`); const serialized = encodeMessage(data);
} if (!serialized) {
console.error('Could not serialize data:', data);
return;
}
ws.send(serialized);
}
function unsubscribeToEvent(event: string) { function ping() {
if (!ws || ws.readyState !== WebSocket.OPEN) return; const serialized = encodeMessage([4]);
ws.send('1/' + event); if (!serialized) {
} console.error('Could not serialize message');
return;
}
ws.send(serialized);
}
function subscribeToEvent(event: string) { return {
if (!ws || ws.readyState !== WebSocket.OPEN) return; subscribe,
ws.send('0/' + event); sendEvent,
} init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event);
if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event);
}
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: unknown) => void);
return { return () => {
subscribe, unsubscribe(event, listener as (data: unknown) => void);
sendEvent, };
init, },
on: <T>(event: string, listener: (data: T) => void): (() => void) => { off: <T>(event: string, listener?: (data: T) => void) => {
let eventListeners = listeners.get(event); unsubscribe(event, listener as (data: unknown) => void);
if (!eventListeners) { },
if (!socketEvents.includes(event as SocketEvent)) { };
subscribeToEvent(event);
}
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: any) => void);
return () => {
unsubscribe(event, listener);
};
},
off: (event: string, listener?: (data: any) => void) => {
unsubscribe(event, listener);
}
};
} }
export const socket = createWebSocket(); export const socket = createWebSocket();
+96 -96
View File
@@ -1,125 +1,125 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { page } from '$app/state'; import { page } from '$app/state';
import { Modals, modals } from 'svelte-modals'; import { Modals, modals } from 'svelte-modals';
import Toast from '$lib/components/toasts/Toast.svelte'; import Toast from '$lib/components/toasts/Toast.svelte';
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import '../app.css'; import '../app.css';
import Menu from '../lib/components/menu/Menu.svelte'; import Menu from '../lib/components/menu/Menu.svelte';
import Statusbar from '../lib/components/statusbar/statusbar.svelte'; import Statusbar from '../lib/components/statusbar/statusbar.svelte';
import { import {
telemetry, telemetry,
analytics, analytics,
ModesEnum, ModesEnum,
kinematicData, kinematicData,
mode, mode,
outControllerData, outControllerData,
servoAngles, servoAngles,
servoAnglesOut, servoAnglesOut,
socket, socket,
location, location,
useFeatureFlags useFeatureFlags,
} from '$lib/stores'; } from '$lib/stores';
import type { Analytics, DownloadOTA } from '$lib/types/models'; import type { Analytics, DownloadOTA } from '$lib/types/models';
interface Props { interface Props {
children?: import('svelte').Snippet; children?: import('svelte').Snippet;
} }
let { children }: Props = $props(); let { children }: Props = $props();
const features = useFeatureFlags(); const features = useFeatureFlags();
onMount(async () => { onMount(async () => {
const ws = $location ? $location : window.location.host; const ws = $location ? $location : window.location.host;
socket.init(`ws://${ws}/api/ws/events`); socket.init(`ws://${ws}/api/ws/events`);
addEventListeners(); addEventListeners();
outControllerData.subscribe(data => socket.sendEvent('input', { data })); outControllerData.subscribe(data => socket.sendEvent('input', data));
mode.subscribe(data => socket.sendEvent('mode', { data })); mode.subscribe(data => socket.sendEvent('mode', data));
servoAnglesOut.subscribe(data => socket.sendEvent('angles', { data })); servoAnglesOut.subscribe(data => socket.sendEvent('angles', data));
kinematicData.subscribe(data => socket.sendEvent('position', { data })); kinematicData.subscribe(data => socket.sendEvent('position', data));
});
onDestroy(() => {
removeEventListeners();
});
const addEventListeners = () => {
socket.on('open', handleOpen);
socket.on('close', handleClose);
socket.on('error', handleError);
socket.on('rssi', handleNetworkStatus);
socket.on('mode', (data: ModesEnum) => mode.set(data));
socket.on('analytics', handleAnalytics);
socket.on('angles', (angles: number[]) => {
if (angles.length) servoAngles.set(angles);
}); });
features.subscribe(data => {
onDestroy(() => { if (data?.download_firmware) socket.on('otastatus', handleOAT);
removeEventListeners(); if (data?.sonar) socket.on('sonar', data => console.log(data));
}); });
};
const addEventListeners = () => { const removeEventListeners = () => {
socket.on('open', handleOpen); socket.off('analytics', handleAnalytics);
socket.on('close', handleClose); socket.off('open', handleOpen);
socket.on('error', handleError); socket.off('close', handleClose);
socket.on('rssi', handleNetworkStatus); socket.off('rssi', handleNetworkStatus);
socket.on('mode', (data: ModesEnum) => mode.set(data)); socket.off('otastatus', handleOAT);
socket.on('analytics', handleAnalytics); };
socket.on('angles', (angles: number[]) => {
if (angles.length) servoAngles.set(angles);
});
features.subscribe(data => {
if (data?.download_firmware) socket.on('otastatus', handleOAT);
if (data?.sonar) socket.on('sonar', data => console.log(data));
});
};
const removeEventListeners = () => { const handleOpen = () => {
socket.off('analytics', handleAnalytics); notifications.success('Connection to device established', 5000);
socket.off('open', handleOpen); };
socket.off('close', handleClose);
socket.off('rssi', handleNetworkStatus);
socket.off('otastatus', handleOAT);
};
const handleOpen = () => { const handleClose = () => {
notifications.success('Connection to device established', 5000); notifications.error('Connection to device lost', 5000);
}; telemetry.setRSSI(0);
};
const handleClose = () => { const handleError = (data: any) => console.error(data);
notifications.error('Connection to device lost', 5000);
telemetry.setRSSI(0);
};
const handleError = (data: any) => console.error(data); const handleAnalytics = (data: Analytics) => analytics.addData(data);
const handleAnalytics = (data: Analytics) => analytics.addData(data); const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data); const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data); let menuOpen = $state(false);
let menuOpen = $state(false);
</script> </script>
<svelte:head> <svelte:head>
<title>{page.data.title}</title> <title>{page.data.title}</title>
</svelte:head> </svelte:head>
<div class="drawer"> <div class="drawer">
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} /> <input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
<div class="drawer-content flex flex-col"> <div class="drawer-content flex flex-col">
<!-- Status bar content here --> <!-- Status bar content here -->
<Statusbar /> <Statusbar />
<!-- Main page content here --> <!-- Main page content here -->
{@render children?.()} {@render children?.()}
</div> </div>
<!-- Side Navigation --> <!-- Side Navigation -->
<div class="drawer-side z-30 shadow-lg"> <div class="drawer-side z-30 shadow-lg">
<label for="main-menu" class="drawer-overlay"></label> <label for="main-menu" class="drawer-overlay"></label>
<Menu menuClicked={() => (menuOpen = false)} /> <Menu menuClicked={() => (menuOpen = false)} />
</div> </div>
</div> </div>
<Modals> <Modals>
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
{#snippet backdrop()} {#snippet backdrop()}
<div <div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm" class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
transition:fade transition:fade
onclick={modals.closeAll} onclick={modals.closeAll}>
></div> </div>
{/snippet} {/snippet}
</Modals> </Modals>
<Toast /> <Toast />
+18 -20
View File
@@ -1,28 +1,26 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte';
import { WiFi } from '$lib/components/icons'; import { WiFi } from '$lib/components/icons';
import { location, socket, useFeatureFlags } from '$lib/stores'; import { location, socket } from '$lib/stores';
const features = useFeatureFlags(); const update = async () => {
const ws = $location ? $location : window.location.host;
const update = () => { socket.init(`ws://${ws}/api/ws/events`);
const ws = $location ? $location : window.location.host; };
socket.init(`ws://${ws}/api/ws/events`);
};
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
{#snippet icon()} {#snippet icon()}
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> <WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<span >Connection</span> <span>Connection</span>
{/snippet} {/snippet}
<div class="flex"> <div class="flex">
<label class="label w-32" for="server">Address:</label> <label class="label w-32" for="server">Address:</label>
<input class="input" bind:value={$location} /> <input class="input" bind:value={$location} />
</div> </div>
<button class="btn btn-primary" onclick={update}>Update</button> <button class="btn btn-primary" onclick={update}>Update</button>
</SettingsCard> </SettingsCard>
+3
View File
@@ -6,3 +6,6 @@ build_flags =
-D SERVE_CONFIG_FILES -D SERVE_CONFIG_FILES
-D CORS_ORIGIN=\"*\" -D CORS_ORIGIN=\"*\"
-D ENABLE_CORS -D ENABLE_CORS
-D USE_MSGPACK=1 ; Use either msgpack or json
-D USE_JSON=0 ; Use either msgpack or json
+120 -69
View File
@@ -2,46 +2,6 @@
SemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex(); SemaphoreHandle_t clientSubscriptionsMutex = xSemaphoreCreateMutex();
message_type_t char_to_message_type(char c) {
switch (c) {
case '0': return CONNECT;
case '1': return DISCONNECT;
case '2': return EVENT;
case '3': return PING;
case '4': return PONG;
case '5': return BINARY_EVENT;
default: throw std::invalid_argument("Invalid message type");
}
}
const char *getEventName(const char *msg) {
const char *start = strchr(msg, '/');
if (!start) return nullptr;
start++;
const char *end = strchr(start, '[');
if (!end) return start;
static char eventName[32];
int len = end - start;
strncpy(eventName, start, len);
eventName[len] = '\0';
return eventName;
}
const char *getEventPayload(const char *msg) {
const char *start = strchr(msg + 2, '[');
const char *end = msg + strlen(msg) - 1;
if (*start == '[') {
start++;
}
int len = end - start;
if (len < 0) return nullptr;
char *payload = new char[len + 1];
strncpy(payload, start, len);
payload[len] = '\0';
return payload;
}
EventSocket::EventSocket() { EventSocket::EventSocket() {
_socket.onOpen((std::bind(&EventSocket::onWSOpen, this, std::placeholders::_1))); _socket.onOpen((std::bind(&EventSocket::onWSOpen, this, std::placeholders::_1)));
_socket.onClose(std::bind(&EventSocket::onWSClose, this, std::placeholders::_1)); _socket.onClose(std::bind(&EventSocket::onWSClose, this, std::placeholders::_1));
@@ -65,26 +25,51 @@ esp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame
ESP_LOGV("EventSocket", "ws[%s][%u] opcode[%d]", request->client()->remoteIP().toString().c_str(), ESP_LOGV("EventSocket", "ws[%s][%u] opcode[%d]", request->client()->remoteIP().toString().c_str(),
request->client()->socket(), frame->type); request->client()->socket(), frame->type);
if (frame->type != HTTPD_WS_TYPE_TEXT) { JsonDocument doc;
ESP_LOGE("EventSocket", "Unsupported frame type");
#if USE_MSGPACK
if (frame->type != HTTPD_WS_TYPE_BINARY) {
ESP_LOGE("EventSocket", "Unsupported frame type: %d", frame->type);
return ESP_OK; return ESP_OK;
} }
if (deserializeMsgPack(doc, frame->payload, frame->len)) {
ESP_LOGV("EventSocket", "Received message: %s", (char *)frame->payload); ESP_LOGE("EventSocket", "Could not deserialize msgpack");
char *msg = (char *)frame->payload;
message_type_t message_type = char_to_message_type(msg[0]);
if (message_type == PING) {
ESP_LOGV("EventSocket", "Ping");
request->client()->sendMessage("3");
return ESP_OK; return ESP_OK;
} else if (message_type == PONG) { };
#else
if (frame->type != HTTPD_WS_TYPE_TEXT) {
ESP_LOGE("EventSocket", "Unsupported frame type: %d", frame->type);
return ESP_OK;
}
if (deserializeJson(doc, frame->payload, frame->len)) {
ESP_LOGE("EventSocket", "Could not deserialize json");
return ESP_OK;
};
#endif
serializeJson(doc, Serial);
Serial.println();
auto msg = doc.as<JsonArray>();
message_type_t message_type = static_cast<message_type_t>(msg[0].as<uint8_t>());
if (message_type == PONG) {
ESP_LOGV("EventSocket", "Pong"); ESP_LOGV("EventSocket", "Pong");
return ESP_OK; return ESP_OK;
} else if (message_type == PING) {
ESP_LOGV("EventSocket", "Ping");
#if USE_MSGPACK
const uint8_t out[] = {0x91, 0x04};
send(request->client(), reinterpret_cast<const char *>(out), sizeof(out));
#else
const char *out = "[4]";
send(request->client(), out, strlen(out));
#endif
return ESP_OK;
} }
const char *event = getEventName(msg); const char *event = msg[1].as<const char *>();
if (!event) { if (!event) {
ESP_LOGE("EventSocket", "Invalid event name"); ESP_LOGE("EventSocket", "Invalid event name");
@@ -99,18 +84,7 @@ esp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame
ESP_LOGV("EventSocket", "Disconnect: %s", event); ESP_LOGV("EventSocket", "Disconnect: %s", event);
client_subscriptions[event].remove(request->client()->socket()); client_subscriptions[event].remove(request->client()->socket());
} else if (message_type == EVENT) { } else if (message_type == EVENT) {
const char *payload = getEventPayload(msg); JsonObject jsonObject = msg[2].as<JsonObject>();
if (!payload) {
ESP_LOGE("EventSocket", "Invalid event payload");
return ESP_OK;
}
JsonDocument doc;
DeserializationError error = deserializeJson(doc, payload);
if (error) {
ESP_LOGE("EventSocket", "Failed to parse JSON payload");
return ESP_OK;
}
JsonObject jsonObject = doc.as<JsonObject>();
handleEventCallbacks(event, jsonObject, request->client()->socket()); handleEventCallbacks(event, jsonObject, request->client()->socket());
return ESP_OK; return ESP_OK;
} }
@@ -127,8 +101,26 @@ void EventSocket::emit(const char *event, const char *payload, const char *origi
xSemaphoreGive(clientSubscriptionsMutex); xSemaphoreGive(clientSubscriptionsMutex);
return; return;
} }
char msg[strlen(event) + strlen(payload) + 10];
snprintf(msg, sizeof(msg), "2/%s[%s]", event, payload); JsonDocument doc;
auto a = doc.to<JsonArray>();
a.add(static_cast<uint8_t>(message_type_t::EVENT));
a.add(event);
JsonDocument payloadDoc;
if (deserializeJson(payloadDoc, payload) == DeserializationError::Ok)
a.add(payloadDoc.as<JsonVariant>());
else
a.add(payload); // fallback: insert as plain string if not valid JSON
String out;
#if USE_MSGPACK
serializeMsgPack(doc, out);
#else
serializeJson(doc, out);
#endif
const char *msg = out.c_str();
// if onlyToSameOrigin == true, send the message back to the origin // if onlyToSameOrigin == true, send the message back to the origin
if (onlyToSameOrigin && originSubscriptionId > 0) { if (onlyToSameOrigin && originSubscriptionId > 0) {
@@ -136,7 +128,7 @@ void EventSocket::emit(const char *event, const char *payload, const char *origi
if (client) { if (client) {
ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event, ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event,
client->remoteIP().toString().c_str(), msg); client->remoteIP().toString().c_str(), msg);
client->sendMessage(msg); send(client, msg, strlen(msg));
} }
} else { // else send the message to all other clients } else { // else send the message to all other clients
@@ -149,12 +141,71 @@ void EventSocket::emit(const char *event, const char *payload, const char *origi
} }
ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event, ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event,
client->remoteIP().toString().c_str(), msg); client->remoteIP().toString().c_str(), msg);
client->sendMessage(msg); send(client, msg, strlen(msg));
} }
} }
xSemaphoreGive(clientSubscriptionsMutex); xSemaphoreGive(clientSubscriptionsMutex);
} }
void EventSocket::emit(const char *event, JsonObject &payload, const char *originId, bool onlyToSameOrigin) {
int originSubscriptionId = originId[0] ? atoi(originId) : -1;
xSemaphoreTake(clientSubscriptionsMutex, portMAX_DELAY);
auto &subscriptions = client_subscriptions[event];
if (subscriptions.empty()) {
xSemaphoreGive(clientSubscriptionsMutex);
return;
}
JsonDocument doc;
auto a = doc.to<JsonArray>();
a.add(static_cast<uint8_t>(message_type_t::EVENT));
a.add(event);
a.add(payload);
String out;
#if USE_MSGPACK
serializeMsgPack(doc, out);
#else
serializeJson(doc, out);
#endif
const char *msg = out.c_str();
// if onlyToSameOrigin == true, send the message back to the origin
if (onlyToSameOrigin && originSubscriptionId > 0) {
auto *client = _socket.getClient(originSubscriptionId);
if (client) {
ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event,
client->remoteIP().toString().c_str(), msg);
send(client, msg, strlen(msg));
}
} else { // else send the message to all other clients
for (int subscription : client_subscriptions[event]) {
if (subscription == originSubscriptionId) continue;
auto *client = _socket.getClient(subscription);
if (!client) {
subscriptions.remove(subscription);
continue;
}
ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event,
client->remoteIP().toString().c_str(), msg);
send(client, msg, strlen(msg));
}
}
xSemaphoreGive(clientSubscriptionsMutex);
}
void EventSocket::send(PsychicWebSocketClient *client, const char *data, size_t len) {
if (!client) return;
#if USE_MSGPACK
client->sendMessage(HTTPD_WS_TYPE_BINARY, data, len);
#else
client->sendMessage(HTTPD_WS_TYPE_TEXT, data, len);
#endif
}
void EventSocket::handleEventCallbacks(String event, JsonObject &jsonObject, int originId) { void EventSocket::handleEventCallbacks(String event, JsonObject &jsonObject, int originId) {
for (auto &callback : event_callbacks[event]) { for (auto &callback : event_callbacks[event]) {
callback(jsonObject, originId); callback(jsonObject, originId);
+3 -2
View File
@@ -25,8 +25,8 @@ class EventSocket {
void onSubscribe(String event, SubscribeCallback callback); void onSubscribe(String event, SubscribeCallback callback);
void emit(const char *event, const char *payload, const char *originId = "", bool onlyToSameOrigin = false); void emit(const char *event, const char *payload, const char *originId = "", bool onlyToSameOrigin = false);
// if onlyToSameOrigin == true, the message will be sent to the originId only, otherwise it will be broadcasted to
// all clients except the originId void emit(const char *event, JsonObject &root, const char *originId = "", bool onlyToSameOrigin = false);
private: private:
PsychicWebSocketHandler _socket; PsychicWebSocketHandler _socket;
@@ -35,6 +35,7 @@ class EventSocket {
std::map<String, std::list<EventCallback>> event_callbacks; std::map<String, std::list<EventCallback>> event_callbacks;
std::map<String, std::list<SubscribeCallback>> subscribe_callbacks; std::map<String, std::list<SubscribeCallback>> subscribe_callbacks;
void handleEventCallbacks(String event, JsonObject &jsonObject, int originId); void handleEventCallbacks(String event, JsonObject &jsonObject, int originId);
void send(PsychicWebSocketClient *client, const char *data, size_t len);
void handleSubscribeCallbacks(String event, const String &originId); void handleSubscribeCallbacks(String event, const String &originId);
void onWSOpen(PsychicWebSocketClient *client); void onWSOpen(PsychicWebSocketClient *client);
+12
View File
@@ -67,6 +67,18 @@
#define USE_MDNS 1 #define USE_MDNS 1
#endif #endif
// ESP32 MSGPACK on by default
#ifndef USE_MSGPACK
#define USE_MSGPACK 1
#endif
// ESP32 JSON off by default
#ifndef USE_JSON
#define USE_JSON 0
#endif
static_assert(!(USE_JSON == 1 && USE_MSGPACK == 1), "Cannot set both USE_JSON and USE_MSGPACK to 1 simultaneously");
namespace feature_service { namespace feature_service {
void printFeatureConfiguration(); void printFeatureConfiguration();