From ce627ef7d493b4424b7b29d1fc598317e7b37151 Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Sat, 9 May 2026 09:50:28 +0200 Subject: [PATCH] add ap mode, modernize ui, split spoof code --- package-lock.json | 30 +- package.json | 3 +- pnpm-lock.yaml | 178 ++++++++ src/app.css | 545 ++++++++++++++++------- src/lib/spoof.ts | 191 ++++++++ src/lib/utils.ts | 6 +- src/main.cpp | 153 ++++--- src/routes/+page.svelte | 945 ++++++++++++++++++---------------------- src/secrets.example.h | 2 + 9 files changed, 1295 insertions(+), 758 deletions(-) create mode 100644 src/lib/spoof.ts diff --git a/package-lock.json b/package-lock.json index fe1951c..e773d6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "chart.js": "^4.5.0", "chartjs-adapter-date-fns": "^3.0.0", "chartjs-plugin-annotation": "^3.1.0", - "date-fns": "^4.1.0" + "date-fns": "^4.1.0", + "lucide-svelte": "^1.0.1" }, "devDependencies": { "@eslint/compat": "^1.2.5", @@ -736,7 +737,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -747,7 +747,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -758,7 +757,6 @@ "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, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -768,14 +766,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.30", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1124,7 +1120,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -1535,7 +1530,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1807,7 +1801,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1870,7 +1863,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -1880,7 +1872,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -2005,7 +1996,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -2378,7 +2368,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "dev": true, "license": "MIT" }, "node_modules/espree": { @@ -2416,7 +2405,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz", "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -2737,7 +2725,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" @@ -3088,7 +3075,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -3128,11 +3114,19 @@ "dev": true, "license": "MIT" }, + "node_modules/lucide-svelte": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-1.0.1.tgz", + "integrity": "sha512-WvzZgk0pqzgda+AErLvgWxHkfg/+GgUwqKMRHvzt0IqyMdmyEDzDCk3Z+Wo/3y753oIgx8u9Q4eUbWkghFa8Jg==", + "license": "ISC", + "peerDependencies": { + "svelte": "^3 || ^4 || ^5.0.0-next.42" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -3921,7 +3915,6 @@ "version": "5.38.8", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.8.tgz", "integrity": "sha512-UDpTbM/iuZ4MaMnn4ODB3rf5JKDyPOi5oJcopP0j7YHQ9BuJtsAqsR71r2N6AnJf7ygbalTJU5y8eSWGAQZjlQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -4372,7 +4365,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", - "dev": true, "license": "MIT" } } diff --git a/package.json b/package.json index 8744964..5bbd5f9 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "chart.js": "^4.5.0", "chartjs-adapter-date-fns": "^3.0.0", "chartjs-plugin-annotation": "^3.1.0", - "date-fns": "^4.1.0" + "date-fns": "^4.1.0", + "lucide-svelte": "^1.0.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7eb045..28243ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,25 @@ settings: importers: .: + dependencies: + '@cubejs-client/core': + specifier: ^1.3.67 + version: 1.6.44 + chart.js: + specifier: ^4.5.0 + version: 4.5.1 + chartjs-adapter-date-fns: + specifier: ^3.0.0 + version: 3.0.0(chart.js@4.5.1)(date-fns@4.1.0) + chartjs-plugin-annotation: + specifier: ^3.1.0 + version: 3.1.0(chart.js@4.5.1) + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + lucide-svelte: + specifier: ^1.0.1 + version: 1.0.1(svelte@5.38.8) devDependencies: '@eslint/compat': specifier: ^1.2.5 @@ -77,6 +96,9 @@ importers: packages: + '@cubejs-client/core@1.6.44': + resolution: {integrity: sha512-0J2i2xb4DQMdpZDnOnPUEz28fMgI04dbkePWxsGjCuCNcdr752f+iq9W1DZGzQ0JMdZfnSLmnaBrWWrcNMjATA==} + '@esbuild/aix-ppc64@0.25.9': resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} @@ -316,6 +338,9 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -694,6 +719,21 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chartjs-adapter-date-fns@3.0.0: + resolution: {integrity: sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg==} + peerDependencies: + chart.js: '>=2.8.0' + date-fns: '>=2.0.0' + + chartjs-plugin-annotation@3.1.0: + resolution: {integrity: sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==} + peerDependencies: + chart.js: '>=4.0.0' + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -720,6 +760,12 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + cross-fetch@3.2.0: + resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -729,6 +775,28 @@ packages: engines: {node: '>=4'} hasBin: true + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -920,6 +988,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1053,6 +1125,11 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lucide-svelte@1.0.1: + resolution: {integrity: sha512-WvzZgk0pqzgda+AErLvgWxHkfg/+GgUwqKMRHvzt0IqyMdmyEDzDCk3Z+Wo/3y753oIgx8u9Q4eUbWkghFa8Jg==} + peerDependencies: + svelte: ^3 || ^4 || ^5.0.0-next.42 + magic-string@0.30.19: resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} @@ -1107,6 +1184,15 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1261,6 +1347,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + ramda@0.27.2: + resolution: {integrity: sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA==} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -1361,6 +1450,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -1386,6 +1478,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-search-params-polyfill@7.0.1: + resolution: {integrity: sha512-bAw7L2E+jn9XHG5P9zrPnHdO0yJub4U+yXJOdpcpkr7OBd9T8oll4lUos0iSGRcDvfZoLUKfx9a6aNmIhJ4+mQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -1446,6 +1541,12 @@ packages: vite: optional: true + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1472,6 +1573,19 @@ packages: snapshots: + '@cubejs-client/core@1.6.44': + dependencies: + core-js: 3.49.0 + cross-fetch: 3.2.0 + d3-format: 3.1.2 + d3-time-format: 4.1.0 + dayjs: 1.11.20 + ramda: 0.27.2 + url-search-params-polyfill: 7.0.1 + uuid: 11.1.0 + transitivePeerDependencies: + - encoding + '@esbuild/aix-ppc64@0.25.9': optional: true @@ -1632,6 +1746,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@kurkle/color@0.3.4': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1987,6 +2103,19 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chartjs-adapter-date-fns@3.0.0(chart.js@4.5.1)(date-fns@4.1.0): + dependencies: + chart.js: 4.5.1 + date-fns: 4.1.0 + + chartjs-plugin-annotation@3.1.0(chart.js@4.5.1): + dependencies: + chart.js: 4.5.1 + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -2005,6 +2134,14 @@ snapshots: cookie@0.6.0: {} + core-js@3.49.0: {} + + cross-fetch@3.2.0: + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2013,6 +2150,24 @@ snapshots: cssesc@3.0.0: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-format@3.1.2: {} + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + date-fns@4.1.0: {} + + dayjs@1.11.20: {} + debug@4.4.1: dependencies: ms: 2.1.3 @@ -2232,6 +2387,8 @@ snapshots: imurmurhash@0.1.4: {} + internmap@2.0.3: {} + is-extglob@2.1.1: {} is-glob@4.0.3: @@ -2330,6 +2487,10 @@ snapshots: lodash.merge@4.6.2: {} + lucide-svelte@1.0.1(svelte@5.38.8): + dependencies: + svelte: 5.38.8 + magic-string@0.30.19: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2369,6 +2530,10 @@ snapshots: natural-compare@1.4.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2450,6 +2615,8 @@ snapshots: queue-microtask@1.2.3: {} + ramda@0.27.2: {} + readdirp@4.1.2: {} resolve-from@4.0.0: {} @@ -2579,6 +2746,8 @@ snapshots: totalist@3.0.1: {} + tr46@0.0.3: {} + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: typescript: 5.9.2 @@ -2604,6 +2773,8 @@ snapshots: dependencies: punycode: 2.3.1 + url-search-params-polyfill@7.0.1: {} + util-deprecate@1.0.2: {} uuid@11.1.0: {} @@ -2630,6 +2801,13 @@ snapshots: optionalDependencies: vite: 7.1.5(jiti@2.5.1)(lightningcss@1.30.1) + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/src/app.css b/src/app.css index 1421140..4f99420 100644 --- a/src/app.css +++ b/src/app.css @@ -2,229 +2,472 @@ @plugin '@tailwindcss/forms'; @plugin '@tailwindcss/typography'; +/* ── Design tokens ─────────────────────────────────────── */ +:root { + --bg: #0f1117; + --surface: #1a1d27; + --surface-raised: #22263a; + --border: rgba(255, 255, 255, 0.07); + --text: #e8eaf0; + --text-muted: #7a7f96; + --accent: #f97316; + --accent-dim: rgba(249, 115, 22, 0.15); + --green: #22c55e; + --red: #ef4444; + --radius: 14px; +} + +[data-theme='light'] { + --bg: #f4f5f7; + --surface: #ffffff; + --surface-raised: #eef0f4; + --border: rgba(0, 0, 0, 0.08); + --text: #1a1d27; + --text-muted: #6b7280; + --accent-dim: rgba(249, 115, 22, 0.12); +} + +/* ── Base ──────────────────────────────────────────────── */ +*, +*::before, +*::after { + box-sizing: border-box; +} html, body { - font-family: - ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; - margin: 0 + font-family: + ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji'; + background: var(--bg); + color: var(--text); + margin: 0; + min-height: 100dvh; } p { - margin: 0; + margin: 0; } +/* ── Nav ───────────────────────────────────────────────── */ .nav { - background-color: black; - color: white; - width: full; - text-align: center; - padding: 1em; + background: var(--surface); + border-bottom: 1px solid var(--border); + padding: 0.75rem 1rem; + display: flex; + align-items: center; + justify-content: center; + position: relative; } .nav-title { - font-size: xx-large; + font-size: 1.5rem; + font-weight: 700; + color: var(--accent); + letter-spacing: 0.05em; } +.dark-mode-btn { + position: absolute; + right: 1rem; + top: 50%; + transform: translateY(-50%); + background: var(--surface-raised); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 0.4rem; + transition: + color 0.15s, + border-color 0.15s; +} + +.dark-mode-btn:hover { + color: var(--accent); + border-color: var(--accent); +} + +/* ── Layout ────────────────────────────────────────────── */ .content { - max-width: 1000px; - margin: auto; + max-width: 960px; + margin: 0 auto; + padding: 1.5rem 1rem 4rem; + display: flex; + flex-direction: column; + gap: 1.25rem; } +/* ── Status cards ──────────────────────────────────────── */ .card-grid { - margin-top: 15px; - display: flex; - justify-content: center; - align-items: center; - gap: 2em; + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; } -.graph { - height: 400px; - overflow: hidden; - display: flex; - justify-content: center; -} - -.plot-wrapper { - height: 400px; +@media (max-width: 540px) { + .card-grid { + grid-template-columns: 1fr 1fr; + } + .card-grid > :nth-child(3) { + grid-column: span 2; + } } .card { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - min-width: 25%; - padding: 1em; - min-height: 120px; - box-shadow: 2px 2px 12px 1px rgba(140, 140, 140, .5); - border-radius: 10px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1.1rem 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + transition: box-shadow 0.2s; } -.reading { - font-weight: bold; - margin-top: 18px; +.card-label { + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-muted); } - -@property --ovenColor1 { - syntax: ''; - initial-value: rgba(255, 0, 0, 0); - inherits: false; +.card-value { + font-size: 2.2rem; + font-weight: 700; + line-height: 1; + color: var(--text); } - -@property --ovenColor2 { - syntax: ''; - initial-value: rgba(255, 0, 0, 0); - inherits: false; +.card-unit { + font-size: 1rem; + font-weight: 400; + color: var(--text-muted); + margin-left: 0.1em; } -.oven { - display: flex; - justify-content: center; - margin: auto; +.card-sub { + font-size: 0.75rem; + color: var(--text-muted); + margin-top: 0.1rem; } -.oven-wrapper { - background: radial-gradient(var(--ovenColor1), var(--ovenColor2), rgba(255, 0, 0, 0), rgba(255, 0, 0, 0)); - width: 50%; - display: flex; - justify-content: center; - overflow: visible; - padding: 30px; - transition: --ovenColor1 3s, --ovenColor2 3s; +.card-target { + font-size: 0.9rem; + font-weight: 600; + color: var(--accent); + opacity: 0.8; + margin-top: 0.25rem; + letter-spacing: 0.02em; } -.oven.on .oven-wrapper { - --ovenColor1: rgb(255, 60, 0); - --ovenColor2: rgba(255, 60, 0, 0.4); +@keyframes extreme-blink { + 0%, + 100% { + color: #ef4444; + } + 50% { + color: #ffffff; + } } -.oven .oven-img-wrapper { - width: 65%; +.temp-extreme { + animation: extreme-blink 0.6s ease-in-out infinite; } -.oven img { - width: 100%; +/* ── Relay card ────────────────────────────────────────── */ +.relay-card { + cursor: pointer; + border: none; + background: var(--surface); + transition: + background 0.3s, + box-shadow 0.3s; } -.relais { - cursor: pointer; +.relay-card:hover { + background: var(--surface-raised); } +.relay-on { + box-shadow: + 0 0 0 2px var(--red), + 0 0 20px rgba(239, 68, 68, 0.25); +} +.relay-off { + box-shadow: none; +} + +/* ON/OFF toggle switch (visual-only inside the relay button) */ .onoffswitch { - pointer-events: none; - margin-top: 10px; - position: relative; - width: 90px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + pointer-events: none; + margin: 0.5rem 0 0.15rem; + position: relative; + width: 90px; + user-select: none; } .onoffswitch-checkbox { - position: absolute; - opacity: 0; - pointer-events: none; + position: absolute; + opacity: 0; + pointer-events: none; } .onoffswitch-label { - display: block; - overflow: hidden; - cursor: pointer; - border: 2px solid #999999; - border-radius: 20px; + display: block; + overflow: hidden; + border: 2px solid rgba(255, 255, 255, 0.15); + border-radius: 20px; } .onoffswitch-inner { - display: block; - width: 200%; - margin-left: -100%; - transition: margin 0.3s ease-in 0s; + display: block; + width: 200%; + margin-left: -100%; + transition: margin 0.25s ease-in; } -.onoffswitch-inner:before, -.onoffswitch-inner:after { - display: block; - float: left; - width: 50%; - height: 30px; - padding: 0; - line-height: 30px; - font-size: 14px; - color: white; - font-family: Trebuchet, Arial, sans-serif; - font-weight: bold; - box-sizing: border-box; +.onoffswitch-inner::before, +.onoffswitch-inner::after { + display: block; + float: left; + width: 50%; + height: 30px; + line-height: 30px; + font-size: 13px; + font-weight: 700; + box-sizing: border-box; + text-align: center; } -.onoffswitch-inner:before { - content: "ON"; - background-color: #C43535; - color: #FFFFFF; - padding-right: 8px; - text-align: center; +.onoffswitch-inner::before { + content: 'ON'; + background-color: #c43535; + color: #ffffff; + padding-right: 8px; } -.onoffswitch-inner:after { - content: "OFF"; - padding-left: 10px; +.onoffswitch-inner::after { + content: 'OFF'; + background-color: #444; + color: #aaa; + padding-left: 14px; +} - background-color: #EEEEEE; - color: #999999; - text-align: center; +[data-theme='light'] .onoffswitch-inner::after { + background-color: #d1d5db; + color: #6b7280; } .onoffswitch-switch { - display: block; - width: 22px; - height: 22px; - margin: 6px; - background: #FFFFFF; - position: absolute; - top: 0; - bottom: 0; - right: 56px; - border: 2px solid #999999; - border-radius: 20px; - transition: all 0.3s ease-in 0s; + display: block; + width: 22px; + height: 22px; + margin: 4px; + margin-left: 3px; + background: #ffffff; + position: absolute; + top: 2px; + bottom: 0; + right: 56px; + border: 2px solid rgba(255, 255, 255, 0.15); + border-radius: 50%; + transition: right 0.25s ease-in; } -.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-inner { - margin-left: 0; +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner { + margin-left: 0; } -.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-switch { - right: 0px; +.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch { + right: 3px; } -.settings { - margin-bottom: 200px; - margin-top: 50px; +/* ── Oven graphic ──────────────────────────────────────── */ +@property --ovenColor1 { + syntax: ''; + initial-value: rgba(255, 0, 0, 0); + inherits: false; +} +@property --ovenColor2 { + syntax: ''; + initial-value: rgba(255, 0, 0, 0); + inherits: false; } -.settings input { - max-width: 50px; - font-size: larger; - font-weight: bold; +.oven { + display: flex; + justify-content: center; } +.oven-wrapper { + background: radial-gradient( + circle at center, + var(--ovenColor1) 0%, + var(--ovenColor2) 30%, + transparent 68% + ); + width: 60%; + max-width: 480px; + display: flex; + justify-content: center; + align-items: center; + padding: 3.5rem; + transition: + --ovenColor1 2.5s, + --ovenColor2 2.5s; +} + +.oven.on .oven-wrapper { + --ovenColor1: rgba(255, 110, 0, 0.85); + --ovenColor2: rgba(255, 60, 0, 0.4); +} + +.oven-img-wrapper { + width: 100%; +} +.oven img { + width: 100%; +} + +/* ── Chart ─────────────────────────────────────────────── */ +.chart-section { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem; + height: 300px; +} + +.chart-section canvas { + width: 100% !important; + height: 100% !important; +} + +/* ── Controls grid ─────────────────────────────────────── */ +.controls-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + align-items: start; +} + +@media (max-width: 600px) { + .controls-grid { + grid-template-columns: 1fr; + } +} + +.control-card { + align-items: flex-start; + gap: 0.75rem; + padding: 1.1rem 1.1rem 1.2rem; +} + +/* ── Inputs ────────────────────────────────────────────── */ +.input-row { + display: flex; + align-items: center; + gap: 0.4rem; +} + +.num-input { + background: var(--surface-raised); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 1.1rem; + font-weight: 600; + padding: 0.35rem 0.5rem; + width: 5.5rem; + outline: none; + transition: border-color 0.15s; +} + +.num-input:focus { + border-color: var(--accent); +} + +.input-unit { + font-size: 0.85rem; + color: var(--text-muted); +} + +.mode-select { + background: var(--surface-raised); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 0.95rem; + padding: 0.4rem 0.6rem; + width: 100%; + outline: none; + cursor: pointer; + transition: border-color 0.15s; +} + +.mode-select:focus { + border-color: var(--accent); +} + +/* ── Sub-controls (PWM / PID params) ───────────────────── */ +.subcontrol-grid { + display: flex; + gap: 1rem; + width: 100%; +} + +.subcontrol { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.sublabel { + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +.subinfo { + font-size: 0.78rem; + color: var(--text-muted); +} + +/* ── Action buttons ────────────────────────────────────── */ +.action-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + background: var(--surface-raised); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + font-size: 0.85rem; + font-weight: 500; + padding: 0.45rem 0.9rem; + cursor: pointer; + text-decoration: none; + transition: + background 0.15s, + border-color 0.15s; + width: 100%; + justify-content: center; +} + +.action-btn:hover { + background: var(--accent-dim); + border-color: var(--accent); + color: var(--accent); +} + +/* ── Utility ───────────────────────────────────────────── */ .hidden { - display: none; + display: none !important; } - -#mode_span::first-letter { - text-transform: capitalize; -} - -.chart div { - font: 10px sans-serif; - background-color: steelblue; - text-align: right; - padding: 3px; - margin: 1px; - color: white; -} \ No newline at end of file diff --git a/src/lib/spoof.ts b/src/lib/spoof.ts new file mode 100644 index 0000000..7ede018 --- /dev/null +++ b/src/lib/spoof.ts @@ -0,0 +1,191 @@ +/** + * SpoofSocket – a fake WebSocket that simulates the ESP32 firmware. + * Used in DEV mode only. Cast to WebSocket with `as unknown as WebSocket`. + * + * Thermal model: + * – S-curve heating: rate peaks at ~50% of the target range (fast in the + * middle, slow at start/end), matching how a real oven behaves. + * – Newton's law of cooling when relay is off. + * – Small Gaussian noise on each tick for realism. + * + * Control modes mirror the firmware: manual, pwm, pid. + */ + +const TICK_MS = 500; // matches WS_INTERVAL on the firmware +const ROOM_TEMP = 22; // °C ambient +const MAX_HEAT_RATE = 14; // °C/s at peak of S-curve +const NOISE_AMP = 0.35; // °C peak random noise + +/** S-curve heating factor: parabolic 4x(1-x), peaks at x = 0.5. */ +function heatFactor(temp: number, target: number): number { + if (target <= ROOM_TEMP) return 0; + const x = Math.max(0, Math.min(1, (temp - ROOM_TEMP) / (target - ROOM_TEMP))); + return 4 * x * (1 - x) + 0.02; // +0.02 so the oven starts moving immediately +} + +/** Newton's law of cooling – rate proportional to excess over ambient. */ +function coolingRate(temp: number): number { + return (1.4 * Math.max(0, temp - ROOM_TEMP)) / 300; +} + +type SensorPayload = { + mode: string; + temperature: number; + relais: number; + target_temp: number; + pwm_on: number; + pwm_off: number; + pid: number; + kp: number; + ki: number; + kd: number; +}; + +type Command = { cmd: string; value?: number | string }; + +export class SpoofSocket { + onopen: (() => void) | null = null; + onclose: (() => void) | null = null; + onmessage: ((e: { data: string }) => void) | null = null; + readyState: number = WebSocket.CONNECTING; + + private s: SensorPayload = { + mode: 'manual', + temperature: ROOM_TEMP, + relais: 0, + target_temp: 460, + pwm_on: 2000, + pwm_off: 4000, + pid: 0, + kp: 0.6, + ki: 0.1, + kd: 0.0 + }; + + private integral = 0; + private lastSwitch = Date.now(); + private ticker: ReturnType | null = null; + + constructor() { + // Simulate async connection handshake + setTimeout(() => { + this.readyState = WebSocket.OPEN; + this.onopen?.(); + this.ticker = setInterval(() => this.tick(), TICK_MS); + }, 80); + } + + send(raw: string): void { + try { + const { cmd, value } = JSON.parse(raw) as Command; + this.handle(cmd, value); + } catch { + // ignore malformed messages + } + } + + /** Stop the simulation ticker without triggering onclose (for cleanup). */ + destroy(): void { + if (this.ticker !== null) { + clearInterval(this.ticker); + this.ticker = null; + } + } + + close(): void { + this.destroy(); + this.readyState = WebSocket.CLOSED; + this.onclose?.(); + } + + private handle(cmd: string, value?: number | string): void { + const s = this.s; + switch (cmd) { + case 'getReadings': + this.emit(); + break; + case 'setTargetTemp': + s.target_temp = Number(value); + this.integral = 0; // reset integral on setpoint change + break; + case 'setMode': + s.mode = String(value); + if (s.mode === 'manual') s.relais = 0; + this.integral = 0; + break; + case 'switchRelais': + if (s.mode === 'manual') { + s.relais = s.relais ? 0 : 1; + this.lastSwitch = Date.now(); + } + break; + case 'setPWMOn': + s.pwm_on = Number(value) * 1000; + break; + case 'setPWMOff': + s.pwm_off = Number(value) * 1000; + break; + case 'setKp': + s.kp = Number(value); + break; + case 'setKi': + s.ki = Number(value); + break; + case 'setKd': + s.kd = Number(value); + break; + } + } + + private tick(): void { + const dt = TICK_MS / 1000; + const s = this.s; + const now = Date.now(); + + // ── relay control (mirrors firmware regulateRelais) ────────────────── + if (s.mode === 'pwm') { + const elapsed = now - this.lastSwitch; + if (s.relais === 1 && elapsed > s.pwm_on) { + s.relais = 0; + this.lastSwitch = now; + } else if (s.relais === 0 && elapsed > s.pwm_off) { + s.relais = 1; + this.lastSwitch = now; + } + } else if (s.mode === 'pid') { + const error = s.target_temp - s.temperature; + this.integral = Math.max(-100, Math.min(100, this.integral + s.ki * error * dt)); + const output = Math.max(0, Math.min(100, s.kp * error + this.integral)); + s.pid = output; + + if (output > 0) { + const offDelay = s.pwm_on / (output / 100) - s.pwm_on; + const elapsed = now - this.lastSwitch; + if (s.relais === 1 && offDelay > 0 && elapsed > s.pwm_on) { + s.relais = 0; + this.lastSwitch = now; + } else if (s.relais === 0 && offDelay > 0 && elapsed > offDelay) { + s.relais = 1; + this.lastSwitch = now; + } + } else { + s.relais = 0; + } + } + + // ── thermal model ──────────────────────────────────────────────────── + if (s.relais) { + s.temperature += MAX_HEAT_RATE * heatFactor(s.temperature, s.target_temp) * dt; + } else { + s.temperature -= coolingRate(s.temperature) * dt; + } + s.temperature = Math.max(ROOM_TEMP, s.temperature); + s.temperature += (Math.random() - 0.5) * NOISE_AMP; + + this.emit(); + } + + private emit(): void { + this.onmessage?.({ data: JSON.stringify(this.s) }); + } +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 478c7a2..8149555 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,7 +1,3 @@ export const pad = (n: string | number, amount = 2) => { - if (amount === 2) { - return ('0' + n).slice(-2); - } else { - return ('00' + n).slice(-3); - } + return String(n).padStart(amount, '0'); }; diff --git a/src/main.cpp b/src/main.cpp index aafb97c..e33275e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,53 +10,62 @@ #include "secrets.h" #include "pid.h" -// #define RELAY 21; -// #define THERMO_DO 19; -// #define THERMO_CS 23; -// #define THERMO_CLK 5; - AsyncWebServer server(80); AsyncWebSocket ws("/ws"); +// Pin assignments +constexpr int relay = 21; +constexpr int thermoDO = 19; +constexpr int thermoCS = 23; +constexpr int thermoCLK = 5; + // Timer variables unsigned long lastSensor = 0; -unsigned long SENSOR_INTERVAL = 100; +const unsigned long SENSOR_INTERVAL = 100; unsigned long lastWebSocket = 0; -unsigned long WS_INTERVAL = 500; - -int relay = 21; -int thermoDO = 19; -int thermoCS = 23; -int thermoCLK = 5; +const unsigned long WS_INTERVAL = 500; float temperature = 0; -int targetTemp = 300; +float targetTemp = 460; unsigned long lastSwitch = 0; float pwmSwitchDelayOn = 2000; // 2s float pwmSwitchDelayOff = 4000; // 4s -float power = 0; +float power = 100; float kp = 0.6; float ki = 0.1; float kd = 0.0; -String mode = "manual"; +enum class Mode { MANUAL, PWM, PID }; +Mode mode = Mode::MANUAL; MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO); PIDController pid(kp, ki, kd, 100.0, 0.0, 100.0); // Kp, Ki, Kd, max, min, max_integral +static const char *modeToStr(Mode m) +{ + switch (m) + { + case Mode::PWM: + return "pwm"; + case Mode::PID: + return "pid"; + default: + return "manual"; + } +} + // Get Sensor Readings and return JSON object String getSensorReadings() - { JsonDocument readings; String jsonString; - readings["mode"] = mode; + readings["mode"] = modeToStr(mode); temperature = thermocouple.readCelsius(); readings["temperature"] = temperature; @@ -95,8 +104,18 @@ void initWiFi() WiFi.mode(WIFI_STA); WiFi.begin(ssid, password); Serial.print("Connecting to WiFi .."); + unsigned long startAttemptTime = millis(); while (WiFi.status() != WL_CONNECTED) { + if (millis() - startAttemptTime > 10000) + { + Serial.println("Wifi not found, starting AP mode"); + WiFi.mode(WIFI_AP); + WiFi.softAP(ap_ssid, ap_password); + Serial.print("AP IP address: "); + Serial.println(WiFi.softAPIP()); + break; + } Serial.print('.'); delay(SENSOR_INTERVAL); yield(); @@ -115,20 +134,30 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) { data[len] = 0; - String message = (char *)data; - if (message.startsWith("getReadings")) + JsonDocument doc; + DeserializationError err = deserializeJson(doc, (char *)data); + if (err) { - String sensorReadings = getSensorReadings(); - notifyClients(sensorReadings); - }; - if (message.startsWith("switchRelais")) + Serial.printf("WS JSON parse error: %s\n", err.c_str()); + return; + } + + const char *cmd = doc["cmd"]; + if (!cmd) + return; + + if (strcmp(cmd, "getReadings") == 0) + { + notifyClients(getSensorReadings()); + } + else if (strcmp(cmd, "switchRelais") == 0) { Serial.printf("\nSwitch Relais"); if (digitalRead(relay) == HIGH) { digitalWrite(relay, LOW); lastSwitch = millis(); - mode = "manual"; + mode = Mode::MANUAL; } else { @@ -136,68 +165,55 @@ void handleWebSocketMessage(void *arg, uint8_t *data, size_t len) lastSwitch = millis(); } } - if (message.startsWith("setTargetTemp")) + else if (strcmp(cmd, "setTargetTemp") == 0) { - String target = message.substring(15, 18); - targetTemp = target.toFloat(); + targetTemp = doc["value"].as(); pid.setSetpoint(targetTemp); - Serial.printf("\nTarget temp set to "); - Serial.print(targetTemp); - Serial.printf("°C"); + Serial.printf("\nTarget temp set to %.1f°C", targetTemp); } - if (message.startsWith("setMode")) + else if (strcmp(cmd, "setMode") == 0) { - if (message.indexOf("pwm") > 0) + const char *value = doc["value"]; + if (value && strcmp(value, "pwm") == 0) { - mode = "pwm"; + mode = Mode::PWM; } - else if (message.indexOf("pid") > 0) + else if (value && strcmp(value, "pid") == 0) { - mode = "pid"; + mode = Mode::PID; } else { - mode = "manual"; + mode = Mode::MANUAL; digitalWrite(relay, LOW); lastSwitch = millis(); } - Serial.printf("\nMode set to "); - Serial.print(mode); + Serial.printf("\nMode set to %s", modeToStr(mode)); } - if (message.startsWith("setKp")) + else if (strcmp(cmd, "setKp") == 0) { - String k = message.substring(6, 12); - kp = k.toFloat(); + kp = doc["value"].as(); pid.setSetKp(kp); } - if (message.startsWith("setKi")) + else if (strcmp(cmd, "setKi") == 0) { - String k = message.substring(6, 12); - ki = k.toFloat(); + ki = doc["value"].as(); pid.setSetKi(ki); } - if (message.startsWith("setKd")) + else if (strcmp(cmd, "setKd") == 0) { - String k = message.substring(6, 12); - kd = k.toFloat(); + kd = doc["value"].as(); pid.setSetKd(kd); } - if (message.startsWith("setPWMOn")) + else if (strcmp(cmd, "setPWMOn") == 0) { - String target = message.substring(10, 15); - pwmSwitchDelayOn = target.toFloat() * 1000; - Serial.printf("\nPWM switch ON set to "); - Serial.print(pwmSwitchDelayOn); - Serial.printf("°C"); + pwmSwitchDelayOn = doc["value"].as() * 1000; + Serial.printf("\nPWM switch ON set to %.0fms", pwmSwitchDelayOn); } - - if (message.startsWith("setPWMOff")) + else if (strcmp(cmd, "setPWMOff") == 0) { - String target = message.substring(11, 16); - pwmSwitchDelayOff = target.toFloat() * 1000; - Serial.printf("\nPWM switch OFF set to "); - Serial.print(pwmSwitchDelayOff); - Serial.printf("°C"); + pwmSwitchDelayOff = doc["value"].as() * 1000; + Serial.printf("\nPWM switch OFF set to %.0fms", pwmSwitchDelayOff); } } } @@ -229,7 +245,7 @@ void initWebSocket() void regulateRelais() { - if (mode == "pwm") + if (mode == Mode::PWM) { if ((millis() - lastSwitch) > pwmSwitchDelayOn) { @@ -249,11 +265,20 @@ void regulateRelais() } } } - if (mode == "pid") + if (mode == Mode::PID) { // output = Kp×error + Ki×∫error×dt + Kd×(Δerror/Δt) power = pid.compute(temperature); - pwmSwitchDelayOff = (pwmSwitchDelayOn / (power / 100)) - pwmSwitchDelayOn; + if (power > 0.0f) + { + pwmSwitchDelayOff = (pwmSwitchDelayOn / (power / 100.0f)) - pwmSwitchDelayOn; + } + else + { + pwmSwitchDelayOff = 0.0f; + digitalWrite(relay, LOW); + lastSwitch = millis(); + } if (pwmSwitchDelayOff > 0) { @@ -291,6 +316,8 @@ void setup() initLittleFS(); initWebSocket(); + digitalWrite(relay, HIGH); + // Web Server Root URL server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { request->send(LittleFS, "/index.html", "text/html"); }); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index e09f60f..c80b7f2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,460 +1,351 @@ +
+
-

Sensor Temperature

-

{temp.toFixed(2)} °C

+

Temperature

+

+ {temp.toFixed(1)}°C +

+

⌖ {targetTemp}°C

- + +
+

PID Output

+

{calculatedPidOutput.toFixed(0)}%

+

{(calculatedPidOutput * 12).toFixed(0)} W

+
-
+ + +
Oven
-
-
-
- -
+ + +
+
-
-
-

Target temperature

-

+ + +

+ +
+

Target Temperature

+
- -

+ °C +
-
-

- Mode: Manual -

-

- -

- {#if mode === 'pwm'} -
-
on
-
off
-
-
- - - - + +
+

Mode

+ + + {#if mode === 'pwm'} +
+
+ +
+ + s +
+
+
+ +
+ + s +
+
-
- {((pwmOn / (pwmOn + pwmOff)) * 100).toFixed(0)}%, {( +

+ Duty {((pwmOn / (pwmOn + pwmOff)) * 100).toFixed(0)}% — {( (pwmOn / (pwmOn + pwmOff)) * 1200 - ).toFixed(0)}W -

+ ).toFixed(0)} W +

{/if} + {#if mode === 'pid'} -
- PID: {calculatedPidOutput ? calculatedPidOutput.toFixed(0) : 0}% {calculatedPidOutput - ? (calculatedPidOutput * 12).toFixed(0) - : 0}W -
-
ON:{pwmOn.toFixed(2)}s OFF:{pwmOff ? pwmOff.toFixed(2) : 0}s
-
-
Kp
-
Ki
- -
-
- changeKValue(e, 'p')} - /> - changeKValue(e, 'i')} - /> - +

+ ON {pwmOn.toFixed(2)} s  /  OFF {pwmOff ? pwmOff.toFixed(2) : 0} s +

+
+
+ + changeKValue(e, 'p')} + /> +
+
+ + changeKValue(e, 'i')} + /> +
{/if}
-
- - - + + +
+

Data

+ + +
diff --git a/src/secrets.example.h b/src/secrets.example.h index caba719..df0aded 100644 --- a/src/secrets.example.h +++ b/src/secrets.example.h @@ -1,3 +1,5 @@ // Replace with your network credentials const char *ssid = "WIFI_SSID"; const char *password = "WIFI_PWD"; +const char *ap_ssid = "CrustCraft"; +const char *ap_password = "crustcr4ft"; \ No newline at end of file