31 Commits

Author SHA1 Message Date
684ce36270 add vite helper files?
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 5m4s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 7m51s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 1m56s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m50s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:33:13 +01:00
98b5df1ff2 add ignore for FreeCAD and node-js intermediate files
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Has been cancelled
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Has been cancelled
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Has been cancelled
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Has been cancelled
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:29:16 +01:00
8128b958cb Merge branch 'feature/website'
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-25 09:26:42 +01:00
955b4bef04 rebuild websites with svelte
still early WIP

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

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

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

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

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-15 00:36:19 +01:00
bccfb80791 show capative portal on connect
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 6m44s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m12s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m54s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 3m56s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-13 00:05:54 +01:00
b77fdee21d refactor persistence manager from cpp to c
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m12s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m17s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m49s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m0s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-10 11:12:31 +01:00
ef0cda1d67 update web ui
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 4m6s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 4m19s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m52s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 4m4s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-10 00:33:34 +01:00
cfca3f1535 mask password in log
Some checks failed
ESP-IDF Build / build (esp32c6, release-v5.4) (push) Failing after 3m21s
ESP-IDF Build / build (esp32c6, release-v5.5) (push) Failing after 5m51s
ESP-IDF Build / build (esp32s3, release-v5.4) (push) Failing after 3m6s
ESP-IDF Build / build (esp32s3, release-v5.5) (push) Failing after 3m26s
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-06 00:18:15 +01:00
d18c9bfea1 add capa endpoint
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-06 00:18:01 +01:00
28e991cf58 scan for WiFi and schon connection state (loading once)
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-06 00:08:03 +01:00
ebf0dc6556 show website via mdns
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-05 23:47:00 +01:00
105 changed files with 7575 additions and 924 deletions

2
.gitignore vendored
View File

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

2
firmware/.clangd Normal file
View File

@@ -0,0 +1,2 @@
CompileFlags:
Remove: [-f*, -m*]

View File

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

View File

@@ -1,4 +1,9 @@
cmake_minimum_required(VERSION 3.5)
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)
target_add_binary_data(${PROJECT_NAME}.elf "main/isrgrootx1.pem" TEXT)

View File

@@ -784,7 +784,7 @@ Sent when WiFi connection status changes.
## Static Files
The web interface files should be served from the `/storage/www/` directory:
The web interface files should be served from the `/spiffs/www/` directory:
| Path | Description |
|----------------------|------------------------------------------------|

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

@@ -0,0 +1,19 @@
idf_component_register(SRCS
src/api_server.c
src/common.c
src/api_handlers.c
src/websocket_handler.c
INCLUDE_DIRS "include"
REQUIRES
esp_http_server
mdns
esp_wifi
esp_netif
esp_event
json
led-manager
simulator
persistence-manager
message-manager
simulator
)

View File

@@ -0,0 +1,38 @@
menu "API Server Configuration"
config API_SERVER_HOSTNAME
string "mDNS Hostname"
default "system-control"
help
The mDNS hostname for the API server.
The device will be accessible at <hostname>.local
config API_SERVER_PORT
int "HTTP Server Port"
default 80
range 1 65535
help
The port number for the HTTP server.
config API_SERVER_MAX_WS_CLIENTS
int "Maximum WebSocket Clients"
default 4
range 1 8
help
Maximum number of concurrent WebSocket connections.
config API_SERVER_ENABLE_CORS
bool "Enable CORS"
default y
help
Enable Cross-Origin Resource Sharing (CORS) headers.
This is required for web interfaces served from different origins.
config API_SERVER_STATIC_FILES_PATH
string "Static Files Path"
default "/spiffs/www"
help
Base path for serving static web files.
endmenu

View File

@@ -0,0 +1,5 @@
dependencies:
idf:
version: '>=5.0.0'
espressif/mdns:
version: '*'

View File

