From 079e101e918341708000ef94220145d6d7d84168 Mon Sep 17 00:00:00 2001 From: Hans Bacher Date: Mon, 19 May 2025 21:11:09 +0200 Subject: [PATCH 01/53] first commit --- .github/workflows/ci.yaml | 25 + .gitignore | 21 + CMakeLists.txt | 3 + ESP32-sveltekit.code-workspace | 64 + README.md | 1 + config.ini | 8 + docs/buildprocess.md | 173 + docs/components.md | 157 + docs/gettingstarted.md | 146 + docs/index.md | 60 + docs/media/Login_dark.png | Bin 0 -> 80780 bytes docs/media/Login_light.png | Bin 0 -> 84117 bytes docs/media/PIO-upload.png | Bin 0 -> 22236 bytes docs/media/Screenshot_dark.png | Bin 0 -> 64905 bytes docs/media/Screenshot_light.png | Bin 0 -> 69036 bytes docs/media/Screenshot_mobile.png | Bin 0 -> 203141 bytes docs/media/favicon.png | Bin 0 -> 1571 bytes docs/media/framework.png | Bin 0 -> 58594 bytes docs/media/mkdocs_gh-pages.PNG | Bin 0 -> 25058 bytes docs/media/svelte-logo.png | Bin 0 -> 19385 bytes docs/restfulapi.md | 32 + docs/statefulservice.md | 609 +++ docs/stores.md | 88 + docs/structure.md | 103 + docs/sveltekit.md | 92 + factory_settings.ini | 54 + features.ini | 13 + interface/.eslintignore | 13 + interface/.eslintrc.cjs | 20 + interface/.gitignore | 10 + interface/.npmrc | 1 + interface/.prettierignore | 13 + interface/.prettierrc | 9 + interface/package-lock.json | 3693 +++++++++++++++++ interface/package.json | 53 + interface/src/app.css | 103 + interface/src/app.d.ts | 16 + interface/src/app.html | 12 + interface/src/lib/DaisyUiHelper.ts | 5 + .../src/lib/assets/icons/radio-module.svg | 53 + .../lib/assets/icons/smoke-detector-2xl.svg | 104 + .../src/lib/assets/icons/smoke-detector-m.svg | 65 + .../lib/assets/icons/smoke-detector-xl.svg | 106 + .../src/lib/assets/icons/smoke-detector.svg | 81 + .../lib/assets/json/topicPayloadConfig.json | 18 + interface/src/lib/assets/logo.png | Bin 0 -> 19385 bytes .../lib/components/BatteryIndicator.svelte | 26 + .../src/lib/components/Collapsible.svelte | 74 + .../src/lib/components/ConfirmDialog.svelte | 60 + .../lib/components/GithubUpdateDialog.svelte | 101 + .../src/lib/components/InfoDialog.svelte | 49 + .../src/lib/components/InputPassword.svelte | 65 + interface/src/lib/components/InputUnit.svelte | 14 + .../src/lib/components/RSSIIndicator.svelte | 42 + .../src/lib/components/SettingsCard.svelte | 97 + interface/src/lib/components/Spinner.svelte | 11 + .../src/lib/components/UpdateIndicator.svelte | 118 + .../src/lib/components/toasts/Toast.svelte | 38 + .../lib/components/toasts/notifications.ts | 42 + interface/src/lib/stores/analytics.ts | 44 + interface/src/lib/stores/battery.ts | 28 + interface/src/lib/stores/socket.ts | 122 + interface/src/lib/stores/telemetry.ts | 56 + interface/src/lib/stores/user.ts | 55 + interface/src/lib/types/models.ts | 284 ++ interface/src/lib/utils.ts | 21 + interface/src/routes/+error.svelte | 19 + interface/src/routes/+layout.svelte | 177 + interface/src/routes/+layout.ts | 17 + interface/src/routes/+page.svelte | 19 + interface/src/routes/connections/+page.ts | 7 + .../src/routes/connections/mqtt/+page.svelte | 17 + .../src/routes/connections/mqtt/+page.ts | 7 + .../connections/mqtt/GatewayMQTTConfig.svelte | 172 + .../src/routes/connections/mqtt/MQTT.svelte | 308 ++ .../src/routes/connections/ntp/+page.svelte | 17 + interface/src/routes/connections/ntp/+page.ts | 7 + .../src/routes/connections/ntp/NTP.svelte | 301 ++ .../src/routes/connections/ntp/timezones.ts | 466 +++ .../src/routes/ffc/settings/+page.svelte | 442 ++ interface/src/routes/ffc/settings/+page.ts | 7 + interface/src/routes/login.svelte | 122 + interface/src/routes/menu.svelte | 259 ++ interface/src/routes/statusbar.svelte | 81 + interface/src/routes/system/+page.ts | 7 + .../src/routes/system/metrics/+page.svelte | 30 + interface/src/routes/system/metrics/+page.ts | 5 + .../system/metrics/BatteryMetrics.svelte | 160 + .../system/metrics/SystemMetrics.svelte | 316 ++ .../src/routes/system/status/+page.svelte | 19 + interface/src/routes/system/status/+page.ts | 5 + .../routes/system/status/SystemStatus.svelte | 368 ++ .../src/routes/system/update/+page.svelte | 26 + interface/src/routes/system/update/+page.ts | 5 + .../update/GithubFirmwareManager.svelte | 169 + .../system/update/UploadFirmware.svelte | 69 + interface/src/routes/user/+page.svelte | 237 ++ interface/src/routes/user/+page.ts | 7 + interface/src/routes/user/EditUser.svelte | 118 + interface/src/routes/wifi/+page.ts | 7 + interface/src/routes/wifi/ap/+page.svelte | 17 + interface/src/routes/wifi/ap/+page.ts | 7 + .../src/routes/wifi/ap/Accesspoint.svelte | 450 ++ interface/src/routes/wifi/sta/+page.svelte | 17 + interface/src/routes/wifi/sta/+page.ts | 7 + interface/src/routes/wifi/sta/Scan.svelte | 160 + interface/src/routes/wifi/sta/Wifi.svelte | 790 ++++ interface/static/favicon.png | Bin 0 -> 1571 bytes interface/static/manifest.json | 12 + interface/svelte.config.js | 22 + interface/tsconfig.json | 17 + interface/vite-plugin-littlefs.ts | 32 + interface/vite.config.ts | 35 + lib/PsychicHttp/.gitignore | 60 + lib/PsychicHttp/CHANGELOG.md | 34 + lib/PsychicHttp/LICENSE | 7 + lib/PsychicHttp/README.md | 826 ++++ lib/PsychicHttp/RELEASE.md | 6 + lib/PsychicHttp/assets/handler-callbacks.svg | 4 + lib/PsychicHttp/assets/request-flow.svg | 4 + lib/PsychicHttp/library.json | 53 + lib/PsychicHttp/request flow.drawio | 1 + lib/PsychicHttp/src/ChunkPrinter.cpp | 85 + lib/PsychicHttp/src/ChunkPrinter.h | 27 + lib/PsychicHttp/src/PsychicClient.cpp | 72 + lib/PsychicHttp/src/PsychicClient.h | 35 + lib/PsychicHttp/src/PsychicCore.h | 107 + lib/PsychicHttp/src/PsychicEndpoint.cpp | 90 + lib/PsychicHttp/src/PsychicEndpoint.h | 37 + lib/PsychicHttp/src/PsychicEventSource.cpp | 225 + lib/PsychicHttp/src/PsychicEventSource.h | 82 + lib/PsychicHttp/src/PsychicFileResponse.cpp | 159 + lib/PsychicHttp/src/PsychicFileResponse.h | 23 + lib/PsychicHttp/src/PsychicHandler.cpp | 111 + lib/PsychicHttp/src/PsychicHandler.h | 66 + lib/PsychicHttp/src/PsychicHttp.h | 24 + lib/PsychicHttp/src/PsychicHttpServer.cpp | 366 ++ lib/PsychicHttp/src/PsychicHttpServer.h | 81 + lib/PsychicHttp/src/PsychicHttpsServer.cpp | 61 + lib/PsychicHttp/src/PsychicHttpsServer.h | 39 + lib/PsychicHttp/src/PsychicJson.cpp | 133 + lib/PsychicHttp/src/PsychicJson.h | 89 + lib/PsychicHttp/src/PsychicRequest.cpp | 583 +++ lib/PsychicHttp/src/PsychicRequest.h | 98 + lib/PsychicHttp/src/PsychicResponse.cpp | 162 + lib/PsychicHttp/src/PsychicResponse.h | 46 + .../src/PsychicStaticFileHander.cpp | 181 + .../src/PsychicStaticFileHandler.h | 41 + lib/PsychicHttp/src/PsychicStreamResponse.cpp | 94 + lib/PsychicHttp/src/PsychicStreamResponse.h | 35 + lib/PsychicHttp/src/PsychicUploadHandler.cpp | 395 ++ lib/PsychicHttp/src/PsychicUploadHandler.h | 68 + lib/PsychicHttp/src/PsychicWebHandler.cpp | 74 + lib/PsychicHttp/src/PsychicWebHandler.h | 34 + lib/PsychicHttp/src/PsychicWebParameter.h | 25 + lib/PsychicHttp/src/PsychicWebSocket.cpp | 258 ++ lib/PsychicHttp/src/PsychicWebSocket.h | 70 + lib/PsychicHttp/src/TemplatePrinter.cpp | 90 + lib/PsychicHttp/src/TemplatePrinter.h | 51 + lib/PsychicHttp/src/async_worker.cpp | 222 + lib/PsychicHttp/src/async_worker.h | 36 + lib/PsychicHttp/src/http_status.cpp | 194 + lib/PsychicHttp/src/http_status.h | 15 + lib/framework/APSettingsService.cpp | 143 + lib/framework/APSettingsService.h | 177 + lib/framework/APStatus.cpp | 50 + lib/framework/APStatus.h | 44 + lib/framework/AnalyticsService.h | 59 + lib/framework/ArduinoJsonJWT.cpp | 158 + lib/framework/ArduinoJsonJWT.h | 47 + lib/framework/AuthenticationService.cpp | 53 + lib/framework/AuthenticationService.h | 40 + lib/framework/BatteryService.cpp | 54 + lib/framework/BatteryService.h | 42 + lib/framework/DownloadFirmwareService.cpp | 170 + lib/framework/DownloadFirmwareService.h | 45 + lib/framework/ESP32SvelteKit.cpp | 254 ++ lib/framework/ESP32SvelteKit.h | 257 ++ lib/framework/ESPFS.h | 21 + lib/framework/EventEndpoint.h | 71 + lib/framework/EventSocket.cpp | 231 ++ lib/framework/EventSocket.h | 68 + lib/framework/FSPersistence.h | 142 + lib/framework/FactoryResetService.cpp | 58 + lib/framework/FactoryResetService.h | 44 + lib/framework/Features.h | 65 + lib/framework/FeaturesService.cpp | 116 + lib/framework/FeaturesService.h | 52 + lib/framework/HttpEndpoint.h | 112 + lib/framework/IPUtils.h | 35 + lib/framework/JsonUtils.h | 52 + lib/framework/LICENSE | 169 + lib/framework/MqttEndpoint.h | 160 + lib/framework/MqttSettingsService.cpp | 202 + lib/framework/MqttSettingsService.h | 154 + lib/framework/MqttStatus.cpp | 51 + lib/framework/MqttStatus.h | 44 + lib/framework/NTPSettingsService.cpp | 114 + lib/framework/NTPSettingsService.h | 95 + lib/framework/NTPStatus.cpp | 78 + lib/framework/NTPStatus.h | 41 + lib/framework/NotificationService.cpp | 22 + lib/framework/NotificationService.h | 43 + lib/framework/RestartService.cpp | 37 + lib/framework/RestartService.h | 53 + lib/framework/SecurityManager.h | 117 + lib/framework/SecuritySettingsService.cpp | 230 + lib/framework/SecuritySettingsService.h | 150 + lib/framework/SettingValue.cpp | 67 + lib/framework/SettingValue.h | 29 + lib/framework/SleepService.cpp | 120 + lib/framework/SleepService.h | 66 + lib/framework/StatefulService.cpp | 18 + lib/framework/StatefulService.h | 214 + lib/framework/SystemStatus.cpp | 144 + lib/framework/SystemStatus.h | 40 + lib/framework/UploadFirmwareService.cpp | 205 + lib/framework/UploadFirmwareService.h | 58 + lib/framework/WebSocketClient.bak | 242 ++ lib/framework/WebSocketServer.h | 158 + lib/framework/WiFiScanner.cpp | 78 + lib/framework/WiFiScanner.h | 42 + lib/framework/WiFiSettingsService.cpp | 262 ++ lib/framework/WiFiSettingsService.h | 240 ++ lib/framework/WiFiStatus.cpp | 98 + lib/framework/WiFiStatus.h | 47 + platformio.ini | 95 + scripts/build_interface.py | 166 + scripts/generate_cert_bundle.py | 209 + scripts/rename_fw.py | 89 + src/AlarmLinesService.cpp | 388 ++ src/AlarmLinesService.h | 195 + src/CC1101Controller.cpp | 65 + src/CC1101Controller.h | 38 + src/CMakeLists.txt | 6 + src/GatewayDevicesService.cpp | 134 + src/GatewayDevicesService.h | 248 ++ src/GatewayMqttSettingsService.cpp | 22 + src/GatewayMqttSettingsService.h | 53 + src/GatewaySettingsService.cpp | 22 + src/GatewaySettingsService.h | 156 + src/GeniusGateway.cpp | 357 ++ src/GeniusGateway.h | 129 + src/Utils.cpp | 25 + src/Utils.hpp | 22 + src/VisualizerSettingsService.cpp | 22 + src/VisualizerSettingsService.h | 58 + src/WebSocketLogger.h | 126 + src/cc1101.c | 643 +++ src/cc1101.h | 306 ++ src/main.cpp | 61 + ssl_certs/DigiCert_Global_Root_CA.pem | 22 + todo.md | 29 + 253 files changed, 28843 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 ESP32-sveltekit.code-workspace create mode 100644 README.md create mode 100644 config.ini create mode 100644 docs/buildprocess.md create mode 100644 docs/components.md create mode 100644 docs/gettingstarted.md create mode 100644 docs/index.md create mode 100644 docs/media/Login_dark.png create mode 100644 docs/media/Login_light.png create mode 100644 docs/media/PIO-upload.png create mode 100644 docs/media/Screenshot_dark.png create mode 100644 docs/media/Screenshot_light.png create mode 100644 docs/media/Screenshot_mobile.png create mode 100644 docs/media/favicon.png create mode 100644 docs/media/framework.png create mode 100644 docs/media/mkdocs_gh-pages.PNG create mode 100644 docs/media/svelte-logo.png create mode 100644 docs/restfulapi.md create mode 100644 docs/statefulservice.md create mode 100644 docs/stores.md create mode 100644 docs/structure.md create mode 100644 docs/sveltekit.md create mode 100644 factory_settings.ini create mode 100644 features.ini create mode 100644 interface/.eslintignore create mode 100644 interface/.eslintrc.cjs create mode 100644 interface/.gitignore create mode 100644 interface/.npmrc create mode 100644 interface/.prettierignore create mode 100644 interface/.prettierrc create mode 100644 interface/package-lock.json create mode 100644 interface/package.json create mode 100644 interface/src/app.css create mode 100644 interface/src/app.d.ts create mode 100644 interface/src/app.html create mode 100644 interface/src/lib/DaisyUiHelper.ts create mode 100644 interface/src/lib/assets/icons/radio-module.svg create mode 100644 interface/src/lib/assets/icons/smoke-detector-2xl.svg create mode 100644 interface/src/lib/assets/icons/smoke-detector-m.svg create mode 100644 interface/src/lib/assets/icons/smoke-detector-xl.svg create mode 100644 interface/src/lib/assets/icons/smoke-detector.svg create mode 100644 interface/src/lib/assets/json/topicPayloadConfig.json create mode 100644 interface/src/lib/assets/logo.png create mode 100644 interface/src/lib/components/BatteryIndicator.svelte create mode 100644 interface/src/lib/components/Collapsible.svelte create mode 100644 interface/src/lib/components/ConfirmDialog.svelte create mode 100644 interface/src/lib/components/GithubUpdateDialog.svelte create mode 100644 interface/src/lib/components/InfoDialog.svelte create mode 100644 interface/src/lib/components/InputPassword.svelte create mode 100644 interface/src/lib/components/InputUnit.svelte create mode 100644 interface/src/lib/components/RSSIIndicator.svelte create mode 100644 interface/src/lib/components/SettingsCard.svelte create mode 100644 interface/src/lib/components/Spinner.svelte create mode 100644 interface/src/lib/components/UpdateIndicator.svelte create mode 100644 interface/src/lib/components/toasts/Toast.svelte create mode 100644 interface/src/lib/components/toasts/notifications.ts create mode 100644 interface/src/lib/stores/analytics.ts create mode 100644 interface/src/lib/stores/battery.ts create mode 100644 interface/src/lib/stores/socket.ts create mode 100644 interface/src/lib/stores/telemetry.ts create mode 100644 interface/src/lib/stores/user.ts create mode 100644 interface/src/lib/types/models.ts create mode 100644 interface/src/lib/utils.ts create mode 100644 interface/src/routes/+error.svelte create mode 100644 interface/src/routes/+layout.svelte create mode 100644 interface/src/routes/+layout.ts create mode 100644 interface/src/routes/+page.svelte create mode 100644 interface/src/routes/connections/+page.ts create mode 100644 interface/src/routes/connections/mqtt/+page.svelte create mode 100644 interface/src/routes/connections/mqtt/+page.ts create mode 100644 interface/src/routes/connections/mqtt/GatewayMQTTConfig.svelte create mode 100644 interface/src/routes/connections/mqtt/MQTT.svelte create mode 100644 interface/src/routes/connections/ntp/+page.svelte create mode 100644 interface/src/routes/connections/ntp/+page.ts create mode 100644 interface/src/routes/connections/ntp/NTP.svelte create mode 100644 interface/src/routes/connections/ntp/timezones.ts create mode 100644 interface/src/routes/ffc/settings/+page.svelte create mode 100644 interface/src/routes/ffc/settings/+page.ts create mode 100644 interface/src/routes/login.svelte create mode 100644 interface/src/routes/menu.svelte create mode 100644 interface/src/routes/statusbar.svelte create mode 100644 interface/src/routes/system/+page.ts create mode 100644 interface/src/routes/system/metrics/+page.svelte create mode 100644 interface/src/routes/system/metrics/+page.ts create mode 100644 interface/src/routes/system/metrics/BatteryMetrics.svelte create mode 100644 interface/src/routes/system/metrics/SystemMetrics.svelte create mode 100644 interface/src/routes/system/status/+page.svelte create mode 100644 interface/src/routes/system/status/+page.ts create mode 100644 interface/src/routes/system/status/SystemStatus.svelte create mode 100644 interface/src/routes/system/update/+page.svelte create mode 100644 interface/src/routes/system/update/+page.ts create mode 100644 interface/src/routes/system/update/GithubFirmwareManager.svelte create mode 100644 interface/src/routes/system/update/UploadFirmware.svelte create mode 100644 interface/src/routes/user/+page.svelte create mode 100644 interface/src/routes/user/+page.ts create mode 100644 interface/src/routes/user/EditUser.svelte create mode 100644 interface/src/routes/wifi/+page.ts create mode 100644 interface/src/routes/wifi/ap/+page.svelte create mode 100644 interface/src/routes/wifi/ap/+page.ts create mode 100644 interface/src/routes/wifi/ap/Accesspoint.svelte create mode 100644 interface/src/routes/wifi/sta/+page.svelte create mode 100644 interface/src/routes/wifi/sta/+page.ts create mode 100644 interface/src/routes/wifi/sta/Scan.svelte create mode 100644 interface/src/routes/wifi/sta/Wifi.svelte create mode 100644 interface/static/favicon.png create mode 100644 interface/static/manifest.json create mode 100644 interface/svelte.config.js create mode 100644 interface/tsconfig.json create mode 100644 interface/vite-plugin-littlefs.ts create mode 100644 interface/vite.config.ts create mode 100644 lib/PsychicHttp/.gitignore create mode 100644 lib/PsychicHttp/CHANGELOG.md create mode 100644 lib/PsychicHttp/LICENSE create mode 100644 lib/PsychicHttp/README.md create mode 100644 lib/PsychicHttp/RELEASE.md create mode 100644 lib/PsychicHttp/assets/handler-callbacks.svg create mode 100644 lib/PsychicHttp/assets/request-flow.svg create mode 100644 lib/PsychicHttp/library.json create mode 100644 lib/PsychicHttp/request flow.drawio create mode 100644 lib/PsychicHttp/src/ChunkPrinter.cpp create mode 100644 lib/PsychicHttp/src/ChunkPrinter.h create mode 100644 lib/PsychicHttp/src/PsychicClient.cpp create mode 100644 lib/PsychicHttp/src/PsychicClient.h create mode 100644 lib/PsychicHttp/src/PsychicCore.h create mode 100644 lib/PsychicHttp/src/PsychicEndpoint.cpp create mode 100644 lib/PsychicHttp/src/PsychicEndpoint.h create mode 100644 lib/PsychicHttp/src/PsychicEventSource.cpp create mode 100644 lib/PsychicHttp/src/PsychicEventSource.h create mode 100644 lib/PsychicHttp/src/PsychicFileResponse.cpp create mode 100644 lib/PsychicHttp/src/PsychicFileResponse.h create mode 100644 lib/PsychicHttp/src/PsychicHandler.cpp create mode 100644 lib/PsychicHttp/src/PsychicHandler.h create mode 100644 lib/PsychicHttp/src/PsychicHttp.h create mode 100644 lib/PsychicHttp/src/PsychicHttpServer.cpp create mode 100644 lib/PsychicHttp/src/PsychicHttpServer.h create mode 100644 lib/PsychicHttp/src/PsychicHttpsServer.cpp create mode 100644 lib/PsychicHttp/src/PsychicHttpsServer.h create mode 100644 lib/PsychicHttp/src/PsychicJson.cpp create mode 100644 lib/PsychicHttp/src/PsychicJson.h create mode 100644 lib/PsychicHttp/src/PsychicRequest.cpp create mode 100644 lib/PsychicHttp/src/PsychicRequest.h create mode 100644 lib/PsychicHttp/src/PsychicResponse.cpp create mode 100644 lib/PsychicHttp/src/PsychicResponse.h create mode 100644 lib/PsychicHttp/src/PsychicStaticFileHander.cpp create mode 100644 lib/PsychicHttp/src/PsychicStaticFileHandler.h create mode 100644 lib/PsychicHttp/src/PsychicStreamResponse.cpp create mode 100644 lib/PsychicHttp/src/PsychicStreamResponse.h create mode 100644 lib/PsychicHttp/src/PsychicUploadHandler.cpp create mode 100644 lib/PsychicHttp/src/PsychicUploadHandler.h create mode 100644 lib/PsychicHttp/src/PsychicWebHandler.cpp create mode 100644 lib/PsychicHttp/src/PsychicWebHandler.h create mode 100644 lib/PsychicHttp/src/PsychicWebParameter.h create mode 100644 lib/PsychicHttp/src/PsychicWebSocket.cpp create mode 100644 lib/PsychicHttp/src/PsychicWebSocket.h create mode 100644 lib/PsychicHttp/src/TemplatePrinter.cpp create mode 100644 lib/PsychicHttp/src/TemplatePrinter.h create mode 100644 lib/PsychicHttp/src/async_worker.cpp create mode 100644 lib/PsychicHttp/src/async_worker.h create mode 100644 lib/PsychicHttp/src/http_status.cpp create mode 100644 lib/PsychicHttp/src/http_status.h create mode 100644 lib/framework/APSettingsService.cpp create mode 100644 lib/framework/APSettingsService.h create mode 100644 lib/framework/APStatus.cpp create mode 100644 lib/framework/APStatus.h create mode 100644 lib/framework/AnalyticsService.h create mode 100644 lib/framework/ArduinoJsonJWT.cpp create mode 100644 lib/framework/ArduinoJsonJWT.h create mode 100644 lib/framework/AuthenticationService.cpp create mode 100644 lib/framework/AuthenticationService.h create mode 100644 lib/framework/BatteryService.cpp create mode 100644 lib/framework/BatteryService.h create mode 100644 lib/framework/DownloadFirmwareService.cpp create mode 100644 lib/framework/DownloadFirmwareService.h create mode 100644 lib/framework/ESP32SvelteKit.cpp create mode 100644 lib/framework/ESP32SvelteKit.h create mode 100644 lib/framework/ESPFS.h create mode 100644 lib/framework/EventEndpoint.h create mode 100644 lib/framework/EventSocket.cpp create mode 100644 lib/framework/EventSocket.h create mode 100644 lib/framework/FSPersistence.h create mode 100644 lib/framework/FactoryResetService.cpp create mode 100644 lib/framework/FactoryResetService.h create mode 100644 lib/framework/Features.h create mode 100644 lib/framework/FeaturesService.cpp create mode 100644 lib/framework/FeaturesService.h create mode 100644 lib/framework/HttpEndpoint.h create mode 100644 lib/framework/IPUtils.h create mode 100644 lib/framework/JsonUtils.h create mode 100644 lib/framework/LICENSE create mode 100644 lib/framework/MqttEndpoint.h create mode 100644 lib/framework/MqttSettingsService.cpp create mode 100644 lib/framework/MqttSettingsService.h create mode 100644 lib/framework/MqttStatus.cpp create mode 100644 lib/framework/MqttStatus.h create mode 100644 lib/framework/NTPSettingsService.cpp create mode 100644 lib/framework/NTPSettingsService.h create mode 100644 lib/framework/NTPStatus.cpp create mode 100644 lib/framework/NTPStatus.h create mode 100644 lib/framework/NotificationService.cpp create mode 100644 lib/framework/NotificationService.h create mode 100644 lib/framework/RestartService.cpp create mode 100644 lib/framework/RestartService.h create mode 100644 lib/framework/SecurityManager.h create mode 100644 lib/framework/SecuritySettingsService.cpp create mode 100644 lib/framework/SecuritySettingsService.h create mode 100644 lib/framework/SettingValue.cpp create mode 100644 lib/framework/SettingValue.h create mode 100644 lib/framework/SleepService.cpp create mode 100644 lib/framework/SleepService.h create mode 100644 lib/framework/StatefulService.cpp create mode 100644 lib/framework/StatefulService.h create mode 100644 lib/framework/SystemStatus.cpp create mode 100644 lib/framework/SystemStatus.h create mode 100644 lib/framework/UploadFirmwareService.cpp create mode 100644 lib/framework/UploadFirmwareService.h create mode 100644 lib/framework/WebSocketClient.bak create mode 100644 lib/framework/WebSocketServer.h create mode 100644 lib/framework/WiFiScanner.cpp create mode 100644 lib/framework/WiFiScanner.h create mode 100644 lib/framework/WiFiSettingsService.cpp create mode 100644 lib/framework/WiFiSettingsService.h create mode 100644 lib/framework/WiFiStatus.cpp create mode 100644 lib/framework/WiFiStatus.h create mode 100644 platformio.ini create mode 100644 scripts/build_interface.py create mode 100644 scripts/generate_cert_bundle.py create mode 100644 scripts/rename_fw.py create mode 100644 src/AlarmLinesService.cpp create mode 100644 src/AlarmLinesService.h create mode 100644 src/CC1101Controller.cpp create mode 100644 src/CC1101Controller.h create mode 100644 src/CMakeLists.txt create mode 100644 src/GatewayDevicesService.cpp create mode 100644 src/GatewayDevicesService.h create mode 100644 src/GatewayMqttSettingsService.cpp create mode 100644 src/GatewayMqttSettingsService.h create mode 100644 src/GatewaySettingsService.cpp create mode 100644 src/GatewaySettingsService.h create mode 100644 src/GeniusGateway.cpp create mode 100644 src/GeniusGateway.h create mode 100644 src/Utils.cpp create mode 100644 src/Utils.hpp create mode 100644 src/VisualizerSettingsService.cpp create mode 100644 src/VisualizerSettingsService.h create mode 100644 src/WebSocketLogger.h create mode 100644 src/cc1101.c create mode 100644 src/cc1101.h create mode 100644 src/main.cpp create mode 100644 ssl_certs/DigiCert_Global_Root_CA.pem create mode 100644 todo.md diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 000000000..0c8adc74a --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,25 @@ +name: ci +on: + push: + branches: + - master + - main +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v3 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..9e129ab60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +.pio +.clang_complete +.gcc-flags.json +*Thumbs.db +/data/www +/interface/build +/interface/node_modules +/interface/.eslintcache +.vscode +node_modules +/releases +/src/certs +/temp +/build/firmware +/lib/framework/WWWData.h +*WWWData.h +lib/framework/WWWData.h +ssl_certs/cacert.pem +/logs +/build +sdkconfig diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..d0d236de2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.16.0) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(fridge-fan-control) diff --git a/ESP32-sveltekit.code-workspace b/ESP32-sveltekit.code-workspace new file mode 100644 index 000000000..d2bc12851 --- /dev/null +++ b/ESP32-sveltekit.code-workspace @@ -0,0 +1,64 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.associations": { + "*.tcc": "cpp", + "algorithm": "cpp", + "esp32-hal-misc.c": "cpp", + "esp_crt_bundle.h": "c", + "functional": "cpp", + "array": "cpp", + "atomic": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "unordered_map": "cpp", + "vector": "cpp", + "exception": "cpp", + "iterator": "cpp", + "map": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "regex": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "fstream": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "istream": "cpp", + "limits": "cpp", + "new": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "cinttypes": "cpp", + "typeinfo": "cpp", + "unordered_set": "cpp", + "iomanip": "cpp" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 000000000..65dcb9e6a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Fridge Fan Control diff --git a/config.ini b/config.ini new file mode 100644 index 000000000..aff023b66 --- /dev/null +++ b/config.ini @@ -0,0 +1,8 @@ +[config] +build_flags = + -D CONFIG_CSN_GPIO=45 + -D CONFIG_MISO_GPIO=11 + -D CONFIG_GDO0_GPIO=47 + -D CONFIG_MOSI_GPIO=13 + -D CONFIG_SCK_GPIO=12 + -D HOST_ID=1 diff --git a/docs/buildprocess.md b/docs/buildprocess.md new file mode 100644 index 000000000..cee2397fc --- /dev/null +++ b/docs/buildprocess.md @@ -0,0 +1,173 @@ +# Build Process + +The build process is controlled by [platformio.ini](https://github.com/theelims/ESP32-sveltekit/platformio.ini) and automates the build of the front end website with Vite as well as the binary compilation for the ESP32 firmware. Whenever PlatformIO is building a new binary it will call the python script [build_interface.py](https://github.com/theelims/ESP32-sveltekit/scripts/build_interface.py) to action. It will check the frontend files for changes. If necessary it will start the Vite build and gzip the resulting files either to the `data/` directory or embed them into a header file. In case the WWW files go into a LITTLEFS partition a file system image for the flash is created for the default build environment and upload to the ESP32. + +## Changing the JS package manager + +This project uses NPM as the default package manager. However, many users might have different preferences and like to use YARN or PNPM instead. Just switch the interface to one of the other package managers. The build script identify the package manager by the presence of its lock-file and start the vite build process accordingly. + +## Serving from Flash or Embedding into the Binary + +The front end website can be served either from the LITTLEFS partition of the flash, or embedded into the firmware binary (default). Later has the advantage that only one binary needs to be distributed easing the OTA process. Further more this is desirable if you like to preserve the settings stored in the LITTLEFS partition, or have other files there that need to survive a firmware update. To serve from the LITTLEFS partition instead please comment the following build flag out: + +```ini +build_flags = + ... + -D EMBED_WWW +``` + +### Partitioning + +If you choose to embed the frontend it becomes part of the firmware binary (default). As many ESP32 modules only come with 4MB built-in flash this results in the binary being too large for the reserved flash. Therefor a partition scheme with a larger section for the executable code is selected. However, this limits the LITTLEFS partition to 200kb. There are a great number of [default partition tables](https://github.com/espressif/arduino-esp32/tree/master/tools/partitions) for Arduino-ESP32 to choose from. If you have 8MB or 16MB flash this would be your first choice. If you don't need OTA you can choose a partition scheme without OTA. + +Should you want to deploy the frontend from the flash's LITTLEFS partition on a 4MB chip you need to comment out the following two lines. Otherwise the 200kb will not be large enough to host the front end code. + +```ini +board_build.partitions = min_spiffs.csv +``` + +## Selecting Features + +Many of the framework's built in features may be enabled or disabled as required at compile time. This can help save sketch space and memory if your project does not require the full suite of features. The access point and WiFi management features are "core features" and are always enabled. Feature selection may be controlled with the build flags defined in [features.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/features.ini). + +Customize the settings as you see fit. A value of 0 will disable the specified feature: + +```ini + -D FT_SECURITY=1 + -D FT_MQTT=1 + -D FT_NTP=1 + -D FT_UPLOAD_FIRMWARE=1 + -D FT_DOWNLOAD_FIRMWARE=1 + -D FT_SLEEP=1 + -D FT_BATTERY=1 +``` + +| Flag | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| FT_SECURITY | Controls whether the [security features](statefulservice.md#security-features) are enabled. Disabling this means you won't need to authenticate to access the device and all authentication predicates will be bypassed. | +| FT_MQTT | Controls whether the MQTT features are enabled. Disable this if your project does not require MQTT support. | +| FT_NTP | Controls whether network time protocol synchronization features are enabled. Disable this if your project does not require accurate time. | +| FT_UPLOAD_FIRMWARE | Controls whether the manual upload firmware feature is enabled. Disable this if you won't be manually uploading firmware. | +| FT_DOWNLOAD_FIRMWARE | Controls whether the firmware download feature is enabled. Disable this if you won't firmware pulled from a server. | +| FT_SLEEP | Controls whether the deep sleep feature is enabled. Disable this if your device is not battery operated or you don't need to place it in deep sleep to save energy. | +| FT_BATTERY | Controls whether the battery state of charge shall be reported to the clients. Disable this if your device is not battery operated. | + +In addition custom features might be added or removed at runtime. See [Custom Features](statefulservice.md#custom-features) on how to use this in your application. + +## Factory Settings + +The framework has built-in factory settings which act as default values for the various configurable services where settings are not saved on the file system. These settings can be overridden using the build flags defined in [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini). All strings entered here must be escaped, especially special characters. + +Customize the settings as you see fit, for example you might configure your home WiFi network as the factory default: + +```ini + -D FACTORY_WIFI_SSID=\"My\ Awesome\ WiFi\ Network\" + -D FACTORY_WIFI_PASSWORD=\"secret\" + -D FACTORY_WIFI_HOSTNAME=\"awesome_light_controller\" +``` + +### Default access point settings + +By default, the factory settings configure the device to bring up an access point on start up which can be used to configure the device: + +- SSID: ESP32-Sveltekit +- Password: esp-sveltekit + +### Security settings and user credentials + +By default, the factory settings configure two user accounts with the following credentials: + +| Username | Password | +| -------- | -------- | +| admin | admin | +| guest | guest | + +It is recommended that you change the user credentials from their defaults to better protect your device. You can do this in the user interface, or by modifying [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini) as mentioned above. + +### Customizing the factory time zone setting + +Changing factory time zone setting is a common requirement. This requires a little effort because the time zone name and POSIX format are stored as separate values for the moment. The time zone names and POSIX formats are contained in the UI code in [timezones.ts](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/routes/connections/timezones.ts). Take the appropriate pair of values from there, for example, for Los Angeles you would use: + +```ini + -D FACTORY_NTP_TIME_ZONE_LABEL=\"America/Los_Angeles\" + -D FACTORY_NTP_TIME_ZONE_FORMAT=\"PST8PDT,M3.2.0,M11.1.0\" +``` + +### Placeholder substitution + +Various settings support placeholder substitution, indicated by comments in [factory_settings.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/factory_settings.ini). This can be particularly useful where settings need to be unique, such as the Access Point SSID or MQTT client id. The following placeholders are supported: + +| Placeholder | Substituted value | +| ------------ | --------------------------------------------------------------------- | +| #{platform} | The microcontroller platform, e.g. "esp32" or "esp32c3" | +| #{unique_id} | A unique identifier derived from the MAC address, e.g. "0b0a859d6816" | +| #{random} | A random number encoded as a hex string, e.g. "55722f94" | + +## Other Build Flags + +### Cross-Origin Resource Sharing + +If you need to enable Cross-Origin Resource Sharing (CORS) on the ESP32 server just uncomment the following build flags: + +```ini +build_flags = +... + ; Uncomment to configure Cross-Origin Resource Sharing + -D ENABLE_CORS + -D CORS_ORIGIN=\"*\" +``` + +This will add the `Access-Control-Allow-Origin` and `Access-Control-Allow-Credentials` headers to any request made. + +### ESP32 `CORE_DEBUG_LEVEL` + +The ESP32 Arduino Core and many other libraries use the ESP Logging tools. To enable these debug and error messages from deep inside your libraries uncomment the following build flag. + +```ini +build_flags = +... + -D CORE_DEBUG_LEVEL=5 +``` + +It accepts values from 5 (Verbose) to 1 (Errors) for different information depths to be logged on the serial terminal. If commented out there won't be debug messages from the core libraries. For a production build you should comment this out. + +### Serve Config Files + +By enabling this build flag the ESP32 will serve all config files stored on the LittleFS flash partition under `http:\\[IP]\config\[filename].json`. This can be helpful to troubleshoot problems. However, it is strongly advised to disable this for production builds. + +```ini +build_flags = +... + -D SERVE_CONFIG_FILES +``` + +### Serial Info + +In some circumstances it might be beneficial to not print any information on the serial consol (Serial1 or USB CDC). By commenting out the following build flag ESP32-Sveltekit will not print any information on the serial console. + +```ini +build_flags = +... + -D SERIAL_INFO +``` + +## SSL Root Certificate Store + +Some features like firmware download or the MQTT client require a SSL connection. For that the SSL Root CA certificate must be known to the ESP32. The build system contains a python script derived from Espressif ESP-IDF building a certificate store containing one or more certificates. In order to create the store you must uncomment the three lines below in `platformio.ini`. + +```ini +extra_scripts = + pre:scripts/generate_cert_bundle.py +board_build.embed_files = src/certs/x509_crt_bundle.bin +board_ssl_cert_source = adafruit +``` + +The script will download a public certificate store from Mozilla (`board_ssl_cert_source = mozilla`) or a repository curated by Adafruit (`board_ssl_cert_source = adafruit`), builds a binary containing all certs and embeds this into the firmware. This will add ~65kb to the firmware image. Should you only need a few known certificates you can place their `*.pem` or `*.der` files in the [ssl_certs](https://github.com/theelims/ESP32-sveltekit/blob/main/ssl_certs) folder and change `board_ssl_cert_source = folder`. Then only these certificates will be included in the store. This is especially useful, if you only need to connect to know servers and need to shave some kb off the firmware image: + +!!! info + + To enable SSL the feature `FT_NTP=1` must be enabled as well. + +## Vite and LittleFS 32 Character Limit + +The static files for the website are build using vite. By default vite adds a unique hash value to all filenames for improved caching performance. However, LittleFS on the ESP32 is limited to filenames with 32 characters. This restricts the number of characters available for the user to name svelte files. To give a little bit more headroom a vite-plugin removes all hash values, as they offer no benefit on an ESP32. However, have the 32 character limit in mind when naming files. Excessively long names may still cause some issues when building the LittleFS binary. diff --git a/docs/components.md b/docs/components.md new file mode 100644 index 000000000..0ad5e6053 --- /dev/null +++ b/docs/components.md @@ -0,0 +1,157 @@ +# Components + +The project includes a number of components to create the user interface. Even though DaisyUI has a huge set of components, it is often beneficial to recreate them as a Svelte component. This offers a much better integration into the Svelte way of doing things, is less troublesome with animations and results in a overall better user experience. + +## Collapsible + +A collapsible container to hide / show content by clicking on the arrow button. + +```ts +import Collapsible from "$lib/components/Collapsible.svelte"; +``` + +It exports a closed / open state with `export open` which you can use to determine the mounting behavior of the component. + +### Slots + +The component has two slots. A named slot `title` for the collapsible title and the main slot for the content that can be hidden or shown. + +``` + + Title + ... + +``` + +The `class` attribute may be used as normal to style the container. By default there is no special styling like background or shadows to accentuate the container element. + +### Events + +The collapsible component dispatches two events. `on:closed` when the collapsible is closed and `on:opened` when it is opened. You can bind to them as to any other event. + +## InputPassword + +This is an input field specifically for passwords. It comes with an "eye"-button on the right border to toggle the visibility of the password. It natively blends into the style from DaisyUI. + +```ts +import InputPassword from "$lib/components/InputPassword.svelte"; +``` + +You may use it like any other form element: + +``` + +``` + +## RSSIIndicator + +This shows the popular WiFi strength indicator icon with differently highlighted circles depending on the received signal strength (RSSI) of the WiFi signal. In addition it can display the signal strength in raw "dBm" as an indicator badge. + +```ts +import RssiIndicator from "$lib/components/RSSIIndicator.svelte"; +``` + +Just use and style as you please. It doesn't have any slots or events. + +``` + +``` + +Two exports control the behavior of the component. `rssi_dbm` accepts a negative number of the raw RSSI in dBm and is used to determine how many circles of reception should be shown. An optional boolean `showDBm` (defaults to `false`) shows the indicator badge with the dBm value. + +## Settings Card + +A Settings Card is in many ways similar to a [collapsible](#collapsible). However, it is styled and is the main element of many settings menus. It also accepts an icon in a dedicate slot and unlike collapsible has no events. + +```ts +import SettingsCard from "$lib/components/SettingsCard.svelte"; +``` + +### Slots + +Three slots are available. Besides the main slot for the content there is a named slot for the `title` and s second one for the `icon`. + +``` + + + Title + ... + +``` + +The component exports two properties to determine its behavior. `collapsible` is a boolean describing wether the component should behave like a collapsible in the first place. `open` is a boolean as well and if set true shows the full content of the body on mount. + +## Spinner + +A small component showing an animated spinner which can be used while waiting for data. + +```ts +import Spinner from "$lib/components/Spinner.svelte"; +``` + +No slots, no events, no properties. Just use `` whenever something is loading. + +## Toast Notifications + +Toast notifications are implemented as a writable store and are easy to use from any script section. They are an easy way to feedback to the user. To use them just import the notifications store + +```ts +import { notifications } from "$lib/components/toasts/notifications"; +``` + +and call one of the 4 toast methods: + +| Method | Description | +| -------------------------------------------------- | --------------------------------------------------- | +| `notification.error(msg:string, timeout:number)` | :octicons-x-circle-16: Shows an error message | +| `notification.warning(msg:string, timeout:number)` | :octicons-alert-16: Shows a warning message | +| `notification.info(msg:string, timeout:number)` | :octicons-info-16: Shows an info message | +| `notification.success(msg:string, timeout:number)` | :octicons-check-circle-16: Shows as success message | + +Each method takes an `msg`-string as an argument, which will be shown as the message body. It accepts HTML to enrich your toasts, if you should desire to do so. The `timeout` argument specifies how many milliseconds the toast notification shall be shown to the user. + +## Github Update Dialog + +This is a modal showing the update progress, possible error messages and makes a full page refresh 5 seconds after the OTA was successful. + +## Update Indicator + +The update indicator is a small widget shown in the upper right corner of the status bar. It indicates the availability of a newer firmware release then the current one. Upon pressing the icon it will automatically update the firmware to the latest release. By default this works through the Github Latest Release API. This must be customized should you use a different update server. Have a look at the [source file](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/src/lib/components/GithubUpdateDialog.svelte) to see what portions to update. + +## Info Dialog + +Shows a modal on the UI which must be deliberately dismissed. It features a `title` and a `message` property. The dismiss button can be customized via the `dismiss` property with a label and an icon. `onDismiss` call back must close the modal and can be used to do something when closing the info dialog. + +```ts +import InfoDialog from "$lib/components/InfoDialog.svelte"; + +modals.open(InfoDialog, { + title: 'You have a new Info', + message: + 'Something really important happened that justifies showing you a modal which must be clicked away.', + dismiss: { label: 'OK', icon: Check }, + onDismiss: () => modals.close(); +}); +``` + +This modal is based on [svelte-modals](https://svelte-modals.mattjennings.io/) where you can find further information. + +## Confirm Dialog + +Shows a confirm modal on the UI which must be confirmed to proceed, or can be canceled. It features a `title` and a `message` property. The `confirm` and `cancel` buttons can be customized via the `labels` property with a label and an icon. `onConfirm` call back must close the modal and can be used to trigger further actions. + +```ts +import ConfirmDialog from "$lib/components/ConfirmDialog.svelte"; + +modals.open(ConfirmDialog, { + title: "Confirm what you are doing", + message: "Are you sure you want to proceed? This could break stuff!", + labels: { + cancel: { label: "Abort", icon: Cancel }, + confirm: { label: "Confirm", icon: Check }, + }, + onConfirm: () => modals.close(), +}); +``` + +This modal is based on [svelte-modals](https://svelte-modals.mattjennings.io/) where you can find further information. diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md new file mode 100644 index 000000000..3feca4092 --- /dev/null +++ b/docs/gettingstarted.md @@ -0,0 +1,146 @@ +# Getting Started + +## Prerequisites + +This project has quite a complicated build chain to prepare the frontend code for the ESP32. You will need to install some tools to make this all work, starting with a powerful code editor. + +### Softwares to Install + +Please install the following software, if you haven't already: + +- [VSCode](https://code.visualstudio.com/) - IDE for development +- [Node.js](https://nodejs.org) - For building the interface with npm + +### VSCode Plugins and Setups + +Please install the following mandatory VSCode Plugins: + +- [PlatformIO](https://platformio.org/) - Embedded development platform +- [Prettier](https://prettier.io/) - Automated code formatter +- Svelte for VS Code - Makes working with Svelte much easier +- Svelte Intellisense - Another Svelte tool +- Tailwind CSS Intellisense - Makes working with Tailwind CSS much easier +- [Prettier plugin for Tailwind CSS](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) - Automatically sorts the Tailwind classes into their recommended order + +Lastly, if you want to make use of Materials for MkDocs as your documentation engine, install [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) by typing the following into the VSCode terminal: + +```bash +pip install mkdocs-material +``` + +!!! tip + + You might need to run this as administrator, if you getting an error message. + +### Project Structure + +| Resource | Description | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [.github/](https://github.com/theelims/ESP32-sveltekit/blob/main/.github) | Github CI pipeline to deploy MkDocs to gh-pages | +| [docs/](https://github.com/theelims/ESP32-sveltekit/blob/main/docs) | MkDocs documentation files | +| [interface/](https://github.com/theelims/ESP32-sveltekit/blob/main/interface) | SvelteKit based front end | +| [lib/framework/](https://github.com/theelims/ESP32-sveltekit/blob/main/lib/framework) | C++ back end for the ESP32 device | +| [src/](https://github.com/theelims/ESP32-sveltekit/blob/main/src) | The main.cpp and demo project to get you started | +| [scripts/](https://github.com/theelims/ESP32-sveltekit/tree/main/scripts) | Scripts that build the interface as part of the platformio build | +| [platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini) | PlatformIO project configuration file | +| [mkdocs.yaml](https://github.com/theelims/ESP32-sveltekit/blob/main/mkdocs.yaml) | MkDocs project configuration file | + +## Setting up PlatformIO + +### Setup Build Target + +!!! danger "Do not use the PlatformIO UI for editing platformio.ini" + + It is tempting to use the PlatformIO user interface to add dependencies or parameters to platformio.ini. However, doing so will remove all "irrelevant" information like comments from the file. Please edit the file directly in the editor. + +[platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini) is the central file controlling the whole build process. It comes pre-configure with a few boards which have different ESP32 chips. It needs to be adapted to the board you want to program. + +```ini +[platformio] +... +default_envs = esp32-s3-devkitc-1 +... + +[env:adafruit_feather_esp32_v2] +board = adafruit_feather_esp32_v2 +board_build.mcu = esp32 + +[env:lolin_c3_mini] +board = lolin_c3_mini +board_build.mcu = esp32c3 + +[env:esp32-s3-devkitc-1] +board = esp32-s3-devkitc-1 +board_build.mcu = esp32s3 +``` + +If your board is not listed in the platformio.ini you may look in the [official board list](https://docs.platformio.org/en/latest/boards/index.html#espressif-32) for supported boards and add their information accordingly. Either delete the obsolete `[env:...]` sections, or change your board as `default_envs = ...`. + +!!! info "Default setup is for an ESP32-S3-DevKitC/M board" + + The projects platformio.ini defaults for an ESP32-S3-DevKitC/M board by Espressif connected to the UART USB port. If you use an other board and the projects shows an undesired behavior it is likely that some parts do not match with pin definitions. + +### Build & Upload Process + +After you've changed [platformio.ini](https://github.com/theelims/ESP32-sveltekit/blob/main/platformio.ini) to suit your board you can upload the sample code to your board. This will download all ESP32 libraries and execute `node install` to install all node packages as well. Select your board's environment under the PlatformIO tab and hit `Upload and Monitor`. + +![PIO Build](media/PIO-upload.png) + +The first build process will take a while. After a couple of minutes you can see the ESP32 outputting information on the terminal. Some of the python scripts might need to install additional packages. In that case the first build process will fail. Just run it a second time. + +!!! tip "Use several terminals in parallel" + + VSCode allows you to have more then one terminal running at the same time. You can dedicate one terminal to the serial monitor, while having the development server running in an other terminal. + +## Setting up SvelteKit + +### Setup Proxy for Development + +To ease the frontend development you can deploy the back end code on an ESP32 board and pass the websocket and REST API calls through the development server's proxy. +The [vite.config.ts](https://github.com/theelims/ESP32-sveltekit/blob/main/interface/vite.config.ts) file defines the location of the services which the development server will proxy. This is defined by the "target" property, which will need to be changed to the the IP address or hostname of the device running the firmware. Change this for both, "http://" and "ws://". + +```ts +proxy: { + // Proxying REST: http://localhost:5173/rest/bar -> http://192.168.1.83/rest/bar + '/rest': { + target: 'http://192.168.1.83', + changeOrigin: true, + }, + // Proxying websockets ws://localhost:5173/ws -> ws://192.168.1.83/ws + '/ws': { + target: 'ws://192.168.1.83', + changeOrigin: true, + ws: true, + }, +}, +``` + +!!! tip + + You must restart the development server for changes of the proxy location to come into effect. + +### Development Server + +The interface comes with Vite as a development server. It allows hot module reloading reflecting code changes to the front end instantly in your browser. Open a new terminal session and execute the following commands: + +```bash +cd interface +npm run dev +``` + +Follow the link to access the front end in your browser. + +## Setup Material for mkdocs + +Material for MkDocs allows you to create great technical documentation pages just from markup. If you don't want to use it just delete the `.github` and `docs` folder, as well as `mkdocs.yaml`. + +Otherwise initiate the github CI pipeline by committing and pushing to your repository once. This triggers the automatic build. After a few minutes a new branch `gh-pages` containing the static website with your documentation should appear. To deploy it go to your github repository go under settings and complete the following steps. +![Deploy on gh-pages](media/mkdocs_gh-pages.PNG) + +### Development Server + +MkDocs comes with a build-in development server which supports hot reload as well. Open a new terminal session in VSCode and type + +``` +mkdocs serve +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..1a152e4c7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,60 @@ +--- +hide: + - navigation + - toc +--- + +# ESP32 SvelteKit - Create Amazing IoT Projects + +
+ + +
+ +A simple and extensible framework for ESP32 based IoT projects with a feature-rich, beautiful, and responsive front-end build with [Sveltekit](https://kit.svelte.dev/), [TailwindCSS](https://tailwindcss.com/) and [DaisyUI](https://daisyui.com/). This is a project template to get you started in no time backed by a powerful back end service, an amazing front end served from the ESP32 and an easy to use build chain to get everything going. + +It was forked from the fabulous [rjwats/esp8266-react](https://github.com/rjwats/esp8266-react) project, from where it inherited the mighty back end services. + +!!! info + + This template repository is not meant to be used stand alone. If you're just looking for a WiFi manager there are plenty of options available. This is a starting point when you need a rich web UI. + +## Features + +### :butterfly: Beautiful UI powered by DaisyUI and TailwindCSS + +Beautiful, responsive UI which works equally well on desktop and on mobile. Gently animated for a snappy and modern feeling without ever being obtrusive or in the way. Easy theming with DaisyUI and media-queries to respect the users wish for a light or dark theme. + +### :simple-svelte: Low Memory Footprint and Easy Customization by Courtesy of SvelteKit + +SvelteKit is ideally suited to be served from constrained devices like an ESP32. It's unique approach leads to very slim files. No bloatware like other popular JS frameworks. Not only the low memory footprint make it ideal but the developer experience is also outstanding letting you customize the front end with ease. Adapt and add functionality as you need it. The back end has you covered as well. + +### :telephone: Rich Communication Interfaces + +Comes with a rich set of communication interfaces to cover most standard needs of an IoT application. Like MQTT client, HTTP RESTful API, a WebSocket based Event Socket and a classic Websocket Server. All communication channels are stateful and fully synchronized. Changes propagate and are communicated to all other participants. The states can be persisted on the file system as well. For accurate time keeping time can by synchronized over NTP. + +### :file_cabinet: WiFi Provisioning and Management + +Naturally ESP32 SvelteKit comes with rich features to manage all your WiFi needs. From pulling up an access point for provisioning or as fall back, to fully manage your WiFi networks. Scan for available networks and connect to them. Advanced configuration options like static IP are on board as well. + +### :people_with_bunny_ears_partying: Secured API and User Management + +Manage different user of your app with two authorization levels. An administrator and a guest user. Authenticate their API calls with a JWT token. Manage the user's profile from the admin interface. Use at own risk, as it is neither secure without the ability to use TLS/SSL encryption on the ESP32 server, nor very convenient, as only an admin can change passwords. + +### :airplane: OTA Upgrade Service + +The framework can provide two different channels for Over-the-Air updates. Either by uploading a \*.bin file from the web interface. Or by pulling a firmware image from an update server. This is implemented with the github release page as an example. It is even possible to have different build environments at the same time and the Github OTA process pulls the correct binary. + +### :construction_site: Automated Build Chain + +The automated build chain takes out the pain and tears of getting all the bits and pieces play nice together. The repository contains a PlatformIO project at its heart. A SvelteKit project for the frontend code and a mkdocs project for the documentation go alongside. The PlatformIO build tools not only build the SvelteKit frontend with Vite, but also ensure that the build results are gzipped and find their way into the flash memory of the ESP32. You have two choices to serve the frontend either from the flash partition, or embedded into the firmware binary. The latter is much more friendly if your frontend code should be distributed OTA as well, leaving all configuration files intact. + +### :fontawesome-solid-microchip: Compatible with all ESP32 Flavours + +The code runs on all variants of the ESP32 chip family. From the plain old ESP32, the ESP32-S3 and ESP32-C3. Other ESP32 variants might work, but haven't been tested. Sorry, no support for the older ESP8266. Go with one of the ESP32's instead. + +[Let's get started!](gettingstarted.md) + +## License + +ESP32 SvelteKit is distributed with two licenses for different sections of the code. The back end code inherits the GNU LESSER GENERAL PUBLIC LICENSE Version 3 and is therefore distributed with said license. The front end code is distributed under the MIT License. See the [LICENSE](https://github.com/theelims/ESP32-sveltekit/blob/main/LICENSE) for a full text of both licenses. diff --git a/docs/media/Login_dark.png b/docs/media/Login_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..70084d24aab4b2f96c8cf79968815492dc578ef4 GIT binary patch literal 80780 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 z

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 z˛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

uti@%Hh5 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