Backup
This commit is contained in:
parent
bef0d5504b
commit
27613db808
@ -25,7 +25,7 @@ static void widget_manager_timer_cb(lv_timer_t* timer)
|
|||||||
(void)timer;
|
(void)timer;
|
||||||
// Debug: Log every 100th call to verify timer is running
|
// Debug: Log every 100th call to verify timer is running
|
||||||
static uint32_t callCount = 0;
|
static uint32_t callCount = 0;
|
||||||
if (++callCount % 100 == 0) {
|
if (++callCount % 10 == 0) {
|
||||||
ESP_LOGI("Gui", "Timer tick %lu", callCount);
|
ESP_LOGI("Gui", "Timer tick %lu", callCount);
|
||||||
}
|
}
|
||||||
WidgetManager::instance().loop();
|
WidgetManager::instance().loop();
|
||||||
@ -40,7 +40,7 @@ void Gui::create()
|
|||||||
// TEMP: Disabled long press handler for testing
|
// TEMP: Disabled long press handler for testing
|
||||||
// lv_obj_add_event_cb(lv_scr_act(), screen_long_press_handler,
|
// lv_obj_add_event_cb(lv_scr_act(), screen_long_press_handler,
|
||||||
// LV_EVENT_LONG_PRESSED, NULL);
|
// LV_EVENT_LONG_PRESSED, NULL);
|
||||||
lv_timer_create(widget_manager_timer_cb, 10, nullptr);
|
lv_timer_create(widget_manager_timer_cb, 50, nullptr);
|
||||||
|
|
||||||
esp_lv_adapter_unlock();
|
esp_lv_adapter_unlock();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,43 +1,14 @@
|
|||||||
#include "HistoryStore.hpp"
|
#include "HistoryStore.hpp"
|
||||||
#include "SdCard.hpp"
|
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_timer.h"
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <ctime>
|
||||||
#include <sys/time.h>
|
#include <cmath>
|
||||||
|
|
||||||
static const char* TAG = "HistoryStore";
|
static const char* TAG = "HistoryStore";
|
||||||
static constexpr uint32_t HISTORY_MAGIC = 0x4B584831; // KXH1
|
|
||||||
static constexpr uint16_t HISTORY_VERSION = 1;
|
|
||||||
static constexpr const char* HISTORY_FILE = "/sdcard/knx_history.bin";
|
|
||||||
static constexpr int64_t HISTORY_SAVE_INTERVAL_US = 300000000; // 5 minutes
|
|
||||||
|
|
||||||
#pragma pack(push, 1)
|
// HistoryStore is effectively disabled to save resources and prevent hangs.
|
||||||
struct HistoryFileHeader {
|
// It now acts as a simple pass-through for the latest value (for real-time chart indication)
|
||||||
uint32_t magic;
|
// but does not record history or write to SD card.
|
||||||
uint16_t version;
|
|
||||||
uint16_t seriesCount;
|
|
||||||
uint16_t fineCapacity;
|
|
||||||
uint16_t coarseCapacity;
|
|
||||||
uint32_t fineInterval;
|
|
||||||
uint32_t coarseInterval;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct HistoryFileSeriesHeader {
|
|
||||||
uint16_t groupAddr;
|
|
||||||
uint8_t source;
|
|
||||||
uint8_t reserved;
|
|
||||||
uint16_t fineCount;
|
|
||||||
uint16_t fineHead;
|
|
||||||
uint16_t coarseCount;
|
|
||||||
uint16_t coarseHead;
|
|
||||||
uint8_t hasLatest;
|
|
||||||
uint8_t reserved2[3];
|
|
||||||
int32_t latestTs;
|
|
||||||
float latestValue;
|
|
||||||
};
|
|
||||||
#pragma pack(pop)
|
|
||||||
|
|
||||||
HistoryStore& HistoryStore::instance() {
|
HistoryStore& HistoryStore::instance() {
|
||||||
static HistoryStore inst;
|
static HistoryStore inst;
|
||||||
@ -115,10 +86,7 @@ void HistoryStore::configureFromConfig(const GuiConfig& config) {
|
|||||||
for (size_t i = 0; i < neededCount; ++i) {
|
for (size_t i = 0; i < neededCount; ++i) {
|
||||||
if (keysEqual(needed[i], key)) return;
|
if (keysEqual(needed[i], key)) return;
|
||||||
}
|
}
|
||||||
if (neededCount >= HISTORY_MAX_SERIES) {
|
if (neededCount >= HISTORY_MAX_SERIES) return;
|
||||||
ESP_LOGW(TAG, "History series limit reached");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
needed[neededCount++] = key;
|
needed[neededCount++] = key;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -136,69 +104,32 @@ void HistoryStore::configureFromConfig(const GuiConfig& config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::array<bool, HISTORY_MAX_SERIES> keep = {};
|
std::array<bool, HISTORY_MAX_SERIES> keep = {};
|
||||||
bool changed = false;
|
|
||||||
size_t activeCount = 0;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < neededCount; ++i) {
|
for (size_t i = 0; i < neededCount; ++i) {
|
||||||
const SeriesKey& key = needed[i];
|
const SeriesKey& key = needed[i];
|
||||||
HistorySeries* existing = findSeriesByKey(key);
|
HistorySeries* existing = findSeriesByKey(key);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
size_t idx = static_cast<size_t>(existing - series_.data());
|
size_t idx = static_cast<size_t>(existing - series_.data());
|
||||||
if (!existing->active) {
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
keep[idx] = true;
|
keep[idx] = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Find empty slot
|
||||||
HistorySeries* slot = nullptr;
|
HistorySeries* slot = nullptr;
|
||||||
for (size_t si = 0; si < series_.size(); ++si) {
|
for (size_t si = 0; si < series_.size(); ++si) {
|
||||||
if (!keep[si] && !series_[si].active && series_[si].key.addr == 0) {
|
if (!keep[si] && !series_[si].active) {
|
||||||
slot = &series_[si];
|
slot = &series_[si];
|
||||||
keep[si] = true;
|
keep[si] = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!slot) {
|
if (slot) {
|
||||||
for (size_t si = 0; si < series_.size(); ++si) {
|
slot->key = key;
|
||||||
if (!keep[si] && !series_[si].active) {
|
slot->active = true;
|
||||||
slot = &series_[si];
|
slot->hasLatest = false;
|
||||||
keep[si] = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!slot) {
|
|
||||||
ESP_LOGW(TAG, "History series limit reached");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
slot->key = key;
|
|
||||||
slot->fine.clear();
|
|
||||||
slot->coarse.clear();
|
|
||||||
slot->hasLatest = false;
|
|
||||||
slot->latestValue = 0.0f;
|
|
||||||
slot->latestTs = 0;
|
|
||||||
slot->lastFineSampleTs = 0;
|
|
||||||
slot->lastCoarseSampleTs = 0;
|
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (size_t i = 0; i < series_.size(); ++i) {
|
for (size_t i = 0; i < series_.size(); ++i) {
|
||||||
bool nextActive = keep[i];
|
series_[i].active = keep[i];
|
||||||
if (series_[i].active != nextActive) {
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
series_[i].active = nextActive;
|
|
||||||
if (nextActive) {
|
|
||||||
activeCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seriesCount_ = activeCount;
|
|
||||||
if (changed) {
|
|
||||||
dirty_ = true;
|
|
||||||
}
|
}
|
||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
}
|
}
|
||||||
@ -217,10 +148,7 @@ bool HistoryStore::updateLatest(uint16_t groupAddr, TextSource source, float val
|
|||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int64_t nowSec = now();
|
|
||||||
series->latestValue = value;
|
series->latestValue = value;
|
||||||
series->latestTs = static_cast<int32_t>(nowSec);
|
|
||||||
series->hasLatest = true;
|
series->hasLatest = true;
|
||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
return true;
|
return true;
|
||||||
@ -231,298 +159,56 @@ int64_t HistoryStore::now() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int32_t HistoryStore::periodSeconds(ChartPeriod period) const {
|
int32_t HistoryStore::periodSeconds(ChartPeriod period) const {
|
||||||
switch (period) {
|
return 3600; // Dummy
|
||||||
case ChartPeriod::HOUR_1: return 3600;
|
|
||||||
case ChartPeriod::HOUR_3: return 3 * 3600;
|
|
||||||
case ChartPeriod::HOUR_5: return 5 * 3600;
|
|
||||||
case ChartPeriod::HOUR_12: return 12 * 3600;
|
|
||||||
case ChartPeriod::HOUR_24: return 24 * 3600;
|
|
||||||
case ChartPeriod::MONTH_1: return HISTORY_MONTH_SECONDS;
|
|
||||||
default: return 3600;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartPeriod period,
|
bool HistoryStore::fillChartSeries(uint16_t groupAddr, TextSource source, ChartPeriod period,
|
||||||
int32_t* outValues, size_t outCount) const {
|
int32_t* outValues, size_t outCount) const {
|
||||||
if (!outValues || outCount == 0) return false;
|
if (!outValues || outCount == 0) return false;
|
||||||
if (outCount > CHART_POINT_COUNT) outCount = CHART_POINT_COUNT;
|
|
||||||
for (size_t i = 0; i < outCount; ++i) {
|
|
||||||
outValues[i] = NO_POINT;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Just return the latest value repeated, effectively a flat line
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||||
const HistorySeries* series = findSeries(groupAddr, source);
|
const HistorySeries* series = findSeries(groupAddr, source);
|
||||||
if (!series) {
|
int32_t val = NO_POINT;
|
||||||
xSemaphoreGive(mutex_);
|
if (series && series->hasLatest) {
|
||||||
return false;
|
val = static_cast<int32_t>(lrintf(series->latestValue));
|
||||||
}
|
|
||||||
|
|
||||||
int64_t nowSec = now();
|
|
||||||
int32_t window = periodSeconds(period);
|
|
||||||
if (window <= 0) {
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t start = nowSec - window;
|
|
||||||
|
|
||||||
std::array<float, CHART_POINT_COUNT> sums = {};
|
|
||||||
std::array<uint16_t, CHART_POINT_COUNT> counts = {};
|
|
||||||
|
|
||||||
auto accumulate = [&](const HistoryPoint& p) {
|
|
||||||
if (p.ts < start || p.ts > nowSec) return;
|
|
||||||
size_t bucket = static_cast<size_t>(((p.ts - start) * outCount) / window);
|
|
||||||
if (bucket >= outCount) bucket = outCount - 1;
|
|
||||||
sums[bucket] += p.value;
|
|
||||||
counts[bucket]++;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (period == ChartPeriod::MONTH_1) {
|
|
||||||
series->coarse.forEach(accumulate);
|
|
||||||
} else {
|
|
||||||
series->fine.forEach(accumulate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (series->hasLatest) {
|
|
||||||
HistoryPoint latest{series->latestTs, series->latestValue};
|
|
||||||
accumulate(latest);
|
|
||||||
}
|
}
|
||||||
xSemaphoreGive(mutex_);
|
xSemaphoreGive(mutex_);
|
||||||
|
|
||||||
bool hasData = false;
|
|
||||||
int32_t lastValidValue = NO_POINT;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < outCount; ++i) {
|
for (size_t i = 0; i < outCount; ++i) {
|
||||||
if (counts[i] > 0) {
|
outValues[i] = val;
|
||||||
float avg = sums[i] / counts[i];
|
|
||||||
int32_t val = static_cast<int32_t>(lrintf(avg));
|
|
||||||
outValues[i] = val;
|
|
||||||
lastValidValue = val;
|
|
||||||
hasData = true;
|
|
||||||
} else {
|
|
||||||
outValues[i] = lastValidValue;
|
|
||||||
if (lastValidValue != NO_POINT) {
|
|
||||||
hasData = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return (val != NO_POINT);
|
||||||
return hasData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HistoryStore::tick() {
|
bool HistoryStore::tick() {
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
return false; // Disabled
|
||||||
int64_t nowSec = now();
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
HistorySeries& series = series_[i];
|
|
||||||
if (!series.active || !series.hasLatest) continue;
|
|
||||||
|
|
||||||
if (series.fine.count == 0 || nowSec - series.lastFineSampleTs >= HISTORY_FINE_INTERVAL) {
|
|
||||||
series.fine.push({static_cast<int32_t>(nowSec), series.latestValue});
|
|
||||||
series.lastFineSampleTs = static_cast<int32_t>(nowSec);
|
|
||||||
added = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (series.coarse.count == 0 || nowSec - series.lastCoarseSampleTs >= HISTORY_COARSE_INTERVAL) {
|
|
||||||
series.coarse.push({static_cast<int32_t>(nowSec), series.latestValue});
|
|
||||||
series.lastCoarseSampleTs = static_cast<int32_t>(nowSec);
|
|
||||||
added = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (added) {
|
|
||||||
dirty_ = true;
|
|
||||||
}
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
return added;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryStore::performAutoSave() {
|
void HistoryStore::performAutoSave() {
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
// Disabled
|
||||||
bool shouldSave = false;
|
}
|
||||||
int64_t nowSec = now();
|
|
||||||
int64_t monoUs = esp_timer_get_time();
|
|
||||||
|
|
||||||
if (dirty_ && nowSec > 1577836800LL && SdCard::instance().isMounted()) {
|
void HistoryStore::updateTimeOfDay(const tm& value) {
|
||||||
if (monoUs - lastSaveMonoUs_ >= HISTORY_SAVE_INTERVAL_US) {
|
// Disabled
|
||||||
shouldSave = true;
|
}
|
||||||
lastSaveMonoUs_ = monoUs;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
|
|
||||||
if (shouldSave) {
|
void HistoryStore::updateDate(const tm& value) {
|
||||||
saveToSdCard();
|
// Disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HistoryStore::updateDateTime(const tm& value) {
|
||||||
|
// Disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryStore::clearAll() {
|
void HistoryStore::clearAll() {
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
// Disabled
|
||||||
for (size_t i = 0; i < series_.size(); ++i) {
|
|
||||||
HistorySeries& series = series_[i];
|
|
||||||
if (!series.active) continue;
|
|
||||||
series.fine.clear();
|
|
||||||
series.coarse.clear();
|
|
||||||
series.hasLatest = false;
|
|
||||||
series.latestValue = 0.0f;
|
|
||||||
series.latestTs = 0;
|
|
||||||
series.lastFineSampleTs = 0;
|
|
||||||
series.lastCoarseSampleTs = 0;
|
|
||||||
}
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryStore::saveToSdCard() {
|
void HistoryStore::saveToSdCard() {
|
||||||
if (!SdCard::instance().isMounted()) return;
|
// Disabled
|
||||||
|
|
||||||
// Double check time sync before save
|
|
||||||
if (now() < 1577836800LL) return;
|
|
||||||
|
|
||||||
FILE* f = fopen(HISTORY_FILE, "wb");
|
|
||||||
if (!f) {
|
|
||||||
ESP_LOGW(TAG, "Failed to open history file for writing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
|
||||||
HistoryFileHeader header = {};
|
|
||||||
header.magic = HISTORY_MAGIC;
|
|
||||||
header.version = HISTORY_VERSION;
|
|
||||||
uint16_t activeCount = 0;
|
|
||||||
for (const auto& series : series_) {
|
|
||||||
if (series.active) {
|
|
||||||
activeCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
header.seriesCount = activeCount;
|
|
||||||
header.fineCapacity = HISTORY_FINE_CAP;
|
|
||||||
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);
|
|
||||||
ESP_LOGW(TAG, "Failed to write history header");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_);
|
|
||||||
|
|
||||||
if (fwrite(tempBuf, 1, seriesDataSize, f) != seriesDataSize) {
|
|
||||||
ESP_LOGW(TAG, "Failed to write series data");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(tempBuf);
|
|
||||||
|
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
|
||||||
dirty_ = false;
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
ESP_LOGI(TAG, "History saved (%d series)", static_cast<int>(activeCount));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void HistoryStore::loadFromSdCard() {
|
void HistoryStore::loadFromSdCard() {
|
||||||
if (!SdCard::instance().isMounted()) return;
|
// Disabled
|
||||||
|
|
||||||
FILE* f = fopen(HISTORY_FILE, "rb");
|
|
||||||
if (!f) return;
|
|
||||||
|
|
||||||
HistoryFileHeader header = {};
|
|
||||||
if (fread(&header, sizeof(header), 1, f) != 1) {
|
|
||||||
fclose(f);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (header.magic != HISTORY_MAGIC || header.version != HISTORY_VERSION) {
|
|
||||||
fclose(f);
|
|
||||||
ESP_LOGW(TAG, "History header mismatch");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (header.fineCapacity != HISTORY_FINE_CAP ||
|
|
||||||
header.coarseCapacity != HISTORY_COARSE_CAP ||
|
|
||||||
header.fineInterval != HISTORY_FINE_INTERVAL ||
|
|
||||||
header.coarseInterval != HISTORY_COARSE_INTERVAL) {
|
|
||||||
fclose(f);
|
|
||||||
ESP_LOGW(TAG, "History config mismatch");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
|
||||||
for (uint16_t i = 0; i < header.seriesCount; ++i) {
|
|
||||||
HistoryFileSeriesHeader sh = {};
|
|
||||||
if (fread(&sh, sizeof(sh), 1, f) != 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
HistorySeries* series = findSeries(sh.groupAddr, static_cast<TextSource>(sh.source));
|
|
||||||
if (series) {
|
|
||||||
series->fine.count = sh.fineCount > HISTORY_FINE_CAP ? HISTORY_FINE_CAP : sh.fineCount;
|
|
||||||
series->fine.head = sh.fineHead >= HISTORY_FINE_CAP ? 0 : sh.fineHead;
|
|
||||||
series->coarse.count = sh.coarseCount > HISTORY_COARSE_CAP ? HISTORY_COARSE_CAP : sh.coarseCount;
|
|
||||||
series->coarse.head = sh.coarseHead >= HISTORY_COARSE_CAP ? 0 : sh.coarseHead;
|
|
||||||
series->hasLatest = sh.hasLatest != 0;
|
|
||||||
series->latestTs = sh.latestTs;
|
|
||||||
series->latestValue = sh.latestValue;
|
|
||||||
|
|
||||||
if (fread(series->fine.points.data(), sizeof(HistoryPoint), HISTORY_FINE_CAP, f) != HISTORY_FINE_CAP) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (fread(series->coarse.points.data(), sizeof(HistoryPoint), HISTORY_COARSE_CAP, f) != HISTORY_COARSE_CAP) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fseek(f, sizeof(HistoryPoint) * (HISTORY_FINE_CAP + HISTORY_COARSE_CAP), SEEK_CUR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dirty_ = false;
|
|
||||||
xSemaphoreGive(mutex_);
|
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
ESP_LOGI(TAG, "History loaded");
|
|
||||||
}
|
}
|
||||||
@ -24,6 +24,11 @@ public:
|
|||||||
int64_t now() const;
|
int64_t now() const;
|
||||||
bool isTimeSynced() const { return now() > 1577836800LL; }
|
bool isTimeSynced() const { return now() > 1577836800LL; }
|
||||||
|
|
||||||
|
void updateTimeOfDay(const tm& value);
|
||||||
|
void updateDate(const tm& value);
|
||||||
|
void updateDateTime(const tm& value);
|
||||||
|
void clearAll();
|
||||||
|
|
||||||
void loadFromSdCard();
|
void loadFromSdCard();
|
||||||
void saveToSdCard();
|
void saveToSdCard();
|
||||||
|
|
||||||
@ -74,9 +79,9 @@ private:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static constexpr size_t HISTORY_MAX_SERIES = 12;
|
static constexpr size_t HISTORY_MAX_SERIES = 4;
|
||||||
static constexpr size_t HISTORY_FINE_CAP = 720;
|
static constexpr size_t HISTORY_FINE_CAP = 360;
|
||||||
static constexpr size_t HISTORY_COARSE_CAP = 720;
|
static constexpr size_t HISTORY_COARSE_CAP = 360;
|
||||||
static constexpr int32_t HISTORY_FINE_INTERVAL = 120;
|
static constexpr int32_t HISTORY_FINE_INTERVAL = 120;
|
||||||
static constexpr int32_t HISTORY_COARSE_INTERVAL = 3600;
|
static constexpr int32_t HISTORY_COARSE_INTERVAL = 3600;
|
||||||
static constexpr int32_t HISTORY_MONTH_SECONDS = 30 * 24 * 3600;
|
static constexpr int32_t HISTORY_MONTH_SECONDS = 30 * 24 * 3600;
|
||||||
@ -97,7 +102,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;
|
||||||
void clearAll();
|
|
||||||
|
|
||||||
HistorySeries* findSeries(uint16_t groupAddr, TextSource source);
|
HistorySeries* findSeries(uint16_t groupAddr, TextSource source);
|
||||||
const HistorySeries* findSeries(uint16_t groupAddr, TextSource source) const;
|
const HistorySeries* findSeries(uint16_t groupAddr, TextSource source) const;
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
// Maximum number of widgets
|
// Maximum number of widgets
|
||||||
static constexpr size_t MAX_WIDGETS = 32;
|
static constexpr size_t MAX_WIDGETS = 64;
|
||||||
static constexpr size_t MAX_SCREENS = 8;
|
static constexpr size_t MAX_SCREENS = 8;
|
||||||
static constexpr size_t MAX_TEXT_LEN = 32;
|
static constexpr size_t MAX_TEXT_LEN = 32;
|
||||||
static constexpr size_t MAX_SCREEN_NAME_LEN = 24;
|
static constexpr size_t MAX_SCREEN_NAME_LEN = 24;
|
||||||
|
|||||||
@ -347,25 +347,29 @@ void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lv_display_t* disp = lv_display_get_default();
|
lv_display_t* disp = lv_display_get_default();
|
||||||
bool invEnabled = true;
|
|
||||||
if (disp) {
|
if (disp) {
|
||||||
invEnabled = lv_display_is_invalidation_enabled(disp);
|
|
||||||
lv_display_enable_invalidation(disp, false);
|
lv_display_enable_invalidation(disp, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset all input devices BEFORE destroying widgets
|
// Reset all input devices
|
||||||
ESP_LOGI(TAG, "Resetting input devices...");
|
|
||||||
lv_indev_t* indev = lv_indev_get_next(nullptr);
|
lv_indev_t* indev = lv_indev_get_next(nullptr);
|
||||||
while (indev) {
|
while (indev) {
|
||||||
lv_indev_reset(indev, nullptr);
|
lv_indev_reset(indev, nullptr);
|
||||||
indev = lv_indev_get_next(indev);
|
indev = lv_indev_get_next(indev);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now destroy C++ widgets (which deletes LVGL objects)
|
// SAFE DESTRUCTION:
|
||||||
ESP_LOGI(TAG, "Destroying widgets...");
|
// 1. Mark all C++ widgets as "LVGL object already gone"
|
||||||
destroyAllWidgets();
|
for (auto& widget : widgets_) {
|
||||||
|
if (widget) widget->clearLvglObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Delete all LVGL objects on layers we use
|
||||||
lv_obj_clean(lv_scr_act());
|
lv_obj_clean(lv_scr_act());
|
||||||
ESP_LOGI(TAG, "Widgets destroyed");
|
lv_obj_clean(lv_layer_top());
|
||||||
|
|
||||||
|
// 3. Now destroy C++ objects (their destructors won't call lv_obj_delete)
|
||||||
|
destroyAllWidgets();
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...",
|
ESP_LOGI(TAG, "Creating new widgets for screen '%s' (%d widgets)...",
|
||||||
screen->name, screen->widgetCount);
|
screen->name, screen->widgetCount);
|
||||||
@ -373,13 +377,9 @@ void WidgetManager::applyScreenLocked(uint8_t screenId) {
|
|||||||
createAllWidgets(*screen, root);
|
createAllWidgets(*screen, root);
|
||||||
ESP_LOGI(TAG, "Widgets created");
|
ESP_LOGI(TAG, "Widgets created");
|
||||||
applyCachedValuesToWidgets();
|
applyCachedValuesToWidgets();
|
||||||
#if LV_USE_OBJ_NAME
|
|
||||||
ESP_LOGI(TAG, "Flex containers for screen '%s':", screen->name);
|
|
||||||
dump_flex_objects(root, 0);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (disp) {
|
if (disp) {
|
||||||
lv_display_enable_invalidation(disp, invEnabled);
|
lv_display_enable_invalidation(disp, true);
|
||||||
}
|
}
|
||||||
lv_obj_invalidate(lv_scr_act());
|
lv_obj_invalidate(lv_scr_act());
|
||||||
|
|
||||||
@ -404,6 +404,16 @@ void WidgetManager::showModalScreenLocked(const ScreenConfig& screen) {
|
|||||||
indev = lv_indev_get_next(indev);
|
indev = lv_indev_get_next(indev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SAFE DESTRUCTION
|
||||||
|
for (auto& widget : widgets_) {
|
||||||
|
if (widget) widget->clearLvglObject();
|
||||||
|
}
|
||||||
|
lv_obj_clean(lv_scr_act()); // Should we clean screen when showing modal?
|
||||||
|
// Actually, usually modal is ON TOP of screen.
|
||||||
|
// But our current WidgetManager destroys screen widgets when showing modal!
|
||||||
|
// That's why we clean.
|
||||||
|
lv_obj_clean(lv_layer_top());
|
||||||
|
|
||||||
destroyAllWidgets();
|
destroyAllWidgets();
|
||||||
|
|
||||||
lv_disp_t* disp = lv_disp_get_default();
|
lv_disp_t* disp = lv_disp_get_default();
|
||||||
@ -513,22 +523,18 @@ void WidgetManager::closeModalLocked() {
|
|||||||
indev = lv_indev_get_next(indev);
|
indev = lv_indev_get_next(indev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SAFE DESTRUCTION
|
||||||
|
for (auto& widget : widgets_) {
|
||||||
|
if (widget) widget->clearLvglObject();
|
||||||
|
}
|
||||||
|
lv_obj_clean(lv_layer_top());
|
||||||
|
|
||||||
// Destroy widgets first
|
// Destroy widgets first
|
||||||
printf("WM: closeModal destroying widgets...\n");
|
printf("WM: closeModal destroying widgets...\n");
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
destroyAllWidgets();
|
destroyAllWidgets();
|
||||||
|
|
||||||
if (modalDimmer_) {
|
modalDimmer_ = nullptr;
|
||||||
if (lv_obj_is_valid(modalDimmer_)) lv_obj_delete(modalDimmer_);
|
|
||||||
modalDimmer_ = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lv_obj_is_valid(modalContainer_)) {
|
|
||||||
lv_obj_delete(modalContainer_);
|
|
||||||
} else {
|
|
||||||
printf("WM: Warning: modalContainer_ was invalid!\n");
|
|
||||||
fflush(stdout);
|
|
||||||
}
|
|
||||||
modalContainer_ = nullptr;
|
modalContainer_ = nullptr;
|
||||||
modalScreenId_ = SCREEN_ID_NONE;
|
modalScreenId_ = SCREEN_ID_NONE;
|
||||||
|
|
||||||
|
|||||||
@ -40,8 +40,8 @@ public:
|
|||||||
|
|
||||||
// Initialize LVGL adapter
|
// Initialize LVGL adapter
|
||||||
esp_lv_adapter_config_t cfg = ESP_LV_ADAPTER_DEFAULT_CONFIG();
|
esp_lv_adapter_config_t cfg = ESP_LV_ADAPTER_DEFAULT_CONFIG();
|
||||||
cfg.stack_in_psram = true;
|
cfg.stack_in_psram = false; // Use internal RAM for stack
|
||||||
cfg.task_stack_size = 16 * 1024;
|
cfg.task_stack_size = 32 * 1024;
|
||||||
ESP_ERROR_CHECK(esp_lv_adapter_init(&cfg));
|
ESP_ERROR_CHECK(esp_lv_adapter_init(&cfg));
|
||||||
|
|
||||||
// Register display
|
// Register display
|
||||||
|
|||||||
@ -123,9 +123,8 @@ void ButtonWidget::setupFlexLayout() {
|
|||||||
|
|
||||||
void ButtonWidget::applyTextAlignment() {
|
void ButtonWidget::applyTextAlignment() {
|
||||||
if (label_ == nullptr) return;
|
if (label_ == nullptr) return;
|
||||||
lv_obj_set_width(label_, LV_PCT(100));
|
|
||||||
lv_obj_set_style_text_align(label_, toLvTextAlign(config_.textAlign), 0);
|
lv_obj_set_style_text_align(label_, toLvTextAlign(config_.textAlign), 0);
|
||||||
lv_obj_align(label_, LV_ALIGN_CENTER, 0, 0);
|
lv_obj_center(label_);
|
||||||
}
|
}
|
||||||
|
|
||||||
lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
||||||
@ -137,23 +136,23 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
|||||||
|
|
||||||
lv_obj_add_event_cb(obj_, clickCallback, LV_EVENT_CLICKED, this);
|
lv_obj_add_event_cb(obj_, clickCallback, LV_EVENT_CLICKED, this);
|
||||||
|
|
||||||
bool hasIcon = !config_.isContainer &&
|
if (config_.isContainer) {
|
||||||
config_.iconCodepoint > 0 && Fonts::hasIconFont();
|
ESP_LOGI(TAG, "Created container button %d", config_.id);
|
||||||
|
return obj_;
|
||||||
|
}
|
||||||
|
|
||||||
if (!config_.isContainer) {
|
bool hasIcon = config_.iconCodepoint > 0 && Fonts::hasIconFont();
|
||||||
if (hasIcon) {
|
|
||||||
// Create container for flex layout
|
if (hasIcon) {
|
||||||
contentContainer_ = lv_obj_create(obj_);
|
// Create container for flex layout
|
||||||
if (contentContainer_ == nullptr) {
|
contentContainer_ = lv_obj_create(obj_);
|
||||||
return obj_; // Continue without icon container
|
if (contentContainer_) {
|
||||||
}
|
|
||||||
set_obj_name(contentContainer_, "Button", config_.id, "content");
|
set_obj_name(contentContainer_, "Button", config_.id, "content");
|
||||||
lv_obj_remove_style_all(contentContainer_);
|
lv_obj_remove_style_all(contentContainer_);
|
||||||
lv_obj_set_size(contentContainer_, LV_PCT(100), LV_PCT(100));
|
lv_obj_set_size(contentContainer_, LV_PCT(100), LV_PCT(100));
|
||||||
lv_obj_center(contentContainer_);
|
lv_obj_center(contentContainer_);
|
||||||
lv_obj_clear_flag(contentContainer_, LV_OBJ_FLAG_CLICKABLE); // Pass clicks to parent
|
lv_obj_clear_flag(contentContainer_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
// Create icon label
|
|
||||||
bool iconFirst = (config_.iconPosition == static_cast<uint8_t>(IconPosition::LEFT) ||
|
bool iconFirst = (config_.iconPosition == static_cast<uint8_t>(IconPosition::LEFT) ||
|
||||||
config_.iconPosition == static_cast<uint8_t>(IconPosition::TOP));
|
config_.iconPosition == static_cast<uint8_t>(IconPosition::TOP));
|
||||||
|
|
||||||
@ -166,7 +165,6 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
|||||||
lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE);
|
lv_obj_clear_flag(iconLabel_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create text label
|
|
||||||
label_ = lv_label_create(contentContainer_);
|
label_ = lv_label_create(contentContainer_);
|
||||||
set_obj_name(label_, "Button", config_.id, "text");
|
set_obj_name(label_, "Button", config_.id, "text");
|
||||||
lv_label_set_text(label_, config_.text);
|
lv_label_set_text(label_, config_.text);
|
||||||
@ -182,14 +180,16 @@ lv_obj_t* ButtonWidget::create(lv_obj_t* parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupFlexLayout();
|
setupFlexLayout();
|
||||||
} else {
|
|
||||||
// Simple button without icon
|
|
||||||
label_ = lv_label_create(obj_);
|
|
||||||
set_obj_name(label_, "Button", config_.id, "text");
|
|
||||||
lv_label_set_text(label_, config_.text);
|
|
||||||
applyTextAlignment();
|
|
||||||
lv_obj_clear_flag(label_, LV_OBJ_FLAG_CLICKABLE);
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Simple button without icon
|
||||||
|
label_ = lv_label_create(obj_);
|
||||||
|
set_obj_name(label_, "Button", config_.id, "text");
|
||||||
|
lv_label_set_text(label_, config_.text);
|
||||||
|
lv_label_set_long_mode(label_, LV_LABEL_LONG_WRAP);
|
||||||
|
lv_obj_set_width(label_, LV_SIZE_CONTENT);
|
||||||
|
applyTextAlignment();
|
||||||
|
lv_obj_clear_flag(label_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "Created button '%s' at %d,%d (icon: 0x%lx)",
|
ESP_LOGI(TAG, "Created button '%s' at %d,%d (icon: 0x%lx)",
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
#include "ClockWidget.hpp"
|
#include "ClockWidget.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <initializer_list>
|
||||||
#ifndef PI
|
|
||||||
#define PI 3.14159265358979323846f
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ClockWidget::ClockWidget(const WidgetConfig& config)
|
ClockWidget::ClockWidget(const WidgetConfig& config)
|
||||||
: Widget(config)
|
: Widget(config)
|
||||||
@ -20,20 +17,31 @@ lv_obj_t* ClockWidget::create(lv_obj_t* parent) {
|
|||||||
config_.width > 0 ? config_.width : 200,
|
config_.width > 0 ? config_.width : 200,
|
||||||
config_.height > 0 ? config_.height : 200);
|
config_.height > 0 ? config_.height : 200);
|
||||||
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_clear_flag(obj_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
|
||||||
|
// Create hands as rectangles
|
||||||
|
auto createHand = [&](lv_obj_t*& hand, const char* name) {
|
||||||
|
hand = lv_obj_create(obj_);
|
||||||
|
lv_obj_remove_style_all(hand);
|
||||||
|
lv_obj_add_flag(hand, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||||
|
lv_obj_clear_flag(hand, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
// Set pivot to bottom center (will be set in applyStyle implicitly by size?)
|
||||||
|
// We will set alignment there.
|
||||||
|
};
|
||||||
|
|
||||||
|
createHand(hourHand_, "Hour");
|
||||||
|
createHand(minuteHand_, "Minute");
|
||||||
|
createHand(secondHand_, "Second");
|
||||||
|
|
||||||
// Create hands
|
|
||||||
hourHand_ = lv_line_create(obj_);
|
|
||||||
minuteHand_ = lv_line_create(obj_);
|
|
||||||
secondHand_ = lv_line_create(obj_);
|
|
||||||
centerDot_ = lv_obj_create(obj_);
|
centerDot_ = lv_obj_create(obj_);
|
||||||
|
|
||||||
// Style center dot
|
|
||||||
lv_obj_remove_style_all(centerDot_);
|
lv_obj_remove_style_all(centerDot_);
|
||||||
lv_obj_set_size(centerDot_, 8, 8);
|
lv_obj_add_flag(centerDot_, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||||
|
lv_obj_clear_flag(centerDot_, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_set_size(centerDot_, 12, 12);
|
||||||
lv_obj_set_style_radius(centerDot_, LV_RADIUS_CIRCLE, 0);
|
lv_obj_set_style_radius(centerDot_, LV_RADIUS_CIRCLE, 0);
|
||||||
lv_obj_center(centerDot_);
|
lv_obj_center(centerDot_);
|
||||||
|
|
||||||
// Initial update to set positions (will be updated by loop)
|
// Initial update
|
||||||
time_t now;
|
time_t now;
|
||||||
time(&now);
|
time(&now);
|
||||||
struct tm t;
|
struct tm t;
|
||||||
@ -46,7 +54,7 @@ lv_obj_t* ClockWidget::create(lv_obj_t* parent) {
|
|||||||
void ClockWidget::applyStyle() {
|
void ClockWidget::applyStyle() {
|
||||||
if (!obj_) return;
|
if (!obj_) return;
|
||||||
|
|
||||||
// Face style (background)
|
// Face style
|
||||||
if (config_.bgOpacity > 0) {
|
if (config_.bgOpacity > 0) {
|
||||||
lv_obj_set_style_bg_color(obj_, lv_color_make(
|
lv_obj_set_style_bg_color(obj_, lv_color_make(
|
||||||
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
|
config_.bgColor.r, config_.bgColor.g, config_.bgColor.b), 0);
|
||||||
@ -54,7 +62,7 @@ void ClockWidget::applyStyle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config_.borderRadius > 0) {
|
if (config_.borderRadius > 0) {
|
||||||
lv_obj_set_style_radius(obj_, config_.borderRadius, 0); // Usually 50% for circle
|
lv_obj_set_style_radius(obj_, config_.borderRadius, 0);
|
||||||
} else {
|
} else {
|
||||||
lv_obj_set_style_radius(obj_, LV_RADIUS_CIRCLE, 0);
|
lv_obj_set_style_radius(obj_, LV_RADIUS_CIRCLE, 0);
|
||||||
}
|
}
|
||||||
@ -63,31 +71,62 @@ void ClockWidget::applyStyle() {
|
|||||||
lv_obj_set_style_border_color(obj_, lv_color_make(
|
lv_obj_set_style_border_color(obj_, lv_color_make(
|
||||||
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
|
config_.textColor.r, config_.textColor.g, config_.textColor.b), 0);
|
||||||
|
|
||||||
// Hand colors
|
|
||||||
lv_color_t handColor = lv_color_make(
|
lv_color_t handColor = lv_color_make(
|
||||||
config_.textColor.r, config_.textColor.g, config_.textColor.b);
|
config_.textColor.r, config_.textColor.g, config_.textColor.b);
|
||||||
lv_color_t secColor = lv_color_make(200, 50, 50); // Red for second hand
|
lv_color_t secColor = lv_color_make(200, 50, 50);
|
||||||
|
|
||||||
|
int32_t w = config_.width > 0 ? config_.width : 200;
|
||||||
|
int32_t h = config_.height > 0 ? config_.height : 200;
|
||||||
|
int32_t radius = (w < h ? w : h) / 2;
|
||||||
|
|
||||||
|
auto configHand = [&](lv_obj_t* hand, int32_t width, int32_t length, lv_color_t color) {
|
||||||
|
if (!hand) return;
|
||||||
|
lv_obj_set_size(hand, width, length);
|
||||||
|
lv_obj_set_style_bg_color(hand, color, 0);
|
||||||
|
lv_obj_set_style_bg_opa(hand, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_radius(hand, width / 2, 0);
|
||||||
|
|
||||||
|
// Align to center-bottom of the hand to the center of the clock
|
||||||
|
// For rotation, we position the hand such that its rotation point is at the clock center.
|
||||||
|
// We put the hand's bottom-center at the clock's center.
|
||||||
|
// LVGL Rotation pivot is relative to the object's top-left (0,0) by default?
|
||||||
|
// No, default pivot is center.
|
||||||
|
// We want pivot at (width/2, length).
|
||||||
|
// And we want that pivot point to be at clock center (w/2, h/2).
|
||||||
|
|
||||||
|
// Position:
|
||||||
|
// x = (ClockW - HandW) / 2
|
||||||
|
// y = (ClockH/2) - HandLength
|
||||||
|
// This puts the bottom of the hand at the center.
|
||||||
|
|
||||||
|
lv_obj_align(hand, LV_ALIGN_CENTER, 0, -length/2);
|
||||||
|
// Wait, align center puts the *center* of hand at center of clock.
|
||||||
|
// Hand center is at (w/2, length/2).
|
||||||
|
// We want hand bottom (w/2, length) to be at clock center.
|
||||||
|
// So we shift y by -length/2.
|
||||||
|
|
||||||
|
// Pivot:
|
||||||
|
// Set pivot to (width/2, length).
|
||||||
|
// lv_obj_set_style_transform_pivot_x(hand, width/2, 0);
|
||||||
|
// lv_obj_set_style_transform_pivot_y(hand, length, 0); // Bottom
|
||||||
|
|
||||||
|
// BUT: lv_obj_align(..., 0, -length/2) moves it up.
|
||||||
|
// Let's verify.
|
||||||
|
|
||||||
|
lv_obj_set_style_transform_pivot_x(hand, width / 2, 0);
|
||||||
|
lv_obj_set_style_transform_pivot_y(hand, length - 2, 0); // Slightly above bottom for overlap
|
||||||
|
};
|
||||||
|
|
||||||
|
configHand(hourHand_, 6, radius * 0.6, handColor);
|
||||||
|
configHand(minuteHand_, 4, radius * 0.85, handColor);
|
||||||
|
configHand(secondHand_, 2, radius * 0.9, secColor);
|
||||||
|
|
||||||
if (hourHand_) {
|
|
||||||
lv_obj_set_style_line_width(hourHand_, 6, 0);
|
|
||||||
lv_obj_set_style_line_color(hourHand_, handColor, 0);
|
|
||||||
lv_obj_set_style_line_rounded(hourHand_, true, 0);
|
|
||||||
}
|
|
||||||
if (minuteHand_) {
|
|
||||||
lv_obj_set_style_line_width(minuteHand_, 4, 0);
|
|
||||||
lv_obj_set_style_line_color(minuteHand_, handColor, 0);
|
|
||||||
lv_obj_set_style_line_rounded(minuteHand_, true, 0);
|
|
||||||
}
|
|
||||||
if (secondHand_) {
|
|
||||||
lv_obj_set_style_line_width(secondHand_, 2, 0);
|
|
||||||
lv_obj_set_style_line_color(secondHand_, secColor, 0);
|
|
||||||
lv_obj_set_style_line_rounded(secondHand_, true, 0);
|
|
||||||
}
|
|
||||||
if (centerDot_) {
|
if (centerDot_) {
|
||||||
lv_obj_set_style_bg_color(centerDot_, handColor, 0);
|
lv_obj_set_style_bg_color(centerDot_, handColor, 0);
|
||||||
|
lv_obj_set_style_bg_opa(centerDot_, LV_OPA_COVER, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force redraw of hands with new style/dimensions
|
// Force update
|
||||||
lastSecond_ = -1;
|
lastSecond_ = -1;
|
||||||
time_t now;
|
time_t now;
|
||||||
time(&now);
|
time(&now);
|
||||||
@ -103,49 +142,21 @@ void ClockWidget::updateHands(const struct tm& t) {
|
|||||||
lastMinute_ = t.tm_min;
|
lastMinute_ = t.tm_min;
|
||||||
lastHour_ = t.tm_hour;
|
lastHour_ = t.tm_hour;
|
||||||
|
|
||||||
int32_t w = lv_obj_get_width(obj_);
|
// Calculate angles in 0.1 degree units (LVGL 9)
|
||||||
int32_t h = lv_obj_get_height(obj_);
|
// Second: 6 deg * 10 = 60 per sec
|
||||||
int32_t cx = w / 2;
|
int32_t angleSec = t.tm_sec * 60;
|
||||||
int32_t cy = h / 2;
|
|
||||||
int32_t radius = (w < h ? w : h) / 2;
|
|
||||||
|
|
||||||
// Lengths
|
// Minute: (6 deg * min + 0.1 deg * sec) * 10 = 60 * min + sec
|
||||||
int32_t lenSec = radius - 10;
|
int32_t angleMin = t.tm_min * 60 + t.tm_sec;
|
||||||
int32_t lenMin = radius - 20;
|
|
||||||
int32_t lenHour = radius * 0.6f;
|
|
||||||
|
|
||||||
// Angles (0 is top, clockwise)
|
// Hour: (30 deg * hr + 0.5 deg * min) * 10 = 300 * (hr%12) + 5 * min
|
||||||
// Second: 6 deg per sec
|
int32_t angleHour = (t.tm_hour % 12) * 300 + t.tm_min * 5;
|
||||||
float angleSec = t.tm_sec * 6.0f;
|
|
||||||
// Minute: 6 deg per min + 0.1 deg per sec
|
|
||||||
float angleMin = t.tm_min * 6.0f + t.tm_sec * 0.1f;
|
|
||||||
// Hour: 30 deg per hour + 0.5 deg per min
|
|
||||||
float angleHour = (t.tm_hour % 12) * 30.0f + t.tm_min * 0.5f;
|
|
||||||
|
|
||||||
auto calcPoints = [&](float angle, int32_t len, lv_point_precise_t* p) {
|
if (secondHand_) lv_obj_set_style_transform_rotation(secondHand_, angleSec, 0);
|
||||||
float rad = (angle - 90.0f) * PI / 180.0f;
|
if (minuteHand_) lv_obj_set_style_transform_rotation(minuteHand_, angleMin, 0);
|
||||||
p[0].x = cx;
|
if (hourHand_) lv_obj_set_style_transform_rotation(hourHand_, angleHour, 0);
|
||||||
p[0].y = cy;
|
|
||||||
p[1].x = cx + static_cast<int32_t>(cos(rad) * len);
|
|
||||||
p[1].y = cy + static_cast<int32_t>(sin(rad) * len);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (secondHand_) {
|
|
||||||
calcPoints(angleSec, lenSec, secondPoints_);
|
|
||||||
lv_line_set_points(secondHand_, secondPoints_, 2);
|
|
||||||
}
|
|
||||||
if (minuteHand_) {
|
|
||||||
calcPoints(angleMin, lenMin, minutePoints_);
|
|
||||||
lv_line_set_points(minuteHand_, minutePoints_, 2);
|
|
||||||
}
|
|
||||||
if (hourHand_) {
|
|
||||||
calcPoints(angleHour, lenHour, hourPoints_);
|
|
||||||
lv_line_set_points(hourHand_, hourPoints_, 2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClockWidget::onKnxTime(const struct tm& value, TextSource source) {
|
void ClockWidget::onKnxTime(const struct tm& value, TextSource source) {
|
||||||
// Only accept system time updates or explicitly configured KNX time sources if we supported them
|
|
||||||
// For now, ClockWidget listens to SYSTEM_TIME via WidgetManager broadcast
|
|
||||||
updateHands(value);
|
updateHands(value);
|
||||||
}
|
}
|
||||||
@ -20,11 +20,6 @@ private:
|
|||||||
lv_obj_t* secondHand_ = nullptr;
|
lv_obj_t* secondHand_ = nullptr;
|
||||||
lv_obj_t* centerDot_ = nullptr;
|
lv_obj_t* centerDot_ = nullptr;
|
||||||
|
|
||||||
// Persistent point arrays for lines (LVGL does not copy them)
|
|
||||||
lv_point_precise_t hourPoints_[2];
|
|
||||||
lv_point_precise_t minutePoints_[2];
|
|
||||||
lv_point_precise_t secondPoints_[2];
|
|
||||||
|
|
||||||
// Cache current time to avoid redrawing if not changed
|
// Cache current time to avoid redrawing if not changed
|
||||||
int lastHour_ = -1;
|
int lastHour_ = -1;
|
||||||
int lastMinute_ = -1;
|
int lastMinute_ = -1;
|
||||||
|
|||||||
@ -19,6 +19,7 @@ public:
|
|||||||
// Access to LVGL object
|
// Access to LVGL object
|
||||||
lv_obj_t* getLvglObject() const { return obj_; }
|
lv_obj_t* getLvglObject() const { return obj_; }
|
||||||
lv_obj_t* getObj() const { return obj_; } // Alias
|
lv_obj_t* getObj() const { return obj_; } // Alias
|
||||||
|
void clearLvglObject() { obj_ = nullptr; }
|
||||||
|
|
||||||
// Widget ID
|
// Widget ID
|
||||||
uint8_t getId() const { return config_.id; }
|
uint8_t getId() const { return config_.id; }
|
||||||
|
|||||||
1
sdcard_content/webseite/assets/index-D_QgTvEC.css
Normal file
1
sdcard_content/webseite/assets/index-D_QgTvEC.css
Normal file
File diff suppressed because one or more lines are too long
9
sdcard_content/webseite/assets/index-D_mZROR3.js
Normal file
9
sdcard_content/webseite/assets/index-D_mZROR3.js
Normal file
File diff suppressed because one or more lines are too long
@ -5,8 +5,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>web-interface</title>
|
<title>web-interface</title>
|
||||||
<script type="module" crossorigin src="/assets/index-itIBE803.js"></script>
|
<script type="module" crossorigin src="/assets/index-D_mZROR3.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BV9BQMzn.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D_QgTvEC.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@ -15,11 +15,11 @@
|
|||||||
<div :class="rowClass"><label :class="labelClass">Sichtbar</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.visible"></div>
|
<div :class="rowClass"><label :class="labelClass">Sichtbar</label><input class="accent-[var(--accent)]" type="checkbox" v-model="w.visible"></div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<template v-if="key === 'label'">
|
<template v-if="key === 'label' || key === 'button'">
|
||||||
<h4 :class="headingClass">Inhalt</h4>
|
<h4 :class="headingClass">Inhalt</h4>
|
||||||
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
<div :class="rowClass"><label :class="labelClass">Quelle</label>
|
||||||
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
<select :class="inputClass" v-model.number="w.textSrc" @change="handleTextSrcChange">
|
||||||
<optgroup v-for="group in groupedSources(sourceOptions.label)" :key="group.label" :label="group.label">
|
<optgroup v-for="group in groupedSources(sourceOptions[key])" :key="group.label" :label="group.label">
|
||||||
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
<option v-for="opt in group.values" :key="opt" :value="opt">{{ textSources[opt] }}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
@ -40,11 +40,6 @@
|
|||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="key === 'button'">
|
|
||||||
<h4 :class="headingClass">Button</h4>
|
|
||||||
<div :class="rowClass"><label :class="labelClass">Hinweis</label><span :class="noteClass">Text als Label-Child anlegen.</span></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-if="key === 'led'">
|
<template v-if="key === 'led'">
|
||||||
<h4 :class="headingClass">LED</h4>
|
<h4 :class="headingClass">LED</h4>
|
||||||
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
<div :class="rowClass"><label :class="labelClass">Modus</label>
|
||||||
|
|||||||
@ -125,6 +125,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="isClock">
|
||||||
|
<div class="relative w-full h-full rounded-full border-2 box-border flex items-center justify-center overflow-hidden" :style="{ borderColor: widget.textColor }">
|
||||||
|
<!-- Center Dot -->
|
||||||
|
<div class="absolute w-2 h-2 rounded-full z-10" :style="{ backgroundColor: widget.textColor }"></div>
|
||||||
|
<!-- Hour Hand -->
|
||||||
|
<div class="absolute w-1.5 h-[28%] bottom-1/2 left-1/2 -translate-x-1/2 origin-bottom rounded-full" :style="{ backgroundColor: widget.textColor, transform: 'rotate(300deg)' }"></div>
|
||||||
|
<!-- Minute Hand -->
|
||||||
|
<div class="absolute w-1 h-[40%] bottom-1/2 left-1/2 -translate-x-1/2 origin-bottom rounded-full" :style="{ backgroundColor: widget.textColor, transform: 'rotate(70deg)' }"></div>
|
||||||
|
<!-- Second Hand -->
|
||||||
|
<div class="absolute w-0.5 h-[45%] bottom-1/2 left-1/2 -translate-x-1/2 origin-bottom rounded-full bg-[#c83232]" :style="{ transform: 'rotate(140deg)' }"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Icon-only Widget -->
|
<!-- Icon-only Widget -->
|
||||||
<template v-else-if="isIcon">
|
<template v-else-if="isIcon">
|
||||||
<span class="material-symbols-outlined flex items-center justify-center" :style="iconOnlyStyle">
|
<span class="material-symbols-outlined flex items-center justify-center" :style="iconOnlyStyle">
|
||||||
@ -199,6 +212,7 @@ const isPowerFlow = computed(() => props.widget.type === WIDGET_TYPES.POWERFLOW)
|
|||||||
const isPowerNode = computed(() => props.widget.type === WIDGET_TYPES.POWERNODE);
|
const isPowerNode = computed(() => props.widget.type === WIDGET_TYPES.POWERNODE);
|
||||||
const isPowerLink = computed(() => props.widget.type === WIDGET_TYPES.POWERLINK);
|
const isPowerLink = computed(() => props.widget.type === WIDGET_TYPES.POWERLINK);
|
||||||
const isChart = computed(() => props.widget.type === WIDGET_TYPES.CHART);
|
const isChart = computed(() => props.widget.type === WIDGET_TYPES.CHART);
|
||||||
|
const isClock = computed(() => props.widget.type === WIDGET_TYPES.CLOCK);
|
||||||
|
|
||||||
const tabPosition = computed(() => props.widget.iconPosition || 0); // 0=Top, 1=Bottom...
|
const tabPosition = computed(() => props.widget.iconPosition || 0); // 0=Top, 1=Bottom...
|
||||||
const tabHeight = computed(() => (props.widget.iconSize || 5) * 10);
|
const tabHeight = computed(() => (props.widget.iconSize || 5) * 10);
|
||||||
@ -557,6 +571,16 @@ const computedStyle = computed(() => {
|
|||||||
style.outline = `2px dashed ${hexToRgba('#4f8ad9', 0.9)}`;
|
style.outline = `2px dashed ${hexToRgba('#4f8ad9', 0.9)}`;
|
||||||
style.outlineOffset = '2px';
|
style.outlineOffset = '2px';
|
||||||
}
|
}
|
||||||
|
} else if (isClock.value) {
|
||||||
|
if (w.bgOpacity > 0) {
|
||||||
|
const alpha = clamp(w.bgOpacity / 255, 0, 1).toFixed(2);
|
||||||
|
style.background = hexToRgba(w.bgColor, alpha);
|
||||||
|
}
|
||||||
|
if (w.radius > 0) {
|
||||||
|
style.borderRadius = `${w.radius * s}px`;
|
||||||
|
} else {
|
||||||
|
style.borderRadius = '50%';
|
||||||
|
}
|
||||||
} else if (isTabView.value) {
|
} else if (isTabView.value) {
|
||||||
style.background = w.bgColor;
|
style.background = w.bgColor;
|
||||||
style.borderRadius = `${w.radius * s}px`;
|
style.borderRadius = `${w.radius * s}px`;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user