29 Commits

Author SHA1 Message Date
99678087cb move to svelte kit
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 2m4s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m23s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 1m53s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m16s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-02-15 18:03:21 +01:00
fe4bd11a21 preparation for switch to C6 MCU (but also compatible with S3)
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 2m23s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 6m1s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 2m17s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 5m57s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-02-06 22:30:48 +01:00
684ce36270 add vite helper files?
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m55s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 7m13s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 1m48s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m10s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:33:13 +01:00
98b5df1ff2 add ignore for FreeCAD and node-js intermediate files
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Has been cancelled
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Has been cancelled
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Has been cancelled
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Has been cancelled
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:29:16 +01:00
8128b958cb Merge branch 'feature/website'
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:26:42 +01:00
955b4bef04 rebuild websites with svelte
still early WIP

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:24:19 +01:00
81141d8859 connect via MQTTS
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 02:15:05 +01:00
e01006cd49 remove PSRAM usage
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 00:28:00 +01:00
c28d7d08df edit of all config data via website
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 00:14:52 +01:00
df50aaedda get/post led segments
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m49s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m44s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m34s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m43s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-21 21:53:03 +01:00
1f02d35a97 try to use react for SPA
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m45s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 7m18s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m16s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m26s
initial setup - no real function yet

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-19 22:47:57 +01:00
501c2de874 update time on website via webSocket
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m44s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m37s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m22s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m24s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-19 00:19:57 +01:00
b39a3be956 show correct schame on website
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 22:45:46 +01:00
3ec7bf7acb more status values
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 22:41:20 +01:00
a12dfe7760 update comments
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 16:51:47 +01:00
dc40acfd06 change schema via REST
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m46s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m56s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m45s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m44s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 16:41:41 +01:00
3d7de05614 read schema files for website
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 16:26:05 +01:00
3f32b791b7 change light mode
day/night/simulation

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 16:09:09 +01:00
ccdc2bb63f send current state via WS
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 6m9s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m54s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m35s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m59s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 15:29:03 +01:00
7d12d98ec9 refresh device ui after REST call
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 15:12:09 +01:00
cdac9cbfb8 rework message manager
use of listener pattern instead of message queue

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 14:26:13 +01:00
1fade06bdb light on/off via REST or GPIO
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 14:13:05 +01:00
f7cedf24e8 shared website header
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 7m45s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m52s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 4m35s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m58s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 10:59:08 +01:00
1c52f7d679 fixed devcontainer image
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 10:58:35 +01:00
7a73fc4b7b implement reset via back button
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 10:58:24 +01:00
1fbc28a628 optimize AP mode
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 6m44s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 3m59s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m51s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 3m52s
- save wifi data
- show status led

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-15 00:36:19 +01:00
bccfb80791 show capative portal on connect
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 6m44s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m12s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m54s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 3m56s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-13 00:05:54 +01:00
b77fdee21d refactor persistence manager from cpp to c
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m12s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m17s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m49s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m0s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-10 11:12:31 +01:00
ef0cda1d67 update web ui
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m6s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m19s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m52s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m4s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-10 00:33:34 +01:00
163 changed files with 13738 additions and 1198 deletions

2
.gitignore vendored
View File

@@ -24,3 +24,5 @@
**/*_front.png **/*_front.png
**/*_schematic*.png **/*_schematic*.png
**/wiki/* **/wiki/*
*.FCBak
firmware/**/node_modules

View File

@@ -1,4 +1,4 @@
ARG DOCKER_TAG=latest ARG DOCKER_TAG=release-v5.4
FROM espressif/idf:${DOCKER_TAG} FROM espressif/idf:${DOCKER_TAG}
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8

View File

