From a43c250ed173dc175d964df14e1ff4adf9a91a29 Mon Sep 17 00:00:00 2001 From: Rune Harlyk Date: Thu, 10 Jul 2025 16:18:09 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Adds=20msgPack=20and=20update=20mes?= =?UTF-8?q?sage=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/package.json | 2 + app/pnpm-lock.yaml | 123 +++++++--- app/src/lib/stores/socket.ts | 240 ++++++++++++-------- app/src/routes/+layout.svelte | 192 ++++++++-------- app/src/routes/connection/Connection.svelte | 38 ++-- esp32/build_settings.ini | 5 +- esp32/lib/ESP32-sveltekit/event_socket.cpp | 189 +++++++++------ esp32/lib/ESP32-sveltekit/event_socket.h | 5 +- esp32/lib/ESP32-sveltekit/features.h | 12 + 9 files changed, 485 insertions(+), 321 deletions(-) diff --git a/app/package.json b/app/package.json index 0986b1a..70ff8b4 100644 --- a/app/package.json +++ b/app/package.json @@ -48,11 +48,13 @@ "@niku/vite-env-caster": "^1.0.2", "@sveltejs/adapter-auto": "^4.0.0", "@tailwindcss/vite": "^4.0.12", + "@types/msgpack-lite": "^0.1.11", "chart.js": "^4.4.2", "compare-versions": "^6.1.0", "cross-env": "^7.0.3", "daisyui": "^5.0.0", "jwt-decode": "^4.0.0", + "msgpack-lite": "^0.1.26", "nipplejs": "^0.10.1", "svelte-dnd-list": "^0.1.8", "svelte-modals": "^2.0.0", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 5b8d5f8..5576d58 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -13,10 +13,13 @@ importers: version: 1.1.2 '@sveltejs/adapter-auto': 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': 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: specifier: ^4.4.2 version: 4.4.2 @@ -32,6 +35,9 @@ importers: jwt-decode: specifier: ^4.0.0 version: 4.0.0 + msgpack-lite: + specifier: ^0.1.26 + version: 0.1.26 nipplejs: specifier: ^0.10.1 version: 0.10.1 @@ -65,13 +71,13 @@ importers: version: 1.49.1 '@sveltejs/adapter-static': 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': 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': 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': specifier: ^8.56.0 version: 8.56.0 @@ -128,10 +134,10 @@ importers: version: 0.18.5 vite: 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: 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: @@ -760,6 +766,12 @@ packages: '@types/json-schema@7.0.15': 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': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -1190,6 +1202,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + event-lite@0.1.3: + resolution: {integrity: sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==} + execa@5.1.1: resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} engines: {node: '>=10'} @@ -1335,6 +1350,9 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -1356,6 +1374,9 @@ packages: inherits@2.0.4: 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: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1394,6 +1415,9 @@ packages: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} 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: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1592,6 +1616,10 @@ packages: ms@2.1.3: 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: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2019,6 +2047,9 @@ packages: ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + universalify@0.2.0: resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} engines: {node: '>= 4.0.0'} @@ -2613,18 +2644,18 @@ snapshots: '@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: - '@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 - '@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: - '@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: - '@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 cookie: 0.6.0 devalue: 5.1.1 @@ -2637,27 +2668,27 @@ snapshots: set-cookie-parser: 2.6.0 sirv: 3.0.1 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: - '@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 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: - 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: - '@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 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: 5.20.4 - vite: 6.2.1(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)) + 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(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)) transitivePeerDependencies: - supports-color @@ -2714,13 +2745,13 @@ snapshots: '@tailwindcss/oxide-win32-arm64-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: '@tailwindcss/node': 4.0.12 '@tailwindcss/oxide': 4.0.12 lightningcss: 1.29.2 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': {} @@ -2737,6 +2768,14 @@ snapshots: '@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/stats.js@0.17.3': {} @@ -3259,6 +3298,8 @@ snapshots: esutils@2.0.3: {} + event-lite@0.1.3: {} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 @@ -3414,6 +3455,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + ieee754@1.2.1: {} + ignore@5.3.1: {} import-fresh@3.3.0: @@ -3432,6 +3475,8 @@ snapshots: inherits@2.0.4: {} + int64-buffer@0.1.10: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -3458,6 +3503,8 @@ snapshots: is-stream@3.0.0: {} + isarray@1.0.0: {} + isexe@2.0.0: {} jiti@2.4.2: {} @@ -3635,6 +3682,13 @@ snapshots: 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.8: {} @@ -4010,6 +4064,8 @@ snapshots: ufo@1.5.3: {} + undici-types@7.8.0: {} + universalify@0.2.0: {} unplugin-icons@0.18.5: @@ -4054,13 +4110,13 @@ snapshots: 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: cac: 6.7.14 debug: 4.4.0 pathe: 1.1.2 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: - '@types/node' - less @@ -4072,31 +4128,33 @@ snapshots: - supports-color - terser - vite@5.4.14(lightningcss@1.29.2): + vite@5.4.14(@types/node@24.0.12)(lightningcss@1.29.2): dependencies: esbuild: 0.21.5 postcss: 8.5.3 rollup: 4.34.8 optionalDependencies: + '@types/node': 24.0.12 fsevents: 2.3.3 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: esbuild: 0.25.0 postcss: 8.5.3 rollup: 4.34.8 optionalDependencies: + '@types/node': 24.0.12 fsevents: 2.3.3 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)): 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: '@vitest/expect': 1.2.0 '@vitest/runner': 1.2.0 @@ -4116,10 +4174,11 @@ snapshots: strip-literal: 1.3.0 tinybench: 2.8.0 tinypool: 0.8.4 - vite: 5.4.14(lightningcss@1.29.2) - vite-node: 1.2.0(lightningcss@1.29.2) + vite: 5.4.14(@types/node@24.0.12)(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 optionalDependencies: + '@types/node': 24.0.12 jsdom: 24.0.0 transitivePeerDependencies: - less diff --git a/app/src/lib/stores/socket.ts b/app/src/lib/stores/socket.ts index acf07ba..aeb4623 100644 --- a/app/src/lib/stores/socket.ts +++ b/app/src/lib/stores/socket.ts @@ -1,122 +1,160 @@ import { writable } from 'svelte/store'; +import msgpack from 'msgpack-lite'; const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const; 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() { - let listeners = new Map void>>(); - const { subscribe, set } = writable(false); - const reconnectTimeoutTime = 5000; - let unresponsiveTimeoutId: number; - let reconnectTimeoutId: number; - let ws: WebSocket; - let socketUrl: string | URL; + const listeners = new Map void>>(); + const { subscribe, set } = writable(false); + const reconnectTimeoutTime = 5000; + let unresponsiveTimeoutId: ReturnType; + let reconnectTimeoutId: ReturnType; + let ws: WebSocket; + let socketUrl: string | URL; - function init(url: string | URL) { - socketUrl = url; - connect(); - } + function init(url: string | URL) { + socketUrl = url; + connect(); + } - function disconnect(reason: SocketEvent, event?: Event) { - ws.close(); - set(false); - clearTimeout(unresponsiveTimeoutId); - clearTimeout(reconnectTimeoutId); - listeners.get(reason)?.forEach((listener) => listener(event)); - reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime); - } + function disconnect(reason: SocketEvent, event?: Event) { + ws.close(); + set(false); + clearTimeout(unresponsiveTimeoutId); + clearTimeout(reconnectTimeoutId); + listeners.get(reason)?.forEach(listener => listener(event)); + reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime); + } - function connect() { - ws = new WebSocket(socketUrl); - ws.onopen = (ev) => { - set(true); - clearTimeout(reconnectTimeoutId); - listeners.get('open')?.forEach((listener) => listener(ev)); - for (const event of listeners.keys()) { - if (socketEvents.includes(event as SocketEvent)) continue; - subscribeToEvent(event); - } - }; - ws.onmessage = (message) => { - resetUnresponsiveCheck(); - let data = message.data; - if (data instanceof ArrayBuffer) { - listeners.get('binary')?.forEach((listener) => listener(data)); - return; - } - data = data.substring(1); + function connect() { + ws = new WebSocket(socketUrl); + ws.binaryType = 'arraybuffer'; + ws.onopen = ev => { + ping(); + useBinary = true; + ping(); + set(true); + clearTimeout(reconnectTimeoutId); + listeners.get('open')?.forEach(listener => listener(ev)); + for (const event of listeners.keys()) { + if (socketEvents.includes(event as SocketEvent)) continue; + subscribeToEvent(event); + } + }; + ws.onmessage = frame => { + resetUnresponsiveCheck(); + const message = decodeMessage(frame.data); + 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('[')); - let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']')); + if (!eventListeners.size) { + unsubscribeToEvent(event); + } + if (listener) { + eventListeners?.delete(listener); + } else { + listeners.delete(event); + } + } - try { - payload = JSON.parse(payload); - } catch (error) {} - if (event) listeners.get(event)?.forEach((listener) => listener(payload)); - }; - ws.onerror = (ev) => disconnect('error', ev); - ws.onclose = (ev) => disconnect('close', ev); - } + function resetUnresponsiveCheck() { + clearTimeout(unresponsiveTimeoutId); + unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime); + } - function unsubscribe(event: string, listener?: (data: any) => void) { - let eventListeners = listeners.get(event); - if (!eventListeners) return; + function sendEvent(event: string, data: unknown) { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + send([2, event, data]); + } - if (!eventListeners.size) { - unsubscribeToEvent(event); - } - if (listener) { - eventListeners?.delete(listener); - } else { - listeners.delete(event); - } - } + function unsubscribeToEvent(event: string) { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + send([1, event]); + } - function resetUnresponsiveCheck() { - clearTimeout(unresponsiveTimeoutId); - unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime); - } + function subscribeToEvent(event: string) { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + send([0, event]); + } - function sendEvent(event: string, data: unknown) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - ws.send(`2/${event}[${JSON.stringify(data)}]`); - } + function send(data: unknown) { + if (!ws || ws.readyState !== WebSocket.OPEN) return; + const serialized = encodeMessage(data); + if (!serialized) { + console.error('Could not serialize data:', data); + return; + } + ws.send(serialized); + } - function unsubscribeToEvent(event: string) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - ws.send('1/' + event); - } + function ping() { + const serialized = encodeMessage([4]); + if (!serialized) { + console.error('Could not serialize message'); + return; + } + ws.send(serialized); + } - function subscribeToEvent(event: string) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - ws.send('0/' + event); - } + return { + subscribe, + sendEvent, + init, + on: (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 { - subscribe, - sendEvent, - init, - on: (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: any) => void); - - return () => { - unsubscribe(event, listener); - }; - }, - off: (event: string, listener?: (data: any) => void) => { - unsubscribe(event, listener); - } - }; + return () => { + unsubscribe(event, listener as (data: unknown) => void); + }; + }, + off: (event: string, listener?: (data: T) => void) => { + unsubscribe(event, listener as (data: unknown) => void); + }, + }; } -export const socket = createWebSocket(); \ No newline at end of file +export const socket = createWebSocket(); diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte index 5220fbe..8640690 100644 --- a/app/src/routes/+layout.svelte +++ b/app/src/routes/+layout.svelte @@ -1,125 +1,125 @@ - {page.data.title} + {page.data.title}
- -
- - + +
+ + - - {@render children?.()} -
- -
- - (menuOpen = false)} /> -
+ + {@render children?.()} +
+ +
+ + (menuOpen = false)} /> +
- - - {#snippet backdrop()} -
- {/snippet} + + + {#snippet backdrop()} +
+
+ {/snippet}
diff --git a/app/src/routes/connection/Connection.svelte b/app/src/routes/connection/Connection.svelte index 26c2ab9..0d0fc8d 100644 --- a/app/src/routes/connection/Connection.svelte +++ b/app/src/routes/connection/Connection.svelte @@ -1,28 +1,26 @@ - {#snippet icon()} - - {/snippet} - {#snippet title()} - Connection - {/snippet} + {#snippet icon()} + + {/snippet} + {#snippet title()} + Connection + {/snippet} -
- - -
+
+ + +
- +
diff --git a/esp32/build_settings.ini b/esp32/build_settings.ini index 65f1cb0..f76665b 100644 --- a/esp32/build_settings.ini +++ b/esp32/build_settings.ini @@ -5,4 +5,7 @@ build_flags = -D EMBED_WWW -D SERVE_CONFIG_FILES -D CORS_ORIGIN=\"*\" - -D ENABLE_CORS \ No newline at end of file + -D ENABLE_CORS + + -D USE_MSGPACK=1 ; Use either msgpack or json + -D USE_JSON=0 ; Use either msgpack or json \ No newline at end of file diff --git a/esp32/lib/ESP32-sveltekit/event_socket.cpp b/esp32/lib/ESP32-sveltekit/event_socket.cpp index ee58ca8..2d1ae23 100644 --- a/esp32/lib/ESP32-sveltekit/event_socket.cpp +++ b/esp32/lib/ESP32-sveltekit/event_socket.cpp @@ -2,46 +2,6 @@ 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() { _socket.onOpen((std::bind(&EventSocket::onWSOpen, 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(), request->client()->socket(), frame->type); - if (frame->type != HTTPD_WS_TYPE_TEXT) { - ESP_LOGE("EventSocket", "Unsupported frame type"); + JsonDocument doc; + +#if USE_MSGPACK + if (frame->type != HTTPD_WS_TYPE_BINARY) { + ESP_LOGE("EventSocket", "Unsupported frame type: %d", frame->type); return ESP_OK; } - - ESP_LOGV("EventSocket", "Received message: %s", (char *)frame->payload); - 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"); + if (deserializeMsgPack(doc, frame->payload, frame->len)) { + ESP_LOGE("EventSocket", "Could not deserialize msgpack"); 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(); + + message_type_t message_type = static_cast(msg[0].as()); + + if (message_type == PONG) { ESP_LOGV("EventSocket", "Pong"); 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(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(); if (!event) { 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); client_subscriptions[event].remove(request->client()->socket()); } else if (message_type == EVENT) { - const char *payload = getEventPayload(msg); - 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 jsonObject = msg[2].as(); handleEventCallbacks(event, jsonObject, request->client()->socket()); return ESP_OK; } @@ -127,8 +101,26 @@ void EventSocket::emit(const char *event, const char *payload, const char *origi xSemaphoreGive(clientSubscriptionsMutex); return; } - char msg[strlen(event) + strlen(payload) + 10]; - snprintf(msg, sizeof(msg), "2/%s[%s]", event, payload); + + JsonDocument doc; + auto a = doc.to(); + a.add(static_cast(message_type_t::EVENT)); + a.add(event); + + JsonDocument payloadDoc; + if (deserializeJson(payloadDoc, payload) == DeserializationError::Ok) + a.add(payloadDoc.as()); + 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 && originSubscriptionId > 0) { @@ -136,7 +128,7 @@ void EventSocket::emit(const char *event, const char *payload, const char *origi if (client) { ESP_LOGV("EventSocket", "Emitting event: %s to %s, Message: %s", event, client->remoteIP().toString().c_str(), msg); - client->sendMessage(msg); + send(client, msg, strlen(msg)); } } 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, client->remoteIP().toString().c_str(), msg); - client->sendMessage(msg); + send(client, msg, strlen(msg)); } } 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(); + a.add(static_cast(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) { for (auto &callback : event_callbacks[event]) { callback(jsonObject, originId); diff --git a/esp32/lib/ESP32-sveltekit/event_socket.h b/esp32/lib/ESP32-sveltekit/event_socket.h index 6eb62c8..6c82008 100644 --- a/esp32/lib/ESP32-sveltekit/event_socket.h +++ b/esp32/lib/ESP32-sveltekit/event_socket.h @@ -25,8 +25,8 @@ class EventSocket { void onSubscribe(String event, SubscribeCallback callback); 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: PsychicWebSocketHandler _socket; @@ -35,6 +35,7 @@ class EventSocket { std::map> event_callbacks; std::map> subscribe_callbacks; 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 onWSOpen(PsychicWebSocketClient *client); diff --git a/esp32/lib/ESP32-sveltekit/features.h b/esp32/lib/ESP32-sveltekit/features.h index a98df9f..b77cbde 100644 --- a/esp32/lib/ESP32-sveltekit/features.h +++ b/esp32/lib/ESP32-sveltekit/features.h @@ -67,6 +67,18 @@ #define USE_MDNS 1 #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 { void printFeatureConfiguration();