Compare commits

4 Commits

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

Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 14:26:13 +01:00
1fade06bdb light on/off via REST or GPIO
Signed-off-by: Peter Siegmund <developer@mars3142.org>
2026-01-18 14:13:05 +01:00
17 changed files with 359 additions and 38 deletions

View File

@@ -13,4 +13,5 @@ idf_component_register(SRCS
json
simulator
persistence-manager
message-manager
)

View File

@@ -1,5 +1,6 @@
#include "api_handlers.h"
#include "common.h"
#include "message_manager.h"
#include <cJSON.h>
#include <esp_http_server.h>
@@ -255,7 +256,22 @@ esp_err_t api_light_power_handler(httpd_req_t *req)
ESP_LOGI(TAG, "Received light power: %s", buf);
// TODO: Parse JSON and control light
cJSON *json = cJSON_Parse(buf);
if (json)
{
cJSON *active = cJSON_GetObjectItem(json, "on");
if (cJSON_IsBool(active))
{
message_t msg = {};
msg.type = MESSAGE_TYPE_SETTINGS;
msg.data.settings.type = SETTINGS_TYPE_BOOL;
strncpy(msg.data.settings.key, "light_active", sizeof(msg.data.settings.key) - 1);
msg.data.settings.value.bool_value = cJSON_IsTrue(active);
message_manager_post(&msg, pdMS_TO_TICKS(100));
}
cJSON_Delete(json);
}
set_cors_headers(req);
return httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
}

View File

@@ -2,12 +2,17 @@
#include <cJSON.h>
#include <stdbool.h>
#include "persistence_manager.h"
// 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();
// TODO: Echte Werte einfügen, aktuell Dummy-Daten
cJSON_AddBoolToObject(json, "on", false);
bool light_active = persistence_manager_get_bool(&pm, "light_active", false);
cJSON_AddBoolToObject(json, "on", light_active);
cJSON_AddBoolToObject(json, "thunder", false);
cJSON_AddStringToObject(json, "mode", "day");
cJSON_AddStringToObject(json, "schema", "schema_03.csv");
@@ -16,5 +21,8 @@ cJSON *create_light_status_json(void)
cJSON_AddNumberToObject(color, "g", 240);
cJSON_AddNumberToObject(color, "b", 220);
cJSON_AddItemToObject(json, "color", color);
persistence_manager_deinit(&pm);
return json;
}

View File