@@ -1,4 +1,9 @@
cmake_minimum_required(VERSION 3.5) cmake_minimum_required(VERSION 3.5)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
idf_build_set_property(BOOTLOADER_EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/bootloader_components_extra/" APPEND)
project(system_control) project(system_control)
target_add_binary_data(${PROJECT_NAME}.elf "main/isrgrootx1.pem" TEXT)

View File

@@ -0,0 +1,9 @@
idf_component_register(SRCS "hooks.c"
REQUIRES extra_component)
# We need to force GCC to integrate this static library into the
# bootloader link. Indeed, by default, as the hooks in the bootloader are weak,
# the linker would just ignore the symbols in the extra. (i.e. not strictly
# required)
# To do so, we need to define the symbol (function) `bootloader_hooks_include`
# within hooks.c source file.

View File

@@ -0,0 +1,21 @@
#include "esp_log.h"
/* Function used to tell the linker to include this file
* with all its symbols.
*/
void bootloader_hooks_include(void)
{
}
/* Keep in my mind that a lot of functions cannot be called from here
* as system initialization has not been performed yet, including
* BSS, SPI flash, or memory protection. */
void bootloader_before_init(void)
{
ESP_LOGI("HOOK", "This hook is called BEFORE bootloader initialization");
}
void bootloader_after_init(void)
{
ESP_LOGI("HOOK", "This hook is called AFTER bootloader initialization");
}

View File

@@ -0,0 +1 @@
idf_component_register(SRCS "extra_component.c")

View File

@@ -0,0 +1,11 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#include "esp_log.h"
void bootloader_extra_dir_function(void)
{
ESP_LOGI("EXTRA", "This function is called from an extra component");
}

View File

@@ -1,5 +1,6 @@
idf_component_register(SRCS idf_component_register(SRCS
src/api_server.c src/api_server.c
src/common.c
src/api_handlers.c src/api_handlers.c
src/websocket_handler.c src/websocket_handler.c
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
@@ -10,5 +11,9 @@ idf_component_register(SRCS
esp_netif esp_netif
esp_event esp_event
json json
led-manager
simulator
persistence-manager
message-manager
simulator simulator
) )

View File

@@ -0,0 +1,9 @@
#ifndef COMMON_H
#define COMMON_H
#include <cJSON.h>
void common_init(void);
cJSON *create_light_status_json(void);
#endif // COMMON_H

View File

@@ -1,5 +1,11 @@
#include "api_handlers.h" #include "api_handlers.h"
#include "common.h"
#include "message_manager.h"
#include "esp_heap_caps.h"
#include "led_segment.h"
#include "persistence_manager.h"
#include "storage.h"
#include <cJSON.h> #include <cJSON.h>
#include <esp_http_server.h> #include <esp_http_server.h>
#include <esp_log.h> #include <esp_log.h>
@@ -7,6 +13,8 @@
#include <string.h> #include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#define MAX_BODY_SIZE 4096
static const char *TAG = "api_handlers"; static const char *TAG = "api_handlers";
// Helper function to set CORS headers // Helper function to set CORS headers
@@ -56,7 +64,7 @@ esp_err_t api_capabilities_get_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "GET /api/capabilities"); ESP_LOGI(TAG, "GET /api/capabilities");
// Thread nur für esp32c6 oder esp32h2 verfügbar // Thread only available for esp32c6 or esp32h2
bool thread = false; bool thread = false;
#if defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2) #if defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2)
thread = true; thread = true;
@@ -87,7 +95,7 @@ esp_err_t api_wifi_scan_handler(httpd_req_t *req)
uint16_t ap_num = 0; uint16_t ap_num = 0;
esp_wifi_scan_get_ap_num(&ap_num); esp_wifi_scan_get_ap_num(&ap_num);
wifi_ap_record_t *ap_list = calloc(ap_num, sizeof(wifi_ap_record_t)); wifi_ap_record_t *ap_list = heap_caps_calloc(ap_num, sizeof(wifi_ap_record_t), MALLOC_CAP_DEFAULT);
if (!ap_list) if (!ap_list)
{ {
return send_error_response(req, 500, "Memory allocation failed"); return send_error_response(req, 500, "Memory allocation failed");
@@ -102,6 +110,8 @@ esp_err_t api_wifi_scan_handler(httpd_req_t *req)
cJSON *entry = cJSON_CreateObject(); cJSON *entry = cJSON_CreateObject();
cJSON_AddStringToObject(entry, "ssid", (const char *)ap_list[i].ssid); cJSON_AddStringToObject(entry, "ssid", (const char *)ap_list[i].ssid);
cJSON_AddNumberToObject(entry, "rssi", ap_list[i].rssi); cJSON_AddNumberToObject(entry, "rssi", ap_list[i].rssi);
bool secure = ap_list[i].authmode != WIFI_AUTH_OPEN;
cJSON_AddBoolToObject(entry, "secure", secure);
cJSON_AddItemToArray(json, entry); cJSON_AddItemToArray(json, entry);
} }
} }
@@ -113,9 +123,21 @@ esp_err_t api_wifi_scan_handler(httpd_req_t *req)
return res; return res;
} }
static void reboot_task(void *param)
{
vTaskDelay(pdMS_TO_TICKS(100));
esp_restart();
}
static bool is_valid(const cJSON *string)
{
return string && cJSON_IsString(string) && string->valuestring && strlen(string->valuestring) > 0;
}
esp_err_t api_wifi_config_handler(httpd_req_t *req) esp_err_t api_wifi_config_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/wifi/config"); ESP_LOGI(TAG, "POST /api/wifi/config");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[256]; char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -125,15 +147,25 @@ esp_err_t api_wifi_config_handler(httpd_req_t *req)
} }
buf[ret] = '\0'; buf[ret] = '\0';
// Passwort maskieren
cJSON *json = cJSON_Parse(buf); cJSON *json = cJSON_Parse(buf);
if (json) if (json)
{ {
cJSON *ssid = cJSON_GetObjectItem(json, "ssid");
cJSON *pw = cJSON_GetObjectItem(json, "password"); cJSON *pw = cJSON_GetObjectItem(json, "password");
if (pw && cJSON_IsString(pw) && pw->valuestring) if (is_valid(ssid) && is_valid(pw))
{
persistence_manager_t pm;
if (persistence_manager_init(&pm, "wifi_config") == ESP_OK)
{
persistence_manager_set_string(&pm, "ssid", ssid->valuestring);
persistence_manager_set_string(&pm, "password", pw->valuestring);
persistence_manager_deinit(&pm);
}
}
if (is_valid(pw))
{ {
size_t pwlen = strlen(pw->valuestring); size_t pwlen = strlen(pw->valuestring);
char *masked = malloc(pwlen + 1); char *masked = heap_caps_malloc(pwlen + 1, MALLOC_CAP_DEFAULT);
if (masked) if (masked)
{ {
memset(masked, '*', pwlen); memset(masked, '*', pwlen);
@@ -160,7 +192,9 @@ esp_err_t api_wifi_config_handler(httpd_req_t *req)
ESP_LOGI(TAG, "Received WiFi config: %s", buf); ESP_LOGI(TAG, "Received WiFi config: %s", buf);
} }
// TODO: Parse JSON and connect to WiFi // Define a reboot task function
xTaskCreate(reboot_task, "reboot_task", 2048, NULL, 5, NULL);
set_cors_headers(req); set_cors_headers(req);
httpd_resp_set_status(req, "200 OK"); httpd_resp_set_status(req, "200 OK");
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
@@ -217,6 +251,7 @@ esp_err_t api_wifi_status_handler(httpd_req_t *req)
esp_err_t api_light_power_handler(httpd_req_t *req) esp_err_t api_light_power_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/light/power"); ESP_LOGI(TAG, "POST /api/light/power");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[64]; char buf[64];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -228,7 +263,22 @@ esp_err_t api_light_power_handler(httpd_req_t *req)
ESP_LOGI(TAG, "Received light power: %s", buf); ESP_LOGI(TAG, "Received light power: %s", buf);
// TODO: Parse JSON and control light cJSON *json = cJSON_Parse(buf);
if (json)
{
cJSON *active = cJSON_GetObjectItem(json, "on");
if (cJSON_IsBool(active))
{
message_t msg = {};
msg.type = MESSAGE_TYPE_SETTINGS;
msg.data.settings.type = SETTINGS_TYPE_BOOL;
strncpy(msg.data.settings.key, "light_active", sizeof(msg.data.settings.key) - 1);
msg.data.settings.value.bool_value = cJSON_IsTrue(active);
message_manager_post(&msg, pdMS_TO_TICKS(100));
}
cJSON_Delete(json);
}
set_cors_headers(req); set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
} }
@@ -255,6 +305,7 @@ esp_err_t api_light_thunder_handler(httpd_req_t *req)
esp_err_t api_light_mode_handler(httpd_req_t *req) esp_err_t api_light_mode_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/light/mode"); ESP_LOGI(TAG, "POST /api/light/mode");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[64]; char buf[64];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -266,7 +317,37 @@ esp_err_t api_light_mode_handler(httpd_req_t *req)
ESP_LOGI(TAG, "Received light mode: %s", buf); ESP_LOGI(TAG, "Received light mode: %s", buf);
// TODO: Parse JSON and set light mode cJSON *json = cJSON_Parse(buf);
if (json)
{
cJSON *mode = cJSON_GetObjectItem(json, "mode");
if (cJSON_IsString(mode))
{
message_t msg = {};
msg.type = MESSAGE_TYPE_SETTINGS;
msg.data.settings.type = SETTINGS_TYPE_INT;
strncpy(msg.data.settings.key, "light_mode", sizeof(msg.data.settings.key) - 1);
if (strcmp(mode->valuestring, "simulation") == 0)
{
msg.data.settings.value.int_value = 0;
}
else if (strcmp(mode->valuestring, "day") == 0)
{
msg.data.settings.value.int_value = 1;
}
else if (strcmp(mode->valuestring, "night") == 0)
{
msg.data.settings.value.int_value = 2;
}
else
{
msg.data.settings.value.int_value = -1; // Unknown mode
}
message_manager_post(&msg, pdMS_TO_TICKS(100));
}
cJSON_Delete(json);
}
set_cors_headers(req); set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
} }
@@ -274,6 +355,7 @@ esp_err_t api_light_mode_handler(httpd_req_t *req)
esp_err_t api_light_schema_handler(httpd_req_t *req) esp_err_t api_light_schema_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/light/schema"); ESP_LOGI(TAG, "POST /api/light/schema");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128]; char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -285,7 +367,25 @@ esp_err_t api_light_schema_handler(httpd_req_t *req)
ESP_LOGI(TAG, "Received schema setting: %s", buf); ESP_LOGI(TAG, "Received schema setting: %s", buf);
// TODO: Parse JSON and set active schema cJSON *json = cJSON_Parse(buf);
if (json)
{
cJSON *schema_file = cJSON_GetObjectItem(json, "schema");
if (cJSON_IsString(schema_file))
{
int schema_id = 0;
sscanf(schema_file->valuestring, "schema_%d.csv", &schema_id);
message_t msg = {};
msg.type = MESSAGE_TYPE_SETTINGS;
msg.data.settings.type = SETTINGS_TYPE_INT;
strncpy(msg.data.settings.key, "light_variant", sizeof(msg.data.settings.key) - 1);
msg.data.settings.value.int_value = schema_id;
message_manager_post(&msg, pdMS_TO_TICKS(100));
}
cJSON_Delete(json);
}
set_cors_headers(req); set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
} }
@@ -293,16 +393,12 @@ esp_err_t api_light_schema_handler(httpd_req_t *req)
esp_err_t api_light_status_handler(httpd_req_t *req) esp_err_t api_light_status_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "GET /api/light/status"); ESP_LOGI(TAG, "GET /api/light/status");
cJSON *json = create_light_status_json();
// TODO: Implement actual light status retrieval char *response = cJSON_PrintUnformatted(json);
const char *response = "{" cJSON_Delete(json);
"\"on\":true," esp_err_t res = send_json_response(req, response);
"\"thunder\":false," free(response);
"\"mode\":\"simulation\"," return res;
"\"schema\":\"schema_01.csv\","
"\"color\":{\"r\":255,\"g\":240,\"b\":220}"
"}";
return send_json_response(req, response);
} }
// ============================================================================ // ============================================================================
@@ -313,31 +409,114 @@ esp_err_t api_wled_config_get_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "GET /api/wled/config"); ESP_LOGI(TAG, "GET /api/wled/config");
// TODO: Implement actual LED config retrieval extern led_segment_t segments[LED_SEGMENT_MAX_LEN];
const char *response = "{" extern size_t segment_count;
"\"segments\":[" size_t required_size = sizeof(segments) * segment_count;
"{\"name\":\"Main Light\",\"start\":0,\"leds\":60},"
"{\"name\":\"Accent Light\",\"start\":60,\"leds\":30}" cJSON *json = cJSON_CreateObject();
"]"
"}"; persistence_manager_t pm;
return send_json_response(req, response); if (persistence_manager_init(&pm, "led_config") == ESP_OK)
{
persistence_manager_get_blob(&pm, "segments", segments, required_size, NULL);
uint8_t segment_count = persistence_manager_get_int(&pm, "segment_count", 0);
persistence_manager_deinit(&pm);
cJSON *segments_arr = cJSON_CreateArray();
for (uint8_t i = 0; i < segment_count; ++i)
{
cJSON *seg = cJSON_CreateObject();
cJSON_AddStringToObject(seg, "name", segments[i].name);
cJSON_AddNumberToObject(seg, "start", segments[i].start);
cJSON_AddNumberToObject(seg, "leds", segments[i].leds);
cJSON_AddItemToArray(segments_arr, seg);
}
cJSON_AddItemToObject(json, "segments", segments_arr);
}
else
{
cJSON_AddItemToObject(json, "segments", cJSON_CreateArray());
}
char *response = cJSON_PrintUnformatted(json);
cJSON_Delete(json);
esp_err_t res = send_json_response(req, response);
free(response);
return res;
} }
esp_err_t api_wled_config_post_handler(httpd_req_t *req) esp_err_t api_wled_config_post_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/wled/config"); ESP_LOGI(TAG, "POST /api/wled/config");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[512]; char *buf = heap_caps_malloc(MAX_BODY_SIZE, MALLOC_CAP_DEFAULT);
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); if (!buf)
if (ret <= 0) return send_error_response(req, 500, "Memory allocation failed");
int total = 0, ret;
while (total < MAX_BODY_SIZE - 1)
{ {
return send_error_response(req, 400, "Failed to receive request body"); ret = httpd_req_recv(req, buf + total, MAX_BODY_SIZE - 1 - total);
if (ret <= 0)
break;
total += ret;
} }
buf[ret] = '\0'; buf[total] = '\0';
ESP_LOGI(TAG, "Received WLED config: %s", buf); ESP_LOGI(TAG, "Received WLED config: %s", buf);
// TODO: Parse JSON and save LED configuration cJSON *json = cJSON_Parse(buf);
free(buf);
if (!json)
{
return send_error_response(req, 400, "Invalid JSON");
}
cJSON *segments_arr = cJSON_GetObjectItem(json, "segments");
if (!cJSON_IsArray(segments_arr))
{
cJSON_Delete(json);
return send_error_response(req, 400, "Missing segments array");
}
extern led_segment_t segments[LED_SEGMENT_MAX_LEN];
extern size_t segment_count;
size_t count = cJSON_GetArraySize(segments_arr);
if (count > LED_SEGMENT_MAX_LEN)
count = LED_SEGMENT_MAX_LEN;
segment_count = count;
for (size_t i = 0; i < LED_SEGMENT_MAX_LEN; ++i)
{
cJSON *seg = cJSON_GetArrayItem(segments_arr, i);
cJSON *name = cJSON_GetObjectItem(seg, "name");
cJSON *start = cJSON_GetObjectItem(seg, "start");
cJSON *leds = cJSON_GetObjectItem(seg, "leds");
if (cJSON_IsString(name) && cJSON_IsNumber(start) && cJSON_IsNumber(leds) && i < count)
{
strncpy(segments[i].name, name->valuestring, sizeof(segments[i].name) - 1);
segments[i].name[sizeof(segments[i].name) - 1] = '\0';
segments[i].start = (uint16_t)start->valuedouble;
segments[i].leds = (uint16_t)leds->valuedouble;
}
else
{
// Invalid entry, skip or set defaults
segments[i].name[0] = '\0';
segments[i].start = 0;
segments[i].leds = 0;
}
}
cJSON_Delete(json);
persistence_manager_t pm;
if (persistence_manager_init(&pm, "led_config") == ESP_OK)
{
persistence_manager_set_blob(&pm, "segments", segments, sizeof(led_segment_t) * segment_count);
persistence_manager_set_int(&pm, "segment_count", (int32_t)segment_count);
persistence_manager_deinit(&pm);
}
set_cors_headers(req); set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
} }
@@ -345,6 +524,16 @@ esp_err_t api_wled_config_post_handler(httpd_req_t *req)
// ============================================================================ // ============================================================================
// Schema API // Schema API
// ============================================================================ // ============================================================================
static char *heap_caps_strdup(const char *src, uint32_t caps)
{
if (!src)
return NULL;
size_t len = strlen(src) + 1;
char *dst = heap_caps_malloc(len, caps);
if (dst)
memcpy(dst, src, len);
return dst;
}
esp_err_t api_schema_get_handler(httpd_req_t *req) esp_err_t api_schema_get_handler(httpd_req_t *req)
{ {
@@ -361,41 +550,116 @@ esp_err_t api_schema_get_handler(httpd_req_t *req)
ESP_LOGI(TAG, "Requested schema: %s", filename); ESP_LOGI(TAG, "Requested schema: %s", filename);
// TODO: Read actual schema file from storage // Read schema file
// For now, return sample CSV data char path[128];
snprintf(path, sizeof(path), "%s", filename);
int line_count = 0;
char **lines = read_lines_filtered(path, &line_count);
set_cors_headers(req); set_cors_headers(req);
httpd_resp_set_type(req, "text/csv"); httpd_resp_set_type(req, "text/csv");
const char *sample_csv = "255,240,220,0,100,250\n"
"255,230,200,0,120,250\n" if (!lines || line_count == 0)
"255,220,180,0,140,250\n"; {
return httpd_resp_sendstr(req, sample_csv); return httpd_resp_sendstr(req, "");
}
// Calculate total length
size_t total_len = 0;
for (int i = 0; i < line_count; ++i)
total_len += strlen(lines[i]) + 1;
char *csv = heap_caps_malloc(total_len + 1, MALLOC_CAP_DEFAULT);
char *p = csv;
for (int i = 0; i < line_count; ++i)
{
size_t l = strlen(lines[i]);
memcpy(p, lines[i], l);
p += l;
*p++ = '\n';
}
*p = '\0';
free_lines(lines, line_count);
esp_err_t res = httpd_resp_sendstr(req, csv);
free(csv);
return res;
} }
esp_err_t api_schema_post_handler(httpd_req_t *req) esp_err_t api_schema_post_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/schema/*"); ESP_LOGI(TAG, "POST /api/schema/*");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
// Extract filename from URI // Extract filename from URI
const char *uri = req->uri; if (!req)
const char *filename = strrchr(uri, '/');
if (filename == NULL)
{ {
ESP_LOGE(TAG, "Request pointer is NULL");
return send_error_response(req, 500, "Internal error: req is NULL");
}
const char *uri = req->uri;
ESP_LOGI(TAG, "Request URI: %s", uri ? uri : "(null)");
if (!uri)
{
ESP_LOGE(TAG, "Request URI is NULL");
return send_error_response(req, 400, "Invalid schema path (no URI)");
}
const char *filename = strrchr(uri, '/');
if (filename == NULL || filename[1] == '\0')
{
ESP_LOGE(TAG, "Could not extract filename from URI: %s", uri);
return send_error_response(req, 400, "Invalid schema path"); return send_error_response(req, 400, "Invalid schema path");
} }
filename++; filename++;
ESP_LOGI(TAG, "Extracted filename: %s", filename);
char buf[2048]; // Dynamically read POST body (like api_wled_config_post_handler)
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); char *buf = heap_caps_malloc(MAX_BODY_SIZE, MALLOC_CAP_DEFAULT);
if (ret <= 0) if (!buf)
{ {
return send_error_response(req, 400, "Failed to receive request body"); ESP_LOGE(TAG, "Memory allocation failed for POST body");
return send_error_response(req, 500, "Memory allocation failed");
} }
buf[ret] = '\0'; int total = 0, ret;
while (total < MAX_BODY_SIZE - 1)
{
ret = httpd_req_recv(req, buf + total, MAX_BODY_SIZE - 1 - total);
if (ret <= 0)
break;
total += ret;
}
buf[total] = '\0';
ESP_LOGI(TAG, "Saving schema %s, size: %d bytes", filename, ret); ESP_LOGI(TAG, "Saving schema %s, size: %d bytes", filename, total);
// TODO: Save schema to storage // Split CSV body into line array
int line_count = 0;
// Count lines
for (int i = 0; i < total; ++i)
if (buf[i] == '\n')
line_count++;
if (total > 0 && buf[total - 1] != '\n')
line_count++; // last line without \n
char **lines = (char **)heap_caps_malloc(line_count * sizeof(char *), MALLOC_CAP_DEFAULT);
int idx = 0;
char *saveptr = NULL;
char *line = strtok_r(buf, "\n", &saveptr);
while (line && idx < line_count)
{
// Ignore empty lines
if (line[0] != '\0')
lines[idx++] = heap_caps_strdup(line, MALLOC_CAP_DEFAULT);
line = strtok_r(NULL, "\n", &saveptr);
}
int actual_count = idx;
esp_err_t err = write_lines(filename, lines, actual_count);
for (int i = 0; i < actual_count; ++i)
free(lines[i]);
free(lines);
set_cors_headers(req); set_cors_headers(req);
if (err != ESP_OK)
return send_error_response(req, 500, "Failed to save schema");
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}"); return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
} }
@@ -418,6 +682,7 @@ esp_err_t api_devices_scan_handler(httpd_req_t *req)
esp_err_t api_devices_pair_handler(httpd_req_t *req) esp_err_t api_devices_pair_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/devices/pair"); ESP_LOGI(TAG, "POST /api/devices/pair");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[256]; char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -448,6 +713,7 @@ esp_err_t api_devices_paired_handler(httpd_req_t *req)
esp_err_t api_devices_update_handler(httpd_req_t *req) esp_err_t api_devices_update_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/devices/update"); ESP_LOGI(TAG, "POST /api/devices/update");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[256]; char buf[256];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -467,6 +733,7 @@ esp_err_t api_devices_update_handler(httpd_req_t *req)
esp_err_t api_devices_unpair_handler(httpd_req_t *req) esp_err_t api_devices_unpair_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/devices/unpair"); ESP_LOGI(TAG, "POST /api/devices/unpair");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128]; char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -486,6 +753,7 @@ esp_err_t api_devices_unpair_handler(httpd_req_t *req)
esp_err_t api_devices_toggle_handler(httpd_req_t *req) esp_err_t api_devices_toggle_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/devices/toggle"); ESP_LOGI(TAG, "POST /api/devices/toggle");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128]; char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -538,6 +806,7 @@ esp_err_t api_scenes_get_handler(httpd_req_t *req)
esp_err_t api_scenes_post_handler(httpd_req_t *req) esp_err_t api_scenes_post_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/scenes"); ESP_LOGI(TAG, "POST /api/scenes");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[512]; char buf[512];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -576,6 +845,7 @@ esp_err_t api_scenes_delete_handler(httpd_req_t *req)
esp_err_t api_scenes_activate_handler(httpd_req_t *req) esp_err_t api_scenes_activate_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "POST /api/scenes/activate"); ESP_LOGI(TAG, "POST /api/scenes/activate");
ESP_LOGI(TAG, "Request content length: %d", req->content_len);
char buf[128]; char buf[128];
int ret = httpd_req_recv(req, buf, sizeof(buf) - 1); int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
@@ -628,12 +898,25 @@ static const char *get_mime_type(const char *path)
esp_err_t api_static_file_handler(httpd_req_t *req) esp_err_t api_static_file_handler(httpd_req_t *req)
{ {
char filepath[CONFIG_HTTPD_MAX_URI_LEN + 16]; char filepath[CONFIG_HTTPD_MAX_URI_LEN + 16];
const char *uri = req->uri;
// Default to index.html for root const char *uri = req->uri;
if (strcmp(uri, "/") == 0) wifi_mode_t mode = 0;
esp_wifi_get_mode(&mode);
// Always serve captive.html in AP mode
if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA)
{ {
uri = "/index.html"; if (strcmp(uri, "/") == 0 || strcmp(uri, "/index.html") == 0)
{
uri = "/captive.html";
}
}
else
{
// Default to index.html for root
if (strcmp(uri, "/") == 0)
{
uri = "/index.html";
}
} }
const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH; const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH;
@@ -690,10 +973,32 @@ esp_err_t api_captive_portal_handler(httpd_req_t *req)
{ {
ESP_LOGI(TAG, "Captive portal detection: %s", req->uri); ESP_LOGI(TAG, "Captive portal detection: %s", req->uri);
// Redirect to captive portal page // Serve captive.html directly (status 200, text/html)
httpd_resp_set_status(req, "302 Found"); const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH;
httpd_resp_set_hdr(req, "Location", "/captive.html"); char filepath[256];
httpd_resp_send(req, NULL, 0); snprintf(filepath, sizeof(filepath), "%s/captive.html", base_path);
FILE *f = fopen(filepath, "r");
if (!f)
{
ESP_LOGE(TAG, "captive.html not found: %s", filepath);
httpd_resp_set_status(req, "500 Internal Server Error");
httpd_resp_sendstr(req, "Captive portal not available");
return ESP_FAIL;
}
httpd_resp_set_type(req, "text/html");
char buf[512];
size_t read_bytes;
while ((read_bytes = fread(buf, 1, sizeof(buf), f)) > 0)
{
if (httpd_resp_send_chunk(req, buf, read_bytes) != ESP_OK)
{
fclose(f);
ESP_LOGE(TAG, "Failed to send captive chunk");
return ESP_FAIL;
}
}
fclose(f);
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK; return ESP_OK;
} }

View File

@@ -2,6 +2,7 @@
#include "api_handlers.h" #include "api_handlers.h"
#include "websocket_handler.h" #include "websocket_handler.h"
#include "common.h"
#include "storage.h" #include "storage.h"
#include <esp_http_server.h> #include <esp_http_server.h>
#include <esp_log.h> #include <esp_log.h>
@@ -86,6 +87,9 @@ static esp_err_t start_webserver(void)
return err; return err;
} }
// Common initialization
common_init();
ESP_LOGI(TAG, "HTTP server started successfully"); ESP_LOGI(TAG, "HTTP server started successfully");
return ESP_OK; return ESP_OK;
} }

View File

@@ -0,0 +1,79 @@
#include "common.h"
#include <cJSON.h>
#include <stdbool.h>
#include "api_server.h"
#include "color.h"
#include "message_manager.h"
#include "persistence_manager.h"
#include "simulator.h"
#include <string.h>
#include <time.h>
const char *system_time = NULL;
rgb_t color = {0, 0, 0};
static void on_message_received(const message_t *msg)
{
if (msg->type == MESSAGE_TYPE_SIMULATION)
{
system_time = msg->data.simulation.time;
color.red = msg->data.simulation.red;
color.green = msg->data.simulation.green;
color.blue = msg->data.simulation.blue;
cJSON *json = create_light_status_json();
cJSON_AddStringToObject(json, "type", "status");
char *response = cJSON_PrintUnformatted(json);
cJSON_Delete(json);
api_server_ws_broadcast(response);
free(response);
}
}
void common_init(void)
{
message_manager_register_listener(on_message_received);
}
// Gibt ein cJSON-Objekt with dem aktuellen Lichtstatus zurück
cJSON *create_light_status_json(void)
{
persistence_manager_t pm;
persistence_manager_init(&pm, "config");
cJSON *json = cJSON_CreateObject();
bool light_active = persistence_manager_get_bool(&pm, "light_active", false);
cJSON_AddBoolToObject(json, "on", light_active);
cJSON_AddBoolToObject(json, "thunder", false);
int mode = persistence_manager_get_int(&pm, "light_mode", 0);
const char *mode_str = "simulation";
if (mode == 1)
{
mode_str = "day";
}
else if (mode == 2)
{
mode_str = "night";
}
cJSON_AddStringToObject(json, "mode", mode_str);
int variant = persistence_manager_get_int(&pm, "light_variant", 3);
char schema_filename[20];
snprintf(schema_filename, sizeof(schema_filename), "schema_%02d.csv", variant);
cJSON_AddStringToObject(json, "schema", schema_filename);
persistence_manager_deinit(&pm);
cJSON *c = cJSON_CreateObject();
cJSON_AddNumberToObject(c, "r", color.red);
cJSON_AddNumberToObject(c, "g", color.green);
cJSON_AddNumberToObject(c, "b", color.blue);
cJSON_AddItemToObject(json, "color", c);
cJSON_AddStringToObject(json, "clock", system_time);
return json;
}

View File

@@ -1,5 +1,8 @@
#include "websocket_handler.h" #include "websocket_handler.h"
#include "api_server.h"
#include "common.h"
#include "message_manager.h"
#include <esp_http_server.h> #include <esp_http_server.h>
#include <esp_log.h> #include <esp_log.h>
#include <string.h> #include <string.h>
@@ -10,6 +13,16 @@ static const char *TAG = "websocket_handler";
static int ws_clients[WS_MAX_CLIENTS]; static int ws_clients[WS_MAX_CLIENTS];
static int ws_client_count = 0; static int ws_client_count = 0;
static void on_message_received(const message_t *msg)
{
cJSON *json = create_light_status_json();
cJSON_AddStringToObject(json, "type", "status");
char *response = cJSON_PrintUnformatted(json);
cJSON_Delete(json);
api_server_ws_broadcast(response);
free(response);
}
static void ws_clients_init(void) static void ws_clients_init(void)
{ {
for (int i = 0; i < WS_MAX_CLIENTS; i++) for (int i = 0; i < WS_MAX_CLIENTS; i++)
@@ -17,7 +30,7 @@ static void ws_clients_init(void)
} }
// Add a client to the list // Add a client to the list
static void add_client(int fd) static bool add_client(int fd)
{ {
for (int i = 0; i < WS_MAX_CLIENTS; i++) for (int i = 0; i < WS_MAX_CLIENTS; i++)
{ {
@@ -26,10 +39,11 @@ static void add_client(int fd)
ws_clients[i] = fd; ws_clients[i] = fd;
ws_client_count++; ws_client_count++;
ESP_LOGI(TAG, "WebSocket client connected: fd=%d (total: %d)", fd, ws_client_count); ESP_LOGI(TAG, "WebSocket client connected: fd=%d (total: %d)", fd, ws_client_count);
return; return true;
} }
} }
ESP_LOGW(TAG, "Max WebSocket clients reached, cannot add fd=%d", fd); ESP_LOGW(TAG, "Max WebSocket clients reached, cannot add fd=%d", fd);
return false;
} }
// Remove a client from the list // Remove a client from the list
@@ -56,15 +70,11 @@ static esp_err_t handle_ws_message(httpd_req_t *req, httpd_ws_frame_t *ws_pkt)
// For now, we just check if it's a status request // For now, we just check if it's a status request
if (ws_pkt->payload != NULL && strstr((char *)ws_pkt->payload, "getStatus") != NULL) if (ws_pkt->payload != NULL && strstr((char *)ws_pkt->payload, "getStatus") != NULL)
{ {
// Send status response // Status-JSON generieren
// TODO: Get actual status values cJSON *json = create_light_status_json();
const char *response = "{" cJSON_AddStringToObject(json, "type", "status");
"\"type\":\"status\"," char *response = cJSON_PrintUnformatted(json);
"\"on\":true," cJSON_Delete(json);
"\"mode\":\"simulation\","
"\"schema\":\"schema_01.csv\","
"\"color\":{\"r\":255,\"g\":240,\"b\":220}"
"}";
httpd_ws_frame_t ws_resp = {.final = true, httpd_ws_frame_t ws_resp = {.final = true,
.fragmented = false, .fragmented = false,
@@ -72,7 +82,9 @@ static esp_err_t handle_ws_message(httpd_req_t *req, httpd_ws_frame_t *ws_pkt)
.payload = (uint8_t *)response, .payload = (uint8_t *)response,
.len = strlen(response)}; .len = strlen(response)};
return httpd_ws_send_frame(req, &ws_resp); esp_err_t ret = httpd_ws_send_frame(req, &ws_resp);
free(response);
return ret;
} }
return ESP_OK; return ESP_OK;
@@ -84,6 +96,13 @@ esp_err_t websocket_handler(httpd_req_t *req)
{ {
// This is the handshake // This is the handshake
ESP_LOGI(TAG, "WebSocket handshake"); ESP_LOGI(TAG, "WebSocket handshake");
int fd = httpd_req_to_sockfd(req);
if (!add_client(fd))
{
// Zu viele Clients, Verbindung schließen
httpd_sess_trigger_close(req->handle, fd);
return ESP_FAIL;
}
return ESP_OK; return ESP_OK;
} }
@@ -172,6 +191,8 @@ static void ws_async_send(void *arg)
esp_err_t websocket_handler_init(httpd_handle_t server) esp_err_t websocket_handler_init(httpd_handle_t server)
{ {
message_manager_register_listener(on_message_received);
ws_clients_init(); ws_clients_init();
// Register WebSocket URI handler // Register WebSocket URI handler
httpd_uri_t ws_uri = {.uri = "/ws", httpd_uri_t ws_uri = {.uri = "/ws",

View File

@@ -2,6 +2,7 @@ idf_component_register(SRCS
src/ble/ble_connection.c src/ble/ble_connection.c
src/ble/ble_scanner.c src/ble/ble_scanner.c
src/ble_manager.c src/ble_manager.c
src/dns_hijack.c
src/wifi_manager.c src/wifi_manager.c
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES REQUIRES
@@ -9,6 +10,7 @@ idf_component_register(SRCS
driver driver
nvs_flash nvs_flash
esp_insights esp_insights
analytics
led-manager led-manager
api-server api-server
) )

View File

@@ -0,0 +1,13 @@
#pragma once
#ifdef __cplusplus
extern "C"
{
#endif
void dns_server_start(const char *ap_ip);
void dns_set_ap_ip(const char *ip);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,79 @@
// Minimaler DNS-Server für Captive Portal (alle Anfragen auf AP-IP)
// Quelle: https://github.com/espressif/esp-idf/blob/master/examples/protocols/sntp/main/dns_server.c (angepasst)
#include <arpa/inet.h>
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <lwip/inet.h>
#include <lwip/sockets.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/socket.h>
#define DNS_PORT 53
#define DNS_MAX_LEN 512
static const char *TAG = "dns_hijack";
static char s_ap_ip[16] = "192.168.4.1"; // Default AP-IP, ggf. dynamisch setzen
void dns_set_ap_ip(const char *ip)
{
strncpy(s_ap_ip, ip, sizeof(s_ap_ip) - 1);
s_ap_ip[sizeof(s_ap_ip) - 1] = 0;
}
static void dns_server_task(void *pvParameters)
{
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
ESP_LOGE(TAG, "Failed to create socket");
vTaskDelete(NULL);
return;
}
struct sockaddr_in server_addr = {0};
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(DNS_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
uint8_t buf[DNS_MAX_LEN];
while (1)
{
struct sockaddr_in from;
socklen_t fromlen = sizeof(from);
int len = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&from, &fromlen);
if (len < 0)
continue;
// DNS Header: 12 bytes, Antwort-Flag setzen
buf[2] |= 0x80; // QR=1 (Antwort)
buf[3] = 0x80; // RA=1, RCODE=0
// Fragen: 1, Antworten: 1
buf[7] = 1;
// Antwort anhängen (Name Pointer auf Frage)
int pos = len;
buf[pos++] = 0xC0;
buf[pos++] = 0x0C; // Name pointer
buf[pos++] = 0x00;
buf[pos++] = 0x01; // Type A
buf[pos++] = 0x00;
buf[pos++] = 0x01; // Class IN
buf[pos++] = 0x00;
buf[pos++] = 0x00;
buf[pos++] = 0x00;
buf[pos++] = 0x3C; // TTL 60s
buf[pos++] = 0x00;
buf[pos++] = 0x04; // Data length
inet_pton(AF_INET, s_ap_ip, &buf[pos]);
pos += 4;
sendto(sock, buf, pos, 0, (struct sockaddr *)&from, fromlen);
}
close(sock);
vTaskDelete(NULL);
}
void dns_server_start(const char *ap_ip)
{
dns_set_ap_ip(ap_ip);
xTaskCreate(dns_server_task, "dns_server", 4096, NULL, 3, NULL);
}

View File

@@ -1,7 +1,8 @@
#include "wifi_manager.h" #include "wifi_manager.h"
#include "dns_hijack.h"
#include "analytics.h"
#include "api_server.h" #include "api_server.h"
#include <esp_event.h> #include <esp_event.h>
#include <esp_insights.h> #include <esp_insights.h>
#include <esp_log.h> #include <esp_log.h>
@@ -13,222 +14,166 @@
#include <led_status.h> #include <led_status.h>
#include <lwip/err.h> #include <lwip/err.h>
#include <lwip/sys.h> #include <lwip/sys.h>
#include <mdns.h>
#include <nvs_flash.h> #include <nvs_flash.h>
#include <persistence_manager.h>
#include <sdkconfig.h> #include <sdkconfig.h>
#include <string.h> #include <string.h>
// Event group to signal when we are connected // Event group to signal WiFi connection status
static EventGroupHandle_t s_wifi_event_group; static EventGroupHandle_t s_wifi_event_group;
// The bits for the event group // Event group bits
#define WIFI_CONNECTED_BIT BIT0 #define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT BIT1 #define WIFI_FAIL_BIT BIT1
static const char *TAG = "wifi_manager"; static const char *TAG = "wifi_manager";
static int s_retry_num = 0; static void led_status_reconnect()
static int s_current_network_index = 0;
// WiFi network configuration structure
typedef struct
{ {
const char *ssid; led_behavior_t led_behavior = {
const char *password; .on_time_ms = 250,
} wifi_network_config_t; .off_time_ms = 100,
.color = {.red = 50, .green = 50, .blue = 0},
// Array of configured WiFi networks .index = 0,
static const wifi_network_config_t s_wifi_networks[] = { .mode = LED_MODE_BLINK,
#if CONFIG_WIFI_ENABLED
{CONFIG_WIFI_SSID_1, CONFIG_WIFI_PASSWORD_1},
#if CONFIG_WIFI_NETWORK_COUNT >= 2
{CONFIG_WIFI_SSID_2, CONFIG_WIFI_PASSWORD_2},
#endif
#if CONFIG_WIFI_NETWORK_COUNT >= 3
{CONFIG_WIFI_SSID_3, CONFIG_WIFI_PASSWORD_3},
#endif
#if CONFIG_WIFI_NETWORK_COUNT >= 4
{CONFIG_WIFI_SSID_4, CONFIG_WIFI_PASSWORD_4},
#endif
#if CONFIG_WIFI_NETWORK_COUNT >= 5
{CONFIG_WIFI_SSID_5, CONFIG_WIFI_PASSWORD_5},
#endif
#endif
};
static const int s_wifi_network_count = sizeof(s_wifi_networks) / sizeof(s_wifi_networks[0]);
static void try_next_network(void);
static void connect_to_network(int index)
{
#if CONFIG_WIFI_ENABLED
if (index >= s_wifi_network_count)
{
ESP_LOGE(TAG, "No more networks to try");
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
return;
}
const wifi_network_config_t *network = &s_wifi_networks[index];
// Skip empty SSIDs
if (network->ssid == NULL || strlen(network->ssid) == 0)
{
ESP_LOGW(TAG, "Skipping empty SSID at index %d", index);
s_current_network_index++;
s_retry_num = 0;
try_next_network();
return;
}
ESP_DIAG_EVENT(TAG, "Trying to connect to network %d: %s", index + 1, network->ssid);
wifi_config_t wifi_config = {
.sta =
{
.threshold.authmode = WIFI_AUTH_WPA2_PSK,
},
}; };
led_status_set_behavior(led_behavior);
strncpy((char *)wifi_config.sta.ssid, network->ssid, sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, network->password, sizeof(wifi_config.sta.password) - 1);
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
esp_wifi_connect();
#endif
} }
static void try_next_network(void) static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{ {
#if CONFIG_WIFI_ENABLED
s_current_network_index++;
s_retry_num = 0;
if (s_current_network_index < s_wifi_network_count)
{
connect_to_network(s_current_network_index);
}
else
{
ESP_LOGE(TAG, "Failed to connect to any configured network");
led_behavior_t led0_behavior = {
.index = 0,
.mode = LED_MODE_BLINK,
.color = {.red = 50, .green = 0, .blue = 0},
.on_time_ms = 1000,
.off_time_ms = 500,
};
led_status_set_behavior(led0_behavior);
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
}
#endif
}
static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
#if CONFIG_WIFI_ENABLED
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{ {
led_behavior_t led0_behavior = { ESP_LOGI(TAG, "WIFI_EVENT_STA_START: Connecting to AP...");
.index = 0, esp_wifi_connect();
.mode = LED_MODE_BLINK,
.color = {.red = 50, .green = 50, .blue = 0},
.on_time_ms = 200,
.off_time_ms = 200,
};
led_status_set_behavior(led0_behavior);
connect_to_network(s_current_network_index);
} }
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{ {
if (s_retry_num < CONFIG_WIFI_CONNECT_RETRIES) ESP_LOGW(TAG, "WIFI_EVENT_STA_DISCONNECTED: Verbindung verloren, versuche erneut...");
{ xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
led_behavior_t led0_behavior = { led_status_reconnect();
.index = 0, esp_wifi_connect();
.mode = LED_MODE_BLINK,
.color = {.red = 50, .green = 50, .blue = 0},
.on_time_ms = 200,
.off_time_ms = 200,
};
led_status_set_behavior(led0_behavior);
s_retry_num++;
ESP_DIAG_EVENT(TAG, "Retrying network %d (%d/%d)", s_current_network_index + 1, s_retry_num,
CONFIG_WIFI_CONNECT_RETRIES);
esp_wifi_connect();
return;
}
// Retries exhausted for current network, try next one
ESP_LOGW(TAG, "Failed to connect to network %d after %d retries, trying next...", s_current_network_index + 1,
CONFIG_WIFI_CONNECT_RETRIES);
try_next_network();
} }
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{ {
led_behavior_t led0_behavior = {
.index = 0,
.mode = LED_MODE_SOLID,
.color = {.red = 0, .green = 50, .blue = 0},
};
led_status_set_behavior(led0_behavior);
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_DIAG_EVENT(TAG, "Got IP address:" IPSTR " (network %d: %s)", IP2STR(&event->ip_info.ip), ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP: IP-Adresse erhalten: " IPSTR, IP2STR(&event->ip_info.ip));
s_current_network_index + 1, s_wifi_networks[s_current_network_index].ssid); analytics_init();
s_retry_num = 0;
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
} }
#endif else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP)
{
ESP_LOGW(TAG, "IP_EVENT_STA_LOST_IP: IP-Adresse verloren! Versuche Reconnect...");
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
led_status_reconnect();
esp_wifi_connect();
}
}
static void wifi_create_ap()
{
ESP_ERROR_CHECK(esp_wifi_stop());
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
wifi_config_t ap_config = {.ap = {.ssid = "system-control",
.ssid_len = strlen("system-control"),
.password = "",
.max_connection = 4,
.authmode = WIFI_AUTH_OPEN}};
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Access Point 'system-control' started");
dns_server_start("192.168.4.1");
led_behavior_t led_behavior = {
.color = {.red = 50, .green = 0, .blue = 0},
.index = 0,
.mode = LED_MODE_SOLID,
};
led_status_set_behavior(led_behavior);
} }
void wifi_manager_init() void wifi_manager_init()
{ {
#if CONFIG_WIFI_ENABLED
s_wifi_event_group = xEventGroupCreate(); s_wifi_event_group = xEventGroupCreate();
s_current_network_index = 0;
s_retry_num = 0;
ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default()); ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); // Default WiFi Station
ESP_ERROR_CHECK(esp_wifi_init(&cfg)); esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
esp_event_handler_instance_t instance_any_id; // Event Handler registrieren
esp_event_handler_instance_t instance_got_ip; ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
ESP_ERROR_CHECK( ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id));
ESP_ERROR_CHECK(
esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); // Try to load stored WiFi configuration
ESP_ERROR_CHECK(esp_wifi_start()); persistence_manager_t pm;
char ssid[33] = {0};
ESP_DIAG_EVENT(TAG, "WiFi manager initialized with %d network(s), waiting for connection...", s_wifi_network_count); char password[65] = {0};
bool have_ssid = false, have_password = false;
/* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or if (persistence_manager_init(&pm, "wifi_config") == ESP_OK)
connection failed for all networks (WIFI_FAIL_BIT). The bits are set by event_handler() */
EventBits_t bits =
xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY);
if (bits & WIFI_CONNECTED_BIT)
{ {
ESP_DIAG_EVENT(TAG, "Connected to AP SSID:%s", s_wifi_networks[s_current_network_index].ssid); persistence_manager_get_string(&pm, "ssid", ssid, sizeof(ssid), "");
persistence_manager_get_string(&pm, "password", password, sizeof(password), "");
api_server_config_t s_config = API_SERVER_CONFIG_DEFAULT(); have_ssid = strlen(ssid) > 0;
ESP_ERROR_CHECK(api_server_start(&s_config)); have_password = strlen(password) > 0;
} }
else if (bits & WIFI_FAIL_BIT)
if (have_ssid && have_password)
{ {
ESP_LOGE(TAG, "Failed to connect to any configured WiFi network"); led_status_reconnect();
ESP_LOGI(TAG, "Found WiFi configuration: SSID='%s'", ssid);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
wifi_config_t wifi_config = {0};
strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1);
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
int retries = 0;
EventBits_t bits;
do
{
ESP_LOGI(TAG, "Warte auf IP-Adresse (DHCP)...");
bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
10000 / portTICK_PERIOD_MS);
if (bits & WIFI_CONNECTED_BIT)
{
led_behavior_t led_behavior = {
.index = 0,
.color = {.red = 0, .green = 50, .blue = 0},
.mode = LED_MODE_SOLID,
};
led_status_set_behavior(led_behavior);
ESP_LOGI(TAG, "WiFi connection established successfully (mit IP)");
break;
}
retries++;
} while (!(bits & WIFI_CONNECTED_BIT) && retries < CONFIG_WIFI_CONNECT_RETRIES);
if (!(bits & WIFI_CONNECTED_BIT))
{
ESP_LOGW(TAG, "WiFi connection failed (keine IP?), switching to Access Point mode");
// AP-Netzwerkschnittstelle initialisieren, falls noch nicht geschehen
esp_netif_create_default_wifi_ap();
wifi_create_ap();
}
} }
else else
{ {
ESP_LOGE(TAG, "Unexpected event"); // Create Access Point
esp_netif_create_default_wifi_ap();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
wifi_create_ap();
} }
#endif
// API server start
api_server_config_t s_config = API_SERVER_CONFIG_DEFAULT();
ESP_ERROR_CHECK(api_server_start(&s_config));
} }

