diff --git a/dependencies.lock b/dependencies.lock index 72222cc..573e67e 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,77 +1,190 @@ dependencies: 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__button - type: local + registry_url: https://components.espressif.com + type: service version: 4.1.5 espressif/cmake_utilities: component_hash: - 05165f30922b422b4b90c08845e6d449329b97370fbd06309803d8cb539d79e3 + 351350613ceafba240b761b4ea991e0f231ac7a9f59a9ee901f751bddc0bb18f dependencies: - name: idf require: private version: '>=4.1' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service - version: 1.1.1 + version: 0.5.3 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__eppp_link - type: local + registry_url: https://components.espressif.com + type: service version: 1.1.4 espressif/esp_hosted: - dependencies: [] + component_hash: + 6f5dc62f18c86b4ac65e1c8cb56fe122de894d80b8c8eaac451f3c0a913b8d76 + dependencies: + - name: idf + require: private + version: '>=5.3' source: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_hosted - type: local + registry_url: https://components.espressif.com + type: service version: 2.11.5 espressif/esp_lcd_touch: - dependencies: [] + component_hash: + 3f85a7d95af876f1a6ecca8eb90a81614890d0f03a038390804e5a77e2caf862 + dependencies: + - name: idf + require: private + version: '>=4.4.2' source: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lcd_touch - type: local + registry_url: https://components.espressif.com + type: service version: 1.2.1 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lcd_touch_gt911 - type: local + registry_url: https://components.espressif.com/ + type: service version: 1.2.0~1 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lv_decoder - type: local + registry_url: https://components.espressif.com + type: service + targets: + - esp32 + - esp32s2 + - esp32s3 + - esp32p4 + - esp32c2 + - esp32c3 + - esp32c5 + - esp32c6 version: 0.3.2 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lv_fs - type: local + registry_url: https://components.espressif.com + type: service version: 1.0.1 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lvgl_adapter - type: local + registry_url: https://components.espressif.com/ + type: service version: 0.3.0 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_lvgl_port - type: local + registry_url: https://components.espressif.com/ + type: service version: 2.7.0 espressif/esp_mmap_assets: component_hash: @@ -85,7 +198,7 @@ dependencies: require: private version: '>=5.0' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 1.4.0 espressif/esp_new_jpeg: @@ -93,7 +206,7 @@ dependencies: e6af208a875abd0ecfc0213d3751a11b504b463ebde6930f24096047925fa5c1 dependencies: [] source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service targets: - esp32 @@ -113,16 +226,30 @@ dependencies: require: private version: '>=5.0' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 1.1.2 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__esp_wifi_remote - type: local - version: 1.3.1 + registry_url: https://components.espressif.com/ + type: service + version: 1.3.2 espressif/freetype: component_hash: cd5e2d8458e6e8d73f1120ac474467cabb669d8ea4b25050bf6a348c1e89225e @@ -131,15 +258,23 @@ dependencies: require: private version: '>=4.4' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 2.13.3~1 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/espressif__i2c_bus - type: local + registry_url: https://components.espressif.com + type: service version: 1.5.0 espressif/knob: component_hash: @@ -153,7 +288,7 @@ dependencies: require: private version: '>=4.4.1' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 1.0.2 espressif/libpng: @@ -168,7 +303,7 @@ dependencies: require: private version: ^1.2.13 source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 1.6.54 espressif/wifi_remote_over_eppp: @@ -183,7 +318,7 @@ dependencies: require: private version: '>=5.3' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 0.3.0 espressif/zlib: @@ -194,7 +329,7 @@ dependencies: require: private version: '>=4.4' source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 1.3.1 idf: @@ -206,40 +341,37 @@ dependencies: 17e68bfd21f0edf4c3ee838e2273da840bf3930e5dbc3bfa6c1190c3aed41f9f dependencies: [] source: - registry_url: https://components.espressif.com/ + registry_url: https://components.espressif.com type: service version: 9.4.0 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: - path: - /home/thomas/projekte/test1/knxdisplay/managed_components/waveshare__esp_lcd_jd9365_10_1 - type: local + registry_url: https://components.espressif.com/ + type: service + targets: + - esp32p4 version: 1.0.4 direct_dependencies: -- espressif/button -- espressif/cmake_utilities -- espressif/eppp_link -- espressif/esp_hosted -- espressif/esp_lcd_touch - espressif/esp_lcd_touch_gt911 -- espressif/esp_lv_decoder -- espressif/esp_lv_fs - espressif/esp_lvgl_adapter - espressif/esp_lvgl_port -- espressif/esp_mmap_assets -- espressif/esp_new_jpeg -- espressif/esp_serial_slave_link - espressif/esp_wifi_remote -- espressif/freetype -- espressif/i2c_bus -- espressif/knob -- espressif/libpng -- espressif/wifi_remote_over_eppp -- espressif/zlib - idf -- lvgl/lvgl - waveshare/esp_lcd_jd9365_10_1 -manifest_hash: ee0446e4a514a791315863af0d77bdcf353d357a7b8081e3e2b252138a66497b +manifest_hash: 2525ec0a57701bb08b15bfef69a11dd2185dbcba5e02784971dcae606254ce64 target: esp32p4 version: 2.0.0 diff --git a/main/HistoryStore.cpp b/main/HistoryStore.cpp index ba59215..5edcc21 100644 --- a/main/HistoryStore.cpp +++ b/main/HistoryStore.cpp @@ -44,7 +44,15 @@ HistoryStore& HistoryStore::instance() { return inst; } -HistoryStore::HistoryStore() = default; +HistoryStore::HistoryStore() { + mutex_ = xSemaphoreCreateMutex(); +} + +HistoryStore::~HistoryStore() { + if (mutex_) { + vSemaphoreDelete(mutex_); + } +} bool HistoryStore::isNumericSource(TextSource source) { return source == TextSource::KNX_DPT_TEMP || @@ -97,6 +105,7 @@ const HistoryStore::HistorySeries* HistoryStore::findSeriesByKey(const SeriesKey } void HistoryStore::configureFromConfig(const GuiConfig& config) { + xSemaphoreTake(mutex_, portMAX_DELAY); std::array needed = {}; size_t neededCount = 0; @@ -191,20 +200,29 @@ void HistoryStore::configureFromConfig(const GuiConfig& config) { if (changed) { dirty_ = true; } + xSemaphoreGive(mutex_); } 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) { + xSemaphoreTake(mutex_, portMAX_DELAY); HistorySeries* series = findSeries(groupAddr, source); - if (!series) return false; + if (!series) { + xSemaphoreGive(mutex_); + return false; + } int64_t nowSec = now(); series->latestValue = value; series->latestTs = static_cast(nowSec); series->hasLatest = true; + xSemaphoreGive(mutex_); return true; } @@ -234,12 +252,19 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP outValues[i] = NO_POINT; } + xSemaphoreTake(mutex_, portMAX_DELAY); const HistorySeries* series = findSeries(groupAddr, source); - if (!series) return false; + if (!series) { + xSemaphoreGive(mutex_); + return false; + } int64_t nowSec = now(); int32_t window = periodSeconds(period); - if (window <= 0) return false; + if (window <= 0) { + xSemaphoreGive(mutex_); + return false; + } int64_t start = nowSec - window; @@ -264,6 +289,7 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP HistoryPoint latest{series->latestTs, series->latestValue}; accumulate(latest); } + xSemaphoreGive(mutex_); bool hasData = false; for (size_t i = 0; i < outCount; ++i) { @@ -278,6 +304,7 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP } bool HistoryStore::tick() { + xSemaphoreTake(mutex_, portMAX_DELAY); int64_t nowSec = now(); bool added = false; @@ -302,35 +329,64 @@ bool HistoryStore::tick() { dirty_ = true; if (timeSynced_) dataEpoch_ = true; } - - int64_t monoUs = esp_timer_get_time(); - if (dirty_ && timeSynced_ && SdCard::instance().isMounted()) { - if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) { - saveToSdCard(); - lastSaveMonoUs_ = monoUs; - } - } - + xSemaphoreGive(mutex_); + + // REMOVED synchronous save from here to avoid blocking UI task 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) { + xSemaphoreTake(mutex_, portMAX_DELAY); hour_ = value.tm_hour; minute_ = value.tm_min; second_ = value.tm_sec; 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(); } void HistoryStore::updateDate(const struct tm& value) { + xSemaphoreTake(mutex_, portMAX_DELAY); year_ = value.tm_year; month_ = value.tm_mon; day_ = value.tm_mday; hasDate_ = true; + xSemaphoreGive(mutex_); applyTimeSync(); } void HistoryStore::updateDateTime(const struct tm& value) { + xSemaphoreTake(mutex_, portMAX_DELAY); year_ = value.tm_year; month_ = value.tm_mon; day_ = value.tm_mday; @@ -339,6 +395,7 @@ void HistoryStore::updateDateTime(const struct tm& value) { second_ = value.tm_sec; hasDate_ = true; hasTime_ = true; + xSemaphoreGive(mutex_); applyTimeSync(); } @@ -360,11 +417,16 @@ int64_t HistoryStore::buildEpoch() const { } bool HistoryStore::applyTimeSync() { - if (!hasDate_ || !hasTime_) return false; + xSemaphoreTake(mutex_, portMAX_DELAY); + if (!hasDate_ || !hasTime_) { + xSemaphoreGive(mutex_); + return false; + } int64_t epoch = buildEpoch(); if (epoch <= 0) { ESP_LOGW(TAG, "Invalid KNX time/date for sync"); + xSemaphoreGive(mutex_); return false; } @@ -373,9 +435,12 @@ bool HistoryStore::applyTimeSync() { baseEpoch_ = epoch; baseMono_ = esp_timer_get_time() / 1000000LL; + // Release lock for system call (optional but safer) + xSemaphoreGive(mutex_); struct timeval tv = {}; tv.tv_sec = static_cast(epoch); settimeofday(&tv, nullptr); + xSemaphoreTake(mutex_, portMAX_DELAY); if (!wasSynced) { bool hasData = false; @@ -394,11 +459,14 @@ bool HistoryStore::applyTimeSync() { } dirty_ = true; + xSemaphoreGive(mutex_); + ESP_LOGI(TAG, "Time synced: %ld", static_cast(epoch)); return !wasSynced; } void HistoryStore::clearAll() { + // Assumes lock is held by caller for (size_t i = 0; i < series_.size(); ++i) { HistorySeries& series = series_[i]; if (!series.active) continue; @@ -414,7 +482,13 @@ void HistoryStore::clearAll() { void HistoryStore::saveToSdCard() { 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"); if (!f) { @@ -422,6 +496,7 @@ void HistoryStore::saveToSdCard() { return; } + xSemaphoreTake(mutex_, portMAX_DELAY); HistoryFileHeader header = {}; header.magic = HISTORY_MAGIC; header.version = HISTORY_VERSION; @@ -436,6 +511,7 @@ void HistoryStore::saveToSdCard() { header.coarseCapacity = HISTORY_COARSE_CAP; header.fineInterval = HISTORY_FINE_INTERVAL; header.coarseInterval = HISTORY_COARSE_INTERVAL; + xSemaphoreGive(mutex_); if (fwrite(&header, sizeof(header), 1, f) != 1) { fclose(f); @@ -443,41 +519,59 @@ void HistoryStore::saveToSdCard() { return; } - for (size_t i = 0; i < series_.size(); ++i) { - const HistorySeries& series = series_[i]; - if (!series.active) continue; - HistoryFileSeriesHeader sh = {}; - sh.groupAddr = series.key.addr; - sh.source = static_cast(series.key.source); - sh.fineCount = static_cast(series.fine.count); - sh.fineHead = static_cast(series.fine.head); - sh.coarseCount = static_cast(series.coarse.count); - sh.coarseHead = static_cast(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; - } + // Allocate temp buffer for one series to minimize lock time + // Size: Header + Fine + Coarse + size_t seriesDataSize = sizeof(HistoryFileSeriesHeader) + + sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP); + uint8_t* tempBuf = (uint8_t*)malloc(seriesDataSize); + if (!tempBuf) { + fclose(f); + ESP_LOGE(TAG, "Failed to allocate temp buffer for save"); + 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(series.key.source); + sh->fineCount = static_cast(series.fine.count); + sh->fineHead = static_cast(series.fine.head); + sh->coarseCount = static_cast(series.coarse.count); + sh->coarseHead = static_cast(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; dataEpoch_ = true; + xSemaphoreGive(mutex_); + + fclose(f); ESP_LOGI(TAG, "History saved (%d series)", static_cast(activeCount)); } @@ -508,6 +602,7 @@ void HistoryStore::loadFromSdCard() { return; } + xSemaphoreTake(mutex_, portMAX_DELAY); for (uint16_t i = 0; i < header.seriesCount; ++i) { HistoryFileSeriesHeader sh = {}; 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); } } - - fclose(f); dirty_ = false; dataEpoch_ = true; + xSemaphoreGive(mutex_); + + fclose(f); ESP_LOGI(TAG, "History loaded"); } diff --git a/main/HistoryStore.hpp b/main/HistoryStore.hpp index 5dabf1b..68c43b9 100644 --- a/main/HistoryStore.hpp +++ b/main/HistoryStore.hpp @@ -1,6 +1,8 @@ #pragma once #include "WidgetConfig.hpp" +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" #include #include #include @@ -14,6 +16,7 @@ public: bool updateLatest(uint16_t groupAddr, TextSource source, float value); bool isTracked(uint16_t groupAddr, TextSource source) const; bool tick(); + void performAutoSave(); bool fillChartSeries(uint16_t groupAddr, TextSource source, ChartPeriod period, int32_t* outValues, size_t outCount) const; @@ -33,6 +36,7 @@ public: private: HistoryStore(); + ~HistoryStore(); struct SeriesKey { uint16_t addr = 0; @@ -124,4 +128,6 @@ private: bool dirty_ = false; int64_t lastSaveMonoUs_ = 0; bool dataEpoch_ = false; + + mutable SemaphoreHandle_t mutex_ = nullptr; }; diff --git a/main/KnxWorker.cpp b/main/KnxWorker.cpp index 574e340..89fbffd 100644 --- a/main/KnxWorker.cpp +++ b/main/KnxWorker.cpp @@ -243,7 +243,14 @@ void KnxWorker::clearSettings() { #endif } +#include "HistoryStore.hpp" + +// ... imports ... + void KnxWorker::loop() { + // Check for auto-save (handled by HistoryStore logic) + HistoryStore::instance().performAutoSave(); + #if UART_DEBUG_MODE // Periodically send U_STATE_REQ to test TX direction uint32_t now = millis(); diff --git a/main/WidgetManager.cpp b/main/WidgetManager.cpp index b7f7523..fb5a028 100644 --- a/main/WidgetManager.cpp +++ b/main/WidgetManager.cpp @@ -322,6 +322,15 @@ const ScreenConfig* WidgetManager::currentScreen() const { } void WidgetManager::applyScreen(uint8_t screenId) { + if (esp_lv_adapter_lock(-1) != ESP_OK) { + ESP_LOGE(TAG, "Failed to acquire LVGL lock!"); + return; + } + applyScreenLocked(screenId); + esp_lv_adapter_unlock(); +} + +void WidgetManager::applyScreenLocked(uint8_t screenId) { ESP_LOGI(TAG, "applyScreen(%d) - start", screenId); ScreenConfig* screen = config_.findScreen(screenId); @@ -334,16 +343,9 @@ void WidgetManager::applyScreen(uint8_t screenId) { if (modalContainer_) { 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(); bool invEnabled = true; if (disp) { @@ -351,17 +353,15 @@ void WidgetManager::applyScreen(uint8_t screenId) { 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 + // Reset all input devices BEFORE destroying widgets 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 + // Now destroy C++ widgets (which deletes LVGL objects) ESP_LOGI(TAG, "Destroying widgets..."); destroyAllWidgets(); lv_obj_clean(lv_scr_act()); @@ -383,25 +383,27 @@ void WidgetManager::applyScreen(uint8_t screenId) { } lv_obj_invalidate(lv_scr_act()); - esp_lv_adapter_unlock(); ESP_LOGI(TAG, "applyScreen(%d) - complete", screenId); } void WidgetManager::showModalScreen(const ScreenConfig& screen) { + if (esp_lv_adapter_lock(-1) != ESP_OK) return; + showModalScreenLocked(screen); + esp_lv_adapter_unlock(); +} + +void WidgetManager::showModalScreenLocked(const ScreenConfig& screen) { if (modalContainer_) { - closeModal(); + closeModalLocked(); } - if (esp_lv_adapter_lock(-1) != ESP_OK) return; - - // Reset all input devices BEFORE destroying widgets + // Reset all 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 any existing widgets before creating modal widgets destroyAllWidgets(); lv_disp_t* disp = lv_disp_get_default(); @@ -488,22 +490,22 @@ void WidgetManager::showModalScreen(const ScreenConfig& screen) { applyCachedValuesToWidgets(); - esp_lv_adapter_unlock(); ESP_LOGI(TAG, "Modal screen %d opened (%ldx%ld)", screen.id, modalWidth, modalHeight); } void WidgetManager::closeModal() { + if (esp_lv_adapter_lock(-1) != ESP_OK) return; + closeModalLocked(); + esp_lv_adapter_unlock(); +} + +void WidgetManager::closeModalLocked() { printf("WM: closeModal Start. Container=%p\n", (void*)modalContainer_); fflush(stdout); if (!modalContainer_) { return; } - 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) { @@ -530,12 +532,11 @@ void WidgetManager::closeModal() { modalContainer_ = nullptr; modalScreenId_ = SCREEN_ID_NONE; - esp_lv_adapter_unlock(); printf("WM: closeModal Complete\n"); fflush(stdout); } -void WidgetManager::showScreen(uint8_t screenId) { +void WidgetManager::showScreenLocked(uint8_t screenId) { ESP_LOGI(TAG, "showScreen(%d) called", screenId); ScreenConfig* screen = config_.findScreen(screenId); @@ -548,14 +549,14 @@ void WidgetManager::showScreen(uint8_t screenId) { static_cast(screen->mode)); if (screen->mode == ScreenMode::MODAL) { - showModalScreen(*screen); + showModalScreenLocked(*screen); return; } previousScreenId_ = activeScreenId_; activeScreenId_ = screen->id; standbyActive_ = false; - applyScreen(activeScreenId_); + applyScreenLocked(activeScreenId_); } 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", (void*)modalContainer_, activeScreenId_, previousScreenId_); fflush(stdout); @@ -611,17 +612,17 @@ void WidgetManager::goBack() { if (modalContainer_) { printf("WM: Closing modal...\n"); fflush(stdout); - closeModal(); + closeModalLocked(); 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_); + applyScreenLocked(activeScreenId_); } else { ESP_LOGE(TAG, "Active screen %d not found after closing modal!", activeScreenId_); if (config_.findScreen(config_.startScreenId)) { activeScreenId_ = config_.startScreenId; - applyScreen(activeScreenId_); + applyScreenLocked(activeScreenId_); } } return; @@ -633,7 +634,7 @@ void WidgetManager::goBack() { if (config_.findScreen(previousScreenId_)) { activeScreenId_ = previousScreenId_; previousScreenId_ = SCREEN_ID_NONE; - applyScreen(activeScreenId_); + applyScreenLocked(activeScreenId_); } else { ESP_LOGW(TAG, "Previous screen %d not found", previousScreenId_); 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() { if (!config_.standbyEnabled || config_.standbyMinutes == 0) return; if (standbyActive_) return; @@ -655,7 +662,7 @@ void WidgetManager::enterStandby() { standbyReturnScreenId_ = activeScreenId_; standbyActive_ = true; activeScreenId_ = standbyScreen->id; - applyScreen(activeScreenId_); + applyScreenLocked(activeScreenId_); } void WidgetManager::loop() { @@ -668,9 +675,9 @@ void WidgetManager::loop() { ESP_LOGI(TAG, "Executing navigation: action=%d target=%d", static_cast(navAction_), navTargetScreen_); if (navAction_ == ButtonAction::JUMP) { - showScreen(navTargetScreen_); + showScreenLocked(navTargetScreen_); } else if (navAction_ == ButtonAction::BACK) { - goBack(); + goBackLocked(); } didUiNav = true; ESP_LOGI(TAG, "Navigation complete"); @@ -681,7 +688,7 @@ void WidgetManager::loop() { standbyWakePending_ = false; if (standbyWakeTarget_ != SCREEN_ID_NONE) { activeScreenId_ = standbyWakeTarget_; - applyScreen(activeScreenId_); + applyScreenLocked(activeScreenId_); } didUiNav = true; } @@ -851,7 +858,8 @@ void WidgetManager::processUiQueue() { if (!uiQueue_) 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 = {}; static constexpr size_t kMaxEventsPerLoop = 8; @@ -879,8 +887,6 @@ void WidgetManager::processUiQueue() { refreshChartWidgetsLocked(); chartRefreshPending_ = false; } - - esp_lv_adapter_unlock(); } void WidgetManager::applyCachedValuesToWidgets() { @@ -944,9 +950,9 @@ void WidgetManager::refreshChartWidgetsLocked() { } 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(); - esp_lv_adapter_unlock(); } void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) { @@ -1724,4 +1730,4 @@ bool WidgetManager::updateConfigFromJson(const char* json) { cJSON_Delete(root); ESP_LOGI(TAG, "Parsed %d screens from JSON", config_.screenCount); return true; -} +} \ No newline at end of file diff --git a/main/WidgetManager.hpp b/main/WidgetManager.hpp index bb43cc4..a97b3f4 100644 --- a/main/WidgetManager.hpp +++ b/main/WidgetManager.hpp @@ -141,9 +141,13 @@ private: void createDefaultConfig(); 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 showModalScreenLocked(const ScreenConfig& screen); void closeModal(); + void closeModalLocked(); + void goBackLocked(); void enterStandby(); ScreenConfig* activeScreen(); const ScreenConfig* activeScreen() const; diff --git a/main/main.cpp b/main/main.cpp index b5c5419..8ad312c 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -52,7 +52,7 @@ public: 1280, // Vertical resolution 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); assert(lv_display != NULL); diff --git a/main/widgets/ChartWidget.cpp b/main/widgets/ChartWidget.cpp index 4412130..f42bebb 100644 --- a/main/widgets/ChartWidget.cpp +++ b/main/widgets/ChartWidget.cpp @@ -71,8 +71,8 @@ lv_obj_t* ChartWidget::create(lv_obj_t* parent) { } } - //applyAxisLabels(); - //refreshData(); + applyAxisLabels(); + refreshData(); return obj_; } diff --git a/main/widgets/PowerNodeWidget.cpp b/main/widgets/PowerNodeWidget.cpp index 6fd2daf..1da075c 100644 --- a/main/widgets/PowerNodeWidget.cpp +++ b/main/widgets/PowerNodeWidget.cpp @@ -100,8 +100,10 @@ lv_obj_t* PowerNodeWidget::create(lv_obj_t* parent) { config_.width > 0 ? config_.width : 120, config_.height > 0 ? config_.height : 120); 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_gap(obj_, 2, 0); if (labelText_[0] != '\0') { labelLabel_ = lv_label_create(obj_); 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); } - layoutChildren(); } void PowerNodeWidget::updateValueText(const char* text) { if (valueLabel_ == nullptr || text == nullptr) return; - if (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; - } + set_label_text_if_changed(valueLabel_, text); } void PowerNodeWidget::onKnxValue(float value) { diff --git a/main/widgets/PowerNodeWidget.hpp b/main/widgets/PowerNodeWidget.hpp index d1eebbd..6cbcb48 100644 --- a/main/widgets/PowerNodeWidget.hpp +++ b/main/widgets/PowerNodeWidget.hpp @@ -21,6 +21,5 @@ private: void parseText(); void updateValueText(const char* text); - void layoutChildren(); static int encodeUtf8(uint32_t codepoint, char* buf); }; diff --git a/sdkconfig b/sdkconfig index 7e0563e..5ca2961 100644 --- a/sdkconfig +++ b/sdkconfig @@ -1485,7 +1485,7 @@ CONFIG_SPIRAM_USE_MALLOC=y CONFIG_SPIRAM_MEMTEST=y CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 # 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_NOINIT_SEG_EXTERNAL_MEMORY is not set # end of PSRAM config