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

View File

@ -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<SeriesKey, HISTORY_MAX_SERIES> 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<int32_t>(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;
}
xSemaphoreGive(mutex_);
int64_t monoUs = esp_timer_get_time();
if (dirty_ && timeSynced_ && SdCard::instance().isMounted()) {
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
saveToSdCard();
lastSaveMonoUs_ = monoUs;
}
}
// 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<time_t>(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<long>(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;
}
// 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;
}
for (size_t i = 0; i < series_.size(); ++i) {
xSemaphoreTake(mutex_, portMAX_DELAY);
const HistorySeries& series = series_[i];
if (!series.active) continue;
HistoryFileSeriesHeader sh = {};
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;
if (fwrite(&sh, sizeof(sh), 1, f) != 1) {
fclose(f);
ESP_LOGW(TAG, "Failed to write history series header");
return;
if (!series.active) {
xSemaphoreGive(mutex_);
continue;
}
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;
// 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;
}
}
fclose(f);
free(tempBuf);
xSemaphoreTake(mutex_, portMAX_DELAY);
dirty_ = false;
dataEpoch_ = true;
xSemaphoreGive(mutex_);
fclose(f);
ESP_LOGI(TAG, "History saved (%d series)", static_cast<int>(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");
}

View File

@ -1,6 +1,8 @@
#pragma once
#include "WidgetConfig.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include <array>
#include <climits>
#include <cstdint>
@ -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;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,6 +21,5 @@ private:
void parseText();
void updateValueText(const char* text);
void layoutChildren();
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_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