This commit is contained in:
Thomas Peterson 2026-01-29 10:07:01 +01:00
parent 8e90872c75
commit d24507263f
6 changed files with 277 additions and 5 deletions

View File

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

View File

@ -5,6 +5,7 @@
#include "lvgl.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/portmacro.h"
#include <array>
#include <memory>
@ -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<KnxNumericCacheEntry, KNX_CACHE_SIZE> knxNumericCache_ = {};
std::array<KnxSwitchCacheEntry, KNX_CACHE_SIZE> knxSwitchCache_ = {};
std::array<KnxTextCacheEntry, KNX_CACHE_SIZE> knxTextCache_ = {};
size_t knxNumericCacheNext_ = 0;
size_t knxSwitchCacheNext_ = 0;
size_t knxTextCacheNext_ = 0;
mutable portMUX_TYPE knxCacheMux_ = {};
};

View File

@ -1,6 +1,7 @@
#include "LabelWidget.hpp"
#include "../Fonts.hpp"
#include <cstdio>
#include <cstring>
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<char>(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);
}

View File

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

View File

@ -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<char>(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) {

View File

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