18 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
142 changed files with 12342 additions and 389 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,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,7 +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 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

@@ -2,14 +2,19 @@
#include "common.h" #include "common.h"
#include "message_manager.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
@@ -59,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;
@@ -90,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");
@@ -132,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);
@@ -159,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);
@@ -245,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);
@@ -298,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);
@@ -309,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\"}");
} }
@@ -317,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);
@@ -328,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\"}");
} }
@@ -352,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\"}");
} }
@@ -384,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)
{ {
@@ -400,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\"}");
} }
@@ -457,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);
@@ -487,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);
@@ -506,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);
@@ -525,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);
@@ -577,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);
@@ -615,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);
@@ -671,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)
@@ -742,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);
@@ -751,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,7 +2,39 @@
#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 "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)
@@ -13,16 +45,35 @@ cJSON *create_light_status_json(void)
bool light_active = persistence_manager_get_bool(&pm, "light_active", false); bool light_active = persistence_manager_get_bool(&pm, "light_active", false);
cJSON_AddBoolToObject(json, "on", light_active); 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); 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

@@ -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>
@@ -59,6 +59,7 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t e
{ {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;
ESP_LOGI(TAG, "IP_EVENT_STA_GOT_IP: IP-Adresse erhalten: " IPSTR, IP2STR(&event->ip_info.ip)); 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); xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
} }
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP)

View File

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

@@ -126,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;

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

@@ -91,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;
} }
@@ -108,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;
} }
@@ -133,10 +139,20 @@ void LightMenu::onMessageReceived(const message_t *msg)
{ {
// Here you can react to messages, e.g. set toggle status // Here you can react to messages, e.g. set toggle status
// Example: If light_active was changed, synchronize toggle // Example: If light_active was changed, synchronize toggle
if (msg && msg->type == MESSAGE_TYPE_SETTINGS && msg->data.settings.type == SETTINGS_TYPE_BOOL && if (msg && msg->type == MESSAGE_TYPE_SETTINGS)
std::strcmp(msg->data.settings.key, "light_active") == 0)
{ {
setToggle(getItem(LightMenuItem::ACTIVATE), msg->data.settings.value.bool_value); 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);
}
} }
} }

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

@@ -3,4 +3,6 @@ idf_component_register(
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
PRIV_REQUIRES PRIV_REQUIRES
persistence-manager persistence-manager
my_mqtt_client
app_update
) )

View File

@@ -13,7 +13,8 @@ extern "C"
typedef enum typedef enum
{ {
MESSAGE_TYPE_SETTINGS, MESSAGE_TYPE_SETTINGS,
MESSAGE_TYPE_BUTTON MESSAGE_TYPE_BUTTON,
MESSAGE_TYPE_SIMULATION
} message_type_t; } message_type_t;
typedef enum typedef enum
@@ -48,12 +49,21 @@ extern "C"
} value; } value;
} settings_message_t; } settings_message_t;
typedef struct
{
char time[6];
uint8_t red;
uint8_t green;
uint8_t blue;
} simulation_message_t;
typedef struct typedef struct
{ {
message_type_t type; message_type_t type;
union { union {
settings_message_t settings; settings_message_t settings;
button_message_t button; button_message_t button;
simulation_message_t simulation;
} data; } data;
} message_t; } message_t;

View File

