This commit is contained in:
Thomas Peterson 2026-01-24 10:42:15 +01:00
parent f34eb810da
commit a4cefd7c09
39 changed files with 754 additions and 441 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

222
main/WidgetConfig.cpp Normal file
View File

@ -0,0 +1,222 @@
#include "WidgetConfig.hpp"
#include <cstring>
#include <cstdio>
// 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<uint8_t>(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<uint8_t>(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<uint8_t>(action);
buf[pos++] = targetScreen;
}
void WidgetConfig::deserialize(const uint8_t* buf) {
size_t pos = 0;
id = buf[pos++];
type = static_cast<WidgetType>(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<TextSource>(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<int8_t>(buf[pos++]);
shadow.offsetY = static_cast<int8_t>(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<ButtonAction>(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<uint8_t>(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;
}

View File

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

View File

@ -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 <memory>
#include <new>
@ -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<WidgetConfig*>(lv_event_get_user_data(e));
if (!cfg) return;
lv_obj_t* target = static_cast<lv_obj_t*>(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<uint8_t>(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<uint8_t>(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<uint8_t>(action);
buf[pos++] = targetScreen;
}
void WidgetConfig::deserialize(const uint8_t* buf) {
size_t pos = 0;
id = buf[pos++];
type = static_cast<WidgetType>(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<TextSource>(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<int8_t>(buf[pos++]);
shadow.offsetY = static_cast<int8_t>(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<ButtonAction>(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<uint8_t>(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<WidgetConfig*>(&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;

View File

@ -1,8 +1,10 @@
#pragma once
#include "WidgetConfig.hpp"
#include "widgets/Widget.hpp"
#include "lvgl.h"
#include <array>
#include <memory>
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<lv_obj_t*, MAX_WIDGETS> widgetObjects_;
// Runtime widget instances (indexed by widget ID)
std::array<std::unique_ptr<Widget>, MAX_WIDGETS> widgets_;
lv_obj_t* screen_ = nullptr;
lv_obj_t* modalContainer_ = nullptr;
lv_obj_t* modalDimmer_ = nullptr;
};

View File

@ -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<ButtonWidget*>(lv_event_get_user_data(e));
if (!widget) return;
lv_obj_t* target = static_cast<lv_obj_t*>(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;
}

View File

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

View File

@ -0,0 +1,42 @@
#include "LabelWidget.hpp"
#include <cstdio>
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);
}

View File

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

View File

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

View File

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

96
main/widgets/Widget.cpp Normal file
View File

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

55
main/widgets/Widget.hpp Normal file
View File

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

View File

@ -0,0 +1,19 @@
#include "WidgetFactory.hpp"
#include "LabelWidget.hpp"
#include "ButtonWidget.hpp"
#include "LedWidget.hpp"
std::unique_ptr<Widget> WidgetFactory::create(const WidgetConfig& config) {
if (!config.visible) return nullptr;
switch (config.type) {
case WidgetType::LABEL:
return std::make_unique<LabelWidget>(config);
case WidgetType::BUTTON:
return std::make_unique<ButtonWidget>(config);
case WidgetType::LED:
return std::make_unique<LedWidget>(config);
default:
return nullptr;
}
}

View File

@ -0,0 +1,11 @@
#pragma once
#include "Widget.hpp"
#include "../WidgetConfig.hpp"
#include <memory>
class WidgetFactory {
public:
// Create widget based on WidgetType
static std::unique_ptr<Widget> create(const WidgetConfig& config);
};