View File

@@ -18,4 +18,5 @@ idf_component_register(SRCS
led-manager led-manager
persistence-manager persistence-manager
simulator simulator
message-manager
) )

View File

@@ -17,7 +17,7 @@
// Project-specific headers // Project-specific headers
#include "common/Widget.h" #include "common/Widget.h"
#include "IPersistenceManager.h" #include "persistence_manager.h"
#include "u8g2.h" #include "u8g2.h"
class MenuItem; class MenuItem;
@@ -28,7 +28,7 @@ class MenuItem;
* @details This structure serves as a configuration container that provides menu widgets * @details This structure serves as a configuration container that provides menu widgets
* with access to the display system, screen management functions, input * with access to the display system, screen management functions, input
* handling callbacks, and persistent storage. * handling callbacks, and persistent storage.
* *
* @see Widget * @see Widget
* @see ButtonType * @see ButtonType
* @see IPersistenceManager * @see IPersistenceManager
@@ -61,14 +61,8 @@ typedef struct
std::function<void(ButtonType button)> onButtonClicked; std::function<void(ButtonType button)> onButtonClicked;
/** /**
* @brief Shared pointer to platform-independent persistence manager * @brief Zeiger auf C-Persistence-Manager-Instanz
* @details This provides access to persistent key-value storage across different
* platforms. The actual implementation (SDL3 or ESP32/NVS) is determined
* at compile time based on the target platform.
*
* @note The persistence manager is shared across all menu widgets and maintains
* its state throughout the application lifecycle.
*/ */
std::shared_ptr<IPersistenceManager> persistenceManager; persistence_manager_t *persistenceManager;
} menu_options_t; } menu_options_t;

View File

@@ -175,6 +175,21 @@ class Menu : public Widget
*/ */
void toggle(const MenuItem &menuItem); void toggle(const MenuItem &menuItem);
/**
* @brief Setzt den Zustand eines Toggle-Menüeintrags explizit
* @param menuItem Der zu ändernde Toggle-Menüeintrag
* @param state Neuer Zustand (true = aktiviert, false = deaktiviert)
*
* @pre menuItem muss vom Typ TOGGLE sein
* @post Der Wert des Menüeintrags wird auf den angegebenen Zustand gesetzt
*
* @details Diese Methode setzt den Wert eines Toggle-Menüeintrags gezielt auf den gewünschten Zustand.
* Der geänderte Eintrag ersetzt das Original in der internen Menüstruktur.
*
* @note Diese Methode verändert direkt den internen Zustand des Menüs.
*/
void setToggle(const MenuItem &menuItem, const bool state);
/** /**
* @brief Changes the selected value of a selection menu item based on button input * @brief Changes the selected value of a selection menu item based on button input
* @param menuItem The selection menu item to modify * @param menuItem The selection menu item to modify
@@ -191,6 +206,8 @@ class Menu : public Widget
*/ */
MenuItem switchValue(const MenuItem &menuItem, ButtonType button); MenuItem switchValue(const MenuItem &menuItem, ButtonType button);
void setSelectionIndex(const MenuItem &menuItem, int index);
private: private:
MenuItem replaceItem(int index, const MenuItem &item); MenuItem replaceItem(int index, const MenuItem &item);

View File

@@ -13,6 +13,7 @@
#include "u8g2.h" #include "u8g2.h"
#include "common/Common.h" #include "common/Common.h"
#include "message_manager.h"
/** /**
* @class Widget * @class Widget
@@ -49,7 +50,9 @@ class Widget
* @details Ensures that derived class destructors are called correctly when * @details Ensures that derived class destructors are called correctly when
* a widget is destroyed through a base class pointer. * a widget is destroyed through a base class pointer.
*/ */
virtual ~Widget() = default; virtual ~Widget();
virtual void onMessageReceived(const message_t *msg);
/** /**
* @brief Called when the widget becomes active or enters the foreground * @brief Called when the widget becomes active or enters the foreground
@@ -178,4 +181,8 @@ class Widget
* the u8g2 context and assumes it is managed externally. * the u8g2 context and assumes it is managed externally.
*/ */
u8g2_t *u8g2; u8g2_t *u8g2;
private:
static std::vector<Widget *> s_instances;
static void globalMessageCallback(const message_t *msg);
}; };

View File

