diff --git a/.cache/clangd/index/ButtonWidget.cpp.6932614AE5FC71F9.idx b/.cache/clangd/index/ButtonWidget.cpp.6932614AE5FC71F9.idx new file mode 100644 index 0000000..92a9d6f Binary files /dev/null and b/.cache/clangd/index/ButtonWidget.cpp.6932614AE5FC71F9.idx differ diff --git a/.cache/clangd/index/ButtonWidget.hpp.551D0D3595AEDB81.idx b/.cache/clangd/index/ButtonWidget.hpp.551D0D3595AEDB81.idx new file mode 100644 index 0000000..7f42f37 Binary files /dev/null and b/.cache/clangd/index/ButtonWidget.hpp.551D0D3595AEDB81.idx differ diff --git a/.cache/clangd/index/ConfigHandlers.cpp.9A6D389FB90DEEB0.idx b/.cache/clangd/index/ConfigHandlers.cpp.9A6D389FB90DEEB0.idx new file mode 100644 index 0000000..2848ec8 Binary files /dev/null and b/.cache/clangd/index/ConfigHandlers.cpp.9A6D389FB90DEEB0.idx differ diff --git a/.cache/clangd/index/FileManagerHandlers.cpp.2F53BB33AABE7329.idx b/.cache/clangd/index/FileManagerHandlers.cpp.2F53BB33AABE7329.idx new file mode 100644 index 0000000..b1e7061 Binary files /dev/null and b/.cache/clangd/index/FileManagerHandlers.cpp.2F53BB33AABE7329.idx differ diff --git a/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx b/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx index 5a2df32..ec388b8 100644 Binary files a/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx and b/.cache/clangd/index/Gui.cpp.BDF53DB313293DD9.idx differ diff --git a/.cache/clangd/index/KnxHandlers.cpp.CB0E869699204D5E.idx b/.cache/clangd/index/KnxHandlers.cpp.CB0E869699204D5E.idx new file mode 100644 index 0000000..f56e822 Binary files /dev/null and b/.cache/clangd/index/KnxHandlers.cpp.CB0E869699204D5E.idx differ diff --git a/.cache/clangd/index/LabelWidget.cpp.86E5A1BF3C34B7BC.idx b/.cache/clangd/index/LabelWidget.cpp.86E5A1BF3C34B7BC.idx new file mode 100644 index 0000000..566e6d8 Binary files /dev/null and b/.cache/clangd/index/LabelWidget.cpp.86E5A1BF3C34B7BC.idx differ diff --git a/.cache/clangd/index/LabelWidget.hpp.C810267C2FCC36BA.idx b/.cache/clangd/index/LabelWidget.hpp.C810267C2FCC36BA.idx new file mode 100644 index 0000000..7584a44 Binary files /dev/null and b/.cache/clangd/index/LabelWidget.hpp.C810267C2FCC36BA.idx differ diff --git a/.cache/clangd/index/LedWidget.cpp.3E101A9B7D9821AD.idx b/.cache/clangd/index/LedWidget.cpp.3E101A9B7D9821AD.idx new file mode 100644 index 0000000..a837a94 Binary files /dev/null and b/.cache/clangd/index/LedWidget.cpp.3E101A9B7D9821AD.idx differ diff --git a/.cache/clangd/index/LedWidget.hpp.C3DFB6FAB9F8428E.idx b/.cache/clangd/index/LedWidget.hpp.C3DFB6FAB9F8428E.idx new file mode 100644 index 0000000..0b51d37 Binary files /dev/null and b/.cache/clangd/index/LedWidget.hpp.C3DFB6FAB9F8428E.idx differ diff --git a/.cache/clangd/index/StaticFileHandlers.cpp.7B27CFEA2E9C1EF7.idx b/.cache/clangd/index/StaticFileHandlers.cpp.7B27CFEA2E9C1EF7.idx new file mode 100644 index 0000000..82e55c4 Binary files /dev/null and b/.cache/clangd/index/StaticFileHandlers.cpp.7B27CFEA2E9C1EF7.idx differ diff --git a/.cache/clangd/index/StatusHandlers.cpp.1021770D2A2A95F2.idx b/.cache/clangd/index/StatusHandlers.cpp.1021770D2A2A95F2.idx new file mode 100644 index 0000000..4c987f9 Binary files /dev/null and b/.cache/clangd/index/StatusHandlers.cpp.1021770D2A2A95F2.idx differ diff --git a/.cache/clangd/index/Touch.cpp.EB81940633A1140F.idx b/.cache/clangd/index/Touch.cpp.EB81940633A1140F.idx index 2ac13a7..cd12240 100644 Binary files a/.cache/clangd/index/Touch.cpp.EB81940633A1140F.idx and b/.cache/clangd/index/Touch.cpp.EB81940633A1140F.idx differ diff --git a/.cache/clangd/index/WebServer.cpp.552E49883800B71B.idx b/.cache/clangd/index/WebServer.cpp.552E49883800B71B.idx new file mode 100644 index 0000000..3b4fbb5 Binary files /dev/null and b/.cache/clangd/index/WebServer.cpp.552E49883800B71B.idx differ diff --git a/.cache/clangd/index/WebServer.hpp.4D1C3D9E0DDB58E5.idx b/.cache/clangd/index/WebServer.hpp.4D1C3D9E0DDB58E5.idx new file mode 100644 index 0000000..1cf60d4 Binary files /dev/null and b/.cache/clangd/index/WebServer.hpp.4D1C3D9E0DDB58E5.idx differ diff --git a/.cache/clangd/index/Widget.cpp.63DB7B9186B85891.idx b/.cache/clangd/index/Widget.cpp.63DB7B9186B85891.idx new file mode 100644 index 0000000..819d584 Binary files /dev/null and b/.cache/clangd/index/Widget.cpp.63DB7B9186B85891.idx differ diff --git a/.cache/clangd/index/Widget.hpp.63E5D98A9B23E60F.idx b/.cache/clangd/index/Widget.hpp.63E5D98A9B23E60F.idx new file mode 100644 index 0000000..64e6887 Binary files /dev/null and b/.cache/clangd/index/Widget.hpp.63E5D98A9B23E60F.idx differ diff --git a/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx b/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx new file mode 100644 index 0000000..d246069 Binary files /dev/null and b/.cache/clangd/index/WidgetConfig.cpp.FD56F9F36C29A5DA.idx differ diff --git a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx index f872fcd..c5d6d71 100644 Binary files a/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx and b/.cache/clangd/index/WidgetConfig.hpp.CAEFE2EEEB2A6996.idx differ diff --git a/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx b/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx new file mode 100644 index 0000000..cca6dac Binary files /dev/null and b/.cache/clangd/index/WidgetFactory.cpp.1026CAEFCA630F22.idx differ diff --git a/.cache/clangd/index/WidgetFactory.hpp.6C7FACD7AA34E439.idx b/.cache/clangd/index/WidgetFactory.hpp.6C7FACD7AA34E439.idx new file mode 100644 index 0000000..045390f Binary files /dev/null and b/.cache/clangd/index/WidgetFactory.hpp.6C7FACD7AA34E439.idx differ diff --git a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx index f41ecf1..b204108 100644 Binary files a/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx and b/.cache/clangd/index/WidgetManager.cpp.D8CE609DC911F13E.idx differ diff --git a/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx b/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx index 212ea47..c2e8389 100644 Binary files a/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx and b/.cache/clangd/index/WidgetManager.hpp.8559171B7B8A010C.idx differ diff --git a/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx b/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx index a60511c..7dcb366 100644 Binary files a/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx and b/.cache/clangd/index/main.cpp.7C677863E2582AB3.idx differ diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 386a88b..8c22062 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,9 @@ -idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" "Touch.cpp" "Gui.cpp" "Wifi.cpp" "LvglIdle.c" "Gui/WifiSetting.cpp" "Gui/EthSetting.cpp" "Hardware/Eth.cpp" "WidgetManager.cpp" "SdCard.cpp" +idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" "Touch.cpp" "Gui.cpp" "Wifi.cpp" "LvglIdle.c" "Gui/WifiSetting.cpp" "Gui/EthSetting.cpp" "Hardware/Eth.cpp" "WidgetManager.cpp" "WidgetConfig.cpp" "SdCard.cpp" + "widgets/Widget.cpp" + "widgets/LabelWidget.cpp" + "widgets/ButtonWidget.cpp" + "widgets/LedWidget.cpp" + "widgets/WidgetFactory.cpp" "webserver/WebServer.cpp" "webserver/StaticFileHandlers.cpp" "webserver/ConfigHandlers.cpp" @@ -7,5 +12,5 @@ idf_component_register(SRCS "KnxWorker.cpp" "Nvs.cpp" "main.cpp" "Display.cpp" " "webserver/FileManagerHandlers.cpp" PRIV_REQUIRES spi_flash esp_driver_ppa esp_lcd usb REQUIRES esp_mm esp_eth esp_driver_ppa esp_timer lvgl knx ethernet_init esp_wifi_remote esp_netif esp_event nvs_flash esp_http_server fatfs sdmmc json tinyusb - INCLUDE_DIRS "webserver" + INCLUDE_DIRS "webserver" "widgets" EMBED_TXTFILES "embedded/filemanager.html") diff --git a/main/WidgetConfig.cpp b/main/WidgetConfig.cpp new file mode 100644 index 0000000..d26f8c9 --- /dev/null +++ b/main/WidgetConfig.cpp @@ -0,0 +1,222 @@ +#include "WidgetConfig.hpp" +#include +#include + +// WidgetConfig implementation +void WidgetConfig::serialize(uint8_t* buf) const { + memset(buf, 0, SERIALIZED_SIZE); + size_t pos = 0; + + buf[pos++] = id; + buf[pos++] = static_cast(type); + buf[pos++] = x & 0xFF; buf[pos++] = (x >> 8) & 0xFF; + buf[pos++] = y & 0xFF; buf[pos++] = (y >> 8) & 0xFF; + buf[pos++] = width & 0xFF; buf[pos++] = (width >> 8) & 0xFF; + buf[pos++] = height & 0xFF; buf[pos++] = (height >> 8) & 0xFF; + buf[pos++] = visible ? 1 : 0; + + buf[pos++] = static_cast(textSource); + memcpy(&buf[pos], text, MAX_TEXT_LEN); pos += MAX_TEXT_LEN; + buf[pos++] = knxAddress & 0xFF; buf[pos++] = (knxAddress >> 8) & 0xFF; + buf[pos++] = fontSize; + + buf[pos++] = textColor.r; buf[pos++] = textColor.g; buf[pos++] = textColor.b; + buf[pos++] = bgColor.r; buf[pos++] = bgColor.g; buf[pos++] = bgColor.b; + buf[pos++] = bgOpacity; + buf[pos++] = borderRadius; + + buf[pos++] = shadow.offsetX; + buf[pos++] = shadow.offsetY; + buf[pos++] = shadow.blur; + buf[pos++] = shadow.spread; + buf[pos++] = shadow.color.r; buf[pos++] = shadow.color.g; buf[pos++] = shadow.color.b; + buf[pos++] = shadow.enabled ? 1 : 0; + + buf[pos++] = isToggle ? 1 : 0; + buf[pos++] = knxAddressWrite & 0xFF; buf[pos++] = (knxAddressWrite >> 8) & 0xFF; + buf[pos++] = static_cast(action); + buf[pos++] = targetScreen; +} + +void WidgetConfig::deserialize(const uint8_t* buf) { + size_t pos = 0; + + id = buf[pos++]; + type = static_cast(buf[pos++]); + x = buf[pos] | (buf[pos+1] << 8); pos += 2; + y = buf[pos] | (buf[pos+1] << 8); pos += 2; + width = buf[pos] | (buf[pos+1] << 8); pos += 2; + height = buf[pos] | (buf[pos+1] << 8); pos += 2; + visible = buf[pos++] != 0; + + textSource = static_cast(buf[pos++]); + memcpy(text, &buf[pos], MAX_TEXT_LEN); pos += MAX_TEXT_LEN; + text[MAX_TEXT_LEN - 1] = '\0'; + knxAddress = buf[pos] | (buf[pos+1] << 8); pos += 2; + fontSize = buf[pos++]; + + textColor.r = buf[pos++]; textColor.g = buf[pos++]; textColor.b = buf[pos++]; + bgColor.r = buf[pos++]; bgColor.g = buf[pos++]; bgColor.b = buf[pos++]; + bgOpacity = buf[pos++]; + borderRadius = buf[pos++]; + + shadow.offsetX = static_cast(buf[pos++]); + shadow.offsetY = static_cast(buf[pos++]); + shadow.blur = buf[pos++]; + shadow.spread = buf[pos++]; + shadow.color.r = buf[pos++]; shadow.color.g = buf[pos++]; shadow.color.b = buf[pos++]; + shadow.enabled = buf[pos++] != 0; + + isToggle = buf[pos++] != 0; + knxAddressWrite = buf[pos] | (buf[pos+1] << 8); + pos += 2; + action = static_cast(buf[pos++]); + targetScreen = buf[pos++]; +} + +WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) { + WidgetConfig cfg = {}; + cfg.id = id; + cfg.type = WidgetType::LABEL; + cfg.x = x; + cfg.y = y; + cfg.width = 150; + cfg.height = 40; + cfg.visible = true; + cfg.textSource = TextSource::STATIC; + strncpy(cfg.text, labelText, MAX_TEXT_LEN - 1); + cfg.fontSize = 1; // 18pt + cfg.textColor = {255, 255, 255}; + cfg.bgColor = {0, 0, 0}; + cfg.bgOpacity = 0; + cfg.borderRadius = 0; + cfg.shadow.enabled = false; + return cfg; +} + +WidgetConfig WidgetConfig::createKnxLabel(uint8_t id, int16_t x, int16_t y, + TextSource source, uint16_t knxAddr, const char* format) { + WidgetConfig cfg = createLabel(id, x, y, format); + cfg.textSource = source; + cfg.knxAddress = knxAddr; + return cfg; +} + +WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y, + const char* labelText, uint16_t knxAddrWrite, bool toggle) { + WidgetConfig cfg = {}; + cfg.id = id; + cfg.type = WidgetType::BUTTON; + cfg.x = x; + cfg.y = y; + cfg.width = 120; + cfg.height = 50; + cfg.visible = true; + cfg.textSource = TextSource::STATIC; + strncpy(cfg.text, labelText, MAX_TEXT_LEN - 1); + cfg.fontSize = 1; + cfg.textColor = {255, 255, 255}; + cfg.bgColor = {33, 150, 243}; // Blue + cfg.bgOpacity = 255; + cfg.borderRadius = 8; + cfg.shadow.enabled = true; + cfg.shadow.offsetX = 2; + cfg.shadow.offsetY = 2; + cfg.shadow.blur = 8; + cfg.shadow.spread = 0; + cfg.shadow.color = {0, 0, 0}; + cfg.isToggle = toggle; + cfg.knxAddressWrite = knxAddrWrite; + cfg.action = ButtonAction::KNX; + cfg.targetScreen = 0; + return cfg; +} + +// ScreenConfig implementation +void ScreenConfig::clear(uint8_t newId, const char* newName) { + id = newId; + mode = ScreenMode::FULLSCREEN; + backgroundColor = {26, 26, 46}; // Dark blue background + widgetCount = 0; + memset(widgets, 0, sizeof(widgets)); + memset(name, 0, sizeof(name)); + if (newName && newName[0] != '\0') { + strncpy(name, newName, sizeof(name) - 1); + } + // Modal defaults + modalX = 0; // 0 = centered + modalY = 0; // 0 = centered + modalWidth = 0; // 0 = auto + modalHeight = 0; // 0 = auto + modalBorderRadius = 12; + modalDimBackground = true; +} + +int ScreenConfig::addWidget(const WidgetConfig& widget) { + if (widgetCount >= MAX_WIDGETS) return -1; + + // Find next free ID + uint8_t newId = 0; + for (uint8_t i = 0; i < widgetCount; i++) { + if (widgets[i].id >= newId) newId = widgets[i].id + 1; + } + + widgets[widgetCount] = widget; + widgets[widgetCount].id = newId; + widgetCount++; + return newId; +} + +bool ScreenConfig::removeWidget(uint8_t id) { + for (uint8_t i = 0; i < widgetCount; i++) { + if (widgets[i].id == id) { + // Shift remaining widgets + for (uint8_t j = i; j < widgetCount - 1; j++) { + widgets[j] = widgets[j + 1]; + } + widgetCount--; + return true; + } + } + return false; +} + +WidgetConfig* ScreenConfig::findWidget(uint8_t id) { + for (uint8_t i = 0; i < widgetCount; i++) { + if (widgets[i].id == id) return &widgets[i]; + } + return nullptr; +} + +const WidgetConfig* ScreenConfig::findWidget(uint8_t id) const { + for (uint8_t i = 0; i < widgetCount; i++) { + if (widgets[i].id == id) return &widgets[i]; + } + return nullptr; +} + +// GuiConfig implementation +void GuiConfig::clear() { + screenCount = 0; + startScreenId = 0; + standbyEnabled = false; + standbyScreenId = 0xFF; + standbyMinutes = 0; + for (size_t i = 0; i < MAX_SCREENS; i++) { + screens[i].clear(static_cast(i), nullptr); + } +} + +ScreenConfig* GuiConfig::findScreen(uint8_t id) { + for (uint8_t i = 0; i < screenCount; i++) { + if (screens[i].id == id) return &screens[i]; + } + return nullptr; +} + +const ScreenConfig* GuiConfig::findScreen(uint8_t id) const { + for (uint8_t i = 0; i < screenCount; i++) { + if (screens[i].id == id) return &screens[i]; + } + return nullptr; +} diff --git a/main/WidgetConfig.hpp b/main/WidgetConfig.hpp index d0987ee..e8b67bd 100644 --- a/main/WidgetConfig.hpp +++ b/main/WidgetConfig.hpp @@ -121,6 +121,14 @@ struct ScreenConfig { uint8_t widgetCount; WidgetConfig widgets[MAX_WIDGETS]; + // Modal-specific properties (only used when mode == MODAL) + int16_t modalX; // Modal position X (0 = centered) + int16_t modalY; // Modal position Y (0 = centered) + int16_t modalWidth; // Modal width (0 = auto from content) + int16_t modalHeight; // Modal height (0 = auto from content) + uint8_t modalBorderRadius; + bool modalDimBackground; // Dim the background behind modal + void clear(uint8_t newId = 0, const char* newName = nullptr); int addWidget(const WidgetConfig& widget); // Returns widget ID or -1 bool removeWidget(uint8_t id); diff --git a/main/WidgetManager.cpp b/main/WidgetManager.cpp index 28871f9..5610379 100644 --- a/main/WidgetManager.cpp +++ b/main/WidgetManager.cpp @@ -1,9 +1,9 @@ #include "WidgetManager.hpp" +#include "widgets/WidgetFactory.hpp" #include "SdCard.hpp" #include "esp_lv_adapter.h" #include "esp_log.h" #include "esp_timer.h" -#include "Gui.hpp" #include "cJSON.h" #include #include @@ -14,226 +14,6 @@ static const char* TAG = "WidgetMgr"; static constexpr uint8_t SCREEN_ID_NONE = 0xFF; -// Button click callback -static void button_click_cb(lv_event_t* e) { - WidgetConfig* cfg = static_cast(lv_event_get_user_data(e)); - if (!cfg) return; - lv_obj_t* target = static_cast(lv_event_get_target(e)); - WidgetManager::instance().handleButtonAction(*cfg, target); -} - -// WidgetConfig implementation -void WidgetConfig::serialize(uint8_t* buf) const { - memset(buf, 0, SERIALIZED_SIZE); - size_t pos = 0; - - buf[pos++] = id; - buf[pos++] = static_cast(type); - buf[pos++] = x & 0xFF; buf[pos++] = (x >> 8) & 0xFF; - buf[pos++] = y & 0xFF; buf[pos++] = (y >> 8) & 0xFF; - buf[pos++] = width & 0xFF; buf[pos++] = (width >> 8) & 0xFF; - buf[pos++] = height & 0xFF; buf[pos++] = (height >> 8) & 0xFF; - buf[pos++] = visible ? 1 : 0; - - buf[pos++] = static_cast(textSource); - memcpy(&buf[pos], text, MAX_TEXT_LEN); pos += MAX_TEXT_LEN; - buf[pos++] = knxAddress & 0xFF; buf[pos++] = (knxAddress >> 8) & 0xFF; - buf[pos++] = fontSize; - - buf[pos++] = textColor.r; buf[pos++] = textColor.g; buf[pos++] = textColor.b; - buf[pos++] = bgColor.r; buf[pos++] = bgColor.g; buf[pos++] = bgColor.b; - buf[pos++] = bgOpacity; - buf[pos++] = borderRadius; - - buf[pos++] = shadow.offsetX; - buf[pos++] = shadow.offsetY; - buf[pos++] = shadow.blur; - buf[pos++] = shadow.spread; - buf[pos++] = shadow.color.r; buf[pos++] = shadow.color.g; buf[pos++] = shadow.color.b; - buf[pos++] = shadow.enabled ? 1 : 0; - - buf[pos++] = isToggle ? 1 : 0; - buf[pos++] = knxAddressWrite & 0xFF; buf[pos++] = (knxAddressWrite >> 8) & 0xFF; - buf[pos++] = static_cast(action); - buf[pos++] = targetScreen; -} - -void WidgetConfig::deserialize(const uint8_t* buf) { - size_t pos = 0; - - id = buf[pos++]; - type = static_cast(buf[pos++]); - x = buf[pos] | (buf[pos+1] << 8); pos += 2; - y = buf[pos] | (buf[pos+1] << 8); pos += 2; - width = buf[pos] | (buf[pos+1] << 8); pos += 2; - height = buf[pos] | (buf[pos+1] << 8); pos += 2; - visible = buf[pos++] != 0; - - textSource = static_cast(buf[pos++]); - memcpy(text, &buf[pos], MAX_TEXT_LEN); pos += MAX_TEXT_LEN; - text[MAX_TEXT_LEN - 1] = '\0'; - knxAddress = buf[pos] | (buf[pos+1] << 8); pos += 2; - fontSize = buf[pos++]; - - textColor.r = buf[pos++]; textColor.g = buf[pos++]; textColor.b = buf[pos++]; - bgColor.r = buf[pos++]; bgColor.g = buf[pos++]; bgColor.b = buf[pos++]; - bgOpacity = buf[pos++]; - borderRadius = buf[pos++]; - - shadow.offsetX = static_cast(buf[pos++]); - shadow.offsetY = static_cast(buf[pos++]); - shadow.blur = buf[pos++]; - shadow.spread = buf[pos++]; - shadow.color.r = buf[pos++]; shadow.color.g = buf[pos++]; shadow.color.b = buf[pos++]; - shadow.enabled = buf[pos++] != 0; - - isToggle = buf[pos++] != 0; - knxAddressWrite = buf[pos] | (buf[pos+1] << 8); - pos += 2; - action = static_cast(buf[pos++]); - targetScreen = buf[pos++]; -} - -WidgetConfig WidgetConfig::createLabel(uint8_t id, int16_t x, int16_t y, const char* labelText) { - WidgetConfig cfg = {}; - cfg.id = id; - cfg.type = WidgetType::LABEL; - cfg.x = x; - cfg.y = y; - cfg.width = 150; - cfg.height = 40; - cfg.visible = true; - cfg.textSource = TextSource::STATIC; - strncpy(cfg.text, labelText, MAX_TEXT_LEN - 1); - cfg.fontSize = 1; // 18pt - cfg.textColor = {255, 255, 255}; - cfg.bgColor = {0, 0, 0}; - cfg.bgOpacity = 0; - cfg.borderRadius = 0; - cfg.shadow.enabled = false; - return cfg; -} - -WidgetConfig WidgetConfig::createKnxLabel(uint8_t id, int16_t x, int16_t y, - TextSource source, uint16_t knxAddr, const char* format) { - WidgetConfig cfg = createLabel(id, x, y, format); - cfg.textSource = source; - cfg.knxAddress = knxAddr; - return cfg; -} - -WidgetConfig WidgetConfig::createButton(uint8_t id, int16_t x, int16_t y, - const char* labelText, uint16_t knxAddrWrite, bool toggle) { - WidgetConfig cfg = {}; - cfg.id = id; - cfg.type = WidgetType::BUTTON; - cfg.x = x; - cfg.y = y; - cfg.width = 120; - cfg.height = 50; - cfg.visible = true; - cfg.textSource = TextSource::STATIC; - strncpy(cfg.text, labelText, MAX_TEXT_LEN - 1); - cfg.fontSize = 1; - cfg.textColor = {255, 255, 255}; - cfg.bgColor = {33, 150, 243}; // Blue - cfg.bgOpacity = 255; - cfg.borderRadius = 8; - cfg.shadow.enabled = true; - cfg.shadow.offsetX = 2; - cfg.shadow.offsetY = 2; - cfg.shadow.blur = 8; - cfg.shadow.spread = 0; - cfg.shadow.color = {0, 0, 0}; - cfg.isToggle = toggle; - cfg.knxAddressWrite = knxAddrWrite; - cfg.action = ButtonAction::KNX; - cfg.targetScreen = 0; - return cfg; -} - -// ScreenConfig implementation -void ScreenConfig::clear(uint8_t newId, const char* newName) { - id = newId; - mode = ScreenMode::FULLSCREEN; - backgroundColor = {26, 26, 46}; // Dark blue background - widgetCount = 0; - memset(widgets, 0, sizeof(widgets)); - memset(name, 0, sizeof(name)); - if (newName && newName[0] != '\0') { - strncpy(name, newName, sizeof(name) - 1); - } -} - -int ScreenConfig::addWidget(const WidgetConfig& widget) { - if (widgetCount >= MAX_WIDGETS) return -1; - - // Find next free ID - uint8_t newId = 0; - for (uint8_t i = 0; i < widgetCount; i++) { - if (widgets[i].id >= newId) newId = widgets[i].id + 1; - } - - widgets[widgetCount] = widget; - widgets[widgetCount].id = newId; - widgetCount++; - return newId; -} - -bool ScreenConfig::removeWidget(uint8_t id) { - for (uint8_t i = 0; i < widgetCount; i++) { - if (widgets[i].id == id) { - // Shift remaining widgets - for (uint8_t j = i; j < widgetCount - 1; j++) { - widgets[j] = widgets[j + 1]; - } - widgetCount--; - return true; - } - } - return false; -} - -WidgetConfig* ScreenConfig::findWidget(uint8_t id) { - for (uint8_t i = 0; i < widgetCount; i++) { - if (widgets[i].id == id) return &widgets[i]; - } - return nullptr; -} - -const WidgetConfig* ScreenConfig::findWidget(uint8_t id) const { - for (uint8_t i = 0; i < widgetCount; i++) { - if (widgets[i].id == id) return &widgets[i]; - } - return nullptr; -} - -// GuiConfig implementation -void GuiConfig::clear() { - screenCount = 0; - startScreenId = 0; - standbyEnabled = false; - standbyScreenId = 0xFF; - standbyMinutes = 0; - for (size_t i = 0; i < MAX_SCREENS; i++) { - screens[i].clear(static_cast(i), nullptr); - } -} - -ScreenConfig* GuiConfig::findScreen(uint8_t id) { - for (uint8_t i = 0; i < screenCount; i++) { - if (screens[i].id == id) return &screens[i]; - } - return nullptr; -} - -const ScreenConfig* GuiConfig::findScreen(uint8_t id) const { - for (uint8_t i = 0; i < screenCount; i++) { - if (screens[i].id == id) return &screens[i]; - } - return nullptr; -} - // WidgetManager implementation WidgetManager& WidgetManager::instance() { static WidgetManager inst; @@ -241,7 +21,7 @@ WidgetManager& WidgetManager::instance() { } WidgetManager::WidgetManager() { - widgetObjects_.fill(nullptr); + // widgets_ is default-initialized to nullptr createDefaultConfig(); activeScreenId_ = config_.startScreenId; lastActivityUs_ = esp_timer_get_time(); @@ -296,7 +76,6 @@ void WidgetManager::loadFromSdCard() { return; } - // Get file size fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); @@ -307,7 +86,6 @@ void WidgetManager::loadFromSdCard() { return; } - // Read file content char* json = new char[size + 1]; if (!json) { ESP_LOGE(TAG, "Failed to allocate memory for config"); @@ -319,7 +97,6 @@ void WidgetManager::loadFromSdCard() { fclose(f); json[read] = '\0'; - // Parse JSON using cJSON bool success = updateConfigFromJson(json); delete[] json; @@ -336,7 +113,6 @@ void WidgetManager::saveToSdCard() { return; } - // Generate JSON using cJSON char* json = new char[32768]; if (!json) { ESP_LOGE(TAG, "Failed to allocate memory for JSON"); @@ -345,7 +121,6 @@ void WidgetManager::saveToSdCard() { getConfigJson(json, 32768); - // Write to file FILE* f = fopen(CONFIG_FILE, "w"); if (!f) { ESP_LOGE(TAG, "Failed to open config file for writing"); @@ -417,10 +192,11 @@ void WidgetManager::applyScreen(uint8_t screenId) { closeModal(); } + // First destroy C++ widgets (which deletes LVGL objects) + destroyAllWidgets(); + if (esp_lv_adapter_lock(-1) == ESP_OK) { lv_obj_t* root = lv_scr_act(); - lv_obj_clean(root); - widgetObjects_.fill(nullptr); createAllWidgets(*screen, root); esp_lv_adapter_unlock(); } @@ -431,34 +207,111 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) { closeModal(); } + // Destroy any existing widgets before creating modal widgets + destroyAllWidgets(); + if (esp_lv_adapter_lock(-1) != ESP_OK) return; - lv_obj_t* overlay = lv_obj_create(lv_layer_top()); - lv_obj_set_style_bg_opa(overlay, LV_OPA_COVER, 0); - lv_obj_clear_flag(overlay, LV_OBJ_FLAG_SCROLLABLE); - lv_disp_t* disp = lv_disp_get_default(); - int32_t hor = disp ? lv_disp_get_hor_res(disp) : 1280; - int32_t ver = disp ? lv_disp_get_ver_res(disp) : 800; - lv_obj_set_size(overlay, hor, ver); + int32_t dispWidth = disp ? lv_disp_get_hor_res(disp) : 1280; + int32_t dispHeight = disp ? lv_disp_get_ver_res(disp) : 800; - modalContainer_ = overlay; + // Create semi-transparent background overlay if dimming enabled + lv_obj_t* dimmer = nullptr; + if (screen.modalDimBackground) { + dimmer = lv_obj_create(lv_layer_top()); + lv_obj_remove_style_all(dimmer); + lv_obj_set_size(dimmer, dispWidth, dispHeight); + lv_obj_set_style_bg_color(dimmer, lv_color_black(), 0); + lv_obj_set_style_bg_opa(dimmer, LV_OPA_50, 0); + lv_obj_clear_flag(dimmer, LV_OBJ_FLAG_SCROLLABLE); + } + + // Create modal container + lv_obj_t* modal = lv_obj_create(lv_layer_top()); + lv_obj_clear_flag(modal, LV_OBJ_FLAG_SCROLLABLE); + + // Calculate modal size + int32_t modalWidth = screen.modalWidth; + int32_t modalHeight = screen.modalHeight; + + // Auto-size: calculate from widget bounds if not specified + if (modalWidth <= 0 || modalHeight <= 0) { + int32_t maxX = 0, maxY = 0; + for (uint8_t i = 0; i < screen.widgetCount; i++) { + const WidgetConfig& w = screen.widgets[i]; + if (w.visible) { + int32_t right = w.x + w.width; + int32_t bottom = w.y + w.height; + if (right > maxX) maxX = right; + if (bottom > maxY) maxY = bottom; + } + } + if (modalWidth <= 0) modalWidth = maxX + 40; // Add padding + if (modalHeight <= 0) modalHeight = maxY + 40; + } + + lv_obj_set_size(modal, modalWidth, modalHeight); + + // Position modal (0 = centered) + if (screen.modalX == 0 && screen.modalY == 0) { + lv_obj_center(modal); + } else { + lv_obj_set_pos(modal, screen.modalX, screen.modalY); + } + + // Style modal + lv_obj_set_style_bg_color(modal, lv_color_make( + screen.backgroundColor.r, + screen.backgroundColor.g, + screen.backgroundColor.b), 0); + lv_obj_set_style_bg_opa(modal, LV_OPA_COVER, 0); + lv_obj_set_style_radius(modal, screen.modalBorderRadius, 0); + lv_obj_set_style_border_width(modal, 0, 0); + lv_obj_set_style_pad_all(modal, 0, 0); + + // Add shadow for modal + lv_obj_set_style_shadow_color(modal, lv_color_black(), 0); + lv_obj_set_style_shadow_opa(modal, LV_OPA_30, 0); + lv_obj_set_style_shadow_width(modal, 20, 0); + lv_obj_set_style_shadow_spread(modal, 5, 0); + + modalContainer_ = modal; + modalDimmer_ = dimmer; modalScreenId_ = screen.id; - createAllWidgets(screen, modalContainer_); + + // Create widgets inside modal (not on full screen) + screen_ = modal; + for (uint8_t i = 0; i < screen.widgetCount; i++) { + const WidgetConfig& cfg = screen.widgets[i]; + auto widget = WidgetFactory::create(cfg); + if (widget && cfg.id < MAX_WIDGETS) { + widget->create(modal); + widget->applyStyle(); + widgets_[cfg.id] = std::move(widget); + } + } esp_lv_adapter_unlock(); + ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight); } void WidgetManager::closeModal() { if (!modalContainer_) return; + // First destroy C++ widgets (which deletes their LVGL objects) + destroyAllWidgets(); + if (esp_lv_adapter_lock(-1) == ESP_OK) { + if (modalDimmer_) { + lv_obj_delete(modalDimmer_); + } lv_obj_delete(modalContainer_); esp_lv_adapter_unlock(); } modalContainer_ = nullptr; + modalDimmer_ = nullptr; modalScreenId_ = SCREEN_ID_NONE; - widgetObjects_.fill(nullptr); } void WidgetManager::showScreen(uint8_t screenId) { @@ -505,7 +358,6 @@ void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target ESP_LOGI(TAG, "Button %d clicked, KNX write to %d, state=%d", cfg.id, cfg.knxAddressWrite, state); // TODO: Send KNX telegram - // Gui::knxWorker.writeSwitch(cfg.knxAddressWrite, state); } break; } @@ -586,175 +438,37 @@ void WidgetManager::onUserActivity() { } void WidgetManager::destroyAllWidgets() { - for (auto& obj : widgetObjects_) { - if (obj != nullptr) { - lv_obj_delete(obj); - obj = nullptr; - } + for (auto& widget : widgets_) { + widget.reset(); } } void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent) { screen_ = parent; - widgetObjects_.fill(nullptr); - // Set background color lv_obj_set_style_bg_color(parent, lv_color_make( screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b), 0); lv_obj_set_style_bg_opa(parent, LV_OPA_COVER, 0); - // Create all widgets for (uint8_t i = 0; i < screen.widgetCount; i++) { const WidgetConfig& cfg = screen.widgets[i]; - lv_obj_t* obj = createWidget(cfg, parent); - if (obj != nullptr && cfg.id < MAX_WIDGETS) { - widgetObjects_[cfg.id] = obj; + auto widget = WidgetFactory::create(cfg); + if (widget && cfg.id < MAX_WIDGETS) { + widget->create(parent); + widget->applyStyle(); + widgets_[cfg.id] = std::move(widget); } } } -lv_obj_t* WidgetManager::createWidget(const WidgetConfig& cfg, lv_obj_t* parent) { - if (!cfg.visible) return nullptr; - - lv_obj_t* obj = nullptr; - - switch (cfg.type) { - case WidgetType::LABEL: { - obj = lv_label_create(parent); - lv_label_set_text(obj, cfg.text); - break; - } - case WidgetType::BUTTON: { - obj = lv_btn_create(parent); - if (cfg.isToggle) { - lv_obj_add_flag(obj, LV_OBJ_FLAG_CHECKABLE); - } - lv_obj_add_event_cb(obj, button_click_cb, LV_EVENT_CLICKED, - const_cast(&cfg)); - - // Create label inside button - lv_obj_t* label = lv_label_create(obj); - lv_label_set_text(label, cfg.text); - lv_obj_set_style_text_color(label, lv_color_make( - cfg.textColor.r, cfg.textColor.g, cfg.textColor.b), 0); - lv_obj_set_style_text_font(label, getFontBySize(cfg.fontSize), 0); - lv_obj_center(label); - break; - } - case WidgetType::LED: { - obj = lv_led_create(parent); - break; - } - } - - if (obj != nullptr) { - lv_obj_set_pos(obj, cfg.x, cfg.y); - if (cfg.width > 0 && cfg.height > 0) { - lv_obj_set_size(obj, cfg.width, cfg.height); - } - if (cfg.type == WidgetType::LED) { - applyLedStyle(obj, cfg); - } else { - applyStyle(obj, cfg); - } - } - - return obj; -} - -void WidgetManager::applyStyle(lv_obj_t* obj, const WidgetConfig& cfg) { - // Text color - lv_obj_set_style_text_color(obj, lv_color_make( - cfg.textColor.r, cfg.textColor.g, cfg.textColor.b), 0); - - // Font - lv_obj_set_style_text_font(obj, getFontBySize(cfg.fontSize), 0); - - // Background (for buttons and labels with bg) - if (cfg.bgOpacity > 0) { - lv_obj_set_style_bg_color(obj, lv_color_make( - cfg.bgColor.r, cfg.bgColor.g, cfg.bgColor.b), 0); - lv_obj_set_style_bg_opa(obj, cfg.bgOpacity, 0); - } - - // Border radius - if (cfg.borderRadius > 0) { - lv_obj_set_style_radius(obj, cfg.borderRadius, 0); - } - - // Shadow - if (cfg.shadow.enabled) { - lv_obj_set_style_shadow_color(obj, lv_color_make( - cfg.shadow.color.r, cfg.shadow.color.g, cfg.shadow.color.b), 0); - lv_obj_set_style_shadow_opa(obj, 180, 0); - lv_obj_set_style_shadow_width(obj, cfg.shadow.blur, 0); - lv_obj_set_style_shadow_spread(obj, cfg.shadow.spread, 0); - lv_obj_set_style_shadow_offset_x(obj, cfg.shadow.offsetX, 0); - lv_obj_set_style_shadow_offset_y(obj, cfg.shadow.offsetY, 0); - } -} - -void WidgetManager::applyLedStyle(lv_obj_t* obj, const WidgetConfig& cfg) { - lv_obj_set_style_radius(obj, LV_RADIUS_CIRCLE, 0); - lv_led_set_color(obj, lv_color_make( - cfg.bgColor.r, cfg.bgColor.g, cfg.bgColor.b)); - lv_led_set_brightness(obj, cfg.bgOpacity); - - if (cfg.shadow.enabled) { - lv_obj_set_style_shadow_color(obj, lv_color_make( - cfg.shadow.color.r, cfg.shadow.color.g, cfg.shadow.color.b), 0); - lv_obj_set_style_shadow_opa(obj, 180, 0); - lv_obj_set_style_shadow_width(obj, cfg.shadow.blur, 0); - lv_obj_set_style_shadow_spread(obj, cfg.shadow.spread, 0); - lv_obj_set_style_shadow_offset_x(obj, cfg.shadow.offsetX, 0); - lv_obj_set_style_shadow_offset_y(obj, cfg.shadow.offsetY, 0); - } -} - -const lv_font_t* WidgetManager::getFontBySize(uint8_t sizeIndex) { - // Font sizes: 0=14, 1=18, 2=22, 3=28, 4=36, 5=48 - // These must be enabled in sdkconfig (CONFIG_LV_FONT_MONTSERRAT_*) - switch (sizeIndex) { - case 0: return &lv_font_montserrat_14; -#if LV_FONT_MONTSERRAT_18 - case 1: return &lv_font_montserrat_18; -#endif -#if LV_FONT_MONTSERRAT_22 - case 2: return &lv_font_montserrat_22; -#endif -#if LV_FONT_MONTSERRAT_28 - case 3: return &lv_font_montserrat_28; -#endif -#if LV_FONT_MONTSERRAT_36 - case 4: return &lv_font_montserrat_36; -#endif -#if LV_FONT_MONTSERRAT_48 - case 5: return &lv_font_montserrat_48; -#endif - default: return &lv_font_montserrat_14; - } -} - void WidgetManager::onKnxValue(uint16_t groupAddr, float value) { if (esp_lv_adapter_lock(100) != ESP_OK) return; - const ScreenConfig* screen = activeScreen(); - if (!screen) { - esp_lv_adapter_unlock(); - return; - } - - for (uint8_t i = 0; i < screen->widgetCount; i++) { - const WidgetConfig& cfg = screen->widgets[i]; - if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_TEMP) { - lv_obj_t* obj = widgetObjects_[cfg.id]; - if (obj != nullptr) { - char buf[32]; - snprintf(buf, sizeof(buf), cfg.text, value); - lv_label_set_text(obj, buf); - } + for (auto& widget : widgets_) { + if (widget && widget->getKnxAddress() == groupAddr) { + widget->onKnxValue(value); } } @@ -764,24 +478,9 @@ void WidgetManager::onKnxValue(uint16_t groupAddr, float value) { void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) { if (esp_lv_adapter_lock(100) != ESP_OK) return; - const ScreenConfig* screen = activeScreen(); - if (!screen) { - esp_lv_adapter_unlock(); - return; - } - - for (uint8_t i = 0; i < screen->widgetCount; i++) { - const WidgetConfig& cfg = screen->widgets[i]; - if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_SWITCH) { - lv_obj_t* obj = widgetObjects_[cfg.id]; - if (obj == nullptr) continue; - - if (cfg.type == WidgetType::LABEL) { - lv_label_set_text(obj, value ? "EIN" : "AUS"); - } else if (cfg.type == WidgetType::LED) { - uint8_t brightness = value ? (cfg.bgOpacity > 0 ? cfg.bgOpacity : 255) : 0; - lv_led_set_brightness(obj, brightness); - } + for (auto& widget : widgets_) { + if (widget && widget->getKnxAddress() == groupAddr) { + widget->onKnxSwitch(value); } } @@ -791,25 +490,21 @@ void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) { void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) { if (esp_lv_adapter_lock(100) != ESP_OK) return; - const ScreenConfig* screen = activeScreen(); - if (!screen) { - esp_lv_adapter_unlock(); - return; - } - - for (uint8_t i = 0; i < screen->widgetCount; i++) { - const WidgetConfig& cfg = screen->widgets[i]; - if (cfg.knxAddress == groupAddr && cfg.textSource == TextSource::KNX_DPT_TEXT) { - lv_obj_t* obj = widgetObjects_[cfg.id]; - if (obj != nullptr) { - lv_label_set_text(obj, text); - } + for (auto& widget : widgets_) { + if (widget && widget->getKnxAddress() == groupAddr) { + widget->onKnxText(text); } } esp_lv_adapter_unlock(); } +// Helper function to parse hex color string +static uint32_t parseHexColor(const char* colorStr) { + if (!colorStr || colorStr[0] != '#') return 0; + return strtoul(colorStr + 1, nullptr, 16); +} + void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON* root = cJSON_CreateObject(); if (!root) { @@ -824,7 +519,6 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_AddNumberToObject(standby, "screen", config_.standbyScreenId); cJSON_AddNumberToObject(standby, "minutes", config_.standbyMinutes); - // Add screens array cJSON* screens = cJSON_AddArrayToObject(root, "screens"); for (uint8_t s = 0; s < config_.screenCount; s++) { @@ -840,6 +534,17 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b); cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr); + // Modal-specific properties + if (screen.mode == ScreenMode::MODAL) { + cJSON* modal = cJSON_AddObjectToObject(screenJson, "modal"); + cJSON_AddNumberToObject(modal, "x", screen.modalX); + cJSON_AddNumberToObject(modal, "y", screen.modalY); + cJSON_AddNumberToObject(modal, "w", screen.modalWidth); + cJSON_AddNumberToObject(modal, "h", screen.modalHeight); + cJSON_AddNumberToObject(modal, "radius", screen.modalBorderRadius); + cJSON_AddBoolToObject(modal, "dim", screen.modalDimBackground); + } + cJSON* widgets = cJSON_AddArrayToObject(screenJson, "widgets"); for (uint8_t i = 0; i < screen.widgetCount; i++) { const WidgetConfig& w = screen.widgets[i]; @@ -857,13 +562,11 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_AddNumberToObject(widget, "knxAddr", w.knxAddress); cJSON_AddNumberToObject(widget, "fontSize", w.fontSize); - // Text color char textColorStr[8]; snprintf(textColorStr, sizeof(textColorStr), "#%02X%02X%02X", w.textColor.r, w.textColor.g, w.textColor.b); cJSON_AddStringToObject(widget, "textColor", textColorStr); - // Background color char widgetBgColorStr[8]; snprintf(widgetBgColorStr, sizeof(widgetBgColorStr), "#%02X%02X%02X", w.bgColor.r, w.bgColor.g, w.bgColor.b); @@ -872,7 +575,6 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_AddNumberToObject(widget, "bgOpacity", w.bgOpacity); cJSON_AddNumberToObject(widget, "radius", w.borderRadius); - // Shadow object cJSON* shadow = cJSON_AddObjectToObject(widget, "shadow"); cJSON_AddBoolToObject(shadow, "enabled", w.shadow.enabled); cJSON_AddNumberToObject(shadow, "x", w.shadow.offsetX); @@ -895,7 +597,6 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_AddItemToArray(screens, screenJson); } - // Print to buffer char* jsonStr = cJSON_PrintUnformatted(root); if (jsonStr) { strncpy(buf, jsonStr, bufSize - 1); @@ -908,12 +609,6 @@ void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON_Delete(root); } -// Helper function to parse hex color string -static uint32_t parseHexColor(const char* colorStr) { - if (!colorStr || colorStr[0] != '#') return 0; - return strtoul(colorStr + 1, nullptr, 16); -} - bool WidgetManager::updateConfigFromJson(const char* json) { cJSON* root = cJSON_Parse(json); if (!root) { @@ -943,7 +638,6 @@ bool WidgetManager::updateConfigFromJson(const char* json) { w.action = ButtonAction::KNX; w.targetScreen = 0; - // Parse basic properties cJSON* id = cJSON_GetObjectItem(widget, "id"); if (cJSON_IsNumber(id)) w.id = id->valueint; @@ -980,7 +674,6 @@ bool WidgetManager::updateConfigFromJson(const char* json) { cJSON* fontSize = cJSON_GetObjectItem(widget, "fontSize"); if (cJSON_IsNumber(fontSize)) w.fontSize = fontSize->valueint; - // Parse colors cJSON* textColor = cJSON_GetObjectItem(widget, "textColor"); if (cJSON_IsString(textColor)) { w.textColor = Color::fromHex(parseHexColor(textColor->valuestring)); @@ -997,7 +690,6 @@ bool WidgetManager::updateConfigFromJson(const char* json) { cJSON* radius = cJSON_GetObjectItem(widget, "radius"); if (cJSON_IsNumber(radius)) w.borderRadius = radius->valueint; - // Parse shadow cJSON* shadow = cJSON_GetObjectItem(widget, "shadow"); if (cJSON_IsObject(shadow)) { cJSON* enabled = cJSON_GetObjectItem(shadow, "enabled"); @@ -1075,6 +767,28 @@ bool WidgetManager::updateConfigFromJson(const char* json) { screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring)); } + // Parse modal-specific properties + cJSON* modal = cJSON_GetObjectItem(screenJson, "modal"); + if (cJSON_IsObject(modal)) { + cJSON* mx = cJSON_GetObjectItem(modal, "x"); + if (cJSON_IsNumber(mx)) screen.modalX = mx->valueint; + + cJSON* my = cJSON_GetObjectItem(modal, "y"); + if (cJSON_IsNumber(my)) screen.modalY = my->valueint; + + cJSON* mw = cJSON_GetObjectItem(modal, "w"); + if (cJSON_IsNumber(mw)) screen.modalWidth = mw->valueint; + + cJSON* mh = cJSON_GetObjectItem(modal, "h"); + if (cJSON_IsNumber(mh)) screen.modalHeight = mh->valueint; + + cJSON* mr = cJSON_GetObjectItem(modal, "radius"); + if (cJSON_IsNumber(mr)) screen.modalBorderRadius = mr->valueint; + + cJSON* dim = cJSON_GetObjectItem(modal, "dim"); + if (cJSON_IsBool(dim)) screen.modalDimBackground = cJSON_IsTrue(dim); + } + cJSON* widgets = cJSON_GetObjectItem(screenJson, "widgets"); if (!parseWidgets(widgets, screen)) { screen.widgetCount = 0; diff --git a/main/WidgetManager.hpp b/main/WidgetManager.hpp index 944d539..278117b 100644 --- a/main/WidgetManager.hpp +++ b/main/WidgetManager.hpp @@ -1,8 +1,10 @@ #pragma once #include "WidgetConfig.hpp" +#include "widgets/Widget.hpp" #include "lvgl.h" #include +#include class WidgetManager { public: @@ -56,10 +58,6 @@ private: void saveToSdCard(); void destroyAllWidgets(); void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent); - lv_obj_t* createWidget(const WidgetConfig& cfg, lv_obj_t* parent); - void applyStyle(lv_obj_t* obj, const WidgetConfig& cfg); - void applyLedStyle(lv_obj_t* obj, const WidgetConfig& cfg); - const lv_font_t* getFontBySize(uint8_t sizeIndex); void createDefaultConfig(); void applyScreen(uint8_t screenId); @@ -85,8 +83,9 @@ private: uint8_t navTargetScreen_ = 0xFF; int64_t lastActivityUs_ = 0; - // Runtime widget references (indexed by widget ID) - std::array widgetObjects_; + // Runtime widget instances (indexed by widget ID) + std::array, MAX_WIDGETS> widgets_; lv_obj_t* screen_ = nullptr; lv_obj_t* modalContainer_ = nullptr; + lv_obj_t* modalDimmer_ = nullptr; }; diff --git a/main/widgets/ButtonWidget.cpp b/main/widgets/ButtonWidget.cpp new file mode 100644 index 0000000..a1bbdcb --- /dev/null +++ b/main/widgets/ButtonWidget.cpp @@ -0,0 +1,56 @@ +#include "ButtonWidget.hpp" +#include "../WidgetManager.hpp" + +ButtonWidget::ButtonWidget(const WidgetConfig& config) + : Widget(config) + , label_(nullptr) +{ +} + +void ButtonWidget::clickCallback(lv_event_t* e) { + ButtonWidget* widget = static_cast(lv_event_get_user_data(e)); + if (!widget) return; + lv_obj_t* target = static_cast(lv_event_get_target(e)); + WidgetManager::instance().handleButtonAction(widget->getConfig(), target); +} + +lv_obj_t* ButtonWidget::create(lv_obj_t* parent) { + obj_ = lv_btn_create(parent); + + if (config_.isToggle) { + lv_obj_add_flag(obj_, LV_OBJ_FLAG_CHECKABLE); + } + + lv_obj_add_event_cb(obj_, clickCallback, LV_EVENT_CLICKED, this); + + // Create label inside button + label_ = lv_label_create(obj_); + lv_label_set_text(label_, config_.text); + lv_obj_center(label_); + + lv_obj_set_pos(obj_, config_.x, config_.y); + if (config_.width > 0 && config_.height > 0) { + lv_obj_set_size(obj_, config_.width, config_.height); + } + + return obj_; +} + +void ButtonWidget::applyStyle() { + if (obj_ == nullptr) return; + + // Apply common style to button + applyCommonStyle(); + + // Apply text style to label + if (label_ != nullptr) { + lv_obj_set_style_text_color(label_, lv_color_make( + config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); + lv_obj_set_style_text_font(label_, getFontBySize(config_.fontSize), 0); + } +} + +bool ButtonWidget::isChecked() const { + if (obj_ == nullptr) return false; + return (lv_obj_get_state(obj_) & LV_STATE_CHECKED) != 0; +} diff --git a/main/widgets/ButtonWidget.hpp b/main/widgets/ButtonWidget.hpp new file mode 100644 index 0000000..638a444 --- /dev/null +++ b/main/widgets/ButtonWidget.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "Widget.hpp" + +class ButtonWidget : public Widget { +public: + explicit ButtonWidget(const WidgetConfig& config); + + lv_obj_t* create(lv_obj_t* parent) override; + void applyStyle() override; + + // Check if button is in checked state + bool isChecked() const; + +private: + lv_obj_t* label_ = nullptr; + + static void clickCallback(lv_event_t* e); +}; diff --git a/main/widgets/LabelWidget.cpp b/main/widgets/LabelWidget.cpp new file mode 100644 index 0000000..f749e1d --- /dev/null +++ b/main/widgets/LabelWidget.cpp @@ -0,0 +1,42 @@ +#include "LabelWidget.hpp" +#include + +LabelWidget::LabelWidget(const WidgetConfig& config) + : Widget(config) +{ +} + +lv_obj_t* LabelWidget::create(lv_obj_t* parent) { + obj_ = lv_label_create(parent); + lv_label_set_text(obj_, config_.text); + + lv_obj_set_pos(obj_, config_.x, config_.y); + if (config_.width > 0 && config_.height > 0) { + lv_obj_set_size(obj_, config_.width, config_.height); + } + + return obj_; +} + +void LabelWidget::onKnxValue(float value) { + if (obj_ == nullptr) return; + if (config_.textSource != TextSource::KNX_DPT_TEMP) return; + + char buf[32]; + snprintf(buf, sizeof(buf), config_.text, value); + lv_label_set_text(obj_, buf); +} + +void LabelWidget::onKnxSwitch(bool value) { + if (obj_ == nullptr) return; + if (config_.textSource != TextSource::KNX_DPT_SWITCH) return; + + lv_label_set_text(obj_, value ? "EIN" : "AUS"); +} + +void LabelWidget::onKnxText(const char* text) { + if (obj_ == nullptr) return; + if (config_.textSource != TextSource::KNX_DPT_TEXT) return; + + lv_label_set_text(obj_, text); +} diff --git a/main/widgets/LabelWidget.hpp b/main/widgets/LabelWidget.hpp new file mode 100644 index 0000000..134b750 --- /dev/null +++ b/main/widgets/LabelWidget.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "Widget.hpp" + +class LabelWidget : public Widget { +public: + explicit LabelWidget(const WidgetConfig& config); + + lv_obj_t* create(lv_obj_t* parent) override; + + // KNX updates + void onKnxValue(float value) override; + void onKnxSwitch(bool value) override; + void onKnxText(const char* text) override; +}; diff --git a/main/widgets/LedWidget.cpp b/main/widgets/LedWidget.cpp new file mode 100644 index 0000000..e0729cf --- /dev/null +++ b/main/widgets/LedWidget.cpp @@ -0,0 +1,38 @@ +#include "LedWidget.hpp" + +LedWidget::LedWidget(const WidgetConfig& config) + : Widget(config) +{ +} + +lv_obj_t* LedWidget::create(lv_obj_t* parent) { + obj_ = lv_led_create(parent); + + lv_obj_set_pos(obj_, config_.x, config_.y); + if (config_.width > 0 && config_.height > 0) { + lv_obj_set_size(obj_, config_.width, config_.height); + } + + return obj_; +} + +void LedWidget::applyStyle() { + if (obj_ == nullptr) return; + + // LED-specific styling + lv_obj_set_style_radius(obj_, LV_RADIUS_CIRCLE, 0); + lv_led_set_color(obj_, lv_color_make( + config_.bgColor.r, config_.bgColor.g, config_.bgColor.b)); + lv_led_set_brightness(obj_, config_.bgOpacity); + + // Shadow + applyShadowStyle(); +} + +void LedWidget::onKnxSwitch(bool value) { + if (obj_ == nullptr) return; + if (config_.textSource != TextSource::KNX_DPT_SWITCH) return; + + uint8_t brightness = value ? (config_.bgOpacity > 0 ? config_.bgOpacity : 255) : 0; + lv_led_set_brightness(obj_, brightness); +} diff --git a/main/widgets/LedWidget.hpp b/main/widgets/LedWidget.hpp new file mode 100644 index 0000000..dde5577 --- /dev/null +++ b/main/widgets/LedWidget.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "Widget.hpp" + +class LedWidget : public Widget { +public: + explicit LedWidget(const WidgetConfig& config); + + lv_obj_t* create(lv_obj_t* parent) override; + void applyStyle() override; + + // KNX update (LED responds to switch) + void onKnxSwitch(bool value) override; +}; diff --git a/main/widgets/Widget.cpp b/main/widgets/Widget.cpp new file mode 100644 index 0000000..34c53a8 --- /dev/null +++ b/main/widgets/Widget.cpp @@ -0,0 +1,96 @@ +#include "Widget.hpp" + +Widget::Widget(const WidgetConfig& config) + : config_(config) + , obj_(nullptr) +{ +} + +Widget::~Widget() { + destroy(); +} + +void Widget::destroy() { + if (obj_ != nullptr) { + lv_obj_delete(obj_); + obj_ = nullptr; + } +} + +void Widget::applyStyle() { + if (obj_ == nullptr) return; + applyCommonStyle(); +} + +void Widget::onKnxValue(float /*value*/) { + // Default: do nothing +} + +void Widget::onKnxSwitch(bool /*value*/) { + // Default: do nothing +} + +void Widget::onKnxText(const char* /*text*/) { + // Default: do nothing +} + +void Widget::applyCommonStyle() { + if (obj_ == nullptr) return; + + // Text color + lv_obj_set_style_text_color(obj_, lv_color_make( + config_.textColor.r, config_.textColor.g, config_.textColor.b), 0); + + // Font + lv_obj_set_style_text_font(obj_, getFontBySize(config_.fontSize), 0); + + // Background (for buttons and labels with bg) + if (config_.bgOpacity > 0) { + lv_obj_set_style_bg_color(obj_, lv_color_make( + config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0); + lv_obj_set_style_bg_opa(obj_, config_.bgOpacity, 0); + } + + // Border radius + if (config_.borderRadius > 0) { + lv_obj_set_style_radius(obj_, config_.borderRadius, 0); + } + + // Shadow + applyShadowStyle(); +} + +void Widget::applyShadowStyle() { + if (obj_ == nullptr || !config_.shadow.enabled) return; + + lv_obj_set_style_shadow_color(obj_, lv_color_make( + config_.shadow.color.r, config_.shadow.color.g, config_.shadow.color.b), 0); + lv_obj_set_style_shadow_opa(obj_, 180, 0); + lv_obj_set_style_shadow_width(obj_, config_.shadow.blur, 0); + lv_obj_set_style_shadow_spread(obj_, config_.shadow.spread, 0); + lv_obj_set_style_shadow_offset_x(obj_, config_.shadow.offsetX, 0); + lv_obj_set_style_shadow_offset_y(obj_, config_.shadow.offsetY, 0); +} + +const lv_font_t* Widget::getFontBySize(uint8_t sizeIndex) { + // Font sizes: 0=14, 1=18, 2=22, 3=28, 4=36, 5=48 + switch (sizeIndex) { + case 0: return &lv_font_montserrat_14; +#if LV_FONT_MONTSERRAT_18 + case 1: return &lv_font_montserrat_18; +#endif +#if LV_FONT_MONTSERRAT_22 + case 2: return &lv_font_montserrat_22; +#endif +#if LV_FONT_MONTSERRAT_28 + case 3: return &lv_font_montserrat_28; +#endif +#if LV_FONT_MONTSERRAT_36 + case 4: return &lv_font_montserrat_36; +#endif +#if LV_FONT_MONTSERRAT_48 + case 5: return &lv_font_montserrat_48; +#endif + default: return &lv_font_montserrat_14; + } +} diff --git a/main/widgets/Widget.hpp b/main/widgets/Widget.hpp new file mode 100644 index 0000000..d975d0d --- /dev/null +++ b/main/widgets/Widget.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include "../WidgetConfig.hpp" +#include "lvgl.h" + +class Widget { +public: + explicit Widget(const WidgetConfig& config); + virtual ~Widget(); + + // Delete copy/move + Widget(const Widget&) = delete; + Widget& operator=(const Widget&) = delete; + + // Destroy LVGL object + void destroy(); + + // Access to LVGL object + lv_obj_t* getLvglObject() const { return obj_; } + + // Widget ID + uint8_t getId() const { return config_.id; } + + // KNX group address for read binding + uint16_t getKnxAddress() const { return config_.knxAddress; } + + // TextSource for KNX callback filtering + TextSource getTextSource() const { return config_.textSource; } + + // Widget type + WidgetType getType() const { return config_.type; } + + // Config access (for button action handling) + const WidgetConfig& getConfig() const { return config_; } + + // Create LVGL widget on parent - must be implemented by subclasses + virtual lv_obj_t* create(lv_obj_t* parent) = 0; + + // Apply styling after create() - can be overridden + virtual void applyStyle(); + + // KNX callbacks - default implementations do nothing + virtual void onKnxValue(float value); + virtual void onKnxSwitch(bool value); + virtual void onKnxText(const char* text); + +protected: + // Common style helper functions + void applyCommonStyle(); + void applyShadowStyle(); + static const lv_font_t* getFontBySize(uint8_t sizeIndex); + + const WidgetConfig& config_; + lv_obj_t* obj_ = nullptr; +}; diff --git a/main/widgets/WidgetFactory.cpp b/main/widgets/WidgetFactory.cpp new file mode 100644 index 0000000..bef481a --- /dev/null +++ b/main/widgets/WidgetFactory.cpp @@ -0,0 +1,19 @@ +#include "WidgetFactory.hpp" +#include "LabelWidget.hpp" +#include "ButtonWidget.hpp" +#include "LedWidget.hpp" + +std::unique_ptr WidgetFactory::create(const WidgetConfig& config) { + if (!config.visible) return nullptr; + + switch (config.type) { + case WidgetType::LABEL: + return std::make_unique(config); + case WidgetType::BUTTON: + return std::make_unique(config); + case WidgetType::LED: + return std::make_unique(config); + default: + return nullptr; + } +} diff --git a/main/widgets/WidgetFactory.hpp b/main/widgets/WidgetFactory.hpp new file mode 100644 index 0000000..7578a60 --- /dev/null +++ b/main/widgets/WidgetFactory.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "Widget.hpp" +#include "../WidgetConfig.hpp" +#include + +class WidgetFactory { +public: + // Create widget based on WidgetType + static std::unique_ptr create(const WidgetConfig& config); +};