25 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
153 changed files with 12899 additions and 455 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

@@ -11,6 +11,9 @@ idf_component_register(SRCS
esp_netif esp_netif
esp_event esp_event
json json
led-manager
simulator simulator
persistence-manager persistence-manager
message-manager
simulator
) )

View File

@@ -3,6 +3,7 @@
#include <cJSON.h> #include <cJSON.h>
void common_init(void);
cJSON *create_light_status_json(void); cJSON *create_light_status_json(void);
#endif // COMMON_H #endif // COMMON_H

View File

@@ -1,14 +1,20 @@
#include "api_handlers.h" #include "api_handlers.h"
#include "common.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>
#include <esp_wifi.h> #include <esp_wifi.h>
#include <persistence_manager.h>
#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
@@ -58,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;
@@ -89,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");
@@ -131,6 +137,7 @@ static bool is_valid(const cJSON *string)
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);
@@ -158,7 +165,7 @@ esp_err_t api_wifi_config_handler(httpd_req_t *req)
if (is_valid(pw)) 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);
@@ -244,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);
@@ -255,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\"}");
} }
@@ -282,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);
@@ -293,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\"}");
} }
@@ -301,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);
@@ -312,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\"}");
} }
@@ -336,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\"}");
} }
@@ -368,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)
{ {
@@ -384,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\"}");
} }
@@ -441,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);
@@ -471,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);
@@ -490,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);
@@ -509,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);
@@ -561,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);
@@ -599,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);
@@ -655,7 +902,7 @@ esp_err_t api_static_file_handler(httpd_req_t *req)
const char *uri = req->uri; const char *uri = req->uri;
wifi_mode_t mode = 0; wifi_mode_t mode = 0;
esp_wifi_get_mode(&mode); esp_wifi_get_mode(&mode);
// Im AP-Modus immer captive.html ausliefern // Always serve captive.html in AP mode
if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA) if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA)
{ {
if (strcmp(uri, "/") == 0 || strcmp(uri, "/index.html") == 0) if (strcmp(uri, "/") == 0 || strcmp(uri, "/index.html") == 0)
@@ -726,7 +973,7 @@ 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);
// captive.html direkt ausliefern (Status 200, text/html) // Serve captive.html directly (status 200, text/html)
const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH; const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH;
char filepath[256]; char filepath[256];
snprintf(filepath, sizeof(filepath), "%s/captive.html", base_path); snprintf(filepath, sizeof(filepath), "%s/captive.html", base_path);
@@ -735,7 +982,7 @@ esp_err_t api_captive_portal_handler(httpd_req_t *req)
{ {
ESP_LOGE(TAG, "captive.html not found: %s", filepath); ESP_LOGE(TAG, "captive.html not found: %s", filepath);
httpd_resp_set_status(req, "500 Internal Server Error"); httpd_resp_set_status(req, "500 Internal Server Error");
httpd_resp_sendstr(req, "Captive Portal nicht verfügbar"); httpd_resp_sendstr(req, "Captive portal not available");
return ESP_FAIL; return ESP_FAIL;
} }
httpd_resp_set_type(req, "text/html"); httpd_resp_set_type(req, "text/html");

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

@@ -2,19 +2,78 @@
#include <cJSON.h> #include <cJSON.h>
#include <stdbool.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 // Gibt ein cJSON-Objekt with dem aktuellen Lichtstatus zurück
cJSON *create_light_status_json(void) cJSON *create_light_status_json(void)
{ {
persistence_manager_t pm;
persistence_manager_init(&pm, "config");
cJSON *json = cJSON_CreateObject(); cJSON *json = cJSON_CreateObject();
// TODO: Echte Werte einfügen, aktuell Dummy-Daten
cJSON_AddBoolToObject(json, "on", false); bool light_active = persistence_manager_get_bool(&pm, "light_active", false);
cJSON_AddBoolToObject(json, "on", light_active);
cJSON_AddBoolToObject(json, "thunder", false); cJSON_AddBoolToObject(json, "thunder", false);
cJSON_AddStringToObject(json, "mode", "day");
cJSON_AddStringToObject(json, "schema", "schema_03.csv"); int mode = persistence_manager_get_int(&pm, "light_mode", 0);
cJSON *color = cJSON_CreateObject(); const char *mode_str = "simulation";
cJSON_AddNumberToObject(color, "r", 255); if (mode == 1)
cJSON_AddNumberToObject(color, "g", 240); {
cJSON_AddNumberToObject(color, "b", 220); mode_str = "day";
cJSON_AddItemToObject(json, "color", color); }
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; return json;
} }

View File

@@ -1,6 +1,8 @@
#include "websocket_handler.h" #include "websocket_handler.h"
#include "api_server.h"
#include "common.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>
@@ -11,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++)
@@ -179,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