@@ -120,6 +120,8 @@ class LightMenu final : public Menu
*/ */
void onButtonPressed(const MenuItem &menuItem, ButtonType button) override; void onButtonPressed(const MenuItem &menuItem, ButtonType button) override;
void onMessageReceived(const message_t *msg);
/** /**
* @brief Pointer to menu options configuration structure * @brief Pointer to menu options configuration structure
* @details Stores a reference to the menu configuration passed during construction. * @details Stores a reference to the menu configuration passed during construction.

View File

@@ -28,7 +28,7 @@ constexpr int BOTTOM_OFFSET = 10;
Menu::Menu(menu_options_t *options) : Widget(options->u8g2), m_options(options) Menu::Menu(menu_options_t *options) : Widget(options->u8g2), m_options(options)
{ {
// Set up button callback using lambda to forward to member function // Set up button callback using a lambda to forward to the member function
m_options->onButtonClicked = [this](const ButtonType button) { OnButtonClicked(button); }; m_options->onButtonClicked = [this](const ButtonType button) { OnButtonClicked(button); };
} }
@@ -64,7 +64,7 @@ void Menu::setItemSize(const size_t size, int8_t startIndex)
constexpr int key_length = 20; constexpr int key_length = 20;
char key[key_length] = ""; char key[key_length] = "";
snprintf(key, key_length, "section_%zu", i + 1 - startIndex); snprintf(key, key_length, "section_%zu", i + 1 - startIndex);
index = m_options->persistenceManager->GetValue(key, index); index = persistence_manager_get_int(m_options->persistenceManager, key, index);
} }
addSelection(i + 1, caption, m_items.at(0).getValues(), index); addSelection(i + 1, caption, m_items.at(0).getValues(), index);
} }
@@ -82,6 +82,12 @@ void Menu::toggle(const MenuItem &menuItem)
replaceItem(menuItem.getId(), item); replaceItem(menuItem.getId(), item);
} }
void Menu::setToggle(const MenuItem &menuItem, const bool state)
{
const auto item = menuItem.copyWith(state ? std::to_string(true) : std::to_string(false));
replaceItem(menuItem.getId(), item);
}
MenuItem Menu::switchValue(const MenuItem &menuItem, ButtonType button) MenuItem Menu::switchValue(const MenuItem &menuItem, ButtonType button)
{ {
MenuItem result = menuItem; MenuItem result = menuItem;
@@ -120,6 +126,15 @@ MenuItem Menu::switchValue(const MenuItem &menuItem, ButtonType button)
return result; return result;
} }
void Menu::setSelectionIndex(const MenuItem &menuItem, int index)
{
if (index >= 0 && index < menuItem.getItemCount())
{
auto item = menuItem.copyWith(index);
replaceItem(menuItem.getId(), item);
}
}
MenuItem Menu::replaceItem(const int index, const MenuItem &item) MenuItem Menu::replaceItem(const int index, const MenuItem &item)
{ {
m_items.at(index) = item; m_items.at(index) = item;
@@ -134,13 +149,13 @@ void Menu::Render()
m_selected_item = 0; m_selected_item = 0;
} }
// Early return if no items to render // Early return if there are no items to render
if (m_items.empty()) if (m_items.empty())
{ {
return; return;
} }
// Clear screen with black background // Clear the screen with a black background
u8g2_SetDrawColor(u8g2, 0); u8g2_SetDrawColor(u8g2, 0);
u8g2_DrawBox(u8g2, 0, 0, u8g2->width, u8g2->height); u8g2_DrawBox(u8g2, 0, 0, u8g2->width, u8g2->height);
@@ -151,7 +166,7 @@ void Menu::Render()
drawScrollBar(); drawScrollBar();
drawSelectionBox(); drawSelectionBox();
// Calculate center position for main item // Calculate center position for the main item
const int centerY = u8g2->height / 2 + 3; const int centerY = u8g2->height / 2 + 3;
// Render the currently selected item (main/center item) // Render the currently selected item (main/center item)
@@ -176,7 +191,7 @@ void Menu::Render()
void Menu::renderWidget(const MenuItem *item, const uint8_t *font, const int x, const int y) const void Menu::renderWidget(const MenuItem *item, const uint8_t *font, const int x, const int y) const
{ {
// Set font and draw main text // Set font and draw the main text
u8g2_SetFont(u8g2, font); u8g2_SetFont(u8g2, font);
u8g2_DrawStr(u8g2, x, y, item->getText().c_str()); u8g2_DrawStr(u8g2, x, y, item->getText().c_str());
@@ -206,7 +221,7 @@ void Menu::renderWidget(const MenuItem *item, const uint8_t *font, const int x,
} }
case MenuItemTypes::TOGGLE: { case MenuItemTypes::TOGGLE: {
// Draw checkbox frame // Draw the checkbox frame
const int frameX = u8g2->width - UIConstants::FRAME_BOX_SIZE - UIConstants::SELECTION_MARGIN; const int frameX = u8g2->width - UIConstants::FRAME_BOX_SIZE - UIConstants::SELECTION_MARGIN;
const int frameY = y - UIConstants::FRAME_OFFSET; const int frameY = y - UIConstants::FRAME_OFFSET;
u8g2_DrawFrame(u8g2, frameX, frameY, UIConstants::FRAME_BOX_SIZE, UIConstants::FRAME_BOX_SIZE); u8g2_DrawFrame(u8g2, frameX, frameY, UIConstants::FRAME_BOX_SIZE, UIConstants::FRAME_BOX_SIZE);
@@ -272,7 +287,7 @@ void Menu::onPressedDown()
if (m_items.empty()) if (m_items.empty())
return; return;
// Wrap around to first item when at the end // Wrap around to the first item when at the end
m_selected_item = (m_selected_item + 1) % m_items.size(); m_selected_item = (m_selected_item + 1) % m_items.size();
} }
@@ -281,7 +296,7 @@ void Menu::onPressedUp()
if (m_items.empty()) if (m_items.empty())
return; return;
// Wrap around to last item when at the beginning // Wrap around to the last item when at the beginning
m_selected_item = (m_selected_item == 0) ? m_items.size() - 1 : m_selected_item - 1; m_selected_item = (m_selected_item == 0) ? m_items.size() - 1 : m_selected_item - 1;
} }
@@ -314,7 +329,7 @@ void Menu::onPressedSelect() const
void Menu::onPressedBack() const void Menu::onPressedBack() const
{ {
// Navigate back to previous screen if callback is available // Navigate back to the previous screen if callback is available
if (m_options && m_options->popScreen) if (m_options && m_options->popScreen)
{ {
m_options->popScreen(); m_options->popScreen();

View File

@@ -1,7 +1,20 @@
#include "common/Widget.h" #include "common/Widget.h"
#include <algorithm>
std::vector<Widget *> Widget::s_instances;
Widget::Widget(u8g2_t *u8g2) : u8g2(u8g2) Widget::Widget(u8g2_t *u8g2) : u8g2(u8g2)
{ {
s_instances.push_back(this);
if (s_instances.size() == 1)
{
message_manager_register_listener(globalMessageCallback);
}
}
Widget::~Widget()
{
s_instances.erase(std::remove(s_instances.begin(), s_instances.end(), this), s_instances.end());
} }
void Widget::onEnter() void Widget::onEnter()
@@ -36,3 +49,15 @@ const char *Widget::getName() const
{ {
return "Widget"; return "Widget";
} }
void Widget::onMessageReceived(const message_t *msg)
{
}
void Widget::globalMessageCallback(const message_t *msg)
{
for (auto *w : s_instances)
{
w->onMessageReceived(msg);
}
}

View File

@@ -1,5 +1,5 @@
#include "ui/ClockScreenSaver.h" #include "ui/ClockScreenSaver.h"
#include "hal_esp32/PersistenceManager.h" #include "persistence_manager.h"
#include "simulator.h" #include "simulator.h"
#include <cstring> #include <cstring>
#include <ctime> #include <ctime>
@@ -38,13 +38,14 @@ void ClockScreenSaver::updateTextDimensions()
void ClockScreenSaver::getCurrentTimeString(char *buffer, size_t bufferSize) const void ClockScreenSaver::getCurrentTimeString(char *buffer, size_t bufferSize) const
{ {
if (m_options && m_options->persistenceManager->GetValue("light_active", false) && if (m_options && m_options->persistenceManager &&
m_options->persistenceManager->GetValue("light_mode", 0) == 0) persistence_manager_get_bool(m_options->persistenceManager, "light_active", false) &&
persistence_manager_get_int(m_options->persistenceManager, "light_mode", 0) == 0)
{ {
char *simulated_time = get_time(); char *simulated_time = get_time();
if (simulated_time != nullptr) if (simulated_time != nullptr)
{ {
strncpy(buffer, simulated_time, bufferSize); snprintf(buffer, bufferSize, "%s Uhr", simulated_time);
return; return;
} }
} }

View File

@@ -1,7 +1,8 @@
#include "ui/LightMenu.h" #include "ui/LightMenu.h"
#include "led_strip_ws2812.h" #include "led_strip_ws2812.h"
#include "message_manager.h"
#include "simulator.h" #include "simulator.h"
#include <cstring>
/** /**
* @namespace LightMenuItem * @namespace LightMenuItem
@@ -27,7 +28,7 @@ LightMenu::LightMenu(menu_options_t *options) : Menu(options), m_options(options
bool active = false; bool active = false;
if (m_options && m_options->persistenceManager) if (m_options && m_options->persistenceManager)
{ {
active = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_ACTIVE, active); active = persistence_manager_get_bool(m_options->persistenceManager, LightMenuOptions::LIGHT_ACTIVE, active);
} }
addToggle(LightMenuItem::ACTIVATE, "Einschalten", active); addToggle(LightMenuItem::ACTIVATE, "Einschalten", active);
@@ -39,7 +40,8 @@ LightMenu::LightMenu(menu_options_t *options) : Menu(options), m_options(options
int mode_value = 0; int mode_value = 0;
if (m_options && m_options->persistenceManager) if (m_options && m_options->persistenceManager)
{ {
mode_value = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_MODE, mode_value); mode_value =
persistence_manager_get_int(m_options->persistenceManager, LightMenuOptions::LIGHT_MODE, mode_value);
} }
addSelection(LightMenuItem::MODE, "Modus", items, mode_value); addSelection(LightMenuItem::MODE, "Modus", items, mode_value);
@@ -50,7 +52,9 @@ LightMenu::LightMenu(menu_options_t *options) : Menu(options), m_options(options
int variant_value = 3; int variant_value = 3;
if (m_options && m_options->persistenceManager) if (m_options && m_options->persistenceManager)
{ {
variant_value = m_options->persistenceManager->GetValue(LightMenuOptions::LIGHT_VARIANT, variant_value) - 1; variant_value =
persistence_manager_get_int(m_options->persistenceManager, LightMenuOptions::LIGHT_VARIANT, variant_value) -
1;
} }
addSelection(LightMenuItem::VARIANT, "Variante", variants, variant_value); addSelection(LightMenuItem::VARIANT, "Variante", variants, variant_value);
} }
@@ -68,12 +72,13 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
{ {
toggle(menuItem); toggle(menuItem);
const auto value = getItem(menuItem.getId()).getValue() == "1"; const auto value = getItem(menuItem.getId()).getValue() == "1";
if (m_options && m_options->persistenceManager) // Post change via message_manager
{ message_t msg = {};
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_ACTIVE, value); msg.type = MESSAGE_TYPE_SETTINGS;
} msg.data.settings.type = SETTINGS_TYPE_BOOL;
strncpy(msg.data.settings.key, LightMenuOptions::LIGHT_ACTIVE, sizeof(msg.data.settings.key) - 1);
start_simulation(); msg.data.settings.value.bool_value = value;
message_manager_post(&msg, pdMS_TO_TICKS(100));
} }
break; break;
} }
@@ -86,11 +91,14 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
const auto value = getItem(item.getId()).getIndex(); const auto value = getItem(item.getId()).getIndex();
if (m_options && m_options->persistenceManager) if (m_options && m_options->persistenceManager)
{ {
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_MODE, value); // Post change via message_manager
m_options->persistenceManager->Save(); message_t msg = {};
msg.type = MESSAGE_TYPE_SETTINGS;
msg.data.settings.type = SETTINGS_TYPE_INT;
strncpy(msg.data.settings.key, LightMenuOptions::LIGHT_MODE, sizeof(msg.data.settings.key) - 1);
msg.data.settings.value.int_value = value;
message_manager_post(&msg, pdMS_TO_TICKS(100));
} }
start_simulation();
} }
break; break;
} }
@@ -103,11 +111,14 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
const auto value = getItem(item.getId()).getIndex() + 1; const auto value = getItem(item.getId()).getIndex() + 1;
if (m_options && m_options->persistenceManager) if (m_options && m_options->persistenceManager)
{ {
m_options->persistenceManager->SetValue(LightMenuOptions::LIGHT_VARIANT, value); // Post change via message_manager
m_options->persistenceManager->Save(); message_t msg = {};
msg.type = MESSAGE_TYPE_SETTINGS;
msg.data.settings.type = SETTINGS_TYPE_INT;
strncpy(msg.data.settings.key, LightMenuOptions::LIGHT_VARIANT, sizeof(msg.data.settings.key) - 1);
msg.data.settings.value.int_value = value;
message_manager_post(&msg, pdMS_TO_TICKS(100));
} }
start_simulation();
} }
break; break;
} }
@@ -124,4 +135,25 @@ void LightMenu::onButtonPressed(const MenuItem &menuItem, const ButtonType butto
} }
} }
void LightMenu::onMessageReceived(const message_t *msg)
{
// Here you can react to messages, e.g. set toggle status
// Example: If light_active was changed, synchronize toggle
if (msg && msg->type == MESSAGE_TYPE_SETTINGS)
{
if (std::strcmp(msg->data.settings.key, LightMenuOptions::LIGHT_ACTIVE) == 0)
{
setToggle(getItem(LightMenuItem::ACTIVATE), msg->data.settings.value.bool_value);
}
if (std::strcmp(msg->data.settings.key, LightMenuOptions::LIGHT_MODE) == 0)
{
setSelectionIndex(getItem(LightMenuItem::MODE), msg->data.settings.value.int_value);
}
if (std::strcmp(msg->data.settings.key, LightMenuOptions::LIGHT_VARIANT) == 0)
{
setSelectionIndex(getItem(LightMenuItem::VARIANT), msg->data.settings.value.int_value - 1);
}
}
}
IMPLEMENT_GET_NAME(LightMenu) IMPLEMENT_GET_NAME(LightMenu)

View File

@@ -12,9 +12,9 @@ typedef struct
typedef struct typedef struct
{ {
uint8_t h; float h;
uint8_t s; float s;
uint8_t v; float v;
} hsv_t; } hsv_t;
__BEGIN_DECLS __BEGIN_DECLS

View File

@@ -0,0 +1,16 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#define LED_SEGMENT_MAX_LEN 15
typedef struct
{
char name[32];
uint16_t start;
uint16_t leds;
} led_segment_t;
led_segment_t segments[LED_SEGMENT_MAX_LEN];
size_t segment_count;

View File

@@ -45,10 +45,10 @@ rgb_t interpolate_color_hsv(rgb_t start, rgb_t end, float factor)
// Interpolate HSV values // Interpolate HSV values
hsv_t interpolated_hsv; hsv_t interpolated_hsv;
interpolated_hsv.h = fmod(h1 + (h2 - h1) * factor, 360.0); interpolated_hsv.h = fmodf(h1 + (h2 - h1) * factor, 360.0f);
if (interpolated_hsv.h < 0) if (interpolated_hsv.h < 0.0f)
{ {
interpolated_hsv.h += 360.0; interpolated_hsv.h += 360.0f;
} }
interpolated_hsv.s = start_hsv.s + (end_hsv.s - start_hsv.s) * factor; interpolated_hsv.s = start_hsv.s + (end_hsv.s - start_hsv.s) * factor;
interpolated_hsv.v = start_hsv.v + (end_hsv.v - start_hsv.v) * factor; interpolated_hsv.v = start_hsv.v + (end_hsv.v - start_hsv.v) * factor;

View File

@@ -97,22 +97,21 @@ esp_err_t led_status_init(int gpio_num)
.max_leds = STATUS_LED_COUNT, .max_leds = STATUS_LED_COUNT,
.led_model = LED_MODEL_WS2812, .led_model = LED_MODEL_WS2812,
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRBW, .color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRBW,
.flags = .flags = {.invert_out = 0},
{
.invert_out = false,
},
}; };
led_strip_rmt_config_t rmt_config = { led_strip_rmt_config_t rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT, .clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 10 * 1000 * 1000, // 10MHz .resolution_hz = 10 * 1000 * 1000, // 10MHz
.mem_block_symbols = 0, .mem_block_symbols = 0,
.flags = .flags = {.with_dma = 0},
{
.with_dma = false,
},
}; };
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip)); esp_err_t ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip);
ESP_LOGI(TAG, "LED strip initialized."); if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init status LED: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "Status LED initialized.");
// Create mutex // Create mutex
mutex = xSemaphoreCreateMutex(); mutex = xSemaphoreCreateMutex();

View File

@@ -72,17 +72,21 @@ esp_err_t led_strip_init(void)
.max_leds = MAX_LEDS, .max_leds = MAX_LEDS,
.led_model = LED_MODEL_WS2812, .led_model = LED_MODEL_WS2812,
.color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB, .color_component_format = LED_STRIP_COLOR_COMPONENT_FMT_GRB,
.flags = {.invert_out = false}, .flags = {.invert_out = 0},
}; };
led_strip_rmt_config_t rmt_config = { led_strip_rmt_config_t rmt_config = {
.clk_src = RMT_CLK_SRC_DEFAULT, .clk_src = RMT_CLK_SRC_DEFAULT,
.resolution_hz = 0, .resolution_hz = 0,
.mem_block_symbols = 0, .mem_block_symbols = 0,
.flags = {.with_dma = true}, .flags = {.with_dma = 0},
}; };
esp_err_t ret = led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip);
ESP_ERROR_CHECK(led_strip_new_rmt_device(&strip_config, &rmt_config, &led_strip)); if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to init main LED strip: %s", esp_err_to_name(ret));
return ret;
}
led_command_queue = xQueueCreate(5, sizeof(led_command_t)); led_command_queue = xQueueCreate(5, sizeof(led_command_t));
if (led_command_queue == NULL) if (led_command_queue == NULL)

View File

@@ -0,0 +1,8 @@
idf_component_register(
SRCS "src/message_manager.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES
persistence-manager
my_mqtt_client
app_update
)

View File

@@ -0,0 +1,79 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
#endif
typedef enum
{
MESSAGE_TYPE_SETTINGS,
MESSAGE_TYPE_BUTTON,
MESSAGE_TYPE_SIMULATION
} message_type_t;
typedef enum
{
BUTTON_EVENT_PRESS,
BUTTON_EVENT_RELEASE
} button_event_type_t;
typedef struct
{
button_event_type_t event_type;
uint8_t button_id;
} button_message_t;
typedef enum
{
SETTINGS_TYPE_BOOL,
SETTINGS_TYPE_INT,
SETTINGS_TYPE_FLOAT,
SETTINGS_TYPE_STRING
} settings_type_t;
typedef struct
{
settings_type_t type;
char key[32];
union {
bool bool_value;
int32_t int_value;
float float_value;
char string_value[64];
} value;
} settings_message_t;
typedef struct
{
char time[6];
uint8_t red;
uint8_t green;
uint8_t blue;
} simulation_message_t;
typedef struct
{
message_type_t type;
union {
settings_message_t settings;
button_message_t button;
simulation_message_t simulation;
} data;
} message_t;
// Observer Pattern: Listener-Typ und Registrierungsfunktionen
typedef void (*message_listener_t)(const message_t *msg);
void message_manager_register_listener(message_listener_t listener);
void message_manager_unregister_listener(message_listener_t listener);
void message_manager_init(void);
bool message_manager_post(const message_t *msg, TickType_t timeout);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,134 @@
#include "message_manager.h"
#include "my_mqtt_client.h"
#include <esp_app_desc.h>
#include <esp_log.h>
#include <esp_mac.h>
#include <esp_system.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <persistence_manager.h>
#include <sdkconfig.h>
#include <string.h>
#define MESSAGE_QUEUE_LENGTH 16
#define MESSAGE_QUEUE_ITEM_SIZE sizeof(message_t)
static const char *TAG = "message_manager";
static QueueHandle_t message_queue = NULL;
// Observer Pattern: Listener-Liste
#define MAX_MESSAGE_LISTENERS 8
static message_listener_t message_listeners[MAX_MESSAGE_LISTENERS] = {0};
static size_t message_listener_count = 0;
void message_manager_register_listener(message_listener_t listener)
{
if (listener && message_listener_count < MAX_MESSAGE_LISTENERS)
{
// Doppelte Registrierung vermeiden
for (size_t i = 0; i < message_listener_count; ++i)
{
if (message_listeners[i] == listener)
return;
}
message_listeners[message_listener_count++] = listener;
}
}
void message_manager_unregister_listener(message_listener_t listener)
{
for (size_t i = 0; i < message_listener_count; ++i)
{
if (message_listeners[i] == listener)
{
// Nachfolgende Listener nach vorne schieben
for (size_t j = i; j < message_listener_count - 1; ++j)
{
message_listeners[j] = message_listeners[j + 1];
}
message_listeners[--message_listener_count] = NULL;
break;
}
}
}
static void message_manager_task(void *param)
{
message_t msg;
persistence_manager_t pm;
while (1)
{
if (xQueueReceive(message_queue, &msg, portMAX_DELAY) == pdTRUE)
{
switch (msg.type)
{
case MESSAGE_TYPE_SETTINGS:
if (persistence_manager_init(&pm, "config") == ESP_OK)
{
switch (msg.data.settings.type)
{
case SETTINGS_TYPE_BOOL:
persistence_manager_set_bool(&pm, msg.data.settings.key, msg.data.settings.value.bool_value);
break;
case SETTINGS_TYPE_INT:
persistence_manager_set_int(&pm, msg.data.settings.key, msg.data.settings.value.int_value);
break;
case SETTINGS_TYPE_FLOAT:
persistence_manager_set_float(&pm, msg.data.settings.key, msg.data.settings.value.float_value);
break;
case SETTINGS_TYPE_STRING:
persistence_manager_set_string(&pm, msg.data.settings.key,
msg.data.settings.value.string_value);
break;
}
persistence_manager_deinit(&pm);
ESP_LOGD(TAG, "Setting written: %s", msg.data.settings.key);
}
break;
case MESSAGE_TYPE_BUTTON:
ESP_LOGD(TAG, "Button event: id=%d, type=%d", msg.data.button.button_id, msg.data.button.event_type);
break;
case MESSAGE_TYPE_SIMULATION:
/// just logging
ESP_LOGD(TAG, "Simulation event: time=%s, color=(%d,%d,%d)", msg.data.simulation.time,
msg.data.simulation.red, msg.data.simulation.green, msg.data.simulation.blue);
break;
}
// Observer Pattern: Listener benachrichtigen
for (size_t i = 0; i < message_listener_count; ++i)
{
if (message_listeners[i])
{
message_listeners[i](&msg);
}
}
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
const esp_app_desc_t *app_desc = esp_app_get_description();
char topic[60];
snprintf(topic, sizeof(topic), "device/%s/%02x%02x", app_desc->project_name, mac[4], mac[5]);
char *data = "{\"key\":\"value\"}";
mqtt_client_publish(topic, data, strlen(data), 0, false);
}
}
}
void message_manager_init(void)
{
if (!message_queue)
{
message_queue = xQueueCreate(MESSAGE_QUEUE_LENGTH, MESSAGE_QUEUE_ITEM_SIZE);
xTaskCreate(message_manager_task, "message_manager_task", 4096, NULL, 5, NULL);
}
}
bool message_manager_post(const message_t *msg, TickType_t timeout)
{
if (!message_queue)
return false;
ESP_LOGD(TAG, "Post: type=%d", msg->type);
return xQueueSend(message_queue, msg, timeout) == pdTRUE;
}

View File

@@ -0,0 +1,7 @@
idf_component_register(
SRCS "src/my_mqtt_client.c"
INCLUDE_DIRS "include"
REQUIRES
mqtt
app_update
)

View File

@@ -0,0 +1,21 @@
menu "MQTT Client Settings"
config MQTT_CLIENT_BROKER_URL
string "MQTT Broker URL (TLS)"
default "mqtts://example.com:8883"
help
Die Adresse des MQTT-Brokers (z.B. mqtts://broker.example.com:8883)
config MQTT_CLIENT_USERNAME
string "MQTT Username"
default "user"
help
Benutzername für die Authentifizierung (optional)
config MQTT_CLIENT_PASSWORD
string "MQTT Password"
default "password"
help
Passwort für die Authentifizierung (optional)
endmenu

View File

@@ -0,0 +1,18 @@
# MQTT Client Component for ESP-IDF
Diese Komponente stellt eine einfache MQTT-Client-Implementierung bereit, die Daten an einen TLS-gesicherten MQTT-Broker sendet.
## Dateien
- mqtt_client.c: Implementierung des MQTT-Clients
- mqtt_client.h: Header-Datei
- CMakeLists.txt: Build-Konfiguration
- Kconfig: Konfiguration für die Komponente
## Abhängigkeiten
- ESP-IDF (empfohlen: >= v4.0)
- Komponenten: esp-mqtt, esp-tls
## Nutzung
1. Füge die Komponente in dein Projekt ein.
2. Passe die Konfiguration in `Kconfig` an.
3. Binde die Komponente in deinem Code ein und nutze die API aus `mqtt_client.h`.

View File

@@ -0,0 +1,2 @@
dependencies:
espressif/mqtt: ^1.0.0

View File

@@ -0,0 +1,14 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
void mqtt_client_start(void);
void mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,122 @@
#include "my_mqtt_client.h"
#include "esp_app_desc.h"
#include "esp_err.h"
#include "esp_interface.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_system.h"
#include "mqtt_client.h"
#include "sdkconfig.h"
static const char *TAG = "mqtt_client";
static esp_mqtt_client_handle_t client = NULL;
extern const uint8_t isrgrootx1_pem_start[] asm("_binary_isrgrootx1_pem_start");
extern const uint8_t isrgrootx1_pem_end[] asm("_binary_isrgrootx1_pem_end");
static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
esp_mqtt_event_handle_t event = event_data;
int msg_id;
switch ((esp_mqtt_event_id_t)event_id)
{
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
msg_id = esp_mqtt_client_subscribe(client, "topic/qos0", 0);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_subscribe(client, "topic/qos1", 1);
ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
msg_id = esp_mqtt_client_unsubscribe(client, "topic/qos1");
ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d, return code=0x%02x ", event->msg_id, (uint8_t)*event->data);
msg_id = esp_mqtt_client_publish(client, "topic/qos0", "data", 0, 0, 0);
ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "MQTT_EVENT_DATA:");
ESP_LOGI(TAG, "TOPIC=%.*s\r\n", event->topic_len, event->topic);
ESP_LOGI(TAG, "DATA=%.*s\r\n", event->data_len, event->data);
break;
case MQTT_EVENT_ERROR:
ESP_LOGE(TAG, "MQTT_EVENT_ERROR");
if (event->error_handle)
{
ESP_LOGE(TAG, "error_type: %d", event->error_handle->error_type);
ESP_LOGE(TAG, "esp-tls error code: 0x%x", event->error_handle->esp_tls_last_esp_err);
ESP_LOGE(TAG, "tls_stack_err: 0x%x", event->error_handle->esp_tls_stack_err);
ESP_LOGE(TAG, "transport_sock_errno: %d", event->error_handle->esp_transport_sock_errno);
}
break;
default:
ESP_LOGI(TAG, "Other event id:%d", event->event_id);
break;
}
}
void mqtt_client_start(void)
{
ESP_LOGI(TAG, "Starte MQTT-Client mit URI: %s", CONFIG_MQTT_CLIENT_BROKER_URL);
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
const esp_app_desc_t *app_desc = esp_app_get_description();
char client_id[60];
snprintf(client_id, sizeof(client_id), "%s/%02x%02x", app_desc->project_name, mac[4], mac[5]);
const esp_mqtt_client_config_t mqtt_cfg = {
.broker.address.uri = CONFIG_MQTT_CLIENT_BROKER_URL,
.broker.verification.certificate = (const char *)isrgrootx1_pem_start,
.broker.verification.certificate_len = isrgrootx1_pem_end - isrgrootx1_pem_start,
.credentials.username = CONFIG_MQTT_CLIENT_USERNAME,
.credentials.client_id = client_id,
.credentials.authentication.password = CONFIG_MQTT_CLIENT_PASSWORD,
};
client = esp_mqtt_client_init(&mqtt_cfg);
if (client == NULL)
{
ESP_LOGE(TAG, "Fehler bei esp_mqtt_client_init!");
return;
}
esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
esp_err_t err = esp_mqtt_client_start(client);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "esp_mqtt_client_start fehlgeschlagen: %s", esp_err_to_name(err));
}
else
{
ESP_LOGI(TAG, "MQTT-Client gestartet");
}
}
void mqtt_client_publish(const char *topic, const char *data, size_t len, int qos, bool retain)
{
if (client)
{
int msg_id = esp_mqtt_client_publish(client, topic, data, len, qos, retain);
ESP_LOGI(TAG, "Publish: topic=%s, msg_id=%d, qos=%d, retain=%d, len=%d", topic, msg_id, qos, retain, (int)len);
}
else
{
ESP_LOGW(TAG, "Publish aufgerufen, aber Client ist nicht initialisiert!");
}
}

View File

@@ -1,5 +1,5 @@
idf_component_register(SRCS idf_component_register(SRCS
src/PersistenceManager.cpp src/persistence_manager.c
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES REQUIRES
nvs_flash nvs_flash

View File

@@ -1,108 +0,0 @@
#pragma once
#include <string>
#include <type_traits>
/**
* @interface IPersistenceManager
* @brief Abstract interface for platform-independent persistence management
* @details This interface defines the contract for key-value storage and retrieval
* systems across different platforms (Desktop/SDL3 and ESP32).
*/
class IPersistenceManager
{
public:
virtual ~IPersistenceManager() = default;
/**
* @brief Template methods for type-safe setting and retrieving of values
* @tparam T The type of value to set (must be one of: bool, int, float, double, std::string)
* @param key The key to associate with the value
* @param value The value to store
*/
template<typename T>
void SetValue(const std::string& key, const T& value) {
static_assert(std::is_same_v<T, bool> ||
std::is_same_v<T, int> ||
std::is_same_v<T, float> ||
std::is_same_v<T, double> ||
std::is_same_v<T, std::string>,
"Unsupported type for IPersistenceManager");
SetValueImpl(key, value);
}
/**
* @brief Template method for type-safe retrieval of values
* @tparam T The type of value to retrieve
* @param key The key to look up
* @param defaultValue The default value to return if the key is not found
* @return The stored value or default value if the key doesn't exist
*/
template<typename T>
[[nodiscard]] T GetValue(const std::string& key, const T& defaultValue = T{}) const {
return GetValueImpl<T>(key, defaultValue);
}
/**
* @brief Utility methods for key management
*/
[[nodiscard]] virtual bool HasKey(const std::string& key) const = 0; ///< Check if a key exists
virtual void RemoveKey(const std::string& key) = 0; ///< Remove a key-value pair
virtual void Clear() = 0; ///< Clear all stored data
[[nodiscard]] virtual size_t GetKeyCount() const = 0; ///< Get the number of stored keys
/**
* @brief Persistence operations
*/
virtual bool Save() = 0; ///< Save data to persistent storage
virtual bool Load() = 0; ///< Load data from persistent storage
protected:
/**
* @brief Template-specific implementations that must be overridden by derived classes
* @details These methods handle the actual storage and retrieval of different data types
*/
virtual void SetValueImpl(const std::string& key, bool value) = 0;
virtual void SetValueImpl(const std::string& key, int value) = 0;
virtual void SetValueImpl(const std::string& key, float value) = 0;
virtual void SetValueImpl(const std::string& key, double value) = 0;
virtual void SetValueImpl(const std::string& key, const std::string& value) = 0;
[[nodiscard]] virtual bool GetValueImpl(const std::string& key, bool defaultValue) const = 0;
[[nodiscard]] virtual int GetValueImpl(const std::string& key, int defaultValue) const = 0;
[[nodiscard]] virtual float GetValueImpl(const std::string& key, float defaultValue) const = 0;
[[nodiscard]] virtual double GetValueImpl(const std::string& key, double defaultValue) const = 0;
[[nodiscard]] virtual std::string GetValueImpl(const std::string& key, const std::string& defaultValue) const = 0;
private:
/**
* @brief Template dispatch methods for type-safe value retrieval
* @tparam T The type to retrieve
* @param key The key to look up
* @param defaultValue The default value to return
* @return The retrieved value or default if not found
*/
template<typename T>
[[nodiscard]] T GetValueImpl(const std::string& key, const T& defaultValue) const
{
if constexpr (std::is_same_v<T, bool>) {
return GetValueImpl(key, static_cast<bool>(defaultValue));
} else if constexpr (std::is_same_v<T, int>) {
return GetValueImpl(key, static_cast<int>(defaultValue));
} else if constexpr (std::is_same_v<T, float>) {
return GetValueImpl(key, static_cast<float>(defaultValue));
} else if constexpr (std::is_same_v<T, double>) {
return GetValueImpl(key, static_cast<double>(defaultValue));
} else if constexpr (std::is_same_v<T, std::string>) {
return GetValueImpl(key, static_cast<const std::string&>(defaultValue));
} else {
static_assert(std::is_same_v<T, bool> ||
std::is_same_v<T, int> ||
std::is_same_v<T, float> ||
std::is_same_v<T, double> ||
std::is_same_v<T, std::string>,
"Unsupported type for IPersistenceManager");
return defaultValue; // This line will never be reached, but satisfies the compiler
}
}
};

View File

@@ -1,58 +0,0 @@
#pragma once
#include "IPersistenceManager.h"
#include <string>
#include <unordered_map>
#include <nvs.h>
#include <nvs_flash.h>
/**
* @class PersistenceManager
* @brief ESP32-specific implementation using NVS (Non-Volatile Storage)
* @details This implementation uses ESP32's NVS API for persistent storage
* in flash memory, providing a platform-optimized solution for
* embedded systems.
*/
class PersistenceManager final : public IPersistenceManager
{
private:
nvs_handle_t nvs_handle_;
std::string namespace_;
bool initialized_;
public:
explicit PersistenceManager(const std::string &nvs_namespace = "config");
~PersistenceManager() override;
bool HasKey(const std::string &key) const override;
void RemoveKey(const std::string &key) override;
void Clear() override;
size_t GetKeyCount() const override;
bool Save() override;
bool Load() override;
bool Initialize();
void Deinitialize();
bool IsInitialized() const
{
return initialized_;
}
protected:
void SetValueImpl(const std::string &key, bool value) override;
void SetValueImpl(const std::string &key, int value) override;
void SetValueImpl(const std::string &key, float value) override;
void SetValueImpl(const std::string &key, double value) override;
void SetValueImpl(const std::string &key, const std::string &value) override;
bool GetValueImpl(const std::string &key, bool defaultValue) const override;
int GetValueImpl(const std::string &key, int defaultValue) const override;
float GetValueImpl(const std::string &key, float defaultValue) const override;
double GetValueImpl(const std::string &key, double defaultValue) const override;
std::string GetValueImpl(const std::string &key, const std::string &defaultValue) const override;
private:
bool EnsureInitialized() const;
};

View File

