#include "WidgetManager.hpp" #include "widgets/WidgetFactory.hpp" #include "widgets/RoomCardWidget.hpp" #include "HistoryStore.hpp" #include "SdCard.hpp" #include "esp_lv_adapter.h" #include "esp_log.h" #include "esp_timer.h" #include "esp_heap_caps.h" #include "cJSON.h" #include #include #include #include #include #include #include static const char* TAG = "WidgetMgr"; static constexpr uint8_t SCREEN_ID_NONE = 0xFF; #if LV_USE_OBJ_NAME static void dump_flex_objects(lv_obj_t* obj, uint8_t depth) { if (!obj) return; if (lv_obj_get_style_layout(obj, LV_PART_MAIN) == LV_LAYOUT_FLEX) { char name[64]; lv_obj_get_name_resolved(obj, name, sizeof(name)); ESP_LOGI("FlexMap", "flex obj=%p name=%s depth=%u", obj, name, depth); } uint32_t count = lv_obj_get_child_count(obj); for (uint32_t i = 0; i < count; ++i) { dump_flex_objects(lv_obj_get_child(obj, static_cast(i)), depth + 1); } } #endif static bool is_valid_utf8(const char* text, size_t len) { size_t i = 0; while (i < len) { uint8_t c = static_cast(text[i]); if (c < 0x80) { i++; continue; } if ((c & 0xE0) == 0xC0) { if (i + 1 >= len) return false; uint8_t c1 = static_cast(text[i + 1]); if ((c1 & 0xC0) != 0x80) return false; i += 2; continue; } if ((c & 0xF0) == 0xE0) { if (i + 2 >= len) return false; uint8_t c1 = static_cast(text[i + 1]); uint8_t c2 = static_cast(text[i + 2]); if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80) return false; i += 3; continue; } if ((c & 0xF8) == 0xF0) { if (i + 3 >= len) return false; uint8_t c1 = static_cast(text[i + 1]); uint8_t c2 = static_cast(text[i + 2]); uint8_t c3 = static_cast(text[i + 3]); if ((c1 & 0xC0) != 0x80 || (c2 & 0xC0) != 0x80 || (c3 & 0xC0) != 0x80) return false; i += 4; continue; } return false; } return true; } static void latin1_to_utf8(const char* src, size_t src_len, char* dst, size_t dst_size) { if (dst_size == 0) return; size_t di = 0; for (size_t si = 0; si < src_len; ++si) { uint8_t c = static_cast(src[si]); if (c < 0x80) { if (di + 1 >= dst_size) break; dst[di++] = static_cast(c); } else { if (di + 2 >= dst_size) break; dst[di++] = static_cast(0xC0 | (c >> 6)); dst[di++] = static_cast(0x80 | (c & 0x3F)); } } dst[di] = '\0'; } static WidgetConfig makeButtonLabelChild(const WidgetConfig& button) { WidgetConfig label = WidgetConfig::createLabel(0, 0, 0, button.text); label.parentId = button.id; if (button.width > 0) label.width = button.width; if (button.height > 0) label.height = button.height; label.fontSize = button.fontSize; label.textAlign = button.textAlign; label.textColor = button.textColor; label.textSource = TextSource::STATIC; label.bgOpacity = 0; label.borderRadius = 0; label.shadow.enabled = false; // Preserve existing icon config if any label.iconCodepoint = button.iconCodepoint; label.iconPosition = button.iconPosition; label.iconSize = button.iconSize; label.iconGap = button.iconGap; if (label.text[0] == '\0') { strncpy(label.text, "Button", MAX_TEXT_LEN - 1); label.text[MAX_TEXT_LEN - 1] = '\0'; } return label; } static void ensureButtonLabels(ScreenConfig& screen) { bool hasLabelChild[MAX_WIDGETS] = {}; for (uint8_t i = 0; i < screen.widgetCount; i++) { const WidgetConfig& w = screen.widgets[i]; if (w.type == WidgetType::LABEL && w.parentId >= 0 && w.parentId < MAX_WIDGETS) { hasLabelChild[w.parentId] = true; } } const uint8_t initialCount = screen.widgetCount; for (uint8_t i = 0; i < initialCount; i++) { WidgetConfig& w = screen.widgets[i]; if (w.type != WidgetType::BUTTON) continue; w.isContainer = true; if (w.id < MAX_WIDGETS && hasLabelChild[w.id]) continue; WidgetConfig label = makeButtonLabelChild(w); int newId = screen.addWidget(label); if (newId < 0) { ESP_LOGW(TAG, "No space to add label child for button %d", w.id); w.isContainer = false; continue; } if (w.id < MAX_WIDGETS) { hasLabelChild[w.id] = true; } } } // WidgetManager implementation WidgetManager& WidgetManager::instance() { static WidgetManager inst; return inst; } WidgetManager::WidgetManager() { // Allocate GuiConfig in PSRAM to save internal RAM config_ = static_cast(heap_caps_malloc(sizeof(GuiConfig), MALLOC_CAP_SPIRAM)); if (!config_) { ESP_LOGE(TAG, "Failed to allocate GuiConfig in PSRAM, trying internal RAM"); config_ = new GuiConfig(); } else { new (config_) GuiConfig(); // Placement new to call constructor } // widgets_ is default-initialized to nullptr portMUX_INITIALIZE(&knxCacheMux_); uiQueue_ = xQueueCreate(UI_EVENT_QUEUE_LEN, sizeof(UiEvent)); if (!uiQueue_) { ESP_LOGE(TAG, "Failed to create UI event queue"); } 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); ensureButtonLabels(screen); config_->startScreenId = screen.id; config_->standbyEnabled = false; config_->standbyScreenId = 0xFF; config_->standbyMinutes = 0; activeScreenId_ = screen.id; } void WidgetManager::init() { loadFromSdCard(); HistoryStore::instance().configureFromConfig(*config_); HistoryStore::instance().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; } 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; } 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'; 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; } char* json = new char[32768]; if (!json) { ESP_LOGE(TAG, "Failed to allocate memory for JSON"); return; } getConfigJson(json, 32768); 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() { HistoryStore::instance().configureFromConfig(*config_); 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_); } const ScreenConfig* WidgetManager::currentScreen() const { return activeScreen(); } void WidgetManager::applyScreen(uint8_t screenId) { if (esp_lv_adapter_lock(-1) != ESP_OK) { ESP_LOGE(TAG, "Failed to acquire LVGL lock!"); return; } applyScreenLocked(screenId); esp_lv_adapter_unlock(); } void WidgetManager::applyScreenLocked(uint8_t screenId) { ESP_LOGI(TAG, "applyScreen(%d) - start", screenId); ScreenConfig* screen = config_->findScreen(screenId); if (!screen) { ESP_LOGW(TAG, "Screen %d not found", screenId); return; } ensureButtonLabels(*screen); if (modalContainer_) { ESP_LOGI(TAG, "Closing modal first"); closeModalLocked(); } lv_display_t* disp = lv_display_get_default(); if (disp) { lv_display_enable_invalidation(disp, false); } // SAFE DESTRUCTION: // 1. Mark all C++ widgets as "LVGL object already gone" for (auto& widget : widgets_) { if (widget) widget->clearLvglObject(); } // 2. Delete all LVGL objects on layers we use lv_obj_clean(lv_scr_act()); lv_obj_clean(lv_layer_top()); // 3. Now destroy C++ objects (their destructors won't call lv_obj_delete) destroyAllWidgets(); ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...", screen->name, screen->widgetCount); lv_obj_t* root = lv_scr_act(); createAllWidgets(*screen, root); ESP_LOGI(TAG, "Widgets created"); applyCachedValuesToWidgets(); if (disp) { lv_display_enable_invalidation(disp, true); } lv_obj_invalidate(lv_scr_act()); ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId); } void WidgetManager::showModalScreen(const ScreenConfig& screen) { if (esp_lv_adapter_lock(-1) != ESP_OK) return; showModalScreenLocked(screen); esp_lv_adapter_unlock(); } void WidgetManager::showModalScreenLocked(const ScreenConfig& screen) { if (modalContainer_) { closeModalLocked(); } // SAFE DESTRUCTION for (auto& widget : widgets_) { if (widget) widget->clearLvglObject(); } lv_obj_clean(lv_scr_act()); // Should we clean screen when showing modal? // Actually, usually modal is ON TOP of screen. // But our current WidgetManager destroys screen widgets when showing modal! // That's why we clean. lv_obj_clean(lv_layer_top()); destroyAllWidgets(); lv_disp_t* disp = lv_disp_get_default(); int32_t dispWidth = disp ? lv_disp_get_hor_res(disp) : 1280; int32_t dispHeight = disp ? lv_disp_get_ver_res(disp) : 800; // Create semi-transparent background overlay if dimming enabled lv_obj_t* dimmer = nullptr; if (screen.modalDimBackground) { dimmer = lv_obj_create(lv_layer_top()); if (dimmer) { 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; // 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 < 256) { widget->create(modal); widget->applyStyle(); widgets_[cfg.id] = std::move(widget); } } applyCachedValuesToWidgets(); ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight); } void WidgetManager::closeModal() { if (esp_lv_adapter_lock(-1) != ESP_OK) return; closeModalLocked(); esp_lv_adapter_unlock(); } void WidgetManager::closeModalLocked() { printf("WM: closeModal Start. Container=%p\n", (void*)modalContainer_); fflush(stdout); if (!modalContainer_) { return; } // SAFE DESTRUCTION for (auto& widget : widgets_) { if (widget) widget->clearLvglObject(); } lv_obj_clean(lv_layer_top()); // Destroy widgets first printf("WM: closeModal destroying widgets...\n"); fflush(stdout); destroyAllWidgets(); modalDimmer_ = nullptr; modalContainer_ = nullptr; modalScreenId_ = SCREEN_ID_NONE; printf("WM: closeModal Complete\n"); fflush(stdout); } void WidgetManager::showScreenLocked(uint8_t screenId) { ESP_LOGI(TAG, "showScreen(%d) called", screenId); ScreenConfig* screen = config_->findScreen(screenId); if (!screen) { ESP_LOGW(TAG, "Screen %d not found", screenId); return; } ESP_LOGI(TAG, "Found screen '%s', mode=%d", screen->name, static_cast(screen->mode)); if (screen->mode == ScreenMode::MODAL) { showModalScreenLocked(*screen); return; } previousScreenId_ = activeScreenId_; activeScreenId_ = screen->id; standbyActive_ = false; applyScreenLocked(activeScreenId_); } void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) { printf("WM: handleButtonAction btn=%d act=%d type=%d\n", cfg.id, (int)cfg.action, (int)cfg.type); fflush(stdout); if (cfg.type != WidgetType::BUTTON) { printf("WM: Not a button!\n"); fflush(stdout); return; } onUserActivity(); switch (cfg.action) { case ButtonAction::JUMP: printf("WM: Action JUMP to %d\n", cfg.targetScreen); fflush(stdout); navAction_ = ButtonAction::JUMP; navTargetScreen_ = cfg.targetScreen; navPending_ = true; navRequestUs_ = esp_timer_get_time(); break; case ButtonAction::BACK: printf("WM: Action BACK\n"); fflush(stdout); navAction_ = ButtonAction::BACK; navTargetScreen_ = SCREEN_ID_NONE; navPending_ = true; navRequestUs_ = esp_timer_get_time(); 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 } break; } } } void WidgetManager::goBackLocked() { printf("WM: goBack called. Modal=%p Active=%d Prev=%d\n", (void*)modalContainer_, activeScreenId_, previousScreenId_); fflush(stdout); if (modalContainer_) { printf("WM: Closing modal...\n"); fflush(stdout); closeModalLocked(); printf("WM: Modal closed. Restoring screen %d\n", activeScreenId_); fflush(stdout); // Restore the active screen (which was in background) if (config_->findScreen(activeScreenId_)) { applyScreenLocked(activeScreenId_); } else { ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_); if (config_->findScreen(config_->startScreenId)) { activeScreenId_ = config_->startScreenId; applyScreenLocked(activeScreenId_); } } return; } if (previousScreenId_ != SCREEN_ID_NONE && previousScreenId_ != activeScreenId_) { printf("WM: Going back to screen %d\n", previousScreenId_); fflush(stdout); if (config_->findScreen(previousScreenId_)) { activeScreenId_ = previousScreenId_; previousScreenId_ = SCREEN_ID_NONE; applyScreenLocked(activeScreenId_); } else { ESP_LOGW(TAG, "Previous screen %d not found", previousScreenId_); previousScreenId_ = SCREEN_ID_NONE; } } else { printf("WM: No previous screen to go back to\n"); fflush(stdout); } } void WidgetManager::goBack() { if (esp_lv_adapter_lock(-1) != ESP_OK) return; goBackLocked(); esp_lv_adapter_unlock(); } void WidgetManager::navigateToScreen(uint8_t screenId) { navAction_ = ButtonAction::JUMP; navTargetScreen_ = screenId; navPending_ = true; navRequestUs_ = esp_timer_get_time(); } void WidgetManager::navigateBack() { navAction_ = ButtonAction::BACK; navTargetScreen_ = SCREEN_ID_NONE; navPending_ = true; navRequestUs_ = esp_timer_get_time(); } void WidgetManager::sendKnxSwitch(uint16_t groupAddr, bool value) { ESP_LOGI(TAG, "sendKnxSwitch: GA=%d, value=%d", groupAddr, value); // TODO: Send actual KNX telegram via KnxWorker // For now, just log and update cache so UI reflects the change cacheKnxSwitch(groupAddr, value); } 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; applyScreenLocked(activeScreenId_); } void WidgetManager::loop() { static uint32_t loopCount = 0; loopCount++; if (loopCount % 40 == 0) { ESP_LOGI(TAG, "Heap: %lu | Internal: %lu", esp_get_free_heap_size(), heap_caps_get_free_size(MALLOC_CAP_INTERNAL)); } bool didUiNav = false; if (navPending_) { int64_t now = esp_timer_get_time(); if (now - navRequestUs_ >= NAV_DELAY_US) { navPending_ = false; printf("WM: [TRACE] Nav start\n"); fflush(stdout); if (navAction_ == ButtonAction::JUMP) { showScreenLocked(navTargetScreen_); } else if (navAction_ == ButtonAction::BACK) { goBackLocked(); } didUiNav = true; printf("WM: [TRACE] Nav end\n"); fflush(stdout); } } // printf("WM: [TRACE] Queue start\n"); fflush(stdout); processUiQueue(); // printf("WM: [TRACE] Queue end\n"); fflush(stdout); // printf("WM: [TRACE] Tick start\n"); fflush(stdout); if (HistoryStore::instance().tick()) { refreshChartWidgets(); } // printf("WM: [TRACE] Tick end\n"); fflush(stdout); // printf("WM: [TRACE] Time start\n"); fflush(stdout); updateSystemTimeWidgets(); // printf("WM: [TRACE] Time end\n"); fflush(stdout); if (didUiNav) 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() { ESP_LOGI(TAG, "destroyAllWidgets: Start (%d widgets)", widgets_.size()); // Destroy in reverse order (last created first) for (int i = static_cast(widgets_.size()) - 1; i >= 0; i--) { if (widgets_[i]) { ESP_LOGD(TAG, "Destroying widget %d", i); widgets_[i].reset(); } } ESP_LOGI(TAG, "destroyAllWidgets: Complete"); } void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent) { screen_ = parent; 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); // Background image (if set) // Note: Requires LV_USE_FS_POSIX=y in sdkconfig with LV_FS_POSIX_LETTER='S' (83) if (screen.bgImagePath[0] != '\0') { char fullPath[64]; snprintf(fullPath, sizeof(fullPath), "S:/sdcard%s", screen.bgImagePath); // Check if file exists, try uppercase IMAGES as fallback struct stat st; char checkPath[64]; snprintf(checkPath, sizeof(checkPath), "/sdcard%s", screen.bgImagePath); if (stat(checkPath, &st) != 0) { // Try uppercase /IMAGES/ instead of /images/ if (strncmp(screen.bgImagePath, "/images/", 8) == 0) { snprintf(fullPath, sizeof(fullPath), "S:/sdcard/IMAGES%s", screen.bgImagePath + 7); ESP_LOGI(TAG, "Trying uppercase path: %s", fullPath); } } ESP_LOGI(TAG, "Loading background image: %s", fullPath); lv_obj_t* bgImg = lv_image_create(parent); lv_image_set_src(bgImg, fullPath); // Position at top-left lv_obj_set_pos(bgImg, 0, 0); // Apply scaling mode switch (screen.bgImageMode) { case BgImageMode::STRETCH: lv_obj_set_size(bgImg, lv_pct(100), lv_pct(100)); lv_image_set_inner_align(bgImg, LV_IMAGE_ALIGN_STRETCH); break; case BgImageMode::CENTER: lv_obj_center(bgImg); break; case BgImageMode::TILE: lv_image_set_inner_align(bgImg, LV_IMAGE_ALIGN_TILE); lv_obj_set_size(bgImg, lv_pct(100), lv_pct(100)); break; default: break; } // Send to background (behind all widgets) lv_obj_move_to_index(bgImg, 0); } // Pass 1: Create root widgets (parentId == -1) for (uint8_t i = 0; i < screen.widgetCount; i++) { const WidgetConfig& cfg = screen.widgets[i]; if (cfg.parentId != -1) continue; auto widget = WidgetFactory::create(cfg); if (widget && cfg.id < 256) { widget->create(parent); widget->applyStyle(); widgets_[cfg.id] = std::move(widget); } } // Pass 2: Create child widgets // Simple 1-level depth support for now. For deeper nesting, we'd need a topological sort or multiple passes. bool madeProgress = true; int remainingPasses = 10; // Prevent infinite loops while (madeProgress && remainingPasses > 0) { madeProgress = false; remainingPasses--; for (uint8_t i = 0; i < screen.widgetCount; i++) { const WidgetConfig& cfg = screen.widgets[i]; // Skip if already created if (cfg.id < 256 && widgets_[cfg.id]) continue; // Skip if it's a root widget (should be created in Pass 1, but if failed/skipped, ignore) if (cfg.parentId == -1) continue; // Check if parent exists if (cfg.parentId >= 0 && cfg.parentId < 256 && widgets_[cfg.parentId]) { // Parent exists! Get its LVGL object lv_obj_t* parentObj = widgets_[cfg.parentId]->getObj(); if (parentObj) { auto widget = WidgetFactory::create(cfg); if (widget && cfg.id < 256) { widget->create(parentObj); widget->applyStyle(); widgets_[cfg.id] = std::move(widget); madeProgress = true; } } } } } } void WidgetManager::onKnxValue(uint16_t groupAddr, float value, TextSource source) { UiEvent event = {}; event.type = UiEventType::KNX_VALUE; event.groupAddr = groupAddr; event.textSource = source; event.value = value; cacheKnxValue(groupAddr, source, value); enqueueUiEvent(event); } void WidgetManager::onKnxValue(uint16_t groupAddr, float value) { onKnxValue(groupAddr, value, TextSource::KNX_DPT_TEMP); } void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) { UiEvent event = {}; event.type = UiEventType::KNX_SWITCH; event.groupAddr = groupAddr; event.state = value; cacheKnxSwitch(groupAddr, value); enqueueUiEvent(event); } void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) { UiEvent event = {}; event.type = UiEventType::KNX_TEXT; event.groupAddr = groupAddr; if (text) { const size_t max_len = sizeof(event.text) - 1; const size_t src_len = strnlen(text, max_len); if (is_valid_utf8(text, src_len)) { memcpy(event.text, text, src_len); event.text[src_len] = '\0'; } else { latin1_to_utf8(text, src_len, event.text, sizeof(event.text)); } } else { event.text[0] = '\0'; } cacheKnxText(groupAddr, event.text); enqueueUiEvent(event); } void WidgetManager::onKnxTime(uint16_t groupAddr, const struct tm& value, KnxTimeType type) { UiEvent event = {}; event.type = UiEventType::KNX_TIME; event.groupAddr = groupAddr; event.timeType = type; event.timeValue = value; cacheKnxTime(groupAddr, type, value); enqueueUiEvent(event); } bool WidgetManager::enqueueUiEvent(const UiEvent& event) { if (!uiQueue_) return false; return xQueueSend(uiQueue_, &event, 0) == pdTRUE; } void WidgetManager::processUiQueue() { if (!uiQueue_) return; if (uxQueueMessagesWaiting(uiQueue_) == 0) return; // Call from loop() which is ALREADY LOCKED by LVGL timer // DO NOT take esp_lv_adapter_lock() here. UiEvent event = {}; static constexpr size_t kMaxEventsPerLoop = 8; size_t processed = 0; while (processed < kMaxEventsPerLoop && xQueueReceive(uiQueue_, &event, 0) == pdTRUE) { switch (event.type) { case UiEventType::KNX_VALUE: applyKnxValue(event.groupAddr, event.value, event.textSource); break; case UiEventType::KNX_SWITCH: applyKnxSwitch(event.groupAddr, event.state); break; case UiEventType::KNX_TEXT: applyKnxText(event.groupAddr, event.text); break; case UiEventType::KNX_TIME: applyKnxTime(event.groupAddr, event.timeValue, event.timeType); break; } processed++; } if (chartRefreshPending_) { refreshChartWidgetsLocked(); chartRefreshPending_ = false; } } void WidgetManager::applyCachedValuesToWidgets() { for (auto& widget : widgets_) { if (!widget) continue; // Primary address uint16_t addr = widget->getKnxAddress(); TextSource source = widget->getTextSource(); if (addr != 0 && source != TextSource::STATIC) { if (source == TextSource::KNX_DPT_TIME || source == TextSource::KNX_DPT_DATE || source == TextSource::KNX_DPT_DATETIME) { KnxTimeType type = KnxTimeType::TIME; if (source == TextSource::KNX_DPT_DATE) { type = KnxTimeType::DATE; } else if (source == TextSource::KNX_DPT_DATETIME) { type = KnxTimeType::DATETIME; } struct tm tmValue = {}; if (getCachedKnxTime(addr, type, &tmValue)) { widget->onKnxTime(tmValue, source); } } else if (source == TextSource::KNX_DPT_SWITCH) { bool state = false; if (getCachedKnxSwitch(addr, &state)) { widget->onKnxSwitch(state); } } else if (source == TextSource::KNX_DPT_TEXT) { char text[MAX_TEXT_LEN] = {}; if (getCachedKnxText(addr, text, sizeof(text))) { widget->onKnxText(text); } } else if (isNumericTextSource(source)) { float value = 0.0f; if (getCachedKnxValue(addr, source, &value)) { widget->onKnxValue(value); } } } // Secondary address (left value) uint16_t addr2 = widget->getKnxAddress2(); TextSource source2 = widget->getTextSource2(); if (addr2 != 0 && source2 != TextSource::STATIC && isNumericTextSource(source2)) { float value = 0.0f; if (getCachedKnxValue(addr2, source2, &value)) { widget->onKnxValue2(value); } } // Tertiary address (right value) uint16_t addr3 = widget->getKnxAddress3(); TextSource source3 = widget->getTextSource3(); if (addr3 != 0 && source3 != TextSource::STATIC && isNumericTextSource(source3)) { float value = 0.0f; if (getCachedKnxValue(addr3, source3, &value)) { widget->onKnxValue3(value); } } } } void WidgetManager::refreshChartWidgetsLocked() { for (auto& widget : widgets_) { if (!widget) continue; if (widget->getType() == WidgetType::CHART) { widget->onHistoryUpdate(); } } } void WidgetManager::refreshChartWidgets() { // Call from loop() which is ALREADY LOCKED by LVGL timer // DO NOT take esp_lv_adapter_lock() here. refreshChartWidgetsLocked(); } void WidgetManager::updateSystemTimeWidgets() { int64_t nowUs = esp_timer_get_time(); if (nowUs - lastSystemTimeUpdateUs_ < 500000) { // Update every 500ms return; } lastSystemTimeUpdateUs_ = nowUs; time_t now; time(&now); struct tm timeinfo; localtime_r(&now, &timeinfo); // Call from loop() which is ALREADY LOCKED by LVGL timer for (auto& widget : widgets_) { if (!widget) continue; TextSource src = widget->getTextSource(); if (src == TextSource::SYSTEM_TIME || src == TextSource::SYSTEM_DATE || src == TextSource::SYSTEM_DATETIME) { widget->onKnxTime(timeinfo, src); } } } void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) { for (auto& widget : widgets_) { if (!widget) continue; // Primary address (bottom value) if (widget->getKnxAddress() == groupAddr && widget->getTextSource() == source) { widget->onKnxValue(value); } // Secondary address (left value) if (widget->getKnxAddress2() == groupAddr && widget->getTextSource2() == source) { widget->onKnxValue2(value); } // Tertiary address (right value) if (widget->getKnxAddress3() == groupAddr && widget->getTextSource3() == source) { widget->onKnxValue3(value); } } if (HistoryStore::instance().updateLatest(groupAddr, source, value)) { chartRefreshPending_ = true; } } void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) { for (auto& widget : widgets_) { if (!widget) continue; if (widget->getKnxAddress() == groupAddr) { widget->onKnxSwitch(value); } // RoomCard sub-button status updates if (widget->getType() == WidgetType::ROOMCARD) { const WidgetConfig& cfg = widget->getConfig(); for (uint8_t i = 0; i < cfg.subButtonCount && i < MAX_SUBBUTTONS; ++i) { if (cfg.subButtons[i].enabled && cfg.subButtons[i].knxAddrRead == groupAddr) { static_cast(widget.get())->onSubButtonStatus(i, value); } } } } if (config_->knxNightModeAddress != 0 && groupAddr == config_->knxNightModeAddress) { nightMode_ = value; } } void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) { for (auto& widget : widgets_) { if (widget && widget->getKnxAddress() == groupAddr) { widget->onKnxText(text); } } } void WidgetManager::applyKnxTime(uint16_t groupAddr, const struct tm& value, KnxTimeType type) { // Simplified system time synchronization bool isGlobalTime = false; if (type == KnxTimeType::TIME && config_->knxTimeAddress != 0 && groupAddr == config_->knxTimeAddress) isGlobalTime = true; if (type == KnxTimeType::DATE && config_->knxDateAddress != 0 && groupAddr == config_->knxDateAddress) isGlobalTime = true; if (type == KnxTimeType::DATETIME && config_->knxDateTimeAddress != 0 && groupAddr == config_->knxDateTimeAddress) isGlobalTime = true; if (isGlobalTime) { time_t now; time(&now); struct tm t_new; localtime_r(&now, &t_new); if (type == KnxTimeType::TIME) { t_new.tm_hour = value.tm_hour; t_new.tm_min = value.tm_min; t_new.tm_sec = value.tm_sec; } else if (type == KnxTimeType::DATE) { t_new.tm_year = value.tm_year; if (t_new.tm_year < 1900) t_new.tm_year += 1900; t_new.tm_mon = value.tm_mon; t_new.tm_mday = value.tm_mday; } else if (type == KnxTimeType::DATETIME) { t_new = value; if (t_new.tm_year < 1900) t_new.tm_year += 1900; } // Final normalization for mktime (expects year - 1900) if (t_new.tm_year >= 1900) t_new.tm_year -= 1900; time_t t = mktime(&t_new); if (t != -1) { struct timeval tv = { .tv_sec = t, .tv_usec = 0 }; settimeofday(&tv, NULL); } } TextSource source = TextSource::STATIC; if (type == KnxTimeType::TIME) source = TextSource::KNX_DPT_TIME; else if (type == KnxTimeType::DATE) source = TextSource::KNX_DPT_DATE; else if (type == KnxTimeType::DATETIME) source = TextSource::KNX_DPT_DATETIME; if (source != TextSource::STATIC) { for (auto& widget : widgets_) { if (widget && widget->getKnxAddress() == groupAddr && widget->getTextSource() == source) { widget->onKnxTime(value, source); } } } chartRefreshPending_ = true; } void WidgetManager::cacheKnxValue(uint16_t groupAddr, TextSource source, float value) { if (groupAddr == 0) return; portENTER_CRITICAL(&knxCacheMux_); size_t freeIndex = KNX_CACHE_SIZE; for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) { auto& entry = knxNumericCache_[i]; if (entry.valid) { if (entry.groupAddr == groupAddr && entry.source == source) { entry.value = value; portEXIT_CRITICAL(&knxCacheMux_); return; } } else if (freeIndex == KNX_CACHE_SIZE) { freeIndex = i; } } size_t index = freeIndex; if (index == KNX_CACHE_SIZE) { index = knxNumericCacheNext_; knxNumericCacheNext_ = (knxNumericCacheNext_ + 1) % KNX_CACHE_SIZE; } auto& entry = knxNumericCache_[index]; entry.groupAddr = groupAddr; entry.source = source; entry.value = value; entry.valid = true; portEXIT_CRITICAL(&knxCacheMux_); } void WidgetManager::cacheKnxSwitch(uint16_t groupAddr, bool value) { if (groupAddr == 0) return; portENTER_CRITICAL(&knxCacheMux_); size_t freeIndex = KNX_CACHE_SIZE; for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) { auto& entry = knxSwitchCache_[i]; if (entry.valid) { if (entry.groupAddr == groupAddr) { entry.value = value; portEXIT_CRITICAL(&knxCacheMux_); return; } } else if (freeIndex == KNX_CACHE_SIZE) { freeIndex = i; } } size_t index = freeIndex; if (index == KNX_CACHE_SIZE) { index = knxSwitchCacheNext_; knxSwitchCacheNext_ = (knxSwitchCacheNext_ + 1) % KNX_CACHE_SIZE; } auto& entry = knxSwitchCache_[index]; entry.groupAddr = groupAddr; entry.value = value; entry.valid = true; portEXIT_CRITICAL(&knxCacheMux_); } void WidgetManager::cacheKnxText(uint16_t groupAddr, const char* text) { if (groupAddr == 0) return; portENTER_CRITICAL(&knxCacheMux_); size_t freeIndex = KNX_CACHE_SIZE; for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) { auto& entry = knxTextCache_[i]; if (entry.valid) { if (entry.groupAddr == groupAddr) { if (text) { strncpy(entry.text, text, MAX_TEXT_LEN - 1); entry.text[MAX_TEXT_LEN - 1] = '\0'; } else { entry.text[0] = '\0'; } portEXIT_CRITICAL(&knxCacheMux_); return; } } else if (freeIndex == KNX_CACHE_SIZE) { freeIndex = i; } } size_t index = freeIndex; if (index == KNX_CACHE_SIZE) { index = knxTextCacheNext_; knxTextCacheNext_ = (knxTextCacheNext_ + 1) % KNX_CACHE_SIZE; } auto& entry = knxTextCache_[index]; entry.groupAddr = groupAddr; if (text) { strncpy(entry.text, text, MAX_TEXT_LEN - 1); entry.text[MAX_TEXT_LEN - 1] = '\0'; } else { entry.text[0] = '\0'; } entry.valid = true; portEXIT_CRITICAL(&knxCacheMux_); } void WidgetManager::cacheKnxTime(uint16_t groupAddr, KnxTimeType type, const struct tm& value) { if (groupAddr == 0) return; portENTER_CRITICAL(&knxCacheMux_); size_t freeIndex = KNX_CACHE_SIZE; for (size_t i = 0; i < KNX_CACHE_SIZE; ++i) { auto& entry = knxTimeCache_[i]; if (entry.valid) { if (entry.groupAddr == groupAddr && entry.type == type) { entry.value = value; portEXIT_CRITICAL(&knxCacheMux_); return; } } else if (freeIndex == KNX_CACHE_SIZE) { freeIndex = i; } } size_t index = freeIndex; if (index == KNX_CACHE_SIZE) { index = knxTimeCacheNext_; knxTimeCacheNext_ = (knxTimeCacheNext_ + 1) % KNX_CACHE_SIZE; } auto& entry = knxTimeCache_[index]; entry.groupAddr = groupAddr; entry.type = type; entry.value = value; entry.valid = true; portEXIT_CRITICAL(&knxCacheMux_); } bool WidgetManager::getCachedKnxValue(uint16_t groupAddr, TextSource source, float* out) const { if (groupAddr == 0 || out == nullptr) return false; bool found = false; portENTER_CRITICAL(&knxCacheMux_); for (const auto& entry : knxNumericCache_) { if (entry.valid && entry.groupAddr == groupAddr && entry.source == source) { *out = entry.value; found = true; break; } } portEXIT_CRITICAL(&knxCacheMux_); return found; } bool WidgetManager::getCachedKnxSwitch(uint16_t groupAddr, bool* out) const { if (groupAddr == 0 || out == nullptr) return false; bool found = false; portENTER_CRITICAL(&knxCacheMux_); for (const auto& entry : knxSwitchCache_) { if (entry.valid && entry.groupAddr == groupAddr) { *out = entry.value; found = true; break; } } portEXIT_CRITICAL(&knxCacheMux_); return found; } bool WidgetManager::getCachedKnxText(uint16_t groupAddr, char* out, size_t outSize) const { if (groupAddr == 0 || out == nullptr || outSize == 0) return false; bool found = false; portENTER_CRITICAL(&knxCacheMux_); for (const auto& entry : knxTextCache_) { if (entry.valid && entry.groupAddr == groupAddr) { strncpy(out, entry.text, outSize - 1); out[outSize - 1] = '\0'; found = true; break; } } portEXIT_CRITICAL(&knxCacheMux_); return found; } bool WidgetManager::getCachedKnxTime(uint16_t groupAddr, KnxTimeType type, struct tm* out) const { if (groupAddr == 0 || out == nullptr) return false; bool found = false; portENTER_CRITICAL(&knxCacheMux_); for (const auto& entry : knxTimeCache_) { if (entry.valid && entry.groupAddr == groupAddr && entry.type == type) { *out = entry.value; found = true; break; } } portEXIT_CRITICAL(&knxCacheMux_); return found; } bool WidgetManager::isNumericTextSource(TextSource source) { return source == TextSource::KNX_DPT_TEMP || source == TextSource::KNX_DPT_PERCENT || source == TextSource::KNX_DPT_POWER || source == TextSource::KNX_DPT_ENERGY || source == TextSource::KNX_DPT_DECIMALFACTOR; } // 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); } static Color defaultChartColor(size_t index) { static const Color kChartColors[CHART_MAX_SERIES] = { {239, 99, 81}, {125, 211, 176}, {94, 162, 239} }; if (index >= CHART_MAX_SERIES) return kChartColors[0]; return kChartColors[index]; } 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); cJSON* knx = cJSON_AddObjectToObject(root, "knx"); cJSON_AddNumberToObject(knx, "time", config_->knxTimeAddress); cJSON_AddNumberToObject(knx, "date", config_->knxDateAddress); cJSON_AddNumberToObject(knx, "dateTime", config_->knxDateTimeAddress); cJSON_AddNumberToObject(knx, "night", config_->knxNightModeAddress); 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); // Background image if (screen.bgImagePath[0] != '\0') { cJSON_AddStringToObject(screenJson, "bgImage", screen.bgImagePath); cJSON_AddNumberToObject(screenJson, "bgImageMode", static_cast(screen.bgImageMode)); } // 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]; 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); cJSON_AddNumberToObject(widget, "textAlign", w.textAlign); cJSON_AddBoolToObject(widget, "isContainer", w.isContainer); char textColorStr[8]; snprintf(textColorStr, sizeof(textColorStr), "#%02X%02X%02X", w.textColor.r, w.textColor.g, w.textColor.b); cJSON_AddStringToObject(widget, "textColor", textColorStr); 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); 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); // Icon properties cJSON_AddNumberToObject(widget, "iconCodepoint", w.iconCodepoint); cJSON_AddNumberToObject(widget, "iconPosition", w.iconPosition); cJSON_AddNumberToObject(widget, "iconSize", w.iconSize); cJSON_AddNumberToObject(widget, "iconGap", w.iconGap); cJSON_AddNumberToObject(widget, "parentId", w.parentId); // Secondary KNX address (left value) if (w.knxAddress2 > 0) { cJSON_AddNumberToObject(widget, "knxAddr2", w.knxAddress2); cJSON_AddNumberToObject(widget, "textSrc2", static_cast(w.textSource2)); cJSON_AddStringToObject(widget, "text2", w.text2); } // Tertiary KNX address (right value) if (w.knxAddress3 > 0) { cJSON_AddNumberToObject(widget, "knxAddr3", w.knxAddress3); cJSON_AddNumberToObject(widget, "textSrc3", static_cast(w.textSource3)); cJSON_AddStringToObject(widget, "text3", w.text3); } // Conditions if (w.conditionCount > 0) { cJSON* conditions = cJSON_AddArrayToObject(widget, "conditions"); for (uint8_t ci = 0; ci < w.conditionCount && ci < MAX_CONDITIONS; ++ci) { const StyleCondition& cond = w.conditions[ci]; if (!cond.enabled) continue; cJSON* condJson = cJSON_CreateObject(); // Source const char* sourceStr = "primary"; if (cond.source == ConditionSource::SECONDARY) sourceStr = "secondary"; else if (cond.source == ConditionSource::TERTIARY) sourceStr = "tertiary"; cJSON_AddStringToObject(condJson, "source", sourceStr); cJSON_AddNumberToObject(condJson, "threshold", cond.threshold); // Operator const char* opStr = "lt"; switch (cond.op) { case ConditionOp::LESS: opStr = "lt"; break; case ConditionOp::LESS_EQUAL: opStr = "lte"; break; case ConditionOp::EQUAL: opStr = "eq"; break; case ConditionOp::GREATER_EQUAL: opStr = "gte"; break; case ConditionOp::GREATER: opStr = "gt"; break; case ConditionOp::NOT_EQUAL: opStr = "neq"; break; } cJSON_AddStringToObject(condJson, "op", opStr); cJSON_AddNumberToObject(condJson, "priority", cond.priority); // Style if (cond.style.iconCodepoint != 0) { cJSON_AddNumberToObject(condJson, "icon", cond.style.iconCodepoint); } if (cond.style.flags & ConditionStyle::FLAG_USE_TEXT_COLOR) { char colorStr[8]; snprintf(colorStr, sizeof(colorStr), "#%02X%02X%02X", cond.style.textColor.r, cond.style.textColor.g, cond.style.textColor.b); cJSON_AddStringToObject(condJson, "textColor", colorStr); } if (cond.style.flags & ConditionStyle::FLAG_USE_BG_COLOR) { char colorStr[8]; snprintf(colorStr, sizeof(colorStr), "#%02X%02X%02X", cond.style.bgColor.r, cond.style.bgColor.g, cond.style.bgColor.b); cJSON_AddStringToObject(condJson, "bgColor", colorStr); } if (cond.style.flags & ConditionStyle::FLAG_HIDE) { cJSON_AddBoolToObject(condJson, "hide", true); } cJSON_AddItemToArray(conditions, condJson); } } if (w.type == WidgetType::CHART) { cJSON* chart = cJSON_AddObjectToObject(widget, "chart"); cJSON_AddNumberToObject(chart, "period", w.chartPeriod); cJSON* series = cJSON_AddArrayToObject(chart, "series"); uint8_t seriesCount = w.chartSeriesCount; if (seriesCount > CHART_MAX_SERIES) seriesCount = CHART_MAX_SERIES; for (uint8_t si = 0; si < seriesCount; ++si) { cJSON* s = cJSON_CreateObject(); cJSON_AddNumberToObject(s, "knxAddr", w.chartKnxAddress[si]); cJSON_AddNumberToObject(s, "textSrc", static_cast(w.chartTextSource[si])); char colorStr[8]; snprintf(colorStr, sizeof(colorStr), "#%02X%02X%02X", w.chartSeriesColor[si].r, w.chartSeriesColor[si].g, w.chartSeriesColor[si].b); cJSON_AddStringToObject(s, "color", colorStr); cJSON_AddItemToArray(series, s); } } // RoomCard sub-buttons if (w.type == WidgetType::ROOMCARD) { cJSON_AddNumberToObject(widget, "subButtonSize", w.subButtonSize); cJSON_AddNumberToObject(widget, "subButtonDistance", w.subButtonDistance); } if (w.type == WidgetType::ROOMCARD && w.subButtonCount > 0) { cJSON* subButtons = cJSON_AddArrayToObject(widget, "subButtons"); for (uint8_t si = 0; si < w.subButtonCount && si < MAX_SUBBUTTONS; ++si) { const SubButtonConfig& sb = w.subButtons[si]; if (!sb.enabled) continue; cJSON* sbJson = cJSON_CreateObject(); cJSON_AddNumberToObject(sbJson, "pos", static_cast(sb.position)); cJSON_AddNumberToObject(sbJson, "icon", sb.iconCodepoint); cJSON_AddNumberToObject(sbJson, "knxRead", sb.knxAddrRead); cJSON_AddNumberToObject(sbJson, "knxWrite", sb.knxAddrWrite); cJSON_AddNumberToObject(sbJson, "action", static_cast(sb.action)); cJSON_AddNumberToObject(sbJson, "target", sb.targetScreen); char colorOnStr[8], colorOffStr[8]; snprintf(colorOnStr, sizeof(colorOnStr), "#%02X%02X%02X", sb.colorOn.r, sb.colorOn.g, sb.colorOn.b); snprintf(colorOffStr, sizeof(colorOffStr), "#%02X%02X%02X", sb.colorOff.r, sb.colorOff.g, sb.colorOff.b); cJSON_AddStringToObject(sbJson, "colorOn", colorOnStr); cJSON_AddStringToObject(sbJson, "colorOff", colorOffStr); cJSON_AddItemToArray(subButtons, sbJson); } } cJSON_AddItemToArray(widgets, widget); } cJSON_AddItemToArray(screens, screenJson); } 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); } 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; w.textAlign = static_cast(TextAlign::CENTER); w.isContainer = false; w.chartPeriod = static_cast(ChartPeriod::HOUR_1); w.chartSeriesCount = 1; for (size_t i = 0; i < CHART_MAX_SERIES; ++i) { w.chartKnxAddress[i] = 0; w.chartTextSource[i] = TextSource::KNX_DPT_TEMP; w.chartSeriesColor[i] = defaultChartColor(i); } 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; cJSON* textAlign = cJSON_GetObjectItem(widget, "textAlign"); if (cJSON_IsNumber(textAlign)) w.textAlign = textAlign->valueint; cJSON* isContainer = cJSON_GetObjectItem(widget, "isContainer"); if (cJSON_IsBool(isContainer)) w.isContainer = cJSON_IsTrue(isContainer); 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; 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; // Icon properties cJSON* iconCodepoint = cJSON_GetObjectItem(widget, "iconCodepoint"); if (cJSON_IsNumber(iconCodepoint)) w.iconCodepoint = static_cast(iconCodepoint->valuedouble); cJSON* iconPosition = cJSON_GetObjectItem(widget, "iconPosition"); if (cJSON_IsNumber(iconPosition)) w.iconPosition = iconPosition->valueint; cJSON* iconSize = cJSON_GetObjectItem(widget, "iconSize"); if (cJSON_IsNumber(iconSize)) w.iconSize = iconSize->valueint; cJSON* iconGap = cJSON_GetObjectItem(widget, "iconGap"); if (cJSON_IsNumber(iconGap)) w.iconGap = iconGap->valueint; cJSON* parentId = cJSON_GetObjectItem(widget, "parentId"); if (cJSON_IsNumber(parentId)) { w.parentId = static_cast(parentId->valueint); } else { w.parentId = -1; // Default to root } // Secondary KNX address (left value) cJSON* knxAddr2 = cJSON_GetObjectItem(widget, "knxAddr2"); if (cJSON_IsNumber(knxAddr2)) w.knxAddress2 = knxAddr2->valueint; cJSON* textSrc2 = cJSON_GetObjectItem(widget, "textSrc2"); if (cJSON_IsNumber(textSrc2)) w.textSource2 = static_cast(textSrc2->valueint); cJSON* text2 = cJSON_GetObjectItem(widget, "text2"); if (cJSON_IsString(text2)) { strncpy(w.text2, text2->valuestring, MAX_FORMAT_LEN - 1); w.text2[MAX_FORMAT_LEN - 1] = '\0'; } // Tertiary KNX address (right value) cJSON* knxAddr3 = cJSON_GetObjectItem(widget, "knxAddr3"); if (cJSON_IsNumber(knxAddr3)) w.knxAddress3 = knxAddr3->valueint; cJSON* textSrc3 = cJSON_GetObjectItem(widget, "textSrc3"); if (cJSON_IsNumber(textSrc3)) w.textSource3 = static_cast(textSrc3->valueint); cJSON* text3 = cJSON_GetObjectItem(widget, "text3"); if (cJSON_IsString(text3)) { strncpy(w.text3, text3->valuestring, MAX_FORMAT_LEN - 1); w.text3[MAX_FORMAT_LEN - 1] = '\0'; } // Conditions cJSON* conditions = cJSON_GetObjectItem(widget, "conditions"); if (cJSON_IsArray(conditions)) { uint8_t condIdx = 0; cJSON* condItem = nullptr; cJSON_ArrayForEach(condItem, conditions) { if (condIdx >= MAX_CONDITIONS) break; StyleCondition& cond = w.conditions[condIdx]; memset(&cond, 0, sizeof(cond)); cond.enabled = true; // Source cJSON* source = cJSON_GetObjectItem(condItem, "source"); if (cJSON_IsString(source)) { if (strcmp(source->valuestring, "secondary") == 0) { cond.source = ConditionSource::SECONDARY; } else if (strcmp(source->valuestring, "tertiary") == 0) { cond.source = ConditionSource::TERTIARY; } else { cond.source = ConditionSource::PRIMARY; } } // Threshold cJSON* threshold = cJSON_GetObjectItem(condItem, "threshold"); if (cJSON_IsNumber(threshold)) cond.threshold = static_cast(threshold->valuedouble); // Operator cJSON* op = cJSON_GetObjectItem(condItem, "op"); if (cJSON_IsString(op)) { if (strcmp(op->valuestring, "lt") == 0) cond.op = ConditionOp::LESS; else if (strcmp(op->valuestring, "lte") == 0) cond.op = ConditionOp::LESS_EQUAL; else if (strcmp(op->valuestring, "eq") == 0) cond.op = ConditionOp::EQUAL; else if (strcmp(op->valuestring, "gte") == 0) cond.op = ConditionOp::GREATER_EQUAL; else if (strcmp(op->valuestring, "gt") == 0) cond.op = ConditionOp::GREATER; else if (strcmp(op->valuestring, "neq") == 0) cond.op = ConditionOp::NOT_EQUAL; } // Priority cJSON* priority = cJSON_GetObjectItem(condItem, "priority"); if (cJSON_IsNumber(priority)) cond.priority = priority->valueint; // Icon cJSON* icon = cJSON_GetObjectItem(condItem, "icon"); if (cJSON_IsNumber(icon)) cond.style.iconCodepoint = static_cast(icon->valuedouble); // Text color cJSON* textColor = cJSON_GetObjectItem(condItem, "textColor"); if (cJSON_IsString(textColor)) { cond.style.textColor = Color::fromHex(parseHexColor(textColor->valuestring)); cond.style.flags |= ConditionStyle::FLAG_USE_TEXT_COLOR; } // Background color cJSON* bgColor = cJSON_GetObjectItem(condItem, "bgColor"); if (cJSON_IsString(bgColor)) { cond.style.bgColor = Color::fromHex(parseHexColor(bgColor->valuestring)); cond.style.flags |= ConditionStyle::FLAG_USE_BG_COLOR; } // Hide cJSON* hide = cJSON_GetObjectItem(condItem, "hide"); if (cJSON_IsBool(hide) && cJSON_IsTrue(hide)) { cond.style.flags |= ConditionStyle::FLAG_HIDE; } condIdx++; } w.conditionCount = condIdx; } cJSON* chart = cJSON_GetObjectItem(widget, "chart"); if (cJSON_IsObject(chart)) { cJSON* period = cJSON_GetObjectItem(chart, "period"); if (cJSON_IsNumber(period)) { int periodVal = period->valueint; if (periodVal < 0) periodVal = 0; if (periodVal > static_cast(ChartPeriod::MONTH_1)) { periodVal = static_cast(ChartPeriod::MONTH_1); } w.chartPeriod = static_cast(periodVal); } cJSON* series = cJSON_GetObjectItem(chart, "series"); if (cJSON_IsArray(series)) { uint8_t idx = 0; cJSON* item = nullptr; cJSON_ArrayForEach(item, series) { if (idx >= CHART_MAX_SERIES) break; cJSON* sAddr = cJSON_GetObjectItem(item, "knxAddr"); if (cJSON_IsNumber(sAddr)) { w.chartKnxAddress[idx] = sAddr->valueint; } cJSON* sSrc = cJSON_GetObjectItem(item, "textSrc"); if (cJSON_IsNumber(sSrc)) { TextSource src = static_cast(sSrc->valueint); if (isNumericTextSource(src)) { w.chartTextSource[idx] = src; } } cJSON* sColor = cJSON_GetObjectItem(item, "color"); if (cJSON_IsString(sColor)) { w.chartSeriesColor[idx] = Color::fromHex(parseHexColor(sColor->valuestring)); } idx++; } if (idx > 0) { w.chartSeriesCount = idx; } } } // RoomCard sub-button size and distance cJSON* subButtonSize = cJSON_GetObjectItem(widget, "subButtonSize"); if (cJSON_IsNumber(subButtonSize)) { w.subButtonSize = subButtonSize->valueint; } else { w.subButtonSize = 40; // Default } cJSON* subButtonDistance = cJSON_GetObjectItem(widget, "subButtonDistance"); if (cJSON_IsNumber(subButtonDistance)) { w.subButtonDistance = subButtonDistance->valueint; } else { w.subButtonDistance = 80; // Default 80px } // RoomCard sub-buttons cJSON* subButtons = cJSON_GetObjectItem(widget, "subButtons"); if (cJSON_IsArray(subButtons)) { uint8_t sbIdx = 0; cJSON* sbItem = nullptr; cJSON_ArrayForEach(sbItem, subButtons) { if (sbIdx >= MAX_SUBBUTTONS) break; SubButtonConfig& sb = w.subButtons[sbIdx]; memset(&sb, 0, sizeof(sb)); sb.enabled = true; cJSON* pos = cJSON_GetObjectItem(sbItem, "pos"); if (cJSON_IsNumber(pos)) { sb.position = static_cast(pos->valueint); } cJSON* icon = cJSON_GetObjectItem(sbItem, "icon"); if (cJSON_IsNumber(icon)) { sb.iconCodepoint = static_cast(icon->valuedouble); } cJSON* knxRead = cJSON_GetObjectItem(sbItem, "knxRead"); if (cJSON_IsNumber(knxRead)) { sb.knxAddrRead = knxRead->valueint; } cJSON* knxWrite = cJSON_GetObjectItem(sbItem, "knxWrite"); if (cJSON_IsNumber(knxWrite)) { sb.knxAddrWrite = knxWrite->valueint; } cJSON* action = cJSON_GetObjectItem(sbItem, "action"); if (cJSON_IsNumber(action)) { sb.action = static_cast(action->valueint); } cJSON* target = cJSON_GetObjectItem(sbItem, "target"); if (cJSON_IsNumber(target)) { sb.targetScreen = target->valueint; } cJSON* colorOn = cJSON_GetObjectItem(sbItem, "colorOn"); if (cJSON_IsString(colorOn)) { sb.colorOn = Color::fromHex(parseHexColor(colorOn->valuestring)); } cJSON* colorOff = cJSON_GetObjectItem(sbItem, "colorOff"); if (cJSON_IsString(colorOff)) { sb.colorOff = Color::fromHex(parseHexColor(colorOff->valuestring)); } sbIdx++; } w.subButtonCount = sbIdx; } 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)); } // Parse background image cJSON* bgImage = cJSON_GetObjectItem(screenJson, "bgImage"); if (cJSON_IsString(bgImage)) { strncpy(screen.bgImagePath, bgImage->valuestring, MAX_BG_IMAGE_PATH_LEN - 1); screen.bgImagePath[MAX_BG_IMAGE_PATH_LEN - 1] = '\0'; } else { screen.bgImagePath[0] = '\0'; } cJSON* bgImageMode = cJSON_GetObjectItem(screenJson, "bgImageMode"); if (cJSON_IsNumber(bgImageMode)) { screen.bgImageMode = static_cast(bgImageMode->valueint); } else { screen.bgImageMode = BgImageMode::STRETCH; } // 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; } ensureButtonLabels(screen); 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; } ensureButtonLabels(screen); } 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); } } cJSON* knx = cJSON_GetObjectItem(root, "knx"); if (cJSON_IsObject(knx)) { cJSON* timeAddr = cJSON_GetObjectItem(knx, "time"); if (cJSON_IsNumber(timeAddr)) newConfig->knxTimeAddress = timeAddr->valueint; cJSON* dateAddr = cJSON_GetObjectItem(knx, "date"); if (cJSON_IsNumber(dateAddr)) newConfig->knxDateAddress = dateAddr->valueint; cJSON* dateTimeAddr = cJSON_GetObjectItem(knx, "dateTime"); if (cJSON_IsNumber(dateTimeAddr)) newConfig->knxDateTimeAddress = dateTimeAddr->valueint; cJSON* nightAddr = cJSON_GetObjectItem(knx, "night"); if (cJSON_IsNumber(nightAddr)) newConfig->knxNightModeAddress = nightAddr->valueint; } 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; }