#include "WidgetManager.hpp" #include "widgets/WidgetFactory.hpp" #include "SdCard.hpp" #include "esp_lv_adapter.h" #include "esp_log.h" #include "esp_timer.h" #include "cJSON.h" #include #include #include #include #include #include static const char* TAG = "WidgetMgr"; static constexpr uint8_t SCREEN_ID_NONE = 0xFF; 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() { // 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(); 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() { 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) { 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"); closeModal(); } ESP_LOGI(TAG, "Acquiring LVGL lock..."); if (esp_lv_adapter_lock(-1) != ESP_OK) { ESP_LOGE(TAG, "Failed to acquire LVGL lock!"); return; } ESP_LOGI(TAG, "LVGL lock acquired"); lv_display_t* disp = lv_display_get_default(); bool invEnabled = true; if (disp) { invEnabled = lv_display_is_invalidation_enabled(disp); lv_display_enable_invalidation(disp, false); } // Reset all input devices BEFORE destroying widgets to clear any // pending input state and prevent use-after-free on widget objects ESP_LOGI(TAG, "Resetting input devices..."); lv_indev_t* indev = lv_indev_get_next(nullptr); while (indev) { lv_indev_reset(indev, nullptr); indev = lv_indev_get_next(indev); } ESP_LOGI(TAG, "Input devices reset"); // Now destroy C++ widgets (which deletes LVGL objects) under LVGL lock ESP_LOGI(TAG, "Destroying widgets..."); destroyAllWidgets(); lv_obj_clean(lv_scr_act()); ESP_LOGI(TAG, "Widgets destroyed"); 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, invEnabled); } lv_obj_invalidate(lv_scr_act()); esp_lv_adapter_unlock(); ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId); } void WidgetManager::showModalScreen(const ScreenConfig& screen) { if (modalContainer_) { closeModal(); } if (esp_lv_adapter_lock(-1) != ESP_OK) return; // Reset all input devices BEFORE destroying widgets lv_indev_t* indev = lv_indev_get_next(nullptr); while (indev) { lv_indev_reset(indev, nullptr); indev = lv_indev_get_next(indev); } // Destroy any existing widgets before creating modal widgets 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 < MAX_WIDGETS) { widget->create(modal); widget->applyStyle(); widgets_[cfg.id] = std::move(widget); } } applyCachedValuesToWidgets(); esp_lv_adapter_unlock(); ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight); } void WidgetManager::closeModal() { printf("WM: closeModal Start. Container=%p\n", (void*)modalContainer_); fflush(stdout); if (!modalContainer_) { return; } if (esp_lv_adapter_lock(-1) != ESP_OK) { ESP_LOGE(TAG, "closeModal: Failed to lock LVGL"); return; } // Reset input devices lv_indev_t* indev = lv_indev_get_next(nullptr); while (indev) { lv_indev_reset(indev, nullptr); indev = lv_indev_get_next(indev); } // Destroy widgets first printf("WM: closeModal destroying widgets...\n"); fflush(stdout); destroyAllWidgets(); if (modalDimmer_) { if (lv_obj_is_valid(modalDimmer_)) lv_obj_delete(modalDimmer_); modalDimmer_ = nullptr; } if (lv_obj_is_valid(modalContainer_)) { lv_obj_delete(modalContainer_); } else { printf("WM: Warning: modalContainer_ was invalid!\n"); fflush(stdout); } modalContainer_ = nullptr; modalScreenId_ = SCREEN_ID_NONE; esp_lv_adapter_unlock(); printf("WM: closeModal Complete\n"); fflush(stdout); } void WidgetManager::showScreen(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) { showModalScreen(*screen); return; } previousScreenId_ = activeScreenId_; activeScreenId_ = screen->id; standbyActive_ = false; applyScreen(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::goBack() { 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); closeModal(); printf("WM: Modal closed. Restoring screen %d\n", activeScreenId_); fflush(stdout); // Restore the active screen (which was in background) if (config_.findScreen(activeScreenId_)) { applyScreen(activeScreenId_); } else { ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_); if (config_.findScreen(config_.startScreenId)) { activeScreenId_ = config_.startScreenId; applyScreen(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; applyScreen(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::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() { bool didUiNav = false; if (navPending_) { int64_t now = esp_timer_get_time(); // Increased delay to ensure touch events are fully processed if (now - navRequestUs_ >= NAV_DELAY_US) { navPending_ = false; ESP_LOGI(TAG, "Executing navigation: action=%d target=%d", static_cast(navAction_), navTargetScreen_); if (navAction_ == ButtonAction::JUMP) { showScreen(navTargetScreen_); } else if (navAction_ == ButtonAction::BACK) { goBack(); } didUiNav = true; ESP_LOGI(TAG, "Navigation complete"); } } if (standbyWakePending_) { standbyWakePending_ = false; if (standbyWakeTarget_ != SCREEN_ID_NONE) { activeScreenId_ = standbyWakeTarget_; applyScreen(activeScreenId_); } didUiNav = true; } processUiQueue(); 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); // 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 < MAX_WIDGETS) { 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 (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 < MAX_WIDGETS && 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 < MAX_WIDGETS) { 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); } 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; if (esp_lv_adapter_lock(-1) != ESP_OK) return; 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; } processed++; } esp_lv_adapter_unlock(); } void WidgetManager::applyCachedValuesToWidgets() { for (auto& widget : widgets_) { if (!widget) continue; uint16_t addr = widget->getKnxAddress(); if (addr == 0) continue; TextSource source = widget->getTextSource(); if (source == TextSource::STATIC) continue; if (source == TextSource::KNX_DPT_SWITCH) { bool state = false; if (getCachedKnxSwitch(addr, &state)) { widget->onKnxSwitch(state); } continue; } if (source == TextSource::KNX_DPT_TEXT) { char text[MAX_TEXT_LEN] = {}; if (getCachedKnxText(addr, text, sizeof(text))) { widget->onKnxText(text); } continue; } if (isNumericTextSource(source)) { float value = 0.0f; if (getCachedKnxValue(addr, source, &value)) { widget->onKnxValue(value); } } } } void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) { for (auto& widget : widgets_) { if (widget && widget->getKnxAddress() == groupAddr && widget->getTextSource() == source) { widget->onKnxValue(value); } } } void WidgetManager::applyKnxSwitch(uint16_t groupAddr, bool value) { for (auto& widget : widgets_) { if (widget && widget->getKnxAddress() == groupAddr) { widget->onKnxSwitch(value); } } } void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) { for (auto& widget : widgets_) { if (widget && widget->getKnxAddress() == groupAddr) { widget->onKnxText(text); } } } 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_); } 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::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); } 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* 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); // 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); 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; 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 } 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 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); } } 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; }