@@ -0,0 +1,237 @@
#pragma once
#include <esp_err.h>
#include <nvs.h>
#include <nvs_flash.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
#endif
/**
* @brief Structure to manage persistent storage using NVS.
*
* This struct holds the NVS handle, namespace, and initialization state
* for managing persistent key-value storage on the device.
*/
typedef struct
{
/** Handle to the NVS storage. */
nvs_handle_t nvs_handle;
/** Namespace used for NVS operations (max 15 chars + null terminator). */
char nvs_namespace[16];
/** Indicates if the persistence manager is initialized. */
bool initialized;
} persistence_manager_t;
/**
* @brief Erases the entire NVS flash (factory reset).
*
* Warning: This will remove all stored data and namespaces!
*
* @return esp_err_t ESP_OK on success, otherwise error code.
*/
esp_err_t persistence_manager_factory_reset(void);
/**
* @brief Initialize the persistence manager with a given NVS namespace.
*
* @param pm Pointer to the persistence manager structure.
* @param nvs_namespace Namespace to use for NVS operations.
*/
esp_err_t persistence_manager_init(persistence_manager_t *pm, const char *nvs_namespace);
/**
* @brief Deinitialize the persistence manager and release resources.
*
* @param pm Pointer to the persistence manager structure.
*/
esp_err_t persistence_manager_deinit(persistence_manager_t *pm);
/**
* @brief Check if the persistence manager is initialized.
*
* @param pm Pointer to the persistence manager structure.
* @return true if initialized, false otherwise.
*/
bool persistence_manager_is_initialized(const persistence_manager_t *pm);
/**
* @brief Check if a key exists in the NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to check for existence.
* @return true if the key exists, false otherwise.
*/
bool persistence_manager_has_key(const persistence_manager_t *pm, const char *key);
/**
* @brief Remove a key from the NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to remove.
*/
void persistence_manager_remove_key(persistence_manager_t *pm, const char *key);
/**
* @brief Clear all keys from the NVS storage in the current namespace.
*
* @param pm Pointer to the persistence manager structure.
*/
void persistence_manager_clear(persistence_manager_t *pm);
/**
* @brief Get the number of keys stored in the current namespace.
*
* @param pm Pointer to the persistence manager structure.
* @return Number of keys.
*/
size_t persistence_manager_get_key_count(const persistence_manager_t *pm);
/**
* @brief Save all pending changes to NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @return true if successful, false otherwise.
*/
bool persistence_manager_save(persistence_manager_t *pm);
/**
* @brief Load all data from NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @return true if successful, false otherwise.
*/
bool persistence_manager_load(persistence_manager_t *pm);
/**
* @brief Set a boolean value for a key in NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to set.
* @param value Boolean value to store.
*/
void persistence_manager_set_bool(persistence_manager_t *pm, const char *key, bool value);
/**
* @brief Set an integer value for a key in NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to set.
* @param value Integer value to store.
*/
void persistence_manager_set_int(persistence_manager_t *pm, const char *key, int32_t value);
/**
* @brief Set a float value for a key in NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to set.
* @param value Float value to store.
*/
void persistence_manager_set_float(persistence_manager_t *pm, const char *key, float value);
/**
* @brief Set a double value for a key in NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to set.
* @param value Double value to store.
*/
void persistence_manager_set_double(persistence_manager_t *pm, const char *key, double value);
/**
* @brief Set a string value for a key in NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to set.
* @param value String value to store.
*/
void persistence_manager_set_string(persistence_manager_t *pm, const char *key, const char *value);
/**
* @brief Get a boolean value for a key from NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to retrieve.
* @param default_value Value to return if key does not exist.
* @return Boolean value.
*/
bool persistence_manager_get_bool(const persistence_manager_t *pm, const char *key, bool default_value);
/**
* @brief Get an integer value for a key from NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to retrieve.
* @param default_value Value to return if key does not exist.
* @return Integer value.
*/
int32_t persistence_manager_get_int(const persistence_manager_t *pm, const char *key, int32_t default_value);
/**
* @brief Get a float value for a key from NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to retrieve.
* @param default_value Value to return if key does not exist.
* @return Float value.
*/
float persistence_manager_get_float(const persistence_manager_t *pm, const char *key, float default_value);
/**
* @brief Get a double value for a key from NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to retrieve.
* @param default_value Value to return if key does not exist.
* @return Double value.
*/
double persistence_manager_get_double(const persistence_manager_t *pm, const char *key, double default_value);
/**
* @brief Get a string value for a key from NVS storage.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to retrieve.
* @param out_value Buffer to store the retrieved string.
* @param max_len Maximum length of the output buffer.
* @param default_value Value to use if key does not exist.
*/
void persistence_manager_get_string(const persistence_manager_t *pm, const char *key, char *out_value,
size_t max_len, const char *default_value);
/**
* @brief Set a blob (binary data) value for a key in NVS storage.
*
* This function stores arbitrary binary data under the given key.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to set.
* @param value Pointer to the data to store.
* @param length Length of the data in bytes.
*/
void persistence_manager_set_blob(persistence_manager_t *pm, const char *key, const void *value, size_t length);
/**
* @brief Get a blob (binary data) value for a key from NVS storage.
*
* This function retrieves binary data previously stored under the given key.
*
* @param pm Pointer to the persistence manager structure.
* @param key Key to retrieve.
* @param out_value Buffer to store the retrieved data.
* @param max_length Maximum length of the output buffer in bytes.
* @param out_length Pointer to variable to receive the actual data length.
* @return true if the blob was found and read successfully, false otherwise.
*/
bool persistence_manager_get_blob(const persistence_manager_t *pm, const char *key, void *out_value,
size_t max_length, size_t *out_length);
#ifdef __cplusplus
}
#endif

View File

@@ -1,279 +0,0 @@
#include "hal_esp32/PersistenceManager.h"
#include <cstring>
#include <esp_log.h>
static const char *TAG = "PersistenceManager";
PersistenceManager::PersistenceManager(const std::string &nvs_namespace)
: namespace_(nvs_namespace), initialized_(false)
{
Initialize();
Load();
}
PersistenceManager::~PersistenceManager()
{
Deinitialize();
}
bool PersistenceManager::Initialize()
{
if (initialized_)
{
return true;
}
// Open NVS handle
esp_err_t err = nvs_open(namespace_.c_str(), NVS_READWRITE, &nvs_handle_);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(err));
return false;
}
initialized_ = true;
ESP_LOGI(TAG, "PersistenceManager initialized with namespace: %s", namespace_.c_str());
return true;
}
void PersistenceManager::Deinitialize()
{
if (initialized_)
{
nvs_close(nvs_handle_);
initialized_ = false;
}
}
bool PersistenceManager::EnsureInitialized() const
{
if (!initialized_)
{
ESP_LOGE(TAG, "PersistenceManager not initialized");
return false;
}
return true;
}
bool PersistenceManager::HasKey(const std::string &key) const
{
if (!EnsureInitialized())
return false;
size_t required_size = 0;
esp_err_t err = nvs_get_blob(nvs_handle_, key.c_str(), nullptr, &required_size);
return err == ESP_OK;
}
void PersistenceManager::RemoveKey(const std::string &key)
{
if (!EnsureInitialized())
return;
esp_err_t err = nvs_erase_key(nvs_handle_, key.c_str());
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND)
{
ESP_LOGE(TAG, "Failed to remove key '%s': %s", key.c_str(), esp_err_to_name(err));
}
}
void PersistenceManager::Clear()
{
if (!EnsureInitialized())
return;
esp_err_t err = nvs_erase_all(nvs_handle_);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to clear all keys: %s", esp_err_to_name(err));
}
}
size_t PersistenceManager::GetKeyCount() const
{
if (!EnsureInitialized())
return 0;
nvs_iterator_t it = nullptr;
esp_err_t err = nvs_entry_find(NVS_DEFAULT_PART_NAME, namespace_.c_str(), NVS_TYPE_ANY, &it);
if (err != ESP_OK || it == nullptr)
{
return 0;
}
size_t count = 0;
while (it != nullptr)
{
count++;
err = nvs_entry_next(&it);
if (err != ESP_OK)
{
break;
}
}
nvs_release_iterator(it);
return count;
}
bool PersistenceManager::Save()
{
if (!EnsureInitialized())
return false;
esp_err_t err = nvs_commit(nvs_handle_);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
return false;
}
return true;
}
bool PersistenceManager::Load()
{
return EnsureInitialized();
}
void PersistenceManager::SetValueImpl(const std::string &key, bool value)
{
if (!EnsureInitialized())
return;
uint8_t val = value ? 1 : 0;
esp_err_t err = nvs_set_u8(nvs_handle_, key.c_str(), val);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set bool key '%s': %s", key.c_str(), esp_err_to_name(err));
}
}
void PersistenceManager::SetValueImpl(const std::string &key, int value)
{
if (!EnsureInitialized())
return;
esp_err_t err = nvs_set_i32(nvs_handle_, key.c_str(), value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set int key '%s': %s", key.c_str(), esp_err_to_name(err));
}
}
void PersistenceManager::SetValueImpl(const std::string &key, float value)
{
if (!EnsureInitialized())
return;
esp_err_t err = nvs_set_blob(nvs_handle_, key.c_str(), &value, sizeof(float));
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set float key '%s': %s", key.c_str(), esp_err_to_name(err));
}
}
void PersistenceManager::SetValueImpl(const std::string &key, double value)
{
if (!EnsureInitialized())
return;
esp_err_t err = nvs_set_blob(nvs_handle_, key.c_str(), &value, sizeof(double));
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set double key '%s': %s", key.c_str(), esp_err_to_name(err));
}
}
void PersistenceManager::SetValueImpl(const std::string &key, const std::string &value)
{
if (!EnsureInitialized())
return;
esp_err_t err = nvs_set_str(nvs_handle_, key.c_str(), value.c_str());
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set string key '%s': %s", key.c_str(), esp_err_to_name(err));
}
}
bool PersistenceManager::GetValueImpl(const std::string &key, bool defaultValue) const
{
if (!EnsureInitialized())
return defaultValue;
uint8_t value;
esp_err_t err = nvs_get_u8(nvs_handle_, key.c_str(), &value);
if (err != ESP_OK)
{
return defaultValue;
}
return value != 0;
}
int PersistenceManager::GetValueImpl(const std::string &key, int defaultValue) const
{
if (!EnsureInitialized())
return defaultValue;
int32_t value;
esp_err_t err = nvs_get_i32(nvs_handle_, key.c_str(), &value);
if (err != ESP_OK)
{
return defaultValue;
}
return static_cast<int>(value);
}
float PersistenceManager::GetValueImpl(const std::string &key, float defaultValue) const
{
if (!EnsureInitialized())
return defaultValue;
float value;
size_t required_size = sizeof(float);
esp_err_t err = nvs_get_blob(nvs_handle_, key.c_str(), &value, &required_size);
if (err != ESP_OK || required_size != sizeof(float))
{
return defaultValue;
}
return value;
}
double PersistenceManager::GetValueImpl(const std::string &key, double defaultValue) const
{
if (!EnsureInitialized())
return defaultValue;
double value;
size_t required_size = sizeof(double);
esp_err_t err = nvs_get_blob(nvs_handle_, key.c_str(), &value, &required_size);
if (err != ESP_OK || required_size != sizeof(double))
{
return defaultValue;
}
return value;
}
std::string PersistenceManager::GetValueImpl(const std::string &key, const std::string &defaultValue) const
{
if (!EnsureInitialized())
return defaultValue;
size_t required_size = 0;
esp_err_t err = nvs_get_str(nvs_handle_, key.c_str(), nullptr, &required_size);
if (err != ESP_OK)
{
return defaultValue;
}
std::string value(required_size - 1, '\0'); // -1 for null terminator
err = nvs_get_str(nvs_handle_, key.c_str(), value.data(), &required_size);
if (err != ESP_OK)
{
return defaultValue;
}
return value;
}

View File

@@ -0,0 +1,274 @@
#include "persistence_manager.h"
#include <esp_log.h>
#include <string.h>
#define TAG "persistence_manager"
esp_err_t persistence_manager_factory_reset(void)
{
// Erase the entire NVS flash (factory reset)
esp_err_t err = nvs_flash_erase();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Factory reset failed: %s", esp_err_to_name(err));
}
return err;
}
esp_err_t persistence_manager_init(persistence_manager_t *pm, const char *nvs_namespace)
{
if (!pm)
return ESP_ERR_INVALID_ARG;
strncpy(pm->nvs_namespace, nvs_namespace ? nvs_namespace : "config", sizeof(pm->nvs_namespace) - 1);
pm->nvs_namespace[sizeof(pm->nvs_namespace) - 1] = '\0';
pm->initialized = false;
esp_err_t err = nvs_open(pm->nvs_namespace, NVS_READWRITE, &pm->nvs_handle);
if (err == ESP_OK)
{
pm->initialized = true;
ESP_LOGD(TAG, "Initialized with namespace: %s", pm->nvs_namespace);
return ESP_OK;
}
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(err));
return err;
}
esp_err_t persistence_manager_deinit(persistence_manager_t *pm)
{
if (pm && pm->initialized)
{
nvs_close(pm->nvs_handle);
pm->initialized = false;
}
return ESP_OK;
}
bool persistence_manager_is_initialized(const persistence_manager_t *pm)
{
return pm && pm->initialized;
}
bool persistence_manager_has_key(const persistence_manager_t *pm, const char *key)
{
if (!persistence_manager_is_initialized(pm))
return false;
size_t required_size = 0;
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, NULL, &required_size);
return err == ESP_OK;
}
void persistence_manager_remove_key(persistence_manager_t *pm, const char *key)
{
if (!persistence_manager_is_initialized(pm))
return;
esp_err_t err = nvs_erase_key(pm->nvs_handle, key);
if (err != ESP_OK && err != ESP_ERR_NVS_NOT_FOUND)
{
ESP_LOGE(TAG, "Failed to remove key '%s': %s", key, esp_err_to_name(err));
}
}
void persistence_manager_clear(persistence_manager_t *pm)
{
if (!persistence_manager_is_initialized(pm))
return;
esp_err_t err = nvs_erase_all(pm->nvs_handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to clear all keys: %s", esp_err_to_name(err));
}
}
size_t persistence_manager_get_key_count(const persistence_manager_t *pm)
{
if (!persistence_manager_is_initialized(pm))
return 0;
nvs_iterator_t it = NULL;
esp_err_t err = nvs_entry_find(NVS_DEFAULT_PART_NAME, pm->nvs_namespace, NVS_TYPE_ANY, &it);
if (err != ESP_OK || it == NULL)
return 0;
size_t count = 0;
while (it != NULL)
{
count++;
err = nvs_entry_next(&it);
if (err != ESP_OK)
break;
}
nvs_release_iterator(it);
return count;
}
bool persistence_manager_save(persistence_manager_t *pm)
{
if (!persistence_manager_is_initialized(pm))
return false;
esp_err_t err = nvs_commit(pm->nvs_handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
return false;
}
return true;
}
bool persistence_manager_load(persistence_manager_t *pm)
{
return persistence_manager_is_initialized(pm);
}
void persistence_manager_set_bool(persistence_manager_t *pm, const char *key, bool value)
{
if (!persistence_manager_is_initialized(pm))
return;
uint8_t val = value ? 1 : 0;
esp_err_t err = nvs_set_u8(pm->nvs_handle, key, val);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set bool key '%s': %s", key, esp_err_to_name(err));
}
}
void persistence_manager_set_int(persistence_manager_t *pm, const char *key, int32_t value)
{
if (!persistence_manager_is_initialized(pm))
return;
esp_err_t err = nvs_set_i32(pm->nvs_handle, key, value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set int key '%s': %s", key, esp_err_to_name(err));
}
}
void persistence_manager_set_float(persistence_manager_t *pm, const char *key, float value)
{
if (!persistence_manager_is_initialized(pm))
return;
esp_err_t err = nvs_set_blob(pm->nvs_handle, key, &value, sizeof(float));
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set float key '%s': %s", key, esp_err_to_name(err));
}
}
void persistence_manager_set_double(persistence_manager_t *pm, const char *key, double value)
{
if (!persistence_manager_is_initialized(pm))
return;
esp_err_t err = nvs_set_blob(pm->nvs_handle, key, &value, sizeof(double));
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set double key '%s': %s", key, esp_err_to_name(err));
}
}
void persistence_manager_set_string(persistence_manager_t *pm, const char *key, const char *value)
{
if (!persistence_manager_is_initialized(pm))
return;
esp_err_t err = nvs_set_str(pm->nvs_handle, key, value);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set string key '%s': %s", key, esp_err_to_name(err));
}
}
void persistence_manager_set_blob(persistence_manager_t *pm, const char *key, const void *value, size_t length)
{
if (!persistence_manager_is_initialized(pm) || !value || length == 0)
return;
esp_err_t err = nvs_set_blob(pm->nvs_handle, key, value, length);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set blob key '%s': %s", key, esp_err_to_name(err));
}
}
bool persistence_manager_get_bool(const persistence_manager_t *pm, const char *key, bool default_value)
{
if (!persistence_manager_is_initialized(pm))
return default_value;
uint8_t value;
esp_err_t err = nvs_get_u8(pm->nvs_handle, key, &value);
if (err != ESP_OK)
return default_value;
return value != 0;
}
int32_t persistence_manager_get_int(const persistence_manager_t *pm, const char *key, int32_t default_value)
{
if (!persistence_manager_is_initialized(pm))
return default_value;
int32_t value;
esp_err_t err = nvs_get_i32(pm->nvs_handle, key, &value);
if (err != ESP_OK)
return default_value;
return value;
}
float persistence_manager_get_float(const persistence_manager_t *pm, const char *key, float default_value)
{
if (!persistence_manager_is_initialized(pm))
return default_value;
float value;
size_t required_size = sizeof(float);
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, &value, &required_size);
if (err != ESP_OK || required_size != sizeof(float))
return default_value;
return value;
}
double persistence_manager_get_double(const persistence_manager_t *pm, const char *key, double default_value)
{
if (!persistence_manager_is_initialized(pm))
return default_value;
double value;
size_t required_size = sizeof(double);
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, &value, &required_size);
if (err != ESP_OK || required_size != sizeof(double))
return default_value;
return value;
}
void persistence_manager_get_string(const persistence_manager_t *pm, const char *key, char *out_value, size_t max_len,
const char *default_value)
{
if (!persistence_manager_is_initialized(pm))
{
strncpy(out_value, default_value, max_len - 1);
out_value[max_len - 1] = '\0';
return;
}
size_t required_size = 0;
esp_err_t err = nvs_get_str(pm->nvs_handle, key, NULL, &required_size);
if (err != ESP_OK || required_size == 0 || required_size > max_len)
{
strncpy(out_value, default_value, max_len - 1);
out_value[max_len - 1] = '\0';
return;
}
err = nvs_get_str(pm->nvs_handle, key, out_value, &required_size);
if (err != ESP_OK)
{
strncpy(out_value, default_value, max_len - 1);
out_value[max_len - 1] = '\0';
return;
}
}
bool persistence_manager_get_blob(const persistence_manager_t *pm, const char *key, void *out_value, size_t max_length,
size_t *out_length)
{
if (!persistence_manager_is_initialized(pm) || !out_value || max_length == 0)
return false;
size_t required_size = 0;
esp_err_t err = nvs_get_blob(pm->nvs_handle, key, NULL, &required_size);
if (err != ESP_OK || required_size == 0 || required_size > max_length)
return false;
err = nvs_get_blob(pm->nvs_handle, key, out_value, &required_size);
if (err != ESP_OK)
return false;
if (out_length)
*out_length = required_size;
return true;
}

View File

@@ -5,5 +5,6 @@ idf_component_register(SRCS
PRIV_REQUIRES PRIV_REQUIRES
led-manager led-manager
persistence-manager persistence-manager
message-manager
spiffs spiffs
) )

View File

@@ -9,12 +9,20 @@ typedef struct
int cycle_duration_minutes; int cycle_duration_minutes;
} simulation_config_t; } simulation_config_t;
char *get_time(void); #ifdef __cplusplus
esp_err_t add_light_item(const char time[5], uint8_t red, uint8_t green, uint8_t blue, uint8_t white, extern "C"
uint8_t brightness, uint8_t saturation); {
void cleanup_light_items(void); #endif
void start_simulate_day(void);
void start_simulate_night(void); char *get_time(void);
void start_simulation_task(void); esp_err_t add_light_item(const char time[5], uint8_t red, uint8_t green, uint8_t blue, uint8_t white,
void stop_simulation_task(void); uint8_t brightness, uint8_t saturation);
void start_simulation(void); void cleanup_light_items(void);
void start_simulate_day(void);
void start_simulate_night(void);
void start_simulation_task(void);
void stop_simulation_task(void);
void start_simulation(void);
#ifdef __cplusplus
}
#endif

View File

