|
|
|
@@ -17,6 +17,7 @@
|
|
|
|
|
#include <Arduino.h>
|
|
|
|
|
#include <ArduinoJson.h>
|
|
|
|
|
#include <Preferences.h> // Dashboard-Design P4-lokal merken (NVS)
|
|
|
|
|
#include <esp_heap_caps.h> // Verlaufs-Historie im PSRAM allozieren (interner RAM ist knapp)
|
|
|
|
|
#include "ui.h"
|
|
|
|
|
#include "theme.h"
|
|
|
|
|
#include "config.h"
|
|
|
|
@@ -124,11 +125,17 @@ static lv_obj_t* g_axHi = nullptr; static lv_obj_t* g_axMid = nullptr; static lv
|
|
|
|
|
static const uint16_t CHART_WIN_MIN[4] = { 2, 10, 30, 60 }; // waehlbare Fenster (Minuten)
|
|
|
|
|
#define CHART_HIST_MAX 3600 // Historie: 60 min à 1 s
|
|
|
|
|
#define CHART_HIST_NONE INT16_MIN // Markierung "keine Probe"
|
|
|
|
|
static int16_t g_histW[CHART_HIST_MAX]; // Zehntelgrad
|
|
|
|
|
static int16_t g_histD[CHART_HIST_MAX];
|
|
|
|
|
static int16_t g_histC[CHART_HIST_MAX];
|
|
|
|
|
static uint8_t g_histDutyW[CHART_HIST_MAX]; // Heizleistung (%)
|
|
|
|
|
static uint8_t g_histDutyD[CHART_HIST_MAX];
|
|
|
|
|
// Die ~29 KB Historie liegen im PSRAM (heap_caps_malloc in ui_init, wie die LVGL-
|
|
|
|
|
// Framebuffer) - als statische Arrays wuerden sie den knappen internen RAM sprengen
|
|
|
|
|
// (Board-Limit war vor diesem Feature bereits zu ~98 % belegt).
|
|
|
|
|
struct ChartHist {
|
|
|
|
|
int16_t w[CHART_HIST_MAX]; // Wasser (Zehntelgrad)
|
|
|
|
|
int16_t d[CHART_HIST_MAX]; // Dampf (Zehntelgrad)
|
|
|
|
|
int16_t c[CHART_HIST_MAX]; // Zusatzsensor (Zehntelgrad; CHART_HIST_NONE = aus)
|
|
|
|
|
uint8_t dutyW[CHART_HIST_MAX]; // Heizleistung Wasser (%)
|
|
|
|
|
uint8_t dutyD[CHART_HIST_MAX]; // Heizleistung Dampf (%)
|
|
|
|
|
};
|
|
|
|
|
static ChartHist* g_hist = nullptr;
|
|
|
|
|
static uint16_t g_histPos = 0, g_histFill = 0;
|
|
|
|
|
static uint8_t g_chartWin = 0; // Index in CHART_WIN_MIN (P4-lokal in NVS, "chartwin")
|
|
|
|
|
static bool g_chartDuty = false; // Heizleistung als Zusatzkurven (NVS "chartduty")
|
|
|
|
@@ -1471,25 +1478,44 @@ static void build_chart(lv_obj_t* p) {
|
|
|
|
|
// Y-Achse ueber das sichtbare Fenster (Werte < 30 Grad = kalter/inaktiver Sensor werden
|
|
|
|
|
// fuer die Skala ignoriert) und positioniert die gestrichelten Soll-Linien.
|
|
|
|
|
static void chart_redraw() {
|
|
|
|
|
if (!g_chart) return;
|
|
|
|
|
if (!g_chart || !g_hist) return;
|
|
|
|
|
const int stride = (int)CHART_WIN_MIN[g_chartWin] * 60 / 120; // Sekunden je Chart-Punkt
|
|
|
|
|
int32_t lo = INT32_MAX, hi = INT32_MIN; // Zehntelgrad
|
|
|
|
|
bool anyW = false, anyD = false;
|
|
|
|
|
for (int i = 0; i < 120; i++) {
|
|
|
|
|
int age = (119 - i) * stride; // Alter der Probe (Sekunden)
|
|
|
|
|
int ageStart = (119 - i) * stride; // juengstes Alter im Bucket (Sekunden)
|
|
|
|
|
int32_t vW = LV_CHART_POINT_NONE, vD = LV_CHART_POINT_NONE, vC = LV_CHART_POINT_NONE;
|
|
|
|
|
int32_t dW = LV_CHART_POINT_NONE, dD = LV_CHART_POINT_NONE;
|
|
|
|
|
if (age < (int)g_histFill) {
|
|
|
|
|
int idx = (int)g_histPos - 1 - age;
|
|
|
|
|
if (idx < 0) idx += CHART_HIST_MAX;
|
|
|
|
|
vW = g_histW[idx];
|
|
|
|
|
vD = g_histD[idx];
|
|
|
|
|
if (g_histC[idx] != CHART_HIST_NONE) vC = g_histC[idx];
|
|
|
|
|
dW = g_histDutyW[idx];
|
|
|
|
|
dD = g_histDutyD[idx];
|
|
|
|
|
if (vW > 300) { anyW = true; if (vW < lo) lo = vW; if (vW > hi) hi = vW; }
|
|
|
|
|
if (vD > 300) { anyD = true; if (vD < lo) lo = vD; if (vD > hi) hi = vD; }
|
|
|
|
|
if (vC != LV_CHART_POINT_NONE && vC > 300) { if (vC < lo) lo = vC; if (vC > hi) hi = vC; }
|
|
|
|
|
if (ageStart < (int)g_histFill) {
|
|
|
|
|
// Ueber den ganzen Bucket mitteln statt ein Einzelsample zu picken: das
|
|
|
|
|
// Abtast-Raster haengt an g_histPos und wandert sonst mit jedem Sekundentick,
|
|
|
|
|
// wodurch schnell schaltende Werte (Duty-PWM) sichtbar zwischen zwei
|
|
|
|
|
// Kurvenbildern hin- und herspringen. Der Mittelwert ist zudem bei der
|
|
|
|
|
// Heizleistung die ehrlichere Aussage als ein zufaelliger Momentwert.
|
|
|
|
|
int32_t sW = 0, sD = 0, sC = 0, sDW = 0, sDD = 0;
|
|
|
|
|
int nT = 0, nC = 0;
|
|
|
|
|
for (int s = 0; s < stride; s++) {
|
|
|
|
|
int age = ageStart + s;
|
|
|
|
|
if (age >= (int)g_histFill) break;
|
|
|
|
|
int idx = (int)g_histPos - 1 - age;
|
|
|
|
|
if (idx < 0) idx += CHART_HIST_MAX;
|
|
|
|
|
sW += g_hist->w[idx];
|
|
|
|
|
sD += g_hist->d[idx];
|
|
|
|
|
sDW += g_hist->dutyW[idx];
|
|
|
|
|
sDD += g_hist->dutyD[idx];
|
|
|
|
|
nT++;
|
|
|
|
|
if (g_hist->c[idx] != CHART_HIST_NONE) { sC += g_hist->c[idx]; nC++; }
|
|
|
|
|
}
|
|
|
|
|
if (nT > 0) {
|
|
|
|
|
vW = (sW + nT / 2) / nT;
|
|
|
|
|
vD = (sD + nT / 2) / nT;
|
|
|
|
|
dW = (sDW + nT / 2) / nT;
|
|
|
|
|
dD = (sDD + nT / 2) / nT;
|
|
|
|
|
if (nC > 0) vC = (sC + nC / 2) / nC;
|
|
|
|
|
if (vW > 300) { anyW = true; if (vW < lo) lo = vW; if (vW > hi) hi = vW; }
|
|
|
|
|
if (vD > 300) { anyD = true; if (vD < lo) lo = vD; if (vD > hi) hi = vD; }
|
|
|
|
|
if (vC != LV_CHART_POINT_NONE && vC > 300) { if (vC < lo) lo = vC; if (vC > hi) hi = vC; }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lv_chart_set_value_by_id(g_chart, g_serW, i, vW);
|
|
|
|
|
lv_chart_set_value_by_id(g_chart, g_serD, i, vD);
|
|
|
|
@@ -1540,15 +1566,15 @@ static void chart_redraw() {
|
|
|
|
|
lv_chart_refresh(g_chart);
|
|
|
|
|
}
|
|
|
|
|
static void chart_sample_cb(lv_timer_t*) {
|
|
|
|
|
if (!g_chart) return;
|
|
|
|
|
if (!g_chart || !g_hist) return;
|
|
|
|
|
// 1-s-Probe in die 60-min-Historie (Ringpuffer); Anzeige zeichnet chart_redraw
|
|
|
|
|
bool caseOn = g_state.caseSensorEnabled && g_state.hasCaseTemp;
|
|
|
|
|
g_histW[g_histPos] = (int16_t)(g_state.tempW * 10.0f + 0.5f);
|
|
|
|
|
g_histD[g_histPos] = (int16_t)(g_state.tempD * 10.0f + 0.5f);
|
|
|
|
|
g_histC[g_histPos] = caseOn ? (int16_t)(g_state.caseTemp * 10.0f + 0.5f) : CHART_HIST_NONE;
|
|
|
|
|
g_hist->w[g_histPos] = (int16_t)(g_state.tempW * 10.0f + 0.5f);
|
|
|
|
|
g_hist->d[g_histPos] = (int16_t)(g_state.tempD * 10.0f + 0.5f);
|
|
|
|
|
g_hist->c[g_histPos] = caseOn ? (int16_t)(g_state.caseTemp * 10.0f + 0.5f) : CHART_HIST_NONE;
|
|
|
|
|
float dw = g_state.dutyW, dd = g_state.dutyD;
|
|
|
|
|
g_histDutyW[g_histPos] = (uint8_t)(dw < 0.0f ? 0 : (dw > 100.0f ? 100 : (int)(dw + 0.5f)));
|
|
|
|
|
g_histDutyD[g_histPos] = (uint8_t)(dd < 0.0f ? 0 : (dd > 100.0f ? 100 : (int)(dd + 0.5f)));
|
|
|
|
|
g_hist->dutyW[g_histPos] = (uint8_t)(dw < 0.0f ? 0 : (dw > 100.0f ? 100 : (int)(dw + 0.5f)));
|
|
|
|
|
g_hist->dutyD[g_histPos] = (uint8_t)(dd < 0.0f ? 0 : (dd > 100.0f ? 100 : (int)(dd + 0.5f)));
|
|
|
|
|
g_histPos = (uint16_t)((g_histPos + 1) % CHART_HIST_MAX);
|
|
|
|
|
if (g_histFill < CHART_HIST_MAX) g_histFill++;
|
|
|
|
|
|
|
|
|
@@ -2789,6 +2815,13 @@ void ui_init(ProtocolClient* client) {
|
|
|
|
|
g_client = client;
|
|
|
|
|
|
|
|
|
|
// Gespeicherte Darstellungs-Einstellungen laden (P4-lokal; Design-Standard: Rundinstrumente)
|
|
|
|
|
// Verlaufs-Historie (60 min à 1 s, ~29 KB) im PSRAM anlegen - wie die LVGL-Framebuffer.
|
|
|
|
|
// Schlaegt die Allokation fehl, bleibt das Chart schlicht leer (alle Zugriffe geprueft).
|
|
|
|
|
if (!g_hist) {
|
|
|
|
|
g_hist = (ChartHist*)heap_caps_malloc(sizeof(ChartHist), MALLOC_CAP_SPIRAM);
|
|
|
|
|
if (!g_hist) g_hist = (ChartHist*)heap_caps_malloc(sizeof(ChartHist), MALLOC_CAP_8BIT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{ Preferences prefs; prefs.begin("ui", false);
|
|
|
|
|
g_tempDesign = prefs.getUChar("tempdsgn", TD_GAUGE);
|
|
|
|
|
g_brewLiveEnabled = prefs.getBool("brewlive", true);
|
|
|
|
|