@@ -0,0 +1,63 @@
#pragma once
#include <esp_http_server.h>
#ifdef __cplusplus
extern "C"
{
#endif
/**
* @brief Register all API handlers with the HTTP server
*
* @param server HTTP server handle
* @return esp_err_t ESP_OK on success
*/
esp_err_t api_handlers_register(httpd_handle_t server);
// Capabilities API
esp_err_t api_capabilities_get_handler(httpd_req_t *req);
// WiFi API
esp_err_t api_wifi_scan_handler(httpd_req_t *req);
esp_err_t api_wifi_config_handler(httpd_req_t *req);
esp_err_t api_wifi_status_handler(httpd_req_t *req);
// Light Control API
esp_err_t api_light_power_handler(httpd_req_t *req);
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_schema_handler(httpd_req_t *req);
esp_err_t api_light_status_handler(httpd_req_t *req);
// LED Configuration API
esp_err_t api_wled_config_get_handler(httpd_req_t *req);
esp_err_t api_wled_config_post_handler(httpd_req_t *req);
// Schema API
esp_err_t api_schema_get_handler(httpd_req_t *req);
esp_err_t api_schema_post_handler(httpd_req_t *req);
// Devices API (Matter)
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_paired_handler(httpd_req_t *req);
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_toggle_handler(httpd_req_t *req);
// Scenes API
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_delete_handler(httpd_req_t *req);
esp_err_t api_scenes_activate_handler(httpd_req_t *req);
// Static file serving
esp_err_t api_static_file_handler(httpd_req_t *req);
// Captive portal detection
esp_err_t api_captive_portal_handler(httpd_req_t *req);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,119 @@
#pragma once
#include <esp_err.h>
#include <esp_http_server.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C"
{
#endif
/**
* @brief Configuration for the API server
*/
typedef struct
{
const char *hostname; ///< mDNS hostname (default: "system-control")
uint16_t port; ///< HTTP server port (default: 80)
const char *base_path; ///< Base path for static files (default: "/storage/www")
bool enable_cors; ///< Enable CORS headers (default: true)
} api_server_config_t;
#ifdef CONFIG_API_SERVER_ENABLE_CORS
#define API_SERVER_ENABLE_CORS true
#else
#define API_SERVER_ENABLE_CORS false
#endif
/**
* @brief Default configuration for the API server
*/
#define API_SERVER_CONFIG_DEFAULT() \
{ \
.hostname = CONFIG_API_SERVER_HOSTNAME, \
.port = CONFIG_API_SERVER_PORT, \
.base_path = CONFIG_API_SERVER_STATIC_FILES_PATH, \
.enable_cors = API_SERVER_ENABLE_CORS, \
}
/**
* @brief Initialize and start the API server with mDNS
*
* This function starts an HTTP server with:
* - REST API endpoints
* - WebSocket endpoint at /ws
* - mDNS registration (system-control.local)
* - Static file serving from SPIFFS
*
* @param config Pointer to server configuration, or NULL for defaults
* @return esp_err_t ESP_OK on success
*/
esp_err_t api_server_start(const api_server_config_t *config);
/**
* @brief Stop the API server and mDNS
*
* @return esp_err_t ESP_OK on success
*/
esp_err_t api_server_stop(void);
/**
* @brief Check if the API server is running
*
* @return true if server is running
*/
bool api_server_is_running(void);
/**
* @brief Get the HTTP server handle
*
* @return httpd_handle_t Server handle, or NULL if not running
*/
httpd_handle_t api_server_get_handle(void);
/**
* @brief Broadcast a message to all connected WebSocket clients
*
* @param message JSON message to broadcast
* @return esp_err_t ESP_OK on success
*/
esp_err_t api_server_ws_broadcast(const char *message);
/**
* @brief Broadcast a status update to all WebSocket clients
*
* @param on Light power state
* @param mode Current mode (day/night/simulation)
* @param schema Active schema filename
* @param r Red value (0-255)
* @param g Green value (0-255)
* @param b Blue value (0-255)
* @return esp_err_t ESP_OK on success
*/
esp_err_t api_server_ws_broadcast_status(bool on, const char *mode, const char *schema, uint8_t r, uint8_t g,
uint8_t b);
/**
* @brief Broadcast a color update to all WebSocket clients
*
* @param r Red value (0-255)
* @param g Green value (0-255)
* @param b Blue value (0-255)
* @return esp_err_t ESP_OK on success
*/
esp_err_t api_server_ws_broadcast_color(uint8_t r, uint8_t g, uint8_t b);
/**
* @brief Broadcast a WiFi status update to all WebSocket clients
*
* @param connected Connection state
* @param ip IP address (can be NULL if not connected)
* @param rssi Signal strength
* @return esp_err_t ESP_OK on success
*/
esp_err_t api_server_ws_broadcast_wifi(bool connected, const char *ip, int rssi);
#ifdef __cplusplus
}
#endif

View File

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

View File

@@ -0,0 +1,60 @@
#pragma once
#include <esp_http_server.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C"
{
#endif
/**
* @brief Maximum number of concurrent WebSocket connections
*/
#define WS_MAX_CLIENTS CONFIG_API_SERVER_MAX_WS_CLIENTS
/**
* @brief Initialize WebSocket handler
*
* @param server HTTP server handle
* @return esp_err_t ESP_OK on success
*/
esp_err_t websocket_handler_init(httpd_handle_t server);
/**
* @brief WebSocket request handler
*
* @param req HTTP request
* @return esp_err_t ESP_OK on success
*/
esp_err_t websocket_handler(httpd_req_t *req);
/**
* @brief Send message to a specific WebSocket client
*
* @param server HTTP server handle
* @param fd Socket file descriptor
* @param message Message to send
* @return esp_err_t ESP_OK on success
*/
esp_err_t websocket_send(httpd_handle_t server, int fd, const char *message);
/**
* @brief Broadcast message to all connected WebSocket clients
*
* @param server HTTP server handle
* @param message Message to broadcast
* @return esp_err_t ESP_OK on success
*/
esp_err_t websocket_broadcast(httpd_handle_t server, const char *message);
/**
* @brief Get number of connected WebSocket clients
*
* @return int Number of connected clients
*/
int websocket_get_client_count(void);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
#include "api_server.h"
#include "api_handlers.h"
#include "websocket_handler.h"
#include "common.h"
#include "storage.h"
#include <esp_http_server.h>
#include <esp_log.h>
#include <mdns.h>
#include <string.h>
static const char *TAG = "api_server";
static httpd_handle_t s_server = NULL;
static api_server_config_t s_config = API_SERVER_CONFIG_DEFAULT();
static esp_err_t init_mdns(const char *hostname)
{
esp_err_t err = mdns_init();
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to initialize mDNS: %s", esp_err_to_name(err));
return err;
}
err = mdns_hostname_set(hostname);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set mDNS hostname: %s", esp_err_to_name(err));
return err;
}
err = mdns_instance_name_set("System Control");
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to set mDNS instance name: %s", esp_err_to_name(err));
return err;
}
// Add HTTP service
err = mdns_service_add("System Control Web Server", "_http", "_tcp", s_config.port, NULL, 0);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to add mDNS HTTP service: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "mDNS initialized: %s.local", hostname);
return ESP_OK;
}
static esp_err_t start_webserver(void)
{
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = s_config.port;
config.lru_purge_enable = true;
config.max_uri_handlers = 32;
config.max_open_sockets = 7;
config.uri_match_fn = httpd_uri_match_wildcard;
ESP_LOGI(TAG, "Starting HTTP server on port %d", config.server_port);
esp_err_t err = httpd_start(&s_server, &config);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(err));
return err;
}
// WebSocket-Handler explizit vor allen API-Handlern
err = websocket_handler_init(s_server);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to initialize WebSocket handler: %s", esp_err_to_name(err));
httpd_stop(s_server);
s_server = NULL;
return err;
}
// Register API handlers
err = api_handlers_register(s_server);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to register API handlers: %s", esp_err_to_name(err));
httpd_stop(s_server);
s_server = NULL;
return err;
}
// Common initialization
common_init();
ESP_LOGI(TAG, "HTTP server started successfully");
return ESP_OK;
}
esp_err_t api_server_start(const api_server_config_t *config)
{
if (s_server != NULL)
{
ESP_LOGW(TAG, "Server already running");
return ESP_ERR_INVALID_STATE;
}
if (config != NULL)
{
s_config = *config;
}
initialize_storage();
// Initialize mDNS
esp_err_t err = init_mdns(s_config.hostname);
if (err != ESP_OK)
{
return err;
}
// Start web server
err = start_webserver();
if (err != ESP_OK)
{
mdns_free();
return err;
}
return ESP_OK;
}
esp_err_t api_server_stop(void)
{
if (s_server == NULL)
{
ESP_LOGW(TAG, "Server not running");
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = httpd_stop(s_server);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to stop HTTP server: %s", esp_err_to_name(err));
return err;
}
s_server = NULL;
mdns_free();
ESP_LOGI(TAG, "Server stopped");
return ESP_OK;
}
bool api_server_is_running(void)
{
return s_server != NULL;
}
httpd_handle_t api_server_get_handle(void)
{
return s_server;
}
esp_err_t api_server_ws_broadcast(const char *message)
{
if (s_server == NULL)
{
return ESP_ERR_INVALID_STATE;
}
return websocket_broadcast(s_server, message);
}
esp_err_t api_server_ws_broadcast_status(bool on, const char *mode, const char *schema, uint8_t r, uint8_t g, uint8_t b)
{
char buffer[256];
snprintf(buffer, sizeof(buffer),
"{\"type\":\"status\",\"on\":%s,\"mode\":\"%s\",\"schema\":\"%s\","
"\"color\":{\"r\":%d,\"g\":%d,\"b\":%d}}",
on ? "true" : "false", mode, schema, r, g, b);
return api_server_ws_broadcast(buffer);
}
esp_err_t api_server_ws_broadcast_color(uint8_t r, uint8_t g, uint8_t b)
{
char buffer[64];
snprintf(buffer, sizeof(buffer), "{\"type\":\"color\",\"r\":%d,\"g\":%d,\"b\":%d}", r, g, b);
return api_server_ws_broadcast(buffer);
}
esp_err_t api_server_ws_broadcast_wifi(bool connected, const char *ip, int rssi)
{
char buffer[128];
if (connected && ip != NULL)
{
snprintf(buffer, sizeof(buffer), "{\"type\":\"wifi\",\"connected\":true,\"ip\":\"%s\",\"rssi\":%d}", ip, rssi);
}
else
{
snprintf(buffer, sizeof(buffer), "{\"type\":\"wifi\",\"connected\":false}");
}
return api_server_ws_broadcast(buffer);
}