@@ -1,6 +1,8 @@
#include "websocket_handler.h"
#include "api_server.h"
#include "common.h"
#include "message_manager.h"
#include <esp_http_server.h>
#include <esp_log.h>
#include <string.h>
@@ -11,6 +13,16 @@ static const char *TAG = "websocket_handler";
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++)
@@ -179,6 +191,8 @@ static void ws_async_send(void *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",

View File

@@ -29,6 +29,18 @@ static EventGroupHandle_t s_wifi_event_group;
static const char *TAG = "wifi_manager";
static void led_status_reconnect()
{
led_behavior_t led_behavior = {
.on_time_ms = 250,
.off_time_ms = 100,
.color = {.red = 50, .green = 50, .blue = 0},
.index = 0,
.mode = LED_MODE_BLINK,
};
led_status_set_behavior(led_behavior);
}
static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
{
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START)
@@ -40,6 +52,8 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t e
{
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)
{
@@ -49,8 +63,10 @@ static void wifi_event_handler(void *arg, esp_event_base_t event_base, int32_t e
}
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP)
{
ESP_LOGW(TAG, "IP_EVENT_STA_LOST_IP: IP-Adresse verloren!");
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();
}
}
@@ -105,14 +121,7 @@ void wifi_manager_init()
if (have_ssid && have_password)
{
led_behavior_t led_behavior = {
.on_time_ms = 250,
.off_time_ms = 100,
.color = {.red = 50, .green = 50, .blue = 0},
.index = 0,
.mode = LED_MODE_BLINK,
};
led_status_set_behavior(led_behavior);
led_status_reconnect();
ESP_LOGI(TAG, "Found WiFi configuration: SSID='%s'", ssid);
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ constexpr int BOTTOM_OFFSET = 10;
Menu::Menu(menu_options_t *options) : Widget(options->u8g2), m_options(options)
{
// Set up button callback using lambda to forward to member function
// Set up button callback using a lambda to forward to the member function
m_options->onButtonClicked = [this](const ButtonType button) { OnButtonClicked(button); };
}
@@ -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;
@@ -134,13 +140,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 +157,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 +182,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 +212,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 +278,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 +287,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 +320,7 @@ void Menu::onPressedSelect() const
void Menu::onPressedBack() const
{
// Navigate back to previous screen if callback is available
// Navigate back to the previous screen if callback is available
if (m_options && m_options->popScreen)
{
m_options->popScreen();

View File

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

View File

@@ -1,7 +1,8 @@
#include "ui/LightMenu.h"
#include "led_strip_ws2812.h"
#include "message_manager.h"
#include "simulator.h"
#include <cstring>
/**
* @namespace LightMenuItem
@@ -71,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)
{
persistence_manager_set_bool(m_options->persistenceManager, 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;
}
@@ -127,4 +129,15 @@ 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 && msg->data.settings.type == SETTINGS_TYPE_BOOL &&
std::strcmp(msg->data.settings.key, "light_active") == 0)
{
setToggle(getItem(LightMenuItem::ACTIVATE), msg->data.settings.value.bool_value);
}
}
IMPLEMENT_GET_NAME(LightMenu)

View File

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

View File

@@ -0,0 +1,69 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C"
{
#endif
typedef enum
{
MESSAGE_TYPE_SETTINGS,
MESSAGE_TYPE_BUTTON
} message_type_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
{
message_type_t type;
union {
settings_message_t settings;
button_message_t button;
} data;
} message_t;
// Observer Pattern: Listener-Typ und Registrierungsfunktionen
typedef void (*message_listener_t)(const message_t *msg);
void message_manager_register_listener(message_listener_t listener);
void message_manager_unregister_listener(message_listener_t listener);
void message_manager_init(void);
bool message_manager_post(const message_t *msg, TickType_t timeout);
#ifdef __cplusplus
}
#endif

View File

@@ -0,0 +1,106 @@
#include "message_manager.h"
#include <esp_log.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/task.h>
#include <persistence_manager.h>
#include <string.h>
#define MESSAGE_QUEUE_LENGTH 16
#define MESSAGE_QUEUE_ITEM_SIZE sizeof(message_t)
static const char *TAG = "message_manager";
static QueueHandle_t message_queue = NULL;
// Observer Pattern: Listener-Liste
#define MAX_MESSAGE_LISTENERS 8
static message_listener_t message_listeners[MAX_MESSAGE_LISTENERS] = {0};
static size_t message_listener_count = 0;
void message_manager_register_listener(message_listener_t listener) {
if (listener && message_listener_count < MAX_MESSAGE_LISTENERS) {
// Doppelte Registrierung vermeiden
for (size_t i = 0; i < message_listener_count; ++i) {
if (message_listeners[i] == listener) return;
}
message_listeners[message_listener_count++] = listener;
}
}
void message_manager_unregister_listener(message_listener_t listener) {
for (size_t i = 0; i < message_listener_count; ++i) {
if (message_listeners[i] == listener) {
// Nachfolgende Listener nach vorne schieben
for (size_t j = i; j < message_listener_count - 1; ++j) {
message_listeners[j] = message_listeners[j + 1];
}
message_listeners[--message_listener_count] = NULL;
break;
}
}
}
static void message_manager_task(void *param)
{
message_t msg;
persistence_manager_t pm;
while (1)
{
if (xQueueReceive(message_queue, &msg, portMAX_DELAY) == pdTRUE)
{
switch (msg.type)
{
case MESSAGE_TYPE_SETTINGS:
if (persistence_manager_init(&pm, "config") == ESP_OK)
{
switch (msg.data.settings.type)
{
case SETTINGS_TYPE_BOOL:
persistence_manager_set_bool(&pm, msg.data.settings.key, msg.data.settings.value.bool_value);
break;
case SETTINGS_TYPE_INT:
persistence_manager_set_int(&pm, msg.data.settings.key, msg.data.settings.value.int_value);
break;
case SETTINGS_TYPE_FLOAT:
persistence_manager_set_float(&pm, msg.data.settings.key, msg.data.settings.value.float_value);
break;
case SETTINGS_TYPE_STRING:
persistence_manager_set_string(&pm, msg.data.settings.key,
msg.data.settings.value.string_value);
break;
}
persistence_manager_deinit(&pm);
ESP_LOGI(TAG, "Setting written: %s", msg.data.settings.key);
}
break;
case MESSAGE_TYPE_BUTTON:
ESP_LOGI(TAG, "Button event: id=%d, type=%d", msg.data.button.button_id, msg.data.button.event_type);
// TODO: Weiterverarbeitung/Callback für Button-Events
break;
}
// Observer Pattern: Listener benachrichtigen
for (size_t i = 0; i < message_listener_count; ++i) {
if (message_listeners[i]) {
message_listeners[i](&msg);
}
}
}
}
}
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_LOGI(TAG, "Post: type=%d", msg->type);
return xQueueSend(message_queue, msg, timeout) == pdTRUE;
}

View File

@@ -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,
#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);
void cleanup_light_items(void);
void start_simulate_day(void);
void start_simulate_night(void);
void start_simulation_task(void);
void stop_simulation_task(void);
void start_simulation(void);
#ifdef __cplusplus
}
#endif

View File

@@ -7,12 +7,14 @@
#include "hal/u8g2_esp32_hal.h"
#include "i2c_checker.h"
#include "led_status.h"
#include "message_manager.h"
#include "persistence_manager.h"
#include "simulator.h"
#include "ui/ClockScreenSaver.h"
#include "ui/ScreenSaver.h"
#include "ui/SplashScreen.h"
#include "wifi_manager.h"
#include <cstring>
#include <driver/i2c.h>
#include <esp_diagnostics.h>
#include <esp_insights.h>
@@ -126,6 +128,15 @@ static void init_ui(void)
u8g2_SendBuffer(&u8g2);
}
static void on_message_received(const message_t *msg)
{
if (msg && msg->type == MESSAGE_TYPE_SETTINGS && msg->data.settings.type == SETTINGS_TYPE_BOOL &&
std::strcmp(msg->data.settings.key, "light_active") == 0)
{
start_simulation();
}
}
static void handle_button(uint8_t button)
{
m_inactivityTracker->reset();
@@ -232,11 +243,15 @@ void app_task(void *args)
}
}
message_manager_init();
setup_buttons();
init_ui();
wifi_manager_init();
message_manager_register_listener(on_message_received);
start_simulation();
auto oldTime = esp_timer_get_time();