@@ -1,9 +1,14 @@
#include "message_manager.h" #include "message_manager.h"
#include "my_mqtt_client.h"
#include <esp_app_desc.h>
#include <esp_log.h> #include <esp_log.h>
#include <esp_mac.h>
#include <esp_system.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/queue.h> #include <freertos/queue.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <persistence_manager.h> #include <persistence_manager.h>
#include <sdkconfig.h>
#include <string.h> #include <string.h>
#define MESSAGE_QUEUE_LENGTH 16 #define MESSAGE_QUEUE_LENGTH 16
@@ -17,21 +22,29 @@ static QueueHandle_t message_queue = NULL;
static message_listener_t message_listeners[MAX_MESSAGE_LISTENERS] = {0}; static message_listener_t message_listeners[MAX_MESSAGE_LISTENERS] = {0};
static size_t message_listener_count = 0; static size_t message_listener_count = 0;
void message_manager_register_listener(message_listener_t listener) { void message_manager_register_listener(message_listener_t listener)
if (listener && message_listener_count < MAX_MESSAGE_LISTENERS) { {
if (listener && message_listener_count < MAX_MESSAGE_LISTENERS)
{
// Doppelte Registrierung vermeiden // Doppelte Registrierung vermeiden
for (size_t i = 0; i < message_listener_count; ++i) { for (size_t i = 0; i < message_listener_count; ++i)
if (message_listeners[i] == listener) return; {
if (message_listeners[i] == listener)
return;
} }
message_listeners[message_listener_count++] = listener; message_listeners[message_listener_count++] = listener;
} }
} }
void message_manager_unregister_listener(message_listener_t 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) { for (size_t i = 0; i < message_listener_count; ++i)
{
if (message_listeners[i] == listener)
{
// Nachfolgende Listener nach vorne schieben // Nachfolgende Listener nach vorne schieben
for (size_t j = i; j < message_listener_count - 1; ++j) { for (size_t j = i; j < message_listener_count - 1; ++j)
{
message_listeners[j] = message_listeners[j + 1]; message_listeners[j] = message_listeners[j + 1];
} }
message_listeners[--message_listener_count] = NULL; message_listeners[--message_listener_count] = NULL;
@@ -70,20 +83,35 @@ static void message_manager_task(void *param)
break; break;
} }
persistence_manager_deinit(&pm); persistence_manager_deinit(&pm);
ESP_LOGI(TAG, "Setting written: %s", msg.data.settings.key); ESP_LOGD(TAG, "Setting written: %s", msg.data.settings.key);
} }
break; break;
case MESSAGE_TYPE_BUTTON: case MESSAGE_TYPE_BUTTON:
ESP_LOGI(TAG, "Button event: id=%d, type=%d", msg.data.button.button_id, msg.data.button.event_type); ESP_LOGD(TAG, "Button event: id=%d, type=%d", msg.data.button.button_id, msg.data.button.event_type);
// TODO: Weiterverarbeitung/Callback für Button-Events 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; break;
} }
// Observer Pattern: Listener benachrichtigen // Observer Pattern: Listener benachrichtigen
for (size_t i = 0; i < message_listener_count; ++i) { for (size_t i = 0; i < message_listener_count; ++i)
if (message_listeners[i]) { {
if (message_listeners[i])
{
message_listeners[i](&msg); 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);
} }
} }
} }
@@ -101,6 +129,6 @@ bool message_manager_post(const message_t *msg, TickType_t timeout)
{ {
if (!message_queue) if (!message_queue)
return false; return false;
ESP_LOGI(TAG, "Post: type=%d", msg->type); ESP_LOGD(TAG, "Post: type=%d", msg->type);
return xQueueSend(message_queue, msg, timeout) == pdTRUE; 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

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

@@ -1,4 +1,3 @@
#include "persistence_manager.h" #include "persistence_manager.h"
#include <esp_log.h> #include <esp_log.h>
#include <string.h> #include <string.h>
@@ -27,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));
@@ -174,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))
@@ -245,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

@@ -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,45 +310,51 @@ void simulate_cycle(void *args)
light_item_node_t *current_item = find_best_light_item_for_time(hhmm); light_item_node_t *current_item = find_best_light_item_for_time(hhmm);
light_item_node_t *next_item = find_next_light_item_for_time(hhmm); light_item_node_t *next_item = find_next_light_item_for_time(hhmm);
if (current_item != NULL && next_item != NULL) if (current_item != NULL)
{ {
int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100); rgb_t color = {0, 0, 0};
int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
if (next_item_time_min < current_item_time_min) // Use head as fallback if next_item is NULL
next_item = next_item ? next_item : head;
if (next_item != NULL)
{ {
next_item_time_min += total_minutes_in_day; int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100);
} int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
int minutes_since_current_item_start = current_minute_of_day - current_item_time_min; if (next_item_time_min < current_item_time_min)
if (minutes_since_current_item_start < 0) {
next_item_time_min += total_minutes_in_day;
}
int minutes_since_current_item_start = current_minute_of_day - current_item_time_min;
if (minutes_since_current_item_start < 0)
{
minutes_since_current_item_start += total_minutes_in_day;
}
int interval_duration = next_item_time_min - current_item_time_min;
if (interval_duration == 0)
{
interval_duration = 1;
}
float interpolation_factor = (float)minutes_since_current_item_start / (float)interval_duration;
// Prepare colors for interpolation
rgb_t start_rgb = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
// Use the interpolation function
color = interpolate_color(start_rgb, end_rgb, interpolation_factor);
led_strip_update(LED_STATE_SIMULATION, color);
}
else
{ {
minutes_since_current_item_start += total_minutes_in_day; // No next_item and no head, use only current
color = (rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
led_strip_update(LED_STATE_SIMULATION, color);
} }
send_simulation_message(time, color);
int interval_duration = next_item_time_min - current_item_time_min;
if (interval_duration == 0)
{
interval_duration = 1;
}
float interpolation_factor = (float)minutes_since_current_item_start / (float)interval_duration;
// Prepare colors for interpolation
rgb_t start_rgb = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
// Use the interpolation function
rgb_t final_rgb = interpolate_color(start_rgb, end_rgb, interpolation_factor);
led_strip_update(LED_STATE_SIMULATION, final_rgb);
}
else if (current_item != NULL)
{
// No next item, just use current
led_strip_update(
LED_STATE_SIMULATION,
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
} }
vTaskDelay(pdMS_TO_TICKS(delay_ms)); vTaskDelay(pdMS_TO_TICKS(delay_ms));
@@ -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