View File

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

View File

@@ -0,0 +1,276 @@
#include "websocket_handler.h"
#include "api_server.h"
#include "common.h"
#include "message_manager.h"
#include <esp_http_server.h>
#include <esp_log.h>
#include <string.h>
static const char *TAG = "websocket_handler";
// Store connected WebSocket client file descriptors
static int ws_clients[WS_MAX_CLIENTS];
static int ws_client_count = 0;
static void on_message_received(const message_t *msg)
{
cJSON *json = create_light_status_json();
cJSON_AddStringToObject(json, "type", "status");
char *response = cJSON_PrintUnformatted(json);
cJSON_Delete(json);
api_server_ws_broadcast(response);
free(response);
}
static void ws_clients_init(void)
{
for (int i = 0; i < WS_MAX_CLIENTS; i++)
ws_clients[i] = -1;
}
// Add a client to the list
static bool add_client(int fd)
{
for (int i = 0; i < WS_MAX_CLIENTS; i++)
{
if (ws_clients[i] == -1)
{
ws_clients[i] = fd;
ws_client_count++;
ESP_LOGI(TAG, "WebSocket client connected: fd=%d (total: %d)", fd, ws_client_count);
return true;
}
}
ESP_LOGW(TAG, "Max WebSocket clients reached, cannot add fd=%d", fd);
return false;
}
// Remove a client from the list
static void remove_client(int fd)
{
for (int i = 0; i < WS_MAX_CLIENTS; i++)
{
if (ws_clients[i] == fd)
{
ws_clients[i] = -1;
ws_client_count--;
ESP_LOGI(TAG, "WebSocket client disconnected: fd=%d (total: %d)", fd, ws_client_count);
return;
}
}
}
// Handle incoming WebSocket message
static esp_err_t handle_ws_message(httpd_req_t *req, httpd_ws_frame_t *ws_pkt)
{
ESP_LOGI(TAG, "Received WS message: %s", (char *)ws_pkt->payload);
// Parse the message and handle different types
// For now, we just check if it's a status request
if (ws_pkt->payload != NULL && strstr((char *)ws_pkt->payload, "getStatus") != NULL)
{
// Status-JSON generieren
cJSON *json = create_light_status_json();
cJSON_AddStringToObject(json, "type", "status");
char *response = cJSON_PrintUnformatted(json);
cJSON_Delete(json);
httpd_ws_frame_t ws_resp = {.final = true,
.fragmented = false,
.type = HTTPD_WS_TYPE_TEXT,
.payload = (uint8_t *)response,
.len = strlen(response)};
esp_err_t ret = httpd_ws_send_frame(req, &ws_resp);
free(response);
return ret;
}
return ESP_OK;
}
esp_err_t websocket_handler(httpd_req_t *req)
{
if (req->method == HTTP_GET)
{
// This is the handshake
ESP_LOGI(TAG, "WebSocket handshake");
int fd = httpd_req_to_sockfd(req);
if (!add_client(fd))
{
// Zu viele Clients, Verbindung schließen
httpd_sess_trigger_close(req->handle, fd);
return ESP_FAIL;
}
return ESP_OK;
}
// Receive the frame
httpd_ws_frame_t ws_pkt;
memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t));
ws_pkt.type = HTTPD_WS_TYPE_TEXT;
// Get frame length first
esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len: %s", esp_err_to_name(ret));
return ret;
}
if (ws_pkt.len > 0)
{
// Allocate buffer for payload
ws_pkt.payload = malloc(ws_pkt.len + 1);
if (ws_pkt.payload == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory for WS payload");
return ESP_ERR_NO_MEM;
}
// Receive the payload
ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "httpd_ws_recv_frame failed: %s", esp_err_to_name(ret));
free(ws_pkt.payload);
return ret;
}
// Null-terminate the payload
ws_pkt.payload[ws_pkt.len] = '\0';
// Handle the message
ret = handle_ws_message(req, &ws_pkt);
free(ws_pkt.payload);
return ret;
}
// Handle close frame
if (ws_pkt.type == HTTPD_WS_TYPE_CLOSE)
{
int fd = httpd_req_to_sockfd(req);
remove_client(fd);
}
return ESP_OK;
}
// Async send structure
typedef struct
{
httpd_handle_t hd;
int fd;
char *message;
} ws_async_arg_t;
// Async send work function
static void ws_async_send(void *arg)
{
ws_async_arg_t *async_arg = (ws_async_arg_t *)arg;
httpd_ws_frame_t ws_pkt = {.final = true,
.fragmented = false,
.type = HTTPD_WS_TYPE_TEXT,
.payload = (uint8_t *)async_arg->message,
.len = strlen(async_arg->message)};
esp_err_t ret = httpd_ws_send_frame_async(async_arg->hd, async_arg->fd, &ws_pkt);
if (ret != ESP_OK)
{
ESP_LOGW(TAG, "Failed to send WS frame to fd=%d: %s", async_arg->fd, esp_err_to_name(ret));
// Remove client on error
remove_client(async_arg->fd);
}
free(async_arg->message);
free(async_arg);
}
esp_err_t websocket_handler_init(httpd_handle_t server)
{
message_manager_register_listener(on_message_received);
ws_clients_init();
// Register WebSocket URI handler
httpd_uri_t ws_uri = {.uri = "/ws",
.method = HTTP_GET,
.handler = websocket_handler,
.is_websocket = true,
.handle_ws_control_frames = true,
.user_ctx = NULL};
esp_err_t ret = httpd_register_uri_handler(server, &ws_uri);
if (ret != ESP_OK)
{
ESP_LOGE(TAG, "Failed to register WebSocket handler: %s", esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "WebSocket handler initialized at /ws");
return ESP_OK;
}
esp_err_t websocket_send(httpd_handle_t server, int fd, const char *message)
{
if (server == NULL || message == NULL)
{
return ESP_ERR_INVALID_ARG;
}
ws_async_arg_t *arg = malloc(sizeof(ws_async_arg_t));
if (arg == NULL)
{
return ESP_ERR_NO_MEM;
}
arg->hd = server;
arg->fd = fd;
arg->message = strdup(message);
if (arg->message == NULL)
{
free(arg);
return ESP_ERR_NO_MEM;
}
esp_err_t ret = httpd_queue_work(server, ws_async_send, arg);
if (ret != ESP_OK)
{
free(arg->message);
free(arg);
}
return ret;
}
esp_err_t websocket_broadcast(httpd_handle_t server, const char *message)
{
if (server == NULL || message == NULL)
{
return ESP_ERR_INVALID_ARG;
}
esp_err_t ret = ESP_OK;
for (int i = 0; i < WS_MAX_CLIENTS; i++)
{
if (ws_clients[i] != -1)
{
esp_err_t send_ret = websocket_send(server, ws_clients[i], message);
if (send_ret != ESP_OK)
{
ESP_LOGW(TAG, "Failed to queue WS message for fd=%d", ws_clients[i]);
ret = send_ret; // Return last error
}
}
}
return ret;
}
int websocket_get_client_count(void)
{
return ws_client_count;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,23 @@
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C"
{
#endif
void initialize_storage();
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
}
#endif

View File

@@ -1,8 +1,9 @@
#include "simulator.h"
#include "color.h"
#include "hal_esp32/PersistenceManager.h"
#include "led_strip_ws2812.h"
#include "message_manager.h"
#include "persistence_manager.h"
#include "storage.h"
#include <esp_heap_caps.h>
#include <esp_log.h>
@@ -15,12 +16,12 @@
#include <string.h>
static const char *TAG = "simulator";
static char *time;
static char *time = NULL;
static char *time_to_string(int hhmm)
{
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;
}
@@ -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)
{
// 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)
{
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;
}
@@ -145,10 +146,12 @@ static void initialize_light_items(void)
initialize_storage();
static char filename[30];
auto persistence = PersistenceManager();
int variant = persistence.GetValue("light_variant", 1);
snprintf(filename, sizeof(filename), "/spiffs/schema_%02d.csv", variant);
persistence_manager_t persistence;
persistence_manager_init(&persistence, "config");
int variant = persistence_manager_get_int(&persistence, "light_variant", 1);
snprintf(filename, sizeof(filename), "schema_%02d.csv", variant);
load_file(filename);
persistence_manager_deinit(&persistence);
if (head == NULL)
{
@@ -233,6 +236,17 @@ static light_item_node_t *find_next_light_item_for_time(int hhmm)
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)
{
initialize_light_items();
@@ -240,8 +254,9 @@ void start_simulate_day(void)
light_item_node_t *current_item = find_best_light_item_for_time(1200);
if (current_item != NULL)
{
led_strip_update(LED_STATE_DAY,
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
rgb_t color = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
led_strip_update(LED_STATE_DAY, color);
send_simulation_message("12:00", color);
}
}
@@ -252,8 +267,9 @@ void start_simulate_night(void)
light_item_node_t *current_item = find_best_light_item_for_time(0);
if (current_item != NULL)
{
led_strip_update(LED_STATE_NIGHT,
(rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
rgb_t color = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
led_strip_update(LED_STATE_NIGHT, color);
send_simulation_message("00:00", color);
}
}
@@ -294,7 +310,13 @@ void simulate_cycle(void *args)
light_item_node_t *current_item = find_best_light_item_for_time(hhmm);
light_item_node_t *next_item = find_next_light_item_for_time(hhmm);
if (current_item != NULL && next_item != NULL)
if (current_item != NULL)
{
rgb_t color = {0, 0, 0};
// Use head as fallback if next_item is NULL
next_item = next_item ? next_item : head;
if (next_item != NULL)
{
int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100);
int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
@@ -323,16 +345,16 @@ void simulate_cycle(void *args)
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);
color = interpolate_color(start_rgb, end_rgb, interpolation_factor);
led_strip_update(LED_STATE_SIMULATION, color);
}
else if (current_item != NULL)
else
{
// 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});
// 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);
}
vTaskDelay(pdMS_TO_TICKS(delay_ms));
@@ -351,7 +373,7 @@ void start_simulation_task(void)
stop_simulation_task();
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)
{
ESP_LOGE(TAG, "Failed to allocate memory for simulation config.");
@@ -398,25 +420,22 @@ void start_simulation(void)
{
stop_simulation_task();
auto persistence = PersistenceManager();
if (persistence.GetValue("light_active", false))
persistence_manager_t persistence;
persistence_manager_init(&persistence, "config");
if (persistence_manager_get_bool(&persistence, "light_active", false))
{
int mode = persistence.GetValue("light_mode", 0);
int mode = persistence_manager_get_int(&persistence, "light_mode", 0);
switch (mode)
{
case 0: // Simulation mode
start_simulation_task();
break;
case 1: // Day mode
start_simulate_day();
break;
case 2: // Night mode
start_simulate_night();
break;
default:
ESP_LOGW(TAG, "Unknown light mode: %d", mode);
break;
@@ -426,4 +445,5 @@ void start_simulation(void)
{
led_strip_update(LED_STATE_OFF, rgb_t{});
}
persistence_manager_deinit(&persistence);
}

View File

@@ -3,14 +3,22 @@
#include "esp_log.h"
#include "esp_spiffs.h"
#include "simulator.h"
#include <cstring>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char *TAG = "storage";
static bool is_spiffs_mounted = false;
void initialize_storage()
{
if (is_spiffs_mounted)
{
return;
}
esp_vfs_spiffs_conf_t conf = {
.base_path = "/spiffs",
.partition_label = NULL,
@@ -36,64 +44,113 @@ void initialize_storage()
}
return;
}
is_spiffs_mounted = true;
}
void load_file(const char *filename)
{
ESP_LOGI(TAG, "Loading file: %s", filename);
FILE *f = fopen(filename, "r");
if (f == NULL)
{
ESP_LOGE(TAG, "Failed to open file for reading");
return;
}
char line[128];
int line_count = 0;
char **lines = read_lines_filtered(filename, &line_count);
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};
int red, green, blue, white, brightness, saturation;
int items_scanned = sscanf(line, "%d,%d,%d,%d,%d,%d", &red, &green, &blue, &white, &brightness, &saturation);
int items_scanned =
sscanf(lines[i], "%d,%d,%d,%d,%d,%d", &red, &green, &blue, &white, &brightness, &saturation);
if (items_scanned == 6)
{
int total_minutes = line_number * 30;
int hours = total_minutes / 60;
int minutes = total_minutes % 60;
snprintf(time, sizeof(time), "%02d%02d", hours, minutes);
add_light_item(time, red, green, blue, white, brightness, saturation);
line_number++;
}
else
{
ESP_LOGW(TAG, "Could not parse line: %s", line);
ESP_LOGW(TAG, "Could not parse line: %s", lines[i]);
}
}
fclose(f);
free_lines(lines, line_count);
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

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

View File

@@ -3,6 +3,5 @@ dependencies:
git: https://github.com/olikraus/u8g2.git
# u8g2_hal:
# git: https://github.com/mkfrey/u8g2-hal-esp-idf.git
espressif/button: ^4.1.3
espressif/button: ^4.1.4
espressif/esp_insights: ^1.2.7
espressif/mqtt: ^1.0.0

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "u8g2_esp32_hal.h"
#include "hal/u8g2_esp32_hal.h"
static const char *TAG = "u8g2_hal";
static const unsigned int I2C_TIMEOUT_MS = 1000;

View File

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

View File

@@ -42,3 +42,6 @@ CONFIG_SPIRAM=y
# SPI RAM config
CONFIG_SPIRAM_SPEED=80
CONFIG_SPIRAM_USE_CAPS_ALLOC=y
# HTTP Server WebSocket Support
CONFIG_HTTPD_WS_SUPPORT=y

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -115,6 +115,20 @@ function updateSimulationOptions() {
} else {
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() {
@@ -173,8 +187,6 @@ async function loadLightStatus() {
// Update schema
if (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
@@ -184,6 +196,15 @@ async function loadLightStatus() {
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) {
console.log('Light status not available');

View File

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

View File

@@ -65,18 +65,32 @@ function updateStatusFromData(status) {
}
if (status.schema) {
document.getElementById('active-schema').value = status.schema;
const activeSchemaEl = document.getElementById('active-schema');
if (activeSchemaEl) {
activeSchemaEl.value = status.schema;
}
const schemaNames = {
'schema_01.csv': 'Schema 1',
'schema_02.csv': 'Schema 2',
'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) {
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) {

View File

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

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

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