Backup
This commit is contained in:
parent
c30a3ef4e7
commit
7e451b4e3b
@ -227,9 +227,7 @@ bool HistoryStore::updateLatest(uint16_t groupAddr, TextSource source, float val
|
|||||||
}
|
}
|
||||||
|
|
||||||
int64_t HistoryStore::now() const {
|
int64_t HistoryStore::now() const {
|
||||||
int64_t monoSec = esp_timer_get_time() / 1000000LL;
|
return (int64_t)time(nullptr);
|
||||||
if (!timeSynced_) return monoSec;
|
|
||||||
return baseEpoch_ + (monoSec - baseMono_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t HistoryStore::periodSeconds(ChartPeriod period) const {
|
int32_t HistoryStore::periodSeconds(ChartPeriod period) const {
|
||||||
@ -294,9 +292,6 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
|
|||||||
bool hasData = false;
|
bool hasData = false;
|
||||||
int32_t lastValidValue = NO_POINT;
|
int32_t lastValidValue = NO_POINT;
|
||||||
|
|
||||||
// Optimization: Try to find a starting value from before the window if index 0 is empty?
|
|
||||||
// For now, simple forward-fill (step) is sufficient to connect sparse points.
|
|
||||||
|
|
||||||
for (size_t i = 0; i < outCount; ++i) {
|
for (size_t i = 0; i < outCount; ++i) {
|
||||||
if (counts[i] > 0) {
|
if (counts[i] > 0) {
|
||||||
float avg = sums[i] / counts[i];
|
float avg = sums[i] / counts[i];
|
||||||
@ -305,15 +300,9 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
|
|||||||
lastValidValue = val;
|
lastValidValue = val;
|
||||||
hasData = true;
|
hasData = true;
|
||||||
} else {
|
} else {
|
||||||
// No data in this bucket.
|
|
||||||
// Use the last valid value to draw a connected line (Step Hold).
|
|
||||||
// If we haven't seen any data yet (lastValidValue == NO_POINT),
|
|
||||||
// we leave it as NO_POINT (gap at start).
|
|
||||||
outValues[i] = lastValidValue;
|
outValues[i] = lastValidValue;
|
||||||
|
|
||||||
// If we just filled with a valid value, count it as having data
|
|
||||||
if (lastValidValue != NO_POINT) {
|
if (lastValidValue != NO_POINT) {
|
||||||
hasData = true; // Ensures y-scaling considers this flat line
|
hasData = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -324,8 +313,14 @@ bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartP
|
|||||||
bool HistoryStore::tick() {
|
bool HistoryStore::tick() {
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||||
int64_t nowSec = now();
|
int64_t nowSec = now();
|
||||||
bool added = false;
|
|
||||||
|
|
||||||
|
// Only collect data if time is roughly synced (after 2020)
|
||||||
|
if (nowSec < 1577836800LL) {
|
||||||
|
xSemaphoreGive(mutex_);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool added = false;
|
||||||
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 || !series.hasLatest) continue;
|
if (!series.active || !series.hasLatest) continue;
|
||||||
@ -345,20 +340,18 @@ bool HistoryStore::tick() {
|
|||||||
|
|
||||||
if (added) {
|
if (added) {
|
||||||
dirty_ = true;
|
dirty_ = true;
|
||||||
if (timeSynced_) dataEpoch_ = true;
|
|
||||||
}
|
}
|
||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
|
|
||||||
// REMOVED synchronous save from here to avoid blocking UI task
|
|
||||||
return added;
|
return added;
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryStore::performAutoSave() {
|
void HistoryStore::performAutoSave() {
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||||
bool shouldSave = false;
|
bool shouldSave = false;
|
||||||
|
int64_t nowSec = now();
|
||||||
int64_t monoUs = esp_timer_get_time();
|
int64_t monoUs = esp_timer_get_time();
|
||||||
|
|
||||||
if (dirty_ && timeSynced_ && SdCard::instance().isMounted()) {
|
if (dirty_ && nowSec > 1577836800LL && SdCard::instance().isMounted()) {
|
||||||
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
|
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
|
||||||
shouldSave = true;
|
shouldSave = true;
|
||||||
lastSaveMonoUs_ = monoUs;
|
lastSaveMonoUs_ = monoUs;
|
||||||
@ -371,120 +364,8 @@ void HistoryStore::performAutoSave() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
hour_ = value.tm_hour;
|
|
||||||
minute_ = value.tm_min;
|
|
||||||
second_ = value.tm_sec;
|
|
||||||
hasDate_ = true;
|
|
||||||
hasTime_ = true;
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
applyTimeSync();
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t HistoryStore::buildEpoch() const {
|
|
||||||
if (year_ < 1990 || month_ < 1 || month_ > 12 || day_ < 1 || day_ > 31) return -1;
|
|
||||||
|
|
||||||
struct tm combined = {};
|
|
||||||
combined.tm_year = year_ - 1900;
|
|
||||||
combined.tm_mon = month_ - 1;
|
|
||||||
combined.tm_mday = day_;
|
|
||||||
combined.tm_hour = hour_;
|
|
||||||
combined.tm_min = minute_;
|
|
||||||
combined.tm_sec = second_;
|
|
||||||
combined.tm_isdst = -1;
|
|
||||||
|
|
||||||
time_t epoch = mktime(&combined);
|
|
||||||
if (epoch < 0) return -1;
|
|
||||||
return static_cast<int64_t>(epoch);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool HistoryStore::applyTimeSync() {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool wasSynced = timeSynced_;
|
|
||||||
timeSynced_ = true;
|
|
||||||
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;
|
|
||||||
for (size_t i = 0; i < series_.size(); ++i) {
|
|
||||||
const HistorySeries& series = series_[i];
|
|
||||||
if (!series.active) continue;
|
|
||||||
if (series.fine.count > 0 || series.coarse.count > 0 || series.hasLatest) {
|
|
||||||
hasData = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!dataEpoch_ && hasData) {
|
|
||||||
clearAll();
|
|
||||||
}
|
|
||||||
dataEpoch_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
dirty_ = true;
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Time synced: %ld", static_cast<long>(epoch));
|
|
||||||
return !wasSynced;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HistoryStore::clearAll() {
|
void HistoryStore::clearAll() {
|
||||||
// Assumes lock is held by caller
|
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||||
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;
|
||||||
@ -496,17 +377,14 @@ void HistoryStore::clearAll() {
|
|||||||
series.lastFineSampleTs = 0;
|
series.lastFineSampleTs = 0;
|
||||||
series.lastCoarseSampleTs = 0;
|
series.lastCoarseSampleTs = 0;
|
||||||
}
|
}
|
||||||
|
xSemaphoreGive(mutex_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryStore::saveToSdCard() {
|
void HistoryStore::saveToSdCard() {
|
||||||
if (!SdCard::instance().isMounted()) return;
|
if (!SdCard::instance().isMounted()) return;
|
||||||
|
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
// Double check time sync before save
|
||||||
if (!timeSynced_) {
|
if (now() < 1577836800LL) return;
|
||||||
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) {
|
||||||
@ -537,8 +415,6 @@ void HistoryStore::saveToSdCard() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate temp buffer for one series to minimize lock time
|
|
||||||
// Size: Header + Fine + Coarse
|
|
||||||
size_t seriesDataSize = sizeof(HistoryFileSeriesHeader) +
|
size_t seriesDataSize = sizeof(HistoryFileSeriesHeader) +
|
||||||
sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP);
|
sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP);
|
||||||
uint8_t* tempBuf = (uint8_t*)malloc(seriesDataSize);
|
uint8_t* tempBuf = (uint8_t*)malloc(seriesDataSize);
|
||||||
@ -556,7 +432,6 @@ void HistoryStore::saveToSdCard() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy to temp buffer
|
|
||||||
HistoryFileSeriesHeader* sh = (HistoryFileSeriesHeader*)tempBuf;
|
HistoryFileSeriesHeader* sh = (HistoryFileSeriesHeader*)tempBuf;
|
||||||
sh->groupAddr = series.key.addr;
|
sh->groupAddr = series.key.addr;
|
||||||
sh->source = static_cast<uint8_t>(series.key.source);
|
sh->source = static_cast<uint8_t>(series.key.source);
|
||||||
@ -575,7 +450,6 @@ void HistoryStore::saveToSdCard() {
|
|||||||
|
|
||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
|
|
||||||
// Write to file (unlocked)
|
|
||||||
if (fwrite(tempBuf, 1, seriesDataSize, f) != seriesDataSize) {
|
if (fwrite(tempBuf, 1, seriesDataSize, f) != seriesDataSize) {
|
||||||
ESP_LOGW(TAG, "Failed to write series data");
|
ESP_LOGW(TAG, "Failed to write series data");
|
||||||
break;
|
break;
|
||||||
@ -586,7 +460,6 @@ void HistoryStore::saveToSdCard() {
|
|||||||
|
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||||
dirty_ = false;
|
dirty_ = false;
|
||||||
dataEpoch_ = true;
|
|
||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
|
|
||||||
fclose(f);
|
fclose(f);
|
||||||
@ -648,7 +521,6 @@ void HistoryStore::loadFromSdCard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
dirty_ = false;
|
dirty_ = false;
|
||||||
dataEpoch_ = true;
|
|
||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
|
|
||||||
fclose(f);
|
fclose(f);
|
||||||
|
|||||||
@ -22,11 +22,7 @@ public:
|
|||||||
int32_t* outValues, size_t outCount) const;
|
int32_t* outValues, size_t outCount) const;
|
||||||
|
|
||||||
int64_t now() const;
|
int64_t now() const;
|
||||||
bool isTimeSynced() const { return timeSynced_; }
|
bool isTimeSynced() const { return now() > 1577836800LL; }
|
||||||
|
|
||||||
void updateTimeOfDay(const struct tm& value);
|
|
||||||
void updateDate(const struct tm& value);
|
|
||||||
void updateDateTime(const struct tm& value);
|
|
||||||
|
|
||||||
void loadFromSdCard();
|
void loadFromSdCard();
|
||||||
void saveToSdCard();
|
void saveToSdCard();
|
||||||
@ -101,8 +97,6 @@ private:
|
|||||||
static bool keysEqual(const SeriesKey& a, const SeriesKey& b);
|
static bool keysEqual(const SeriesKey& a, const SeriesKey& b);
|
||||||
|
|
||||||
int32_t periodSeconds(ChartPeriod period) const;
|
int32_t periodSeconds(ChartPeriod period) const;
|
||||||
int64_t buildEpoch() const;
|
|
||||||
bool applyTimeSync();
|
|
||||||
void clearAll();
|
void clearAll();
|
||||||
|
|
||||||
HistorySeries* findSeries(uint16_t groupAddr, TextSource source);
|
HistorySeries* findSeries(uint16_t groupAddr, TextSource source);
|
||||||
@ -113,21 +107,8 @@ private:
|
|||||||
std::array<HistorySeries, HISTORY_MAX_SERIES> series_ = {};
|
std::array<HistorySeries, HISTORY_MAX_SERIES> series_ = {};
|
||||||
size_t seriesCount_ = 0;
|
size_t seriesCount_ = 0;
|
||||||
|
|
||||||
bool timeSynced_ = false;
|
|
||||||
bool hasDate_ = false;
|
|
||||||
bool hasTime_ = false;
|
|
||||||
int year_ = 0;
|
|
||||||
int month_ = 0;
|
|
||||||
int day_ = 0;
|
|
||||||
int hour_ = 0;
|
|
||||||
int minute_ = 0;
|
|
||||||
int second_ = 0;
|
|
||||||
int64_t baseEpoch_ = 0;
|
|
||||||
int64_t baseMono_ = 0;
|
|
||||||
|
|
||||||
bool dirty_ = false;
|
bool dirty_ = false;
|
||||||
int64_t lastSaveMonoUs_ = 0;
|
int64_t lastSaveMonoUs_ = 0;
|
||||||
bool dataEpoch_ = false;
|
|
||||||
|
|
||||||
mutable SemaphoreHandle_t mutex_ = nullptr;
|
mutable SemaphoreHandle_t mutex_ = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -64,6 +64,9 @@ enum class TextSource : uint8_t {
|
|||||||
KNX_DPT_TIME = 8, // KNX Time of day (DPT 10.001)
|
KNX_DPT_TIME = 8, // KNX Time of day (DPT 10.001)
|
||||||
KNX_DPT_DATE = 9, // KNX Date (DPT 11.001)
|
KNX_DPT_DATE = 9, // KNX Date (DPT 11.001)
|
||||||
KNX_DPT_DATETIME = 10, // KNX DateTime (DPT 19.001)
|
KNX_DPT_DATETIME = 10, // KNX DateTime (DPT 19.001)
|
||||||
|
SYSTEM_TIME = 11, // System Time (RTC)
|
||||||
|
SYSTEM_DATE = 12, // System Date (RTC)
|
||||||
|
SYSTEM_DATETIME = 13, // System DateTime (RTC)
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TextAlign : uint8_t {
|
enum class TextAlign : uint8_t {
|
||||||
|
|||||||
@ -481,7 +481,7 @@ void WidgetManager::showModalScreenLocked(const ScreenConfig& screen) {
|
|||||||
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
for (uint8_t i = 0; i < screen.widgetCount; i++) {
|
||||||
const WidgetConfig& cfg = screen.widgets[i];
|
const WidgetConfig& cfg = screen.widgets[i];
|
||||||
auto widget = WidgetFactory::create(cfg);
|
auto widget = WidgetFactory::create(cfg);
|
||||||
if (widget && cfg.id < MAX_WIDGETS) {
|
if (widget && cfg.id < 256) {
|
||||||
widget->create(modal);
|
widget->create(modal);
|
||||||
widget->applyStyle();
|
widget->applyStyle();
|
||||||
widgets_[cfg.id] = std::move(widget);
|
widgets_[cfg.id] = std::move(widget);
|
||||||
@ -699,6 +699,8 @@ void WidgetManager::loop() {
|
|||||||
refreshChartWidgets();
|
refreshChartWidgets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSystemTimeWidgets();
|
||||||
|
|
||||||
if (didUiNav) return;
|
if (didUiNav) return;
|
||||||
|
|
||||||
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
|
if (!config_.standbyEnabled || config_.standbyMinutes == 0) return;
|
||||||
@ -753,7 +755,7 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren
|
|||||||
if (cfg.parentId != -1) continue;
|
if (cfg.parentId != -1) continue;
|
||||||
|
|
||||||
auto widget = WidgetFactory::create(cfg);
|
auto widget = WidgetFactory::create(cfg);
|
||||||
if (widget && cfg.id < MAX_WIDGETS) {
|
if (widget && cfg.id < 256) {
|
||||||
widget->create(parent);
|
widget->create(parent);
|
||||||
widget->applyStyle();
|
widget->applyStyle();
|
||||||
widgets_[cfg.id] = std::move(widget);
|
widgets_[cfg.id] = std::move(widget);
|
||||||
@ -773,18 +775,18 @@ void WidgetManager::createAllWidgets(const ScreenConfig& screen, lv_obj_t* paren
|
|||||||
const WidgetConfig& cfg = screen.widgets[i];
|
const WidgetConfig& cfg = screen.widgets[i];
|
||||||
|
|
||||||
// Skip if already created
|
// Skip if already created
|
||||||
if (widgets_[cfg.id]) continue;
|
if (cfg.id < 256 && widgets_[cfg.id]) continue;
|
||||||
|
|
||||||
// Skip if it's a root widget (should be created in Pass 1, but if failed/skipped, ignore)
|
// Skip if it's a root widget (should be created in Pass 1, but if failed/skipped, ignore)
|
||||||
if (cfg.parentId == -1) continue;
|
if (cfg.parentId == -1) continue;
|
||||||
|
|
||||||
// Check if parent exists
|
// Check if parent exists
|
||||||
if (cfg.parentId >= 0 && cfg.parentId < MAX_WIDGETS && widgets_[cfg.parentId]) {
|
if (cfg.parentId >= 0 && cfg.parentId < 256 && widgets_[cfg.parentId]) {
|
||||||
// Parent exists! Get its LVGL object
|
// Parent exists! Get its LVGL object
|
||||||
lv_obj_t* parentObj = widgets_[cfg.parentId]->getObj();
|
lv_obj_t* parentObj = widgets_[cfg.parentId]->getObj();
|
||||||
if (parentObj) {
|
if (parentObj) {
|
||||||
auto widget = WidgetFactory::create(cfg);
|
auto widget = WidgetFactory::create(cfg);
|
||||||
if (widget && cfg.id < MAX_WIDGETS) {
|
if (widget && cfg.id < 256) {
|
||||||
widget->create(parentObj);
|
widget->create(parentObj);
|
||||||
widget->applyStyle();
|
widget->applyStyle();
|
||||||
widgets_[cfg.id] = std::move(widget);
|
widgets_[cfg.id] = std::move(widget);
|
||||||
@ -955,6 +957,30 @@ void WidgetManager::refreshChartWidgets() {
|
|||||||
refreshChartWidgetsLocked();
|
refreshChartWidgetsLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WidgetManager::updateSystemTimeWidgets() {
|
||||||
|
int64_t nowUs = esp_timer_get_time();
|
||||||
|
if (nowUs - lastSystemTimeUpdateUs_ < 500000) { // Update every 500ms
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastSystemTimeUpdateUs_ = nowUs;
|
||||||
|
|
||||||
|
time_t now;
|
||||||
|
time(&now);
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
|
||||||
|
// Call from loop() which is ALREADY LOCKED by LVGL timer
|
||||||
|
for (auto& widget : widgets_) {
|
||||||
|
if (!widget) continue;
|
||||||
|
TextSource src = widget->getTextSource();
|
||||||
|
if (src == TextSource::SYSTEM_TIME ||
|
||||||
|
src == TextSource::SYSTEM_DATE ||
|
||||||
|
src == TextSource::SYSTEM_DATETIME) {
|
||||||
|
widget->onKnxTime(timeinfo, src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) {
|
void WidgetManager::applyKnxValue(uint16_t groupAddr, float value, TextSource source) {
|
||||||
for (auto& widget : widgets_) {
|
for (auto& widget : widgets_) {
|
||||||
if (widget && widget->getKnxAddress() == groupAddr &&
|
if (widget && widget->getKnxAddress() == groupAddr &&
|
||||||
@ -989,26 +1015,40 @@ void WidgetManager::applyKnxText(uint16_t groupAddr, const char* text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WidgetManager::applyKnxTime(uint16_t groupAddr, const struct tm& value, KnxTimeType type) {
|
void WidgetManager::applyKnxTime(uint16_t groupAddr, const struct tm& value, KnxTimeType type) {
|
||||||
bool updated = false;
|
// Simplified system time synchronization
|
||||||
switch (type) {
|
bool isGlobalTime = false;
|
||||||
case KnxTimeType::TIME:
|
if (type == KnxTimeType::TIME && config_.knxTimeAddress != 0 && groupAddr == config_.knxTimeAddress) isGlobalTime = true;
|
||||||
if (config_.knxTimeAddress != 0 && groupAddr == config_.knxTimeAddress) {
|
if (type == KnxTimeType::DATE && config_.knxDateAddress != 0 && groupAddr == config_.knxDateAddress) isGlobalTime = true;
|
||||||
HistoryStore::instance().updateTimeOfDay(value);
|
if (type == KnxTimeType::DATETIME && config_.knxDateTimeAddress != 0 && groupAddr == config_.knxDateTimeAddress) isGlobalTime = true;
|
||||||
updated = true;
|
|
||||||
|
if (isGlobalTime) {
|
||||||
|
time_t now;
|
||||||
|
time(&now);
|
||||||
|
struct tm t_new;
|
||||||
|
localtime_r(&now, &t_new);
|
||||||
|
|
||||||
|
if (type == KnxTimeType::TIME) {
|
||||||
|
t_new.tm_hour = value.tm_hour;
|
||||||
|
t_new.tm_min = value.tm_min;
|
||||||
|
t_new.tm_sec = value.tm_sec;
|
||||||
|
} else if (type == KnxTimeType::DATE) {
|
||||||
|
t_new.tm_year = value.tm_year;
|
||||||
|
if (t_new.tm_year < 1900) t_new.tm_year += 1900;
|
||||||
|
t_new.tm_mon = value.tm_mon;
|
||||||
|
t_new.tm_mday = value.tm_mday;
|
||||||
|
} else if (type == KnxTimeType::DATETIME) {
|
||||||
|
t_new = value;
|
||||||
|
if (t_new.tm_year < 1900) t_new.tm_year += 1900;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case KnxTimeType::DATE:
|
// Final normalization for mktime (expects year - 1900)
|
||||||
if (config_.knxDateAddress != 0 && groupAddr == config_.knxDateAddress) {
|
if (t_new.tm_year >= 1900) t_new.tm_year -= 1900;
|
||||||
HistoryStore::instance().updateDate(value);
|
|
||||||
updated = true;
|
time_t t = mktime(&t_new);
|
||||||
|
if (t != -1) {
|
||||||
|
struct timeval tv = { .tv_sec = t, .tv_usec = 0 };
|
||||||
|
settimeofday(&tv, NULL);
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case KnxTimeType::DATETIME:
|
|
||||||
if (config_.knxDateTimeAddress != 0 && groupAddr == config_.knxDateTimeAddress) {
|
|
||||||
HistoryStore::instance().updateDateTime(value);
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TextSource source = TextSource::STATIC;
|
TextSource source = TextSource::STATIC;
|
||||||
@ -1025,10 +1065,8 @@ void WidgetManager::applyKnxTime(uint16_t groupAddr, const struct tm& value, Knx
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
chartRefreshPending_ = true;
|
chartRefreshPending_ = true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void WidgetManager::cacheKnxValue(uint16_t groupAddr, TextSource source, float value) {
|
void WidgetManager::cacheKnxValue(uint16_t groupAddr, TextSource source, float value) {
|
||||||
if (groupAddr == 0) return;
|
if (groupAddr == 0) return;
|
||||||
|
|||||||
@ -45,6 +45,9 @@ public:
|
|||||||
// User activity (resets standby timer)
|
// User activity (resets standby timer)
|
||||||
void onUserActivity();
|
void onUserActivity();
|
||||||
|
|
||||||
|
// Update widgets displaying system time
|
||||||
|
void updateSystemTimeWidgets();
|
||||||
|
|
||||||
// Thread-safe KNX updates (queued to UI thread)
|
// Thread-safe KNX updates (queued to UI thread)
|
||||||
void onKnxValue(uint16_t groupAddr, float value, TextSource source);
|
void onKnxValue(uint16_t groupAddr, float value, TextSource source);
|
||||||
void onKnxValue(uint16_t groupAddr, float value);
|
void onKnxValue(uint16_t groupAddr, float value);
|
||||||
@ -170,9 +173,10 @@ private:
|
|||||||
uint8_t navTargetScreen_ = 0xFF;
|
uint8_t navTargetScreen_ = 0xFF;
|
||||||
int64_t navRequestUs_ = 0;
|
int64_t navRequestUs_ = 0;
|
||||||
int64_t lastActivityUs_ = 0;
|
int64_t lastActivityUs_ = 0;
|
||||||
|
int64_t lastSystemTimeUpdateUs_ = 0;
|
||||||
|
|
||||||
// Runtime widget instances (indexed by widget ID)
|
// Runtime widget instances (indexed by widget ID)
|
||||||
std::array<std::unique_ptr<Widget>, MAX_WIDGETS> widgets_;
|
std::array<std::unique_ptr<Widget>, 256> widgets_;
|
||||||
lv_obj_t* screen_ = nullptr;
|
lv_obj_t* screen_ = nullptr;
|
||||||
lv_obj_t* modalContainer_ = nullptr;
|
lv_obj_t* modalContainer_ = nullptr;
|
||||||
lv_obj_t* modalDimmer_ = nullptr;
|
lv_obj_t* modalDimmer_ = nullptr;
|
||||||
|
|||||||
@ -174,6 +174,17 @@ lv_obj_t* LabelWidget::create(lv_obj_t* parent) {
|
|||||||
|
|
||||||
if (obj_ != nullptr) {
|
if (obj_ != nullptr) {
|
||||||
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
|
// Initial update for system time widgets to avoid empty display
|
||||||
|
if (config_.textSource == TextSource::SYSTEM_TIME ||
|
||||||
|
config_.textSource == TextSource::SYSTEM_DATE ||
|
||||||
|
config_.textSource == TextSource::SYSTEM_DATETIME) {
|
||||||
|
time_t now;
|
||||||
|
time(&now);
|
||||||
|
struct tm timeinfo;
|
||||||
|
localtime_r(&now, &timeinfo);
|
||||||
|
onKnxTime(timeinfo, config_.textSource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return obj_;
|
return obj_;
|
||||||
}
|
}
|
||||||
@ -260,16 +271,25 @@ void LabelWidget::onKnxTime(const struct tm& value, TextSource source) {
|
|||||||
if (config_.textSource != source) return;
|
if (config_.textSource != source) return;
|
||||||
if (source != TextSource::KNX_DPT_TIME &&
|
if (source != TextSource::KNX_DPT_TIME &&
|
||||||
source != TextSource::KNX_DPT_DATE &&
|
source != TextSource::KNX_DPT_DATE &&
|
||||||
source != TextSource::KNX_DPT_DATETIME) {
|
source != TextSource::KNX_DPT_DATETIME &&
|
||||||
|
source != TextSource::SYSTEM_TIME &&
|
||||||
|
source != TextSource::SYSTEM_DATE &&
|
||||||
|
source != TextSource::SYSTEM_DATETIME) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int year = value.tm_year;
|
int year = value.tm_year;
|
||||||
|
// tm_year is usually year-1900.
|
||||||
|
// If value comes from KNX raw decode, it might be full year depending on logic.
|
||||||
|
// Standard tm_year: 123 for 2023.
|
||||||
|
// Our existing logic handled year < 1900 by adding 1900.
|
||||||
|
// Let's ensure consistent behavior for SYSTEM time which uses standard tm struct.
|
||||||
if (year > 0 && year < 1900) {
|
if (year > 0 && year < 1900) {
|
||||||
year += 1900;
|
year += 1900;
|
||||||
}
|
}
|
||||||
int month = value.tm_mon;
|
int month = value.tm_mon;
|
||||||
if (month < 1 || month > 12) {
|
if (month < 1 || month > 12) {
|
||||||
|
// tm_mon is 0-11
|
||||||
if (month >= 0 && month <= 11) {
|
if (month >= 0 && month <= 11) {
|
||||||
month += 1;
|
month += 1;
|
||||||
}
|
}
|
||||||
@ -277,9 +297,9 @@ void LabelWidget::onKnxTime(const struct tm& value, TextSource source) {
|
|||||||
|
|
||||||
const char* fmt = config_.text;
|
const char* fmt = config_.text;
|
||||||
if (!fmt || fmt[0] == '\0' || strchr(fmt, '%') == nullptr) {
|
if (!fmt || fmt[0] == '\0' || strchr(fmt, '%') == nullptr) {
|
||||||
if (source == TextSource::KNX_DPT_TIME) {
|
if (source == TextSource::KNX_DPT_TIME || source == TextSource::SYSTEM_TIME) {
|
||||||
fmt = "%02d:%02d:%02d";
|
fmt = "%02d:%02d:%02d";
|
||||||
} else if (source == TextSource::KNX_DPT_DATE) {
|
} else if (source == TextSource::KNX_DPT_DATE || source == TextSource::SYSTEM_DATE) {
|
||||||
fmt = "%02d.%02d.%04d";
|
fmt = "%02d.%02d.%04d";
|
||||||
} else {
|
} else {
|
||||||
fmt = "%02d.%02d.%04d %02d:%02d:%02d";
|
fmt = "%02d.%02d.%04d %02d:%02d:%02d";
|
||||||
@ -287,9 +307,9 @@ void LabelWidget::onKnxTime(const struct tm& value, TextSource source) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
char buf[32];
|
char buf[32];
|
||||||
if (source == TextSource::KNX_DPT_TIME) {
|
if (source == TextSource::KNX_DPT_TIME || source == TextSource::SYSTEM_TIME) {
|
||||||
snprintf(buf, sizeof(buf), fmt, value.tm_hour, value.tm_min, value.tm_sec);
|
snprintf(buf, sizeof(buf), fmt, value.tm_hour, value.tm_min, value.tm_sec);
|
||||||
} else if (source == TextSource::KNX_DPT_DATE) {
|
} else if (source == TextSource::KNX_DPT_DATE || source == TextSource::SYSTEM_DATE) {
|
||||||
snprintf(buf, sizeof(buf), fmt, value.tm_mday, month, year);
|
snprintf(buf, sizeof(buf), fmt, value.tm_mday, month, year);
|
||||||
} else {
|
} else {
|
||||||
snprintf(buf, sizeof(buf), fmt, value.tm_mday, month, year,
|
snprintf(buf, sizeof(buf), fmt, value.tm_mday, month, year,
|
||||||
|
|||||||
9
sdcard_content/webseite/assets/index-DYaUUEn9.js
Normal file
9
sdcard_content/webseite/assets/index-DYaUUEn9.js
Normal file
File diff suppressed because one or more lines are too long
9
sdcard_content/webseite/assets/index-DeUfQjDD.js
Normal file
9
sdcard_content/webseite/assets/index-DeUfQjDD.js
Normal file
File diff suppressed because one or more lines are too long
1
sdcard_content/webseite/assets/index-kFitTaMN.css
Normal file
1
sdcard_content/webseite/assets/index-kFitTaMN.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
1
sdcard_content/webseite/vite.svg
Normal file
1
sdcard_content/webseite/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div :class="rowClass"><label :class="labelClass">Format</label><input :class="inputClass" type="text" v-model="w.text"></div>
|
<div :class="rowClass"><label :class="labelClass">Format</label><input :class="inputClass" type="text" v-model="w.text"></div>
|
||||||
<div :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
<div v-if="w.textSrc < 11" :class="rowClass"><label :class="labelClass">KNX Objekt</label>
|
||||||
<select :class="inputClass" v-model.number="w.knxAddr">
|
<select :class="inputClass" v-model.number="w.knxAddr">
|
||||||
<option :value="0">-- Waehlen --</option>
|
<option :value="0">-- Waehlen --</option>
|
||||||
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
<option v-for="addr in store.knxAddresses" :key="`${addr.addr}-${addr.index}`" :value="addr.addr">
|
||||||
|
|||||||
@ -72,11 +72,15 @@ export const textSources = {
|
|||||||
7: 'KNX Dezimalfaktor (DPT 5.005)',
|
7: 'KNX Dezimalfaktor (DPT 5.005)',
|
||||||
8: 'KNX Uhrzeit (DPT 10.001)',
|
8: 'KNX Uhrzeit (DPT 10.001)',
|
||||||
9: 'KNX Datum (DPT 11.001)',
|
9: 'KNX Datum (DPT 11.001)',
|
||||||
10: 'KNX Datum & Uhrzeit (DPT 19.001)'
|
10: 'KNX Datum & Uhrzeit (DPT 19.001)',
|
||||||
|
11: 'System Uhrzeit',
|
||||||
|
12: 'System Datum',
|
||||||
|
13: 'System Datum & Uhrzeit'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const textSourceGroups = [
|
export const textSourceGroups = [
|
||||||
{ label: 'Statisch', values: [0] },
|
{ label: 'Statisch', values: [0] },
|
||||||
|
{ label: 'System', values: [11, 12, 13] },
|
||||||
{ label: 'DPT 1.x', values: [2] },
|
{ label: 'DPT 1.x', values: [2] },
|
||||||
{ label: 'DPT 5.x', values: [3, 7] },
|
{ label: 'DPT 5.x', values: [3, 7] },
|
||||||
{ label: 'DPT 9.x', values: [1] },
|
{ label: 'DPT 9.x', values: [1] },
|
||||||
@ -89,7 +93,7 @@ export const textSourceGroups = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const sourceOptions = {
|
export const sourceOptions = {
|
||||||
label: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
label: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13],
|
||||||
button: [0],
|
button: [0],
|
||||||
led: [0, 2],
|
led: [0, 2],
|
||||||
icon: [0, 2],
|
icon: [0, 2],
|
||||||
@ -126,7 +130,10 @@ export const defaultFormats = {
|
|||||||
7: '%d',
|
7: '%d',
|
||||||
8: '%02d:%02d:%02d',
|
8: '%02d:%02d:%02d',
|
||||||
9: '%02d.%02d.%04d',
|
9: '%02d.%02d.%04d',
|
||||||
10: '%02d.%02d.%04d %02d:%02d:%02d'
|
10: '%02d.%02d.%04d %02d:%02d:%02d',
|
||||||
|
11: '%02d:%02d:%02d',
|
||||||
|
12: '%02d.%02d.%04d',
|
||||||
|
13: '%02d.%02d.%04d %02d:%02d:%02d'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WIDGET_DEFAULTS = {
|
export const WIDGET_DEFAULTS = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user