diff --git a/firmware/.clangd b/firmware/.clangd
new file mode 100644
index 0000000..437f255
--- /dev/null
+++ b/firmware/.clangd
@@ -0,0 +1,2 @@
+CompileFlags:
+ Remove: [-f*, -m*]
diff --git a/firmware/.devcontainer/Dockerfile b/firmware/.devcontainer/Dockerfile
index dafb8ad..2534683 100644
--- a/firmware/.devcontainer/Dockerfile
+++ b/firmware/.devcontainer/Dockerfile
@@ -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
diff --git a/firmware/CMakeLists.txt b/firmware/CMakeLists.txt
index 0fd6d96..b543594 100755
--- a/firmware/CMakeLists.txt
+++ b/firmware/CMakeLists.txt
@@ -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)
\ No newline at end of file
diff --git a/firmware/README-API.md b/firmware/README-API.md
new file mode 100644
index 0000000..277ab86
--- /dev/null
+++ b/firmware/README-API.md
@@ -0,0 +1,859 @@
+# System Control - API Documentation
+
+This document describes all REST API endpoints and WebSocket messages required for the ESP32 firmware implementation.
+
+## Table of Contents
+
+- [REST API Endpoints](#rest-api-endpoints)
+ - [Capabilities](#capabilities)
+ - [WiFi](#wifi)
+ - [Light Control](#light-control)
+ - [LED Configuration](#led-configuration)
+ - [Schema](#schema)
+ - [Devices](#devices)
+ - [Scenes](#scenes)
+- [WebSocket](#websocket)
+ - [Connection](#connection)
+ - [Client to Server Messages](#client-to-server-messages)
+ - [Server to Client Messages](#server-to-client-messages)
+
+---
+
+## REST API Endpoints
+
+### Capabilities
+
+#### Get Device Capabilities
+
+Returns the device capabilities. Used to determine which features are available.
+
+- **URL:** `/api/capabilities`
+- **Method:** `GET`
+- **Response:**
+
+```json
+{
+ "thread": true
+}
+```
+
+| Field | Type | Description |
+|--------|---------|-------------------------------------------------------|
+| thread | boolean | Whether Thread/Matter features are enabled |
+
+**Notes:**
+- If `thread` is `true`, the UI shows Matter device management and Scenes
+- If `thread` is `false` or the endpoint is unavailable, these features are hidden
+- The client can also force-enable features via URL parameter `?thread=true`
+
+---
+
+### WiFi
+
+#### Scan Networks
+
+Scans for available WiFi networks.
+
+- **URL:** `/api/wifi/scan`
+- **Method:** `GET`
+- **Response:**
+
+```json
+[
+ {
+ "ssid": "NetworkName",
+ "rssi": -45
+ },
+ {
+ "ssid": "AnotherNetwork",
+ "rssi": -72
+ }
+]
+```
+
+| Field | Type | Description |
+|-------|--------|------------------------------------|
+| ssid | string | Network name (SSID) |
+| rssi | number | Signal strength in dBm |
+
+---
+
+#### Save WiFi Configuration
+
+Saves WiFi credentials and initiates connection.
+
+- **URL:** `/api/wifi/config`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "ssid": "NetworkName",
+ "password": "SecretPassword123"
+}
+```
+
+| Field | Type | Required | Description |
+|----------|--------|----------|-------------------|
+| ssid | string | Yes | Network name |
+| password | string | No | Network password |
+
+- **Response:** `200 OK` on success, `400 Bad Request` on error
+
+---
+
+#### Get WiFi Status
+
+Returns current WiFi connection status.
+
+- **URL:** `/api/wifi/status`
+- **Method:** `GET`
+- **Response:**
+
+```json
+{
+ "connected": true,
+ "ssid": "NetworkName",
+ "ip": "192.168.1.100",
+ "rssi": -45
+}
+```
+
+| Field | Type | Description |
+|-----------|---------|------------------------------------------|
+| connected | boolean | Whether connected to a network |
+| ssid | string | Connected network name (if connected) |
+| ip | string | Assigned IP address (if connected) |
+| rssi | number | Signal strength in dBm (if connected) |
+
+---
+
+### Light Control
+
+#### Set Light Power
+
+Turns the main light on or off.
+
+- **URL:** `/api/light/power`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "on": true
+}
+```
+
+| Field | Type | Required | Description |
+|-------|---------|----------|--------------------------|
+| on | boolean | Yes | `true` = on, `false` = off |
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Set Thunder Effect
+
+Turns the thunder/lightning effect on or off.
+
+- **URL:** `/api/light/thunder`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "on": true
+}
+```
+
+| Field | Type | Required | Description |
+|-------|---------|----------|--------------------------------|
+| on | boolean | Yes | `true` = on, `false` = off |
+
+- **Response:** `200 OK` on success
+
+**Notes:**
+- When enabled, random lightning flashes are triggered
+- Can be combined with any light mode
+- Thunder effect stops automatically when light is turned off
+
+---
+
+#### Set Light Mode
+
+Sets the lighting mode.
+
+- **URL:** `/api/light/mode`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "mode": "simulation"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|--------|----------|------------------------------------------------|
+| mode | string | Yes | One of: `day`, `night`, `simulation` |
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Set Active Schema
+
+Sets the active schema for simulation mode.
+
+- **URL:** `/api/light/schema`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "schema": "schema_01.csv"
+}
+```
+
+| Field | Type | Required | Description |
+|--------|--------|----------|-------------------------------------------------------|
+| schema | string | Yes | Schema filename: `schema_01.csv`, `schema_02.csv`, etc. |
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Get Light Status
+
+Returns current light status (alternative to WebSocket).
+
+- **URL:** `/api/light/status`
+- **Method:** `GET`
+- **Response:**
+
+```json
+{
+ "on": true,
+ "thunder": false,
+ "mode": "simulation",
+ "schema": "schema_01.csv",
+ "color": {
+ "r": 255,
+ "g": 240,
+ "b": 220
+ }
+}
+```
+
+| Field | Type | Description |
+|---------|---------|--------------------------------------|
+| on | boolean | Current power state |
+| thunder | boolean | Current thunder effect state |
+| mode | string | Current mode (day/night/simulation) |
+| schema | string | Active schema filename |
+| color | object | Current RGB color being displayed |
+
+---
+
+### LED Configuration
+
+#### Get LED Configuration
+
+Returns the current LED segment configuration.
+
+- **URL:** `/api/wled/config`
+- **Method:** `GET`
+- **Response:**
+
+```json
+{
+ "segments": [
+ {
+ "name": "Main Light",
+ "start": 0,
+ "leds": 60
+ },
+ {
+ "name": "Accent Light",
+ "start": 60,
+ "leds": 30
+ }
+ ]
+}
+```
+
+| Field | Type | Description |
+|--------------------|--------|------------------------------------------|
+| segments | array | List of LED segments |
+| segments[].name | string | Optional segment name |
+| segments[].start | number | Start LED index (0-based) |
+| segments[].leds | number | Number of LEDs in this segment |
+
+---
+
+#### Save LED Configuration
+
+Saves the LED segment configuration.
+
+- **URL:** `/api/wled/config`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "segments": [
+ {
+ "name": "Main Light",
+ "start": 0,
+ "leds": 60
+ },
+ {
+ "name": "Accent Light",
+ "start": 60,
+ "leds": 30
+ }
+ ]
+}
+```
+
+| Field | Type | Required | Description |
+|--------------------|--------|----------|------------------------------------------|
+| segments | array | Yes | List of LED segments (can be empty) |
+| segments[].name | string | No | Optional segment name |
+| segments[].start | number | Yes | Start LED index (0-based) |
+| segments[].leds | number | Yes | Number of LEDs in this segment |
+
+- **Response:** `200 OK` on success, `400 Bad Request` on validation error
+
+**Notes:**
+- Segments define how the LED strip is divided into logical groups
+- Changes are persisted to NVS (non-volatile storage)
+- Each segment can be controlled independently in the light schema
+
+---
+
+### Schema
+
+#### Load Schema
+
+Loads a schema file.
+
+- **URL:** `/api/schema/{filename}`
+- **Method:** `GET`
+- **URL Parameters:**
+ - `filename`: Schema file name (e.g., `schema_01.csv`)
+- **Response:** CSV text data
+
+```
+255,240,220,0,100,250
+255,230,200,0,120,250
+...
+```
+
+The CSV format has 48 rows (one per 30-minute interval) with 6 values per row:
+
+| Column | Description | Range |
+|--------|--------------------------------|---------|
+| 1 | Red (R) | 0-255 |
+| 2 | Green (G) | 0-255 |
+| 3 | Blue (B) | 0-255 |
+| 4 | Value 1 (V1) - custom value | 0-255 |
+| 5 | Value 2 (V2) - custom value | 0-255 |
+| 6 | Value 3 (V3) - custom value | 0-255 |
+
+---
+
+#### Save Schema
+
+Saves a schema file.
+
+- **URL:** `/api/schema/{filename}`
+- **Method:** `POST`
+- **Content-Type:** `text/csv`
+- **URL Parameters:**
+ - `filename`: Schema file name (e.g., `schema_01.csv`)
+- **Request Body:** CSV text data (same format as above)
+- **Response:** `200 OK` on success
+
+---
+
+### Devices
+
+#### Scan for Devices
+
+Scans for available Matter devices to pair.
+
+- **URL:** `/api/devices/scan`
+- **Method:** `GET`
+- **Response:**
+
+```json
+[
+ {
+ "id": "matter-001",
+ "type": "light",
+ "name": "Matter Lamp"
+ },
+ {
+ "id": "matter-002",
+ "type": "sensor",
+ "name": "Temperature Sensor"
+ }
+]
+```
+
+| Field | Type | Description |
+|-------|--------|-----------------------------------------------|
+| id | string | Unique device identifier |
+| type | string | Device type: `light`, `sensor`, `unknown` |
+| name | string | Device name (can be empty) |
+
+---
+
+#### Pair Device
+
+Pairs a discovered device.
+
+- **URL:** `/api/devices/pair`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "id": "matter-001",
+ "name": "Living Room Lamp"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|--------|----------|------------------------------|
+| id | string | Yes | Device ID from scan |
+| name | string | Yes | User-defined device name |
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Get Paired Devices
+
+Returns list of all paired devices.
+
+- **URL:** `/api/devices/paired`
+- **Method:** `GET`
+- **Response:**
+
+```json
+[
+ {
+ "id": "matter-001",
+ "type": "light",
+ "name": "Living Room Lamp"
+ }
+]
+```
+
+| Field | Type | Description |
+|-------|--------|-------------------------------------------|
+| id | string | Unique device identifier |
+| type | string | Device type: `light`, `sensor`, `unknown` |
+| name | string | User-defined device name |
+
+---
+
+#### Update Device Name
+
+Updates the name of a paired device.
+
+- **URL:** `/api/devices/update`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "id": "matter-001",
+ "name": "New Device Name"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|--------|----------|------------------------|
+| id | string | Yes | Device ID |
+| name | string | Yes | New device name |
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Unpair Device
+
+Removes a paired device.
+
+- **URL:** `/api/devices/unpair`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "id": "matter-001"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|--------|----------|---------------|
+| id | string | Yes | Device ID |
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Toggle Device
+
+Toggles a device (e.g., light on/off).
+
+- **URL:** `/api/devices/toggle`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "id": "matter-001"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|--------|----------|---------------|
+| id | string | Yes | Device ID |
+
+- **Response:** `200 OK` on success
+
+---
+
+### Scenes
+
+#### Get All Scenes
+
+Returns all configured scenes.
+
+- **URL:** `/api/scenes`
+- **Method:** `GET`
+- **Response:**
+
+```json
+[
+ {
+ "id": "scene-1",
+ "name": "Evening Mood",
+ "icon": "π ",
+ "actions": {
+ "light": "on",
+ "mode": "simulation",
+ "schema": "schema_02.csv",
+ "devices": [
+ {
+ "id": "matter-001",
+ "state": "on"
+ }
+ ]
+ }
+ },
+ {
+ "id": "scene-2",
+ "name": "Night Mode",
+ "icon": "π",
+ "actions": {
+ "light": "on",
+ "mode": "night"
+ }
+ }
+]
+```
+
+**Scene Object:**
+
+| Field | Type | Description |
+|---------|--------|------------------------------------|
+| id | string | Unique scene identifier |
+| name | string | Scene name |
+| icon | string | Emoji icon for the scene |
+| actions | object | Actions to execute (see below) |
+
+**Actions Object:**
+
+| Field | Type | Optional | Description |
+|---------|--------|----------|------------------------------------------|
+| light | string | Yes | `"on"` or `"off"` |
+| mode | string | Yes | `"day"`, `"night"`, or `"simulation"` |
+| schema | string | Yes | Schema filename (e.g., `schema_01.csv`) |
+| devices | array | Yes | Array of device actions (see below) |
+
+**Device Action Object:**
+
+| Field | Type | Description |
+|-------|--------|----------------------------------|
+| id | string | Device ID |
+| state | string | `"on"` or `"off"` |
+
+---
+
+#### Create/Update Scene
+
+Creates a new scene or updates an existing one.
+
+- **URL:** `/api/scenes`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "id": "scene-1",
+ "name": "Evening Mood",
+ "icon": "π ",
+ "actions": {
+ "light": "on",
+ "mode": "simulation",
+ "schema": "schema_02.csv",
+ "devices": [
+ {
+ "id": "matter-001",
+ "state": "on"
+ }
+ ]
+ }
+}
+```
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Delete Scene
+
+Deletes a scene.
+
+- **URL:** `/api/scenes`
+- **Method:** `DELETE`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "id": "scene-1"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|--------|----------|---------------|
+| id | string | Yes | Scene ID |
+
+- **Response:** `200 OK` on success
+
+---
+
+#### Activate Scene
+
+Executes all actions of a scene.
+
+- **URL:** `/api/scenes/activate`
+- **Method:** `POST`
+- **Content-Type:** `application/json`
+- **Request Body:**
+
+```json
+{
+ "id": "scene-1"
+}
+```
+
+| Field | Type | Required | Description |
+|-------|--------|----------|---------------|
+| id | string | Yes | Scene ID |
+
+- **Response:** `200 OK` on success
+
+---
+
+## WebSocket
+
+### Connection
+
+- **URL:** `ws://{host}/ws` or `wss://{host}/ws`
+- **Protocol:** JSON messages
+
+The WebSocket connection is used for real-time status updates. The client should reconnect automatically if the connection is lost (recommended: 3 second delay).
+
+---
+
+### Client to Server Messages
+
+#### Request Status
+
+Requests the current system status.
+
+```json
+{
+ "type": "getStatus"
+}
+```
+
+---
+
+### Server to Client Messages
+
+#### Status Update
+
+Sent in response to `getStatus` or when status changes.
+
+```json
+{
+ "type": "status",
+ "on": true,
+ "mode": "simulation",
+ "schema": "schema_01.csv",
+ "color": {
+ "r": 255,
+ "g": 240,
+ "b": 220
+ }
+}
+```
+
+| Field | Type | Description |
+|--------|---------|--------------------------------------|
+| type | string | Always `"status"` |
+| on | boolean | Light power state |
+| mode | string | Current mode (day/night/simulation) |
+| schema | string | Active schema filename |
+| color | object | Current RGB color (optional) |
+
+---
+
+#### Color Update
+
+Sent when the current color changes (during simulation).
+
+```json
+{
+ "type": "color",
+ "r": 255,
+ "g": 200,
+ "b": 150
+}
+```
+
+| Field | Type | Description |
+|-------|--------|--------------------------|
+| type | string | Always `"color"` |
+| r | number | Red value (0-255) |
+| g | number | Green value (0-255) |
+| b | number | Blue value (0-255) |
+
+---
+
+#### WiFi Status Update
+
+Sent when WiFi connection status changes.
+
+```json
+{
+ "type": "wifi",
+ "connected": true,
+ "ip": "192.168.1.100",
+ "rssi": -45
+}
+```
+
+| Field | Type | Description |
+|-----------|---------|---------------------------------------|
+| type | string | Always `"wifi"` |
+| connected | boolean | Whether connected to a network |
+| ip | string | Assigned IP address (if connected) |
+| rssi | number | Signal strength in dBm (if connected) |
+
+---
+
+## Static Files
+
+The web interface files should be served from the `/spiffs/www/` directory:
+
+| Path | Description |
+|----------------------|------------------------------------------------|
+| `/` | Serves `index.html` (or `captive.html` in AP mode) |
+| `/index.html` | Main HTML file (full interface) |
+| `/captive.html` | Captive portal (WiFi setup only) |
+| `/css/shared.css` | Shared styles for all pages |
+| `/css/index.css` | Styles for main interface |
+| `/css/captive.css` | Styles for captive portal |
+| `/js/wifi-shared.js` | Shared WiFi configuration logic |
+| `/js/*.js` | JavaScript modules |
+
+---
+
+## Captive Portal
+
+When the ESP32 is in Access Point (AP) mode (no WiFi configured or connection failed), it should serve the captive portal:
+
+### Behavior
+
+1. **AP Mode Activation:**
+ - ESP32 creates an access point (e.g., "marklin-setup")
+ - DNS server redirects all requests to the ESP32's IP (captive portal detection)
+
+2. **Captive Portal Detection:**
+ - Respond to common captive portal detection URLs:
+ - `/generate_204` (Android)
+ - `/hotspot-detect.html` (Apple)
+ - `/connecttest.txt` (Windows)
+ - Return redirect or serve `captive.html`
+
+3. **Serving Files in AP Mode:**
+ - `/` β `captive.html`
+ - `/captive.html` β Captive portal page
+ - `/js/wifi-shared.js` β WiFi functions
+ - API endpoints remain the same (`/api/wifi/*`)
+
+4. **After Successful Configuration:**
+ - ESP32 attempts to connect to the configured network
+ - If successful, switch to Station mode and serve `index.html`
+ - If failed, remain in AP mode
+
+### Recommended AP Settings
+
+| Setting | Value |
+|---------------|--------------------------|
+| SSID | `marklin-setup` |
+| Password | None (open) or `marklin` |
+| IP Address | `192.168.4.1` |
+| Gateway | `192.168.4.1` |
+| Subnet | `255.255.255.0` |
+
+---
+
+## Error Handling
+
+All endpoints should return appropriate HTTP status codes:
+
+| Code | Description |
+|------|---------------------------------------|
+| 200 | Success |
+| 400 | Bad Request (invalid input) |
+| 404 | Not Found (resource doesn't exist) |
+| 500 | Internal Server Error |
+
+Error responses should include a JSON body with an error message:
+
+```json
+{
+ "error": "Description of what went wrong"
+}
+```
diff --git a/firmware/README-captive.md b/firmware/README-captive.md
new file mode 100644
index 0000000..4b4d8a5
--- /dev/null
+++ b/firmware/README-captive.md
@@ -0,0 +1,209 @@
+# Captive Portal Implementation Guide
+
+This document describes how to implement the captive portal functionality on the ESP32 side to work with `captive.html`.
+
+## Overview
+
+When the ESP32 has no WiFi credentials stored (or connection fails), it should start in Access Point (AP) mode and serve a captive portal that allows users to configure WiFi settings.
+
+## How Captive Portal Detection Works
+
+Operating systems automatically send HTTP requests to known URLs to check for internet connectivity:
+
+| OS | Detection URL | Expected Response |
+|---|---|---|
+| **iOS/macOS** | `http://captive.apple.com/hotspot-detect.html` | `
SuccessSuccess` |
+| **Android** | `http://connectivitycheck.gstatic.com/generate_204` | HTTP 204 No Content |
+| **Windows** | `http://www.msftconnecttest.com/connecttest.txt` | `Microsoft Connect Test` |
+
+If the response doesn't match, the OS assumes there's a captive portal and opens a browser.
+
+## ESP32 Implementation Steps
+
+### 1. Start Access Point Mode
+
+```c
+wifi_config_t ap_config = {
+ .ap = {
+ .ssid = "SystemControl-Setup",
+ .ssid_len = 0,
+ .password = "", // Open network for easy access
+ .max_connection = 4,
+ .authmode = WIFI_AUTH_OPEN
+ }
+};
+esp_wifi_set_mode(WIFI_MODE_AP);
+esp_wifi_set_config(WIFI_IF_AP, &ap_config);
+esp_wifi_start();
+```
+
+### 2. Start DNS Server (DNS Hijacking)
+
+Redirect ALL DNS queries to the ESP32's IP address:
+
+```c
+// Simplified example - use a proper DNS server component
+void dns_server_task(void *pvParameters) {
+ // Listen on UDP port 53
+ // For any DNS query, respond with ESP32's AP IP (e.g., 192.168.4.1)
+}
+```
+
+### 3. Configure HTTP Server with Redirects
+
+```c
+// Handler for captive portal detection URLs
+esp_err_t captive_redirect_handler(httpd_req_t *req) {
+ httpd_resp_set_status(req, "302 Found");
+ httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/captive.html");
+ httpd_resp_send(req, NULL, 0);
+ return ESP_OK;
+}
+
+// Register handlers for detection URLs
+httpd_uri_t apple_detect = {
+ .uri = "/hotspot-detect.html",
+ .method = HTTP_GET,
+ .handler = captive_redirect_handler
+};
+
+httpd_uri_t android_detect = {
+ .uri = "/generate_204",
+ .method = HTTP_GET,
+ .handler = captive_redirect_handler
+};
+
+// Catch-all for any unknown paths
+httpd_uri_t catch_all = {
+ .uri = "/*",
+ .method = HTTP_GET,
+ .handler = captive_redirect_handler
+};
+```
+
+### 4. Serve Static Files
+
+Serve the captive portal files from SPIFFS/LittleFS:
+
+- `/captive.html` - Main captive portal page
+- `/favicon.svg` - Favicon
+- `/css/shared.css` - Shared styles
+- `/css/captive.css` - Captive-specific styles
+- `/js/i18n.js` - Internationalization
+- `/js/wifi-shared.js` - WiFi configuration logic
+
+### 5. Implement WiFi Configuration API
+
+```c
+// POST /api/wifi/config
+// Body: { "ssid": "NetworkName", "password": "SecretPassword" }
+esp_err_t wifi_config_handler(httpd_req_t *req) {
+ // 1. Parse JSON body
+ // 2. Store credentials in NVS
+ // 3. Send success response
+ // 4. Schedule restart/reconnect
+ return ESP_OK;
+}
+
+// GET /api/wifi/scan
+// Returns: [{ "ssid": "Network1", "rssi": -45 }, ...]
+esp_err_t wifi_scan_handler(httpd_req_t *req) {
+ // 1. Perform WiFi scan
+ // 2. Return JSON array of networks
+ return ESP_OK;
+}
+```
+
+## Flow After User Submits WiFi Credentials
+
+```
+1. User enters SSID + Password, clicks "Connect"
+ β
+2. Frontend sends POST /api/wifi/config
+ β
+3. ESP32 stores credentials in NVS (Non-Volatile Storage)
+ β
+4. ESP32 sends HTTP 200 OK response
+ β
+5. Frontend shows countdown (10 seconds)
+ β
+6. ESP32 stops AP mode
+ β
+7. ESP32 connects to configured WiFi
+ β
+8. ESP32 gets new IP from router (e.g., 192.168.1.42)
+ β
+9. User connects phone/PC to normal WiFi
+ β
+10. User accesses ESP32 via new IP or mDNS (e.g., http://system-control.local)
+```
+
+## Recommended: mDNS Support
+
+Register an mDNS hostname so users can access the device without knowing the IP:
+
+```c
+mdns_init();
+mdns_hostname_set("system-control");
+mdns_instance_name_set("System Control");
+```
+
+Then the device is accessible at: `http://system-control.local`
+
+## Error Handling / Fallback
+
+If WiFi connection fails after credentials are saved:
+
+1. Wait for connection timeout (e.g., 30 seconds)
+2. If connection fails, restart in AP mode
+3. Show error message on captive portal
+4. Allow user to re-enter credentials
+
+```c
+// Pseudo-code
+if (wifi_connect_timeout()) {
+ nvs_erase_key("wifi_ssid");
+ nvs_erase_key("wifi_password");
+ esp_restart(); // Will boot into AP mode again
+}
+```
+
+## API Endpoints Summary
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | `/api/wifi/scan` | Scan for available networks |
+| POST | `/api/wifi/config` | Save WiFi credentials |
+| GET | `/api/wifi/status` | Get current connection status |
+
+### Request/Response Examples
+
+**GET /api/wifi/scan**
+```json
+[
+ { "ssid": "HomeNetwork", "rssi": -45, "secure": true },
+ { "ssid": "GuestWiFi", "rssi": -67, "secure": false }
+]
+```
+
+**POST /api/wifi/config**
+```json
+{ "ssid": "HomeNetwork", "password": "MySecretPassword" }
+```
+
+**GET /api/wifi/status**
+```json
+{
+ "connected": true,
+ "ssid": "HomeNetwork",
+ "ip": "192.168.1.42",
+ "rssi": -52
+}
+```
+
+## Security Considerations
+
+1. **Open AP**: The setup AP is intentionally open for easy access. Keep setup time short.
+2. **HTTPS**: Consider using HTTPS for the main interface (after WiFi setup).
+3. **Timeout**: Auto-disable AP mode after successful connection.
+4. **Button Reset**: Implement a physical button to reset WiFi credentials and re-enter AP mode.
diff --git a/firmware/bootloader_components/my_boot_hooks/CMakeLists.txt b/firmware/bootloader_components/my_boot_hooks/CMakeLists.txt
new file mode 100644
index 0000000..9d43803
--- /dev/null
+++ b/firmware/bootloader_components/my_boot_hooks/CMakeLists.txt
@@ -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.
\ No newline at end of file
diff --git a/firmware/bootloader_components/my_boot_hooks/hooks.c b/firmware/bootloader_components/my_boot_hooks/hooks.c
new file mode 100644
index 0000000..1433c67
--- /dev/null
+++ b/firmware/bootloader_components/my_boot_hooks/hooks.c
@@ -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");
+}
diff --git a/firmware/bootloader_components_extra/extra_component/CMakeLists.txt b/firmware/bootloader_components_extra/extra_component/CMakeLists.txt
new file mode 100644
index 0000000..ac58fa9
--- /dev/null
+++ b/firmware/bootloader_components_extra/extra_component/CMakeLists.txt
@@ -0,0 +1 @@
+idf_component_register(SRCS "extra_component.c")
diff --git a/firmware/bootloader_components_extra/extra_component/extra_component.c b/firmware/bootloader_components_extra/extra_component/extra_component.c
new file mode 100644
index 0000000..78b558c
--- /dev/null
+++ b/firmware/bootloader_components_extra/extra_component/extra_component.c
@@ -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");
+}
diff --git a/firmware/components/api-server/CMakeLists.txt b/firmware/components/api-server/CMakeLists.txt
new file mode 100644
index 0000000..9a6ef99
--- /dev/null
+++ b/firmware/components/api-server/CMakeLists.txt
@@ -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
+)
diff --git a/firmware/components/api-server/Kconfig b/firmware/components/api-server/Kconfig
new file mode 100644
index 0000000..06fe4aa
--- /dev/null
+++ b/firmware/components/api-server/Kconfig
@@ -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 .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
+
diff --git a/firmware/components/api-server/idf_component.yml b/firmware/components/api-server/idf_component.yml
new file mode 100644
index 0000000..3c6c200
--- /dev/null
+++ b/firmware/components/api-server/idf_component.yml
@@ -0,0 +1,5 @@
+dependencies:
+ idf:
+ version: '>=5.0.0'
+ espressif/mdns:
+ version: '*'
diff --git a/firmware/components/api-server/include/api_handlers.h b/firmware/components/api-server/include/api_handlers.h
new file mode 100644
index 0000000..03635e1
--- /dev/null
+++ b/firmware/components/api-server/include/api_handlers.h
@@ -0,0 +1,63 @@
+#pragma once
+
+#include
+
+#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
diff --git a/firmware/components/api-server/include/api_server.h b/firmware/components/api-server/include/api_server.h
new file mode 100644
index 0000000..1b1d85e
--- /dev/null
+++ b/firmware/components/api-server/include/api_server.h
@@ -0,0 +1,119 @@
+#pragma once
+
+#include
+#include
+#include
+
+#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
diff --git a/firmware/components/api-server/include/common.h b/firmware/components/api-server/include/common.h
new file mode 100644
index 0000000..f5d153a
--- /dev/null
+++ b/firmware/components/api-server/include/common.h
@@ -0,0 +1,9 @@
+#ifndef COMMON_H
+#define COMMON_H
+
+#include
+
+void common_init(void);
+cJSON *create_light_status_json(void);
+
+#endif // COMMON_H
diff --git a/firmware/components/api-server/include/websocket_handler.h b/firmware/components/api-server/include/websocket_handler.h
new file mode 100644
index 0000000..64b57bc
--- /dev/null
+++ b/firmware/components/api-server/include/websocket_handler.h
@@ -0,0 +1,60 @@
+#pragma once
+
+#include
+#include
+
+#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
diff --git a/firmware/components/api-server/src/api_handlers.c b/firmware/components/api-server/src/api_handlers.c
new file mode 100644
index 0000000..d358fc0
--- /dev/null
+++ b/firmware/components/api-server/src/api_handlers.c
@@ -0,0 +1,1165 @@
+#include "api_handlers.h"
+#include "common.h"
+#include "message_manager.h"
+
+#include "esp_heap_caps.h"
+#include "led_segment.h"
+#include "persistence_manager.h"
+#include "storage.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define MAX_BODY_SIZE 4096
+
+static const char *TAG = "api_handlers";
+
+// Helper function to set CORS headers
+static esp_err_t set_cors_headers(httpd_req_t *req)
+{
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
+ httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
+ return ESP_OK;
+}
+
+// Helper function to send JSON response
+static esp_err_t send_json_response(httpd_req_t *req, const char *json)
+{
+ set_cors_headers(req);
+ httpd_resp_set_type(req, "application/json");
+ return httpd_resp_sendstr(req, json);
+}
+
+// Helper function to send error response
+static esp_err_t send_error_response(httpd_req_t *req, int status_code, const char *message)
+{
+ set_cors_headers(req);
+ httpd_resp_set_type(req, "application/json");
+ httpd_resp_set_status(req, status_code == 400 ? "400 Bad Request"
+ : status_code == 404 ? "404 Not Found"
+ : "500 Internal Server Error");
+ char buffer[128];
+ snprintf(buffer, sizeof(buffer), "{\"error\":\"%s\"}", message);
+ return httpd_resp_sendstr(req, buffer);
+}
+
+// OPTIONS handler for CORS preflight
+static esp_err_t options_handler(httpd_req_t *req)
+{
+ set_cors_headers(req);
+ httpd_resp_set_status(req, "204 No Content");
+ httpd_resp_send(req, NULL, 0);
+ return ESP_OK;
+}
+
+// ============================================================================
+// Capabilities API
+// ============================================================================
+
+esp_err_t api_capabilities_get_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/capabilities");
+
+ // Thread only available for esp32c6 or esp32h2
+ bool thread = false;
+#if defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32H2)
+ thread = true;
+#endif
+ cJSON *json = cJSON_CreateObject();
+ cJSON_AddBoolToObject(json, "thread", thread);
+ char *response = cJSON_PrintUnformatted(json);
+ cJSON_Delete(json);
+ esp_err_t res = send_json_response(req, response);
+ free(response);
+ return res;
+}
+
+// ============================================================================
+// WiFi API
+// ============================================================================
+
+esp_err_t api_wifi_scan_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/wifi/scan");
+
+ wifi_scan_config_t scan_config = {.ssid = NULL, .bssid = NULL, .channel = 0, .show_hidden = true};
+ esp_err_t err = esp_wifi_scan_start(&scan_config, true);
+ if (err != ESP_OK)
+ {
+ return send_error_response(req, 500, "WiFi scan failed");
+ }
+
+ uint16_t ap_num = 0;
+ esp_wifi_scan_get_ap_num(&ap_num);
+ wifi_ap_record_t *ap_list = heap_caps_calloc(ap_num, sizeof(wifi_ap_record_t), MALLOC_CAP_DEFAULT);
+ if (!ap_list)
+ {
+ return send_error_response(req, 500, "Memory allocation failed");
+ }
+ esp_wifi_scan_get_ap_records(&ap_num, ap_list);
+
+ cJSON *json = cJSON_CreateArray();
+ for (int i = 0; i < ap_num; i++)
+ {
+ if (ap_list[i].ssid[0] != '\0')
+ {
+ cJSON *entry = cJSON_CreateObject();
+ cJSON_AddStringToObject(entry, "ssid", (const char *)ap_list[i].ssid);
+ cJSON_AddNumberToObject(entry, "rssi", ap_list[i].rssi);
+ bool secure = ap_list[i].authmode != WIFI_AUTH_OPEN;
+ cJSON_AddBoolToObject(entry, "secure", secure);
+ cJSON_AddItemToArray(json, entry);
+ }
+ }
+ char *response = cJSON_PrintUnformatted(json);
+ cJSON_Delete(json);
+ free(ap_list);
+ esp_err_t res = send_json_response(req, response);
+ free(response);
+ return res;
+}
+
+static void reboot_task(void *param)
+{
+ vTaskDelay(pdMS_TO_TICKS(100));
+ esp_restart();
+}
+
+static bool is_valid(const cJSON *string)
+{
+ return string && cJSON_IsString(string) && string->valuestring && strlen(string->valuestring) > 0;
+}
+
+esp_err_t api_wifi_config_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/wifi/config");
+
+ char buf[256];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ cJSON *json = cJSON_Parse(buf);
+ if (json)
+ {
+ cJSON *ssid = cJSON_GetObjectItem(json, "ssid");
+ cJSON *pw = cJSON_GetObjectItem(json, "password");
+ if (is_valid(ssid) && is_valid(pw))
+ {
+ persistence_manager_t pm;
+ if (persistence_manager_init(&pm, "wifi_config") == ESP_OK)
+ {
+ persistence_manager_set_string(&pm, "ssid", ssid->valuestring);
+ persistence_manager_set_string(&pm, "password", pw->valuestring);
+ persistence_manager_deinit(&pm);
+ }
+ }
+ if (is_valid(pw))
+ {
+ size_t pwlen = strlen(pw->valuestring);
+ char *masked = heap_caps_malloc(pwlen + 1, MALLOC_CAP_DEFAULT);
+ if (masked)
+ {
+ memset(masked, '*', pwlen);
+ masked[pwlen] = '\0';
+ cJSON_ReplaceItemInObject(json, "password", cJSON_CreateString(masked));
+ char *logstr = cJSON_PrintUnformatted(json);
+ ESP_LOGI(TAG, "Received WiFi config: %s", logstr);
+ free(logstr);
+ free(masked);
+ }
+ else
+ {
+ ESP_LOGI(TAG, "Received WiFi config: %s", buf);
+ }
+ }
+ else
+ {
+ ESP_LOGI(TAG, "Received WiFi config: %s", buf);
+ }
+ cJSON_Delete(json);
+ }
+ else
+ {
+ ESP_LOGI(TAG, "Received WiFi config: %s", buf);
+ }
+
+ // Define a reboot task function
+ xTaskCreate(reboot_task, "reboot_task", 2048, NULL, 5, NULL);
+
+ set_cors_headers(req);
+ httpd_resp_set_status(req, "200 OK");
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_wifi_status_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/wifi/status");
+
+ wifi_ap_record_t ap_info;
+ bool connected = false;
+ char ssid[33] = "";
+ char ip[16] = "";
+ int rssi = 0;
+
+ wifi_mode_t mode;
+ esp_wifi_get_mode(&mode);
+ if (mode == WIFI_MODE_STA || mode == WIFI_MODE_APSTA)
+ {
+ if (esp_wifi_sta_get_ap_info(&ap_info) == ESP_OK)
+ {
+ connected = true;
+ strncpy(ssid, (const char *)ap_info.ssid, sizeof(ssid) - 1);
+ rssi = ap_info.rssi;
+ }
+ esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
+ if (netif)
+ {
+ esp_netif_ip_info_t ip_info;
+ if (esp_netif_get_ip_info(netif, &ip_info) == ESP_OK)
+ {
+ snprintf(ip, sizeof(ip), "%d.%d.%d.%d", esp_ip4_addr1(&ip_info.ip), esp_ip4_addr2(&ip_info.ip),
+ esp_ip4_addr3(&ip_info.ip), esp_ip4_addr4(&ip_info.ip));
+ }
+ }
+ }
+
+ cJSON *json = cJSON_CreateObject();
+ cJSON_AddBoolToObject(json, "connected", connected);
+ cJSON_AddStringToObject(json, "ssid", ssid);
+ cJSON_AddStringToObject(json, "ip", ip);
+ cJSON_AddNumberToObject(json, "rssi", rssi);
+ char *response = cJSON_PrintUnformatted(json);
+ cJSON_Delete(json);
+ esp_err_t res = send_json_response(req, response);
+ free(response);
+ return res;
+}
+
+// ============================================================================
+// Light Control API
+// ============================================================================
+
+esp_err_t api_light_power_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/light/power");
+
+ char buf[64];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Received light power: %s", buf);
+
+ cJSON *json = cJSON_Parse(buf);
+ if (json)
+ {
+ cJSON *active = cJSON_GetObjectItem(json, "on");
+ if (cJSON_IsBool(active))
+ {
+ message_t msg = {};
+ msg.type = MESSAGE_TYPE_SETTINGS;
+ msg.data.settings.type = SETTINGS_TYPE_BOOL;
+ strncpy(msg.data.settings.key, "light_active", sizeof(msg.data.settings.key) - 1);
+ msg.data.settings.value.bool_value = cJSON_IsTrue(active);
+ message_manager_post(&msg, pdMS_TO_TICKS(100));
+ }
+ cJSON_Delete(json);
+ }
+
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_light_thunder_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/light/thunder");
+
+ char buf[64];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Received thunder setting: %s", buf);
+
+ // TODO: Parse JSON and control thunder effect
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_light_mode_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/light/mode");
+
+ char buf[64];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Received light mode: %s", buf);
+
+ cJSON *json = cJSON_Parse(buf);
+ if (json)
+ {
+ cJSON *mode = cJSON_GetObjectItem(json, "mode");
+ if (cJSON_IsString(mode))
+ {
+ message_t msg = {};
+ msg.type = MESSAGE_TYPE_SETTINGS;
+ msg.data.settings.type = SETTINGS_TYPE_INT;
+ strncpy(msg.data.settings.key, "light_mode", sizeof(msg.data.settings.key) - 1);
+ if (strcmp(mode->valuestring, "simulation") == 0)
+ {
+ msg.data.settings.value.int_value = 0;
+ }
+ else if (strcmp(mode->valuestring, "day") == 0)
+ {
+ msg.data.settings.value.int_value = 1;
+ }
+ else if (strcmp(mode->valuestring, "night") == 0)
+ {
+ msg.data.settings.value.int_value = 2;
+ }
+ else
+ {
+ msg.data.settings.value.int_value = -1; // Unknown mode
+ }
+ message_manager_post(&msg, pdMS_TO_TICKS(100));
+ }
+ cJSON_Delete(json);
+ }
+
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_light_schema_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/light/schema");
+
+ char buf[128];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Received schema setting: %s", buf);
+
+ cJSON *json = cJSON_Parse(buf);
+ if (json)
+ {
+ cJSON *schema_file = cJSON_GetObjectItem(json, "schema");
+ if (cJSON_IsString(schema_file))
+ {
+ int schema_id = 0;
+ sscanf(schema_file->valuestring, "schema_%d.csv", &schema_id);
+
+ message_t msg = {};
+ msg.type = MESSAGE_TYPE_SETTINGS;
+ msg.data.settings.type = SETTINGS_TYPE_INT;
+ strncpy(msg.data.settings.key, "light_variant", sizeof(msg.data.settings.key) - 1);
+ msg.data.settings.value.int_value = schema_id;
+ message_manager_post(&msg, pdMS_TO_TICKS(100));
+ }
+ cJSON_Delete(json);
+ }
+
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_light_status_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/light/status");
+ cJSON *json = create_light_status_json();
+ char *response = cJSON_PrintUnformatted(json);
+ cJSON_Delete(json);
+ esp_err_t res = send_json_response(req, response);
+ free(response);
+ return res;
+}
+
+// ============================================================================
+// LED Configuration API
+// ============================================================================
+
+esp_err_t api_wled_config_get_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/wled/config");
+
+ extern led_segment_t segments[LED_SEGMENT_MAX_LEN];
+ extern size_t segment_count;
+ size_t required_size = sizeof(segments) * segment_count;
+
+ cJSON *json = cJSON_CreateObject();
+
+ persistence_manager_t pm;
+ if (persistence_manager_init(&pm, "led_config") == ESP_OK)
+ {
+ persistence_manager_get_blob(&pm, "segments", segments, required_size, NULL);
+ uint8_t segment_count = persistence_manager_get_int(&pm, "segment_count", 0);
+ persistence_manager_deinit(&pm);
+
+ cJSON *segments_arr = cJSON_CreateArray();
+ for (uint8_t i = 0; i < segment_count; ++i)
+ {
+ cJSON *seg = cJSON_CreateObject();
+ cJSON_AddStringToObject(seg, "name", segments[i].name);
+ cJSON_AddNumberToObject(seg, "start", segments[i].start);
+ cJSON_AddNumberToObject(seg, "leds", segments[i].leds);
+ cJSON_AddItemToArray(segments_arr, seg);
+ }
+ cJSON_AddItemToObject(json, "segments", segments_arr);
+ }
+ else
+ {
+ cJSON_AddItemToObject(json, "segments", cJSON_CreateArray());
+ }
+
+ char *response = cJSON_PrintUnformatted(json);
+ cJSON_Delete(json);
+ esp_err_t res = send_json_response(req, response);
+ free(response);
+ return res;
+}
+
+esp_err_t api_wled_config_post_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/wled/config");
+
+ char *buf = heap_caps_malloc(MAX_BODY_SIZE, MALLOC_CAP_DEFAULT);
+ if (!buf)
+ return send_error_response(req, 500, "Memory allocation failed");
+ int total = 0, ret;
+ while (total < MAX_BODY_SIZE - 1)
+ {
+ ret = httpd_req_recv(req, buf + total, MAX_BODY_SIZE - 1 - total);
+ if (ret <= 0)
+ break;
+ total += ret;
+ }
+ buf[total] = '\0';
+
+ ESP_LOGI(TAG, "Received WLED config: %s", buf);
+
+ cJSON *json = cJSON_Parse(buf);
+ free(buf);
+
+ if (!json)
+ {
+ return send_error_response(req, 400, "Invalid JSON");
+ }
+
+ cJSON *segments_arr = cJSON_GetObjectItem(json, "segments");
+ if (!cJSON_IsArray(segments_arr))
+ {
+ cJSON_Delete(json);
+ return send_error_response(req, 400, "Missing segments array");
+ }
+
+ extern led_segment_t segments[LED_SEGMENT_MAX_LEN];
+ extern size_t segment_count;
+ size_t count = cJSON_GetArraySize(segments_arr);
+ if (count > LED_SEGMENT_MAX_LEN)
+ count = LED_SEGMENT_MAX_LEN;
+ segment_count = count;
+ for (size_t i = 0; i < LED_SEGMENT_MAX_LEN; ++i)
+ {
+ cJSON *seg = cJSON_GetArrayItem(segments_arr, i);
+ cJSON *name = cJSON_GetObjectItem(seg, "name");
+ cJSON *start = cJSON_GetObjectItem(seg, "start");
+ cJSON *leds = cJSON_GetObjectItem(seg, "leds");
+ if (cJSON_IsString(name) && cJSON_IsNumber(start) && cJSON_IsNumber(leds) && i < count)
+ {
+ strncpy(segments[i].name, name->valuestring, sizeof(segments[i].name) - 1);
+ segments[i].name[sizeof(segments[i].name) - 1] = '\0';
+ segments[i].start = (uint16_t)start->valuedouble;
+ segments[i].leds = (uint16_t)leds->valuedouble;
+ }
+ else
+ {
+ // Invalid entry, skip or set defaults
+ segments[i].name[0] = '\0';
+ segments[i].start = 0;
+ segments[i].leds = 0;
+ }
+ }
+ cJSON_Delete(json);
+
+ persistence_manager_t pm;
+ if (persistence_manager_init(&pm, "led_config") == ESP_OK)
+ {
+ persistence_manager_set_blob(&pm, "segments", segments, sizeof(led_segment_t) * segment_count);
+ persistence_manager_set_int(&pm, "segment_count", (int32_t)segment_count);
+ persistence_manager_deinit(&pm);
+ }
+
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+// ============================================================================
+// Schema API
+// ============================================================================
+static char *heap_caps_strdup(const char *src, uint32_t caps)
+{
+ if (!src)
+ return NULL;
+ size_t len = strlen(src) + 1;
+ char *dst = heap_caps_malloc(len, caps);
+ if (dst)
+ memcpy(dst, src, len);
+ return dst;
+}
+
+esp_err_t api_schema_get_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/schema/*");
+
+ // Extract filename from URI
+ const char *uri = req->uri;
+ const char *filename = strrchr(uri, '/');
+ if (filename == NULL)
+ {
+ return send_error_response(req, 400, "Invalid schema path");
+ }
+ filename++; // Skip the '/'
+
+ ESP_LOGI(TAG, "Requested schema: %s", filename);
+
+ // Read schema file
+ char path[128];
+ snprintf(path, sizeof(path), "%s", filename);
+
+ int line_count = 0;
+ char **lines = read_lines_filtered(path, &line_count);
+
+ set_cors_headers(req);
+ httpd_resp_set_type(req, "text/csv");
+
+ if (!lines || line_count == 0)
+ {
+ return httpd_resp_sendstr(req, "");
+ }
+
+ // Calculate total length
+ size_t total_len = 0;
+ for (int i = 0; i < line_count; ++i)
+ total_len += strlen(lines[i]) + 1;
+ char *csv = heap_caps_malloc(total_len + 1, MALLOC_CAP_DEFAULT);
+ char *p = csv;
+ for (int i = 0; i < line_count; ++i)
+ {
+ size_t l = strlen(lines[i]);
+ memcpy(p, lines[i], l);
+ p += l;
+ *p++ = '\n';
+ }
+ *p = '\0';
+ free_lines(lines, line_count);
+ esp_err_t res = httpd_resp_sendstr(req, csv);
+ free(csv);
+ return res;
+}
+
+esp_err_t api_schema_post_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/schema/*");
+
+ // Extract filename from URI
+ if (!req)
+ {
+ ESP_LOGE(TAG, "Request pointer is NULL");
+ return send_error_response(req, 500, "Internal error: req is NULL");
+ }
+ const char *uri = req->uri;
+ ESP_LOGI(TAG, "Request URI: %s", uri ? uri : "(null)");
+ if (!uri)
+ {
+ ESP_LOGE(TAG, "Request URI is NULL");
+ return send_error_response(req, 400, "Invalid schema path (no URI)");
+ }
+ const char *filename = strrchr(uri, '/');
+ if (filename == NULL || filename[1] == '\0')
+ {
+ ESP_LOGE(TAG, "Could not extract filename from URI: %s", uri);
+ return send_error_response(req, 400, "Invalid schema path");
+ }
+ filename++;
+ ESP_LOGI(TAG, "Extracted filename: %s", filename);
+
+ // Dynamically read POST body (like api_wled_config_post_handler)
+ char *buf = heap_caps_malloc(MAX_BODY_SIZE, MALLOC_CAP_DEFAULT);
+ if (!buf)
+ {
+ ESP_LOGE(TAG, "Memory allocation failed for POST body");
+ return send_error_response(req, 500, "Memory allocation failed");
+ }
+ int total = 0, ret;
+ while (total < MAX_BODY_SIZE - 1)
+ {
+ ret = httpd_req_recv(req, buf + total, MAX_BODY_SIZE - 1 - total);
+ if (ret <= 0)
+ break;
+ total += ret;
+ }
+ buf[total] = '\0';
+
+ ESP_LOGI(TAG, "Saving schema %s, size: %d bytes", filename, total);
+
+ // Split CSV body into line array
+ int line_count = 0;
+ // Count lines
+ for (int i = 0; i < total; ++i)
+ if (buf[i] == '\n')
+ line_count++;
+ if (total > 0 && buf[total - 1] != '\n')
+ line_count++; // last line without \n
+
+ char **lines = (char **)heap_caps_malloc(line_count * sizeof(char *), MALLOC_CAP_DEFAULT);
+ int idx = 0;
+ char *saveptr = NULL;
+ char *line = strtok_r(buf, "\n", &saveptr);
+ while (line && idx < line_count)
+ {
+ // Ignore empty lines
+ if (line[0] != '\0')
+ lines[idx++] = heap_caps_strdup(line, MALLOC_CAP_DEFAULT);
+ line = strtok_r(NULL, "\n", &saveptr);
+ }
+ int actual_count = idx;
+ esp_err_t err = write_lines(filename, lines, actual_count);
+ for (int i = 0; i < actual_count; ++i)
+ free(lines[i]);
+ free(lines);
+ set_cors_headers(req);
+
+ if (err != ESP_OK)
+ return send_error_response(req, 500, "Failed to save schema");
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+// ============================================================================
+// Devices API (Matter)
+// ============================================================================
+
+esp_err_t api_devices_scan_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/devices/scan");
+
+ // TODO: Implement Matter device scanning
+ const char *response = "["
+ "{\"id\":\"matter-001\",\"type\":\"light\",\"name\":\"Matter Lamp\"},"
+ "{\"id\":\"matter-002\",\"type\":\"sensor\",\"name\":\"Temperature Sensor\"}"
+ "]";
+ return send_json_response(req, response);
+}
+
+esp_err_t api_devices_pair_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/devices/pair");
+
+ char buf[256];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Pairing device: %s", buf);
+
+ // TODO: Implement Matter device pairing
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_devices_paired_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/devices/paired");
+
+ // TODO: Get list of paired devices
+ const char *response = "["
+ "{\"id\":\"matter-001\",\"type\":\"light\",\"name\":\"Living Room Lamp\"}"
+ "]";
+ return send_json_response(req, response);
+}
+
+esp_err_t api_devices_update_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/devices/update");
+
+ char buf[256];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Updating device: %s", buf);
+
+ // TODO: Update device name
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_devices_unpair_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/devices/unpair");
+
+ char buf[128];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Unpairing device: %s", buf);
+
+ // TODO: Unpair device
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_devices_toggle_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/devices/toggle");
+
+ char buf[128];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Toggling device: %s", buf);
+
+ // TODO: Toggle device
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+// ============================================================================
+// Scenes API
+// ============================================================================
+
+esp_err_t api_scenes_get_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "GET /api/scenes");
+
+ // TODO: Get scenes from storage
+ const char *response = "["
+ "{"
+ "\"id\":\"scene-1\","
+ "\"name\":\"Evening Mood\","
+ "\"icon\":\"π \","
+ "\"actions\":{"
+ "\"light\":\"on\","
+ "\"mode\":\"simulation\","
+ "\"schema\":\"schema_02.csv\""
+ "}"
+ "},"
+ "{"
+ "\"id\":\"scene-2\","
+ "\"name\":\"Night Mode\","
+ "\"icon\":\"π\","
+ "\"actions\":{"
+ "\"light\":\"on\","
+ "\"mode\":\"night\""
+ "}"
+ "}"
+ "]";
+ return send_json_response(req, response);
+}
+
+esp_err_t api_scenes_post_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/scenes");
+
+ char buf[512];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Creating/updating scene: %s", buf);
+
+ // TODO: Save scene to storage
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_scenes_delete_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "DELETE /api/scenes");
+
+ char buf[128];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Deleting scene: %s", buf);
+
+ // TODO: Delete scene from storage
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+esp_err_t api_scenes_activate_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "POST /api/scenes/activate");
+
+ char buf[128];
+ int ret = httpd_req_recv(req, buf, sizeof(buf) - 1);
+ if (ret <= 0)
+ {
+ return send_error_response(req, 400, "Failed to receive request body");
+ }
+ buf[ret] = '\0';
+
+ ESP_LOGI(TAG, "Activating scene: %s", buf);
+
+ // TODO: Activate scene
+ set_cors_headers(req);
+ return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
+}
+
+// ============================================================================
+// Static file serving
+// ============================================================================
+
+// Get MIME type from file extension
+static const char *get_mime_type(const char *path)
+{
+ const char *ext = strrchr(path, '.');
+ if (ext == NULL)
+ return "text/plain";
+
+ if (strcmp(ext, ".html") == 0)
+ return "text/html";
+ if (strcmp(ext, ".css") == 0)
+ return "text/css";
+ if (strcmp(ext, ".js") == 0)
+ return "application/javascript";
+ if (strcmp(ext, ".json") == 0)
+ return "application/json";
+ if (strcmp(ext, ".png") == 0)
+ return "image/png";
+ if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0)
+ return "image/jpeg";
+ if (strcmp(ext, ".svg") == 0)
+ return "image/svg+xml";
+ if (strcmp(ext, ".ico") == 0)
+ return "image/x-icon";
+ if (strcmp(ext, ".csv") == 0)
+ return "text/csv";
+
+ return "text/plain";
+}
+
+esp_err_t api_static_file_handler(httpd_req_t *req)
+{
+ char filepath[CONFIG_HTTPD_MAX_URI_LEN + 16];
+
+ const char *uri = req->uri;
+ wifi_mode_t mode = 0;
+ esp_wifi_get_mode(&mode);
+ // Always serve captive.html in AP mode
+ if (mode == WIFI_MODE_AP || mode == WIFI_MODE_APSTA)
+ {
+ if (strcmp(uri, "/") == 0 || strcmp(uri, "/index.html") == 0)
+ {
+ uri = "/captive.html";
+ }
+ }
+ else
+ {
+ // Default to index.html for root
+ if (strcmp(uri, "/") == 0)
+ {
+ uri = "/index.html";
+ }
+ }
+
+ const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH;
+ int written = snprintf(filepath, sizeof(filepath), "%s%s", base_path, uri);
+ if (written < 0 || (size_t)written >= sizeof(filepath))
+ {
+ ESP_LOGE(TAG, "URI too long: %s", uri);
+ return send_error_response(req, 400, "URI too long");
+ }
+
+ ESP_LOGI(TAG, "Serving static file: %s", filepath);
+
+ // Check if file exists
+ struct stat st;
+ if (stat(filepath, &st) != 0)
+ {
+ ESP_LOGW(TAG, "File not found: %s", filepath);
+ return send_error_response(req, 404, "File not found");
+ }
+
+ // Open and serve file
+ FILE *f = fopen(filepath, "r");
+ if (f == NULL)
+ {
+ ESP_LOGE(TAG, "Failed to open file: %s", filepath);
+ return send_error_response(req, 500, "Failed to open file");
+ }
+
+ set_cors_headers(req);
+ httpd_resp_set_type(req, get_mime_type(filepath));
+
+ char buf[512];
+ size_t read_bytes;
+ while ((read_bytes = fread(buf, 1, sizeof(buf), f)) > 0)
+ {
+ if (httpd_resp_send_chunk(req, buf, read_bytes) != ESP_OK)
+ {
+ fclose(f);
+ ESP_LOGE(TAG, "Failed to send file chunk");
+ return ESP_FAIL;
+ }
+ }
+
+ fclose(f);
+ httpd_resp_send_chunk(req, NULL, 0); // End response
+ return ESP_OK;
+}
+
+// ============================================================================
+// Captive portal detection
+// ============================================================================
+
+esp_err_t api_captive_portal_handler(httpd_req_t *req)
+{
+ ESP_LOGI(TAG, "Captive portal detection: %s", req->uri);
+
+ // Serve captive.html directly (status 200, text/html)
+ const char *base_path = CONFIG_API_SERVER_STATIC_FILES_PATH;
+ char filepath[256];
+ snprintf(filepath, sizeof(filepath), "%s/captive.html", base_path);
+ FILE *f = fopen(filepath, "r");
+ if (!f)
+ {
+ ESP_LOGE(TAG, "captive.html not found: %s", filepath);
+ httpd_resp_set_status(req, "500 Internal Server Error");
+ httpd_resp_sendstr(req, "Captive portal not available");
+ return ESP_FAIL;
+ }
+ httpd_resp_set_type(req, "text/html");
+ char buf[512];
+ size_t read_bytes;
+ while ((read_bytes = fread(buf, 1, sizeof(buf), f)) > 0)
+ {
+ if (httpd_resp_send_chunk(req, buf, read_bytes) != ESP_OK)
+ {
+ fclose(f);
+ ESP_LOGE(TAG, "Failed to send captive chunk");
+ return ESP_FAIL;
+ }
+ }
+ fclose(f);
+ httpd_resp_send_chunk(req, NULL, 0);
+ return ESP_OK;
+}
+
+// ============================================================================
+// Handler Registration
+// ============================================================================
+
+esp_err_t api_handlers_register(httpd_handle_t server)
+{
+ esp_err_t err;
+
+ // Capabilities
+ httpd_uri_t capabilities_get = {
+ .uri = "/api/capabilities", .method = HTTP_GET, .handler = api_capabilities_get_handler};
+ err = httpd_register_uri_handler(server, &capabilities_get);
+ if (err != ESP_OK)
+ return err;
+
+ // WiFi endpoints
+ httpd_uri_t wifi_scan = {.uri = "/api/wifi/scan", .method = HTTP_GET, .handler = api_wifi_scan_handler};
+ err = httpd_register_uri_handler(server, &wifi_scan);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t wifi_config = {.uri = "/api/wifi/config", .method = HTTP_POST, .handler = api_wifi_config_handler};
+ err = httpd_register_uri_handler(server, &wifi_config);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t wifi_status = {.uri = "/api/wifi/status", .method = HTTP_GET, .handler = api_wifi_status_handler};
+ err = httpd_register_uri_handler(server, &wifi_status);
+ if (err != ESP_OK)
+ return err;
+
+ // Light endpoints
+ httpd_uri_t light_power = {.uri = "/api/light/power", .method = HTTP_POST, .handler = api_light_power_handler};
+ err = httpd_register_uri_handler(server, &light_power);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t light_thunder = {
+ .uri = "/api/light/thunder", .method = HTTP_POST, .handler = api_light_thunder_handler};
+ err = httpd_register_uri_handler(server, &light_thunder);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t light_mode = {.uri = "/api/light/mode", .method = HTTP_POST, .handler = api_light_mode_handler};
+ err = httpd_register_uri_handler(server, &light_mode);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t light_schema = {.uri = "/api/light/schema", .method = HTTP_POST, .handler = api_light_schema_handler};
+ err = httpd_register_uri_handler(server, &light_schema);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t light_status = {.uri = "/api/light/status", .method = HTTP_GET, .handler = api_light_status_handler};
+ err = httpd_register_uri_handler(server, &light_status);
+ if (err != ESP_OK)
+ return err;
+
+ // WLED config endpoints
+ httpd_uri_t wled_config_get = {
+ .uri = "/api/wled/config", .method = HTTP_GET, .handler = api_wled_config_get_handler};
+ err = httpd_register_uri_handler(server, &wled_config_get);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t wled_config_post = {
+ .uri = "/api/wled/config", .method = HTTP_POST, .handler = api_wled_config_post_handler};
+ err = httpd_register_uri_handler(server, &wled_config_post);
+ if (err != ESP_OK)
+ return err;
+
+ // Schema endpoints (wildcard)
+ httpd_uri_t schema_get = {.uri = "/api/schema/*", .method = HTTP_GET, .handler = api_schema_get_handler};
+ err = httpd_register_uri_handler(server, &schema_get);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t schema_post = {.uri = "/api/schema/*", .method = HTTP_POST, .handler = api_schema_post_handler};
+ err = httpd_register_uri_handler(server, &schema_post);
+ if (err != ESP_OK)
+ return err;
+
+ // Devices endpoints
+ httpd_uri_t devices_scan = {.uri = "/api/devices/scan", .method = HTTP_GET, .handler = api_devices_scan_handler};
+ err = httpd_register_uri_handler(server, &devices_scan);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t devices_pair = {.uri = "/api/devices/pair", .method = HTTP_POST, .handler = api_devices_pair_handler};
+ err = httpd_register_uri_handler(server, &devices_pair);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t devices_paired = {
+ .uri = "/api/devices/paired", .method = HTTP_GET, .handler = api_devices_paired_handler};
+ err = httpd_register_uri_handler(server, &devices_paired);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t devices_update = {
+ .uri = "/api/devices/update", .method = HTTP_POST, .handler = api_devices_update_handler};
+ err = httpd_register_uri_handler(server, &devices_update);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t devices_unpair = {
+ .uri = "/api/devices/unpair", .method = HTTP_POST, .handler = api_devices_unpair_handler};
+ err = httpd_register_uri_handler(server, &devices_unpair);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t devices_toggle = {
+ .uri = "/api/devices/toggle", .method = HTTP_POST, .handler = api_devices_toggle_handler};
+ err = httpd_register_uri_handler(server, &devices_toggle);
+ if (err != ESP_OK)
+ return err;
+
+ // Scenes endpoints
+ httpd_uri_t scenes_get = {.uri = "/api/scenes", .method = HTTP_GET, .handler = api_scenes_get_handler};
+ err = httpd_register_uri_handler(server, &scenes_get);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t scenes_post = {.uri = "/api/scenes", .method = HTTP_POST, .handler = api_scenes_post_handler};
+ err = httpd_register_uri_handler(server, &scenes_post);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t scenes_delete = {.uri = "/api/scenes", .method = HTTP_DELETE, .handler = api_scenes_delete_handler};
+ err = httpd_register_uri_handler(server, &scenes_delete);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t scenes_activate = {
+ .uri = "/api/scenes/activate", .method = HTTP_POST, .handler = api_scenes_activate_handler};
+ err = httpd_register_uri_handler(server, &scenes_activate);
+ if (err != ESP_OK)
+ return err;
+
+ // Captive portal detection endpoints
+ httpd_uri_t captive_generate_204 = {
+ .uri = "/generate_204", .method = HTTP_GET, .handler = api_captive_portal_handler};
+ err = httpd_register_uri_handler(server, &captive_generate_204);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t captive_hotspot = {
+ .uri = "/hotspot-detect.html", .method = HTTP_GET, .handler = api_captive_portal_handler};
+ err = httpd_register_uri_handler(server, &captive_hotspot);
+ if (err != ESP_OK)
+ return err;
+
+ httpd_uri_t captive_connecttest = {
+ .uri = "/connecttest.txt", .method = HTTP_GET, .handler = api_captive_portal_handler};
+ err = httpd_register_uri_handler(server, &captive_connecttest);
+ if (err != ESP_OK)
+ return err;
+
+ // OPTIONS handler for CORS preflight (wildcard)
+ httpd_uri_t options = {.uri = "/api/*", .method = HTTP_OPTIONS, .handler = options_handler};
+ err = httpd_register_uri_handler(server, &options);
+ if (err != ESP_OK)
+ return err;
+
+ // Static file handler (must be last due to wildcard)
+ httpd_uri_t static_files = {.uri = "/*", .method = HTTP_GET, .handler = api_static_file_handler};
+ err = httpd_register_uri_handler(server, &static_files);
+ if (err != ESP_OK)
+ return err;
+
+ ESP_LOGI(TAG, "All API handlers registered");
+ return ESP_OK;
+}
diff --git a/firmware/components/api-server/src/api_server.c b/firmware/components/api-server/src/api_server.c
new file mode 100644
index 0000000..69e57f8
--- /dev/null
+++ b/firmware/components/api-server/src/api_server.c
@@ -0,0 +1,200 @@
+#include "api_server.h"
+#include "api_handlers.h"
+#include "websocket_handler.h"
+
+#include "common.h"
+#include "storage.h"
+#include
+#include
+#include
+#include
+
+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);
+}
diff --git a/firmware/components/api-server/src/common.c b/firmware/components/api-server/src/common.c
new file mode 100644
index 0000000..f3ed6d4
--- /dev/null
+++ b/firmware/components/api-server/src/common.c
@@ -0,0 +1,79 @@
+#include "common.h"
+#include
+#include
+
+#include "api_server.h"
+#include "color.h"
+#include "message_manager.h"
+#include "persistence_manager.h"
+#include "simulator.h"
+#include
+#include
+
+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;
+}
diff --git a/firmware/components/api-server/src/websocket_handler.c b/firmware/components/api-server/src/websocket_handler.c
new file mode 100644
index 0000000..310c7b1
--- /dev/null
+++ b/firmware/components/api-server/src/websocket_handler.c
@@ -0,0 +1,276 @@
+#include "websocket_handler.h"
+#include "api_server.h"
+#include "common.h"
+
+#include "message_manager.h"
+#include
+#include
+#include
+
+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;
+}
diff --git a/firmware/components/connectivity-manager/CMakeLists.txt b/firmware/components/connectivity-manager/CMakeLists.txt
index 28bd01e..a57233e 100644
--- a/firmware/components/connectivity-manager/CMakeLists.txt
+++ b/firmware/components/connectivity-manager/CMakeLists.txt
@@ -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
)
diff --git a/firmware/components/connectivity-manager/include/dns_hijack.h b/firmware/components/connectivity-manager/include/dns_hijack.h
new file mode 100644
index 0000000..4638e52
--- /dev/null
+++ b/firmware/components/connectivity-manager/include/dns_hijack.h
@@ -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
diff --git a/firmware/components/connectivity-manager/src/dns_hijack.c b/firmware/components/connectivity-manager/src/dns_hijack.c
new file mode 100644
index 0000000..eb5657b
--- /dev/null
+++ b/firmware/components/connectivity-manager/src/dns_hijack.c
@@ -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
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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);
+}
diff --git a/firmware/components/connectivity-manager/src/wifi_manager.c b/firmware/components/connectivity-manager/src/wifi_manager.c
index 559bb1a..612eb81 100644
--- a/firmware/components/connectivity-manager/src/wifi_manager.c
+++ b/firmware/components/connectivity-manager/src/wifi_manager.c
@@ -1,5 +1,8 @@
#include "wifi_manager.h"
+#include "dns_hijack.h"
+#include "analytics.h"
+#include "api_server.h"
#include
#include
#include
@@ -11,219 +14,166 @@
#include
#include
#include
+#include
#include
+#include
#include
#include
-// 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,
- },
+ 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,
};
-
- 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
+ led_status_set_behavior(led_behavior);
}
-static void try_next_network(void)
+static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
-#if CONFIG_WIFI_ENABLED
- s_current_network_index++;
- s_retry_num = 0;
-
- if (s_current_network_index < s_wifi_network_count)
- {
- connect_to_network(s_current_network_index);
- }
- else
- {
- ESP_LOGE(TAG, "Failed to connect to any configured network");
- led_behavior_t led0_behavior = {
- .index = 0,
- .mode = LED_MODE_BLINK,
- .color = {.red = 50, .green = 0, .blue = 0},
- .on_time_ms = 1000,
- .off_time_ms = 500,
- };
- led_status_set_behavior(led0_behavior);
- xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
- }
-#endif
-}
-
-static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
-{
-#if CONFIG_WIFI_ENABLED
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
{
- 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_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();
+ ESP_LOGW(TAG, "WIFI_EVENT_STA_DISCONNECTED: Verbindung verloren, versuche erneut...");
+ xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
+ led_status_reconnect();
+ esp_wifi_connect();
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP)
{
- 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();
- wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
- ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+ // Default WiFi Station
+ esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
- 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));
+ // 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));
- ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
- 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);
-
- if (bits & WIFI_CONNECTED_BIT)
+ // 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)
{
- ESP_DIAG_EVENT(TAG, "Connected to AP SSID:%s", s_wifi_networks[s_current_network_index].ssid);
+ persistence_manager_get_string(&pm, "ssid", ssid, sizeof(ssid), "");
+ persistence_manager_get_string(&pm, "password", password, sizeof(password), "");
+ have_ssid = strlen(ssid) > 0;
+ have_password = strlen(password) > 0;
}
- else if (bits & WIFI_FAIL_BIT)
+
+ if (have_ssid && have_password)
{
- ESP_LOGE(TAG, "Failed to connect to any configured WiFi network");
+ led_status_reconnect();
+
+ ESP_LOGI(TAG, "Found WiFi configuration: SSID='%s'", ssid);
+ wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
+ ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+ wifi_config_t wifi_config = {0};
+ strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
+ strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1);
+ wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+
+ int retries = 0;
+ EventBits_t bits;
+ do
+ {
+ ESP_LOGI(TAG, "Warte auf IP-Adresse (DHCP)...");
+ bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE,
+ 10000 / portTICK_PERIOD_MS);
+ if (bits & WIFI_CONNECTED_BIT)
+ {
+ led_behavior_t led_behavior = {
+ .index = 0,
+ .color = {.red = 0, .green = 50, .blue = 0},
+ .mode = LED_MODE_SOLID,
+ };
+ led_status_set_behavior(led_behavior);
+ ESP_LOGI(TAG, "WiFi connection established successfully (mit IP)");
+ break;
+ }
+ retries++;
+ } while (!(bits & WIFI_CONNECTED_BIT) && retries < CONFIG_WIFI_CONNECT_RETRIES);
+
+ if (!(bits & WIFI_CONNECTED_BIT))
+ {
+ ESP_LOGW(TAG, "WiFi connection failed (keine IP?), switching to Access Point mode");
+ // AP-Netzwerkschnittstelle initialisieren, falls noch nicht geschehen
+ esp_netif_create_default_wifi_ap();
+ wifi_create_ap();
+ }
}
else
{
- 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));
}
diff --git a/firmware/components/insa/CMakeLists.txt b/firmware/components/insa/CMakeLists.txt
index 6c3388d..3e5360f 100644
--- a/firmware/components/insa/CMakeLists.txt
+++ b/firmware/components/insa/CMakeLists.txt
@@ -18,4 +18,5 @@ idf_component_register(SRCS
led-manager
persistence-manager
simulator
+ message-manager
)
diff --git a/firmware/components/insa/include/MenuOptions.h b/firmware/components/insa/include/MenuOptions.h
index e926d8f..99f8b97 100644
--- a/firmware/components/insa/include/MenuOptions.h
+++ b/firmware/components/insa/include/MenuOptions.h
@@ -17,7 +17,7 @@
// Project-specific headers
#include "common/Widget.h"
-#include "IPersistenceManager.h"
+#include "persistence_manager.h"
#include "u8g2.h"
class MenuItem;
@@ -28,7 +28,7 @@ class MenuItem;
* @details This structure serves as a configuration container that provides menu widgets
* with access to the display system, screen management functions, input
* handling callbacks, and persistent storage.
- *
+ *
* @see Widget
* @see ButtonType
* @see IPersistenceManager
@@ -61,14 +61,8 @@ typedef struct
std::function 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 persistenceManager;
+ persistence_manager_t *persistenceManager;
} menu_options_t;
\ No newline at end of file
diff --git a/firmware/components/insa/include/common/Menu.h b/firmware/components/insa/include/common/Menu.h
index a9b992a..e8794c3 100644
--- a/firmware/components/insa/include/common/Menu.h
+++ b/firmware/components/insa/include/common/Menu.h
@@ -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);
diff --git a/firmware/components/insa/include/common/Widget.h b/firmware/components/insa/include/common/Widget.h
index 603c6b4..a6d03ca 100644
--- a/firmware/components/insa/include/common/Widget.h
+++ b/firmware/components/insa/include/common/Widget.h
@@ -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 s_instances;
+ static void globalMessageCallback(const message_t *msg);
};
\ No newline at end of file
diff --git a/firmware/components/insa/include/ui/LightMenu.h b/firmware/components/insa/include/ui/LightMenu.h
index 10a4d43..d393466 100644
--- a/firmware/components/insa/include/ui/LightMenu.h
+++ b/firmware/components/insa/include/ui/LightMenu.h
@@ -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.
diff --git a/firmware/components/insa/src/common/Menu.cpp b/firmware/components/insa/src/common/Menu.cpp
index 3f5742a..efacc65 100644
--- a/firmware/components/insa/src/common/Menu.cpp
+++ b/firmware/components/insa/src/common/Menu.cpp
@@ -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();
diff --git a/firmware/components/insa/src/common/Widget.cpp b/firmware/components/insa/src/common/Widget.cpp
index 16054b0..274efd9 100644
--- a/firmware/components/insa/src/common/Widget.cpp
+++ b/firmware/components/insa/src/common/Widget.cpp
@@ -1,7 +1,20 @@
#include "common/Widget.h"
+#include
+
+std::vector 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);
+ }
+}
diff --git a/firmware/components/insa/src/ui/ClockScreenSaver.cpp b/firmware/components/insa/src/ui/ClockScreenSaver.cpp
index 0b45171..3692c37 100644
--- a/firmware/components/insa/src/ui/ClockScreenSaver.cpp
+++ b/firmware/components/insa/src/ui/ClockScreenSaver.cpp
@@ -1,5 +1,5 @@
#include "ui/ClockScreenSaver.h"
-#include "hal_esp32/PersistenceManager.h"
+#include "persistence_manager.h"
#include "simulator.h"
#include
#include
@@ -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;
}
}
diff --git a/firmware/components/insa/src/ui/LightMenu.cpp b/firmware/components/insa/src/ui/LightMenu.cpp
index 05dfb71..4adbb53 100644
--- a/firmware/components/insa/src/ui/LightMenu.cpp
+++ b/firmware/components/insa/src/ui/LightMenu.cpp
@@ -1,7 +1,8 @@
#include "ui/LightMenu.h"
-
#include "led_strip_ws2812.h"
+#include "message_manager.h"
#include "simulator.h"
+#include
/**
* @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)
diff --git a/firmware/components/led-manager/include/color.h b/firmware/components/led-manager/include/color.h
index 6de1694..fee62f1 100644
--- a/firmware/components/led-manager/include/color.h
+++ b/firmware/components/led-manager/include/color.h
@@ -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
diff --git a/firmware/components/led-manager/include/led_segment.h b/firmware/components/led-manager/include/led_segment.h
new file mode 100644
index 0000000..8a1af7c
--- /dev/null
+++ b/firmware/components/led-manager/include/led_segment.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include
+#include
+
+#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;
diff --git a/firmware/components/led-manager/src/color.c b/firmware/components/led-manager/src/color.c
index 28ae1ac..771a517 100644
--- a/firmware/components/led-manager/src/color.c
+++ b/firmware/components/led-manager/src/color.c
@@ -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;
diff --git a/firmware/components/message-manager/CMakeLists.txt b/firmware/components/message-manager/CMakeLists.txt
new file mode 100644
index 0000000..32926cd
--- /dev/null
+++ b/firmware/components/message-manager/CMakeLists.txt
@@ -0,0 +1,8 @@
+idf_component_register(
+ SRCS "src/message_manager.c"
+ INCLUDE_DIRS "include"
+ PRIV_REQUIRES
+ persistence-manager
+ my_mqtt_client
+ app_update
+)
diff --git a/firmware/components/message-manager/include/message_manager.h b/firmware/components/message-manager/include/message_manager.h
new file mode 100644
index 0000000..c53a116
--- /dev/null
+++ b/firmware/components/message-manager/include/message_manager.h
@@ -0,0 +1,79 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#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
diff --git a/firmware/components/message-manager/src/message_manager.c b/firmware/components/message-manager/src/message_manager.c
new file mode 100644
index 0000000..5550cae
--- /dev/null
+++ b/firmware/components/message-manager/src/message_manager.c
@@ -0,0 +1,134 @@
+#include "message_manager.h"
+#include "my_mqtt_client.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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;
+}
diff --git a/firmware/components/my_mqtt_client/CMakeLists.txt b/firmware/components/my_mqtt_client/CMakeLists.txt
new file mode 100644
index 0000000..f3c20a5
--- /dev/null
+++ b/firmware/components/my_mqtt_client/CMakeLists.txt
@@ -0,0 +1,7 @@
+idf_component_register(
+ SRCS "src/my_mqtt_client.c"
+ INCLUDE_DIRS "include"
+ REQUIRES
+ mqtt
+ app_update
+)
\ No newline at end of file
diff --git a/firmware/components/my_mqtt_client/Kconfig b/firmware/components/my_mqtt_client/Kconfig
new file mode 100644
index 0000000..baf7e88
--- /dev/null
+++ b/firmware/components/my_mqtt_client/Kconfig
@@ -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
diff --git a/firmware/components/my_mqtt_client/README.md b/firmware/components/my_mqtt_client/README.md
new file mode 100644
index 0000000..1d59eb2
--- /dev/null
+++ b/firmware/components/my_mqtt_client/README.md
@@ -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`.
diff --git a/firmware/components/my_mqtt_client/idf_component.yml b/firmware/components/my_mqtt_client/idf_component.yml
new file mode 100644
index 0000000..a63de67
--- /dev/null
+++ b/firmware/components/my_mqtt_client/idf_component.yml
@@ -0,0 +1,2 @@
+dependencies:
+ espressif/mqtt: ^1.0.0
diff --git a/firmware/components/my_mqtt_client/include/my_mqtt_client.h b/firmware/components/my_mqtt_client/include/my_mqtt_client.h
new file mode 100644
index 0000000..eab36f9
--- /dev/null
+++ b/firmware/components/my_mqtt_client/include/my_mqtt_client.h
@@ -0,0 +1,14 @@
+#pragma once
+#include
+#include
+
+#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
diff --git a/firmware/components/my_mqtt_client/src/my_mqtt_client.c b/firmware/components/my_mqtt_client/src/my_mqtt_client.c
new file mode 100644
index 0000000..ffbe2fd
--- /dev/null
+++ b/firmware/components/my_mqtt_client/src/my_mqtt_client.c
@@ -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!");
+ }
+}
diff --git a/firmware/components/persistence-manager/CMakeLists.txt b/firmware/components/persistence-manager/CMakeLists.txt
index ea6266b..e453012 100644
--- a/firmware/components/persistence-manager/CMakeLists.txt
+++ b/firmware/components/persistence-manager/CMakeLists.txt
@@ -1,5 +1,5 @@
idf_component_register(SRCS
- src/PersistenceManager.cpp
+ src/persistence_manager.c
INCLUDE_DIRS "include"
REQUIRES
nvs_flash
diff --git a/firmware/components/persistence-manager/include/IPersistenceManager.h b/firmware/components/persistence-manager/include/IPersistenceManager.h
deleted file mode 100644
index 284e733..0000000
--- a/firmware/components/persistence-manager/include/IPersistenceManager.h
+++ /dev/null
@@ -1,108 +0,0 @@
-#pragma once
-
-#include
-#include
-
-/**
- * @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
- void SetValue(const std::string& key, const T& value) {
- static_assert(std::is_same_v ||
- std::is_same_v ||
- std::is_same_v ||
- std::is_same_v ||
- std::is_same_v,
- "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
- [[nodiscard]] T GetValue(const std::string& key, const T& defaultValue = T{}) const {
- return GetValueImpl(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
- [[nodiscard]] T GetValueImpl(const std::string& key, const T& defaultValue) const
- {
- if constexpr (std::is_same_v) {
- return GetValueImpl(key, static_cast(defaultValue));
- } else if constexpr (std::is_same_v) {
- return GetValueImpl(key, static_cast(defaultValue));
- } else if constexpr (std::is_same_v) {
- return GetValueImpl(key, static_cast(defaultValue));
- } else if constexpr (std::is_same_v) {
- return GetValueImpl(key, static_cast(defaultValue));
- } else if constexpr (std::is_same_v) {
- return GetValueImpl(key, static_cast(defaultValue));
- } else {
- static_assert(std::is_same_v ||
- std::is_same_v ||
- std::is_same_v ||
- std::is_same_v ||
- std::is_same_v,
- "Unsupported type for IPersistenceManager");
- return defaultValue; // This line will never be reached, but satisfies the compiler
- }
- }
-};
\ No newline at end of file
diff --git a/firmware/components/persistence-manager/include/hal_esp32/PersistenceManager.h b/firmware/components/persistence-manager/include/hal_esp32/PersistenceManager.h
deleted file mode 100644
index 723e152..0000000
--- a/firmware/components/persistence-manager/include/hal_esp32/PersistenceManager.h
+++ /dev/null
@@ -1,58 +0,0 @@
-#pragma once
-
-#include "IPersistenceManager.h"
-#include
-#include
-
-#include
-#include
-
-/**
- * @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;
-};
\ No newline at end of file
diff --git a/firmware/components/persistence-manager/include/persistence_manager.h b/firmware/components/persistence-manager/include/persistence_manager.h
new file mode 100644
index 0000000..140952f
--- /dev/null
+++ b/firmware/components/persistence-manager/include/persistence_manager.h
@@ -0,0 +1,237 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#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
diff --git a/firmware/components/persistence-manager/src/PersistenceManager.cpp b/firmware/components/persistence-manager/src/PersistenceManager.cpp
deleted file mode 100644
index fca3591..0000000
--- a/firmware/components/persistence-manager/src/PersistenceManager.cpp
+++ /dev/null
@@ -1,279 +0,0 @@
-#include "hal_esp32/PersistenceManager.h"
-#include
-#include
-
-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(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;
-}
diff --git a/firmware/components/persistence-manager/src/persistence_manager.c b/firmware/components/persistence-manager/src/persistence_manager.c
new file mode 100644
index 0000000..51b8b14
--- /dev/null
+++ b/firmware/components/persistence-manager/src/persistence_manager.c
@@ -0,0 +1,274 @@
+#include "persistence_manager.h"
+#include
+#include
+
+#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;
+}
diff --git a/firmware/components/simulator/CMakeLists.txt b/firmware/components/simulator/CMakeLists.txt
index de0f075..b19e264 100644
--- a/firmware/components/simulator/CMakeLists.txt
+++ b/firmware/components/simulator/CMakeLists.txt
@@ -5,5 +5,6 @@ idf_component_register(SRCS
PRIV_REQUIRES
led-manager
persistence-manager
+ message-manager
spiffs
)
diff --git a/firmware/components/simulator/include/simulator.h b/firmware/components/simulator/include/simulator.h
index f15f057..9614a2a 100644
--- a/firmware/components/simulator/include/simulator.h
+++ b/firmware/components/simulator/include/simulator.h
@@ -9,12 +9,20 @@ typedef struct
int cycle_duration_minutes;
} simulation_config_t;
-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);
-void cleanup_light_items(void);
-void start_simulate_day(void);
-void start_simulate_night(void);
-void start_simulation_task(void);
-void stop_simulation_task(void);
-void start_simulation(void);
+#ifdef __cplusplus
+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);
+ void cleanup_light_items(void);
+ void start_simulate_day(void);
+ void start_simulate_night(void);
+ void start_simulation_task(void);
+ void stop_simulation_task(void);
+ void start_simulation(void);
+#ifdef __cplusplus
+}
+#endif
\ No newline at end of file
diff --git a/firmware/components/simulator/include/storage.h b/firmware/components/simulator/include/storage.h
index 642ac66..9c67231 100644
--- a/firmware/components/simulator/include/storage.h
+++ b/firmware/components/simulator/include/storage.h
@@ -1,4 +1,23 @@
#pragma once
-void initialize_storage();
-void load_file(const char *filename);
+#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
diff --git a/firmware/components/simulator/src/simulator.cpp b/firmware/components/simulator/src/simulator.cpp
index 2a02e31..3eb1a7d 100644
--- a/firmware/components/simulator/src/simulator.cpp
+++ b/firmware/components/simulator/src/simulator.cpp
@@ -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
#include
@@ -15,12 +16,12 @@
#include
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,45 +310,51 @@ void simulate_cycle(void *args)
light_item_node_t *current_item = find_best_light_item_for_time(hhmm);
light_item_node_t *next_item = find_next_light_item_for_time(hhmm);
- if (current_item != NULL && next_item != NULL)
+ if (current_item != NULL)
{
- int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100);
- int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
+ rgb_t color = {0, 0, 0};
- if (next_item_time_min < current_item_time_min)
+ // Use head as fallback if next_item is NULL
+ next_item = next_item ? next_item : head;
+ if (next_item != NULL)
{
- next_item_time_min += total_minutes_in_day;
- }
+ int current_item_time_min = (atoi(current_item->time) / 100) * 60 + (atoi(current_item->time) % 100);
+ int next_item_time_min = (atoi(next_item->time) / 100) * 60 + (atoi(next_item->time) % 100);
- int minutes_since_current_item_start = current_minute_of_day - current_item_time_min;
- if (minutes_since_current_item_start < 0)
+ if (next_item_time_min < current_item_time_min)
+ {
+ next_item_time_min += total_minutes_in_day;
+ }
+
+ int minutes_since_current_item_start = current_minute_of_day - current_item_time_min;
+ if (minutes_since_current_item_start < 0)
+ {
+ minutes_since_current_item_start += total_minutes_in_day;
+ }
+
+ int interval_duration = next_item_time_min - current_item_time_min;
+ if (interval_duration == 0)
+ {
+ interval_duration = 1;
+ }
+
+ float interpolation_factor = (float)minutes_since_current_item_start / (float)interval_duration;
+
+ // Prepare colors for interpolation
+ rgb_t start_rgb = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
+ rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
+
+ // Use the interpolation function
+ color = interpolate_color(start_rgb, end_rgb, interpolation_factor);
+ led_strip_update(LED_STATE_SIMULATION, color);
+ }
+ else
{
- minutes_since_current_item_start += total_minutes_in_day;
+ // No next_item and no head, use only current
+ color = (rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
+ led_strip_update(LED_STATE_SIMULATION, color);
}
-
- int interval_duration = next_item_time_min - current_item_time_min;
- if (interval_duration == 0)
- {
- interval_duration = 1;
- }
-
- float interpolation_factor = (float)minutes_since_current_item_start / (float)interval_duration;
-
- // Prepare colors for interpolation
- rgb_t start_rgb = {.red = current_item->red, .green = current_item->green, .blue = current_item->blue};
- rgb_t end_rgb = {.red = next_item->red, .green = next_item->green, .blue = next_item->blue};
-
- // Use the interpolation function
- rgb_t final_rgb = interpolate_color(start_rgb, end_rgb, interpolation_factor);
-
- led_strip_update(LED_STATE_SIMULATION, final_rgb);
- }
- else if (current_item != NULL)
- {
- // No next item, just use current
- led_strip_update(
- LED_STATE_SIMULATION,
- (rgb_t){.red = current_item->red, .green = current_item->green, .blue = current_item->blue});
+ 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);
}
diff --git a/firmware/components/simulator/src/storage.cpp b/firmware/components/simulator/src/storage.cpp
index 6f5a3d5..e788f62 100644
--- a/firmware/components/simulator/src/storage.cpp
+++ b/firmware/components/simulator/src/storage.cpp
@@ -3,14 +3,22 @@
#include "esp_log.h"
#include "esp_spiffs.h"
#include "simulator.h"
-#include
#include
#include
+#include
+#include
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;
+}
diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt
index da1dab4..d3309e1 100755
--- a/firmware/main/CMakeLists.txt
+++ b/firmware/main/CMakeLists.txt
@@ -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)
diff --git a/firmware/main/idf_component.yml b/firmware/main/idf_component.yml
index dc3a1fe..0a9790b 100755
--- a/firmware/main/idf_component.yml
+++ b/firmware/main/idf_component.yml
@@ -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
diff --git a/firmware/main/app_task.h b/firmware/main/include/app_task.h
similarity index 100%
rename from firmware/main/app_task.h
rename to firmware/main/include/app_task.h
diff --git a/firmware/main/button_handling.h b/firmware/main/include/button_handling.h
similarity index 100%
rename from firmware/main/button_handling.h
rename to firmware/main/include/button_handling.h
diff --git a/firmware/main/common.h b/firmware/main/include/common.h
similarity index 100%
rename from firmware/main/common.h
rename to firmware/main/include/common.h
diff --git a/firmware/main/hal/u8g2_esp32_hal.h b/firmware/main/include/hal/u8g2_esp32_hal.h
similarity index 100%
rename from firmware/main/hal/u8g2_esp32_hal.h
rename to firmware/main/include/hal/u8g2_esp32_hal.h
diff --git a/firmware/main/i2c_checker.h b/firmware/main/include/i2c_checker.h
similarity index 100%
rename from firmware/main/i2c_checker.h
rename to firmware/main/include/i2c_checker.h
diff --git a/firmware/main/isrgrootx1.pem b/firmware/main/isrgrootx1.pem
new file mode 100644
index 0000000..b85c803
--- /dev/null
+++ b/firmware/main/isrgrootx1.pem
@@ -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-----
diff --git a/firmware/main/app_task.cpp b/firmware/main/src/app_task.cpp
similarity index 66%
rename from firmware/main/app_task.cpp
rename to firmware/main/src/app_task.cpp
index 63996b1..ef12984 100644
--- a/firmware/main/app_task.cpp
+++ b/firmware/main/src/app_task.cpp
@@ -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
#include
#include
#include
@@ -33,7 +37,8 @@ uint8_t received_signal;
std::shared_ptr m_widget;
std::vector> m_history;
std::unique_ptr m_inactivityTracker;
-std::shared_ptr 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();
+ persistence_manager_init(&g_persistence_manager, "config");
options = {
.u8g2 = &u8g2,
.setScreen = [](const std::shared_ptr &screen) { setScreen(screen); },
.pushScreen = [](const std::shared_ptr &screen) { pushScreen(screen); },
.popScreen = []() { popScreen(); },
.onButtonClicked = nullptr,
- .persistenceManager = m_persistenceManager,
+ .persistenceManager = &g_persistence_manager,
};
m_widget = std::make_shared(&options);
m_inactivityTracker = std::make_unique(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();
diff --git a/firmware/main/button_handling.c b/firmware/main/src/button_handling.c
similarity index 100%
rename from firmware/main/button_handling.c
rename to firmware/main/src/button_handling.c
diff --git a/firmware/main/hal/u8g2_esp32_hal.c b/firmware/main/src/hal/u8g2_esp32_hal.c
similarity index 99%
rename from firmware/main/hal/u8g2_esp32_hal.c
rename to firmware/main/src/hal/u8g2_esp32_hal.c
index fe58c88..09de770 100644
--- a/firmware/main/hal/u8g2_esp32_hal.c
+++ b/firmware/main/src/hal/u8g2_esp32_hal.c
@@ -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;
diff --git a/firmware/main/i2c_checker.c b/firmware/main/src/i2c_checker.c
similarity index 100%
rename from firmware/main/i2c_checker.c
rename to firmware/main/src/i2c_checker.c
diff --git a/firmware/main/main.cpp b/firmware/main/src/main.cpp
similarity index 85%
rename from firmware/main/main.cpp
rename to firmware/main/src/main.cpp
index 1614819..cf29983 100644
--- a/firmware/main/main.cpp
+++ b/firmware/main/src/main.cpp
@@ -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
#include
@@ -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);
diff --git a/firmware/sdkconfig.defaults b/firmware/sdkconfig.defaults
index a4dd8c4..e70ad99 100755
--- a/firmware/sdkconfig.defaults
+++ b/firmware/sdkconfig.defaults
@@ -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
diff --git a/firmware/storage/www/captive.html b/firmware/storage/www/captive.html
new file mode 100644
index 0000000..9e17dbe
--- /dev/null
+++ b/firmware/storage/www/captive.html
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+ System Control - WLAN Setup
+
+
+
+
+
+
+
+
+
+
+
+
+
π System Control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ βΉοΈ Hinweis:
+ Nach dem Speichern verbindet sich das GerΓ€t mit dem gewΓ€hlten
+ Netzwerk.
+ Diese Seite wird dann nicht mehr erreichbar sein. Verbinden Sie sich mit Ihrem normalen WLAN,
+ um auf das GerΓ€t zuzugreifen.
+