@@ -10,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

@@ -1,8 +1,8 @@
#include "wifi_manager.h" #include "wifi_manager.h"
#include "dns_hijack.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>
@@ -29,6 +29,48 @@ static EventGroupHandle_t s_wifi_event_group;
static const char *TAG = "wifi_manager"; static const char *TAG = "wifi_manager";
static void led_status_reconnect()
{
led_behavior_t led_behavior = {
.on_time_ms = 250,
.off_time_ms = 100,
.color = {.red = 50, .green = 50, .blue = 0},
.index = 0,
.mode = LED_MODE_BLINK,
};
led_status_set_behavior(led_behavior);
}
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
ESP_LOGI(TAG, "WIFI_EVENT_STA_START: Connecting to AP...");
esp_wifi_connect();
}
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED)
{
ESP_LOGW(TAG, "WIFI_EVENT_STA_DISCONNECTED: Verbindung verloren, versuche erneut...");
xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
led_status_reconnect();
esp_wifi_connect();
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP: IP-Adresse erhalten: " IPSTR, IP2STR(&event->ip_info.ip));
analytics_init();
xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
}
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() static void wifi_create_ap()
{ {
ESP_ERROR_CHECK(esp_wifi_stop()); ESP_ERROR_CHECK(esp_wifi_stop());
@@ -58,6 +100,13 @@ void wifi_manager_init()
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());
// Default WiFi Station
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
// Event Handler registrieren
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));
// Try to load stored WiFi configuration // Try to load stored WiFi configuration
persistence_manager_t pm; persistence_manager_t pm;
char ssid[33] = {0}; char ssid[33] = {0};
@@ -73,14 +122,7 @@ void wifi_manager_init()
if (have_ssid && have_password) if (have_ssid && have_password)
{ {
led_behavior_t led_behavior = { led_status_reconnect();
.on_time_ms = 250,
.off_time_ms = 100,
.color = {.red = 50, .green = 50, .blue = 0},
.index = 0,
.mode = LED_MODE_BLINK,
};
led_status_set_behavior(led_behavior);
ESP_LOGI(TAG, "Found WiFi configuration: SSID='%s'", ssid); ESP_LOGI(TAG, "Found WiFi configuration: SSID='%s'", ssid);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
@@ -97,9 +139,9 @@ void wifi_manager_init()
EventBits_t bits; EventBits_t bits;
do do
{ {
esp_wifi_connect(); ESP_LOGI(TAG, "Warte auf IP-Adresse (DHCP)...");
bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
5000 / portTICK_PERIOD_MS); 10000 / portTICK_PERIOD_MS);
if (bits & WIFI_CONNECTED_BIT) if (bits & WIFI_CONNECTED_BIT)
{ {
led_behavior_t led_behavior = { led_behavior_t led_behavior = {
@@ -108,7 +150,7 @@ void wifi_manager_init()
.mode = LED_MODE_SOLID, .mode = LED_MODE_SOLID,
}; };
led_status_set_behavior(led_behavior); led_status_set_behavior(led_behavior);
ESP_LOGI(TAG, "WiFi connection established successfully"); ESP_LOGI(TAG, "WiFi connection established successfully (mit IP)");
break; break;
} }
retries++; retries++;
@@ -116,7 +158,9 @@ void wifi_manager_init()
if (!(bits & WIFI_CONNECTED_BIT)) if (!(bits & WIFI_CONNECTED_BIT))
{ {
ESP_LOGW(TAG, "WiFi connection failed, switching to Access Point mode"); 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(); wifi_create_ap();
} }
} }

