This commit is contained in:
Thomas Peterson 2026-01-30 11:15:56 +01:00
parent 87e8deab0e
commit 31471fb6ce
11 changed files with 431 additions and 231 deletions

View File

@ -1,77 +1,190 @@
dependencies: dependencies:
espressif/button: espressif/button:
dependencies: [] component_hash:
fccb18c37f1cfe0797b74a53a44d3f400f5fd01f4993b40052dfb7f401915089
dependencies:
- name: espressif/cmake_utilities
registry_url: https://components.espressif.com
require: private
version: '*'
- name: idf
require: private
version: '>=4.0'
source: source:
path: registry_url: https://components.espressif.com
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__button type: service
type: local
version: 4.1.5 version: 4.1.5
espressif/cmake_utilities: espressif/cmake_utilities:
component_hash: component_hash:
05165f30922b422b4b90c08845e6d449329b97370fbd06309803d8cb539d79e3 351350613ceafba240b761b4ea991e0f231ac7a9f59a9ee901f751bddc0bb18f
dependencies: dependencies:
- name: idf - name: idf
require: private require: private
version: '>=4.1' version: '>=4.1'
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 1.1.1 version: 0.5.3
espressif/eppp_link: espressif/eppp_link:
dependencies: [] component_hash:
9472e6825f4bb71eca2b39cf1bc92659c1ac60bfd7416560ad033a7dd8641b17
dependencies:
- name: espressif/esp_serial_slave_link
registry_url: https://components.espressif.com
require: private
version: ^1.1.0
- name: idf
require: private
version: '>=5.2'
source: source:
path: registry_url: https://components.espressif.com
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__eppp_link type: service
type: local
version: 1.1.4 version: 1.1.4
espressif/esp_hosted: espressif/esp_hosted:
dependencies: [] component_hash:
6f5dc62f18c86b4ac65e1c8cb56fe122de894d80b8c8eaac451f3c0a913b8d76
dependencies:
- name: idf
require: private
version: '>=5.3'
source: source:
path: registry_url: https://components.espressif.com
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_hosted type: service
type: local
version: 2.11.5 version: 2.11.5
espressif/esp_lcd_touch: espressif/esp_lcd_touch:
dependencies: [] component_hash:
3f85a7d95af876f1a6ecca8eb90a81614890d0f03a038390804e5a77e2caf862
dependencies:
- name: idf
require: private
version: '>=4.4.2'
source: source:
path: registry_url: https://components.espressif.com
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lcd_touch type: service
type: local
version: 1.2.1 version: 1.2.1
espressif/esp_lcd_touch_gt911: espressif/esp_lcd_touch_gt911:
dependencies: [] component_hash:
be02e243d18b9a661bc13b0d22c0a5cfa3f708cf04d6eb059772276c8c8a4d76
dependencies:
- name: espressif/esp_lcd_touch
registry_url: https://components.espressif.com
require: public
version: ^1.2.0
- name: idf
require: private
version: '>=4.4.2'
source: source:
path: registry_url: https://components.espressif.com/
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lcd_touch_gt911 type: service
type: local
version: 1.2.0~1 version: 1.2.0~1
espressif/esp_lv_decoder: espressif/esp_lv_decoder:
dependencies: [] component_hash:
0eb7b2bceaf73484ef80f5004337ee31617b2450a3d40621812998a47e7dd349
dependencies:
- name: espressif/esp_new_jpeg
registry_url: https://components.espressif.com
require: private
version: 0.*
- name: espressif/libpng
registry_url: https://components.espressif.com
require: private
version: 1.*
- name: idf
require: private
version: '>=5.3'
- name: lvgl/lvgl
registry_url: https://components.espressif.com
require: private
version: '>=8,<10'
source: source:
path: registry_url: https://components.espressif.com
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lv_decoder type: service
type: local targets:
- esp32
- esp32s2
- esp32s3
- esp32p4
- esp32c2
- esp32c3
- esp32c5
- esp32c6
version: 0.3.2 version: 0.3.2
espressif/esp_lv_fs: espressif/esp_lv_fs:
dependencies: [] component_hash:
66896007884b817df34c964f9a114fff538ee2674e99fee7159162498b93f94b
dependencies:
- name: espressif/cmake_utilities
registry_url: https://components.espressif.com
require: private
version: 0.*
- name: espressif/esp_mmap_assets
registry_url: https://components.espressif.com
require: private
version: '>=1.2'
- name: idf
require: private
version: '>=4.4'
- name: lvgl/lvgl
registry_url: https://components.espressif.com
require: private
version: '>=8,<10'
source: source:
path: registry_url: https://components.espressif.com
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lv_fs type: service
type: local
version: 1.0.1 version: 1.0.1
espressif/esp_lvgl_adapter: espressif/esp_lvgl_adapter:
dependencies: [] component_hash:
4ba6ad754b2533cb582bff81ba672ea7a682f4724a02327e57f96e9c73d330c2
dependencies:
- name: espressif/button
registry_url: https://components.espressif.com
require: public
version: 4.*
- name: espressif/esp_lcd_touch
registry_url: https://components.espressif.com
require: public
version: 1.*
- name: espressif/esp_lv_decoder
registry_url: https://components.espressif.com
require: public
version: 0.*
- name: espressif/esp_lv_fs
registry_url: https://components.espressif.com
require: public
version: 1.*
- name: espressif/freetype
registry_url: https://components.espressif.com
require: public
version: 2.*
- name: espressif/knob
registry_url: https://components.espressif.com
require: public
version: 1.*
- name: idf
require: private
version: '>=5.5'
- name: lvgl/lvgl
registry_url: https://components.espressif.com
require: private
version: '>=8,<10'
source: source:
path: registry_url: https://components.espressif.com/
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lvgl_adapter type: service
type: local
version: 0.3.0 version: 0.3.0
espressif/esp_lvgl_port: espressif/esp_lvgl_port:
dependencies: [] component_hash:
f872401524cb645ee6ff1c9242d44fb4ddcfd4d37d7be8b9ed3f4e85a404efcd
dependencies:
- name: idf
require: private
version: '>=5.1'
- name: lvgl/lvgl
registry_url: https://components.espressif.com
require: public
version: '>=8,<10'
source: source:
path: registry_url: https://components.espressif.com/
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lvgl_port type: service
type: local
version: 2.7.0 version: 2.7.0
espressif/esp_mmap_assets: espressif/esp_mmap_assets:
component_hash: component_hash:
@ -85,7 +198,7 @@ dependencies:
require: private require: private
version: '>=5.0' version: '>=5.0'
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 1.4.0 version: 1.4.0
espressif/esp_new_jpeg: espressif/esp_new_jpeg:
@ -93,7 +206,7 @@ dependencies:
e6af208a875abd0ecfc0213d3751a11b504b463ebde6930f24096047925fa5c1 e6af208a875abd0ecfc0213d3751a11b504b463ebde6930f24096047925fa5c1
dependencies: [] dependencies: []
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
targets: targets:
- esp32 - esp32
@ -113,16 +226,30 @@ dependencies:
require: private require: private
version: '>=5.0' version: '>=5.0'
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 1.1.2 version: 1.1.2
espressif/esp_wifi_remote: espressif/esp_wifi_remote:
dependencies: [] component_hash:
67efd839bd84efb94b149a28044f9918473e7f4facf709bf4121744e4927df09
dependencies:
- name: espressif/esp_hosted
registry_url: https://components.espressif.com
require: private
rules:
- if: target in [esp32h2, esp32p4]
version: '>=2.11'
- name: espressif/wifi_remote_over_eppp
registry_url: https://components.espressif.com
require: private
version: '>=0.1'
- name: idf
require: private
version: '>=5.3'
source: source:
path: registry_url: https://components.espressif.com/
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_wifi_remote type: service
type: local version: 1.3.2
version: 1.3.1
espressif/freetype: espressif/freetype:
component_hash: component_hash:
cd5e2d8458e6e8d73f1120ac474467cabb669d8ea4b25050bf6a348c1e89225e cd5e2d8458e6e8d73f1120ac474467cabb669d8ea4b25050bf6a348c1e89225e
@ -131,15 +258,23 @@ dependencies:
require: private require: private
version: '>=4.4' version: '>=4.4'
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 2.13.3~1 version: 2.13.3~1
espressif/i2c_bus: espressif/i2c_bus:
dependencies: [] component_hash:
4e990dc11734316186b489b362c61d41f23f79d58bc169795cec215e528cba14
dependencies:
- name: espressif/cmake_utilities
registry_url: https://components.espressif.com
require: private
version: '*'
- name: idf
require: private
version: '>=4.0'
source: source:
path: registry_url: https://components.espressif.com
/home/thomas/projekte/test1/knxdisplay/managed_components/espressif__i2c_bus type: service
type: local
version: 1.5.0 version: 1.5.0
espressif/knob: espressif/knob:
component_hash: component_hash:
@ -153,7 +288,7 @@ dependencies:
require: private require: private
version: '>=4.4.1' version: '>=4.4.1'
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 1.0.2 version: 1.0.2
espressif/libpng: espressif/libpng:
@ -168,7 +303,7 @@ dependencies:
require: private require: private
version: ^1.2.13 version: ^1.2.13
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 1.6.54 version: 1.6.54
espressif/wifi_remote_over_eppp: espressif/wifi_remote_over_eppp:
@ -183,7 +318,7 @@ dependencies:
require: private require: private
version: '>=5.3' version: '>=5.3'
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 0.3.0 version: 0.3.0
espressif/zlib: espressif/zlib:
@ -194,7 +329,7 @@ dependencies:
require: private require: private
version: '>=4.4' version: '>=4.4'
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 1.3.1 version: 1.3.1
idf: idf:
@ -206,40 +341,37 @@ dependencies:
17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f 17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f
dependencies: [] dependencies: []
source: source:
registry_url: https://components.espressif.com/ registry_url: https://components.espressif.com
type: service type: service
version: 9.4.0 version: 9.4.0
waveshare/esp_lcd_jd9365_10_1: waveshare/esp_lcd_jd9365_10_1:
dependencies: [] component_hash:
6c1336b93a37df2b5be42c49c4c364d0bacdbf96f053a934f881349457fac679
dependencies:
- name: espressif/cmake_utilities
registry_url: https://components.espressif.com
require: private
version: 0.*
- name: espressif/i2c_bus
registry_url: https://components.espressif.com
require: private
version: ^1.3.0
- name: idf
require: private
version: '>=5.3'
source: source:
path: registry_url: https://components.espressif.com/
/home/thomas/projekte/test1/knxdisplay/managed_components/waveshare__esp_lcd_jd9365_10_1 type: service
type: local targets:
- esp32p4
version: 1.0.4 version: 1.0.4
direct_dependencies: direct_dependencies:
- espressif/button
- espressif/cmake_utilities
- espressif/eppp_link
- espressif/esp_hosted
- espressif/esp_lcd_touch
- espressif/esp_lcd_touch_gt911 - espressif/esp_lcd_touch_gt911
- espressif/esp_lv_decoder
- espressif/esp_lv_fs
- espressif/esp_lvgl_adapter - espressif/esp_lvgl_adapter
- espressif/esp_lvgl_port - espressif/esp_lvgl_port
- espressif/esp_mmap_assets
- espressif/esp_new_jpeg
- espressif/esp_serial_slave_link
- espressif/esp_wifi_remote - espressif/esp_wifi_remote
- espressif/freetype
- espressif/i2c_bus
- espressif/knob
- espressif/libpng
- espressif/wifi_remote_over_eppp
- espressif/zlib
- idf - idf
- lvgl/lvgl
- waveshare/esp_lcd_jd9365_10_1 - waveshare/esp_lcd_jd9365_10_1
manifest_hash: ee0446e4a514a791315863af0d77bdcf353d357a7b8081e3e2b252138a66497b manifest_hash: 2525ec0a57701bb08b15bfef69a11dd2185dbcba5e02784971dcae606254ce64
target: esp32p4 target: esp32p4
version: 2.0.0 version: 2.0.0

