#include "WidgetManager.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 #include #include #include 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; return inst; } WidgetManager::WidgetManager() { widgetObjects_.fill(nullptr); createDefaultConfig(); activeScreenId_ = config_.startScreenId; lastActivityUs_ = esp_timer_get_time(); } void WidgetManager::createDefaultConfig() { config_.clear(); config_.screenCount = 1; ScreenConfig& screen = config_.screens[0]; screen.clear(0, "Screen 1"); // Default: Temperature label auto tempLabel = WidgetConfig::createKnxLabel(0, 50, 20, TextSource::KNX_DPT_TEMP, 1, "%.1f °C"); tempLabel.fontSize = 3; // 28pt screen.addWidget(tempLabel); // Default: KNX Prog button auto progBtn = WidgetConfig::createButton(1, 50, 100, "KNX Prog", 0, true); progBtn.bgColor = {200, 50, 50}; // Red screen.addWidget(progBtn); config_.startScreenId = screen.id; config_.standbyEnabled = false; config_.standbyScreenId = 0xFF; config_.standbyMinutes = 0; activeScreenId_ = screen.id; } void WidgetManager::init() { loadFromSdCard(); if (config_.findScreen(config_.startScreenId)) { activeScreenId_ = config_.startScreenId; } else if (config_.screenCount > 0) { activeScreenId_ = config_.screens[0].id; } else { activeScreenId_ = 0; } lastActivityUs_ = esp_timer_get_time(); ESP_LOGI(TAG, "WidgetManager initialized with %d screens", config_.screenCount); } void WidgetManager::loadFromSdCard() { if (!SdCard::instance().isMounted()) { ESP_LOGI(TAG, "SD card not mounted, using defaults"); return; } FILE* f = fopen(CONFIG_FILE, "r"); if (!f) { ESP_LOGI(TAG, "No config file found, using defaults"); return; } // Get file size fseek(f, 0, SEEK_END); long size = ftell(f); fseek(f, 0, SEEK_SET); if (size <= 0 || size > 32768) { ESP_LOGE(TAG, "Invalid config file size: %ld", size); fclose(f); return; } // Read file content char* json = new char[size + 1]; if (!json) { ESP_LOGE(TAG, "Failed to allocate memory for config"); fclose(f); return; } size_t read = fread(json, 1, size, f); fclose(f); json[read] = '\0'; // Parse JSON using cJSON bool success = updateConfigFromJson(json); delete[] json; if (success) { ESP_LOGI(TAG, "Loaded %d screens from SD card", config_.screenCount); } else { ESP_LOGE(TAG, "Failed to parse config file"); } } void WidgetManager::saveToSdCard() { if (!SdCard::instance().isMounted()) { ESP_LOGE(TAG, "SD card not mounted, cannot save config"); return; } // Generate JSON using cJSON char* json = new char[32768]; if (!json) { ESP_LOGE(TAG, "Failed to allocate memory for JSON"); return; } getConfigJson(json, 32768); // Write to file FILE* f = fopen(CONFIG_FILE, "w"); if (!f) { ESP_LOGE(TAG, "Failed to open config file for writing"); delete[] json; return; } size_t written = fwrite(json, 1, strlen(json), f); fclose(f); delete[] json; if (written > 0) { ESP_LOGI(TAG, "Saved %d screens to SD card", config_.screenCount); } else { ESP_LOGE(TAG, "Failed to write config file"); } } void WidgetManager::applyConfig() { if (!config_.findScreen(activeScreenId_)) { if (config_.findScreen(config_.startScreenId)) { activeScreenId_ = config_.startScreenId; } else if (config_.screenCount > 0) { activeScreenId_ = config_.screens[0].id; } } applyScreen(activeScreenId_); } void WidgetManager::saveAndApply() { saveToSdCard(); applyConfig(); ESP_LOGI(TAG, "Config saved and applied"); } void WidgetManager::resetToDefaults() { closeModal(); standbyActive_ = false; standbyWakePending_ = false; standbyReturnScreenId_ = SCREEN_ID_NONE; previousScreenId_ = SCREEN_ID_NONE; createDefaultConfig(); saveAndApply(); ESP_LOGI(TAG, "Reset to defaults"); } ScreenConfig* WidgetManager::activeScreen() { if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) { return config_.findScreen(modalScreenId_); } return config_.findScreen(activeScreenId_); } const ScreenConfig* WidgetManager::activeScreen() const { if (modalContainer_ && modalScreenId_ != SCREEN_ID_NONE) { return config_.findScreen(modalScreenId_); } return config_.findScreen(activeScreenId_); } void WidgetManager::applyScreen(uint8_t screenId) { ScreenConfig* screen = config_.findScreen(screenId); if (!screen) { ESP_LOGW(TAG, "Screen %d not found", screenId); return; } if (modalContainer_) { closeModal(); } 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(); } } void WidgetManager::showModalScreen(const ScreenConfig& screen) { if (modalContainer_) { closeModal(); } 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); modalContainer_ = overlay; modalScreenId_ = screen.id; createAllWidgets(screen, modalContainer_); esp_lv_adapter_unlock(); } void WidgetManager::closeModal() { if (!modalContainer_) return; if (esp_lv_adapter_lock(-1) == ESP_OK) { lv_obj_delete(modalContainer_); esp_lv_adapter_unlock(); } modalContainer_ = nullptr; modalScreenId_ = SCREEN_ID_NONE; widgetObjects_.fill(nullptr); } void WidgetManager::showScreen(uint8_t screenId) { ScreenConfig* screen = config_.findScreen(screenId); if (!screen) { ESP_LOGW(TAG, "Screen %d not found", screenId); return; } if (screen->mode == ScreenMode::MODAL) { showModalScreen(*screen); return; } previousScreenId_ = activeScreenId_; activeScreenId_ = screen->id; standbyActive_ = false; applyScreen(activeScreenId_); } void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) { if (cfg.type != WidgetType::BUTTON) return; onUserActivity(); switch (cfg.action) { case ButtonAction::JUMP: navPending_ = true; navAction_ = ButtonAction::JUMP; navTargetScreen_ = cfg.targetScreen; break; case ButtonAction::BACK: navPending_ = true; navAction_ = ButtonAction::BACK; navTargetScreen_ = SCREEN_ID_NONE; break; case ButtonAction::KNX: default: { if (cfg.knxAddressWrite > 0) { bool state = false; if (target) { state = (lv_obj_get_state(target) & LV_STATE_CHECKED) != 0; } 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; } } } void WidgetManager::goBack() { if (modalContainer_) { closeModal(); applyScreen(activeScreenId_); return; } if (previousScreenId_ != SCREEN_ID_NONE && previousScreenId_ != activeScreenId_) { activeScreenId_ = previousScreenId_; previousScreenId_ = SCREEN_ID_NONE; applyScreen(activeScreenId_); } } void WidgetManager::enterStandby() { if (!config_.standbyEnabled || config_.standbyMinutes == 0) return; if (standbyActive_) return; if (config_.standbyScreenId == SCREEN_ID_NONE) return; ScreenConfig* standbyScreen = config_.findScreen(config_.standbyScreenId); if (!standbyScreen) return; standbyReturnScreenId_ = activeScreenId_; standbyActive_ = true; activeScreenId_ = standbyScreen->id; applyScreen(activeScreenId_); } void WidgetManager::loop() { if (navPending_) { navPending_ = false; if (navAction_ == ButtonAction::JUMP) { showScreen(navTargetScreen_); } else if (navAction_ == ButtonAction::BACK) { goBack(); } return; } if (standbyWakePending_) { standbyWakePending_ = false; if (standbyWakeTarget_ != SCREEN_ID_NONE) { activeScreenId_ = standbyWakeTarget_; applyScreen(activeScreenId_); } return; } if (!config_.standbyEnabled || config_.standbyMinutes == 0) return; if (standbyActive_) return; if (config_.standbyScreenId == SCREEN_ID_NONE) return; int64_t now = esp_timer_get_time(); int64_t idleUs = now - lastActivityUs_; int64_t timeoutUs = static_cast(config_.standbyMinutes) * 60 * 1000000LL; if (idleUs >= timeoutUs) { enterStandby(); } } void WidgetManager::onUserActivity() { lastActivityUs_ = esp_timer_get_time(); if (standbyActive_) { standbyActive_ = false; uint8_t returnId = standbyReturnScreenId_; if (returnId == SCREEN_ID_NONE) { returnId = config_.startScreenId; } standbyWakeTarget_ = returnId; standbyWakePending_ = true; } } void WidgetManager::destroyAllWidgets() { for (auto& obj : widgetObjects_) { if (obj != nullptr) { lv_obj_delete(obj); obj = nullptr; } } } 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; } } } 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); } } } esp_lv_adapter_unlock(); } 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); } } } esp_lv_adapter_unlock(); } 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); } } } esp_lv_adapter_unlock(); } void WidgetManager::getConfigJson(char* buf, size_t bufSize) const { cJSON* root = cJSON_CreateObject(); if (!root) { snprintf(buf, bufSize, "{}"); return; } cJSON_AddNumberToObject(root, "startScreen", config_.startScreenId); cJSON* standby = cJSON_AddObjectToObject(root, "standby"); cJSON_AddBoolToObject(standby, "enabled", config_.standbyEnabled); 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++) { const ScreenConfig& screen = config_.screens[s]; cJSON* screenJson = cJSON_CreateObject(); cJSON_AddNumberToObject(screenJson, "id", screen.id); cJSON_AddStringToObject(screenJson, "name", screen.name); cJSON_AddNumberToObject(screenJson, "mode", static_cast(screen.mode)); char bgColorStr[8]; snprintf(bgColorStr, sizeof(bgColorStr), "#%02X%02X%02X", screen.backgroundColor.r, screen.backgroundColor.g, screen.backgroundColor.b); cJSON_AddStringToObject(screenJson, "bgColor", bgColorStr); cJSON* widgets = cJSON_AddArrayToObject(screenJson, "widgets"); for (uint8_t i = 0; i < screen.widgetCount; i++) { const WidgetConfig& w = screen.widgets[i]; cJSON* widget = cJSON_CreateObject(); cJSON_AddNumberToObject(widget, "id", w.id); cJSON_AddNumberToObject(widget, "type", static_cast(w.type)); cJSON_AddNumberToObject(widget, "x", w.x); cJSON_AddNumberToObject(widget, "y", w.y); cJSON_AddNumberToObject(widget, "w", w.width); cJSON_AddNumberToObject(widget, "h", w.height); cJSON_AddBoolToObject(widget, "visible", w.visible); cJSON_AddNumberToObject(widget, "textSrc", static_cast(w.textSource)); cJSON_AddStringToObject(widget, "text", w.text); 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); cJSON_AddStringToObject(widget, "bgColor", widgetBgColorStr); 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); cJSON_AddNumberToObject(shadow, "y", w.shadow.offsetY); cJSON_AddNumberToObject(shadow, "blur", w.shadow.blur); cJSON_AddNumberToObject(shadow, "spread", w.shadow.spread); char shadowColorStr[8]; snprintf(shadowColorStr, sizeof(shadowColorStr), "#%02X%02X%02X", w.shadow.color.r, w.shadow.color.g, w.shadow.color.b); cJSON_AddStringToObject(shadow, "color", shadowColorStr); cJSON_AddBoolToObject(widget, "isToggle", w.isToggle); cJSON_AddNumberToObject(widget, "knxAddrWrite", w.knxAddressWrite); cJSON_AddNumberToObject(widget, "action", static_cast(w.action)); cJSON_AddNumberToObject(widget, "targetScreen", w.targetScreen); cJSON_AddItemToArray(widgets, widget); } cJSON_AddItemToArray(screens, screenJson); } // Print to buffer char* jsonStr = cJSON_PrintUnformatted(root); if (jsonStr) { strncpy(buf, jsonStr, bufSize - 1); buf[bufSize - 1] = '\0'; free(jsonStr); } else { snprintf(buf, bufSize, "{}"); } 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) { ESP_LOGE(TAG, "Failed to parse JSON"); return false; } std::unique_ptr newConfig(new (std::nothrow) GuiConfig()); if (!newConfig) { ESP_LOGE(TAG, "Out of memory for config"); cJSON_Delete(root); return false; } newConfig->clear(); auto parseWidgets = [&](cJSON* widgets, ScreenConfig& screen) -> bool { if (!cJSON_IsArray(widgets)) return false; screen.widgetCount = 0; cJSON* widget = nullptr; cJSON_ArrayForEach(widget, widgets) { if (screen.widgetCount >= MAX_WIDGETS) break; WidgetConfig& w = screen.widgets[screen.widgetCount]; memset(&w, 0, sizeof(w)); w.visible = true; w.action = ButtonAction::KNX; w.targetScreen = 0; // Parse basic properties cJSON* id = cJSON_GetObjectItem(widget, "id"); if (cJSON_IsNumber(id)) w.id = id->valueint; cJSON* type = cJSON_GetObjectItem(widget, "type"); if (cJSON_IsNumber(type)) w.type = static_cast(type->valueint); cJSON* x = cJSON_GetObjectItem(widget, "x"); if (cJSON_IsNumber(x)) w.x = x->valueint; cJSON* y = cJSON_GetObjectItem(widget, "y"); if (cJSON_IsNumber(y)) w.y = y->valueint; cJSON* width = cJSON_GetObjectItem(widget, "w"); if (cJSON_IsNumber(width)) w.width = width->valueint; cJSON* height = cJSON_GetObjectItem(widget, "h"); if (cJSON_IsNumber(height)) w.height = height->valueint; cJSON* visible = cJSON_GetObjectItem(widget, "visible"); if (cJSON_IsBool(visible)) w.visible = cJSON_IsTrue(visible); cJSON* textSrc = cJSON_GetObjectItem(widget, "textSrc"); if (cJSON_IsNumber(textSrc)) w.textSource = static_cast(textSrc->valueint); cJSON* text = cJSON_GetObjectItem(widget, "text"); if (cJSON_IsString(text)) { strncpy(w.text, text->valuestring, MAX_TEXT_LEN - 1); w.text[MAX_TEXT_LEN - 1] = '\0'; } cJSON* knxAddr = cJSON_GetObjectItem(widget, "knxAddr"); if (cJSON_IsNumber(knxAddr)) w.knxAddress = knxAddr->valueint; 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)); } cJSON* widgetBgColor = cJSON_GetObjectItem(widget, "bgColor"); if (cJSON_IsString(widgetBgColor)) { w.bgColor = Color::fromHex(parseHexColor(widgetBgColor->valuestring)); } cJSON* bgOpacity = cJSON_GetObjectItem(widget, "bgOpacity"); if (cJSON_IsNumber(bgOpacity)) w.bgOpacity = bgOpacity->valueint; 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"); if (cJSON_IsBool(enabled)) w.shadow.enabled = cJSON_IsTrue(enabled); cJSON* sx = cJSON_GetObjectItem(shadow, "x"); if (cJSON_IsNumber(sx)) w.shadow.offsetX = sx->valueint; cJSON* sy = cJSON_GetObjectItem(shadow, "y"); if (cJSON_IsNumber(sy)) w.shadow.offsetY = sy->valueint; cJSON* blur = cJSON_GetObjectItem(shadow, "blur"); if (cJSON_IsNumber(blur)) w.shadow.blur = blur->valueint; cJSON* spread = cJSON_GetObjectItem(shadow, "spread"); if (cJSON_IsNumber(spread)) w.shadow.spread = spread->valueint; cJSON* shadowColor = cJSON_GetObjectItem(shadow, "color"); if (cJSON_IsString(shadowColor)) { w.shadow.color = Color::fromHex(parseHexColor(shadowColor->valuestring)); } } cJSON* isToggle = cJSON_GetObjectItem(widget, "isToggle"); if (cJSON_IsBool(isToggle)) w.isToggle = cJSON_IsTrue(isToggle); cJSON* knxAddrWrite = cJSON_GetObjectItem(widget, "knxAddrWrite"); if (cJSON_IsNumber(knxAddrWrite)) w.knxAddressWrite = knxAddrWrite->valueint; cJSON* action = cJSON_GetObjectItem(widget, "action"); if (cJSON_IsNumber(action)) w.action = static_cast(action->valueint); cJSON* targetScreen = cJSON_GetObjectItem(widget, "targetScreen"); if (cJSON_IsNumber(targetScreen)) w.targetScreen = targetScreen->valueint; screen.widgetCount++; } return true; }; cJSON* screens = cJSON_GetObjectItem(root, "screens"); if (cJSON_IsArray(screens)) { cJSON* screenJson = nullptr; cJSON_ArrayForEach(screenJson, screens) { if (newConfig->screenCount >= MAX_SCREENS) break; uint8_t screenId = newConfig->screenCount; const char* screenName = nullptr; cJSON* id = cJSON_GetObjectItem(screenJson, "id"); if (cJSON_IsNumber(id)) { int idVal = id->valueint; if (idVal < 0) idVal = 0; screenId = static_cast(idVal); } cJSON* name = cJSON_GetObjectItem(screenJson, "name"); if (cJSON_IsString(name)) screenName = name->valuestring; ScreenConfig& screen = newConfig->screens[newConfig->screenCount]; screen.clear(screenId, screenName); if (!screen.name[0]) { char fallback[16]; snprintf(fallback, sizeof(fallback), "Screen %d", screenId); strncpy(screen.name, fallback, sizeof(screen.name) - 1); } cJSON* mode = cJSON_GetObjectItem(screenJson, "mode"); if (cJSON_IsNumber(mode)) screen.mode = static_cast(mode->valueint); cJSON* bgColor = cJSON_GetObjectItem(screenJson, "bgColor"); if (cJSON_IsString(bgColor)) { screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring)); } cJSON* widgets = cJSON_GetObjectItem(screenJson, "widgets"); if (!parseWidgets(widgets, screen)) { screen.widgetCount = 0; } newConfig->screenCount++; } } else { newConfig->screenCount = 1; ScreenConfig& screen = newConfig->screens[0]; screen.clear(0, "Screen 1"); cJSON* bgColor = cJSON_GetObjectItem(root, "bgColor"); if (cJSON_IsString(bgColor)) { screen.backgroundColor = Color::fromHex(parseHexColor(bgColor->valuestring)); } cJSON* widgets = cJSON_GetObjectItem(root, "widgets"); if (!parseWidgets(widgets, screen)) { cJSON_Delete(root); return false; } } cJSON* startScreen = cJSON_GetObjectItem(root, "startScreen"); if (cJSON_IsNumber(startScreen)) { int val = startScreen->valueint; if (val < 0) val = 0; newConfig->startScreenId = static_cast(val); } cJSON* standby = cJSON_GetObjectItem(root, "standby"); if (cJSON_IsObject(standby)) { cJSON* enabled = cJSON_GetObjectItem(standby, "enabled"); if (cJSON_IsBool(enabled)) newConfig->standbyEnabled = cJSON_IsTrue(enabled); cJSON* screen = cJSON_GetObjectItem(standby, "screen"); if (cJSON_IsNumber(screen)) { int val = screen->valueint; if (val < 0) { newConfig->standbyScreenId = SCREEN_ID_NONE; } else { newConfig->standbyScreenId = static_cast(val); } } cJSON* minutes = cJSON_GetObjectItem(standby, "minutes"); if (cJSON_IsNumber(minutes)) { int val = minutes->valueint; if (val < 0) val = 0; newConfig->standbyMinutes = static_cast(val); } } if (newConfig->screenCount == 0) { cJSON_Delete(root); return false; } if (!newConfig->findScreen(newConfig->startScreenId)) { newConfig->startScreenId = newConfig->screens[0].id; } if (!newConfig->findScreen(newConfig->standbyScreenId)) { newConfig->standbyEnabled = false; newConfig->standbyScreenId = SCREEN_ID_NONE; } config_ = *newConfig; cJSON_Delete(root); ESP_LOGI(TAG, "Parsed %d screens from JSON", config_.screenCount); return true; }