@@ -1,11 +1,23 @@
#pragma once #pragma once
#include "esp_err.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" extern "C"
{ {
#endif #endif
void initialize_storage(); void initialize_storage();
void load_file(const char *filename); void load_file(const char *filename);
char **read_lines_filtered(const char *filename, int *out_count);
void free_lines(char **lines, int count);
/**
* Write an array of lines to a file (CSV or other text).
* @param filename File name (without /spiffs/)
* @param lines Array of lines (null-terminated strings)
* @param count Number of lines
* @return ESP_OK on success, error code otherwise
*/
esp_err_t write_lines(const char *filename, char **lines, int count);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@@ -1,8 +1,9 @@
#include "simulator.h" #include "simulator.h"
#include "color.h" #include "color.h"
#include "hal_esp32/PersistenceManager.h"
#include "led_strip_ws2812.h" #include "led_strip_ws2812.h"
#include "message_manager.h"
#include "persistence_manager.h"
#include "storage.h" #include "storage.h"
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <esp_log.h> #include <esp_log.h>
@@ -15,12 +16,12 @@
#include <string.h> #include <string.h>
static const char *TAG = "simulator"; static const char *TAG = "simulator";
static char *time; static char *time = NULL;
static char *time_to_string(int hhmm) static char *time_to_string(int hhmm)
{ {
static char buffer[20]; static char buffer[20];
snprintf(buffer, sizeof(buffer), "%02d:%02d Uhr", hhmm / 100, hhmm % 100); snprintf(buffer, sizeof(buffer), "%02d:%02d", hhmm / 100, hhmm % 100);
return buffer; return buffer;
} }
@@ -81,10 +82,10 @@ esp_err_t add_light_item(const char time[5], uint8_t red, uint8_t green, uint8_t
uint8_t brightness, uint8_t saturation) uint8_t brightness, uint8_t saturation)
{ {
// Allocate memory for a new node in PSRAM. // Allocate memory for a new node in PSRAM.
light_item_node_t *new_node = (light_item_node_t *)heap_caps_malloc(sizeof(light_item_node_t), MALLOC_CAP_SPIRAM); light_item_node_t *new_node = (light_item_node_t *)heap_caps_malloc(sizeof(light_item_node_t), MALLOC_CAP_DEFAULT);
if (new_node == NULL) if (new_node == NULL)
{ {
ESP_LOGE(TAG, "Failed to allocate memory in PSRAM for new light_item_node_t."); ESP_LOGE(TAG, "Failed to allocate memory for new light_item_node_t.");
return ESP_FAIL; return ESP_FAIL;
} }
@@ -145,10 +146,12 @@ static void initialize_light_items(void)
initialize_storage(); initialize_storage();
static char filename[30]; static char filename[30];
auto persistence = PersistenceManager(); persistence_manager_t persistence;
int variant = persistence.GetValue("light_variant", 1); persistence_manager_init(&persistence, "config");
snprintf(filename, sizeof(filename), "/spiffs/schema_%02d.csv", variant); int variant = persistence_manager_get_int(&persistence, "light_variant", 1);
snprintf(filename, sizeof(filename), "schema_%02d.csv", variant);
load_file(filename); load_file(filename);
persistence_manager_deinit(&persistence);
if (head == NULL) if (head == NULL)
{ {
@@ -233,6 +236,17 @@ static light_item_node_t *find_next_light_item_for_time(int hhmm)
return next_item; return next_item;
} }
static void send_simulation_message(const char *time, rgb_t color)
{
message_t msg = {};
msg.type = MESSAGE_TYPE_SIMULATION;
strncpy(msg.data.simulation.time, time, sizeof(msg.data.simulation.time) - 1);
msg.data.simulation.red = color.red;
msg.data.simulation.green = color.green;
msg.data.simulation.blue = color.blue;
message_manager_post(&msg, pdMS_TO_TICKS(100));
}
void start_simulate_day(void) void start_simulate_day(void)
{ {
initialize_light_items(); initialize_light_items();
@@ -240,8 +254,9 @@ void start_simulate_day(void)
light_item_node_t *current_item = find_best_light_item_for_time(1200); light_item_node_t *current_item = find_best_light_item_for_time(1200);
if (current_item != NULL) if (current_item != NULL)
{ {
led_strip_update(LED_STATE_DAY, rgb_t color = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue}); led_strip_update(LED_STATE_DAY, color);
send_simulation_message("12:00", color);
} }
} }
@@ -252,8 +267,9 @@ void start_simulate_night(void)
light_item_node_t *current_item = find_best_light_item_for_time(0); light_item_node_t *current_item = find_best_light_item_for_time(0);
if (current_item != NULL) if (current_item != NULL)
{ {
led_strip_update(LED_STATE_NIGHT, rgb_t color = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue}); led_strip_update(LED_STATE_NIGHT, color);
send_simulation_message("00:00", color);
} }
} }
@@ -294,45 +310,51 @@ void simulate_cycle(void *args)
light_item_node_t *current_item = find_best_light_item_for_time(hhmm); light_item_node_t *current_item = find_best_light_item_for_time(hhmm);
light_item_node_t *next_item = find_next_light_item_for_time(hhmm); light_item_node_t *next_item = find_next_light_item_for_time(hhmm);
if (current_item != NULL && next_item != NULL) if (current_item != NULL)
{ {
int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100); rgb_t color = {0, 0, 0};
int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
if (next_item_time_min < current_item_time_min) // Use head as fallback if next_item is NULL
next_item = next_item ? next_item : head;
if (next_item != NULL)
{ {
next_item_time_min += total_minutes_in_day; int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100);
} int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
int minutes_since_current_item_start = current_minute_of_day - current_item_time_min; if (next_item_time_min < current_item_time_min)
if (minutes_since_current_item_start < 0) {
next_item_time_min += total_minutes_in_day;
}
int minutes_since_current_item_start = current_minute_of_day - current_item_time_min;
if (minutes_since_current_item_start < 0)
{
minutes_since_current_item_start += total_minutes_in_day;
}
int interval_duration = next_item_time_min - current_item_time_min;
if (interval_duration == 0)
{
interval_duration = 1;
}
float interpolation_factor = (float)minutes_since_current_item_start / (float)interval_duration;
// Prepare colors for interpolation
rgb_t start_rgb = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
// Use the interpolation function
color = interpolate_color(start_rgb, end_rgb, interpolation_factor);
led_strip_update(LED_STATE_SIMULATION, color);
}
else
{ {
minutes_since_current_item_start += total_minutes_in_day; // No next_item and no head, use only current
color = (rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
led_strip_update(LED_STATE_SIMULATION, color);
} }
send_simulation_message(time, color);
int interval_duration = next_item_time_min - current_item_time_min;
if (interval_duration == 0)
{
interval_duration = 1;
}
float interpolation_factor = (float)minutes_since_current_item_start / (float)interval_duration;
// Prepare colors for interpolation
rgb_t start_rgb = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
// Use the interpolation function
rgb_t final_rgb = interpolate_color(start_rgb, end_rgb, interpolation_factor);
led_strip_update(LED_STATE_SIMULATION, final_rgb);
}
else if (current_item != NULL)
{
// No next item, just use current
led_strip_update(
LED_STATE_SIMULATION,
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
} }
vTaskDelay(pdMS_TO_TICKS(delay_ms)); vTaskDelay(pdMS_TO_TICKS(delay_ms));
@@ -351,7 +373,7 @@ void start_simulation_task(void)
stop_simulation_task(); stop_simulation_task();
simulation_config_t *config = simulation_config_t *config =
(simulation_config_t *)heap_caps_malloc(sizeof(simulation_config_t), MALLOC_CAP_SPIRAM); (simulation_config_t *)heap_caps_malloc(sizeof(simulation_config_t), MALLOC_CAP_DEFAULT);
if (config == NULL) if (config == NULL)
{ {
ESP_LOGE(TAG, "Failed to allocate memory for simulation config."); ESP_LOGE(TAG, "Failed to allocate memory for simulation config.");
@@ -398,25 +420,22 @@ void start_simulation(void)
{ {
stop_simulation_task(); stop_simulation_task();
auto persistence = PersistenceManager(); persistence_manager_t persistence;
if (persistence.GetValue("light_active", false)) persistence_manager_init(&persistence, "config");
if (persistence_manager_get_bool(&persistence, "light_active", false))
{ {
int mode = persistence_manager_get_int(&persistence, "light_mode", 0);
int mode = persistence.GetValue("light_mode", 0);
switch (mode) switch (mode)
{ {
case 0: // Simulation mode case 0: // Simulation mode
start_simulation_task(); start_simulation_task();
break; break;
case 1: // Day mode case 1: // Day mode
start_simulate_day(); start_simulate_day();
break; break;
case 2: // Night mode case 2: // Night mode
start_simulate_night(); start_simulate_night();
break; break;
default: default:
ESP_LOGW(TAG, "Unknown light mode: %d", mode); ESP_LOGW(TAG, "Unknown light mode: %d", mode);
break; break;
@@ -426,4 +445,5 @@ void start_simulation(void)
{ {
led_strip_update(LED_STATE_OFF, rgb_t{}); led_strip_update(LED_STATE_OFF, rgb_t{});
} }
persistence_manager_deinit(&persistence);
} }

View File

@@ -5,6 +5,8 @@
#include "simulator.h" #include "simulator.h"
#include <errno.h> #include <errno.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *TAG = "storage"; static const char *TAG = "storage";
@@ -49,59 +51,106 @@ void initialize_storage()
void load_file(const char *filename) void load_file(const char *filename)
{ {
ESP_LOGI(TAG, "Loading file: %s", filename); ESP_LOGI(TAG, "Loading file: %s", filename);
FILE *f = fopen(filename, "r"); int line_count = 0;
if (f == NULL) char **lines = read_lines_filtered(filename, &line_count);
{
ESP_LOGE(TAG, "Failed to open file for reading");
return;
}
char line[128];
uint8_t line_number = 0; uint8_t line_number = 0;
while (fgets(line, sizeof(line), f)) for (int i = 0; i < line_count; ++i)
{ {
char *pos = strchr(line, '\n');
if (pos)
{
*pos = '\0';
}
if (strlen(line) == 0)
{
continue;
}
char *trimmed = line;
while (*trimmed == ' ' || *trimmed == '\t')
{
trimmed++;
}
if (*trimmed == '#' || *trimmed == '\0')
{
continue;
}
char time[10] = {0}; char time[10] = {0};
int red, green, blue, white, brightness, saturation; int red, green, blue, white, brightness, saturation;
int items_scanned =
int items_scanned = sscanf(line, "%d,%d,%d,%d,%d,%d", &red, &green, &blue, &white, &brightness, &saturation); sscanf(lines[i], "%d,%d,%d,%d,%d,%d", &red, &green, &blue, &white, &brightness, &saturation);
if (items_scanned == 6) if (items_scanned == 6)
{ {
int total_minutes = line_number * 30; int total_minutes = line_number * 30;
int hours = total_minutes / 60; int hours = total_minutes / 60;
int minutes = total_minutes % 60; int minutes = total_minutes % 60;
snprintf(time, sizeof(time), "%02d%02d", hours, minutes); snprintf(time, sizeof(time), "%02d%02d", hours, minutes);
add_light_item(time, red, green, blue, white, brightness, saturation); add_light_item(time, red, green, blue, white, brightness, saturation);
line_number++; line_number++;
} }
else else
{ {
ESP_LOGW(TAG, "Could not parse line: %s", line); ESP_LOGW(TAG, "Could not parse line: %s", lines[i]);
} }
} }
free_lines(lines, line_count);
fclose(f);
ESP_LOGI(TAG, "Finished loading file. Loaded %d entries.", line_number); ESP_LOGI(TAG, "Finished loading file. Loaded %d entries.", line_number);
} }
char **read_lines_filtered(const char *filename, int *out_count)
{
char fullpath[128];
snprintf(fullpath, sizeof(fullpath), "/spiffs/%s", filename[0] == '/' ? filename + 1 : filename);
FILE *f = fopen(fullpath, "r");
if (!f)
{
ESP_LOGE(TAG, "Failed to open file: %s", fullpath);
*out_count = 0;
return NULL;
}
size_t capacity = 16;
size_t count = 0;
char **lines = (char **)malloc(capacity * sizeof(char *));
char line[256];
while (fgets(line, sizeof(line), f))
{
// Zeilenumbruch entfernen
char *pos = strchr(line, '\n');
if (pos)
*pos = '\0';
// Trim vorne
char *trimmed = line;
while (*trimmed == ' ' || *trimmed == '\t')
trimmed++;
// Leere oder Kommentarzeile überspringen
if (*trimmed == '\0' || *trimmed == '#')
continue;
// Trim hinten
size_t len = strlen(trimmed);
while (len > 0 && (trimmed[len - 1] == ' ' || trimmed[len - 1] == '\t'))
trimmed[--len] = '\0';
// Kopieren
if (count >= capacity)
{
capacity *= 2;
lines = (char **)realloc(lines, capacity * sizeof(char *));
}
lines[count++] = strdup(trimmed);
}
fclose(f);
*out_count = (int)count;
return lines;
}
void free_lines(char **lines, int count)
{
for (int i = 0; i < count; ++i)
free(lines[i]);
free(lines);
}
esp_err_t write_lines(const char *filename, char **lines, int count)
{
char fullpath[128];
snprintf(fullpath, sizeof(fullpath), "/spiffs/%s", filename[0] == '/' ? filename + 1 : filename);
FILE *f = fopen(fullpath, "w");
if (!f)
{
ESP_LOGE(TAG, "Failed to open file for writing: %s", fullpath);
return ESP_FAIL;
}
for (int i = 0; i < count; ++i)
{
if (fprintf(f, "%s\n", lines[i]) < 0)
{
ESP_LOGE(TAG, "Failed to write line %d", i);
fclose(f);
return ESP_FAIL;
}
}
fclose(f);
ESP_LOGI(TAG, "Wrote %d lines to %s", count, fullpath);
return ESP_OK;
}

View File

@@ -0,0 +1,13 @@
[17:12:14.017] [INFO] Initializing Storybook
[17:12:14.195] [DEBUG] Getting package.json info for /Users/mars3142/Coding/git.mars3142.dev/system-control/firmware/package.json...
[17:12:14.195] [DEBUG] Getting CLI versions from NPM for storybook...
[17:12:14.195] [DEBUG] Executing command: npm info storybook version
[17:12:14.850] [INFO] Adding Storybook version 10.2.8 to your project
[17:12:14.851] [ERROR] Unable to initialize Storybook in this directory.
Storybook couldn't detect a supported framework or configuration for your project. Make sure you're inside a framework project (e.g., React, Vue, Svelte, Angular, Next.js) and that its dependencies are installed.
Tips:
- Run init in an empty directory or create a new framework app first.
- If this directory contains unrelated files, try a new directory for Storybook.
[17:12:14.853] [INFO] Storybook collects completely anonymous usage telemetry. We use it to shape Storybook's roadmap and prioritize features. You can learn more, including how to opt out, at https://storybook.js.org/telemetry

View File

@@ -1,10 +1,10 @@
idf_component_register(SRCS idf_component_register(SRCS
main.cpp src/main.cpp
app_task.cpp src/app_task.cpp
button_handling.c src/button_handling.c
i2c_checker.c src/i2c_checker.c
hal/u8g2_esp32_hal.c src/hal/u8g2_esp32_hal.c
INCLUDE_DIRS "." INCLUDE_DIRS "include"
PRIV_REQUIRES PRIV_REQUIRES
analytics analytics
insa insa
@@ -21,6 +21,7 @@ idf_component_register(SRCS
app_update app_update
rmaker_common rmaker_common
driver driver
my_mqtt_client
) )
spiffs_create_partition_image(storage ../storage FLASH_IN_PROJECT) spiffs_create_partition_image(storage ../storage FLASH_IN_PROJECT)

View File

@@ -6,84 +6,6 @@ menu "System Control"
help help
Enable or disable WiFi connectivity. Enable or disable WiFi connectivity.
config WIFI_NETWORK_COUNT
depends on WIFI_ENABLED
int "Number of WiFi Networks"
default 1
range 1 5
help
Number of WiFi networks to configure (1-5).
config WIFI_SSID_1
depends on WIFI_ENABLED
string "WiFi SSID 1"
default "YourSSID1"
help
The SSID of the first WiFi network.
config WIFI_PASSWORD_1
depends on WIFI_ENABLED
string "WiFi Password 1"
default "YourPassword1"
help
The password of the first WiFi network.
config WIFI_SSID_2
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 2
string "WiFi SSID 2"
default ""
help
The SSID of the second WiFi network.
config WIFI_PASSWORD_2
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 2
string "WiFi Password 2"
default ""
help
The password of the second WiFi network.
config WIFI_SSID_3
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 3
string "WiFi SSID 3"
default ""
help
The SSID of the third WiFi network.
config WIFI_PASSWORD_3
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 3
string "WiFi Password 3"
default ""
help
The password of the third WiFi network.
config WIFI_SSID_4
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 4
string "WiFi SSID 4"
default ""
help
The SSID of the fourth WiFi network.
config WIFI_PASSWORD_4
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 4
string "WiFi Password 4"
default ""
help
The password of the fourth WiFi network.
config WIFI_SSID_5
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 5
string "WiFi SSID 5"
default ""
help
The SSID of the fifth WiFi network.
config WIFI_PASSWORD_5
depends on WIFI_ENABLED && WIFI_NETWORK_COUNT >= 5
string "WiFi Password 5"
default ""
help
The password of the fifth WiFi network.
config WIFI_CONNECT_RETRIES config WIFI_CONNECT_RETRIES
depends on WIFI_ENABLED depends on WIFI_ENABLED
int "WiFi Connection Retry Attempts per Network" int "WiFi Connection Retry Attempts per Network"
@@ -105,4 +27,42 @@ menu "System Control"
help help
GPIO pin number for the SCL line of the display. GPIO pin number for the SCL line of the display.
endmenu endmenu
menu "Button Configuration"
config BUTTON_UP
int "Button UP GPIO Pin"
default 1
help
GPIO pin number for the up button.
config BUTTON_DOWN
int "Button DOWN GPIO Pin"
default 6
help
GPIO pin number for the down button.
config BUTTON_LEFT
int "Button LEFT GPIO Pin"
default 3
help
GPIO pin number for the left button.
config BUTTON_RIGHT
int "Button RIGHT GPIO Pin"
default 5
help
GPIO pin number for the right button.
config BUTTON_SELECT
int "Button SELECT GPIO Pin"
default 18
help
GPIO pin number for the select button.
config BUTTON_BACK
int "Button BACK GPIO Pin"
default 16
help
GPIO pin number for the back button.
endmenu
endmenu endmenu

View File

@@ -1,8 +0,0 @@
#pragma once
#define BUTTON_UP GPIO_NUM_1
#define BUTTON_DOWN GPIO_NUM_6
#define BUTTON_LEFT GPIO_NUM_3
#define BUTTON_RIGHT GPIO_NUM_5
#define BUTTON_SELECT GPIO_NUM_18
#define BUTTON_BACK GPIO_NUM_16

View File

@@ -1,74 +0,0 @@
#include "i2c_checker.h"
#include "driver/i2c.h"
#include "esp_insights.h"
#include "esp_log.h"
#include "hal/u8g2_esp32_hal.h"
static const char *TAG = "i2c_checker";
esp_err_t i2c_device_check(i2c_port_t i2c_port, uint8_t device_address)
{
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
// Send the device address with the write bit (LSB = 0)
i2c_master_write_byte(cmd, (device_address << 1) | I2C_MASTER_WRITE, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(i2c_port, cmd, pdMS_TO_TICKS(100));
i2c_cmd_link_delete(cmd);
return ret;
}
esp_err_t i2c_bus_scan_and_check(void)
{
// 1. Configure and install I2C bus
i2c_config_t conf = {
.mode = I2C_MODE_MASTER,
.sda_io_num = I2C_MASTER_SDA_PIN,
.scl_io_num = I2C_MASTER_SCL_PIN,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.master.clk_speed = I2C_MASTER_FREQ_HZ,
};
esp_err_t err = i2c_param_config(I2C_MASTER_NUM, &conf);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "I2C parameter configuration failed: %s", esp_err_to_name(err));
return err;
}
err = i2c_driver_install(I2C_MASTER_NUM, conf.mode, 0, 0, 0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "I2C driver installation failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "I2C driver initialized. Searching for device...");
// 2. Check if the device is present
err = i2c_device_check(I2C_MASTER_NUM, DISPLAY_I2C_ADDRESS);
if (err == ESP_OK)
{
ESP_LOGI(TAG, "Device found at address 0x%02X!", DISPLAY_I2C_ADDRESS);
// Here you could now call e.g. setup_screen()
}
else if (err == ESP_ERR_TIMEOUT)
{
ESP_LOGE(TAG, "Timeout! Device at address 0x%02X is not responding.", DISPLAY_I2C_ADDRESS);
}
else
{
ESP_LOGE(TAG, "Error communicating with address 0x%02X: %s", DISPLAY_I2C_ADDRESS, esp_err_to_name(err));
}
// 3. Uninstall I2C driver if it is no longer needed
i2c_driver_delete(I2C_MASTER_NUM);
ESP_DIAG_EVENT(TAG, "I2C driver uninstalled.");
return err;
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include "driver/gpio.h"
#define BUTTON_UP ((gpio_num_t)CONFIG_BUTTON_UP)
#define BUTTON_DOWN ((gpio_num_t)CONFIG_BUTTON_DOWN)
#define BUTTON_LEFT ((gpio_num_t)CONFIG_BUTTON_LEFT)
#define BUTTON_RIGHT ((gpio_num_t)CONFIG_BUTTON_RIGHT)
#define BUTTON_SELECT ((gpio_num_t)CONFIG_BUTTON_SELECT)
#define BUTTON_BACK ((gpio_num_t)CONFIG_BUTTON_BACK)

View File

@@ -13,10 +13,11 @@
#include "u8g2.h" #include "u8g2.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/spi_master.h" #include "driver/spi_master.h"
#include "hal/i2c_types.h" #include "hal/i2c_types.h"
#include "driver/i2c_master.h"
#define U8G2_ESP32_HAL_UNDEFINED GPIO_NUM_NC #define U8G2_ESP32_HAL_UNDEFINED GPIO_NUM_NC
#define I2C_MASTER_NUM I2C_NUM_0 // I2C port number for master dev #define I2C_MASTER_NUM I2C_NUM_0 // I2C port number for master dev

View File

@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

View File

@@ -2,16 +2,20 @@
#include "analytics.h" #include "analytics.h"
#include "button_handling.h" #include "button_handling.h"
#include "common.h"
#include "common/InactivityTracker.h" #include "common/InactivityTracker.h"
#include "hal/u8g2_esp32_hal.h" #include "hal/u8g2_esp32_hal.h"
#include "hal_esp32/PersistenceManager.h"
#include "i2c_checker.h" #include "i2c_checker.h"
#include "led_status.h" #include "led_status.h"
#include "message_manager.h"
#include "my_mqtt_client.h"
#include "persistence_manager.h"
#include "simulator.h" #include "simulator.h"
#include "ui/ClockScreenSaver.h" #include "ui/ClockScreenSaver.h"
#include "ui/ScreenSaver.h" #include "ui/ScreenSaver.h"
#include "ui/SplashScreen.h" #include "ui/SplashScreen.h"
#include "wifi_manager.h" #include "wifi_manager.h"
#include <cstring>
#include <driver/i2c.h> #include <driver/i2c.h>
#include <esp_diagnostics.h> #include <esp_diagnostics.h>
#include <esp_insights.h> #include <esp_insights.h>
@@ -33,7 +37,8 @@ uint8_t received_signal;
std::shared_ptr<Widget> m_widget; std::shared_ptr<Widget> m_widget;
std::vector<std::shared_ptr<Widget>> m_history; std::vector<std::shared_ptr<Widget>> m_history;
std::unique_ptr<InactivityTracker> m_inactivityTracker; std::unique_ptr<InactivityTracker> m_inactivityTracker;
std::shared_ptr<PersistenceManager> m_persistenceManager; // Persistence Manager for C-API
persistence_manager_t g_persistence_manager;
extern QueueHandle_t buttonQueue; extern QueueHandle_t buttonQueue;
@@ -93,10 +98,7 @@ void popScreen()
m_history.pop_back(); m_history.pop_back();
if (m_widget) if (m_widget)
{ {
if (m_persistenceManager != nullptr) persistence_manager_save(&g_persistence_manager);
{
m_persistenceManager->Save();
}
m_widget->onExit(); m_widget->onExit();
} }
m_widget = m_history.back(); m_widget = m_history.back();
@@ -107,14 +109,14 @@ void popScreen()
static void init_ui(void) static void init_ui(void)
{ {
m_persistenceManager = std::make_shared<PersistenceManager>(); persistence_manager_init(&g_persistence_manager, "config");
options = { options = {
.u8g2 = &u8g2, .u8g2 = &u8g2,
.setScreen = [](const std::shared_ptr<Widget> &screen) { setScreen(screen); }, .setScreen = [](const std::shared_ptr<Widget> &screen) { setScreen(screen); },
.pushScreen = [](const std::shared_ptr<Widget> &screen) { pushScreen(screen); }, .pushScreen = [](const std::shared_ptr<Widget> &screen) { pushScreen(screen); },
.popScreen = []() { popScreen(); }, .popScreen = []() { popScreen(); },
.onButtonClicked = nullptr, .onButtonClicked = nullptr,
.persistenceManager = m_persistenceManager, .persistenceManager = &g_persistence_manager,
}; };
m_widget = std::make_shared<SplashScreen>(&options); m_widget = std::make_shared<SplashScreen>(&options);
m_inactivityTracker = std::make_unique<InactivityTracker>(60000, []() { m_inactivityTracker = std::make_unique<InactivityTracker>(60000, []() {
@@ -127,6 +129,17 @@ static void init_ui(void)
u8g2_SendBuffer(&u8g2); u8g2_SendBuffer(&u8g2);
} }
static void on_message_received(const message_t *msg)
{
if (msg && msg->type == MESSAGE_TYPE_SETTINGS &&
(std::strcmp(msg->data.settings.key, "light_active") == 0 ||
std::strcmp(msg->data.settings.key, "light_variant") == 0 ||
std::strcmp(msg->data.settings.key, "light_mode") == 0))
{
start_simulation();
}
}
static void handle_button(uint8_t button) static void handle_button(uint8_t button)
{ {
m_inactivityTracker->reset(); m_inactivityTracker->reset();
@@ -184,14 +197,65 @@ void app_task(void *args)
return; return;
} }
// Initialize display so that info can be shown
setup_screen(); setup_screen();
// Check BACK button and delete settings if necessary (with countdown)
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pin_bit_mask = (1ULL << BUTTON_BACK);
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE;
gpio_config(&io_conf);
vTaskDelay(pdMS_TO_TICKS(10));
if (gpio_get_level(BUTTON_BACK) == 0)
{
u8g2_SetFont(&u8g2, u8g2_font_ncenB08_tr);
for (int i = 5; i > 0; --i)
{
u8g2_ClearBuffer(&u8g2);
u8g2_DrawStr(&u8g2, 5, 20, "BACK gedrueckt!");
u8g2_DrawStr(&u8g2, 5, 35, "Halte fuer Reset...");
char buf[32];
snprintf(buf, sizeof(buf), "Loesche in %d s", i);
u8g2_DrawStr(&u8g2, 5, 55, buf);
u8g2_SendBuffer(&u8g2);
vTaskDelay(pdMS_TO_TICKS(1000));
if (gpio_get_level(BUTTON_BACK) != 0)
{
// Button released, abort
break;
}
if (i == 1)
{
// After 5 seconds still pressed: perform factory reset
u8g2_ClearBuffer(&u8g2);
u8g2_DrawStr(&u8g2, 5, 30, "Alle Einstellungen ");
u8g2_DrawStr(&u8g2, 5, 45, "werden geloescht...");
u8g2_SendBuffer(&u8g2);
persistence_manager_factory_reset();
vTaskDelay(pdMS_TO_TICKS(1000));
u8g2_ClearBuffer(&u8g2);
u8g2_DrawStr(&u8g2, 5, 35, "Fertig. Neustart...");
u8g2_SendBuffer(&u8g2);
vTaskDelay(pdMS_TO_TICKS(1000));
esp_restart();
}
}
}
message_manager_init();
setup_buttons(); setup_buttons();
init_ui(); init_ui();
#if CONFIG_WIFI_ENABLED
wifi_manager_init(); wifi_manager_init();
analytics_init();
#endif mqtt_client_start();
message_manager_register_listener(on_message_received);
start_simulation(); start_simulation();

View File

@@ -7,13 +7,17 @@
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "u8g2_esp32_hal.h" #include "hal/u8g2_esp32_hal.h"
static const char *TAG = "u8g2_hal"; static const char *TAG = "u8g2_hal";
static const unsigned int I2C_TIMEOUT_MS = 1000; static const unsigned int I2C_TIMEOUT_MS = 1000;
static spi_device_handle_t handle_spi; // SPI handle. static spi_device_handle_t handle_spi; // SPI handle.
static i2c_cmd_handle_t handle_i2c; // I2C handle. static i2c_master_bus_handle_t i2c_bus; // I2C bus handle (new driver).
static i2c_master_dev_handle_t i2c_dev; // I2C device handle (new driver).
static uint8_t i2c_tx_buf[256]; // Buffer for one I2C transaction.
static size_t i2c_tx_len; // Current length in buffer.
static uint8_t current_i2c_addr7; // Current 7-bit device address.
static u8g2_esp32_hal_t u8g2_esp32_hal; // HAL state data. static u8g2_esp32_hal_t u8g2_esp32_hal; // HAL state data.
static bool i2c_transfer_failed = false; // Flag to track I2C transfer errors static bool i2c_transfer_failed = false; // Flag to track I2C transfer errors
@@ -148,21 +152,23 @@ uint8_t u8g2_esp32_i2c_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void
break; break;
} }
i2c_config_t conf = {0}; // Neue I2C-Master-API: Bus einmalig anlegen
conf.mode = I2C_MODE_MASTER; if (i2c_bus == NULL)
ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda); {
conf.sda_io_num = u8g2_esp32_hal.bus.i2c.sda; i2c_master_bus_config_t bus_cfg = {
conf.sda_pullup_en = GPIO_PULLUP_ENABLE; .i2c_port = I2C_MASTER_NUM,
ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl); .scl_io_num = u8g2_esp32_hal.bus.i2c.scl,
conf.scl_io_num = u8g2_esp32_hal.bus.i2c.scl; .sda_io_num = u8g2_esp32_hal.bus.i2c.sda,
conf.scl_pullup_en = GPIO_PULLUP_ENABLE; .clk_source = I2C_CLK_SRC_DEFAULT,
ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ); .flags = {.enable_internal_pullup = true},
conf.master.clk_speed = I2C_MASTER_FREQ_HZ; };
ESP_LOGI(TAG, "i2c_param_config %d", conf.mode);
ESP_ERROR_CHECK(i2c_param_config(I2C_MASTER_NUM, &conf)); ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda);
ESP_LOGI(TAG, "i2c_driver_install %d", I2C_MASTER_NUM); ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl);
ESP_ERROR_CHECK( ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ);
i2c_driver_install(I2C_MASTER_NUM, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0)); ESP_LOGI(TAG, "i2c_new_master_bus %d", I2C_MASTER_NUM);
ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &i2c_bus));
}
break; break;
} }
@@ -174,37 +180,55 @@ uint8_t u8g2_esp32_i2c_byte_cb(u8x8_t *u8x8, uint8_t msg, uint8_t arg_int, void
uint8_t *data_ptr = (uint8_t *)arg_ptr; uint8_t *data_ptr = (uint8_t *)arg_ptr;
ESP_LOG_BUFFER_HEXDUMP(TAG, data_ptr, arg_int, ESP_LOG_VERBOSE); ESP_LOG_BUFFER_HEXDUMP(TAG, data_ptr, arg_int, ESP_LOG_VERBOSE);
while (arg_int > 0) // Bytes in lokalen Puffer sammeln, tatsächliche Übertragung bei END_TRANSFER
if (i2c_tx_len + (size_t)arg_int > sizeof(i2c_tx_buf))
{ {
I2C_ERROR_CHECK(i2c_master_write_byte(handle_i2c, *data_ptr, ACK_CHECK_EN)); ESP_LOGW(TAG, "I2C tx buffer overflow (%zu + %d)", i2c_tx_len, arg_int);
if (i2c_transfer_failed) i2c_transfer_failed = true;
{ break;
break;
}
data_ptr++;
arg_int--;
} }
memcpy(&i2c_tx_buf[i2c_tx_len], data_ptr, arg_int);
i2c_tx_len += (size_t)arg_int;
break; break;
} }
case U8X8_MSG_BYTE_START_TRANSFER: { case U8X8_MSG_BYTE_START_TRANSFER: {
uint8_t i2c_address = u8x8_GetI2CAddress(u8x8); uint8_t i2c_address = u8x8_GetI2CAddress(u8x8);
handle_i2c = i2c_cmd_link_create();
i2c_transfer_failed = false; // Reset error flag at start of transfer
ESP_LOGD(TAG, "Start I2C transfer to %02X.", i2c_address >> 1); ESP_LOGD(TAG, "Start I2C transfer to %02X.", i2c_address >> 1);
I2C_ERROR_CHECK(i2c_master_start(handle_i2c)); i2c_transfer_failed = false; // Reset error flag at start of transfer
I2C_ERROR_CHECK(i2c_master_write_byte(handle_i2c, i2c_address | I2C_MASTER_WRITE, ACK_CHECK_EN));
// Für neuen Treiber: Device-Handle für diese 7-Bit-Adresse anlegen (oder wiederverwenden)
uint8_t addr7 = i2c_address >> 1;
if (i2c_dev == NULL || addr7 != current_i2c_addr7)
{
if (i2c_dev)
{
i2c_master_bus_rm_device(i2c_dev);
i2c_dev = NULL;
}
i2c_device_config_t dev_cfg = {
.device_address = addr7,
.scl_speed_hz = I2C_MASTER_FREQ_HZ,
};
ESP_ERROR_CHECK(i2c_master_bus_add_device(i2c_bus, &dev_cfg, &i2c_dev));
current_i2c_addr7 = addr7;
}
i2c_tx_len = 0;
break; break;
} }
case U8X8_MSG_BYTE_END_TRANSFER: { case U8X8_MSG_BYTE_END_TRANSFER: {
ESP_LOGD(TAG, "End I2C transfer."); ESP_LOGD(TAG, "End I2C transfer.");
if (!i2c_transfer_failed) if (!i2c_transfer_failed && i2c_dev != NULL && i2c_tx_len > 0)
{ {
I2C_ERROR_CHECK(i2c_master_stop(handle_i2c)); esp_err_t rc = i2c_master_transmit(i2c_dev, i2c_tx_buf, i2c_tx_len, I2C_TIMEOUT_MS);
I2C_ERROR_CHECK(i2c_master_cmd_begin(I2C_MASTER_NUM, handle_i2c, pdMS_TO_TICKS(I2C_TIMEOUT_MS))); if (rc != ESP_OK)
{
ESP_LOGW(TAG, "I2C error: i2c_master_transmit = %d", rc);
i2c_transfer_failed = true;
}
} }
i2c_cmd_link_delete(handle_i2c);
break; break;
} }
} }