View File

@ -44,7 +44,15 @@ HistoryStore& HistoryStore::instance() {
return inst; return inst;
} }
HistoryStore::HistoryStore() = default; HistoryStore::HistoryStore() {
mutex_ = xSemaphoreCreateMutex();
}
HistoryStore::~HistoryStore() {
if (mutex_) {
vSemaphoreDelete(mutex_);
}
}
bool HistoryStore::isNumericSource(TextSource source) { bool HistoryStore::isNumericSource(TextSource source) {
return source == TextSource::KNX_DPT_TEMP || return source == TextSource::KNX_DPT_TEMP ||
@ -97,6 +105,7 @@ const HistoryStore::HistorySeries* HistoryStore::findSeriesByKey(const SeriesKey
} }
void HistoryStore::configureFromConfig(const GuiConfig& config) { void HistoryStore::configureFromConfig(const GuiConfig& config) {
xSemaphoreTake(mutex_, portMAX_DELAY);
std::array<SeriesKey, HISTORY_MAX_SERIES> needed = {}; std::array<SeriesKey, HISTORY_MAX_SERIES> needed = {};
size_t neededCount = 0; size_t neededCount = 0;
@ -191,20 +200,29 @@ void HistoryStore::configureFromConfig(const GuiConfig& config) {
if (changed) { if (changed) {
dirty_ = true; dirty_ = true;
} }
xSemaphoreGive(mutex_);
} }
bool HistoryStore::isTracked(uint16_t groupAddr, TextSource source) const { bool HistoryStore::isTracked(uint16_t groupAddr, TextSource source) const {
return findSeries(groupAddr, source) != nullptr; xSemaphoreTake(mutex_, portMAX_DELAY);
bool tracked = findSeries(groupAddr, source) != nullptr;
xSemaphoreGive(mutex_);
return tracked;
} }
bool HistoryStore::updateLatest(uint16_t groupAddr, TextSource source, float value) { bool HistoryStore::updateLatest(uint16_t groupAddr, TextSource source, float value) {
xSemaphoreTake(mutex_, portMAX_DELAY);
HistorySeries* series = findSeries(groupAddr, source); HistorySeries* series = findSeries(groupAddr, source);
if (!series) return false; if (!series) {
xSemaphoreGive(mutex_);
return false;
}
int64_t nowSec = now(); int64_t nowSec = now();
series->latestValue = value; series->latestValue = value;
series->latestTs = static_cast<int32_t>(nowSec); series->latestTs = static_cast<int32_t>(nowSec);
series->hasLatest = true; series->hasLatest = true;
xSemaphoreGive(mutex_);
return true; return true;
} }
@ -234,12 +252,19 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
outValues[i] = NO_POINT; outValues[i] = NO_POINT;
} }
xSemaphoreTake(mutex_, portMAX_DELAY);
const HistorySeries* series = findSeries(groupAddr, source); const HistorySeries* series = findSeries(groupAddr, source);
if (!series) return false; if (!series) {
xSemaphoreGive(mutex_);
return false;
}
int64_t nowSec = now(); int64_t nowSec = now();
int32_t window = periodSeconds(period); int32_t window = periodSeconds(period);
if (window <= 0) return false; if (window <= 0) {
xSemaphoreGive(mutex_);
return false;
}
int64_t start = nowSec - window; int64_t start = nowSec - window;
@ -264,6 +289,7 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
HistoryPoint latest{series->latestTs, series->latestValue}; HistoryPoint latest{series->latestTs, series->latestValue};
accumulate(latest); accumulate(latest);
} }
xSemaphoreGive(mutex_);
bool hasData = false; bool hasData = false;
for (size_t i = 0; i < outCount; ++i) { for (size_t i = 0; i < outCount; ++i) {
@ -278,6 +304,7 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
} }
bool HistoryStore::tick() { bool HistoryStore::tick() {
xSemaphoreTake(mutex_, portMAX_DELAY);
int64_t nowSec = now(); int64_t nowSec = now();
bool added = false; bool added = false;
@ -302,35 +329,64 @@ bool HistoryStore::tick() {
dirty_ = true; dirty_ = true;
if (timeSynced_) dataEpoch_ = true; if (timeSynced_) dataEpoch_ = true;
} }
xSemaphoreGive(mutex_);
int64_t monoUs = esp_timer_get_time();
if (dirty_ && timeSynced_ && SdCard::instance().isMounted()) { // REMOVED synchronous save from here to avoid blocking UI task
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
saveToSdCard();
lastSaveMonoUs_ = monoUs;
}
}
return added; return added;
} }
void HistoryStore::performAutoSave() {
xSemaphoreTake(mutex_, portMAX_DELAY);
bool shouldSave = false;
int64_t monoUs = esp_timer_get_time();
if (dirty_ && timeSynced_ && SdCard::instance().isMounted()) {
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
shouldSave = true;
lastSaveMonoUs_ = monoUs;
}
}
xSemaphoreGive(mutex_);
if (shouldSave) {
saveToSdCard();
}
}
void HistoryStore::updateTimeOfDay(const struct tm& value) { void HistoryStore::updateTimeOfDay(const struct tm& value) {
xSemaphoreTake(mutex_, portMAX_DELAY);
hour_ = value.tm_hour; hour_ = value.tm_hour;
minute_ = value.tm_min; minute_ = value.tm_min;
second_ = value.tm_sec; second_ = value.tm_sec;
hasTime_ = true; hasTime_ = true;
// applyTimeSync calls internal logic, assume called inside lock or extracted
// But applyTimeSync calls clearAll which touches series.
// So we must be careful about locking.
// Let's unlock before applyTimeSync and re-lock inside?
// No, let's just make applyTimeSync assume lock or lock itself?
// applyTimeSync modifies member vars.
// Refactoring for simplicity: lock around the whole block
// BUT applyTimeSync calls settimeofday which is system call (thread safe?)
// We will inline the logic briefly or handle it carefully.
// For now, assume lock held is OK for short duration.
// We need to implement applyTimeSync correctly with locking.
xSemaphoreGive(mutex_);
applyTimeSync(); applyTimeSync();
} }
void HistoryStore::updateDate(const struct tm& value) { void HistoryStore::updateDate(const struct tm& value) {
xSemaphoreTake(mutex_, portMAX_DELAY);
year_ = value.tm_year; year_ = value.tm_year;
month_ = value.tm_mon; month_ = value.tm_mon;
day_ = value.tm_mday; day_ = value.tm_mday;
hasDate_ = true; hasDate_ = true;
xSemaphoreGive(mutex_);
applyTimeSync(); applyTimeSync();
} }
void HistoryStore::updateDateTime(const struct tm& value) { void HistoryStore::updateDateTime(const struct tm& value) {
xSemaphoreTake(mutex_, portMAX_DELAY);
year_ = value.tm_year; year_ = value.tm_year;
month_ = value.tm_mon; month_ = value.tm_mon;
day_ = value.tm_mday; day_ = value.tm_mday;
@ -339,6 +395,7 @@ void HistoryStore::updateDateTime(const struct tm& value) {
second_ = value.tm_sec; second_ = value.tm_sec;
hasDate_ = true; hasDate_ = true;
hasTime_ = true; hasTime_ = true;
xSemaphoreGive(mutex_);
applyTimeSync(); applyTimeSync();
} }
@ -360,11 +417,16 @@ int64_t HistoryStore::buildEpoch() const {
} }
bool HistoryStore::applyTimeSync() { bool HistoryStore::applyTimeSync() {
if (!hasDate_ || !hasTime_) return false; xSemaphoreTake(mutex_, portMAX_DELAY);
if (!hasDate_ || !hasTime_) {
xSemaphoreGive(mutex_);
return false;
}
int64_t epoch = buildEpoch(); int64_t epoch = buildEpoch();
if (epoch <= 0) { if (epoch <= 0) {
ESP_LOGW(TAG, "Invalid KNX time/date for sync"); ESP_LOGW(TAG, "Invalid KNX time/date for sync");
xSemaphoreGive(mutex_);
return false; return false;
} }
@ -373,9 +435,12 @@ bool HistoryStore::applyTimeSync() {
baseEpoch_ = epoch; baseEpoch_ = epoch;
baseMono_ = esp_timer_get_time() / 1000000LL; baseMono_ = esp_timer_get_time() / 1000000LL;
// Release lock for system call (optional but safer)
xSemaphoreGive(mutex_);
struct timeval tv = {}; struct timeval tv = {};
tv.tv_sec = static_cast<time_t>(epoch); tv.tv_sec = static_cast<time_t>(epoch);
settimeofday(&tv, nullptr); settimeofday(&tv, nullptr);
xSemaphoreTake(mutex_, portMAX_DELAY);
if (!wasSynced) { if (!wasSynced) {
bool hasData = false; bool hasData = false;
@ -394,11 +459,14 @@ bool HistoryStore::applyTimeSync() {
} }
dirty_ = true; dirty_ = true;
xSemaphoreGive(mutex_);
ESP_LOGI(TAG, "Time synced: %ld", static_cast<long>(epoch)); ESP_LOGI(TAG, "Time synced: %ld", static_cast<long>(epoch));
return !wasSynced; return !wasSynced;
} }
void HistoryStore::clearAll() { void HistoryStore::clearAll() {
// Assumes lock is held by caller
for (size_t i = 0; i < series_.size(); ++i) { for (size_t i = 0; i < series_.size(); ++i) {
HistorySeries& series = series_[i]; HistorySeries& series = series_[i];
if (!series.active) continue; if (!series.active) continue;
@ -414,7 +482,13 @@ void HistoryStore::clearAll() {
void HistoryStore::saveToSdCard() { void HistoryStore::saveToSdCard() {
if (!SdCard::instance().isMounted()) return; if (!SdCard::instance().isMounted()) return;
if (!timeSynced_) return;
xSemaphoreTake(mutex_, portMAX_DELAY);
if (!timeSynced_) {
xSemaphoreGive(mutex_);
return;
}
xSemaphoreGive(mutex_); // Release to open file
FILE* f = fopen(HISTORY_FILE, "wb"); FILE* f = fopen(HISTORY_FILE, "wb");
if (!f) { if (!f) {
@ -422,6 +496,7 @@ void HistoryStore::saveToSdCard() {
return; return;
} }
xSemaphoreTake(mutex_, portMAX_DELAY);
HistoryFileHeader header = {}; HistoryFileHeader header = {};
header.magic = HISTORY_MAGIC; header.magic = HISTORY_MAGIC;
header.version = HISTORY_VERSION; header.version = HISTORY_VERSION;
@ -436,6 +511,7 @@ void HistoryStore::saveToSdCard() {
header.coarseCapacity = HISTORY_COARSE_CAP; header.coarseCapacity = HISTORY_COARSE_CAP;
header.fineInterval = HISTORY_FINE_INTERVAL; header.fineInterval = HISTORY_FINE_INTERVAL;
header.coarseInterval = HISTORY_COARSE_INTERVAL; header.coarseInterval = HISTORY_COARSE_INTERVAL;
xSemaphoreGive(mutex_);
if (fwrite(&header, sizeof(header), 1, f) != 1) { if (fwrite(&header, sizeof(header), 1, f) != 1) {
fclose(f); fclose(f);
@ -443,41 +519,59 @@ void HistoryStore::saveToSdCard() {
return; return;
} }
for (size_t i = 0; i < series_.size(); ++i) { // Allocate temp buffer for one series to minimize lock time
const HistorySeries& series = series_[i]; // Size: Header + Fine + Coarse
if (!series.active) continue; size_t seriesDataSize = sizeof(HistoryFileSeriesHeader) +
HistoryFileSeriesHeader sh = {}; sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP);
sh.groupAddr = series.key.addr; uint8_t* tempBuf = (uint8_t*)malloc(seriesDataSize);
sh.source = static_cast<uint8_t>(series.key.source); if (!tempBuf) {
sh.fineCount = static_cast<uint16_t>(series.fine.count); fclose(f);
sh.fineHead = static_cast<uint16_t>(series.fine.head); ESP_LOGE(TAG, "Failed to allocate temp buffer for save");
sh.coarseCount = static_cast<uint16_t>(series.coarse.count); return;
sh.coarseHead = static_cast<uint16_t>(series.coarse.head);
sh.hasLatest = series.hasLatest ? 1 : 0;
sh.latestTs = series.latestTs;
sh.latestValue = series.latestValue;
if (fwrite(&sh, sizeof(sh), 1, f) != 1) {
fclose(f);
ESP_LOGW(TAG, "Failed to write history series header");
return;
}
if (fwrite(series.fine.points.data(), sizeof(HistoryPoint), HISTORY_FINE_CAP, f) != HISTORY_FINE_CAP) {
fclose(f);
ESP_LOGW(TAG, "Failed to write fine history data");
return;
}
if (fwrite(series.coarse.points.data(), sizeof(HistoryPoint), HISTORY_COARSE_CAP, f) != HISTORY_COARSE_CAP) {
fclose(f);
ESP_LOGW(TAG, "Failed to write coarse history data");
return;
}
} }
fclose(f); for (size_t i = 0; i < series_.size(); ++i) {
xSemaphoreTake(mutex_, portMAX_DELAY);
const HistorySeries& series = series_[i];
if (!series.active) {
xSemaphoreGive(mutex_);
continue;
}
// Copy to temp buffer
HistoryFileSeriesHeader* sh = (HistoryFileSeriesHeader*)tempBuf;
sh->groupAddr = series.key.addr;
sh->source = static_cast<uint8_t>(series.key.source);
sh->fineCount = static_cast<uint16_t>(series.fine.count);
sh->fineHead = static_cast<uint16_t>(series.fine.head);
sh->coarseCount = static_cast<uint16_t>(series.coarse.count);
sh->coarseHead = static_cast<uint16_t>(series.coarse.head);
sh->hasLatest = series.hasLatest ? 1 : 0;
sh->latestTs = series.latestTs;
sh->latestValue = series.latestValue;
uint8_t* dataPtr = tempBuf + sizeof(HistoryFileSeriesHeader);
memcpy(dataPtr, series.fine.points.data(), sizeof(HistoryPoint) * HISTORY_FINE_CAP);
dataPtr += sizeof(HistoryPoint) * HISTORY_FINE_CAP;
memcpy(dataPtr, series.coarse.points.data(), sizeof(HistoryPoint) * HISTORY_COARSE_CAP);
xSemaphoreGive(mutex_);
// Write to file (unlocked)
if (fwrite(tempBuf, 1, seriesDataSize, f) != seriesDataSize) {
ESP_LOGW(TAG, "Failed to write series data");
break;
}
}
free(tempBuf);
xSemaphoreTake(mutex_, portMAX_DELAY);
dirty_ = false; dirty_ = false;
dataEpoch_ = true; dataEpoch_ = true;
xSemaphoreGive(mutex_);
fclose(f);
ESP_LOGI(TAG, "History saved (%d series)", static_cast<int>(activeCount)); ESP_LOGI(TAG, "History saved (%d series)", static_cast<int>(activeCount));
} }
@ -508,6 +602,7 @@ void HistoryStore::loadFromSdCard() {
return; return;
} }
xSemaphoreTake(mutex_, portMAX_DELAY);
for (uint16_t i = 0; i < header.seriesCount; ++i) { for (uint16_t i = 0; i < header.seriesCount; ++i) {
HistoryFileSeriesHeader sh = {}; HistoryFileSeriesHeader sh = {};
if (fread(&sh, sizeof(sh), 1, f) != 1) { if (fread(&sh, sizeof(sh), 1, f) != 1) {
@ -534,9 +629,10 @@ void HistoryStore::loadFromSdCard() {
fseek(f, sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP), SEEK_CUR); fseek(f, sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP), SEEK_CUR);
} }
} }
fclose(f);
dirty_ = false; dirty_ = false;
dataEpoch_ = true; dataEpoch_ = true;
xSemaphoreGive(mutex_);
fclose(f);
ESP_LOGI(TAG, "History loaded"); ESP_LOGI(TAG, "History loaded");
} }

View File

@ -1,6 +1,8 @@
#pragma once #pragma once
#include "WidgetConfig.hpp" #include "WidgetConfig.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include <array> #include <array>
#include <climits> #include <climits>
#include <cstdint> #include <cstdint>
@ -14,6 +16,7 @@ public:
bool updateLatest(uint16_t groupAddr, TextSource source, float value); bool updateLatest(uint16_t groupAddr, TextSource source, float value);
bool isTracked(uint16_t groupAddr, TextSource source) const; bool isTracked(uint16_t groupAddr, TextSource source) const;
bool tick(); bool tick();
void performAutoSave();
bool fillChartSeries(uint16_t groupAddr, TextSource source, ChartPeriod period, bool fillChartSeries(uint16_t groupAddr, TextSource source, ChartPeriod period,
int32_t* outValues, size_t outCount) const; int32_t* outValues, size_t outCount) const;
@ -33,6 +36,7 @@ public:
private: private:
HistoryStore(); HistoryStore();
~HistoryStore();
struct SeriesKey { struct SeriesKey {
uint16_t addr = 0; uint16_t addr = 0;
@ -124,4 +128,6 @@ private:
bool dirty_ = false; bool dirty_ = false;
int64_t lastSaveMonoUs_ = 0; int64_t lastSaveMonoUs_ = 0;
bool dataEpoch_ = false; bool dataEpoch_ = false;
mutable SemaphoreHandle_t mutex_ = nullptr;
}; };

View File

@ -243,7 +243,14 @@ void KnxWorker::clearSettings() {
#endif #endif
} }
#include "HistoryStore.hpp"
// ... imports ...
void KnxWorker::loop() { void KnxWorker::loop() {
// Check for auto-save (handled by HistoryStore logic)
HistoryStore::instance().performAutoSave();
#if UART_DEBUG_MODE #if UART_DEBUG_MODE
// Periodically send U_STATE_REQ to test TX direction // Periodically send U_STATE_REQ to test TX direction
uint32_t now = millis(); uint32_t now = millis();

View File

@ -322,6 +322,15 @@ const ScreenConfig* WidgetManager::currentScreen() const {
} }
void WidgetManager::applyScreen(uint8_t screenId) { 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); ESP_LOGI(TAG, "applyScreen(%d) - start", screenId);
ScreenConfig* screen = config_.findScreen(screenId); ScreenConfig* screen = config_.findScreen(screenId);
@ -334,16 +343,9 @@ void WidgetManager::applyScreen(uint8_t screenId) {
if (modalContainer_) { if (modalContainer_) {
ESP_LOGI(TAG, "Closing modal first"); ESP_LOGI(TAG, "Closing modal first");
closeModal(); closeModalLocked();
} }
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(); lv_display_t* disp = lv_display_get_default();
bool invEnabled = true; bool invEnabled = true;
if (disp) { if (disp) {
@ -351,17 +353,15 @@ void WidgetManager::applyScreen(uint8_t screenId) {
lv_display_enable_invalidation(disp, false); lv_display_enable_invalidation(disp, false);
} }
// Reset all input devices BEFORE destroying widgets to clear any // Reset all input devices BEFORE destroying widgets
// pending input state and prevent use-after-free on widget objects
ESP_LOGI(TAG, "Resetting input devices..."); ESP_LOGI(TAG, "Resetting input devices...");
lv_indev_t* indev = lv_indev_get_next(nullptr); lv_indev_t* indev = lv_indev_get_next(nullptr);
while (indev) { while (indev) {
lv_indev_reset(indev, nullptr); lv_indev_reset(indev, nullptr);
indev = lv_indev_get_next(indev); indev = lv_indev_get_next(indev);
} }
ESP_LOGI(TAG, "Input devices reset");
// Now destroy C++ widgets (which deletes LVGL objects) under LVGL lock // Now destroy C++ widgets (which deletes LVGL objects)
ESP_LOGI(TAG, "Destroying widgets..."); ESP_LOGI(TAG, "Destroying widgets...");
destroyAllWidgets(); destroyAllWidgets();
lv_obj_clean(lv_scr_act()); lv_obj_clean(lv_scr_act());
@ -383,25 +383,27 @@ void WidgetManager::applyScreen(uint8_t screenId) {
} }
lv_obj_invalidate(lv_scr_act()); lv_obj_invalidate(lv_scr_act());
esp_lv_adapter_unlock();
ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId); ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId);
} }
void WidgetManager::showModalScreen(const ScreenConfig& screen) { 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_) { if (modalContainer_) {
closeModal(); closeModalLocked();
} }
if (esp_lv_adapter_lock(-1) != ESP_OK) return; // Reset all input devices
// Reset all input devices BEFORE destroying widgets
lv_indev_t* indev = lv_indev_get_next(nullptr); lv_indev_t* indev = lv_indev_get_next(nullptr);
while (indev) { while (indev) {
lv_indev_reset(indev, nullptr); lv_indev_reset(indev, nullptr);
indev = lv_indev_get_next(indev); indev = lv_indev_get_next(indev);
} }
// Destroy any existing widgets before creating modal widgets
destroyAllWidgets(); destroyAllWidgets();
lv_disp_t* disp = lv_disp_get_default(); lv_disp_t* disp = lv_disp_get_default();
@ -488,22 +490,22 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) {
applyCachedValuesToWidgets(); applyCachedValuesToWidgets();
esp_lv_adapter_unlock();
ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight); ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight);
} }
void WidgetManager::closeModal() { 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_); printf("WM: closeModal Start. Container=%p\n", (void*)modalContainer_);
fflush(stdout); fflush(stdout);
if (!modalContainer_) { if (!modalContainer_) {
return; return;
} }
if (esp_lv_adapter_lock(-1) != ESP_OK) {
ESP_LOGE(TAG, "closeModal: Failed to lock LVGL");
return;
}
// Reset input devices // Reset input devices
lv_indev_t* indev = lv_indev_get_next(nullptr); lv_indev_t* indev = lv_indev_get_next(nullptr);
while (indev) { while (indev) {
@ -530,12 +532,11 @@ void WidgetManager::closeModal() {
modalContainer_ = nullptr; modalContainer_ = nullptr;
modalScreenId_ = SCREEN_ID_NONE; modalScreenId_ = SCREEN_ID_NONE;
esp_lv_adapter_unlock();
printf("WM: closeModal Complete\n"); printf("WM: closeModal Complete\n");
fflush(stdout); fflush(stdout);
} }
void WidgetManager::showScreen(uint8_t screenId) { void WidgetManager::showScreenLocked(uint8_t screenId) {
ESP_LOGI(TAG, "showScreen(%d) called", screenId); ESP_LOGI(TAG, "showScreen(%d) called", screenId);
ScreenConfig* screen = config_.findScreen(screenId); ScreenConfig* screen = config_.findScreen(screenId);
@ -548,14 +549,14 @@ void WidgetManager::showScreen(uint8_t screenId) {
static_cast<int>(screen->mode)); static_cast<int>(screen->mode));
if (screen->mode == ScreenMode::MODAL) { if (screen->mode == ScreenMode::MODAL) {
showModalScreen(*screen); showModalScreenLocked(*screen);
return; return;
} }
previousScreenId_ = activeScreenId_; previousScreenId_ = activeScreenId_;
activeScreenId_ = screen->id; activeScreenId_ = screen->id;
standbyActive_ = false; standbyActive_ = false;
applyScreen(activeScreenId_); applyScreenLocked(activeScreenId_);
} }
void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) { void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target) {
@ -603,7 +604,7 @@ void WidgetManager::handleButtonAction(const WidgetConfig& cfg, lv_obj_t* target
} }
} }
void WidgetManager::goBack() { void WidgetManager::goBackLocked() {
printf("WM: goBack called. Modal=%p Active=%d Prev=%d\n", printf("WM: goBack called. Modal=%p Active=%d Prev=%d\n",
(void*)modalContainer_, activeScreenId_, previousScreenId_); (void*)modalContainer_, activeScreenId_, previousScreenId_);
fflush(stdout); fflush(stdout);
@ -611,17 +612,17 @@ void WidgetManager::goBack() {
if (modalContainer_) { if (modalContainer_) {
printf("WM: Closing modal...\n"); printf("WM: Closing modal...\n");
fflush(stdout); fflush(stdout);
closeModal(); closeModalLocked();
printf("WM: Modal closed. Restoring screen %d\n", activeScreenId_); printf("WM: Modal closed. Restoring screen %d\n", activeScreenId_);
fflush(stdout); fflush(stdout);
// Restore the active screen (which was in background) // Restore the active screen (which was in background)
if (config_.findScreen(activeScreenId_)) { if (config_.findScreen(activeScreenId_)) {
applyScreen(activeScreenId_); applyScreenLocked(activeScreenId_);
} else { } else {
ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_); ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_);
if (config_.findScreen(config_.startScreenId)) { if (config_.findScreen(config_.startScreenId)) {
activeScreenId_ = config_.startScreenId; activeScreenId_ = config_.startScreenId;
applyScreen(activeScreenId_); applyScreenLocked(activeScreenId_);
} }
} }
return; return;
@ -633,7 +634,7 @@ void WidgetManager::goBack() {
if (config_.findScreen(previousScreenId_)) { if (config_.findScreen(previousScreenId_)) {
activeScreenId_ = previousScreenId_; activeScreenId_ = previousScreenId_;
previousScreenId_ = SCREEN_ID_NONE; previousScreenId_ = SCREEN_ID_NONE;
applyScreen(activeScreenId_); applyScreenLocked(activeScreenId_);
} else { } else {
ESP_LOGW(TAG, "Previous screen %d not found", previousScreenId_); ESP_LOGW(TAG, "Previous screen %d not found", previousScreenId_);
previousScreenId_ = SCREEN_ID_NONE; previousScreenId_ = SCREEN_ID_NONE;
@ -644,6 +645,12 @@ void WidgetManager::goBack() {
} }
} }
void WidgetManager::goBack() {
if (esp_lv_adapter_lock(-1) != ESP_OK) return;
goBackLocked();
esp_lv_adapter_unlock();
}
void WidgetManager::enterStandby() { void WidgetManager::enterStandby() {
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return; if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
if (standbyActive_) return; if (standbyActive_) return;
@ -655,7 +662,7 @@ void WidgetManager::enterStandby() {
standbyReturnScreenId_ = activeScreenId_; standbyReturnScreenId_ = activeScreenId_;
standbyActive_ = true; standbyActive_ = true;
activeScreenId_ = standbyScreen->id; activeScreenId_ = standbyScreen->id;
applyScreen(activeScreenId_); applyScreenLocked(activeScreenId_);
} }
void WidgetManager::loop() { void WidgetManager::loop() {
@ -668,9 +675,9 @@ void WidgetManager::loop() {
ESP_LOGI(TAG, "Executing navigation: action=%d target=%d", ESP_LOGI(TAG, "Executing navigation: action=%d target=%d",
static_cast<int>(navAction_), navTargetScreen_); static_cast<int>(navAction_), navTargetScreen_);
if (navAction_ == ButtonAction::JUMP) { if (navAction_ == ButtonAction::JUMP) {
showScreen(navTargetScreen_); showScreenLocked(navTargetScreen_);
} else if (navAction_ == ButtonAction::BACK) { } else if (navAction_ == ButtonAction::BACK) {
goBack(); goBackLocked();
} }
didUiNav = true; didUiNav = true;
ESP_LOGI(TAG, "Navigation complete"); ESP_LOGI(TAG, "Navigation complete");
@ -681,7 +688,7 @@ void WidgetManager::loop() {
standbyWakePending_ = false; standbyWakePending_ = false;
if (standbyWakeTarget_ != SCREEN_ID_NONE) { if (standbyWakeTarget_ != SCREEN_ID_NONE) {
activeScreenId_ = standbyWakeTarget_; activeScreenId_ = standbyWakeTarget_;
applyScreen(activeScreenId_); applyScreenLocked(activeScreenId_);
} }
didUiNav = true; didUiNav = true;
} }
@ -851,7 +858,8 @@ void WidgetManager::processUiQueue() {
if (!uiQueue_) return; if (!uiQueue_) return;
if (uxQueueMessagesWaiting(uiQueue_) == 0) return; if (uxQueueMessagesWaiting(uiQueue_) == 0) return;
if (esp_lv_adapter_lock(-1) != ESP_OK) return; // Call from loop() which is ALREADY LOCKED by LVGL timer
// DO NOT take esp_lv_adapter_lock() here.
UiEvent event = {}; UiEvent event = {};
static constexpr size_t kMaxEventsPerLoop = 8; static constexpr size_t kMaxEventsPerLoop = 8;
@ -879,8 +887,6 @@ void WidgetManager::processUiQueue() {
refreshChartWidgetsLocked(); refreshChartWidgetsLocked();
chartRefreshPending_ = false; chartRefreshPending_ = false;
} }
esp_lv_adapter_unlock();
} }
void WidgetManager::applyCachedValuesToWidgets() { void WidgetManager::applyCachedValuesToWidgets() {
@ -944,9 +950,9 @@ void WidgetManager::refreshChartWidgetsLocked() {
} }
void WidgetManager::refreshChartWidgets() { void WidgetManager::refreshChartWidgets() {
if (esp_lv_adapter_lock(-1) != ESP_OK) return; // Call from loop() which is ALREADY LOCKED by LVGL timer
// DO NOT take esp_lv_adapter_lock() here.
refreshChartWidgetsLocked(); refreshChartWidgetsLocked();
esp_lv_adapter_unlock();
} }
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) { void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) {
@ -1724,4 +1730,4 @@ bool WidgetManager::updateConfigFromJson(const char* json) {
cJSON_Delete(root); cJSON_Delete(root);
ESP_LOGI(TAG, "Parsed %d screens from JSON", config_.screenCount); ESP_LOGI(TAG, "Parsed %d screens from JSON", config_.screenCount);
return true; return true;
} }

View File

@ -141,9 +141,13 @@ private:
void createDefaultConfig(); void createDefaultConfig();
void applyScreen(uint8_t screenId); void applyScreen(uint8_t screenId);
void showScreen(uint8_t screenId); void applyScreenLocked(uint8_t screenId);
void showScreenLocked(uint8_t screenId);
void showModalScreen(const ScreenConfig& screen); void showModalScreen(const ScreenConfig& screen);
void showModalScreenLocked(const ScreenConfig& screen);
void closeModal(); void closeModal();
void closeModalLocked();
void goBackLocked();
void enterStandby(); void enterStandby();
ScreenConfig* activeScreen(); ScreenConfig* activeScreen();
const ScreenConfig* activeScreen() const; const ScreenConfig* activeScreen() const;

View File

@ -52,7 +52,7 @@ public:
1280, // Vertical resolution 1280, // Vertical resolution
ESP_LV_ADAPTER_ROTATE_90 // Rotation ESP_LV_ADAPTER_ROTATE_90 // Rotation
); );
disp_cfg.profile.buffer_height = 4; // Keep internal draw buffer small. disp_cfg.profile.buffer_height = 34; // Reduced to 20 to fit in RAM (32KB)
lv_disp_t* lv_display = esp_lv_adapter_register_display(&disp_cfg); lv_disp_t* lv_display = esp_lv_adapter_register_display(&disp_cfg);
assert(lv_display != NULL); assert(lv_display != NULL);

View File

@ -71,8 +71,8 @@ lv_obj_t* ChartWidget::create(lv_obj_t* parent) {
} }
} }
//applyAxisLabels(); applyAxisLabels();
//refreshData(); refreshData();
return obj_; return obj_;
} }