View File

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

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); };
} }
@@ -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

@@ -45,7 +45,7 @@ void ClockScreenSaver::getCurrentTimeString(char *buffer, size_t bufferSize) con
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
@@ -71,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 = {};
persistence_manager_set_bool(m_options->persistenceManager, 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;
} }
@@ -89,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)
{ {
persistence_manager_set_int(m_options->persistenceManager, LightMenuOptions::LIGHT_MODE, value); // Post change via message_manager
persistence_manager_save(m_options->persistenceManager); 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;
} }
@@ -106,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)
{ {
persistence_manager_set_int(m_options->persistenceManager, LightMenuOptions::LIGHT_VARIANT, value); // Post change via message_manager
persistence_manager_save(m_options->persistenceManager); 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;
} }
@@ -127,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

@@ -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

@@ -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

@@ -28,6 +28,15 @@ extern "C"
bool initialized; bool initialized;
} persistence_manager_t; } 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. * @brief Initialize the persistence manager with a given NVS namespace.
* *
@@ -196,6 +205,33 @@ extern "C"
void persistence_manager_get_string(const persistence_manager_t *pm, const char *key, char *out_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); 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 #ifdef __cplusplus
} }
#endif #endif

View File

@@ -4,6 +4,17 @@
#define TAG "persistence_manager" #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) esp_err_t persistence_manager_init(persistence_manager_t *pm, const char *nvs_namespace)
{ {
if (!pm) if (!pm)
@@ -15,7 +26,7 @@ esp_err_t persistence_manager_init(persistence_manager_t *pm, const char *nvs_na
if (err == ESP_OK) if (err == ESP_OK)
{ {
pm->initialized = true; pm->initialized = true;
ESP_LOGI(TAG, "Initialized with namespace: %s", pm->nvs_namespace); ESP_LOGD(TAG, "Initialized with namespace: %s", pm->nvs_namespace);
return ESP_OK; return ESP_OK;
} }
ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "Failed to open NVS handle: %s", esp_err_to_name(err));
@@ -162,6 +173,17 @@ void persistence_manager_set_string(persistence_manager_t *pm, const char *key,
} }
} }
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) bool persistence_manager_get_bool(const persistence_manager_t *pm, const char *key, bool default_value)
{ {
if (!persistence_manager_is_initialized(pm)) if (!persistence_manager_is_initialized(pm))
@@ -233,3 +255,20 @@ void persistence_manager_get_string(const persistence_manager_t *pm, const char
return; 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"
{
#endif
char *get_time(void);
esp_err_t add_light_item(const char time[5], uint8_t red, uint8_t green, uint8_t blue, uint8_t white,
uint8_t brightness, uint8_t saturation); uint8_t brightness, uint8_t saturation);
void cleanup_light_items(void); void cleanup_light_items(void);
void start_simulate_day(void); void start_simulate_day(void);
void start_simulate_night(void); void start_simulate_night(void);
void start_simulation_task(void); void start_simulation_task(void);
void stop_simulation_task(void); void stop_simulation_task(void);
void start_simulation(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

@@ -2,6 +2,7 @@
#include "color.h" #include "color.h"
#include "led_strip_ws2812.h" #include "led_strip_ws2812.h"
#include "message_manager.h"
#include "persistence_manager.h" #include "persistence_manager.h"
#include "storage.h" #include "storage.h"
#include <esp_heap_caps.h> #include <esp_heap_caps.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;
} }
@@ -148,7 +149,7 @@ static void initialize_light_items(void)
persistence_manager_t persistence; persistence_manager_t persistence;
persistence_manager_init(&persistence, "config"); persistence_manager_init(&persistence, "config");
int variant = persistence_manager_get_int(&persistence, "light_variant", 1); int variant = persistence_manager_get_int(&persistence, "light_variant", 1);
snprintf(filename, sizeof(filename), "/spiffs/schema_%02d.csv", variant); snprintf(filename, sizeof(filename), "schema_%02d.csv", variant);
load_file(filename); load_file(filename);
persistence_manager_deinit(&persistence); persistence_manager_deinit(&persistence);
@@ -235,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();
@@ -242,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);
} }
} }
@@ -254,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);
} }
} }
@@ -296,7 +310,13 @@ 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)
{
rgb_t color = {0, 0, 0};
// Use head as fallback if next_item is NULL
next_item = next_item ? next_item : head;
if (next_item != NULL)
{ {
int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100); 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 next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
@@ -325,16 +345,16 @@ void simulate_cycle(void *args)
rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue}; rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
// Use the interpolation function // Use the interpolation function
rgb_t final_rgb = interpolate_color(start_rgb, end_rgb, interpolation_factor); color = interpolate_color(start_rgb, end_rgb, interpolation_factor);
led_strip_update(LED_STATE_SIMULATION, color);
led_strip_update(LED_STATE_SIMULATION, final_rgb);
} }
else if (current_item != NULL) else
{ {
// No next item, just use current // No next_item and no head, use only current
led_strip_update( color = (rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
LED_STATE_SIMULATION, led_strip_update(LED_STATE_SIMULATION, color);
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue}); }
send_simulation_message(time, color);
} }
vTaskDelay(pdMS_TO_TICKS(delay_ms)); vTaskDelay(pdMS_TO_TICKS(delay_ms));
@@ -353,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.");

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 "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 "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,7 @@ 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;
// Persistence Manager für C-API // Persistence Manager for C-API
persistence_manager_t g_persistence_manager; persistence_manager_t g_persistence_manager;
extern QueueHandle_t buttonQueue; extern QueueHandle_t buttonQueue;
@@ -125,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();
@@ -182,12 +197,66 @@ 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();
wifi_manager_init(); wifi_manager_init();
mqtt_client_start();
message_manager_register_listener(on_message_received);
start_simulation(); start_simulation();
auto oldTime = esp_timer_get_time(); auto oldTime = esp_timer_get_time();

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)
{
i2c_master_bus_config_t bus_cfg = {
.i2c_port = I2C_MASTER_NUM,
.scl_io_num = u8g2_esp32_hal.bus.i2c.scl,
.sda_io_num = u8g2_esp32_hal.bus.i2c.sda,
.clk_source = I2C_CLK_SRC_DEFAULT,
.flags = {.enable_internal_pullup = true},
};
ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda); ESP_LOGI(TAG, "sda_io_num %d", u8g2_esp32_hal.bus.i2c.sda);
conf.sda_io_num = u8g2_esp32_hal.bus.i2c.sda;
conf.sda_pullup_en = GPIO_PULLUP_ENABLE;
ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl); ESP_LOGI(TAG, "scl_io_num %d", u8g2_esp32_hal.bus.i2c.scl);
conf.scl_io_num = u8g2_esp32_hal.bus.i2c.scl;
conf.scl_pullup_en = GPIO_PULLUP_ENABLE;
ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ); ESP_LOGI(TAG, "clk_speed %d", I2C_MASTER_FREQ_HZ);
conf.master.clk_speed = I2C_MASTER_FREQ_HZ; ESP_LOGI(TAG, "i2c_new_master_bus %d", I2C_MASTER_NUM);
ESP_LOGI(TAG, "i2c_param_config %d", conf.mode); ESP_ERROR_CHECK(i2c_new_master_bus(&bus_cfg, &i2c_bus));
ESP_ERROR_CHECK(i2c_param_config(I2C_MASTER_NUM, &conf)); }
ESP_LOGI(TAG, "i2c_driver_install %d", I2C_MASTER_NUM);
ESP_ERROR_CHECK(
i2c_driver_install(I2C_MASTER_NUM, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0));
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));
if (i2c_transfer_failed)
{ {
ESP_LOGW(TAG, "I2C tx buffer overflow (%zu + %d)", i2c_tx_len, arg_int);
i2c_transfer_failed = true;
break; break;
} }
data_ptr++; memcpy(&i2c_tx_buf[i2c_tx_len], data_ptr, arg_int);
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