@@ -8,6 +8,7 @@
#include "i2c_checker.h" #include "i2c_checker.h"
#include "led_status.h" #include "led_status.h"
#include "message_manager.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"
@@ -36,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;
@@ -130,8 +131,10 @@ static void init_ui(void)
static void on_message_received(const message_t *msg) static void on_message_received(const message_t *msg)
{ {
if (msg && msg->type == MESSAGE_TYPE_SETTINGS && msg->data.settings.type == SETTINGS_TYPE_BOOL && if (msg && msg->type == MESSAGE_TYPE_SETTINGS &&
std::strcmp(msg->data.settings.key, "light_active") == 0) (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(); start_simulation();
} }
@@ -194,10 +197,10 @@ void app_task(void *args)
return; return;
} }
// Display initialisieren, damit Info angezeigt werden kann // Initialize display so that info can be shown
setup_screen(); setup_screen();
// BACK-Button prüfen und ggf. Einstellungen löschen (mit Countdown) // Check BACK button and delete settings if necessary (with countdown)
gpio_config_t io_conf = {}; gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_DISABLE; io_conf.intr_type = GPIO_INTR_DISABLE;
io_conf.mode = GPIO_MODE_INPUT; io_conf.mode = GPIO_MODE_INPUT;
@@ -222,7 +225,7 @@ void app_task(void *args)
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
if (gpio_get_level(BUTTON_BACK) != 0) if (gpio_get_level(BUTTON_BACK) != 0)
{ {
// Button losgelassen, abbrechen // Button released, abort
break; break;
} }
if (i == 1) if (i == 1)
@@ -250,6 +253,8 @@ void app_task(void *args)
wifi_manager_init(); wifi_manager_init();
mqtt_client_start();
message_manager_register_listener(on_message_received); message_manager_register_listener(on_message_received);
start_simulation(); start_simulation();

View File

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

View File

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

View File

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

@@ -557,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 {

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>

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

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

View File

@@ -0,0 +1,7 @@
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
import { setProjectAnnotations } from '@storybook/sveltekit';
import * as projectAnnotations from './preview';
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);

View File

@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}

View File

@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv create --template minimal --types ts --add prettier eslint vitest="usages:unit,component" tailwindcss="plugins:typography" storybook paraglide="languageTags:en, de+demo:yes" --install npm website
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View File

@@ -0,0 +1,42 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import storybook from 'eslint-plugin-storybook';
import prettier from 'eslint-config-prettier';
import path from 'node:path';
import { includeIgnoreFile } from '@eslint/compat';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import { defineConfig } from 'eslint/config';
import globals from 'globals';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
export default defineConfig(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
rules: {
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off'
}
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from de!"
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"hello_world": "Hello, {name} from en!"
}

6278
firmware/website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
{
"name": "website",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:unit": "vitest",
"test": "npm run test:unit -- --run",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@eslint/compat": "^2.0.2",
"@eslint/js": "^9.39.2",
"@inlang/paraglide-js": "^2.10.0",
"@storybook/addon-a11y": "^10.2.8",
"@storybook/addon-docs": "^10.2.8",
"@storybook/addon-svelte-csf": "^5.0.11",
"@storybook/addon-vitest": "^10.2.8",
"@storybook/sveltekit": "^10.2.8",
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^24",
"@vitest/browser-playwright": "^4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-storybook": "^10.2.8",
"eslint-plugin-svelte": "^3.14.0",
"globals": "^17.3.0",
"playwright": "^1.58.1",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"storybook": "^10.2.8",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1",
"vitest": "^4.0.18",
"vitest-browser-svelte": "^2.0.2"
}
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./messages/{locale}.json"
},
"baseLocale": "en",
"locales": ["en", "de"]
}

13
firmware/website/src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="%paraglide.lang%">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

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