View File

@ -100,8 +100,10 @@ lv_obj_t* PowerNodeWidget::create(lv_obj_t* parent) {
config_.width > 0 ? config_.width : 120, config_.width > 0 ? config_.width : 120,
config_.height > 0 ? config_.height : 120); config_.height > 0 ? config_.height : 120);
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE); lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_set_layout(obj_, LV_LAYOUT_NONE); lv_obj_set_flex_flow(obj_, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(obj_, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_pad_all(obj_, 6, 0); lv_obj_set_style_pad_all(obj_, 6, 0);
lv_obj_set_style_pad_gap(obj_, 2, 0);
if (labelText_[0] != '\0') { if (labelText_[0] != '\0') {
labelLabel_ = lv_label_create(obj_); labelLabel_ = lv_label_create(obj_);
set_obj_name(labelLabel_, "PowerNode", config_.id, "label"); set_obj_name(labelLabel_, "PowerNode", config_.id, "label");
@ -175,63 +177,11 @@ void PowerNodeWidget::applyStyle() {
lv_obj_set_style_text_align(valueLabel_, LV_TEXT_ALIGN_CENTER, 0); lv_obj_set_style_text_align(valueLabel_, LV_TEXT_ALIGN_CENTER, 0);
} }
layoutChildren();
} }
void PowerNodeWidget::updateValueText(const char* text) { void PowerNodeWidget::updateValueText(const char* text) {
if (valueLabel_ == nullptr || text == nullptr) return; if (valueLabel_ == nullptr || text == nullptr) return;
if (set_label_text_if_changed(valueLabel_, text)) { set_label_text_if_changed(valueLabel_, text);
layoutChildren();
}
}
void PowerNodeWidget::layoutChildren() {
if (obj_ == nullptr) return;
lv_obj_update_layout(obj_);
lv_coord_t width = lv_obj_get_width(obj_);
lv_coord_t height = lv_obj_get_height(obj_);
if (width <= 0 || height <= 0) return;
lv_coord_t pad_left = lv_obj_get_style_pad_left(obj_, LV_PART_MAIN);
lv_coord_t pad_right = lv_obj_get_style_pad_right(obj_, LV_PART_MAIN);
lv_coord_t pad_top = lv_obj_get_style_pad_top(obj_, LV_PART_MAIN);
lv_coord_t pad_bottom = lv_obj_get_style_pad_bottom(obj_, LV_PART_MAIN);
lv_coord_t avail_w = width - pad_left - pad_right;
lv_coord_t avail_h = height - pad_top - pad_bottom;
if (avail_w <= 0 || avail_h <= 0) return;
lv_obj_t* items[3];
uint8_t count = 0;
if (labelLabel_) items[count++] = labelLabel_;
if (iconLabel_) items[count++] = iconLabel_;
if (valueLabel_) items[count++] = valueLabel_;
if (count == 0) return;
const lv_coord_t gap = 2;
lv_coord_t total = 0;
for (uint8_t i = 0; i < count; ++i) {
total += lv_obj_get_height(items[i]);
}
total += gap * (count - 1);
lv_coord_t start_y = pad_top;
if (avail_h > total) {
start_y += (avail_h - total) / 2;
}
lv_coord_t y = start_y;
for (uint8_t i = 0; i < count; ++i) {
lv_coord_t item_w = lv_obj_get_width(items[i]);
lv_coord_t x = pad_left;
if (avail_w > item_w) {
x += (avail_w - item_w) / 2;
}
lv_obj_set_pos(items[i], x, y);
y += lv_obj_get_height(items[i]) + gap;
}
} }
void PowerNodeWidget::onKnxValue(float value) { void PowerNodeWidget::onKnxValue(float value) {

View File

@ -21,6 +21,5 @@ private:
void parseText(); void parseText();
void updateValueText(const char* text); void updateValueText(const char* text);
void layoutChildren();
static int encodeUtf8(uint32_t codepoint, char* buf); static int encodeUtf8(uint32_t codepoint, char* buf);
}; };

View File

@ -1485,7 +1485,7 @@ CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_MEMTEST=y CONFIG_SPIRAM_MEMTEST=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384
# CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP is not set # CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP is not set
CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768 CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=100000
CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
# CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set # CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set
# end of PSRAM config # end of PSRAM config