View File

@@ -0,0 +1,63 @@
#include "i2c_checker.h"
#include "driver/i2c_master.h"
#include "esp_insights.h"
#include "esp_log.h"
#include "hal/u8g2_esp32_hal.h"
static const char *TAG = "i2c_checker";
static esp_err_t i2c_device_check(i2c_master_bus_handle_t i2c_bus, uint8_t device_address)
{
// Use the new I2C master driver to probe for the device.
return i2c_master_probe(i2c_bus, device_address, 100);
}
esp_err_t i2c_bus_scan_and_check(void)
{
// 1. Configure and create I2C master bus using the new driver API
i2c_master_bus_handle_t i2c_bus = NULL;
i2c_master_bus_config_t bus_cfg = {
.i2c_port = I2C_MASTER_NUM,
.scl_io_num = I2C_MASTER_SCL_PIN,
.sda_io_num = I2C_MASTER_SDA_PIN,
.clk_source = I2C_CLK_SRC_DEFAULT,
.flags = {.enable_internal_pullup = true},
};
esp_err_t err = i2c_new_master_bus(&bus_cfg, &i2c_bus);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "I2C bus creation failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "I2C master bus initialized. Searching for device...");
// 2. Check if the device is present using the new API
err = i2c_device_check(i2c_bus, DISPLAY_I2C_ADDRESS);
if (err == ESP_OK)
{
ESP_LOGI(TAG, "Device found at address 0x%02X!", DISPLAY_I2C_ADDRESS);
// Here you could now call e.g. setup_screen()
}
else if (err == ESP_ERR_TIMEOUT)
{
ESP_LOGE(TAG, "Timeout! Device at address 0x%02X is not responding.", DISPLAY_I2C_ADDRESS);
}
else
{
ESP_LOGE(TAG, "Error communicating with address 0x%02X: %s", DISPLAY_I2C_ADDRESS, esp_err_to_name(err));
}
// 3. Delete I2C master bus if it is no longer needed
esp_err_t del_err = i2c_del_master_bus(i2c_bus);
if (del_err != ESP_OK)
{
ESP_LOGW(TAG, "Failed to delete I2C master bus: %s", esp_err_to_name(del_err));
}
ESP_DIAG_EVENT(TAG, "I2C master bus deleted.");
return err;
}

View File

@@ -1,8 +1,8 @@
#include "app_task.h" #include "app_task.h"
#include "color.h" #include "color.h"
#include "hal_esp32/PersistenceManager.h"
#include "led_status.h" #include "led_status.h"
#include "led_strip_ws2812.h" #include "led_strip_ws2812.h"
#include "persistence_manager.h"
#include "wifi_manager.h" #include "wifi_manager.h"
#include <ble_manager.h> #include <ble_manager.h>
#include <esp_event.h> #include <esp_event.h>
@@ -24,8 +24,9 @@ void app_main(void)
ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(nvs_flash_init());
} }
auto persistence = PersistenceManager(); persistence_manager_t persistence;
persistence.Load(); persistence_manager_init(&persistence, "config");
persistence_manager_load(&persistence);
led_status_init(CONFIG_STATUS_WLED_PIN); led_status_init(CONFIG_STATUS_WLED_PIN);

View File

@@ -45,3 +45,8 @@ CONFIG_SPIRAM_USE_CAPS_ALLOC=y
# HTTP Server WebSocket Support # HTTP Server WebSocket Support
CONFIG_HTTPD_WS_SUPPORT=y CONFIG_HTTPD_WS_SUPPORT=y
# MQTT
CONFIG_MQTT_CLIENT_BROKER_URL="mqtts://mqtt.mars3142.dev:8883"
CONFIG_MQTT_CLIENT_USERNAME="mars3142"
CONFIG_MQTT_CLIENT_PASSWORD="KPkEyzs9aur3Y7LfEybnd8PsxWd94ouQZGNGJ24y"

View File

@@ -1,2 +1,20 @@
# default ESP target # default ESP target
CONFIG_IDF_TARGET="esp32c6" CONFIG_IDF_TARGET="esp32c6"
#
# Display Settings
#
CONFIG_DISPLAY_SDA_PIN=9
CONFIG_DISPLAY_SCL_PIN=8
# end of Display Settings
#
# Button Configuration
#
CONFIG_BUTTON_UP=7
CONFIG_BUTTON_DOWN=4
CONFIG_BUTTON_LEFT=6
CONFIG_BUTTON_RIGHT=5
CONFIG_BUTTON_SELECT=19
CONFIG_BUTTON_BACK=20
# end of Button Configuration

View File

@@ -13,39 +13,23 @@
</head> </head>
<body> <body>
<div class="header-controls captive-header">
<button class="lang-toggle" onclick="toggleLanguage()" aria-label="Sprache wechseln">
<span class="lang-flag" id="lang-flag">🇩🇪</span>
<span id="lang-label">DE</span>
</button>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Theme wechseln">
<span class="theme-toggle-icon" id="theme-icon">🌙</span>
<span id="theme-label">Dark</span>
</button>
</div>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<div class="header-controls">
<button class="lang-toggle" onclick="toggleLanguage()" aria-label="Sprache wechseln">
<span class="lang-flag" id="lang-flag">🇩🇪</span>
<span class="lang-label" id="lang-label">DE</span>
</button>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Theme wechseln">
<span class="theme-toggle-icon" id="theme-icon">🌙</span>
<span class="theme-toggle-label" id="theme-label">Dark</span>
</button>
</div>
<h1>🚂 System Control</h1> <h1>🚂 System Control</h1>
<p data-i18n="captive.subtitle">WLAN-Einrichtung</p>
</div> </div>
<div class="card"> <div class="card">
<div id="scan-section">
<button class="btn btn-secondary" onclick="scanNetworks()" data-i18n="captive.scan">
📡 Netzwerke suchen
</button>
<div id="loading" class="loading">
<div class="spinner"></div>
<p data-i18n="captive.scanning">Suche nach Netzwerken...</p>
</div>
<div id="network-list" class="network-list" style="display: none;"></div>
</div>
<div class="divider"><span data-i18n="captive.or.manual">oder manuell eingeben</span></div>
<div class="form-group"> <div class="form-group">
<label for="ssid" data-i18n="wifi.ssid">WLAN-Name (SSID)</label> <label for="ssid" data-i18n="wifi.ssid">WLAN-Name (SSID)</label>
<input type="text" id="ssid" data-i18n-placeholder="wifi.ssid.placeholder" <input type="text" id="ssid" data-i18n-placeholder="wifi.ssid.placeholder"
@@ -62,7 +46,8 @@
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-primary" onclick="saveWifi()" data-i18n="captive.connect"> <button class="btn btn-primary" id="connect-btn" onclick="saveWifi()" data-i18n="captive.connect"
disabled>
💾 Verbinden 💾 Verbinden
</button> </button>
</div> </div>
@@ -112,26 +97,23 @@
setTheme(current === 'dark' ? 'light' : 'dark'); setTheme(current === 'dark' ? 'light' : 'dark');
} }
// Button aktivieren/deaktivieren
function updateConnectBtn() {
const ssid = document.getElementById('ssid').value;
const pw = document.getElementById('password').value;
const btn = document.getElementById('connect-btn');
btn.disabled = !(ssid.length > 0 && pw.length > 0);
}
// Initialize on load // Initialize on load
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initTheme(); initTheme();
initI18n(); initI18n();
// Auto-scan on load document.getElementById('ssid').addEventListener('input', updateConnectBtn);
setTimeout(scanNetworks, 500); document.getElementById('password').addEventListener('input', updateConnectBtn);
updateConnectBtn();
}); });
// Toggle password visibility
function togglePassword() {
const input = document.getElementById('password');
const btn = document.getElementById('password-btn');
if (input.type === 'password') {
input.type = 'text';
btn.textContent = '🙈';
} else {
input.type = 'password';
btn.textContent = '👁️';
}
}
</script> </script>
</body> </body>

View File

