From d24507263fa816bdd7a47f1d72aa040936c0f690 Mon Sep 17 00:00:00 2001 From: Thomas Peterson Date: Thu, 29 Jan 2026 10:07:01 +0100 Subject: [PATCH] Backup --- main/WidgetManager.cpp | 210 +++++++++++++++++++++++++++++++ main/WidgetManager.hpp | 38 ++++++ main/widgets/LabelWidget.cpp | 14 ++- main/widgets/PowerLinkWidget.cpp | 8 +- main/widgets/PowerNodeWidget.cpp | 9 +- sdkconfig.defaults | 3 + 6 files changed, 277 insertions(+), 5 deletions(-) diff --git a/main/WidgetManager.cpp b/main/WidgetManager.cpp index 32795ac..3f72223 100644 --- a/main/WidgetManager.cpp +++ b/main/WidgetManager.cpp @@ -132,6 +132,7 @@ WidgetManager& WidgetManager::instance() { 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"); @@ -324,6 +325,13 @@ void WidgetManager::applyScreen(uint8_t screenId) { } 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..."); @@ -337,6 +345,7 @@ void WidgetManager::applyScreen(uint8_t screenId) { // 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)...", @@ -344,6 +353,12 @@ void WidgetManager::applyScreen(uint8_t screenId) { 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); @@ -448,6 +463,8 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) { } } + applyCachedValuesToWidgets(); + esp_lv_adapter_unlock(); ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight); } @@ -751,6 +768,7 @@ void WidgetManager::onKnxValue(uint16_t groupAddr, float value, TextSource sourc event.groupAddr = groupAddr; event.textSource = source; event.value = value; + cacheKnxValue(groupAddr, source, value); enqueueUiEvent(event); } @@ -763,6 +781,7 @@ void WidgetManager::onKnxSwitch(uint16_t groupAddr, bool value) { event.type = UiEventType::KNX_SWITCH; event.groupAddr = groupAddr; event.state = value; + cacheKnxSwitch(groupAddr, value); enqueueUiEvent(event); } @@ -782,6 +801,7 @@ void WidgetManager::onKnxText(uint16_t groupAddr, const char* text) { } else { event.text[0] = '\0'; } + cacheKnxText(groupAddr, event.text); enqueueUiEvent(event); } @@ -818,6 +838,41 @@ void WidgetManager::processUiQueue() { 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 && @@ -843,6 +898,161 @@ void WidgetManager::applyKnxText(uint16_t groupAddr, const char* 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; diff --git a/main/WidgetManager.hpp b/main/WidgetManager.hpp index 2f15bc6..a21f899 100644 --- a/main/WidgetManager.hpp +++ b/main/WidgetManager.hpp @@ -5,6 +5,7 @@ #include "lvgl.h" #include "freertos/FreeRTOS.h" #include "freertos/queue.h" +#include "freertos/portmacro.h" #include #include @@ -76,15 +77,44 @@ private: char text[UI_EVENT_TEXT_LEN]; }; + static constexpr size_t KNX_CACHE_SIZE = MAX_WIDGETS * MAX_SCREENS; + + struct KnxNumericCacheEntry { + uint16_t groupAddr = 0; + TextSource source = TextSource::STATIC; + float value = 0.0f; + bool valid = false; + }; + + struct KnxSwitchCacheEntry { + uint16_t groupAddr = 0; + bool value = false; + bool valid = false; + }; + + struct KnxTextCacheEntry { + uint16_t groupAddr = 0; + char text[MAX_TEXT_LEN] = {}; + bool valid = false; + }; + void loadFromSdCard(); void saveToSdCard(); void destroyAllWidgets(); void createAllWidgets(const ScreenConfig& screen, lv_obj_t* parent); bool enqueueUiEvent(const UiEvent& event); void processUiQueue(); + void applyCachedValuesToWidgets(); void applyKnxValue(uint16_t groupAddr, float value, TextSource source); void applyKnxSwitch(uint16_t groupAddr, bool value); void applyKnxText(uint16_t groupAddr, const char* text); + void cacheKnxValue(uint16_t groupAddr, TextSource source, float value); + void cacheKnxSwitch(uint16_t groupAddr, bool value); + void cacheKnxText(uint16_t groupAddr, const char* text); + bool getCachedKnxValue(uint16_t groupAddr, TextSource source, float* out) const; + bool getCachedKnxSwitch(uint16_t groupAddr, bool* out) const; + bool getCachedKnxText(uint16_t groupAddr, char* out, size_t outSize) const; + static bool isNumericTextSource(TextSource source); void createDefaultConfig(); void applyScreen(uint8_t screenId); @@ -118,4 +148,12 @@ private: lv_obj_t* modalContainer_ = nullptr; lv_obj_t* modalDimmer_ = nullptr; QueueHandle_t uiQueue_ = nullptr; + + std::array knxNumericCache_ = {}; + std::array knxSwitchCache_ = {}; + std::array knxTextCache_ = {}; + size_t knxNumericCacheNext_ = 0; + size_t knxSwitchCacheNext_ = 0; + size_t knxTextCacheNext_ = 0; + mutable portMUX_TYPE knxCacheMux_ = {}; }; diff --git a/main/widgets/LabelWidget.cpp b/main/widgets/LabelWidget.cpp index 2dbb866..cfbbcba 100644 --- a/main/widgets/LabelWidget.cpp +++ b/main/widgets/LabelWidget.cpp @@ -1,6 +1,7 @@ #include "LabelWidget.hpp" #include "../Fonts.hpp" #include +#include LabelWidget::LabelWidget(const WidgetConfig& config) : Widget(config) @@ -22,6 +23,13 @@ static lv_flex_align_t toFlexAlign(uint8_t align) { return LV_FLEX_ALIGN_CENTER; } +static void set_label_text_if_changed(lv_obj_t* label, const char* text) { + if (!label || !text) return; + const char* current = lv_label_get_text(label); + if (current && strcmp(current, text) == 0) return; + lv_label_set_text(label, text); +} + int LabelWidget::encodeUtf8(uint32_t codepoint, char* buf) { if (codepoint < 0x80) { buf[0] = static_cast(codepoint); @@ -203,7 +211,7 @@ void LabelWidget::onKnxValue(float value) { } else { snprintf(buf, sizeof(buf), config_.text, value); } - lv_label_set_text(label, buf); + set_label_text_if_changed(label, buf); } void LabelWidget::onKnxSwitch(bool value) { @@ -211,7 +219,7 @@ void LabelWidget::onKnxSwitch(bool value) { if (label == nullptr) return; if (config_.textSource != TextSource::KNX_DPT_SWITCH) return; - lv_label_set_text(label, value ? "EIN" : "AUS"); + set_label_text_if_changed(label, value ? "EIN" : "AUS"); } void LabelWidget::onKnxText(const char* text) { @@ -219,5 +227,5 @@ void LabelWidget::onKnxText(const char* text) { if (label == nullptr) return; if (config_.textSource != TextSource::KNX_DPT_TEXT) return; - lv_label_set_text(label, text); + set_label_text_if_changed(label, text); } diff --git a/main/widgets/PowerLinkWidget.cpp b/main/widgets/PowerLinkWidget.cpp index 7240861..faca5e9 100644 --- a/main/widgets/PowerLinkWidget.cpp +++ b/main/widgets/PowerLinkWidget.cpp @@ -216,12 +216,18 @@ void PowerLinkWidget::dotAnimExec(void* var, int32_t value) { void PowerLinkWidget::updateAnimation(float speed, bool reverse) { if (dot_ == nullptr || obj_ == nullptr) return; + bool shouldHide = (speed <= 0.01f || pathLen_ <= 0.5f); + bool isHidden = lv_obj_has_flag(dot_, LV_OBJ_FLAG_HIDDEN); + if (std::fabs(speed - speed_) < 0.01f && reverse == reverse_ && isHidden == shouldHide) { + return; + } + speed_ = speed; reverse_ = reverse; lv_anim_del(this, nullptr); - if (speed_ <= 0.01f || pathLen_ <= 0.5f) { + if (shouldHide) { lv_obj_add_flag(dot_, LV_OBJ_FLAG_HIDDEN); return; } diff --git a/main/widgets/PowerNodeWidget.cpp b/main/widgets/PowerNodeWidget.cpp index 42db462..4382a4c 100644 --- a/main/widgets/PowerNodeWidget.cpp +++ b/main/widgets/PowerNodeWidget.cpp @@ -10,6 +10,13 @@ PowerNodeWidget::PowerNodeWidget(const WidgetConfig& config) valueFormat_[0] = '\0'; } +static void set_label_text_if_changed(lv_obj_t* label, const char* text) { + if (!label || !text) return; + const char* current = lv_label_get_text(label); + if (current && strcmp(current, text) == 0) return; + lv_label_set_text(label, text); +} + int PowerNodeWidget::encodeUtf8(uint32_t codepoint, char* buf) { if (codepoint < 0x80) { buf[0] = static_cast(codepoint); @@ -152,7 +159,7 @@ void PowerNodeWidget::applyStyle() { void PowerNodeWidget::updateValueText(const char* text) { if (valueLabel_ == nullptr || text == nullptr) return; - lv_label_set_text(valueLabel_, text); + set_label_text_if_changed(valueLabel_, text); } void PowerNodeWidget::onKnxValue(float value) { diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 47d3c1d..46166f8 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -10,6 +10,9 @@ CONFIG_LV_FONT_MONTSERRAT_48=y # Keep LVGL draw thread stack reasonable to avoid xTaskCreate failures CONFIG_LV_DRAW_THREAD_STACK_SIZE=32768 +# Increase LVGL heap to avoid draw task OOM with dynamic UI +CONFIG_LV_MEM_SIZE_KILOBYTES=128 + # Enable FreeType fonts for extended glyph coverage (e.g. umlauts) CONFIG_LV_USE_FREETYPE=y CONFIG_ESP_LVGL_ADAPTER_ENABLE_FREETYPE=y