@@ -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

@@ -114,18 +114,6 @@
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;
@@ -163,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

@@ -92,18 +92,18 @@
<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>
@@ -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>
<div class="password-toggle">
<input type="password" id="password" data-i18n-placeholder="wifi.password.placeholder" <input type="password" id="password" data-i18n-placeholder="wifi.password.placeholder"
placeholder="Passwort eingeben" autocomplete="off"> 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',
@@ -174,7 +175,8 @@ const translations = {
// General // General
'loading': 'Laden...', 'loading': 'Laden...',
'error': 'Fehler', 'error': 'Fehler',
'success': 'Erfolg' 'success': 'Erfolg',
'clock.suffix': 'Uhr'
}, },
en: { en: {
@@ -216,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',
@@ -349,7 +352,8 @@ const translations = {
// General // General
'loading': 'Loading...', 'loading': 'Loading...',
'error': 'Error', 'error': 'Error',
'success': 'Success' 'success': 'Success',
'clock.suffix': "o'clock"
} }
}; };

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

@@ -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

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

View File

@@ -0,0 +1,18 @@
// src/theme.ts
export function setTheme(theme: 'light' | 'dark') {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
const icon = document.getElementById('theme-icon');
const label = document.getElementById('theme-label');
const metaTheme = document.querySelector('meta[name="theme-color"]') as HTMLMetaElement | null;
if (icon) icon.textContent = theme === 'light' ? '☀️' : '🌙';
if (label) label.textContent = theme === 'light' ? 'Light' : 'Dark';
if (metaTheme) metaTheme.content = theme === 'light' ? '#f0f2f5' : '#1a1a2e';
}
export function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') as 'light' | 'dark' | null || 'dark';
setTheme(current === 'dark' ? 'light' : 'dark');
}

