diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e48bc14
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+.pio
+.vscode
+
+node_modules
+
+src/secrets.h
+
+data
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5107154
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# CrustCraft
+
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..b80780e
--- /dev/null
+++ b/index.html
@@ -0,0 +1,63 @@
+
+
+
+
+ CrustCraft
+
+
+
+
+
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+ Target temperature
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..24307ee
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1049 @@
+{
+ "name": "crust-craft",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "highcharts": "^12.3.0",
+ "typescript": "^5.9.2"
+ },
+ "devDependencies": {
+ "vite": "^7.1.3"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.49.0.tgz",
+ "integrity": "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.49.0.tgz",
+ "integrity": "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.49.0.tgz",
+ "integrity": "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.49.0.tgz",
+ "integrity": "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.49.0.tgz",
+ "integrity": "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.49.0.tgz",
+ "integrity": "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.49.0.tgz",
+ "integrity": "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.49.0.tgz",
+ "integrity": "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.49.0.tgz",
+ "integrity": "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.49.0.tgz",
+ "integrity": "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.49.0.tgz",
+ "integrity": "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.49.0.tgz",
+ "integrity": "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.49.0.tgz",
+ "integrity": "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.49.0.tgz",
+ "integrity": "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.49.0.tgz",
+ "integrity": "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.49.0.tgz",
+ "integrity": "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.49.0.tgz",
+ "integrity": "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.49.0.tgz",
+ "integrity": "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.49.0.tgz",
+ "integrity": "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.49.0.tgz",
+ "integrity": "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "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/esbuild": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.9",
+ "@esbuild/android-arm": "0.25.9",
+ "@esbuild/android-arm64": "0.25.9",
+ "@esbuild/android-x64": "0.25.9",
+ "@esbuild/darwin-arm64": "0.25.9",
+ "@esbuild/darwin-x64": "0.25.9",
+ "@esbuild/freebsd-arm64": "0.25.9",
+ "@esbuild/freebsd-x64": "0.25.9",
+ "@esbuild/linux-arm": "0.25.9",
+ "@esbuild/linux-arm64": "0.25.9",
+ "@esbuild/linux-ia32": "0.25.9",
+ "@esbuild/linux-loong64": "0.25.9",
+ "@esbuild/linux-mips64el": "0.25.9",
+ "@esbuild/linux-ppc64": "0.25.9",
+ "@esbuild/linux-riscv64": "0.25.9",
+ "@esbuild/linux-s390x": "0.25.9",
+ "@esbuild/linux-x64": "0.25.9",
+ "@esbuild/netbsd-arm64": "0.25.9",
+ "@esbuild/netbsd-x64": "0.25.9",
+ "@esbuild/openbsd-arm64": "0.25.9",
+ "@esbuild/openbsd-x64": "0.25.9",
+ "@esbuild/openharmony-arm64": "0.25.9",
+ "@esbuild/sunos-x64": "0.25.9",
+ "@esbuild/win32-arm64": "0.25.9",
+ "@esbuild/win32-ia32": "0.25.9",
+ "@esbuild/win32-x64": "0.25.9"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/highcharts": {
+ "version": "12.3.0",
+ "resolved": "https://registry.npmjs.org/highcharts/-/highcharts-12.3.0.tgz",
+ "integrity": "sha512-QIKmaemgheRa1K2Ia9MLj1KLtBU1Tu/VQ6KAMqtMBMsAC4NzcFq6g96LF03ZO3IFFiSifmZx8ItEyRcz4w75cg==",
+ "license": "https://www.highcharts.com/license"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.49.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.49.0.tgz",
+ "integrity": "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.49.0",
+ "@rollup/rollup-android-arm64": "4.49.0",
+ "@rollup/rollup-darwin-arm64": "4.49.0",
+ "@rollup/rollup-darwin-x64": "4.49.0",
+ "@rollup/rollup-freebsd-arm64": "4.49.0",
+ "@rollup/rollup-freebsd-x64": "4.49.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.49.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.49.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.49.0",
+ "@rollup/rollup-linux-arm64-musl": "4.49.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.49.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.49.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.49.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.49.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.49.0",
+ "@rollup/rollup-linux-x64-gnu": "4.49.0",
+ "@rollup/rollup-linux-x64-musl": "4.49.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.49.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.49.0",
+ "@rollup/rollup-win32-x64-msvc": "4.49.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
+ "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.14"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2d15165
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "dependencies": {
+ "highcharts": "^12.3.0",
+ "typescript": "^5.9.2"
+ },
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^7.1.3"
+ }
+}
diff --git a/platformio.ini b/platformio.ini
new file mode 100644
index 0000000..925384f
--- /dev/null
+++ b/platformio.ini
@@ -0,0 +1,29 @@
+; PlatformIO Project Configuration File
+;
+; Build options: build flags, source filter
+; Upload options: custom upload port, speed and extra flags
+; Library options: dependencies, extra library storages
+; Advanced options: extra scripting
+;
+; Please visit documentation for the other options and examples
+; https://docs.platformio.org/page/projectconf.html
+
+[env:esp32doit-devkit-v1]
+platform = espressif32
+board = esp32doit-devkit-v1
+
+framework = arduino
+lib_compat_mode = strict
+lib_ldf_mode = chain
+
+lib_deps =
+ adafruit/MAX6675 library@^1.1.2
+ bblanchon/ArduinoJson@^7.4.2
+ ayushsharma82/ElegantOTA@^3.1.7
+ ESP32Async/AsyncTCP
+ ESP32Async/ESPAsyncWebServer
+
+board_build.filesystem = littlefs
+
+build_flags =
+ -D ELEGANTOTA_USE_ASYNC_WEBSERVER=1
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..facfa89
Binary files /dev/null and b/public/favicon.ico differ
diff --git a/public/oven.png b/public/oven.png
new file mode 100644
index 0000000..cd85248
Binary files /dev/null and b/public/oven.png differ
diff --git a/public/oven_small.png b/public/oven_small.png
new file mode 100644
index 0000000..c6b333c
Binary files /dev/null and b/public/oven_small.png differ
diff --git a/src/globals.d.ts b/src/globals.d.ts
new file mode 100644
index 0000000..35306c6
--- /dev/null
+++ b/src/globals.d.ts
@@ -0,0 +1 @@
+declare module '*.css';
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..9696c14
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,190 @@
+#include
+#include
+#include
+#include
+#include "LittleFS.h"
+#include
+#include "max6675.h"
+#include
+
+#include "secrets.h"
+
+// Create AsyncWebServer object on port 80
+AsyncWebServer server(80);
+
+// Create a WebSocket object
+AsyncWebSocket ws("/ws");
+
+// Json Variable to Hold Sensor Readings
+JsonDocument readings;
+String jsonString;
+
+// Timer variables
+unsigned long lastTime = 0;
+unsigned long timerDelay = 250;
+
+int relay = 21;
+int thermoDO = 19;
+int thermoCS = 23;
+int thermoCLK = 5;
+
+MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
+
+int targetTemp = 300;
+int overShoot = 10;
+int underShoot = 5;
+unsigned long lastSwitch = 0;
+unsigned long switchDelay = 60000;
+bool autoSwitch = true;
+
+// Get Sensor Readings and return JSON object
+String getSensorReadings()
+{
+ readings["temperature"] = thermocouple.readCelsius();
+ if (digitalRead(relay) == HIGH)
+ {
+ readings["relais"] = 1;
+ }
+ else
+ {
+ readings["relais"] = 0;
+ }
+ serializeJson(readings, jsonString);
+ return jsonString;
+}
+
+// Initialize LittleFS
+void initLittleFS()
+{
+ if (!LittleFS.begin(true))
+ {
+ Serial.println("An error has occurred while mounting LittleFS");
+ }
+ Serial.println("LittleFS mounted successfully");
+}
+
+// Initialize WiFi
+void initWiFi()
+{
+
+ WiFi.mode(WIFI_STA);
+ WiFi.begin(ssid, password);
+ Serial.print("Connecting to WiFi ..");
+ while (WiFi.status() != WL_CONNECTED)
+ {
+ Serial.print('.');
+ delay(1000);
+ }
+ Serial.println(WiFi.localIP());
+}
+
+void notifyClients(String sensorReadings)
+{
+ ws.textAll(sensorReadings);
+}
+
+void handleWebSocketMessage(void *arg, uint8_t *data, size_t len)
+{
+ AwsFrameInfo *info = (AwsFrameInfo *)arg;
+ if (info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)
+ {
+ data[len] = 0;
+ String message = (char *)data;
+ if (strcmp((char *)data, "getReadings") == 0)
+ {
+ String sensorReadings = getSensorReadings();
+ notifyClients(sensorReadings);
+ };
+ if (strcmp((char *)data, "switchRelais") == 0)
+ {
+ Serial.printf("\nSwitch Relais");
+ if (digitalRead(relay) == HIGH)
+ {
+ digitalWrite(relay, LOW);
+ lastSwitch = millis();
+ }
+ else
+ {
+ digitalWrite(relay, HIGH);
+ lastSwitch = millis();
+ }
+ }
+ if (message.startsWith("setTargetTemp"))
+ {
+ String target = message.substring(15, 18);
+ targetTemp = target.toInt();
+ Serial.printf("\nTarget temp set to ");
+ Serial.print(targetTemp);
+ Serial.printf("°C");
+ }
+ }
+}
+
+void onEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
+{
+ switch (type)
+ {
+ case WS_EVT_CONNECT:
+ Serial.printf("WebSocket client #%u connected from %s\n", client->id(), client->remoteIP().toString().c_str());
+ break;
+ case WS_EVT_DISCONNECT:
+ Serial.printf("WebSocket client #%u disconnected\n", client->id());
+ break;
+ case WS_EVT_DATA:
+ handleWebSocketMessage(arg, data, len);
+ break;
+ case WS_EVT_PONG:
+ case WS_EVT_ERROR:
+ break;
+ }
+}
+
+void initWebSocket()
+{
+ ws.onEvent(onEvent);
+ server.addHandler(&ws);
+}
+
+void setup()
+{
+ Serial.begin(9600);
+ pinMode(relay, OUTPUT);
+
+ initWiFi();
+ initLittleFS();
+ initWebSocket();
+
+ // Web Server Root URL
+ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
+ { request->send(LittleFS, "/index.html", "text/html"); });
+
+ server.serveStatic("/", LittleFS, "/");
+
+ ElegantOTA.begin(&server);
+
+ // Start server
+ server.begin();
+}
+
+void loop()
+{
+ if ((millis() - lastTime) > timerDelay)
+ {
+ String sensorReadings = getSensorReadings();
+ notifyClients(sensorReadings);
+ if (((millis() - lastSwitch) > switchDelay) && thermocouple.readCelsius() >= (targetTemp - overShoot)) // >= 285°C
+ {
+ digitalWrite(relay, LOW);
+ lastSwitch = millis();
+ }
+ if (((millis() - lastSwitch) > switchDelay) && thermocouple.readCelsius() < (targetTemp - underShoot)) // < 270°C
+ {
+ digitalWrite(relay, HIGH);
+ lastSwitch = millis();
+ }
+ lastTime = millis();
+ }
+
+ ws.cleanupClients();
+ ElegantOTA.loop();
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..bd2ee0f
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,345 @@
+import Highcharts, { type DashStyleValue } from 'highcharts/highstock'
+
+import 'highcharts/modules/accessibility'
+
+import './styles.css'
+
+export const pad = (n: string | number, amount = 2) => {
+ if (amount == 2) {
+ return ('0' + n).slice(-2);
+ } else {
+ return ('00' + n).slice(-3);
+ }
+};
+
+
+let gateway = `ws://${window.location.hostname}/ws`;
+let ws: WebSocket;
+
+const now = new Date()
+
+let container: HTMLElement | null = document.querySelector('#plot')
+let temperatureData = [
+ [now.getTime(), 25]
+]
+
+let relais = 0
+let relaisData = [
+ [now.getTime(), 0]
+]
+
+let temp = 25;
+let targetTemp = 350
+
+const oven = document.querySelector('#oven')
+const temperature = document.querySelector('#temperature')
+const relaisSwitch = document.querySelector('#relais_switch')
+const relaisSwitchInput: HTMLInputElement | null = document.querySelector('#relais_switch_input')
+const targetTempInput: HTMLInputElement | null = document.querySelector('#target_temperature')
+
+let maxValues = 4 * 60 * 60 // 60 minutes (4 values/sec)
+
+let hs: Highcharts.StockChart;
+let hsOptions: Highcharts.Options
+
+const getRelaisBands = () => {
+ let relaisBands = []
+ let active = false;
+
+ for (let r of relaisData) {
+ if (r[1] === 1 && !active) {
+ active = true
+ relaisBands.push({
+ from: r[0],
+ to: relaisData[relaisData.length - 1][0] + 1000,
+ color: 'rgba(255, 0, 0, 0.25)'
+
+ })
+ }
+ if (r[1] === 0 && active) {
+ relaisBands[relaisBands.length - 1]['to'] = r[0]
+ active = false
+ }
+ }
+ return relaisBands
+}
+
+const dashStyle: DashStyleValue = 'Dash'
+const getTargetLine = () => {
+ return [
+ { value: targetTemp, width: 3, dashStyle: dashStyle }
+ ]
+}
+
+const createChart = () => {
+ hsOptions = {
+ chart: {
+ animation: {
+ duration: 250,
+ easing: 'linear'
+ },
+ },
+
+ credits: {
+ enabled: false
+ },
+
+ title: {
+ text: 'Temperature',
+ align: 'left',
+ },
+
+ rangeSelector: {
+ enabled: false,
+ inputDateFormat: '%H:%M:%S'
+ },
+
+ yAxis: {
+ title: {
+ text: '°C'
+ },
+ opposite: false,
+ min: 0,
+ max: 600,
+ tickInterval: 100,
+ plotBands: [{
+ from: 0,
+ to: 250,
+ color: 'rgba(69, 171, 255, 0.34)'
+ }, {
+ from: 250,
+ to: 420,
+ color: 'rgba(0, 255, 60, 0.34)'
+ },
+ {
+ from: 420,
+ to: 530,
+ color: 'rgba(255, 162, 0, 0.4)'
+ },
+ {
+ from: 530,
+ to: 600,
+ color: 'rgba(255, 0, 0, 0.4)'
+ }],
+ plotLines: getTargetLine()
+ },
+
+ xAxis: {
+ type: 'datetime',
+ tickInterval: 30 * 1000, // 30 seconds
+ labels: {
+ rotation: -45,
+ formatter: (f) => {
+ let d = new Date(f.pos)
+ return pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds())
+ },
+ style: {
+ fontSize: '0.55em'
+ }
+ },
+ plotBands: getRelaisBands(),
+
+ },
+
+ legend: {
+ enabled: false,
+ },
+
+ tooltip: {
+ formatter: function () {
+ let d = new Date(this.x)
+ return [pad(d.getHours()) + ":" + pad(d.getMinutes()) + ":" + pad(d.getSeconds()), `${this.y?.toFixed(1)}°C`]
+ }
+ },
+
+ plotOptions: {
+ series: {
+ animation: false,
+ tooltip: {
+ valueDecimals: 2,
+ },
+ }
+ },
+ data: {
+ enablePolling: true,
+ dataRefreshRate: 0.5
+ },
+
+ series: [{
+ type: 'areaspline',
+ name: 'Temperature',
+ data: temperatureData
+ }],
+
+ responsive: {
+ rules: [{
+ condition: {
+ maxWidth: 1000
+ },
+ chartOptions: {
+ legend: {
+ layout: 'horizontal',
+ align: 'center',
+ verticalAlign: 'bottom'
+ }
+ }
+ }]
+ }
+ }
+
+ if (container && hsOptions)
+ hs = Highcharts.stockChart(container, hsOptions)
+}
+
+const onLoad = () => {
+ initWebSocket();
+
+
+ if (container) {
+ createChart()
+
+ relaisSwitch?.addEventListener('click', switchRelais)
+ targetTempInput?.addEventListener('change', changeTargetTemp)
+
+
+ if (import.meta.env.DEV) {
+ // spoof some test data
+ setInterval(() => {
+ if (temperatureData.length > maxValues) {
+ temperatureData.shift()
+ relaisData.shift()
+ }
+ let d = new Date()
+
+ if (temp < 150) {
+ temp = temp + Math.random() * 4 - 1
+ } else if (temp < 20) {
+ temp = temp + Math.random() * 4 - 1.3
+ } else if (temp < targetTemp) {
+ temp = temp + Math.random() * 4 - 1.5
+ } else if (temp >= targetTemp) {
+ temp = temp + Math.random() * 3 - 1.6
+ }
+ if (relais) {
+ temp = temp + 0.2
+ } else {
+ temp = temp - 0.1
+ }
+
+ if (temperature)
+ temperature.innerHTML = temp.toFixed(2);
+ temperatureData.push([d.getTime(), temp])
+ relaisData.push([d.getTime(), relais])
+
+
+ hs.xAxis[0].update({ plotBands: getRelaisBands() })
+ hs.yAxis[0].update({ plotLines: getTargetLine() })
+ hs.series[0].setData(temperatureData, true)
+ }, 250)
+ }
+ }
+}
+
+
+window.addEventListener('load', onLoad);
+
+const getReadings = () => {
+ ws.send("getReadings");
+}
+
+const initWebSocket = () => {
+ if (!import.meta.env.DEV) {
+ console.log('Trying to open a WebSocket connection…');
+ ws = new WebSocket(gateway);
+ ws.onopen = onOpen;
+ ws.onclose = onClose;
+ ws.onmessage = onMessage;
+ }
+}
+
+// When websocket is established, call the getReadings() function
+const onOpen = () => {
+ console.log('Connection opened');
+ getReadings();
+}
+
+const onClose = () => {
+ console.log('Connection closed');
+ setTimeout(initWebSocket, 2000);
+}
+
+// Function that receives the message from the ESP32 with the readings
+const onMessage = (event: MessageEvent) => {
+ let myObj = JSON.parse(event.data);
+ let keys = Object.keys(myObj);
+
+ for (var i = 0; i < keys.length; i++) {
+ let key = keys[i];
+ let element = document.getElementById(key)
+ if (element) {
+ element.innerHTML = myObj[key].toFixed(2);
+ }
+ let d = new Date()
+ if (key === 'temperature') {
+ if (temperatureData.length > maxValues) {
+ temperatureData.shift()
+ }
+ temperatureData.push([d.getTime(), myObj[key]])
+
+ } else if (key === 'relais') {
+ if (relaisData.length > maxValues) {
+ relaisData.shift()
+ }
+ relaisData.push([d.getTime(), myObj[key]])
+ if (myObj[key]) {
+ oven?.classList.add('on')
+ relaisSwitch?.classList.add('on')
+ if (relaisSwitchInput)
+ relaisSwitchInput.checked = true
+ } else {
+ oven?.classList.remove('on')
+ relaisSwitch?.classList.remove('on')
+ if (relaisSwitchInput)
+ relaisSwitchInput.checked = false
+ }
+ }
+
+ hs.xAxis[0].update({ plotBands: getRelaisBands() })
+ hs.yAxis[0].update({ plotLines: getTargetLine() })
+ hs.series[0].setData(temperatureData, true)
+ }
+}
+
+const switchRelais = () => {
+ console.log('Send switch relais')
+ if (!import.meta.env.DEV) {
+ ws.send("switchRelais");
+ } else {
+ // spoof some test data
+ if (!relais) {
+ relais = 1
+ oven?.classList.add('on')
+ relaisSwitch?.classList.add('on')
+ if (relaisSwitchInput)
+ relaisSwitchInput.checked = true
+ } else {
+ relais = 0
+ oven?.classList.remove('on')
+
+ relaisSwitch?.classList.remove('on')
+ if (relaisSwitchInput)
+ relaisSwitchInput.checked = false
+ }
+ }
+}
+
+const changeTargetTemp = (e: Event) => {
+ const target = e.target as HTMLTextAreaElement;
+ let value = Number(target?.value)
+ targetTemp = value
+ console.log('Changed target temp to ' + targetTemp)
+ if (!import.meta.env.DEV) {
+ ws.send("setTargetTemp: " + pad(targetTemp, 3));
+ }
+}
+
diff --git a/src/secrets.example.h b/src/secrets.example.h
new file mode 100644
index 0000000..caba719
--- /dev/null
+++ b/src/secrets.example.h
@@ -0,0 +1,3 @@
+// Replace with your network credentials
+const char *ssid = "WIFI_SSID";
+const char *password = "WIFI_PWD";
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..c0997ca
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,204 @@
+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
+}
+
+p {
+ margin: 0;
+}
+
+.nav {
+ background-color: black;
+ color: white;
+ width: full;
+ text-align: center;
+ padding: 1em;
+}
+
+.nav-title {
+ font-size: xx-large;
+}
+
+.content {
+ max-width: 1000px;
+ margin: auto;
+}
+
+.card-grid {
+ margin-top: 15px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 2em;
+}
+
+.graph {
+ height: 310px;
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+}
+
+.plot-wrapper {
+ height: 400px;
+}
+
+.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;
+}
+
+.reading {
+ font-weight: bold;
+ margin-top: 18px;
+}
+
+
+@property --ovenColor1 {
+ syntax: '';
+ initial-value: rgba(255, 0, 0, 0);
+ inherits: false;
+}
+
+
+@property --ovenColor2 {
+ syntax: '';
+ initial-value: rgba(255, 0, 0, 0);
+ inherits: false;
+}
+
+.oven {
+ display: flex;
+ justify-content: center;
+ margin: auto;
+}
+
+.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;
+}
+
+.oven.on .oven-wrapper {
+ --ovenColor1: rgb(255, 60, 0);
+ --ovenColor2: rgba(255, 60, 0, 0.4);
+}
+
+.oven .oven-img-wrapper {
+ width: 65%;
+}
+
+.oven img {
+ width: 100%;
+}
+
+.relais {
+ cursor: pointer;
+}
+
+.onoffswitch {
+ pointer-events: none;
+ margin-top: 10px;
+ position: relative;
+ width: 90px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+}
+
+.onoffswitch-checkbox {
+ position: absolute;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.onoffswitch-label {
+ display: block;
+ overflow: hidden;
+ cursor: pointer;
+ border: 2px solid #999999;
+ border-radius: 20px;
+}
+
+.onoffswitch-inner {
+ display: block;
+ width: 200%;
+ margin-left: -100%;
+ transition: margin 0.3s ease-in 0s;
+}
+
+.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 {
+ content: "ON";
+ padding-left: 10px;
+ background-color: #C43535;
+ color: #FFFFFF;
+}
+
+.onoffswitch-inner:after {
+ content: "OFF";
+ padding-right: 10px;
+ background-color: #EEEEEE;
+ color: #999999;
+ text-align: right;
+}
+
+.onoffswitch-switch {
+ display: block;
+ width: 18px;
+ 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;
+}
+
+.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-inner {
+ margin-left: 0;
+}
+
+.onoffswitch-checkbox:checked+.onoffswitch-label .onoffswitch-switch {
+ right: 0px;
+}
+
+.settings {
+ margin-top: 50px;
+}
+
+.settings input {
+ max-width: 50px;
+ font-size: larger;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/src/vite.dev.d.ts b/src/vite.dev.d.ts
new file mode 100644
index 0000000..704b833
--- /dev/null
+++ b/src/vite.dev.d.ts
@@ -0,0 +1,7 @@
+interface ImportMetaEnv {
+ readonly DEV: boolean;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..8b62f07
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": [
+ "ES2022",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": true,
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..e56f581
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vite';
+
+export default defineConfig({
+ build: {
+ outDir: 'data',
+ chunkSizeWarningLimit: 1000
+ }
+});