@@ -1,29 +1,57 @@
@media (max-width: 600px) {
.header {
flex-direction: row;
align-items: flex-start;
text-align: left;
}
.header h1 {
flex: 1 1 100%;
text-align: center;
order: 2;
margin-top: 8px;
}
.header-controls {
order: 1;
flex: 1 1 auto;
justify-content: flex-start;
display: flex;
gap: 8px;
}
}
/* Captive Portal CSS - WiFi setup specific styles */ /* Captive Portal CSS - WiFi setup specific styles */
/* Base styles are in shared.css */ /* Base styles are in shared.css */
body { body {
padding: 16px; padding: 12px;
display: flex;
align-items: center;
justify-content: center;
} }
.container { .container {
max-width: 900px;
margin: 0 auto;
width: 100%; width: 100%;
max-width: 400px;
} }
/* Header */ /* Header */
.header { .header {
text-align: center; display: flex;
margin-bottom: 24px; justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
} }
.header h1 { .header h1 {
font-size: 1.4rem; font-size: 1.5rem;
margin-bottom: 8px; margin: 0;
} }
.header p { .header p {
color: var(--text-muted); color: var(--text-muted);
font-size: 0.9rem; font-size: 0.9rem;
@@ -39,6 +67,15 @@ body {
box-shadow: 0 4px 20px var(--shadow); box-shadow: 0 4px 20px var(--shadow);
} }
#connect-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
box-shadow: none;
background-color: #888 !important;
}
}
/* Form Group - More spacing */ /* Form Group - More spacing */
.form-group { .form-group {
margin-bottom: 16px; margin-bottom: 16px;
@@ -154,27 +191,6 @@ select {
font-size: 1.1rem; font-size: 1.1rem;
} }
/* Password Toggle */
.password-toggle {
position: relative;
}
.password-toggle input {
padding-right: 50px;
}
.password-toggle button {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
font-size: 1.2rem;
cursor: pointer;
padding: 4px;
}
/* Info Box */ /* Info Box */
.info-box { .info-box {

View File

@@ -20,10 +20,6 @@ body {
gap: 10px; gap: 10px;
} }
.header h1 {
font-size: 1.5rem;
margin: 0;
}
.form-group { .form-group {
margin-bottom: 12px; margin-bottom: 12px;
@@ -561,6 +557,11 @@ body {
align-items: center; align-items: center;
padding: 10px 0; padding: 10px 0;
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
display: none;
}
.status-item.visible {
display: flex;
} }
.status-item:last-child { .status-item:last-child {

View File

@@ -1,3 +1,60 @@
/* Passwortfeld Toggle (zentral für alle Seiten) */
.password-toggle {
position: relative;
display: flex;
align-items: center;
gap: 0;
}
.password-toggle input {
padding-right: 50px;
flex: 1;
}
.password-toggle button {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
font-size: 1.2rem;
cursor: pointer;
padding: 4px;
transition: color 0.2s;
}
.password-toggle button:active {
color: var(--accent);
}
/* Passwortfeld Toggle */
.password-toggle {
display: flex;
align-items: center;
gap: 6px;
}
.password-toggle input[type="password"],
.password-toggle input[type="text"] {
flex: 1;
}
.password-toggle button {
background: none;
border: none;
font-size: 1.2em;
cursor: pointer;
color: var(--text-muted);
padding: 0 6px;
transition: color 0.2s;
}
.password-toggle button:active {
color: var(--accent);
}
/* Shared CSS - Base styles for all pages */ /* Shared CSS - Base styles for all pages */
/* CSS Variables - Dark Mode (default) */ /* CSS Variables - Dark Mode (default) */

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,6 @@
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>🚂 System Control</h1>
<div class="header-controls"> <div class="header-controls">
<button class="lang-toggle" onclick="toggleLanguage()" aria-label="Sprache wechseln"> <button class="lang-toggle" onclick="toggleLanguage()" aria-label="Sprache wechseln">
<span class="lang-flag" id="lang-flag">🇩🇪</span> <span class="lang-flag" id="lang-flag">🇩🇪</span>
@@ -27,6 +26,7 @@
<span class="theme-toggle-label" id="theme-label">Dark</span> <span class="theme-toggle-label" id="theme-label">Dark</span>
</button> </button>
</div> </div>
<h1>🚂 System Control</h1>
</div> </div>
<div class="tabs"> <div class="tabs">
@@ -70,7 +70,7 @@
<span class="mode-icon">🌙</span> <span class="mode-icon">🌙</span>
<span class="mode-name" data-i18n="mode.night">Nacht</span> <span class="mode-name" data-i18n="mode.night">Nacht</span>
</button> </button>
<button class="mode-btn active" id="mode-simulation" onclick="setMode('simulation')"> <button class="mode-btn" id="mode-simulation" onclick="setMode('simulation')">
<span class="mode-icon">🔄</span> <span class="mode-icon">🔄</span>
<span class="mode-name" data-i18n="mode.simulation">Simulation</span> <span class="mode-name" data-i18n="mode.simulation">Simulation</span>
</button> </button>
@@ -92,24 +92,24 @@
<div class="control-section"> <div class="control-section">
<h3 data-i18n="control.status.title">Aktueller Status</h3> <h3 data-i18n="control.status.title">Aktueller Status</h3>
<div class="status-display"> <div class="status-display">
<div class="status-item"> <div class="status-item visible">
<span class="status-label" data-i18n="control.status.mode">Modus</span> <span class="status-label" data-i18n="control.status.mode">Modus</span>
<span class="status-value" id="current-mode" data-i18n="mode.simulation">Simulation</span> <span class="status-value" id="current-mode" data-i18n="mode.simulation">Simulation</span>
</div> </div>
<div class="status-item"> <div class="status-item visible">
<span class="status-label" data-i18n="control.status.schema">Schema</span>
<span class="status-value" id="current-schema">Schema 1</span>
</div>
<div class="status-item">
<span class="status-label" data-i18n="control.status.color">Aktuelle Farbe</span> <span class="status-label" data-i18n="control.status.color">Aktuelle Farbe</span>
<div class="current-color-preview" id="current-color"></div> <div class="current-color-preview" id="current-color"></div>
</div> </div>
<div class="status-item">
<span class="status-label" data-i18n="control.status.clock">Uhrzeit</span>
<span class="status-value" id="current-clock">--:-- Uhr</span>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Szenen Card --> <!-- Szenen Card -->
<div class="card" id="scenes-control-card"> <div class="card" id="scenes-control-card" style="display: none;">
<h2 data-i18n="scenes.title">Szenen</h2> <h2 data-i18n="scenes.title">Szenen</h2>
<div id="scenes-control-list" class="scenes-grid"> <div id="scenes-control-list" class="scenes-grid">
<!-- Wird dynamisch gefüllt --> <!-- Wird dynamisch gefüllt -->
@@ -123,7 +123,7 @@
</div> </div>
<!-- Externe Geräte Card --> <!-- Externe Geräte Card -->
<div class="card" id="devices-control-card"> <div class="card" id="devices-control-card" style="display: none;">
<h2 data-i18n="devices.external">Externe Geräte</h2> <h2 data-i18n="devices.external">Externe Geräte</h2>
<div id="devices-control-list" class="devices-control-grid"> <div id="devices-control-list" class="devices-control-grid">
<!-- Wird dynamisch gefüllt --> <!-- Wird dynamisch gefüllt -->
@@ -162,8 +162,11 @@
<div class="form-group"> <div class="form-group">
<label for="password" data-i18n="wifi.password">WLAN Passwort</label> <label for="password" data-i18n="wifi.password">WLAN Passwort</label>
<input type="password" id="password" data-i18n-placeholder="wifi.password.placeholder" <div class="password-toggle">
placeholder="Passwort eingeben" autocomplete="off"> <input type="password" id="password" data-i18n-placeholder="wifi.password.placeholder"
placeholder="Passwort eingeben" autocomplete="off">
<button type="button" onclick="togglePassword()" id="password-btn">👁️</button>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@@ -40,7 +40,6 @@ document.addEventListener('touchend', (e) => {
lastTouchEnd = now; lastTouchEnd = now;
}, false); }, false);
// Initialization
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
initI18n(); initI18n();
initTheme(); initTheme();

View File

@@ -41,6 +41,7 @@ const translations = {
'control.status.mode': 'Modus', 'control.status.mode': 'Modus',
'control.status.schema': 'Schema', 'control.status.schema': 'Schema',
'control.status.color': 'Aktuelle Farbe', 'control.status.color': 'Aktuelle Farbe',
'control.status.clock': 'Uhrzeit',
// Common // Common
'common.on': 'AN', 'common.on': 'AN',
@@ -108,12 +109,16 @@ const translations = {
'wifi.disconnected': '❌ Nicht verbunden', 'wifi.disconnected': '❌ Nicht verbunden',
'wifi.unavailable': '⚠️ Status nicht verfügbar', 'wifi.unavailable': '⚠️ Status nicht verfügbar',
'wifi.searching': 'Suche läuft...', 'wifi.searching': 'Suche läuft...',
'wifi.scan.error': 'Fehler beim Scannen', 'wifi.scan.error': 'Fehler beim WLAN-Scan',
'wifi.scan.failed': 'Netzwerksuche fehlgeschlagen', 'wifi.scan.failed': 'Netzwerksuche fehlgeschlagen',
'wifi.saved': 'WLAN-Konfiguration gespeichert! Gerät verbindet sich...', 'wifi.saved': 'WLAN-Konfiguration gespeichert! Gerät verbindet sich...',
'wifi.error.ssid': 'Bitte WLAN-Name eingeben', 'wifi.error.ssid': 'Bitte WLAN-Name eingeben',
'wifi.error.save': 'Fehler beim Speichern', 'wifi.error.save': 'Fehler beim Speichern',
'wifi.networks.found': '{count} Netzwerk(e) gefunden', 'wifi.networks.found': '{count} Netzwerk(e) gefunden',
'wifi.networks.notfound': 'Keine Netzwerke gefunden.',
'wifi.signal': 'Signal',
'wifi.secure': 'Gesichert',
'wifi.open': 'Offen',
// Schema Editor // Schema Editor
'schema.editor.title': 'Licht-Schema Editor', 'schema.editor.title': 'Licht-Schema Editor',
@@ -170,7 +175,8 @@ const translations = {
// General // General
'loading': 'Laden...', 'loading': 'Laden...',
'error': 'Fehler', 'error': 'Fehler',
'success': 'Erfolg' 'success': 'Erfolg',
'clock.suffix': 'Uhr'
}, },
en: { en: {
@@ -212,6 +218,7 @@ const translations = {
'control.status.mode': 'Mode', 'control.status.mode': 'Mode',
'control.status.schema': 'Schema', 'control.status.schema': 'Schema',
'control.status.color': 'Current Color', 'control.status.color': 'Current Color',
'control.status.clock': "Time",
// Common // Common
'common.on': 'ON', 'common.on': 'ON',
@@ -285,6 +292,10 @@ const translations = {
'wifi.error.ssid': 'Please enter WiFi name', 'wifi.error.ssid': 'Please enter WiFi name',
'wifi.error.save': 'Error saving', 'wifi.error.save': 'Error saving',
'wifi.networks.found': '{count} network(s) found', 'wifi.networks.found': '{count} network(s) found',
'wifi.networks.notfound': 'No networks found.',
'wifi.signal': 'Signal',
'wifi.secure': 'Secured',
'wifi.open': 'Open',
// Schema Editor // Schema Editor
'schema.editor.title': 'Light Schema Editor', 'schema.editor.title': 'Light Schema Editor',
@@ -341,7 +352,8 @@ const translations = {
// General // General
'loading': 'Loading...', 'loading': 'Loading...',
'error': 'Error', 'error': 'Error',
'success': 'Success' 'success': 'Success',
'clock.suffix': "o'clock"
} }
}; };
@@ -440,6 +452,10 @@ function updatePageLanguage() {
if (titleEl) { if (titleEl) {
document.title = t(titleEl.getAttribute('data-i18n')); document.title = t(titleEl.getAttribute('data-i18n'));
} }
// WLAN-Optionen dynamisch übersetzen
if (typeof updateWifiOptionsLanguage === 'function') {
updateWifiOptionsLanguage();
}
} }
/** /**

View File

@@ -115,6 +115,20 @@ function updateSimulationOptions() {
} else { } else {
options.classList.remove('visible'); options.classList.remove('visible');
} }
[
'control.status.clock'
].forEach(i18nKey => {
const label = document.querySelector(`.status-item .status-label[data-i18n="${i18nKey}"]`);
const item = label ? label.closest('.status-item') : null;
if (item) {
if (currentMode === 'simulation') {
item.classList.add('visible');
} else {
item.classList.remove('visible');
}
}
});
} }
async function setActiveSchema() { async function setActiveSchema() {
@@ -173,8 +187,6 @@ async function loadLightStatus() {
// Update schema // Update schema
if (status.schema) { if (status.schema) {
document.getElementById('active-schema').value = status.schema; document.getElementById('active-schema').value = status.schema;
const schemaNum = status.schema.replace('schema_0', '').replace('.csv', '');
document.getElementById('current-schema').textContent = t(`schema.name.${schemaNum}`);
} }
// Update current color // Update current color
@@ -184,6 +196,15 @@ async function loadLightStatus() {
colorPreview.style.backgroundColor = `rgb(${status.color.r}, ${status.color.g}, ${status.color.b})`; colorPreview.style.backgroundColor = `rgb(${status.color.r}, ${status.color.g}, ${status.color.b})`;
} }
} }
// Update clock/time
if (status.clock) {
const clockEl = document.getElementById('current-clock');
if (clockEl) {
// Use one translation key for the suffix, language is handled by t()
clockEl.textContent = status.clock + ' ' + t('clock.suffix');
}
}
} }
} catch (error) { } catch (error) {
console.log('Light status not available'); console.log('Light status not available');

View File

@@ -45,7 +45,7 @@ function switchSubTab(subTabName) {
document.querySelector(`.sub-tab[onclick="switchSubTab('${subTabName}')"]`).classList.add('active'); document.querySelector(`.sub-tab[onclick="switchSubTab('${subTabName}')"]`).classList.add('active');
document.getElementById(`subtab-${subTabName}`).classList.add('active'); document.getElementById(`subtab-${subTabName}`).classList.add('active');
if (subTabName === 'schema' && schemaData.length === 0) { if (subTabName === 'schema' && typeof schemaData !== 'undefined' && schemaData.length === 0) {
loadSchema(); loadSchema();
} }
} }

View File

@@ -65,18 +65,32 @@ function updateStatusFromData(status) {
} }
if (status.schema) { if (status.schema) {
document.getElementById('active-schema').value = status.schema; const activeSchemaEl = document.getElementById('active-schema');
if (activeSchemaEl) {
activeSchemaEl.value = status.schema;
}
const schemaNames = { const schemaNames = {
'schema_01.csv': 'Schema 1', 'schema_01.csv': 'Schema 1',
'schema_02.csv': 'Schema 2', 'schema_02.csv': 'Schema 2',
'schema_03.csv': 'Schema 3' 'schema_03.csv': 'Schema 3'
}; };
document.getElementById('current-schema').textContent = schemaNames[status.schema] || status.schema; const currentSchemaEl = document.getElementById('current-schema');
if (currentSchemaEl) {
currentSchemaEl.textContent = schemaNames[status.schema] || status.schema;
}
} }
if (status.color) { if (status.color) {
updateColorPreview(status.color.r, status.color.g, status.color.b); updateColorPreview(status.color.r, status.color.g, status.color.b);
} }
// Update clock/time
if (status.clock) {
const clockEl = document.getElementById('current-clock');
if (clockEl) {
clockEl.textContent = status.clock + ' ' + (typeof t === 'function' ? t('clock.suffix') : '');
}
}
} }
function updateColorPreview(r, g, b) { function updateColorPreview(r, g, b) {

View File

@@ -1,3 +1,18 @@
/**
* Passwortfeld sichtbar/unsichtbar schalten (shared)
*/
function togglePassword() {
const input = document.getElementById('password');
const btn = document.getElementById('password-btn');
if (!input || !btn) return;
if (input.type === 'password') {
input.type = 'text';
btn.textContent = '🙈';
} else {
input.type = 'password';
btn.textContent = '👁️';
}
}
// Shared WiFi configuration functions // Shared WiFi configuration functions
// Used by both captive.html and index.html // Used by both captive.html and index.html
@@ -43,6 +58,13 @@ async function scanNetworks() {
try { try {
const response = await fetch('/api/wifi/scan'); const response = await fetch('/api/wifi/scan');
if (!response.ok) {
// Fehlerhafte API-Antwort, aber ESP32 ist erreichbar
const errorText = await response.text();
showStatus('wifi-status', t('wifi.error.scan') + ': ' + errorText, 'error');
if (loading) loading.classList.remove('active');
return;
}
const networks = await response.json(); const networks = await response.json();
if (loading) { if (loading) {
@@ -92,43 +114,43 @@ async function scanNetworks() {
if (loading) { if (loading) {
loading.classList.remove('active'); loading.classList.remove('active');
} }
// Nur bei Netzwerkfehlern Demo-Daten anzeigen
// Demo mode for local testing if (error instanceof TypeError) {
const demoNetworks = [ const demoNetworks = [
{ ssid: 'Demo-Netzwerk', rssi: -45 }, { ssid: 'Demo-Netzwerk', rssi: -45 },
{ ssid: 'Gast-WLAN', rssi: -67 }, { ssid: 'Gast-WLAN', rssi: -67 },
{ ssid: 'Nachbar-WiFi', rssi: -82 } { ssid: 'Nachbar-WiFi', rssi: -82 }
]; ];
if (networkList) {
if (networkList) { demoNetworks.forEach(network => {
demoNetworks.forEach(network => { const signalIcon = getSignalIcon(network.rssi);
const signalIcon = getSignalIcon(network.rssi); const item = document.createElement('div');
const item = document.createElement('div'); item.className = 'network-item';
item.className = 'network-item'; item.onclick = () => selectNetwork(network.ssid, item);
item.onclick = () => selectNetwork(network.ssid, item); item.innerHTML = `
item.innerHTML = ` <span class="network-name">
<span class="network-name"> <span class="signal-icon">${signalIcon}</span>
<span class="signal-icon">${signalIcon}</span> ${escapeHtml(network.ssid)}
${escapeHtml(network.ssid)} </span>
</span> <span class="network-signal">${network.rssi} dBm</span>
<span class="network-signal">${network.rssi} dBm</span> `;
`; networkList.appendChild(item);
networkList.appendChild(item); });
}); networkList.style.display = 'block';
networkList.style.display = 'block'; }
if (select) {
select.innerHTML = `<option value="">${t('wifi.scan.hint')}</option>`;
demoNetworks.forEach(network => {
const option = document.createElement('option');
option.value = network.ssid;
option.textContent = `${network.ssid} (${network.rssi} dBm)`;
select.appendChild(option);
});
}
showStatus('wifi-status', 'Demo: ' + t('wifi.networks.found', { count: demoNetworks.length }), 'info');
} else {
showStatus('wifi-status', t('wifi.error.scan') + ': ' + error.message, 'error');
} }
if (select) {
select.innerHTML = `<option value="">${t('wifi.scan.hint')}</option>`;
demoNetworks.forEach(network => {
const option = document.createElement('option');
option.value = network.ssid;
option.textContent = `${network.ssid} (${network.rssi} dBm)`;
select.appendChild(option);
});
}
showStatus('wifi-status', 'Demo: ' + t('wifi.networks.found', { count: demoNetworks.length }), 'info');
} }
} }

View File

@@ -0,0 +1,12 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="dev" type="js.build_tools.npm" nameIsGenerated="true">
<package-json value="$PROJECT_DIR$/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

View File

@@ -0,0 +1,8 @@
{
"hash": "15486339",
"configHash": "5ec1f82b",
"lockfileHash": "2bc40369",
"browserHash": "9efd6930",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

View File

@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#1a1a2e">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<title>System Control</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,32 @@
{
"compilerOptions": {
"moduleResolution": "bundler",
"target": "ESNext",
"module": "ESNext",
/**
* svelte-preprocess cannot figure out whether you have
* a value or a type, so tell TypeScript to enforce using
* `import type` instead of `import` for Types.
*/
"verbatimModuleSyntax": true,
"isolatedModules": true,
"resolveJsonModule": true,
/**
* To have warnings / errors of the Svelte compiler at the
* correct position, enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable this if you'd like to use dynamic types.
*/
"checkJs": true
},
/**
* Use global.d.ts instead of compilerOptions.types
* to avoid limiting type declarations.
*/
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
}

1560
firmware/website.bak/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
{
"name": "learn-svelte",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.1.1",
"svelte": "^5.38.1",
"vite": "^7.1.2",
"vite-plugin-compression": "^0.5.1"
},
"dependencies": {
"@fontsource/atkinson-hyperlegible": "^5.2.6",
"@picocss/pico": "^2.1.1",
"gsap": "^3.13.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y=".9em" font-size="90">🚂</text></svg>

After

Width:  |  Height:  |  Size: 109 B

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import Header from "./compoents/Header.svelte";
import Index from "./Index.svelte";
import Captive from "./Captive.svelte";
import { onMount } from "svelte";
import { writable } from "svelte/store";
const isCaptive = writable(false);
function checkHash() {
isCaptive.set(window.location.hash === "#/captive");
}
onMount(() => {
checkHash();
window.addEventListener("hashchange", checkHash);
return () => window.removeEventListener("hashchange", checkHash);
});
</script>
<Header />
{#if $isCaptive}
<Captive />
{:else}
<Index />
{/if}

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import { t } from "./i18n/store";
</script>
<h1>{$t("welcome")} - Captive Portal</h1>

View File

@@ -0,0 +1,5 @@
<script lang="ts">
import { t } from "./i18n/store";
</script>
<h1>{$t("welcome")}</h1>

View File

@@ -0,0 +1,70 @@
:root {
--bg-color: #1a1a2e;
--card-bg: #16213e;
--accent: #0f3460;
--text: #eaeaea;
--text-muted: #a0a0a0;
--success: #00d26a;
--error: #ff6b6b;
--border: #2a2a4a;
--input-bg: #1a1a2e;
--shadow: rgba(0, 0, 0, 0.3);
--primary: #c41e3a;
}
[data-theme="light"] {
--bg-color: #faf8f5;
--card-bg: #ffffff;
--accent: #fef2f2;
--text: #1a1a2e;
--text-muted: #6b7280;
--success: #c41e3a;
--error: #dc2626;
--border: #e5d9d0;
--input-bg: #ffffff;
--shadow: rgba(196, 30, 58, 0.1);
--primary: #c41e3a;
}
[data-theme="light"] .header h1 {
color: var(--primary);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-color);
color: var(--text);
min-height: 100vh;
transition: background 0.3s, color 0.3s;
padding: 12px;
}
h1 {
font-size: 1.5rem;
color: var(--text);
}
#app {
max-width: 900px;
margin: 0 auto;
}
@media (max-width: 600px) {
body {
padding: 6px;
}
}
@supports (padding: max(0px)) {
body {
padding-left: max(12px, env(safe-area-inset-left));
padding-right: max(12px, env(safe-area-inset-right));
padding-bottom: max(12px, env(safe-area-inset-bottom));
}
}

View File

@@ -0,0 +1,133 @@
<script lang="ts">
import Toggle from "./Toggle.svelte";
import {toggleTheme} from "../theme";
import {onMount} from "svelte";
import {writable} from "svelte/store";
import {lang, t} from "../i18n/store";
const theme = writable<"dark" | "light">("dark");
function applyInitialTheme() {
const userTheme = localStorage.getItem("theme");
if (userTheme) {
document.documentElement.setAttribute("data-theme", userTheme);
} else if (window.matchMedia("(prefers-color-scheme: light)").matches) {
document.documentElement.setAttribute("data-theme", "light");
} else {
document.documentElement.setAttribute("data-theme", "dark");
}
}
function updateThemeFromDom() {
const t = document.documentElement.getAttribute("data-theme");
theme.set(t === "light" ? "light" : "dark");
}
function handleThemeToggle() {
toggleTheme();
updateThemeFromDom();
}
let themeIcon = $state("🌙");
let themeLabel = $state("Dark");
let currentLangCode = $state($lang);
let currentLang = $state("Deutsch");
let currentFlag = $state("🇩🇪");
$effect(() => {
theme.subscribe(($theme) => {
themeIcon = $theme === "light" ? "☀️" : "🌙";
themeLabel = $theme === "light" ? "Light" : "Dark";
});
lang.subscribe(($lang) => {
currentLangCode = $lang;
currentLang = $lang === "de" ? "Deutsch" : "English";
currentFlag = $lang === "de" ? "🇩🇪" : "🇬🇧";
});
});
function handleLangChange(newLang: "de" | "en") {
lang.set(newLang);
localStorage.setItem("lang", newLang);
}
onMount(() => {
applyInitialTheme();
updateThemeFromDom();
window.addEventListener("storage", updateThemeFromDom);
// Listener für OS-Theme-Änderung
const mql = window.matchMedia("(prefers-color-scheme: light)");
const osThemeListener = () => {
// Nur reagieren, wenn kein User-Theme gesetzt ist
if (!localStorage.getItem("theme")) {
applyInitialTheme();
updateThemeFromDom();
}
};
mql.addEventListener("change", osThemeListener);
return () => {
window.removeEventListener("storage", updateThemeFromDom);
mql.removeEventListener("change", osThemeListener);
};
});
</script>
<div class="header">
<div class="header-controls">
<Toggle
label={currentLang}
icon={currentFlag}
ariaLabel="Sprache wechseln"
onClick={() => {
const newLang = currentLangCode === "de" ? "en" : "de";
handleLangChange(newLang);
}}
/>
<Toggle
label={themeLabel}
icon={themeIcon}
ariaLabel="Theme wechseln"
onClick={handleThemeToggle}
/>
</div>
<h1>🚂 System Control</h1>
</div>
<style>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 10px;
}
.header-controls {
display: flex;
align-items: center;
gap: 8px;
}
@media (max-width: 600px) {
.header {
flex-direction: column;
align-items: stretch;
text-align: center;
}
.header h1 {
order: 1;
}
}
@media (max-width: 380px) {
.header h1 {
font-size: 1.2rem;
}
}
</style>

View File

@@ -0,0 +1,37 @@
<script lang="ts">
const { label, icon, ariaLabel, onClick } = $props<{
label: string;
icon: string;
ariaLabel: string;
onClick?: () => void;
}>();
</script>
<button class="toggle" aria-label={ariaLabel} onclick={onClick}>
<span class="icon" id="icon">{icon}</span>
<span class="label" id="label">{label}</span>
</button>
<style>
.toggle {
display: flex;
align-items: center;
gap: 6px;
background: var(--card-bg);
padding: 8px 12px;
border-radius: 20px;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
font-size: 0.85rem;
color: var(--text-muted);
}
.toggle:hover {
border-color: var(--success);
}
.icon {
font-size: 1.2rem;
}
</style>

View File

@@ -0,0 +1,6 @@
{
"hello": "Hallo Welt",
"welcome": "Willkommen",
"language": "Sprache",
"save": "Speichern"
}

View File

@@ -0,0 +1,6 @@
{
"hello": "Hello World",
"welcome": "Welcome",
"language": "Language",
"save": "Save"
}

View File

@@ -0,0 +1,12 @@
import de from './de.json';
import en from './en.json';
export const translations = { de, en };
export type Lang = keyof typeof translations;
export function getInitialLang(): Lang {
const navLang = navigator.language.slice(0, 2);
if (navLang in translations) return navLang as Lang;
return 'en';
}

View File

@@ -0,0 +1,16 @@
import { writable, derived } from 'svelte/store';
import { translations, getInitialLang, type Lang } from './index';
function getLang(): Lang {
const stored = localStorage.getItem('lang');
if (stored && stored in translations) return stored as Lang;
return getInitialLang();
}
export const lang = writable<Lang>(getLang());
export const t = derived(lang, $lang => {
return (key: string) => {
return translations[$lang][key] || key;
};
});

View File

@@ -0,0 +1,9 @@
import { mount } from 'svelte'
import App from './App.svelte'
import './app.css'
const app = mount(App, {
target: document.getElementById('app'),
})
export default app

Some files were not shown because too many files have changed in this diff Show More