View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

View File

@@ -0,0 +1,17 @@
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import("@sveltejs/vite-plugin-svelte").SvelteConfig} */
export default {
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
// for more information about preprocessors
preprocess: vitePreprocess(),
compilerOptions: {
runes: true,
},
vitePlugin: {
inspector: {
showToggleButton: 'always',
toggleButtonPos: 'bottom-right'
}
}
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import { svelte } from '@sveltejs/vite-plugin-svelte'
import viteCompression from 'vite-plugin-compression';
export default defineConfig({
plugins: [
svelte(),
viteCompression()
],
build: {
outDir: '../storage/website',
assetsDir: '',
},
})

29
firmware/website/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# Paraglide
src/lib/paraglide
project.inlang/cache/
*storybook.log
storybook-static

1
firmware/website/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

View File

@@ -0,0 +1,9 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb
# Miscellaneous
/static/

View File

@@ -0,0 +1,16 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/routes/layout.css"
}

View File

@@ -0,0 +1,17 @@
import type { StorybookConfig } from '@storybook/sveltekit';
const config: StorybookConfig = {
"stories": [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|ts|svelte)"
],
"addons": [
"@storybook/addon-svelte-csf",
"@chromatic-com/storybook",
"@storybook/addon-vitest",
"@storybook/addon-a11y",
"@storybook/addon-docs"
],
"framework": "@storybook/sveltekit"
};
export default config;

View File

@@ -0,0 +1,21 @@
import type { Preview } from '@storybook/sveltekit'
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
a11y: {
// 'todo' - show a11y violations in the test UI only
// 'error' - fail CI on a11y violations
// 'off' - skip a11y checks entirely
test: 'todo'
}
},
};
export default preview;

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