Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d97e4c34d5 | ||
|
|
00a4c80b13 | ||
|
|
a1431a9d42 | ||
|
|
d9bc31b555 | ||
|
|
5691a4c04f | ||
|
|
f8df6f85cd | ||
|
|
f43375e991 | ||
|
|
92ef4fbfb6 | ||
|
|
fd6ec5371b | ||
|
|
ea64e7de04 | ||
|
|
7d240cd13e | ||
|
|
27dabd884a | ||
|
|
dc6cbbb191 | ||
|
|
940dc0fcd0 |
@@ -1,3 +1,24 @@
|
|||||||
|
Version 5.1.1:
|
||||||
|
- Aktives Profil wird jetzt geführt und angezeigt: Die Steuerung merkt sich das zuletzt geladene bzw.
|
||||||
|
gespeicherte Profil dauerhaft im EEPROM (überlebt Neustarts). Web-UI: Anzeige auf dem Dashboard über der
|
||||||
|
Profil-Auswahl sowie auf der Profil-Seite (aktives Profil zusätzlich mit grünem Punkt in der Liste).
|
||||||
|
- „Geändert"-Erkennung: Werden nach dem Laden profil-relevante Einstellungen geändert (Solltemperaturen,
|
||||||
|
PID-Werte inkl. AutoTune-Ergebnis und AutoTune-Parameter, Boost, Eco, Dampfverzögerung, Fast-Heat-Up,
|
||||||
|
Brew-Control, Piezo), erscheint hinter dem Profilnamen „✱ (geändert)". Reine Anzeige-/Systemeinstellungen
|
||||||
|
(Anzeigemodus, Aufheizzeit, Licht, FlowGuard, Waage/Sensoren) lösen das bewusst nicht aus.
|
||||||
|
- Profil löschen setzt die Anzeige zurück, wenn das gelöschte Profil das aktive war; Werksreset ebenso.
|
||||||
|
- Neue additive State-Felder "profile" und "profDirty" im UART-JSON für das Touch-Display (Protokoll bleibt
|
||||||
|
kompatibel, ältere Display-Firmware ignoriert die Felder) - Grundlage für die Profil-Schnellwahl am Display.
|
||||||
|
|
||||||
|
Version 5.1.0:
|
||||||
|
- Display-Auswahl in die Web-UI (/Sensoren) verlegt: Ohne Display / OLED (SH1106, I2C) / UART-Touch-Display.
|
||||||
|
Der Compile-Schalter ENABLE_DISPLAY entfällt, die Einstellung wird im EEPROM gespeichert und ohne Neustart
|
||||||
|
übernommen. Default ist "Ohne Display", damit die Steuerung ohne angeschlossene Anzeige immer sicher startet.
|
||||||
|
- Boot-Schutz: Vor der OLED-Initialisierung wird per I2C-Probe (0x3C) geprüft, ob das Display wirklich
|
||||||
|
angeschlossen ist. Ohne Antwort bleibt die Anzeige deaktiviert und die Steuerung bootet normal weiter -
|
||||||
|
eine falsch gesetzte Display-Einstellung kann die Maschine damit nicht mehr blockieren.
|
||||||
|
- UART-Touch-Anbindung (Serial1) wird nur noch initialisiert und bedient, wenn "UART-Touch-Display" gewählt ist.
|
||||||
|
|
||||||
Version 5.0.12:
|
Version 5.0.12:
|
||||||
- Neues additives Feld "piState" im State-JSON für das UART-Touch-Display (0=inaktiv, 1=Pre-Infusion,
|
- Neues additives Feld "piState" im State-JSON für das UART-Touch-Display (0=inaktiv, 1=Pre-Infusion,
|
||||||
2=Pause, 3=Hauptbezug): Die Pre-Infusion-Phasenleiste auf dem Display folgt damit quelltreu der echten
|
2=Pause, 3=Hauptbezug): Die Pre-Infusion-Phasenleiste auf dem Display folgt damit quelltreu der echten
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ Erzeugt von `buildTouchUartStateJson()`. Alle Felder in einem flachen JSON-Objek
|
|||||||
| `steamHeatDisabled` | bool | Dampfheizung per Nutzer deaktiviert |
|
| `steamHeatDisabled` | bool | Dampfheizung per Nutzer deaktiviert |
|
||||||
| `lightOn` | bool | Beleuchtung an |
|
| `lightOn` | bool | Beleuchtung an |
|
||||||
| `piezoEnabled` | bool | Piezo-Summer aktiv |
|
| `piezoEnabled` | bool | Piezo-Summer aktiv |
|
||||||
|
| `profile` | string | zuletzt geladenes/gespeichertes Profil, leer = keines (additiv seit S3 5.1.1) |
|
||||||
|
| `profDirty` | bool | profil-relevante Einstellungen seit dem Laden geändert (additiv seit S3 5.1.1) |
|
||||||
| `weight` | float | aktuelles Waagengewicht (UI) |
|
| `weight` | float | aktuelles Waagengewicht (UI) |
|
||||||
| `targetWeight` | float | Ziel-Gewicht (Brew-by-Weight) |
|
| `targetWeight` | float | Ziel-Gewicht (Brew-by-Weight) |
|
||||||
| `scaleEnabled` | bool | Waage aktiviert |
|
| `scaleEnabled` | bool | Waage aktiviert |
|
||||||
|
|||||||
+360
-87
@@ -52,8 +52,17 @@
|
|||||||
// Standard-Waagen-Typ (kann in /Sensoren geändert werden)
|
// Standard-Waagen-Typ (kann in /Sensoren geändert werden)
|
||||||
#define DEFAULT_SCALE_TYPE SCALE_ESPNOW
|
#define DEFAULT_SCALE_TYPE SCALE_ESPNOW
|
||||||
|
|
||||||
// Nachfolgende Zeile auskommentieren, falls die Steuerung ohne OLED-Display verwendet wird!
|
/************************************************************************************
|
||||||
//#define ENABLE_DISPLAY
|
* DISPLAY-KONFIGURATION
|
||||||
|
* Der Display-Typ wird zur Laufzeit in der Web-UI (/Sensoren) gewaehlt und im
|
||||||
|
* EEPROM gespeichert. Default ist "Ohne Display", damit die Steuerung ohne
|
||||||
|
* angeschlossene Anzeige immer sicher startet.
|
||||||
|
************************************************************************************/
|
||||||
|
#define DISPLAY_TYPE_NONE 0 // Ohne Display
|
||||||
|
#define DISPLAY_TYPE_OLED 1 // OLED (SH1106, I2C)
|
||||||
|
#define DISPLAY_TYPE_TOUCH_UART 2 // JC-UART-Touch-Display (Serial1)
|
||||||
|
#define DEFAULT_DISPLAY_TYPE DISPLAY_TYPE_NONE
|
||||||
|
#define OLED_I2C_ADDRESS 0x3C
|
||||||
|
|
||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* Vorwärtsdeklarationen benutzerdefinierter Strukturen
|
* Vorwärtsdeklarationen benutzerdefinierter Strukturen
|
||||||
@@ -80,6 +89,9 @@ bool executeDashboardAction(const String& action, const String& value,
|
|||||||
bool& settingsChanged, bool& pidNeedsUpdate);
|
bool& settingsChanged, bool& pidNeedsUpdate);
|
||||||
void touchUartRxTick();
|
void touchUartRxTick();
|
||||||
void touchUartTick();
|
void touchUartTick();
|
||||||
|
void initTouchUart();
|
||||||
|
bool initOledDisplay(bool showSplash);
|
||||||
|
void applyDisplayTypeChange(uint8_t newType);
|
||||||
void applyLightOutput();
|
void applyLightOutput();
|
||||||
void initResetDiagnostics();
|
void initResetDiagnostics();
|
||||||
void recordResetCheckpoint(uint16_t checkpoint);
|
void recordResetCheckpoint(uint16_t checkpoint);
|
||||||
@@ -91,10 +103,10 @@ void syncFullyKioskWithStandby(bool active);
|
|||||||
************************************************************************************/
|
************************************************************************************/
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
|
|
||||||
#ifdef ENABLE_DISPLAY
|
// OLED-Bibliotheken werden immer einkompiliert; ob das Display genutzt wird,
|
||||||
|
// entscheidet zur Laufzeit die Einstellung displayType (Web-UI /Sensoren).
|
||||||
#include <Adafruit_GFX.h>
|
#include <Adafruit_GFX.h>
|
||||||
#include <Adafruit_SH110X.h>
|
#include <Adafruit_SH110X.h>
|
||||||
#endif // ENABLE_DISPLAY
|
|
||||||
|
|
||||||
#include <EEPROM.h>
|
#include <EEPROM.h>
|
||||||
#include <PID_v1.h>
|
#include <PID_v1.h>
|
||||||
@@ -154,9 +166,9 @@ const unsigned long DASHBOARD_WS_SHOT_END_GRACE_MS = 500;
|
|||||||
#define DASHBOARD_JSON_BUFFER_SIZE 2400
|
#define DASHBOARD_JSON_BUFFER_SIZE 2400
|
||||||
unsigned long lastDashboardWsPushMs = 0;
|
unsigned long lastDashboardWsPushMs = 0;
|
||||||
|
|
||||||
// JC-Touch-Display (UART). Bewusst UNABHAENGIG von ENABLE_DISPLAY (OLED), damit alle
|
// JC-Touch-Display (UART). Ob die UART-Anbindung genutzt wird, entscheidet zur
|
||||||
// Kombinationen baubar sind (nur OLED / nur JC / beide / keines). Auf 1 setzen, sobald
|
// Laufzeit displayType (Web-UI /Sensoren); dieser Compile-Schalter bleibt als
|
||||||
// das JC-Display an Serial1 (GPIO43/44) angeschlossen ist.
|
// Notausstieg, um den Code komplett herauszubauen.
|
||||||
#define TOUCH_UART_ENABLED 1
|
#define TOUCH_UART_ENABLED 1
|
||||||
#define TOUCH_UART_TX_PIN 43
|
#define TOUCH_UART_TX_PIN 43
|
||||||
#define TOUCH_UART_RX_PIN 44
|
#define TOUCH_UART_RX_PIN 44
|
||||||
@@ -179,6 +191,13 @@ const unsigned long TOUCH_UART_SHOT_END_GRACE_MS = 500;
|
|||||||
const unsigned long TOUCH_UART_CLIENT_TIMEOUT_MS = 15000;
|
const unsigned long TOUCH_UART_CLIENT_TIMEOUT_MS = 15000;
|
||||||
unsigned long lastTouchUartPushMs = 0;
|
unsigned long lastTouchUartPushMs = 0;
|
||||||
|
|
||||||
|
// Aktiver Display-Typ (DISPLAY_TYPE_*). Wird beim Start aus dem EEPROM geladen
|
||||||
|
// und kann in der Web-UI (/Sensoren) zur Laufzeit umgestellt werden.
|
||||||
|
uint8_t displayType = DEFAULT_DISPLAY_TYPE;
|
||||||
|
// true, sobald das OLED per I2C gefunden UND initialisiert wurde. Schuetzt davor,
|
||||||
|
// dass Renderaufrufe ohne angeschlossenes Display laufen (Boot-Haenger-Schutz).
|
||||||
|
bool oledDisplayPresent = false;
|
||||||
|
|
||||||
HardwareSerial& touchUart = Serial1;
|
HardwareSerial& touchUart = Serial1;
|
||||||
String touchUartRxLine;
|
String touchUartRxLine;
|
||||||
bool touchUartRxOverflow = false;
|
bool touchUartRxOverflow = false;
|
||||||
@@ -342,19 +361,15 @@ MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO);
|
|||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* Display-Objekt (Adafruit SH1106G)
|
* Display-Objekt (Adafruit SH1106G)
|
||||||
************************************************************************************/
|
************************************************************************************/
|
||||||
#ifdef ENABLE_DISPLAY
|
// Objekt existiert immer; initialisiert wird es nur bei displayType == DISPLAY_TYPE_OLED
|
||||||
/************************************************************************************
|
// und erfolgreicher I2C-Probe (siehe initOledDisplay()).
|
||||||
* Display-Objekt (Adafruit SH1106G)
|
|
||||||
************************************************************************************/
|
|
||||||
// Dieser Block muss vorhanden sein, wenn Display aktiviert!
|
|
||||||
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
|
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
|
||||||
#endif // ENABLE_DISPLAY
|
|
||||||
|
|
||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* Firmware-Informationen
|
* Firmware-Informationen
|
||||||
************************************************************************************/
|
************************************************************************************/
|
||||||
|
|
||||||
String version = "5.0.12";
|
String version = "5.1.1";
|
||||||
String versionHersteller = "Thomas Müller";
|
String versionHersteller = "Thomas Müller";
|
||||||
String versionHerstellerMail =
|
String versionHerstellerMail =
|
||||||
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
|
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
|
||||||
@@ -926,7 +941,10 @@ const int EEPROM_ADDR_HEATUP_MINUTES = 904; // 4 Bytes (int) -> Ende
|
|||||||
const int EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT = 908; // 1 Byte (uint8_t) -> Ende 908
|
const int EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT = 908; // 1 Byte (uint8_t) -> Ende 908
|
||||||
const int EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT = 909; // 1 Byte (uint8_t) -> Ende 909
|
const int EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT = 909; // 1 Byte (uint8_t) -> Ende 909
|
||||||
const int EEPROM_ADDR_POWERON_STANDBY = 910; // 1 Byte (bool) -> Ende 910
|
const int EEPROM_ADDR_POWERON_STANDBY = 910; // 1 Byte (bool) -> Ende 910
|
||||||
// Naechste freie Adresse: 911 (Innerhalb EEPROM_SIZE=1024)
|
const int EEPROM_ADDR_DISPLAY_TYPE = 911; // 1 Byte (uint8_t) -> Ende 911
|
||||||
|
const int EEPROM_ADDR_ACTIVE_PROFILE_NAME = 912; // 31 Bytes (char[31], nullterminiert) -> Ende 942
|
||||||
|
const int EEPROM_ADDR_ACTIVE_PROFILE_DIRTY = 943; // 1 Byte (bool) -> Ende 943
|
||||||
|
// Naechste freie Adresse: 944 (Innerhalb EEPROM_SIZE=1024)
|
||||||
|
|
||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* Eco-Mode Variablen
|
* Eco-Mode Variablen
|
||||||
@@ -1296,15 +1314,52 @@ char hostname[33] = "Dual-PID"; // Max 32 Zeichen + Nullterminator
|
|||||||
String profileStatusMessage = ""; // für Nachrichten auf der Profil-Seite
|
String profileStatusMessage = ""; // für Nachrichten auf der Profil-Seite
|
||||||
String standbyTimerStatusMessage = ""; // fuer Nachrichten auf der Timer-Seite
|
String standbyTimerStatusMessage = ""; // fuer Nachrichten auf der Timer-Seite
|
||||||
|
|
||||||
|
// Zuletzt geladenes/gespeichertes Profil (leer = keines). Wird im EEPROM gehalten,
|
||||||
|
// damit die Anzeige einen Neustart überlebt. activeProfileDirty markiert, dass nach
|
||||||
|
// dem Laden profil-relevante Einstellungen geändert wurden (Anzeige "geändert").
|
||||||
|
String activeProfileName = "";
|
||||||
|
bool activeProfileDirty = false;
|
||||||
|
|
||||||
|
// Aktives Profil + Dirty-Flag ins EEPROM schreiben (char[31], nullterminiert)
|
||||||
|
void persistActiveProfile() {
|
||||||
|
char nameBuf[31];
|
||||||
|
memset(nameBuf, 0, sizeof(nameBuf));
|
||||||
|
strncpy(nameBuf, activeProfileName.c_str(), sizeof(nameBuf) - 1);
|
||||||
|
for (size_t i = 0; i < sizeof(nameBuf); i++) {
|
||||||
|
EEPROM.write(EEPROM_ADDR_ACTIVE_PROFILE_NAME + i, (uint8_t)nameBuf[i]);
|
||||||
|
}
|
||||||
|
EEPROM.write(EEPROM_ADDR_ACTIVE_PROFILE_DIRTY, activeProfileDirty ? 1 : 0);
|
||||||
|
EEPROM.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setActiveProfile(const String& name) {
|
||||||
|
activeProfileName = name;
|
||||||
|
activeProfileDirty = false;
|
||||||
|
persistActiveProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearActiveProfile() {
|
||||||
|
activeProfileName = "";
|
||||||
|
activeProfileDirty = false;
|
||||||
|
persistActiveProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nach einer profil-relevanten Einstellungsänderung aufrufen: markiert das aktive
|
||||||
|
// Profil als "geändert". Ohne aktives Profil oder bei bereits gesetztem Flag ein No-Op.
|
||||||
|
void markProfileDirty() {
|
||||||
|
if (activeProfileName.length() == 0 || activeProfileDirty) return;
|
||||||
|
activeProfileDirty = true;
|
||||||
|
EEPROM.write(EEPROM_ADDR_ACTIVE_PROFILE_DIRTY, 1);
|
||||||
|
EEPROM.commit();
|
||||||
|
}
|
||||||
|
|
||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* WiFi Signal Bitmap (Display-Anzeige)
|
* WiFi Signal Bitmap (Display-Anzeige)
|
||||||
************************************************************************************/
|
************************************************************************************/
|
||||||
#ifdef ENABLE_DISPLAY
|
|
||||||
static const unsigned char PROGMEM wifiSymbol[] = {
|
static const unsigned char PROGMEM wifiSymbol[] = {
|
||||||
0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48,
|
0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48,
|
||||||
0x27, 0x20, 0x08, 0x80, 0x02, 0x00
|
0x27, 0x20, 0x08, 0x80, 0x02, 0x00
|
||||||
};
|
};
|
||||||
#endif // ENABLE_DISPLAY
|
|
||||||
|
|
||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* Gemeinsame CSS-Styles (PROGMEM) - Unverändert
|
* Gemeinsame CSS-Styles (PROGMEM) - Unverändert
|
||||||
@@ -2451,12 +2506,10 @@ void checkWifiConnection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_DISPLAY
|
// WiFi-Symbol aufs Display zeichnen
|
||||||
// WiFi-Symbol aufs Display zeichnen
|
|
||||||
void drawwifiSymbol(int16_t x, int16_t y) {
|
void drawwifiSymbol(int16_t x, int16_t y) {
|
||||||
display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
|
display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
|
||||||
}
|
}
|
||||||
#endif // ENABLE_DISPLAY
|
|
||||||
|
|
||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* Handler zum Rücksetzen der Betriebszeit - URL angepasst
|
* Handler zum Rücksetzen der Betriebszeit - URL angepasst
|
||||||
@@ -2896,6 +2949,7 @@ void resetToDefaults() {
|
|||||||
EEPROM.put(EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT, defaultBacklightActivePercent);
|
EEPROM.put(EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT, defaultBacklightActivePercent);
|
||||||
EEPROM.put(EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT, defaultBacklightStandbyClockPercent);
|
EEPROM.put(EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT, defaultBacklightStandbyClockPercent);
|
||||||
EEPROM.put(EEPROM_ADDR_POWERON_STANDBY, (uint8_t)(defaultPowerOnStandby ? 1 : 0));
|
EEPROM.put(EEPROM_ADDR_POWERON_STANDBY, (uint8_t)(defaultPowerOnStandby ? 1 : 0));
|
||||||
|
EEPROM.put(EEPROM_ADDR_DISPLAY_TYPE, (uint8_t)DEFAULT_DISPLAY_TYPE);
|
||||||
EEPROM.put(EEPROM_ADDR_OFFSET_COMPENSATION_ENABLED, (uint8_t)0);
|
EEPROM.put(EEPROM_ADDR_OFFSET_COMPENSATION_ENABLED, (uint8_t)0);
|
||||||
EEPROM.put(EEPROM_ADDR_SCALE_ENABLED, (uint8_t)(defaultScaleEnabled ? 1 : 0));
|
EEPROM.put(EEPROM_ADDR_SCALE_ENABLED, (uint8_t)(defaultScaleEnabled ? 1 : 0));
|
||||||
EEPROM.put(EEPROM_ADDR_SCALE_TYPE, defaultScaleType);
|
EEPROM.put(EEPROM_ADDR_SCALE_TYPE, defaultScaleType);
|
||||||
@@ -2912,6 +2966,9 @@ void resetToDefaults() {
|
|||||||
// Alle Änderungen ins EEPROM schreiben
|
// Alle Änderungen ins EEPROM schreiben
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
|
||||||
|
// Werksreset -> kein Profil mehr aktiv
|
||||||
|
clearActiveProfile();
|
||||||
|
|
||||||
// WICHTIG: Globale Variablen auch auf Default setzen
|
// WICHTIG: Globale Variablen auch auf Default setzen
|
||||||
SetpointWasser = defaultSetpointWasser; SetpointDampf = defaultSetpointDampf;
|
SetpointWasser = defaultSetpointWasser; SetpointDampf = defaultSetpointDampf;
|
||||||
OffsetWasser = defaultOffsetWasser; OffsetDampf = defaultOffsetDampf;
|
OffsetWasser = defaultOffsetWasser; OffsetDampf = defaultOffsetDampf;
|
||||||
@@ -2960,6 +3017,7 @@ void resetToDefaults() {
|
|||||||
xSwitchLongAction = defaultXSwitchLongAction;
|
xSwitchLongAction = defaultXSwitchLongAction;
|
||||||
buttonLongPressMs = (uint16_t)DEFAULT_BUTTON_LONG_PRESS_MS;
|
buttonLongPressMs = (uint16_t)DEFAULT_BUTTON_LONG_PRESS_MS;
|
||||||
dashboardTempDisplayMode = defaultDashboardTempDisplayMode;
|
dashboardTempDisplayMode = defaultDashboardTempDisplayMode;
|
||||||
|
applyDisplayTypeChange(DEFAULT_DISPLAY_TYPE); // deaktiviert ein ggf. aktives Display sauber
|
||||||
xSwitchActive = false;
|
xSwitchActive = false;
|
||||||
ecoForcedActive = false;
|
ecoForcedActive = false;
|
||||||
standbyOverriddenByWeb = false;
|
standbyOverriddenByWeb = false;
|
||||||
@@ -3866,7 +3924,7 @@ void stopBrewSequence(unsigned long durationMillis, bool manualStop = false, boo
|
|||||||
|
|
||||||
/************************************************************************************
|
/************************************************************************************
|
||||||
* setup(): Initialisierung des Systems
|
* setup(): Initialisierung des Systems
|
||||||
* - Angepasst für optionale Display-Unterstützung via ENABLE_DISPLAY
|
* - Optionale Display-Unterstützung via Laufzeit-Einstellung displayType (Web-UI)
|
||||||
* - Korrekte Wire.begin() Logik für Display/Waage
|
* - Korrekte Wire.begin() Logik für Display/Waage
|
||||||
* - Zusätzliche Serial-Ausgaben für Headless-Betrieb
|
* - Zusätzliche Serial-Ausgaben für Headless-Betrieb
|
||||||
************************************************************************************/
|
************************************************************************************/
|
||||||
@@ -3995,15 +4053,6 @@ void setup() {
|
|||||||
enterRescueMode(); // kehrt nie zurueck
|
enterRescueMode(); // kehrt nie zurueck
|
||||||
}
|
}
|
||||||
|
|
||||||
#if TOUCH_UART_ENABLED
|
|
||||||
// TX-Puffer >= max. Zeilenlaenge (State-JSON bis TOUCH_UART_JSON_BUFFER_SIZE=3000
|
|
||||||
// + "\r\n"), damit println() die Hauptschleife NIE blockiert (sonst bis ~41 ms
|
|
||||||
// Stall -> PID-Jitter). Headroom deckt zudem OTA-Chunks (~1,4 KB) und gestaute
|
|
||||||
// Nachrichten (ack direkt vor State-Push) ab.
|
|
||||||
touchUart.setTxBufferSize(4096); // war 2048; nicht-blockierendes Senden bis 3000-Byte-Zeile
|
|
||||||
touchUart.begin(TOUCH_UART_BAUDRATE, SERIAL_8N1, TOUCH_UART_RX_PIN, TOUCH_UART_TX_PIN);
|
|
||||||
touchUartRxLine.reserve(TOUCH_UART_MAX_LINE_LEN);
|
|
||||||
#endif
|
|
||||||
EEPROM.begin(EEPROM_SIZE); // EEPROM initialisieren
|
EEPROM.begin(EEPROM_SIZE); // EEPROM initialisieren
|
||||||
loadFullyKioskConfig(fullyKioskConfig);
|
loadFullyKioskConfig(fullyKioskConfig);
|
||||||
|
|
||||||
@@ -4253,6 +4302,34 @@ void setup() {
|
|||||||
storedBacklightStandbyClock = defaultBacklightStandbyClockPercent;
|
storedBacklightStandbyClock = defaultBacklightStandbyClockPercent;
|
||||||
}
|
}
|
||||||
backlightStandbyClockPercent = storedBacklightStandbyClock;
|
backlightStandbyClockPercent = storedBacklightStandbyClock;
|
||||||
|
uint8_t storedDisplayType = DEFAULT_DISPLAY_TYPE;
|
||||||
|
EEPROM.get(EEPROM_ADDR_DISPLAY_TYPE, storedDisplayType);
|
||||||
|
if (storedDisplayType > DISPLAY_TYPE_TOUCH_UART) {
|
||||||
|
storedDisplayType = DEFAULT_DISPLAY_TYPE; // u. a. unprogrammierte 0xFF
|
||||||
|
}
|
||||||
|
displayType = storedDisplayType;
|
||||||
|
// Zuletzt geladenes Profil aus dem EEPROM holen. Unprogrammierter Bereich (0xFF)
|
||||||
|
// oder nicht druckbare Zeichen -> kein aktives Profil.
|
||||||
|
{
|
||||||
|
char nameBuf[31];
|
||||||
|
for (size_t i = 0; i < sizeof(nameBuf); i++) {
|
||||||
|
nameBuf[i] = (char)EEPROM.read(EEPROM_ADDR_ACTIVE_PROFILE_NAME + i);
|
||||||
|
}
|
||||||
|
nameBuf[sizeof(nameBuf) - 1] = '\0';
|
||||||
|
bool valid = true;
|
||||||
|
for (size_t i = 0; nameBuf[i] != '\0'; i++) {
|
||||||
|
// Steuerzeichen oder 0xFF (unprogrammiertes EEPROM) -> ungueltig.
|
||||||
|
// Bytes >= 0x80 bleiben erlaubt (UTF-8-Umlaute im Anzeigenamen).
|
||||||
|
if ((uint8_t)nameBuf[i] < 0x20 || (uint8_t)nameBuf[i] == 0xFF) { valid = false; break; }
|
||||||
|
}
|
||||||
|
if (valid && nameBuf[0] != '\0') {
|
||||||
|
activeProfileName = String(nameBuf);
|
||||||
|
activeProfileDirty = (EEPROM.read(EEPROM_ADDR_ACTIVE_PROFILE_DIRTY) == 1);
|
||||||
|
} else {
|
||||||
|
activeProfileName = "";
|
||||||
|
activeProfileDirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
uint8_t storedScaleEnabled = 0;
|
uint8_t storedScaleEnabled = 0;
|
||||||
uint8_t storedScaleType = defaultScaleType;
|
uint8_t storedScaleType = defaultScaleType;
|
||||||
EEPROM.get(EEPROM_ADDR_SCALE_ENABLED, storedScaleEnabled);
|
EEPROM.get(EEPROM_ADDR_SCALE_ENABLED, storedScaleEnabled);
|
||||||
@@ -4356,11 +4433,6 @@ void setup() {
|
|||||||
// Serial.println("---------------------------------------------");
|
// Serial.println("---------------------------------------------");
|
||||||
// Serial.print("Firmware Version: "); Serial.println(version);
|
// Serial.print("Firmware Version: "); Serial.println(version);
|
||||||
|
|
||||||
// #ifdef ENABLE_DISPLAY
|
|
||||||
// Serial.println("Display: Aktiviert");
|
|
||||||
// #else
|
|
||||||
// Serial.println("Display: Deaktiviert");
|
|
||||||
// #endif
|
|
||||||
// if (WiFi.getMode() == WIFI_AP || WiFi.status() != WL_CONNECTED) {
|
// if (WiFi.getMode() == WIFI_AP || WiFi.status() != WL_CONNECTED) {
|
||||||
// Serial.println(F("WiFi Modus: Access Point (AP)"));
|
// Serial.println(F("WiFi Modus: Access Point (AP)"));
|
||||||
// Serial.print(F("SSID: ")); Serial.println(WiFi.softAPSSID());
|
// Serial.print(F("SSID: ")); Serial.println(WiFi.softAPSSID());
|
||||||
@@ -4393,51 +4465,13 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Display-Initialisierung (nur wenn aktiviert) ---
|
// --- Display-Initialisierung gemaess Web-UI-Einstellung (displayType) ---
|
||||||
#ifdef ENABLE_DISPLAY
|
|
||||||
recordResetCheckpoint(RESET_CP_SETUP_DISPLAY);
|
recordResetCheckpoint(RESET_CP_SETUP_DISPLAY);
|
||||||
if (!display.begin(0x3C, true)) { // display.begin benutzt das ggf. schon initialisierte Wire
|
if (displayType == DISPLAY_TYPE_OLED) {
|
||||||
Serial.println(F("Display konnte nicht initialisiert werden!"));
|
initOledDisplay(true);
|
||||||
} else {
|
} else if (displayType == DISPLAY_TYPE_TOUCH_UART) {
|
||||||
Serial.println(F("Display initialisiert."));
|
initTouchUart();
|
||||||
// Display-Ausgabe am Start (1)
|
|
||||||
display.clearDisplay();
|
|
||||||
display.setCursor(0, 0);
|
|
||||||
display.setTextColor(SH110X_WHITE);
|
|
||||||
display.setTextSize(1);
|
|
||||||
display.println(infoHersteller);
|
|
||||||
display.println(infoModell);
|
|
||||||
display.println(infoZusatz);
|
|
||||||
display.println("");
|
|
||||||
display.println(F("Dual-PID"));
|
|
||||||
display.print(F("Version: "));
|
|
||||||
display.println(version);
|
|
||||||
display.println(F("von Thomas M\201ller")); // \201 ist ü für manche Displays
|
|
||||||
display.display();
|
|
||||||
delay(delayInit1); // Verzögerung nur bei aktiviertem Display
|
|
||||||
|
|
||||||
// Display-Ausgabe am Start (2) - WiFi Info
|
|
||||||
display.clearDisplay();
|
|
||||||
display.setCursor(0, 0);
|
|
||||||
display.setTextColor(SH110X_WHITE);
|
|
||||||
display.setTextSize(1);
|
|
||||||
display.println(infoHersteller);
|
|
||||||
display.println(infoModell);
|
|
||||||
display.println(infoZusatz);
|
|
||||||
display.println("");
|
|
||||||
if (WiFi.status() != WL_CONNECTED) {
|
|
||||||
display.println(F("WiFi: AP-Modus"));
|
|
||||||
display.print(F("SSID: ")); display.println(WiFi.softAPSSID());
|
|
||||||
display.print(F("IP: ")); display.println(WiFi.softAPIP());
|
|
||||||
} else {
|
|
||||||
display.println(F("Webserver gestartet"));
|
|
||||||
display.print(F("IP: ")); display.println(WiFi.localIP());
|
|
||||||
display.print(F("Host: ")); display.print(hostname); display.println(".local");
|
|
||||||
}
|
|
||||||
display.display();
|
|
||||||
delay(delayInit2); // Verzögerung nur bei aktiviertem Display
|
|
||||||
}
|
}
|
||||||
#endif // ENABLE_DISPLAY
|
|
||||||
|
|
||||||
|
|
||||||
// --- Waage Initialisierung (nur ESP32) ---
|
// --- Waage Initialisierung (nur ESP32) ---
|
||||||
@@ -5177,10 +5211,10 @@ void loop() {
|
|||||||
applyLightOutput();
|
applyLightOutput();
|
||||||
|
|
||||||
// Display regelmaessig updaten, wenn aktiviert / angeschlossen
|
// Display regelmaessig updaten, wenn aktiviert / angeschlossen
|
||||||
#ifdef ENABLE_DISPLAY
|
if (displayType == DISPLAY_TYPE_OLED && oledDisplayPresent) {
|
||||||
recordResetCheckpoint(RESET_CP_LOOP_DISPLAY);
|
recordResetCheckpoint(RESET_CP_LOOP_DISPLAY);
|
||||||
updateDisplay();
|
updateDisplay();
|
||||||
#endif // ENABLE_DISPLAY
|
}
|
||||||
|
|
||||||
// --- Waagen-spezifische Logik in der Hauptschleife ---
|
// --- Waagen-spezifische Logik in der Hauptschleife ---
|
||||||
recordResetCheckpoint(RESET_CP_LOOP_SCALE);
|
recordResetCheckpoint(RESET_CP_LOOP_SCALE);
|
||||||
@@ -5350,8 +5384,108 @@ double readCaseTemperature() {
|
|||||||
return readNTCTemperatureFromPin(ANALOG_CASE_NTC_PIN);
|
return readNTCTemperatureFromPin(ANALOG_CASE_NTC_PIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_DISPLAY
|
/************************************************************************************
|
||||||
|
* OLED-Display initialisieren (Laufzeit, mit I2C-Probe als Boot-Schutz)
|
||||||
|
* Liefert true, wenn das Display gefunden und initialisiert wurde. Ohne ACK auf
|
||||||
|
* OLED_I2C_ADDRESS bleibt die Anzeige deaktiviert und die Steuerung startet normal.
|
||||||
|
************************************************************************************/
|
||||||
|
bool initOledDisplay(bool showSplash) {
|
||||||
|
oledDisplayPresent = false;
|
||||||
|
|
||||||
|
// I2C-Probe: haengt kein Display am Bus, wird der Init uebersprungen statt zu blockieren.
|
||||||
|
Wire.beginTransmission(OLED_I2C_ADDRESS);
|
||||||
|
if (Wire.endTransmission() != 0) {
|
||||||
|
Serial.println(F("OLED-Display nicht gefunden (I2C 0x3C) - Anzeige deaktiviert."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!display.begin(OLED_I2C_ADDRESS, true)) { // display.begin benutzt das ggf. schon initialisierte Wire
|
||||||
|
Serial.println(F("Display konnte nicht initialisiert werden!"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
oledDisplayPresent = true;
|
||||||
|
Serial.println(F("Display initialisiert."));
|
||||||
|
|
||||||
|
if (showSplash) {
|
||||||
|
// Display-Ausgabe am Start (1)
|
||||||
|
display.clearDisplay();
|
||||||
|
display.setCursor(0, 0);
|
||||||
|
display.setTextColor(SH110X_WHITE);
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.println(infoHersteller);
|
||||||
|
display.println(infoModell);
|
||||||
|
display.println(infoZusatz);
|
||||||
|
display.println("");
|
||||||
|
display.println(F("Dual-PID"));
|
||||||
|
display.print(F("Version: "));
|
||||||
|
display.println(version);
|
||||||
|
display.println(F("von Thomas M\201ller")); // \201 ist ü für manche Displays
|
||||||
|
display.display();
|
||||||
|
delay(delayInit1); // Verzögerung nur bei aktiviertem Display
|
||||||
|
|
||||||
|
// Display-Ausgabe am Start (2) - WiFi Info
|
||||||
|
display.clearDisplay();
|
||||||
|
display.setCursor(0, 0);
|
||||||
|
display.setTextColor(SH110X_WHITE);
|
||||||
|
display.setTextSize(1);
|
||||||
|
display.println(infoHersteller);
|
||||||
|
display.println(infoModell);
|
||||||
|
display.println(infoZusatz);
|
||||||
|
display.println("");
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
display.println(F("WiFi: AP-Modus"));
|
||||||
|
display.print(F("SSID: ")); display.println(WiFi.softAPSSID());
|
||||||
|
display.print(F("IP: ")); display.println(WiFi.softAPIP());
|
||||||
|
} else {
|
||||||
|
display.println(F("Webserver gestartet"));
|
||||||
|
display.print(F("IP: ")); display.println(WiFi.localIP());
|
||||||
|
display.print(F("Host: ")); display.print(hostname); display.println(".local");
|
||||||
|
}
|
||||||
|
display.display();
|
||||||
|
delay(delayInit2); // Verzögerung nur bei aktiviertem Display
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/************************************************************************************
|
||||||
|
* Display-Typ zur Laufzeit umschalten (Web-UI /Sensoren)
|
||||||
|
* Deaktiviert das bisherige Display sauber und initialisiert das neue - ganz ohne
|
||||||
|
* Neustart. Der Splash wird beim Laufzeit-Wechsel uebersprungen (keine delays im
|
||||||
|
* Web-Handler-Kontext).
|
||||||
|
************************************************************************************/
|
||||||
|
void applyDisplayTypeChange(uint8_t newType) {
|
||||||
|
if (newType > DISPLAY_TYPE_TOUCH_UART) { newType = DEFAULT_DISPLAY_TYPE; }
|
||||||
|
if (newType == displayType) { return; }
|
||||||
|
|
||||||
|
// Bisheriges Display deaktivieren
|
||||||
|
if (displayType == DISPLAY_TYPE_OLED && oledDisplayPresent) {
|
||||||
|
display.clearDisplay();
|
||||||
|
display.display();
|
||||||
|
oledDisplayPresent = false;
|
||||||
|
}
|
||||||
|
if (displayType == DISPLAY_TYPE_TOUCH_UART) {
|
||||||
|
#if TOUCH_UART_ENABLED
|
||||||
|
touchUart.end();
|
||||||
|
#endif
|
||||||
|
touchUartClientActive = false;
|
||||||
|
touchUartClientFirmware = "";
|
||||||
|
touchUartRxLine = "";
|
||||||
|
touchUartRxOverflow = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayType = newType;
|
||||||
|
|
||||||
|
if (displayType == DISPLAY_TYPE_OLED) {
|
||||||
|
initOledDisplay(false);
|
||||||
|
} else if (displayType == DISPLAY_TYPE_TOUCH_UART) {
|
||||||
|
initTouchUart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void updateDisplay() {
|
void updateDisplay() {
|
||||||
|
if (!oledDisplayPresent) { return; }
|
||||||
recordResetCheckpoint(RESET_CP_DISPLAY_RENDER);
|
recordResetCheckpoint(RESET_CP_DISPLAY_RENDER);
|
||||||
double displayedWaterTemp = getDisplayedWaterTemperature();
|
double displayedWaterTemp = getDisplayedWaterTemperature();
|
||||||
double displayedSteamTemp = getDisplayedSteamTemperature();
|
double displayedSteamTemp = getDisplayedSteamTemperature();
|
||||||
@@ -5992,7 +6126,6 @@ if (scaleModeActive) {
|
|||||||
yield();
|
yield();
|
||||||
display.display(); // Display *immer* am Ende aktualisieren
|
display.display(); // Display *immer* am Ende aktualisieren
|
||||||
}
|
}
|
||||||
#endif // ENABLE_DISPLAY
|
|
||||||
|
|
||||||
void updateShotTimer() {
|
void updateShotTimer() {
|
||||||
if (cleaningAssistantActive) {
|
if (cleaningAssistantActive) {
|
||||||
@@ -6286,6 +6419,7 @@ void handleAutoTune(AsyncWebServerRequest *request) {
|
|||||||
autoTuneWasserResultKi = (float)KiWasser;
|
autoTuneWasserResultKi = (float)KiWasser;
|
||||||
autoTuneWasserResultKd = (float)KdWasser;
|
autoTuneWasserResultKd = (float)KdWasser;
|
||||||
autoTuneWasserStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS;
|
autoTuneWasserStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS;
|
||||||
|
markProfileDirty(); // PID-Werte weichen jetzt vom geladenen Profil ab
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serial.println("AutoTune Wasser erfolgreich abgeschlossen.");
|
// Serial.println("AutoTune Wasser erfolgreich abgeschlossen.");
|
||||||
@@ -6344,6 +6478,7 @@ void handleAutoTune(AsyncWebServerRequest *request) {
|
|||||||
autoTuneDampfResultKi = (float)KiDampf;
|
autoTuneDampfResultKi = (float)KiDampf;
|
||||||
autoTuneDampfResultKd = (float)KdDampf;
|
autoTuneDampfResultKd = (float)KdDampf;
|
||||||
autoTuneDampfStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS;
|
autoTuneDampfStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS;
|
||||||
|
markProfileDirty(); // PID-Werte weichen jetzt vom geladenen Profil ab
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serial.println("AutoTune Dampf erfolgreich abgeschlossen.");
|
// Serial.println("AutoTune Dampf erfolgreich abgeschlossen.");
|
||||||
@@ -6640,7 +6775,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
|
|||||||
if (update.hasFlowGuardEnabled && update.flowGuardEnabled != flowGuardEnabled) {
|
if (update.hasFlowGuardEnabled && update.flowGuardEnabled != flowGuardEnabled) {
|
||||||
flowGuardEnabled = update.flowGuardEnabled;
|
flowGuardEnabled = update.flowGuardEnabled;
|
||||||
EEPROM.put(EEPROM_ADDR_FLOWGUARD_ENABLED, flowGuardEnabled);
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_ENABLED, flowGuardEnabled);
|
||||||
changed = true;
|
|
||||||
flowGuardConfigChanged = true;
|
flowGuardConfigChanged = true;
|
||||||
}
|
}
|
||||||
if (update.hasFlowGuardMinSecs) {
|
if (update.hasFlowGuardMinSecs) {
|
||||||
@@ -6648,7 +6782,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
|
|||||||
if (newFlowGuardMinSecs >= FLOW_GUARD_MIN_SECONDS_MIN && newFlowGuardMinSecs <= FLOW_GUARD_MIN_SECONDS_MAX && fabs(newFlowGuardMinSecs - flowGuardMinBrewSeconds) > 0.01f) {
|
if (newFlowGuardMinSecs >= FLOW_GUARD_MIN_SECONDS_MIN && newFlowGuardMinSecs <= FLOW_GUARD_MIN_SECONDS_MAX && fabs(newFlowGuardMinSecs - flowGuardMinBrewSeconds) > 0.01f) {
|
||||||
flowGuardMinBrewSeconds = newFlowGuardMinSecs;
|
flowGuardMinBrewSeconds = newFlowGuardMinSecs;
|
||||||
EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_SECONDS, flowGuardMinBrewSeconds);
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_SECONDS, flowGuardMinBrewSeconds);
|
||||||
changed = true;
|
|
||||||
flowGuardConfigChanged = true;
|
flowGuardConfigChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6657,7 +6790,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
|
|||||||
if (newPulseMs >= FLOW_GUARD_PULSE_PERIOD_MIN_MS && newPulseMs <= FLOW_GUARD_PULSE_PERIOD_MAX_MS && newPulseMs != flowGuardPulsePeriodMs) {
|
if (newPulseMs >= FLOW_GUARD_PULSE_PERIOD_MIN_MS && newPulseMs <= FLOW_GUARD_PULSE_PERIOD_MAX_MS && newPulseMs != flowGuardPulsePeriodMs) {
|
||||||
flowGuardPulsePeriodMs = newPulseMs;
|
flowGuardPulsePeriodMs = newPulseMs;
|
||||||
EEPROM.put(EEPROM_ADDR_FLOWGUARD_PULSE_PERIOD_MS, flowGuardPulsePeriodMs);
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_PULSE_PERIOD_MS, flowGuardPulsePeriodMs);
|
||||||
changed = true;
|
|
||||||
flowGuardConfigChanged = true;
|
flowGuardConfigChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6666,7 +6798,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
|
|||||||
if (newFlowGuardMinDuty >= FLOW_GUARD_MIN_DUTY_MIN && newFlowGuardMinDuty <= FLOW_GUARD_MIN_DUTY_MAX && fabs(newFlowGuardMinDuty - flowGuardMinDutyPercent) > 0.01f) {
|
if (newFlowGuardMinDuty >= FLOW_GUARD_MIN_DUTY_MIN && newFlowGuardMinDuty <= FLOW_GUARD_MIN_DUTY_MAX && fabs(newFlowGuardMinDuty - flowGuardMinDutyPercent) > 0.01f) {
|
||||||
flowGuardMinDutyPercent = newFlowGuardMinDuty;
|
flowGuardMinDutyPercent = newFlowGuardMinDuty;
|
||||||
EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_DUTY, flowGuardMinDutyPercent);
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_DUTY, flowGuardMinDutyPercent);
|
||||||
changed = true;
|
|
||||||
flowGuardConfigChanged = true;
|
flowGuardConfigChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6711,10 +6842,16 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "changed" umfasst hier nur noch profil-relevante Felder (BBT/BBW/PI/SBT);
|
||||||
|
// FlowGuard ist kein Bestandteil von TemperatureProfile und laeuft ueber
|
||||||
|
// flowGuardConfigChanged separat -> markProfileDirty() bleibt praezise.
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
markProfileDirty();
|
||||||
|
}
|
||||||
|
if (changed || flowGuardConfigChanged) {
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
}
|
}
|
||||||
return changed;
|
return changed || flowGuardConfigChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleSaveBrewControl(AsyncWebServerRequest *request) {
|
void handleSaveBrewControl(AsyncWebServerRequest *request) {
|
||||||
@@ -7652,6 +7789,25 @@ static const char sensorsHtmlHeadEnd[] PROGMEM = R"rawliteral(
|
|||||||
static const char sensorsBodyStart[] PROGMEM = R"rawliteral(
|
static const char sensorsBodyStart[] PROGMEM = R"rawliteral(
|
||||||
<body>
|
<body>
|
||||||
<h1>Sensoren</h1>
|
<h1>Sensoren</h1>
|
||||||
|
<form action='/updateSensorSettings' method='POST'>
|
||||||
|
<input type='hidden' name='sensorSection' value='display'>
|
||||||
|
<h3>Display</h3>
|
||||||
|
<label for='displayType'>Typ:</label>
|
||||||
|
<select id='displayType' name='displayType'>
|
||||||
|
<option value='0' )rawliteral";
|
||||||
|
|
||||||
|
static const char sensorsDisplayOptionMid1[] PROGMEM = R"rawliteral(>Ohne Display</option>
|
||||||
|
<option value='1' )rawliteral";
|
||||||
|
|
||||||
|
static const char sensorsDisplayOptionMid2[] PROGMEM = R"rawliteral(>OLED-Display (SH1106, I2C)</option>
|
||||||
|
<option value='2' )rawliteral";
|
||||||
|
|
||||||
|
static const char sensorsDisplaySectionEnd[] PROGMEM = R"rawliteral(>UART-Touch-Display</option>
|
||||||
|
</select>
|
||||||
|
<small class="select-description">Wird sofort übernommen, kein Neustart nötig. Das OLED wird nur aktiviert, wenn es am I2C-Bus (0x3C) gefunden wird – ohne angeschlossenes Display startet die Steuerung immer sicher.</small>
|
||||||
|
<br>
|
||||||
|
<input type='submit' value='Einstellungen speichern'>
|
||||||
|
</form>
|
||||||
<form action='/updateSensorSettings' method='POST'>
|
<form action='/updateSensorSettings' method='POST'>
|
||||||
<input type='hidden' name='sensorSection' value='case'>
|
<input type='hidden' name='sensorSection' value='case'>
|
||||||
<h3>Zusatz-Temperatur-Sensor</h3>
|
<h3>Zusatz-Temperatur-Sensor</h3>
|
||||||
@@ -7902,6 +8058,18 @@ void handleSensors(AsyncWebServerRequest *request) {
|
|||||||
response->print(FPSTR(sensorsHtmlHeadEnd));
|
response->print(FPSTR(sensorsHtmlHeadEnd));
|
||||||
response->print(FPSTR(commonNav));
|
response->print(FPSTR(commonNav));
|
||||||
response->print(FPSTR(sensorsBodyStart));
|
response->print(FPSTR(sensorsBodyStart));
|
||||||
|
if (displayType == DISPLAY_TYPE_NONE) {
|
||||||
|
response->print(F("selected"));
|
||||||
|
}
|
||||||
|
response->print(FPSTR(sensorsDisplayOptionMid1));
|
||||||
|
if (displayType == DISPLAY_TYPE_OLED) {
|
||||||
|
response->print(F("selected"));
|
||||||
|
}
|
||||||
|
response->print(FPSTR(sensorsDisplayOptionMid2));
|
||||||
|
if (displayType == DISPLAY_TYPE_TOUCH_UART) {
|
||||||
|
response->print(F("selected"));
|
||||||
|
}
|
||||||
|
response->print(FPSTR(sensorsDisplaySectionEnd));
|
||||||
if (caseSensorEnabled) {
|
if (caseSensorEnabled) {
|
||||||
response->print(F(" checked"));
|
response->print(F(" checked"));
|
||||||
}
|
}
|
||||||
@@ -8409,6 +8577,7 @@ void handleFastHeatUpSettings(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
|
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
markProfileDirty(); // fastHeatUpAktiv ist Profilfeld
|
||||||
|
|
||||||
AsyncWebServerResponse *resp = request->beginResponse(303);
|
AsyncWebServerResponse *resp = request->beginResponse(303);
|
||||||
resp->addHeader(F("Location"), F("/Fast-Heat-Up"));
|
resp->addHeader(F("Location"), F("/Fast-Heat-Up"));
|
||||||
@@ -8901,6 +9070,7 @@ void handleSettingsUpdate(AsyncWebServerRequest *request) {
|
|||||||
// --- EEPROM speichern und PID aktualisieren ---
|
// --- EEPROM speichern und PID aktualisieren ---
|
||||||
if (valueChanged) { // Nur speichern, wenn sich mindestens ein Wert geändert hat
|
if (valueChanged) { // Nur speichern, wenn sich mindestens ein Wert geändert hat
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
markProfileDirty(); // Setpoints/Offsets/PID/Boost/MaxTemp/Window sind Profilfelder
|
||||||
}
|
}
|
||||||
|
|
||||||
// PID-Parameter an die Regler übergeben
|
// PID-Parameter an die Regler übergeben
|
||||||
@@ -9288,6 +9458,7 @@ void handleUpdateAutotuneSettings(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
markProfileDirty(); // AutoTune-Parameter sind Profilfelder
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncWebServerResponse *response = request->beginResponse(303);
|
AsyncWebServerResponse *response = request->beginResponse(303);
|
||||||
@@ -9635,6 +9806,8 @@ void handleEcoUpdate(AsyncWebServerRequest *request) {
|
|||||||
EEPROM.put(EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT, backlightActivePercent);
|
EEPROM.put(EEPROM_ADDR_BACKLIGHT_ACTIVE_PERCENT, backlightActivePercent);
|
||||||
EEPROM.put(EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT, backlightStandbyClockPercent);
|
EEPROM.put(EEPROM_ADDR_BACKLIGHT_STANDBY_CLOCK_PERCENT, backlightStandbyClockPercent);
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
// Eco/Dampfverzoegerung/Wake-Verhalten sind Profilfelder -> Profil als geaendert markieren
|
||||||
|
markProfileDirty();
|
||||||
|
|
||||||
// Wenn Eco deaktiviert, Setpoints zurückladen
|
// Wenn Eco deaktiviert, Setpoints zurückladen
|
||||||
if (ecoModeMinutes == 0 && !ecoSwitchActive) {
|
if (ecoModeMinutes == 0 && !ecoSwitchActive) {
|
||||||
@@ -9698,6 +9871,7 @@ void handleUpdatePiezoSettings(AsyncWebServerRequest *request) {
|
|||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
markProfileDirty(); // piezoEnabled ist Teil des Profils
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncWebServerResponse *resp = request->beginResponse(303);
|
AsyncWebServerResponse *resp = request->beginResponse(303);
|
||||||
@@ -9806,6 +9980,10 @@ struct SensorSettingsUpdate {
|
|||||||
uint8_t xSwitchLongAction = 0;
|
uint8_t xSwitchLongAction = 0;
|
||||||
bool hasButtonLongPressMs = false;
|
bool hasButtonLongPressMs = false;
|
||||||
uint16_t buttonLongPressMs = 0;
|
uint16_t buttonLongPressMs = 0;
|
||||||
|
|
||||||
|
bool updateDisplaySection = false;
|
||||||
|
bool hasDisplayType = false;
|
||||||
|
uint8_t displayType = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SensorSettingsApplyResult {
|
struct SensorSettingsApplyResult {
|
||||||
@@ -9944,6 +10122,16 @@ SensorSettingsApplyResult applySensorSettingsUpdate(const SensorSettingsUpdate&
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (update.updateDisplaySection && update.hasDisplayType) {
|
||||||
|
uint8_t newDisplayType = update.displayType;
|
||||||
|
if (newDisplayType > DISPLAY_TYPE_TOUCH_UART) { newDisplayType = DEFAULT_DISPLAY_TYPE; }
|
||||||
|
if (newDisplayType != displayType) {
|
||||||
|
applyDisplayTypeChange(newDisplayType);
|
||||||
|
EEPROM.put(EEPROM_ADDR_DISPLAY_TYPE, displayType);
|
||||||
|
result.changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (result.changed) {
|
if (result.changed) {
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
}
|
}
|
||||||
@@ -9959,11 +10147,13 @@ void handleUpdateSensorSettings(AsyncWebServerRequest *request) {
|
|||||||
bool isCaseSection = (section.length() == 0 || section == "case");
|
bool isCaseSection = (section.length() == 0 || section == "case");
|
||||||
bool isScaleSection = (section.length() == 0 || section == "scale");
|
bool isScaleSection = (section.length() == 0 || section == "scale");
|
||||||
bool isXSwitchSection = (section.length() == 0 || section == "xSwitch");
|
bool isXSwitchSection = (section.length() == 0 || section == "xSwitch");
|
||||||
|
bool isDisplaySection = (section.length() == 0 || section == "display");
|
||||||
|
|
||||||
SensorSettingsUpdate update;
|
SensorSettingsUpdate update;
|
||||||
update.updateCaseSection = isCaseSection;
|
update.updateCaseSection = isCaseSection;
|
||||||
update.updateScaleSection = isScaleSection;
|
update.updateScaleSection = isScaleSection;
|
||||||
update.updateXSwitchSection = isXSwitchSection;
|
update.updateXSwitchSection = isXSwitchSection;
|
||||||
|
update.updateDisplaySection = isDisplaySection;
|
||||||
|
|
||||||
if (isCaseSection) {
|
if (isCaseSection) {
|
||||||
update.hasCaseEnabled = true;
|
update.hasCaseEnabled = true;
|
||||||
@@ -9989,6 +10179,11 @@ void handleUpdateSensorSettings(AsyncWebServerRequest *request) {
|
|||||||
update.hx711DisplaySmoothing = request->hasArg("hx711DisplaySmoothing");
|
update.hx711DisplaySmoothing = request->hasArg("hx711DisplaySmoothing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDisplaySection) {
|
||||||
|
update.hasDisplayType = request->hasArg("displayType");
|
||||||
|
if (update.hasDisplayType) { update.displayType = (uint8_t)request->arg("displayType").toInt(); }
|
||||||
|
}
|
||||||
|
|
||||||
if (isXSwitchSection) {
|
if (isXSwitchSection) {
|
||||||
update.hasXSwitchAction = request->hasArg("xSwitchAction");
|
update.hasXSwitchAction = request->hasArg("xSwitchAction");
|
||||||
if (update.hasXSwitchAction) { update.xSwitchAction = (uint8_t)request->arg("xSwitchAction").toInt(); }
|
if (update.hasXSwitchAction) { update.xSwitchAction = (uint8_t)request->arg("xSwitchAction").toInt(); }
|
||||||
@@ -11152,6 +11347,8 @@ bool saveProfile(const String& profileName, const TemperatureProfile& profileDat
|
|||||||
|
|
||||||
if (bytesWritten == sizeof(TemperatureProfile)) {
|
if (bytesWritten == sizeof(TemperatureProfile)) {
|
||||||
// Serial.println(" Profile saved successfully (Binary Format).");
|
// Serial.println(" Profile saved successfully (Binary Format).");
|
||||||
|
// Gespeicherter Ist-Zustand entspricht ab jetzt diesem Profil -> als aktiv merken
|
||||||
|
setActiveProfile(displayName);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Serial.printf(" ERROR: Failed to write complete profile data! Bytes written: %d, Expected: %d\n", bytesWritten, sizeof(TemperatureProfile));
|
// Serial.printf(" ERROR: Failed to write complete profile data! Bytes written: %d, Expected: %d\n", bytesWritten, sizeof(TemperatureProfile));
|
||||||
@@ -11299,6 +11496,9 @@ void applyProfileSettings(const TemperatureProfile& profileData) {
|
|||||||
// Eco-Status zurücksetzen
|
// Eco-Status zurücksetzen
|
||||||
ecoModeAktiv = false; ecoModeActivatedTime = 0;
|
ecoModeAktiv = false; ecoModeActivatedTime = 0;
|
||||||
// Serial.println(" Eco mode status reset.");
|
// Serial.println(" Eco mode status reset.");
|
||||||
|
|
||||||
|
// Erfolgreich angewendet -> als aktives Profil merken (setzt Dirty-Flag zurück)
|
||||||
|
setActiveProfile(normalizeProfileDisplayName(String(profileData.profileName)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -11361,6 +11561,11 @@ bool deleteProfile(const String& profileName) {
|
|||||||
if (LittleFS.exists(filePath)) {
|
if (LittleFS.exists(filePath)) {
|
||||||
if (LittleFS.remove(filePath)) {
|
if (LittleFS.remove(filePath)) {
|
||||||
// Serial.println(" Profile deleted successfully.");
|
// Serial.println(" Profile deleted successfully.");
|
||||||
|
// War das geloeschte Profil das aktive -> Anzeige zuruecksetzen
|
||||||
|
if (activeProfileName.length() > 0 &&
|
||||||
|
sanitizeProfileName(activeProfileName) == sanitizedName) {
|
||||||
|
clearActiveProfile();
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Serial.println(" ERROR: Failed to delete profile file.");
|
// Serial.println(" ERROR: Failed to delete profile file.");
|
||||||
@@ -11626,6 +11831,19 @@ void handleProfilesPage(AsyncWebServerRequest *request) {
|
|||||||
profileStatusMessage = ""; // Nachricht entfernen, nachdem sie angezeigt wurde
|
profileStatusMessage = ""; // Nachricht entfernen, nachdem sie angezeigt wurde
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Aktives Profil anzeigen (zuletzt geladen/gespeichert; Stern = danach geändert)
|
||||||
|
{
|
||||||
|
String activeDiv = "<div style=\"margin-bottom:12px;opacity:0.85;\">Aktives Profil: ";
|
||||||
|
if (activeProfileName.length() > 0) {
|
||||||
|
activeDiv += htmlEscape(activeProfileName);
|
||||||
|
if (activeProfileDirty) { activeDiv += " ✱ (geändert)"; }
|
||||||
|
} else {
|
||||||
|
activeDiv += "—";
|
||||||
|
}
|
||||||
|
activeDiv += "</div>";
|
||||||
|
response->print(activeDiv);
|
||||||
|
}
|
||||||
|
|
||||||
// Profilliste generieren
|
// Profilliste generieren
|
||||||
response->print(FPSTR(profilesPageListStart)); // <ul>
|
response->print(FPSTR(profilesPageListStart)); // <ul>
|
||||||
if (profiles.empty()) {
|
if (profiles.empty()) {
|
||||||
@@ -11637,6 +11855,9 @@ void handleProfilesPage(AsyncWebServerRequest *request) {
|
|||||||
String escapedConfirmJs = htmlEscape(confirmJs);
|
String escapedConfirmJs = htmlEscape(confirmJs);
|
||||||
String listItem = FPSTR(profilesPageListItemStart);
|
String listItem = FPSTR(profilesPageListItemStart);
|
||||||
listItem += escapedName;
|
listItem += escapedName;
|
||||||
|
if (name == activeProfileName) {
|
||||||
|
listItem += F(" <span style=\"color:#4CAF50;\" title=\"aktives Profil\">●</span>");
|
||||||
|
}
|
||||||
listItem += F("</span><div class=\"profile-actions\">");
|
listItem += F("</span><div class=\"profile-actions\">");
|
||||||
listItem += F("<form action=\"/loadProfile\" method=\"POST\" class=\"profiles-form\"><input type=\"hidden\" name=\"profileName\" value=\"");
|
listItem += F("<form action=\"/loadProfile\" method=\"POST\" class=\"profiles-form\"><input type=\"hidden\" name=\"profileName\" value=\"");
|
||||||
listItem += escapedName;
|
listItem += escapedName;
|
||||||
@@ -13440,6 +13661,7 @@ static const char dashboardControlPanelHTML[] PROGMEM = R"rawliteral(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>Profile</h3>
|
<h3>Profile</h3>
|
||||||
|
<div id="active_profile_line" style="margin-bottom:8px;opacity:0.85;">Aktives Profil: <span id="active_profile_name">—</span></div>
|
||||||
<select id="profile_select">
|
<select id="profile_select">
|
||||||
<option value="">Profil wählen...</option>
|
<option value="">Profil wählen...</option>
|
||||||
{PROFILE_OPTIONS} </select>
|
{PROFILE_OPTIONS} </select>
|
||||||
@@ -13747,6 +13969,16 @@ static const char dashboardJavaScript[] PROGMEM = R"rawliteral(
|
|||||||
// Status Text
|
// Status Text
|
||||||
if (statusSpan) { statusSpan.textContent = data.statusText || 'Unbekannt'; statusSpan.classList.toggle('error', data.errorW || data.errorD || data.safetyW || data.safetyD); }
|
if (statusSpan) { statusSpan.textContent = data.statusText || 'Unbekannt'; statusSpan.classList.toggle('error', data.errorW || data.errorD || data.safetyW || data.safetyD); }
|
||||||
|
|
||||||
|
// Aktives Profil (zuletzt geladen/gespeichert; Stern = danach geänderte Einstellungen)
|
||||||
|
const activeProfileEl = document.getElementById('active_profile_name');
|
||||||
|
if (activeProfileEl) {
|
||||||
|
if (data.profile && data.profile.length > 0) {
|
||||||
|
activeProfileEl.textContent = data.profile + (data.profDirty ? ' ✱ (geändert)' : '');
|
||||||
|
} else {
|
||||||
|
activeProfileEl.textContent = '—';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (caseTempDisplay) {
|
if (caseTempDisplay) {
|
||||||
const showCaseTemp = (data.caseTempDashboard === true);
|
const showCaseTemp = (data.caseTempDashboard === true);
|
||||||
caseTempDisplay.style.display = showCaseTemp ? 'block' : 'none';
|
caseTempDisplay.style.display = showCaseTemp ? 'block' : 'none';
|
||||||
@@ -14620,6 +14852,14 @@ bool executeDashboardAction(const String& action, const String& value,
|
|||||||
// --- Abschlussaktionen ---
|
// --- Abschlussaktionen ---
|
||||||
if (settingsChanged) {
|
if (settingsChanged) {
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
// Profil-relevante Aenderung (Feld ist Teil von TemperatureProfile) -> aktives
|
||||||
|
// Profil als "geaendert" markieren. Reine Anzeige-/Systemeinstellungen
|
||||||
|
// (setTempDisplayMode, setHeatUpMinutes, toggleLight) zaehlen nicht.
|
||||||
|
if (action == "setTempW" || action == "setTempD" ||
|
||||||
|
action == "toggleBoostW" || action == "toggleBoostD" ||
|
||||||
|
action == "togglePI" || action == "toggleBBT" || action == "toggleBBW") {
|
||||||
|
markProfileDirty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pidNeedsUpdate) {
|
if (pidNeedsUpdate) {
|
||||||
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
||||||
@@ -14912,6 +15152,15 @@ void buildDashboardJson(char *buffer, size_t bufferSize) {
|
|||||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"statusText\":\"%s\",", statusText.c_str());
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"statusText\":\"%s\",", statusText.c_str());
|
||||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"statusKey\":\"%s\",", statusKey.c_str());
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"statusKey\":\"%s\",", statusKey.c_str());
|
||||||
|
|
||||||
|
// Zuletzt geladenes Profil (leer = keines) + "geaendert"-Flag
|
||||||
|
{
|
||||||
|
String escapedProfile = activeProfileName;
|
||||||
|
escapedProfile.replace("\\", "\\\\");
|
||||||
|
escapedProfile.replace("\"", "\\\"");
|
||||||
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"profile\":\"%s\",", escapedProfile.c_str());
|
||||||
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"profDirty\":%s,", activeProfileDirty ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
bool caseTempDashboardActive = (caseTempOnDashboard && caseSensorEnabled);
|
bool caseTempDashboardActive = (caseTempOnDashboard && caseSensorEnabled);
|
||||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTempDashboard\":%s,", caseTempDashboardActive ? "true" : "false");
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTempDashboard\":%s,", caseTempDashboardActive ? "true" : "false");
|
||||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"caseType\":%u,", (unsigned int)caseSensorType);
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"caseType\":%u,", (unsigned int)caseSensorType);
|
||||||
@@ -15204,6 +15453,14 @@ static void buildTouchUartStateJson(char *buffer, size_t bufferSize) {
|
|||||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"steamHeatDisabled\":%s,", steamHeatDisabledByUser ? "true" : "false");
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"steamHeatDisabled\":%s,", steamHeatDisabledByUser ? "true" : "false");
|
||||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"lightOn\":%s,", lightOn ? "true" : "false");
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"lightOn\":%s,", lightOn ? "true" : "false");
|
||||||
offset += snprintf(buffer + offset, bufferSize - offset, "\"piezoEnabled\":%s,", piezoEnabled ? "true" : "false");
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"piezoEnabled\":%s,", piezoEnabled ? "true" : "false");
|
||||||
|
// Zuletzt geladenes Profil (leer = keines) + "geaendert"-Flag fuer die Anzeige am Display
|
||||||
|
{
|
||||||
|
String escapedProfile = activeProfileName;
|
||||||
|
escapedProfile.replace("\\", "\\\\");
|
||||||
|
escapedProfile.replace("\"", "\\\"");
|
||||||
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"profile\":\"%s\",", escapedProfile.c_str());
|
||||||
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"profDirty\":%s,", activeProfileDirty ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
dtostrf(getWeightReadingForUi(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"weight\":%s,", floatBuf);
|
dtostrf(getWeightReadingForUi(), 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"weight\":%s,", floatBuf);
|
||||||
dtostrf(brewByWeightTargetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"targetWeight\":%s,", floatBuf);
|
dtostrf(brewByWeightTargetGrams, 4, 1, floatBuf); offset += snprintf(buffer + offset, bufferSize - offset, "\"targetWeight\":%s,", floatBuf);
|
||||||
@@ -15540,8 +15797,21 @@ static bool jsonGetUIntValue(const String& json, const char* key, uint32_t& outV
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void initTouchUart() {
|
||||||
|
#if TOUCH_UART_ENABLED
|
||||||
|
// TX-Puffer >= max. Zeilenlaenge (State-JSON bis TOUCH_UART_JSON_BUFFER_SIZE=3000
|
||||||
|
// + "\r\n"), damit println() die Hauptschleife NIE blockiert (sonst bis ~41 ms
|
||||||
|
// Stall -> PID-Jitter). Headroom deckt zudem OTA-Chunks (~1,4 KB) und gestaute
|
||||||
|
// Nachrichten (ack direkt vor State-Push) ab.
|
||||||
|
touchUart.setTxBufferSize(4096);
|
||||||
|
touchUart.begin(TOUCH_UART_BAUDRATE, SERIAL_8N1, TOUCH_UART_RX_PIN, TOUCH_UART_TX_PIN);
|
||||||
|
touchUartRxLine.reserve(TOUCH_UART_MAX_LINE_LEN);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
static void touchUartSendLine(const String& line) {
|
static void touchUartSendLine(const String& line) {
|
||||||
#if TOUCH_UART_ENABLED
|
#if TOUCH_UART_ENABLED
|
||||||
|
if (displayType != DISPLAY_TYPE_TOUCH_UART) { return; }
|
||||||
touchUart.println(line);
|
touchUart.println(line);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -16126,6 +16396,7 @@ static bool touchUartHandleCommandLine(const String& line) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
EEPROM.commit();
|
EEPROM.commit();
|
||||||
|
markProfileDirty(); // PID-Werte sind Profilfelder
|
||||||
if (changedW) { pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); }
|
if (changedW) { pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); }
|
||||||
if (changedD) { pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); }
|
if (changedD) { pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); }
|
||||||
touchUartSendAck(id, true, "PID gespeichert");
|
touchUartSendAck(id, true, "PID gespeichert");
|
||||||
@@ -16283,6 +16554,7 @@ static bool touchUartHandleCommandLine(const String& line) {
|
|||||||
|
|
||||||
void touchUartRxTick() {
|
void touchUartRxTick() {
|
||||||
#if TOUCH_UART_ENABLED
|
#if TOUCH_UART_ENABLED
|
||||||
|
if (displayType != DISPLAY_TYPE_TOUCH_UART) return;
|
||||||
if (displayOtaActive) return; // waehrend OTA hat der Upload-Handler die UART exklusiv
|
if (displayOtaActive) return; // waehrend OTA hat der Upload-Handler die UART exklusiv
|
||||||
recordResetCheckpoint(RESET_CP_TOUCH_UART_RX);
|
recordResetCheckpoint(RESET_CP_TOUCH_UART_RX);
|
||||||
while (touchUart.available() > 0) {
|
while (touchUart.available() > 0) {
|
||||||
@@ -16359,6 +16631,7 @@ static void touchUartPollWifiScan() {
|
|||||||
|
|
||||||
void touchUartTick() {
|
void touchUartTick() {
|
||||||
#if TOUCH_UART_ENABLED
|
#if TOUCH_UART_ENABLED
|
||||||
|
if (displayType != DISPLAY_TYPE_TOUCH_UART) return;
|
||||||
if (displayOtaActive) return; // waehrend OTA keine State-Pushes
|
if (displayOtaActive) return; // waehrend OTA keine State-Pushes
|
||||||
recordResetCheckpoint(RESET_CP_TOUCH_UART_TX);
|
recordResetCheckpoint(RESET_CP_TOUCH_UART_TX);
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
|
|||||||
@@ -1,3 +1,53 @@
|
|||||||
|
Version 1.0.13:
|
||||||
|
- Profil-Schnellwahl + Anzeige des aktiven Profils (braucht S3-Firmware ab 5.1.1):
|
||||||
|
- Neuer Profil-Chip im Header (auf allen Seiten sichtbar): zeigt das zuletzt geladene bzw.
|
||||||
|
gespeicherte Profil; ein „*" hinter dem Namen bedeutet, dass seitdem profil-relevante
|
||||||
|
Einstellungen geändert wurden. Lange Namen werden gekürzt; ohne bekanntes Profil
|
||||||
|
(oder mit älterer S3-Firmware) bleibt der Chip unsichtbar.
|
||||||
|
- Antippen des Chips öffnet die Profil-Schnellwahl: ein Overlay mit allen gespeicherten
|
||||||
|
Profilen, das aktive mit Häkchen und Akzentfarbe hervorgehoben. Ein Tap lädt das Profil
|
||||||
|
direkt (Toast bestätigt, Chip aktualisiert sich mit dem nächsten State) — zwei Berührungen
|
||||||
|
vom Dashboard zum Profilwechsel statt Umweg über die Profile-Seite.
|
||||||
|
- Profile-Seite: das aktive Profil wird in der Liste mit Häkchen-Symbol und Akzentfarbe
|
||||||
|
markiert.
|
||||||
|
|
||||||
|
Version 1.0.12:
|
||||||
|
- Verlauf-Chart: Flackernde Heizleistungs-Kurven in den großen Zeitfenstern behoben. Die
|
||||||
|
Dezimierung pickte pro Chart-Punkt ein Einzelsample, dessen Abtast-Raster mit jedem
|
||||||
|
Sekundentick um eine Position wanderte — die schnell schaltende Heizleistung (PWM)
|
||||||
|
sprang dadurch sichtbar zwischen zwei Kurvenbildern hin und her. Jetzt wird über den
|
||||||
|
gesamten Zeitabschnitt eines Chart-Punkts gemittelt: das Bild steht ruhig, und die
|
||||||
|
Leistungs-Kurve zeigt die tatsächliche mittlere Heizleistung statt zufälliger
|
||||||
|
Momentwerte. Im 2-min-Fenster (1 Sample je Punkt) ändert sich nichts.
|
||||||
|
|
||||||
|
Version 1.0.11:
|
||||||
|
- Verlauf-Seite ausgebaut (nur P4, keine S3-/Protokolländerung nötig):
|
||||||
|
- Umschaltbares Zeitfenster: Buttons „2 min / 10 min / 30 min / 60 min" in der Kopfzeile.
|
||||||
|
Intern läuft eine 1-s-Historie als Ringpuffer über volle 60 Minuten; das Chart zeigt
|
||||||
|
weiterhin 120 Punkte und dezimiert je Fenster. Dadurch ist beim Fensterwechsel der
|
||||||
|
komplette Verlauf sofort da (kein leeres Chart, kein Neuaufbau). Die Wahl wird P4-lokal
|
||||||
|
im NVS gespeichert und überlebt Neustarts.
|
||||||
|
- Gestrichelte Soll-Linien für Wasser und Dampf im Chart (Farbe der jeweiligen Kurve,
|
||||||
|
gedimmt). Die Y-Skala bezieht die Soll-Werte mit ein, damit die Linien im Bild liegen —
|
||||||
|
aber nur für Kreise, deren Ist-Kurve im Fenster sichtbar ist (ein kalter Dampfkessel
|
||||||
|
zieht die Skala nicht auseinander).
|
||||||
|
- Heizleistung zuschaltbar: Button „Leistung" blendet Duty-Cycle Wasser/Dampf als gedimmte
|
||||||
|
Zusatzkurven auf einer zweiten Achse (0–100 %) ein, inkl. Live-Legende. Einstellung
|
||||||
|
ebenfalls P4-lokal im NVS.
|
||||||
|
- Feinere Kurven: Die Temperatur-Historie wird jetzt in Zehntelgrad geführt und gezeichnet
|
||||||
|
(vorher ganzzahlig gerundet) — PID-Schwingungen um ±1 °C sind damit sauber ablesbar.
|
||||||
|
- Die ~29 KB Historie liegen im PSRAM (heap_caps_malloc, wie die LVGL-Framebuffer) —
|
||||||
|
als statische Arrays sprengten sie den internen RAM (Linker: 107 % belegt).
|
||||||
|
- Shot-Zusammenfassung: Karte auf 560 px verbreitert und Spaltenabstand zwischen
|
||||||
|
Dauer/Gewicht/Flow deutlich vergrößert (28 px statt 10 px) — die drei Werte standen
|
||||||
|
zu nahe beisammen und waren schlecht lesbar.
|
||||||
|
- PID-AutoTune sichtbar gemacht: Während eines Tuning-Laufs erscheint auf der Verlauf-Seite
|
||||||
|
eine Live-Karte (mit Spinner) — je Kreis Laufzeit, aktuelle Phase (heizt/kühlt ab) und
|
||||||
|
Schwingungshub samt Mittelpunkt (Min/Max der Ist-Temperatur seit Start). Nach dem Ende
|
||||||
|
zeigt die Karte ~30 s das Ergebnis (Erfolg/Abbruch samt Grund, farbcodiert). Die
|
||||||
|
PID-Tuning-Meldung in der Statuszeile ist jetzt antippbar und springt zur Verlauf-Seite,
|
||||||
|
wo die Schwingung auch als Kurve zu sehen ist.
|
||||||
|
|
||||||
Version 1.0.10:
|
Version 1.0.10:
|
||||||
- Live-Bezugsschirm (abschaltbar): Beim Start eines echten Bezugs blendet sich ein Vollbild-Overlay
|
- Live-Bezugsschirm (abschaltbar): Beim Start eines echten Bezugs blendet sich ein Vollbild-Overlay
|
||||||
weich ein — große Bezugszeit, Live-Gewicht und Flow samt Flow-Verlaufskurve der letzten 30 Sekunden
|
weich ein — große Bezugszeit, Live-Gewicht und Flow samt Flow-Verlaufskurve der letzten 30 Sekunden
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
// -------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------
|
||||||
// Firmware
|
// Firmware
|
||||||
// -------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------
|
||||||
#define DISPLAY_FW_VERSION "1.0.10" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
|
#define DISPLAY_FW_VERSION "1.0.13" // Firmware-Stand der P4-Display-Steuerung (Info-Seite)
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------
|
||||||
// Protokoll
|
// Protokoll
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ struct MachineState {
|
|||||||
long heatUpRemainSec = -1; // verbleibende Sekunden bis "durchgewaermt" (-1 = aus)
|
long heatUpRemainSec = -1; // verbleibende Sekunden bis "durchgewaermt" (-1 = aus)
|
||||||
bool lightOn = false;
|
bool lightOn = false;
|
||||||
bool piezoEnabled = false;
|
bool piezoEnabled = false;
|
||||||
|
String activeProfile; // zuletzt geladenes/gespeichertes Profil (leer = keines; seit S3 5.1.1)
|
||||||
|
bool profileDirty = false; // Einstellungen weichen seit dem Laden vom Profil ab
|
||||||
|
|
||||||
// --- Waage ---
|
// --- Waage ---
|
||||||
float weight = 0, targetWeight = 0;
|
float weight = 0, targetWeight = 0;
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ void ProtocolClient::applyState(const String& json) {
|
|||||||
s.heatUpRemainSec = doc["heatUpRemainSec"] | (long)-1;
|
s.heatUpRemainSec = doc["heatUpRemainSec"] | (long)-1;
|
||||||
s.lightOn = doc["lightOn"] | false;
|
s.lightOn = doc["lightOn"] | false;
|
||||||
s.piezoEnabled = doc["piezoEnabled"] | false;
|
s.piezoEnabled = doc["piezoEnabled"] | false;
|
||||||
|
s.activeProfile = String((const char*)(doc["profile"] | "")); // fehlt bei aelterer S3-FW -> leer
|
||||||
|
s.profileDirty = doc["profDirty"] | false;
|
||||||
|
|
||||||
s.weight = doc["weight"] | s.weight;
|
s.weight = doc["weight"] | s.weight;
|
||||||
s.targetWeight = doc["targetWeight"] | s.targetWeight;
|
s.targetWeight = doc["targetWeight"] | s.targetWeight;
|
||||||
|
|||||||
+507
-52
@@ -17,6 +17,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <Preferences.h> // Dashboard-Design P4-lokal merken (NVS)
|
#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 "ui.h"
|
||||||
#include "theme.h"
|
#include "theme.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
@@ -103,6 +104,12 @@ static lv_obj_t* bulbTempW = nullptr; static lv_obj_t* bulbTempD = nullptr;
|
|||||||
static lv_obj_t* g_designBtn[TD_DESIGN_COUNT] = {}; // Umschalter auf der Info-Seite
|
static lv_obj_t* g_designBtn[TD_DESIGN_COUNT] = {}; // Umschalter auf der Info-Seite
|
||||||
static lv_obj_t* g_readyPill = nullptr; // "BEREIT"-Pille im Header (Kessel im Zielband)
|
static lv_obj_t* g_readyPill = nullptr; // "BEREIT"-Pille im Header (Kessel im Zielband)
|
||||||
static bool g_readyState = false; // mit Hysterese, gegen Flackern an der Bandgrenze
|
static bool g_readyState = false; // mit Hysterese, gegen Flackern an der Bandgrenze
|
||||||
|
// Profil-Chip im Header (zuletzt geladenes Profil; "*" = danach geaenderte Einstellungen).
|
||||||
|
// Antippen oeffnet die Profil-Schnellwahl (Overlay mit allen Profilen zum Direkt-Laden).
|
||||||
|
static lv_obj_t* g_profChip = nullptr;
|
||||||
|
static lv_obj_t* g_profPicker = nullptr; // Schnellwahl-Overlay (Vollbild-Abdunkelung)
|
||||||
|
static lv_obj_t* g_profPickerList = nullptr; // Button-Liste im Overlay
|
||||||
|
static lv_obj_t* g_profPickerInfo = nullptr; // "Lade Profile..."-Hinweis im Overlay
|
||||||
static lv_obj_t* cellShot; static lv_obj_t* lblShotTimer; static lv_obj_t* g_bbtBar = nullptr;
|
static lv_obj_t* cellShot; static lv_obj_t* lblShotTimer; static lv_obj_t* g_bbtBar = nullptr;
|
||||||
static lv_obj_t* cellSteam; static lv_obj_t* lblSteamTimer; static lv_obj_t* g_sbtBar = nullptr;
|
static lv_obj_t* cellSteam; static lv_obj_t* lblSteamTimer; static lv_obj_t* g_sbtBar = nullptr;
|
||||||
static lv_obj_t* cellBezug; static lv_obj_t* lblBezug; static lv_obj_t* g_bbwBar = nullptr;
|
static lv_obj_t* cellBezug; static lv_obj_t* lblBezug; static lv_obj_t* g_bbwBar = nullptr;
|
||||||
@@ -118,6 +125,46 @@ static lv_chart_series_t* g_serC = nullptr; // Zusatzsensor (Gehaeuse/Tassenab
|
|||||||
static lv_obj_t* g_legW = nullptr; static lv_obj_t* g_legD = nullptr; static lv_obj_t* g_legC = nullptr;
|
static lv_obj_t* g_legW = nullptr; static lv_obj_t* g_legD = nullptr; static lv_obj_t* g_legC = nullptr;
|
||||||
static lv_obj_t* g_axHi = nullptr; static lv_obj_t* g_axMid = nullptr; static lv_obj_t* g_axLo = nullptr; // Y-Skala
|
static lv_obj_t* g_axHi = nullptr; static lv_obj_t* g_axMid = nullptr; static lv_obj_t* g_axLo = nullptr; // Y-Skala
|
||||||
#define COL_CASE lv_color_hex(0xba68c8) // Farbe Zusatzsensor im Chart (Violett; klar distinkt von Wasser-Gold/Dampf-Teal)
|
#define COL_CASE lv_color_hex(0xba68c8) // Farbe Zusatzsensor im Chart (Violett; klar distinkt von Wasser-Gold/Dampf-Teal)
|
||||||
|
// Umschaltbares Zeitfenster: das Chart zeigt immer 120 Punkte; die Werte kommen aus einer
|
||||||
|
// 1-s-Historie (Ringpuffer ueber 60 min) und werden je Fenster dezimiert. So bleibt beim
|
||||||
|
// Fensterwechsel der komplette Verlauf sofort sichtbar (kein leeres Chart).
|
||||||
|
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"
|
||||||
|
// 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")
|
||||||
|
static lv_obj_t* g_chartTitle = nullptr;
|
||||||
|
static lv_obj_t* g_chartWinBtn[4] = {};
|
||||||
|
static lv_obj_t* g_chartDutyBtn = nullptr;
|
||||||
|
static lv_chart_series_t* g_serDutyW = nullptr; // Heizleistung auf Sekundaerachse 0..100 %
|
||||||
|
static lv_chart_series_t* g_serDutyD = nullptr;
|
||||||
|
static lv_obj_t* g_legDuty = nullptr;
|
||||||
|
static lv_obj_t* g_lineSetW = nullptr; // gestrichelte Soll-Linien im Chart
|
||||||
|
static lv_obj_t* g_lineSetD = nullptr;
|
||||||
|
static lv_point_precise_t g_lineSetPts[2][2];
|
||||||
|
#define COL_DUTY_W lv_color_hex(0x8a6206) // gedimmtes Gold (Heizleistung Wasser)
|
||||||
|
#define COL_DUTY_D lv_color_hex(0x4d7a76) // gedimmtes Teal (Heizleistung Dampf)
|
||||||
|
// PID-Tuning-Live-Karte (Verlauf-Seite): Laufzeit, Heiz-/Abkuehlphase und Schwingungshub
|
||||||
|
static lv_obj_t* g_tuneCard = nullptr;
|
||||||
|
static lv_obj_t* g_tuneCardTitle = nullptr;
|
||||||
|
static lv_obj_t* g_tuneCardBody = nullptr;
|
||||||
|
static lv_obj_t* g_tuneCardSpin = nullptr;
|
||||||
|
static bool g_tuneCardShown = false;
|
||||||
|
static uint32_t g_tuneStartW = 0, g_tuneStartD = 0; // millis() beim Tuning-Start
|
||||||
|
static float g_tuneMinW = 0, g_tuneMaxW = 0; // Schwingungs-Min/Max seit Start
|
||||||
|
static float g_tuneMinD = 0, g_tuneMaxD = 0;
|
||||||
|
|
||||||
// Shot-Zusammenfassung (Karte nach Bezug; blendet weich ein/aus)
|
// Shot-Zusammenfassung (Karte nach Bezug; blendet weich ein/aus)
|
||||||
static lv_obj_t* g_shotSum = nullptr;
|
static lv_obj_t* g_shotSum = nullptr;
|
||||||
@@ -901,10 +948,12 @@ static lv_obj_t* infoCell(lv_obj_t* parent, const char* title, lv_obj_t** val, l
|
|||||||
lv_obj_set_style_text_align(*val, LV_TEXT_ALIGN_CENTER, 0); // fuer mehrzeilige Werte
|
lv_obj_set_style_text_align(*val, LV_TEXT_ALIGN_CENTER, 0); // fuer mehrzeilige Werte
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
// Tipp auf die Statuszeile: Wartungsmeldung -> Serviceseite, Reinigungsmeldung -> Sub-Screen
|
// Tipp auf die Statuszeile: Wartung -> Serviceseite, Reinigung -> Sub-Screen,
|
||||||
|
// PID-Tuning -> Verlauf-Seite (dort liegen Live-Karte und Schwingungskurve)
|
||||||
static void status_line_cb(lv_event_t*) {
|
static void status_line_cb(lv_event_t*) {
|
||||||
if (g_statusShown == ST_MAINT) show_page(PG_SERVICE);
|
if (g_statusShown == ST_MAINT) show_page(PG_SERVICE);
|
||||||
else if (g_statusShown == ST_CLEAN) cln_open(nullptr);
|
else if (g_statusShown == ST_CLEAN) cln_open(nullptr);
|
||||||
|
else if (g_statusShown == ST_TUNE) show_page(PG_CHART);
|
||||||
}
|
}
|
||||||
static void wifi_icon_cb(lv_event_t*) { show_page(PG_WIFI); } // Tipp auf WLAN-Symbol -> WiFi-Seite
|
static void wifi_icon_cb(lv_event_t*) { show_page(PG_WIFI); } // Tipp auf WLAN-Symbol -> WiFi-Seite
|
||||||
static void build_dashboard(lv_obj_t* p) {
|
static void build_dashboard(lv_obj_t* p) {
|
||||||
@@ -1260,9 +1309,107 @@ static void build_light(lv_obj_t* p) {
|
|||||||
// Verlauf (Chart): Wasser/Dampf + Zusatzsensor (Gehaeuse/Tassenablage), ~2 min,
|
// Verlauf (Chart): Wasser/Dampf + Zusatzsensor (Gehaeuse/Tassenablage), ~2 min,
|
||||||
// mit automatisch skalierter Temperatur-Achse und Y-Skala-Beschriftung links
|
// mit automatisch skalierter Temperatur-Achse und Y-Skala-Beschriftung links
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
|
// Zeitfenster-/Leistungs-Buttons gemaess Auswahl einfaerben, Titel aktualisieren
|
||||||
|
static void chart_win_btns_refresh() {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (!g_chartWinBtn[i]) continue;
|
||||||
|
bool sel = (g_chartWin == i);
|
||||||
|
lv_obj_set_style_bg_color(g_chartWinBtn[i], sel ? COL_ACCENT : COL_CARD2, 0);
|
||||||
|
lv_obj_set_style_text_color(lv_obj_get_child(g_chartWinBtn[i], 0),
|
||||||
|
sel ? lv_color_hex(0x000000) : COL_TEXT, 0);
|
||||||
|
}
|
||||||
|
if (g_chartDutyBtn) {
|
||||||
|
lv_obj_set_style_bg_color(g_chartDutyBtn, g_chartDuty ? COL_ACCENT : COL_CARD2, 0);
|
||||||
|
lv_obj_set_style_text_color(lv_obj_get_child(g_chartDutyBtn, 0),
|
||||||
|
g_chartDuty ? lv_color_hex(0x000000) : COL_TEXT, 0);
|
||||||
|
}
|
||||||
|
if (g_chartTitle)
|
||||||
|
lv_label_set_text_fmt(g_chartTitle, "Temperatur-Verlauf (%u min)", (unsigned)CHART_WIN_MIN[g_chartWin]);
|
||||||
|
}
|
||||||
|
static void chart_redraw(); // Definition weiter unten
|
||||||
|
static void chart_win_btn_cb(lv_event_t* e) {
|
||||||
|
int idx = (int)(intptr_t)lv_event_get_user_data(e);
|
||||||
|
if (idx == (int)g_chartWin) return;
|
||||||
|
g_chartWin = (uint8_t)idx;
|
||||||
|
{ Preferences prefs; prefs.begin("ui", false); prefs.putUChar("chartwin", g_chartWin); prefs.end(); }
|
||||||
|
chart_win_btns_refresh();
|
||||||
|
chart_redraw(); // neues Fenster sofort aus der Historie zeichnen
|
||||||
|
}
|
||||||
|
static void chart_duty_btn_cb(lv_event_t*) {
|
||||||
|
g_chartDuty = !g_chartDuty;
|
||||||
|
{ Preferences prefs; prefs.begin("ui", false); prefs.putBool("chartduty", g_chartDuty); prefs.end(); }
|
||||||
|
chart_win_btns_refresh();
|
||||||
|
if (g_chart && g_serDutyW) {
|
||||||
|
lv_chart_hide_series(g_chart, g_serDutyW, !g_chartDuty);
|
||||||
|
lv_chart_hide_series(g_chart, g_serDutyD, !g_chartDuty);
|
||||||
|
}
|
||||||
|
if (g_legDuty) {
|
||||||
|
if (g_chartDuty) lv_obj_remove_flag(g_legDuty, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
else lv_obj_add_flag(g_legDuty, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
chart_redraw();
|
||||||
|
}
|
||||||
static void build_chart(lv_obj_t* p) {
|
static void build_chart(lv_obj_t* p) {
|
||||||
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
section_title(p, "Temperatur-Verlauf (2 min)");
|
|
||||||
|
// Kopfzeile: Titel links, Zeitfenster-Umschalter + Heizleistung-Toggle rechts
|
||||||
|
lv_obj_t* hdr = row(p, nullptr);
|
||||||
|
g_chartTitle = section_title(hdr, "Temperatur-Verlauf (2 min)");
|
||||||
|
lv_obj_t* btns = lv_obj_create(hdr);
|
||||||
|
lv_obj_set_size(btns, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_bg_opa(btns, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_border_width(btns, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(btns, 0, 0);
|
||||||
|
lv_obj_set_style_pad_column(btns, 6, 0);
|
||||||
|
lv_obj_set_flex_flow(btns, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_clear_flag(btns, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
static const char* winNames[4] = { "2 min", "10 min", "30 min", "60 min" };
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
g_chartWinBtn[i] = th_button(btns, winNames[i], COL_CARD2, COL_TEXT);
|
||||||
|
lv_obj_set_style_pad_hor(g_chartWinBtn[i], 10, 0);
|
||||||
|
lv_obj_set_style_pad_ver(g_chartWinBtn[i], 5, 0);
|
||||||
|
lv_obj_set_style_text_font(lv_obj_get_child(g_chartWinBtn[i], 0), &lv_font_maven_pro_14, 0);
|
||||||
|
lv_obj_add_event_cb(g_chartWinBtn[i], chart_win_btn_cb, LV_EVENT_CLICKED, (void*)(intptr_t)i);
|
||||||
|
}
|
||||||
|
g_chartDutyBtn = th_button(btns, "Leistung", COL_CARD2, COL_TEXT);
|
||||||
|
lv_obj_set_style_pad_hor(g_chartDutyBtn, 10, 0);
|
||||||
|
lv_obj_set_style_pad_ver(g_chartDutyBtn, 5, 0);
|
||||||
|
lv_obj_set_style_text_font(lv_obj_get_child(g_chartDutyBtn, 0), &lv_font_maven_pro_14, 0);
|
||||||
|
lv_obj_add_event_cb(g_chartDutyBtn, chart_duty_btn_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
|
chart_win_btns_refresh();
|
||||||
|
|
||||||
|
// PID-Tuning-Live-Karte: nur waehrend/kurz nach einem AutoTune-Lauf sichtbar
|
||||||
|
// (Inhalt kommt aus ui_update; die Statuszeilen-Meldung ist antippbar und fuehrt hierher)
|
||||||
|
g_tuneCard = th_card(p);
|
||||||
|
lv_obj_set_width(g_tuneCard, LV_PCT(100));
|
||||||
|
lv_obj_set_height(g_tuneCard, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_flex_flow(g_tuneCard, LV_FLEX_FLOW_ROW);
|
||||||
|
lv_obj_set_flex_align(g_tuneCard, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
|
||||||
|
lv_obj_set_style_pad_column(g_tuneCard, 12, 0);
|
||||||
|
lv_obj_clear_flag(g_tuneCard, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_add_flag(g_tuneCard, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
g_tuneCardSpin = lv_spinner_create(g_tuneCard);
|
||||||
|
lv_spinner_set_anim_params(g_tuneCardSpin, 1000, 60);
|
||||||
|
lv_obj_set_size(g_tuneCardSpin, 28, 28);
|
||||||
|
lv_obj_set_style_arc_width(g_tuneCardSpin, 4, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_arc_width(g_tuneCardSpin, 4, LV_PART_INDICATOR);
|
||||||
|
lv_obj_set_style_arc_color(g_tuneCardSpin, COL_CARD2, LV_PART_MAIN);
|
||||||
|
lv_obj_set_style_arc_color(g_tuneCardSpin, COL_WARN, LV_PART_INDICATOR);
|
||||||
|
lv_obj_t* tcol = lv_obj_create(g_tuneCard);
|
||||||
|
lv_obj_set_size(tcol, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
|
||||||
|
lv_obj_set_style_bg_opa(tcol, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_border_width(tcol, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(tcol, 0, 0);
|
||||||
|
lv_obj_set_style_pad_row(tcol, 2, 0);
|
||||||
|
lv_obj_set_flex_flow(tcol, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_clear_flag(tcol, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
g_tuneCardTitle = lv_label_create(tcol);
|
||||||
|
lv_label_set_text(g_tuneCardTitle, "PID-Tuning läuft...");
|
||||||
|
lv_obj_set_style_text_color(g_tuneCardTitle, COL_WARN, 0);
|
||||||
|
lv_obj_set_style_text_font(g_tuneCardTitle, &lv_font_maven_pro_18, 0);
|
||||||
|
g_tuneCardBody = lv_label_create(tcol);
|
||||||
|
lv_label_set_text(g_tuneCardBody, "--");
|
||||||
|
lv_obj_set_style_text_color(g_tuneCardBody, COL_TEXT, 0);
|
||||||
|
|
||||||
g_chart = lv_chart_create(p);
|
g_chart = lv_chart_create(p);
|
||||||
lv_obj_set_width(g_chart, LV_PCT(100));
|
lv_obj_set_width(g_chart, LV_PCT(100));
|
||||||
@@ -1276,17 +1423,38 @@ static void build_chart(lv_obj_t* p) {
|
|||||||
lv_obj_set_style_width(g_chart, 0, LV_PART_INDICATOR); // keine Punkt-Marker
|
lv_obj_set_style_width(g_chart, 0, LV_PART_INDICATOR); // keine Punkt-Marker
|
||||||
lv_obj_set_style_height(g_chart, 0, LV_PART_INDICATOR);
|
lv_obj_set_style_height(g_chart, 0, LV_PART_INDICATOR);
|
||||||
lv_chart_set_type(g_chart, LV_CHART_TYPE_LINE);
|
lv_chart_set_type(g_chart, LV_CHART_TYPE_LINE);
|
||||||
lv_chart_set_point_count(g_chart, 120); // 120 * 1 s = 2 min
|
lv_chart_set_point_count(g_chart, 120); // fest; Zeit je Punkt haengt am Fenster
|
||||||
lv_chart_set_update_mode(g_chart, LV_CHART_UPDATE_MODE_SHIFT);
|
|
||||||
lv_chart_set_div_line_count(g_chart, 5, 0);
|
lv_chart_set_div_line_count(g_chart, 5, 0);
|
||||||
lv_chart_set_range(g_chart, LV_CHART_AXIS_PRIMARY_Y, 80, 160); // Temperaturen (Grad C)
|
lv_chart_set_range(g_chart, LV_CHART_AXIS_PRIMARY_Y, 800, 1600); // Zehntelgrad (feinere Kurven)
|
||||||
|
lv_chart_set_range(g_chart, LV_CHART_AXIS_SECONDARY_Y, 0, 100); // Heizleistung (%)
|
||||||
|
|
||||||
g_serW = lv_chart_add_series(g_chart, COL_ACCENT, LV_CHART_AXIS_PRIMARY_Y);
|
g_serW = lv_chart_add_series(g_chart, COL_ACCENT, LV_CHART_AXIS_PRIMARY_Y);
|
||||||
g_serD = lv_chart_add_series(g_chart, lv_color_hex(0x80cbc4), LV_CHART_AXIS_PRIMARY_Y);
|
g_serD = lv_chart_add_series(g_chart, lv_color_hex(0x80cbc4), LV_CHART_AXIS_PRIMARY_Y);
|
||||||
g_serC = lv_chart_add_series(g_chart, COL_CASE, LV_CHART_AXIS_PRIMARY_Y);
|
g_serC = lv_chart_add_series(g_chart, COL_CASE, LV_CHART_AXIS_PRIMARY_Y);
|
||||||
|
g_serDutyW = lv_chart_add_series(g_chart, COL_DUTY_W, LV_CHART_AXIS_SECONDARY_Y);
|
||||||
|
g_serDutyD = lv_chart_add_series(g_chart, COL_DUTY_D, LV_CHART_AXIS_SECONDARY_Y);
|
||||||
lv_chart_set_all_value(g_chart, g_serW, LV_CHART_POINT_NONE);
|
lv_chart_set_all_value(g_chart, g_serW, LV_CHART_POINT_NONE);
|
||||||
lv_chart_set_all_value(g_chart, g_serD, LV_CHART_POINT_NONE);
|
lv_chart_set_all_value(g_chart, g_serD, LV_CHART_POINT_NONE);
|
||||||
lv_chart_set_all_value(g_chart, g_serC, LV_CHART_POINT_NONE);
|
lv_chart_set_all_value(g_chart, g_serC, LV_CHART_POINT_NONE);
|
||||||
|
lv_chart_set_all_value(g_chart, g_serDutyW, LV_CHART_POINT_NONE);
|
||||||
|
lv_chart_set_all_value(g_chart, g_serDutyD, LV_CHART_POINT_NONE);
|
||||||
|
lv_chart_hide_series(g_chart, g_serDutyW, !g_chartDuty);
|
||||||
|
lv_chart_hide_series(g_chart, g_serDutyD, !g_chartDuty);
|
||||||
|
|
||||||
|
// Gestrichelte Soll-Linien (Wasser/Dampf); Position rechnet chart_redraw aus der Skala
|
||||||
|
lv_color_t setCols[2] = { COL_ACCENT, lv_color_hex(0x80cbc4) };
|
||||||
|
lv_obj_t** setLines[2] = { &g_lineSetW, &g_lineSetD };
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
lv_obj_t* ln = lv_line_create(g_chart);
|
||||||
|
lv_obj_set_style_line_width(ln, 1, 0);
|
||||||
|
lv_obj_set_style_line_color(ln, setCols[i], 0);
|
||||||
|
lv_obj_set_style_line_opa(ln, LV_OPA_60, 0);
|
||||||
|
lv_obj_set_style_line_dash_width(ln, 6, 0);
|
||||||
|
lv_obj_set_style_line_dash_gap(ln, 6, 0);
|
||||||
|
lv_obj_clear_flag(ln, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_add_flag(ln, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
*setLines[i] = ln;
|
||||||
|
}
|
||||||
|
|
||||||
// Y-Skala-Beschriftung (Grad C) im linken Gutter: oben=hi, mitte, unten=lo
|
// Y-Skala-Beschriftung (Grad C) im linken Gutter: oben=hi, mitte, unten=lo
|
||||||
g_axHi = lv_label_create(g_chart); lv_obj_align(g_axHi, LV_ALIGN_TOP_LEFT, -38, -2);
|
g_axHi = lv_label_create(g_chart); lv_obj_align(g_axHi, LV_ALIGN_TOP_LEFT, -38, -2);
|
||||||
@@ -1308,39 +1476,119 @@ static void build_chart(lv_obj_t* p) {
|
|||||||
g_legC = lv_label_create(leg); lv_label_set_text(g_legC, "");
|
g_legC = lv_label_create(leg); lv_label_set_text(g_legC, "");
|
||||||
lv_obj_set_style_text_color(g_legC, COL_CASE, 0);
|
lv_obj_set_style_text_color(g_legC, COL_CASE, 0);
|
||||||
lv_obj_add_flag(g_legC, LV_OBJ_FLAG_HIDDEN); // nur wenn Zusatzsensor aktiv
|
lv_obj_add_flag(g_legC, LV_OBJ_FLAG_HIDDEN); // nur wenn Zusatzsensor aktiv
|
||||||
|
g_legDuty = lv_label_create(leg); lv_label_set_text(g_legDuty, "Leistung --");
|
||||||
|
lv_obj_set_style_text_color(g_legDuty, COL_TEXT_DIM, 0);
|
||||||
|
if (!g_chartDuty) lv_obj_add_flag(g_legDuty, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
// Zeichnet die 120 Chart-Punkte aus der 1-s-Historie (je Fenster dezimiert), skaliert die
|
||||||
|
// 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 || !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 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 (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);
|
||||||
|
lv_chart_set_value_by_id(g_chart, g_serC, i, vC);
|
||||||
|
lv_chart_set_value_by_id(g_chart, g_serDutyW, i, dW);
|
||||||
|
lv_chart_set_value_by_id(g_chart, g_serDutyD, i, dD);
|
||||||
|
}
|
||||||
|
// Soll-Werte in die Skala einbeziehen, damit die Soll-Linien im Bild liegen -
|
||||||
|
// aber nur fuer Kreise, deren Ist-Kurve im Fenster ueberhaupt sichtbar ist
|
||||||
|
int32_t setWt = (int32_t)(g_state.setW * 10.0f + 0.5f);
|
||||||
|
int32_t setDt = (int32_t)(g_state.setD * 10.0f + 0.5f);
|
||||||
|
bool showLineW = anyW && setWt > 300;
|
||||||
|
bool showLineD = anyD && setDt > 300;
|
||||||
|
if (showLineW) { if (setWt < lo) lo = setWt; if (setWt > hi) hi = setWt; }
|
||||||
|
if (showLineD) { if (setDt < lo) lo = setDt; if (setDt > hi) hi = setDt; }
|
||||||
|
if (hi >= lo) {
|
||||||
|
int loDeg = ((int)(lo / 10) - 3) / 5 * 5; // auf 5 Grad abrunden (mit Rand)
|
||||||
|
int hiDeg = ((int)(hi / 10) + 7) / 5 * 5; // auf 5 Grad aufrunden (mit Rand)
|
||||||
|
if (hiDeg - loDeg < 10) hiDeg = loDeg + 10; // Mindestspanne 10 Grad
|
||||||
|
lv_chart_set_range(g_chart, LV_CHART_AXIS_PRIMARY_Y, loDeg * 10, hiDeg * 10);
|
||||||
|
if (g_axHi) lv_label_set_text_fmt(g_axHi, "%d", hiDeg);
|
||||||
|
if (g_axMid) lv_label_set_text_fmt(g_axMid, "%d", (loDeg + hiDeg) / 2);
|
||||||
|
if (g_axLo) lv_label_set_text_fmt(g_axLo, "%d", loDeg);
|
||||||
|
|
||||||
|
// Soll-Linien positionieren (horizontale Linie ueber die volle Plotbreite)
|
||||||
|
int32_t w = lv_obj_get_content_width(g_chart);
|
||||||
|
int32_t h = lv_obj_get_content_height(g_chart);
|
||||||
|
struct { lv_obj_t* line; lv_point_precise_t* pts; int32_t val; bool show; } L[2] = {
|
||||||
|
{ g_lineSetW, g_lineSetPts[0], setWt, showLineW },
|
||||||
|
{ g_lineSetD, g_lineSetPts[1], setDt, showLineD },
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 2; i++) {
|
||||||
|
if (!L[i].line) continue;
|
||||||
|
int32_t span = (hiDeg - loDeg) * 10;
|
||||||
|
if (L[i].show && w > 0 && h > 0 && span > 0 &&
|
||||||
|
L[i].val >= loDeg * 10 && L[i].val <= hiDeg * 10) {
|
||||||
|
int32_t y = (int32_t)((int64_t)h * (hiDeg * 10 - L[i].val) / span);
|
||||||
|
L[i].pts[0].x = 0; L[i].pts[0].y = 0;
|
||||||
|
L[i].pts[1].x = w; L[i].pts[1].y = 0;
|
||||||
|
lv_line_set_points(L[i].line, L[i].pts, 2);
|
||||||
|
lv_obj_align(L[i].line, LV_ALIGN_TOP_LEFT, 0, y);
|
||||||
|
lv_obj_remove_flag(L[i].line, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
lv_obj_add_flag(L[i].line, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lv_chart_refresh(g_chart);
|
||||||
}
|
}
|
||||||
static void chart_sample_cb(lv_timer_t*) {
|
static void chart_sample_cb(lv_timer_t*) {
|
||||||
if (!g_chart) return;
|
if (!g_chart || !g_hist) return;
|
||||||
int wv = (int)(g_state.tempW + 0.5f);
|
// 1-s-Probe in die 60-min-Historie (Ringpuffer); Anzeige zeichnet chart_redraw
|
||||||
int dv = (int)(g_state.tempD + 0.5f);
|
|
||||||
bool caseOn = g_state.caseSensorEnabled && g_state.hasCaseTemp;
|
bool caseOn = g_state.caseSensorEnabled && g_state.hasCaseTemp;
|
||||||
int cv = caseOn ? (int)(g_state.caseTemp + 0.5f) : 0;
|
g_hist->w[g_histPos] = (int16_t)(g_state.tempW * 10.0f + 0.5f);
|
||||||
lv_chart_set_next_value(g_chart, g_serW, (int32_t)wv);
|
g_hist->d[g_histPos] = (int16_t)(g_state.tempD * 10.0f + 0.5f);
|
||||||
lv_chart_set_next_value(g_chart, g_serD, (int32_t)dv);
|
g_hist->c[g_histPos] = caseOn ? (int16_t)(g_state.caseTemp * 10.0f + 0.5f) : CHART_HIST_NONE;
|
||||||
lv_chart_set_next_value(g_chart, g_serC, caseOn ? (int32_t)cv : LV_CHART_POINT_NONE);
|
float dw = g_state.dutyW, dd = g_state.dutyD;
|
||||||
|
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++;
|
||||||
|
|
||||||
// Automatische Skalierung der Temperatur-Achse ueber ein gleitendes 120-Werte-Fenster.
|
chart_redraw();
|
||||||
// Werte < 30 (kalter/inaktiver Sensor) werden ignoriert, damit sie die Skala nicht
|
|
||||||
// an den unteren Rand ziehen (sonst sehen die Kurven flach aus).
|
if (g_legDuty && g_chartDuty)
|
||||||
static int16_t hW[120], hD[120], hC[120];
|
lv_label_set_text_fmt(g_legDuty, "Leistung W %d %% / D %d %%",
|
||||||
static int pos = 0, fill = 0;
|
(int)(dw + 0.5f), (int)(dd + 0.5f));
|
||||||
hW[pos] = (int16_t)wv; hD[pos] = (int16_t)dv; hC[pos] = caseOn ? (int16_t)cv : -1000;
|
|
||||||
pos = (pos + 1) % 120; if (fill < 120) fill++;
|
|
||||||
int lo = 1000, hi = -1000;
|
|
||||||
for (int i = 0; i < fill; i++) {
|
|
||||||
if (hW[i] > 30) { if (hW[i] < lo) lo = hW[i]; if (hW[i] > hi) hi = hW[i]; }
|
|
||||||
if (hD[i] > 30) { if (hD[i] < lo) lo = hD[i]; if (hD[i] > hi) hi = hD[i]; }
|
|
||||||
if (hC[i] > 30) { if (hC[i] < lo) lo = hC[i]; if (hC[i] > hi) hi = hC[i]; }
|
|
||||||
}
|
|
||||||
if (hi >= lo) {
|
|
||||||
lo = ((lo - 3) / 5) * 5; // auf 5 abrunden (mit Rand)
|
|
||||||
hi = ((hi + 7) / 5) * 5; // auf 5 aufrunden (mit Rand)
|
|
||||||
if (hi - lo < 10) hi = lo + 10; // Mindestspanne 10 Grad
|
|
||||||
lv_chart_set_range(g_chart, LV_CHART_AXIS_PRIMARY_Y, lo, hi);
|
|
||||||
if (g_axHi) lv_label_set_text_fmt(g_axHi, "%d", hi);
|
|
||||||
if (g_axMid) lv_label_set_text_fmt(g_axMid, "%d", (lo + hi) / 2);
|
|
||||||
if (g_axLo) lv_label_set_text_fmt(g_axLo, "%d", lo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (g_legW) lv_label_set_text(g_legW, ("Wasser " + tempStr(g_state.tempW, 1)).c_str());
|
if (g_legW) lv_label_set_text(g_legW, ("Wasser " + tempStr(g_state.tempW, 1)).c_str());
|
||||||
if (g_legD) lv_label_set_text(g_legD, ("Dampf " + tempStr(g_state.tempD, 0)).c_str());
|
if (g_legD) lv_label_set_text(g_legD, ("Dampf " + tempStr(g_state.tempD, 0)).c_str());
|
||||||
@@ -2133,6 +2381,112 @@ static void prof_save_cb(lv_event_t*) {
|
|||||||
}
|
}
|
||||||
static void prof_refresh_cb(lv_event_t*) { if (g_client) g_client->sendListProfiles(); }
|
static void prof_refresh_cb(lv_event_t*) { if (g_client) g_client->sendListProfiles(); }
|
||||||
|
|
||||||
|
// --- Profil-Schnellwahl: Overlay mit allen Profilen zum Direkt-Laden ------------------
|
||||||
|
// Geoeffnet ueber den Profil-Chip im Header; ein Tap laedt das Profil sofort (die
|
||||||
|
// Bestaetigung uebernimmt der Toast + der aktualisierte Chip aus dem naechsten State).
|
||||||
|
static void prof_picker_close() {
|
||||||
|
if (!g_profPicker) return;
|
||||||
|
lv_obj_delete(g_profPicker);
|
||||||
|
g_profPicker = nullptr;
|
||||||
|
g_profPickerList = nullptr;
|
||||||
|
g_profPickerInfo = nullptr;
|
||||||
|
}
|
||||||
|
static void prof_picker_bg_cb(lv_event_t* e) {
|
||||||
|
// nur ein Tap auf die Abdunkelung selbst schliesst (Karte bubbelt nicht)
|
||||||
|
if (lv_event_get_target(e) == lv_event_get_current_target(e)) prof_picker_close();
|
||||||
|
}
|
||||||
|
static void prof_picker_close_cb(lv_event_t*) { prof_picker_close(); }
|
||||||
|
static void prof_picker_load_cb(lv_event_t* e) {
|
||||||
|
lv_obj_t* btn = (lv_obj_t*)lv_event_get_current_target(e);
|
||||||
|
lv_obj_t* lbl = lv_obj_get_child(btn, 0); // Kind 0 = Name (pur), Kind 1 = optionales Haekchen
|
||||||
|
if (!lbl || !g_client) return;
|
||||||
|
String name = lv_label_get_text(lbl);
|
||||||
|
prof_picker_close();
|
||||||
|
if (name == g_state.activeProfile && !g_state.profileDirty) {
|
||||||
|
showToast(("Profil ist bereits aktiv: " + name).c_str(), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
g_client->sendLoadProfile(name);
|
||||||
|
showToast(("Lade Profil: " + name).c_str(), false);
|
||||||
|
}
|
||||||
|
static void prof_picker_fill(const String profiles[], int count) {
|
||||||
|
if (!g_profPickerList) return;
|
||||||
|
lv_obj_clean(g_profPickerList);
|
||||||
|
if (g_profPickerInfo) {
|
||||||
|
if (count == 0) {
|
||||||
|
lv_label_set_text(g_profPickerInfo, "Keine Profile gespeichert.");
|
||||||
|
lv_obj_remove_flag(g_profPickerInfo, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
lv_obj_add_flag(g_profPickerInfo, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
bool active = (profiles[i] == g_state.activeProfile);
|
||||||
|
lv_obj_t* b = lv_button_create(g_profPickerList);
|
||||||
|
lv_obj_set_width(b, LV_PCT(100));
|
||||||
|
lv_obj_set_style_bg_color(b, active ? COL_ACCENT : COL_CARD2, 0);
|
||||||
|
lv_obj_set_style_radius(b, 10, 0);
|
||||||
|
lv_obj_set_style_pad_ver(b, 12, 0);
|
||||||
|
lv_obj_set_style_pad_hor(b, 14, 0);
|
||||||
|
lv_obj_set_style_shadow_width(b, 0, 0);
|
||||||
|
lv_obj_t* l = lv_label_create(b); // Kind 0: purer Profilname (prof_picker_load_cb liest ihn)
|
||||||
|
lv_label_set_text(l, profiles[i].c_str());
|
||||||
|
lv_obj_set_style_text_color(l, active ? lv_color_hex(0x000000) : COL_TEXT, 0);
|
||||||
|
lv_obj_align(l, LV_ALIGN_LEFT_MID, 0, 0);
|
||||||
|
if (active) {
|
||||||
|
lv_obj_t* chk = lv_label_create(b);
|
||||||
|
lv_label_set_text(chk, LV_SYMBOL_OK);
|
||||||
|
lv_obj_set_style_text_color(chk, lv_color_hex(0x000000), 0);
|
||||||
|
lv_obj_align(chk, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||||
|
}
|
||||||
|
lv_obj_add_event_cb(b, prof_picker_load_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void prof_picker_open() {
|
||||||
|
if (g_profPicker) return;
|
||||||
|
g_profPicker = lv_obj_create(lv_screen_active());
|
||||||
|
lv_obj_set_size(g_profPicker, LV_PCT(100), LV_PCT(100));
|
||||||
|
lv_obj_set_pos(g_profPicker, 0, 0);
|
||||||
|
lv_obj_set_style_bg_color(g_profPicker, lv_color_hex(0x000000), 0);
|
||||||
|
lv_obj_set_style_bg_opa(g_profPicker, LV_OPA_60, 0);
|
||||||
|
lv_obj_set_style_border_width(g_profPicker, 0, 0);
|
||||||
|
lv_obj_set_style_radius(g_profPicker, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(g_profPicker, 0, 0);
|
||||||
|
lv_obj_clear_flag(g_profPicker, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
lv_obj_add_event_cb(g_profPicker, prof_picker_bg_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
|
|
||||||
|
lv_obj_t* card = th_card(g_profPicker);
|
||||||
|
lv_obj_set_size(card, 460, 400);
|
||||||
|
lv_obj_center(card);
|
||||||
|
lv_obj_set_flex_flow(card, LV_FLEX_FLOW_COLUMN);
|
||||||
|
lv_obj_set_style_pad_row(card, 8, 0);
|
||||||
|
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
|
lv_obj_t* hdr = row(card, nullptr);
|
||||||
|
lv_obj_t* t = lv_label_create(hdr);
|
||||||
|
lv_label_set_text(t, "Profil laden");
|
||||||
|
lv_obj_set_style_text_color(t, COL_ACCENT, 0);
|
||||||
|
lv_obj_set_style_text_font(t, &lv_font_maven_pro_18, 0);
|
||||||
|
lv_obj_t* x = th_button(hdr, LV_SYMBOL_CLOSE, COL_CARD2, COL_TEXT);
|
||||||
|
lv_obj_add_event_cb(x, prof_picker_close_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
|
|
||||||
|
g_profPickerInfo = lv_label_create(card);
|
||||||
|
lv_label_set_text(g_profPickerInfo, "Lade Profile...");
|
||||||
|
lv_obj_set_style_text_color(g_profPickerInfo, COL_TEXT_DIM, 0);
|
||||||
|
|
||||||
|
g_profPickerList = lv_obj_create(card);
|
||||||
|
lv_obj_set_width(g_profPickerList, LV_PCT(100));
|
||||||
|
lv_obj_set_flex_grow(g_profPickerList, 1);
|
||||||
|
lv_obj_set_style_bg_opa(g_profPickerList, LV_OPA_TRANSP, 0);
|
||||||
|
lv_obj_set_style_border_width(g_profPickerList, 0, 0);
|
||||||
|
lv_obj_set_style_pad_all(g_profPickerList, 0, 0);
|
||||||
|
lv_obj_set_style_pad_row(g_profPickerList, 6, 0);
|
||||||
|
lv_obj_set_flex_flow(g_profPickerList, LV_FLEX_FLOW_COLUMN);
|
||||||
|
|
||||||
|
if (g_client) g_client->sendListProfiles(); // Antwort fuellt via ui_set_profiles/prof_picker_fill
|
||||||
|
}
|
||||||
|
static void prof_chip_cb(lv_event_t*) { prof_picker_open(); }
|
||||||
|
|
||||||
static void build_profiles(lv_obj_t* p) {
|
static void build_profiles(lv_obj_t* p) {
|
||||||
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_clear_flag(p, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
|
|
||||||
@@ -2573,9 +2927,19 @@ void ui_init(ProtocolClient* client) {
|
|||||||
g_client = client;
|
g_client = client;
|
||||||
|
|
||||||
// Gespeicherte Darstellungs-Einstellungen laden (P4-lokal; Design-Standard: Rundinstrumente)
|
// 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);
|
{ Preferences prefs; prefs.begin("ui", false);
|
||||||
g_tempDesign = prefs.getUChar("tempdsgn", TD_GAUGE);
|
g_tempDesign = prefs.getUChar("tempdsgn", TD_GAUGE);
|
||||||
g_brewLiveEnabled = prefs.getBool("brewlive", true);
|
g_brewLiveEnabled = prefs.getBool("brewlive", true);
|
||||||
|
g_chartWin = prefs.getUChar("chartwin", 0);
|
||||||
|
if (g_chartWin > 3) g_chartWin = 0;
|
||||||
|
g_chartDuty = prefs.getBool("chartduty", false);
|
||||||
prefs.end();
|
prefs.end();
|
||||||
if (g_tempDesign >= TD_DESIGN_COUNT) g_tempDesign = TD_GAUGE; }
|
if (g_tempDesign >= TD_DESIGN_COUNT) g_tempDesign = TD_GAUGE; }
|
||||||
|
|
||||||
@@ -2607,6 +2971,22 @@ void ui_init(ProtocolClient* client) {
|
|||||||
lv_obj_set_flex_grow(g_hdrTitle, 1);
|
lv_obj_set_flex_grow(g_hdrTitle, 1);
|
||||||
lv_obj_set_style_text_font(g_hdrTitle, &lv_font_maven_pro_18, 0);
|
lv_obj_set_style_text_font(g_hdrTitle, &lv_font_maven_pro_18, 0);
|
||||||
|
|
||||||
|
// Profil-Chip: zuletzt geladenes Profil ("*" = seitdem geaenderte Einstellungen).
|
||||||
|
// Antippen oeffnet die Profil-Schnellwahl. Unsichtbar, solange die S3 kein Profil meldet.
|
||||||
|
g_profChip = lv_label_create(header);
|
||||||
|
lv_label_set_text(g_profChip, "");
|
||||||
|
lv_obj_set_style_bg_color(g_profChip, COL_CARD2, 0);
|
||||||
|
lv_obj_set_style_bg_opa(g_profChip, LV_OPA_COVER, 0);
|
||||||
|
lv_obj_set_style_text_color(g_profChip, COL_TEXT, 0);
|
||||||
|
lv_obj_set_style_radius(g_profChip, 12, 0);
|
||||||
|
lv_obj_set_style_pad_hor(g_profChip, 10, 0);
|
||||||
|
lv_obj_set_style_pad_ver(g_profChip, 3, 0);
|
||||||
|
lv_obj_set_style_margin_right(g_profChip, 10, 0);
|
||||||
|
lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_CLICKABLE);
|
||||||
|
lv_obj_set_ext_click_area(g_profChip, 8);
|
||||||
|
lv_obj_add_event_cb(g_profChip, prof_chip_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
|
lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
|
||||||
g_readyPill = lv_label_create(header); // "BEREIT": Kessel im Zielband (auf allen Seiten sichtbar)
|
g_readyPill = lv_label_create(header); // "BEREIT": Kessel im Zielband (auf allen Seiten sichtbar)
|
||||||
lv_label_set_text(g_readyPill, "BEREIT");
|
lv_label_set_text(g_readyPill, "BEREIT");
|
||||||
lv_obj_set_style_bg_color(g_readyPill, COL_OK, 0);
|
lv_obj_set_style_bg_color(g_readyPill, COL_OK, 0);
|
||||||
@@ -2815,7 +3195,7 @@ void ui_init(ProtocolClient* client) {
|
|||||||
|
|
||||||
// Shot-Zusammenfassung (zentrierte Karte mit grossen Werten, antippbar zum Schliessen)
|
// Shot-Zusammenfassung (zentrierte Karte mit grossen Werten, antippbar zum Schliessen)
|
||||||
g_shotSum = lv_obj_create(lv_layer_top());
|
g_shotSum = lv_obj_create(lv_layer_top());
|
||||||
lv_obj_set_size(g_shotSum, 460, LV_SIZE_CONTENT);
|
lv_obj_set_size(g_shotSum, 560, LV_SIZE_CONTENT); // breit genug, damit die drei Werte nicht zusammenruecken
|
||||||
lv_obj_set_style_bg_color(g_shotSum, COL_CARD, 0);
|
lv_obj_set_style_bg_color(g_shotSum, COL_CARD, 0);
|
||||||
lv_obj_set_style_bg_opa(g_shotSum, LV_OPA_COVER, 0);
|
lv_obj_set_style_bg_opa(g_shotSum, LV_OPA_COVER, 0);
|
||||||
lv_obj_set_style_border_color(g_shotSum, COL_ACCENT, 0);
|
lv_obj_set_style_border_color(g_shotSum, COL_ACCENT, 0);
|
||||||
@@ -2841,7 +3221,7 @@ void ui_init(ProtocolClient* client) {
|
|||||||
lv_obj_set_style_bg_opa(sumRow, LV_OPA_TRANSP, 0);
|
lv_obj_set_style_bg_opa(sumRow, LV_OPA_TRANSP, 0);
|
||||||
lv_obj_set_style_border_width(sumRow, 0, 0);
|
lv_obj_set_style_border_width(sumRow, 0, 0);
|
||||||
lv_obj_set_style_pad_all(sumRow, 0, 0);
|
lv_obj_set_style_pad_all(sumRow, 0, 0);
|
||||||
lv_obj_set_style_pad_column(sumRow, 10, 0);
|
lv_obj_set_style_pad_column(sumRow, 28, 0); // deutlicher Abstand zwischen Dauer/Gewicht/Flow
|
||||||
lv_obj_set_flex_flow(sumRow, LV_FLEX_FLOW_ROW);
|
lv_obj_set_flex_flow(sumRow, LV_FLEX_FLOW_ROW);
|
||||||
lv_obj_clear_flag(sumRow, LV_OBJ_FLAG_SCROLLABLE);
|
lv_obj_clear_flag(sumRow, LV_OBJ_FLAG_SCROLLABLE);
|
||||||
lv_obj_clear_flag(sumRow, LV_OBJ_FLAG_CLICKABLE); // Tipp faellt auf die Karte durch
|
lv_obj_clear_flag(sumRow, LV_OBJ_FLAG_CLICKABLE); // Tipp faellt auf die Karte durch
|
||||||
@@ -2914,6 +3294,25 @@ static void appendTuneResult(String& out, const char* kreis, uint8_t status, int
|
|||||||
if (s > sev) sev = s;
|
if (s > sev) sev = s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Eine Live-Zeile der Tuning-Karte: Kreis, Laufzeit, Heiz-/Abkuehlphase und Schwingungshub
|
||||||
|
// (Min/Max der Ist-Temperatur seit Tuning-Start; zeigt, wie stark der Kessel pendelt).
|
||||||
|
static void appendTuneLive(String& out, const char* kreis, uint32_t elapsedMs,
|
||||||
|
float duty, float tMin, float tMax) {
|
||||||
|
if (out.length()) out += "\n";
|
||||||
|
uint32_t s = elapsedMs / 1000;
|
||||||
|
char b[16];
|
||||||
|
snprintf(b, sizeof(b), "%lu:%02lu", (unsigned long)(s / 60), (unsigned long)(s % 60));
|
||||||
|
out += kreis;
|
||||||
|
out += " · ";
|
||||||
|
out += b;
|
||||||
|
out += " min · ";
|
||||||
|
out += (duty >= 50.0f) ? "heizt" : "kühlt ab";
|
||||||
|
float hub = tMax - tMin;
|
||||||
|
if (hub >= 0.05f) {
|
||||||
|
out += " · Hub " + tempStr(hub, 1) + " um " + tempStr((tMin + tMax) * 0.5f, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// Live-Aktualisierung
|
// Live-Aktualisierung
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
@@ -3041,10 +3440,15 @@ void ui_update(const MachineState& st) {
|
|||||||
status_set(ST_CLEAN, false);
|
status_set(ST_CLEAN, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// PID-Tuning-Hinweis (AutoTune Wasser/Dampf laeuft)
|
// PID-Tuning: Statuszeile (antippbar -> Verlauf-Seite) + Live-Karte auf der Verlauf-Seite
|
||||||
{
|
{
|
||||||
// Fallende Flanke je Kreis erkennen -> Ergebnis ~30 s einblenden
|
|
||||||
uint32_t nowMs = millis();
|
uint32_t nowMs = millis();
|
||||||
|
// Steigende Flanke je Kreis: Startzeit latchen, Schwingungs-Min/Max zuruecksetzen
|
||||||
|
if (!g_tunePrevW && st.autoTuneW) { g_tuneStartW = nowMs; g_tuneMinW = g_tuneMaxW = st.tempW; }
|
||||||
|
if (!g_tunePrevD && st.autoTuneD) { g_tuneStartD = nowMs; g_tuneMinD = g_tuneMaxD = st.tempD; }
|
||||||
|
if (st.autoTuneW) { if (st.tempW < g_tuneMinW) g_tuneMinW = st.tempW; if (st.tempW > g_tuneMaxW) g_tuneMaxW = st.tempW; }
|
||||||
|
if (st.autoTuneD) { if (st.tempD < g_tuneMinD) g_tuneMinD = st.tempD; if (st.tempD > g_tuneMaxD) g_tuneMaxD = st.tempD; }
|
||||||
|
// Fallende Flanke je Kreis erkennen -> Ergebnis ~30 s einblenden
|
||||||
if (g_tunePrevW && !st.autoTuneW) { g_tuneResW = st.autoTuneWStatus; g_tuneResUntilW = nowMs + 30000; }
|
if (g_tunePrevW && !st.autoTuneW) { g_tuneResW = st.autoTuneWStatus; g_tuneResUntilW = nowMs + 30000; }
|
||||||
if (g_tunePrevD && !st.autoTuneD) { g_tuneResD = st.autoTuneDStatus; g_tuneResUntilD = nowMs + 30000; }
|
if (g_tunePrevD && !st.autoTuneD) { g_tuneResD = st.autoTuneDStatus; g_tuneResUntilD = nowMs + 30000; }
|
||||||
g_tunePrevW = st.autoTuneW;
|
g_tunePrevW = st.autoTuneW;
|
||||||
@@ -3055,26 +3459,55 @@ void ui_update(const MachineState& st) {
|
|||||||
if (g_tuneResUntilW != 0 && !showResW) g_tuneResUntilW = 0; // abgelaufen -> aus
|
if (g_tuneResUntilW != 0 && !showResW) g_tuneResUntilW = 0; // abgelaufen -> aus
|
||||||
if (g_tuneResUntilD != 0 && !showResD) g_tuneResUntilD = 0;
|
if (g_tuneResUntilD != 0 && !showResD) g_tuneResUntilD = 0;
|
||||||
|
|
||||||
|
// Ergebnistext des/der zuletzt beendeten Laufs/Laeufe (Statuszeile und Karte teilen ihn)
|
||||||
|
String resTxt; int resSev = 0;
|
||||||
|
if (showResW) appendTuneResult(resTxt, "Wasser", g_tuneResW, resSev);
|
||||||
|
if (showResD) appendTuneResult(resTxt, "Dampf", g_tuneResD, resSev);
|
||||||
|
|
||||||
if (st.autoTuneW || st.autoTuneD) {
|
if (st.autoTuneW || st.autoTuneD) {
|
||||||
const char* txt = (st.autoTuneW && st.autoTuneD) ? LV_SYMBOL_SETTINGS " PID-Tuning läuft (Wasser + Dampf)..."
|
const char* txt = (st.autoTuneW && st.autoTuneD) ? LV_SYMBOL_SETTINGS " PID-Tuning läuft (Wasser + Dampf)..."
|
||||||
: st.autoTuneW ? LV_SYMBOL_SETTINGS " PID-Tuning Wasser läuft..."
|
: st.autoTuneW ? LV_SYMBOL_SETTINGS " PID-Tuning Wasser läuft..."
|
||||||
: LV_SYMBOL_SETTINGS " PID-Tuning Dampf läuft...";
|
: LV_SYMBOL_SETTINGS " PID-Tuning Dampf läuft...";
|
||||||
status_set(ST_TUNE, true, txt, COL_WARN, lv_color_hex(0x000000));
|
status_set(ST_TUNE, true, txt, COL_WARN, lv_color_hex(0x000000));
|
||||||
} else if (showResW || showResD) {
|
} else if (resTxt.length()) {
|
||||||
// Ergebnis des/der zuletzt beendeten Laufs/Laeufe anzeigen
|
lv_color_t bg = (resSev >= 2) ? COL_DANGER : (resSev == 1 ? COL_WARN : COL_OK);
|
||||||
String txt; int sev = 0;
|
lv_color_t fg = (resSev == 1) ? lv_color_hex(0x000000) : COL_TEXT;
|
||||||
if (showResW) appendTuneResult(txt, "Wasser", g_tuneResW, sev);
|
status_set(ST_TUNE, true, resTxt.c_str(), bg, fg);
|
||||||
if (showResD) appendTuneResult(txt, "Dampf", g_tuneResD, sev);
|
|
||||||
if (txt.length()) {
|
|
||||||
lv_color_t bg = (sev >= 2) ? COL_DANGER : (sev == 1 ? COL_WARN : COL_OK);
|
|
||||||
lv_color_t fg = (sev == 1) ? lv_color_hex(0x000000) : COL_TEXT;
|
|
||||||
status_set(ST_TUNE, true, txt.c_str(), bg, fg);
|
|
||||||
} else {
|
|
||||||
status_set(ST_TUNE, false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
status_set(ST_TUNE, false);
|
status_set(ST_TUNE, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Live-Karte auf der Verlauf-Seite: waehrend des Laufs Laufzeit/Phase/Schwingungshub,
|
||||||
|
// nach dem Ende ~30 s das Ergebnis; erscheint und verschwindet weich (Fade)
|
||||||
|
bool wantCard = st.autoTuneW || st.autoTuneD || resTxt.length() > 0;
|
||||||
|
if (g_tuneCard) {
|
||||||
|
if (st.autoTuneW || st.autoTuneD) {
|
||||||
|
if (g_tuneCardSpin) lv_obj_remove_flag(g_tuneCardSpin, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
if (g_tuneCardTitle) {
|
||||||
|
lv_label_set_text(g_tuneCardTitle, "PID-Tuning läuft...");
|
||||||
|
lv_obj_set_style_text_color(g_tuneCardTitle, COL_WARN, 0);
|
||||||
|
}
|
||||||
|
String b;
|
||||||
|
if (st.autoTuneW) appendTuneLive(b, "Wasser", nowMs - g_tuneStartW, st.dutyW, g_tuneMinW, g_tuneMaxW);
|
||||||
|
if (st.autoTuneD) appendTuneLive(b, "Dampf", nowMs - g_tuneStartD, st.dutyD, g_tuneMinD, g_tuneMaxD);
|
||||||
|
if (g_tuneCardBody) label_set_if_changed(g_tuneCardBody, b.c_str());
|
||||||
|
} else if (wantCard) {
|
||||||
|
if (g_tuneCardSpin) lv_obj_add_flag(g_tuneCardSpin, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
lv_color_t c = (resSev >= 2) ? COL_DANGER : (resSev == 1 ? COL_WARN : COL_OK);
|
||||||
|
if (g_tuneCardTitle) {
|
||||||
|
lv_label_set_text(g_tuneCardTitle, "PID-Tuning beendet");
|
||||||
|
lv_obj_set_style_text_color(g_tuneCardTitle, c, 0);
|
||||||
|
}
|
||||||
|
String b = resTxt;
|
||||||
|
b.replace(" | ", "\n"); // Karte darf mehrzeilig sein
|
||||||
|
if (g_tuneCardBody) label_set_if_changed(g_tuneCardBody, b.c_str());
|
||||||
|
}
|
||||||
|
if (wantCard != g_tuneCardShown) {
|
||||||
|
g_tuneCardShown = wantCard;
|
||||||
|
if (wantCard) ui_fade_in(g_tuneCard, UI_FADE_MS);
|
||||||
|
else ui_fade_out_hide(g_tuneCard, UI_FADE_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reinigungsassistent-Sub-Screen: Fortschritt/Sichtbarkeit aktualisieren (nur wenn offen)
|
// Reinigungsassistent-Sub-Screen: Fortschritt/Sichtbarkeit aktualisieren (nur wenn offen)
|
||||||
@@ -3132,6 +3565,24 @@ void ui_update(const MachineState& st) {
|
|||||||
else ui_fade_out_hide(g_readyPill, UI_FADE_MS);
|
else ui_fade_out_hide(g_readyPill, UI_FADE_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Profil-Chip im Header: Name des zuletzt geladenen Profils, "*" wenn seitdem
|
||||||
|
// profil-relevante Einstellungen geaendert wurden. Lange Namen werden gekuerzt,
|
||||||
|
// damit der Header nicht ueberlaeuft. Leer (alte S3-FW / kein Profil) -> versteckt.
|
||||||
|
if (g_profChip) {
|
||||||
|
if (st.activeProfile.length() > 0) {
|
||||||
|
String t = st.activeProfile;
|
||||||
|
if (t.length() > 18) {
|
||||||
|
int cut = 17; // nicht mitten in einem UTF-8-Mehrbyte-Zeichen (Umlaut) schneiden
|
||||||
|
while (cut > 0 && ((uint8_t)t[cut] & 0xC0) == 0x80) cut--;
|
||||||
|
t = t.substring(0, cut) + "...";
|
||||||
|
}
|
||||||
|
if (st.profileDirty) t += " *";
|
||||||
|
label_set_if_changed(g_profChip, t.c_str());
|
||||||
|
lv_obj_remove_flag(g_profChip, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
} else {
|
||||||
|
lv_obj_add_flag(g_profChip, LV_OBJ_FLAG_HIDDEN);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Timer-Synchronisation (die laufende Anzeige macht der 100-ms-Timer dash_timers_cb)
|
// Timer-Synchronisation (die laufende Anzeige macht der 100-ms-Timer dash_timers_cb)
|
||||||
// Tarier-Phase vor Brew-by-Weight-Bezug -> dash_timers_cb zeigt "Tariere..." im Shot-Timer
|
// Tarier-Phase vor Brew-by-Weight-Bezug -> dash_timers_cb zeigt "Tariere..." im Shot-Timer
|
||||||
g_scaleTaring = st.scaleTaring;
|
g_scaleTaring = st.scaleTaring;
|
||||||
@@ -3324,12 +3775,16 @@ void ui_update(const MachineState& st) {
|
|||||||
void ui_toast(const char* txt, bool error) { showToast(txt, error); }
|
void ui_toast(const char* txt, bool error) { showToast(txt, error); }
|
||||||
|
|
||||||
void ui_set_profiles(const String profiles[], int count) {
|
void ui_set_profiles(const String profiles[], int count) {
|
||||||
|
// Schnellwahl-Overlay (falls offen) mit derselben Antwort befuellen
|
||||||
|
prof_picker_fill(profiles, count);
|
||||||
if (!profList) return;
|
if (!profList) return;
|
||||||
lv_obj_clean(profList);
|
lv_obj_clean(profList);
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
lv_obj_t* b = lv_list_add_button(profList, LV_SYMBOL_FILE, profiles[i].c_str());
|
// aktives Profil markieren (Haekchen + Akzentfarbe statt Datei-Symbol)
|
||||||
|
bool active = (profiles[i] == g_state.activeProfile);
|
||||||
|
lv_obj_t* b = lv_list_add_button(profList, active ? LV_SYMBOL_OK : LV_SYMBOL_FILE, profiles[i].c_str());
|
||||||
lv_obj_set_style_bg_color(b, COL_CARD2, 0);
|
lv_obj_set_style_bg_color(b, COL_CARD2, 0);
|
||||||
lv_obj_set_style_text_color(b, COL_TEXT, 0);
|
lv_obj_set_style_text_color(b, active ? COL_ACCENT : COL_TEXT, 0);
|
||||||
lv_obj_add_event_cb(b, prof_select_cb, LV_EVENT_CLICKED, nullptr);
|
lv_obj_add_event_cb(b, prof_select_cb, LV_EVENT_CLICKED, nullptr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user