Compare commits

...
Author SHA1 Message Date
raw-designsandClaude Opus 5 c697e65a01 feat(S3): Cold Extraction überlebt Standby und Neustart (5.3.1)
Der Modus wird im EEPROM (Adresse 977) gespeichert und beim Start wieder
übernommen, sofern die Funktion auf /Brew-Control freigeschaltet ist. Standby
beendet ihn nicht mehr, sondern verwirft nur die Laufzeitdaten.

Grund: Wer nur kalte Bezüge machen will, musste den Modus nach jedem Standby
neu einschalten. Bis dahin heizte die Maschine bereits auf und war für die
nächste kalte Extraktion zu warm — man musste auf das Abkühlen warten, also
genau die Situation, die der Modus vermeiden soll.

Der Modus endet nur noch durch explizites Ausschalten, durch Entzug der
Freischaltung oder durch einen Werksreset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:31:59 +02:00
thomas d7a1a8dee2 Merge pull request 'feat(S3): Cold Extraction (kalte Extraktion) mit Temperatur-Freigabe (5.3.0)' (#11) from feat/cold-extraction into main 2026-08-10 21:04:17 +02:00
raw-designsandClaude Opus 5 db1b080dc4 feat(S3): Cold Extraction (kalte Extraktion) mit Temperatur-Freigabe (5.3.0)
Kalte Extraktion ohne Kessel-Bypass: Da kaltes Wasser nur bei kaltem Wasserkessel
am Puck ankommt, wird die Funktion über eine Temperaturschwelle freigegeben
(Standard 30 °C, 10–40 °C einstellbar). Solange der Modus aktiv ist, heizt der
Wasserkreis nicht (SSR gesperrt, Stellgröße und dutyW-Telemetrie auf 0, I-Anteil
eingefroren, Feed-Forward-Boost aus); der Dampfkreis bleibt unberührt.

Die Freigabe wird vor jedem Bezug neu geprüft, weil der weiter heizende
Dampfkessel den Wasserkessel über die Zeit über die Schwelle bringen kann. Ein
abgewiesener Start meldet den Grund auf OLED, Touch-Display und Web-Dashboard;
ein laufender Bezug wird nie abgebrochen. 5 °C Hysterese, Sensorfehler sperrt.

Fluss über Pumpen-Pulsung nach einer Vorbenetzung mit voller Leistung; Ende über
Zielgewicht oder Timeout. Brew-by-Time/Weight und FlowGuard sind während einer
kalten Extraktion außer Kraft, da FlowGuard sonst um denselben Pumpen-Ausgang
konkurrieren würde. Pumpenschutz über kumulierte Einschaltzeit mit Zwangspause
plus Stillstands-Erkennung bei aktiver Waage.

Kalte Bezüge werden im CSV-Log mit einem vierten Feld "C" gekennzeichnet, zählen
in allen Stückzahlen mit, bleiben aber aus der durchschnittlichen Bezugsdauer
heraus. Neue additive State-Felder cx* im UART-JSON (Protokoll bleibt v2) und
neuer statusKey "coldextraction".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:02:11 +02:00
thomas 5a1e8d4b50 Merge pull request 'feat(S3): Feed-Forward-Boost bei Bezug für den Wasserkessel (5.2.0)' (#10) from feat/feed-forward-bei-bezug into main 2026-07-18 04:11:40 +02:00
raw-designsandClaude Fable 5 4f840173a9 feat(S3): Feed-Forward-Boost bei Bezug für den Wasserkessel (5.2.0)
Während eines Bezugs strömt kaltes Wasser in den Kessel; die PID reagiert
erst auf den verzögert gemessenen Temperaturabfall. Der neue Feed-Forward
schaltet der Heizung sofort einen einstellbaren Leistungs-Boost auf
(0-100 % der Fenstergröße, 0 % = aus), die PID regelt nur den Restfehler.

- Zustandslose Aufschaltung in der SSR-Fensterlogik (effectiveOutputWasserMs),
  gedeckelt auf die Fenstergröße; Duty-Telemetrie (dutyW) zeigt den Boost mit
- Optional: I-Anteil der Wasser-PID während des Bezugs einfrieren
  (zentral in der PID-Schleife verwaltet, applyPidWasserTunings-Helper)
- Nur bei echten Bezügen (Hardware/Software-Shot), nicht bei Spülen,
  Wartungsmodus oder AutoTune; Sicherheitslogik unverändert
- Web-UI: neue Sektion „Feed-Forward bei Bezug (Wasser)“ auf /PID;
  EEPROM 944/945, bewusst kein Profilfeld (maschinenabhängig)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 04:09:13 +02:00
thomas 6371b22683 Merge pull request 'fix(S3): „Aktives Profil"-Anzeige auf der Profil-Seite ins Karten-Layout eingefügt' (#9) from fix/profilseite-aktiv-anzeige-layout into main 2026-07-14 00:15:30 +02:00
raw-designsandClaude Fable 5 de2d0635b2 fix(S3): „Aktives Profil"-Anzeige auf der Profil-Seite ins Karten-Layout eingefügt
Die Zeile stand linksbündig ohne max-width außerhalb der zentrierten
Seitenstruktur. Jetzt im status-message-Stil der Seite (zentriert,
max. 600 px, dezenter neutraler Rahmen), Profilname fett. Version 5.1.2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 00:14:53 +02:00
thomas 801a659b0c Merge pull request 'feat(P4): Profil-Schnellwahl im Header + Anzeige des aktiven Profils' (#8) from feat/profil-schnellwahl-p4 into main 2026-07-13 20:55:47 +02:00
raw-designsandClaude Fable 5 d97e4c34d5 feat(P4): Profil-Schnellwahl im Header + Anzeige des aktiven Profils
Neuer Profil-Chip im Header (alle Seiten): zeigt das zuletzt geladene
bzw. gespeicherte Profil aus den neuen S3-State-Feldern profile/profDirty
(S3 ab 5.1.1), „*" = seitdem geänderte Einstellungen. Antippen öffnet
ein Schnellwahl-Overlay mit allen Profilen (aktives mit Häkchen und
Akzentfarbe); ein Tap lädt direkt per loadProfile. Profile-Seite
markiert das aktive Profil in der Liste. Bei älterer S3-Firmware bleibt
der Chip unsichtbar. Version 1.0.13, Protokoll-Doku ergänzt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:55:10 +02:00
thomas 00a4c80b13 Merge pull request 'feat(S3): Aktives Profil führen und anzeigen — Grundlage für Profil-Schnellwahl am Display' (#7) from feat/aktives-profil-anzeige-s3 into main 2026-07-13 20:48:40 +02:00
raw-designsandClaude Fable 5 a1431a9d42 feat(S3): Aktives Profil führen und anzeigen — Grundlage für Profil-Schnellwahl am Display
Die Steuerung merkt sich das zuletzt geladene bzw. gespeicherte Profil
dauerhaft im EEPROM (Adressen 912-943). Profil-relevante Änderungen
(Setpoints, PID inkl. AutoTune, Boost, Eco, Brew-Control, Piezo,
Fast-Heat-Up) markieren das Profil als „geändert"; reine Anzeige-/
Systemeinstellungen bewusst nicht. Löschen des aktiven Profils und
Werksreset setzen die Anzeige zurück.

Sichtbar in der Web-UI (Dashboard + Profil-Seite) und als neue additive
State-Felder "profile"/"profDirty" im UART-JSON für das Touch-Display.
Version 5.1.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:47:59 +02:00
thomas d9bc31b555 Merge pull request 'feat(S3): Display-Auswahl über die Web-UI statt Compile-Schalter — Default „Ohne Display"' (#6) from feat/display-auswahl-über-web-ui into main 2026-07-13 01:56:25 +02:00
raw-designsandClaude Fable 5 5691a4c04f feat(S3): Display-Auswahl in die Web-UI verlegt — Default „Ohne Display" für sicheren Start
Compile-Schalter ENABLE_DISPLAY entfällt: Der Display-Typ (Ohne / OLED SH1106 /
UART-Touch) wird jetzt zur Laufzeit unter /Sensoren gewählt, im EEPROM (Adresse 911)
gespeichert und ohne Neustart übernommen. Vor der OLED-Initialisierung prüft eine
I2C-Probe (0x3C), ob das Display angeschlossen ist — ohne Antwort bleibt die Anzeige
deaktiviert und die Steuerung bootet sicher weiter. Die Touch-UART (Serial1) wird nur
noch bei gewähltem UART-Touch-Display initialisiert und bedient.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:54:37 +02:00
thomas f8df6f85cd Merge pull request 'fix(P4): Heizleistungs-Kurven flackerten in großen Zeitfenstern — Bucket-Mittelung statt Einzelsample' (#5) from fix/verlauf-duty-dezimierung-mitteln into main 2026-07-13 01:27:13 +02:00
raw-designsandClaude Fable 5 f43375e991 fix(P4): Heizleistungs-Kurven flackerten in großen Zeitfenstern — Bucket-Mittelung statt Einzelsample
Die Dezimierung im Verlauf-Chart pickte pro Punkt ein Einzelsample; das
Abtast-Raster hing an g_histPos und wanderte mit jedem Sekundentick um
eine Position. Die schnell schaltende Heizleistung (PWM) sprang dadurch
sichtbar zwischen zwei Kurvenbildern hin und her. Jetzt wird über den
gesamten stride-Bucket gemittelt — Bild steht ruhig, Leistungs-Kurve
zeigt die echte mittlere Heizleistung. Version 1.0.12.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 01:26:26 +02:00
thomas 92ef4fbfb6 Merge pull request 'fix(P4): Verlaufs-Historie ins PSRAM verlagern — interner RAM lief über' (#4) from fix/verlaufs-historie-ins-psram into main 2026-07-12 21:17:13 +02:00
8 changed files with 1695 additions and 153 deletions
+81
View File
@@ -1,3 +1,84 @@
Version 5.3.1:
- Cold Extraction ist jetzt persistent: Der Modus wird im EEPROM gespeichert und überlebt sowohl den
Standby als auch einen Neustart. Zuvor wurde er beim Standby beendet und war nach dem Aufwachen aus -
die Maschine heizte dann erst auf, obwohl der nächste kalte Bezug einen kalten Kessel braucht, und man
musste auf das Abkühlen warten. Wer nur kalte Bezüge machen will, schaltet den Modus damit einmal ein
und wieder aus, wenn er fertig ist.
- Der Modus fällt nur weg, wenn auf /Brew-Control die Freischaltung entzogen wird oder ein Werksreset
läuft. Solange er aktiv ist, heizt der Wasserkreis nicht - Dashboard, OLED („(COLD)" in der Wasserzeile)
und Touch-Display zeigen das durchgehend an.
Version 5.3.0:
- Neu: Cold Extraction (kalte Extraktion). Die Maschine hat keinen Bypass um den Wasserkessel, kaltes Wasser
kommt also nur bei kaltem Kessel am Puck an. Die Funktion wird deshalb über eine Temperaturschwelle
freigegeben (Standard 30 °C, einstellbar 1040 °C) und ist praktisch aus dem kalten Zustand heraus nutzbar.
- Solange der Modus aktiv ist, heizt der Wasserkreis nicht (SSR gesperrt, Stellgröße und damit auch die
dutyW-Telemetrie auf 0, I-Anteil der Wasser-PID eingefroren, Feed-Forward-Boost deaktiviert). Der
Dampfkreis bleibt unberührt und darf weiter heizen.
- Die Freigabe wird vor jedem Bezug neu geprüft, nicht nur beim Aktivieren: Da der Dampfkessel weiter heizt,
kann der Wasserkessel über die Zeit über die Schwelle steigen. In diesem Fall wird der nächste Bezug
abgewiesen (Meldung auf OLED, Touch-Display und im Web-Dashboard) - ein laufender Bezug wird nie abgebrochen.
Hysterese von 5 °C, damit die Anzeige an der Schwelle nicht flattert; Sensorfehler sperrt die Funktion.
- Der Fluss wird über Pumpen-Pulsung reduziert (Pulsperiode 2005000 ms, Pumpenleistung 5100 %), davor läuft
eine einstellbare Vorbenetzung mit voller Pumpenleistung. Bezugsende über Zielgewicht oder Sicherheits-Timeout;
Brew-by-Time/Brew-by-Weight und FlowGuard sind während einer kalten Extraktion bewusst außer Kraft (FlowGuard
und Cold Extraction würden sonst um denselben Pumpen-Ausgang konkurrieren).
- Pumpenschutz: Die Steuerung zählt die kumulierte Pumpen-Einschaltzeit und legt nach der eingestellten
Laufzeit eine Zwangspause ein (Standard 60 s Laufzeit / 20 s Pause). Zusätzlich Stillstands-Erkennung -
läuft die Pumpe bei aktiver Waage 15 s ohne Zulauf, wird der Bezug beendet (dichter Puck oder ausgelöster
Pumpen-Thermoschutz).
- Bedienung: Parameter auf der Seite /Brew-Control, Ein-/Ausschalten über das Web-Dashboard oder das
UART-Touch-Display (neue Aktionen "startColdExtraction"/"stopColdExtraction"/"toggleColdExtraction").
Cold Extraction ist bewusst kein Profilfeld (maschinen- und situationsabhängig, wie der Feed-Forward);
der Modus selbst wird nicht persistiert und ist nach einem Neustart immer aus. Standby beendet ihn.
- Statistik: Kalte Bezüge werden im Log mit einem vierten CSV-Feld "C" gekennzeichnet, zählen in allen
Stückzahlen mit, gehen aber NICHT in die durchschnittliche Bezugsdauer ein (Minuten statt Sekunden würden
den Wert verzerren). Die Info-Seite weist die Anzahl separat aus, der Verlaufs-Download hat eine neue
Spalte "Art". Ältere Logzeilen bleiben unverändert lesbar.
- Neue additive State-Felder im UART-JSON (Protokoll bleibt v2): cxEnabled, cxActive, cxShot, cxAllowed,
cxResting, cxMaxTemp, cxTarget, cxMaxSecs, cxMessage sowie "cx" pro Eintrag in usageStats; neuer
statusKey "coldextraction". Ältere Display-Firmware ignoriert die Felder.
Version 5.2.0:
- Feed-Forward bei Bezug (Störgrößenaufschaltung) für den Wasserkessel: Während eines Bezugs wird der
Heizung sofort ein einstellbarer Leistungs-Boost (0100 % der Fenstergröße, 0 % = aus) aufgeschaltet,
statt auf den vom Sensor gemeldeten Temperaturabfall zu warten. Die PID regelt nur noch den Restfehler;
der Temperatur-Dip während des Shots wird deutlich kleiner. Einstellung auf der /PID-Seite
(„Feed-Forward bei Bezug"), gespeichert im EEPROM (bewusst kein Profilfeld, da maschinenabhängig).
- Optional: I-Anteil der Wasser-PID während des Bezugs einfrieren. Verhindert das Aufladen des
Integral-Anteils während des Shots und damit das Überschwingen nach Bezugsende.
- Der Boost wirkt nur bei echten Bezügen (Hardware-Schalter oder Software-Start), nicht bei
Spülen/Wartungsmodus/AutoTune. Sicherheitslogik unverändert: Sensorfehler, Übertemperatur-Abschaltung
und „Heizen oberhalb Setpoint verhindern" haben weiterhin Vorrang; Deckelung auf die Fenstergröße.
- Die Heizleistungs-Telemetrie (dutyW, u. a. für das Verlaufs-Chart des Touch-Displays) zeigt den
Boost mit an - damit ist die Wirkung direkt im Chart kalibrierbar.
Version 5.1.2:
- Profil-Seite (Web-UI): Die neue „Aktives Profil"-Anzeige stand linksbündig außerhalb des
Karten-Layouts. Sie nutzt jetzt den status-message-Stil der Seite (zentriert, max. 600 px,
dezenter Rahmen) und fügt sich damit in das bestehende Layout ein; Profilname zusätzlich fett.
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
+44 -1
View File
@@ -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 |
@@ -313,6 +315,26 @@ Alle gültigen `action`-Strings aus `executeDashboardAction()` (Stand v4.8.1, Ze
| `disableSteamHeat` | — | Dampf-Heizung abschalten | | `disableSteamHeat` | — | Dampf-Heizung abschalten |
| `enableSteamHeat` | — | Dampf-Heizung wieder aktivieren | | `enableSteamHeat` | — | Dampf-Heizung wieder aktivieren |
**Cold Extraction (kalte Extraktion, ab S3 5.3.0)**
| `action` | `value` | Wirkung |
|---|---|---|
| `startColdExtraction` | — | Modus aktivieren (Wasserkreis heizt nicht, Dampf bleibt möglich) |
| `stopColdExtraction` | — | Modus beenden |
| `toggleColdExtraction` | — | Modus umschalten |
Alle drei Aktionen werden abgewiesen (`ok:false` + `message`), solange die Bedingungen nicht
erfüllt sind — Hauptfall ist ein zu warmer Wasserkessel (`cxAllowed:false`). Der Modus ist
**persistent** (EEPROM) und überlebt Standby und Neustart; er endet nur durch explizites
Ausschalten, durch Entzug der Freischaltung auf `/Brew-Control` oder durch einen Werksreset.
Das P4-UI sollte `cxActive` deshalb dauerhaft sichtbar spiegeln — sonst wundert sich der
Benutzer, warum die Maschine nicht heizt. Ein laufender Bezug wird nie abgebrochen,
`stopColdExtraction` schlägt dann fehl.
Die Parameter der kalten Extraktion (Schwelle, Vorbenetzung, Pulsperiode, Pumpenleistung,
Zielgewicht, Timeout, Pumpenschutz) sind bewusst **nicht** über `saveBrew` erreichbar, sondern
nur über die Web-Seite `/Brew-Control`. Das Display schaltet den Modus, konfiguriert ihn nicht.
**Persistente Toggles** **Persistente Toggles**
| `action` | `value` | Wirkung | | `action` | `value` | Wirkung |
@@ -352,7 +374,28 @@ Aus `getTouchUartStatus()`. Für sprach-unabhängige UI-Logik/Icons auf dem P4 v
(statt `statusText` zu parsen): (statt `statusText` zu parsen):
`ready`, `error`, `standby`, `maintenance`, `tuning`, `brewing`, `eco`, `fastheatup`, `ready`, `error`, `standby`, `maintenance`, `tuning`, `brewing`, `eco`, `fastheatup`,
`heating`, `ready_eco_pending`. `heating`, `ready_eco_pending`, `coldextraction` (ab S3 5.3.0).
### Cold-Extraction-Felder im `state` (ab S3 5.3.0, additiv — Protokoll bleibt v2)
| Feld | Typ | Bedeutung |
|---|---|---|
| `cxEnabled` | bool | Funktion in der Web-UI freigeschaltet (sonst Bedienelement ausblenden) |
| `cxActive` | bool | Modus scharf: Wasserkreis heizt nicht |
| `cxShot` | bool | Der laufende Bezug ist eine kalte Extraktion |
| `cxAllowed` | bool | Freigabe **jetzt** (Wasserkessel kalt genug, kein Standby/Wartung/Tuning) |
| `cxResting` | bool | Zwangspause des Pumpenschutzes läuft (Bezug läuft weiter) |
| `cxMaxTemp` | float | Freigabeschwelle in °C (für eigene Hinweistexte am Display) |
| `cxTarget` | float | Zielgewicht in g |
| `cxMaxSecs` | float | Sicherheits-Timeout des Bezugs in s |
| `cxMessage` | string | Sperr-/Abbruchmeldung, leer = keine (läuft nach ~6 s selbst ab) |
Während `cxShot` folgt die Phase weiterhin `piState`: `1` = Vorbenetzung, `3` = Hauptbezug
(gepulst). Eine Pause-Phase (`2`) gibt es bei der kalten Extraktion nicht.
In der Antwort auf `getUsageStats` trägt jeder Eintrag in `shots[]` zusätzlich `cx` (bool) —
kalte Bezüge zählen in `total`/`today`/`week` mit, sind aber in der durchschnittlichen
Bezugsdauer der S3-Statistik ausgenommen.
--- ---
+1342 -119
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -1,3 +1,25 @@
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: Version 1.0.11:
- Verlauf-Seite ausgebaut (nur P4, keine S3-/Protokolländerung nötig): - 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. - Umschaltbares Zeitfenster: Buttons „2 min / 10 min / 30 min / 60 min" in der Kopfzeile.
+1 -1
View File
@@ -65,7 +65,7 @@
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
// Firmware // Firmware
// ------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------
#define DISPLAY_FW_VERSION "1.0.11" // 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
+2
View File
@@ -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;
+2
View File
@@ -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;
+178 -9
View File
@@ -104,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;
@@ -1483,21 +1489,40 @@ static void chart_redraw() {
int32_t lo = INT32_MAX, hi = INT32_MIN; // Zehntelgrad int32_t lo = INT32_MAX, hi = INT32_MIN; // Zehntelgrad
bool anyW = false, anyD = false; bool anyW = false, anyD = false;
for (int i = 0; i < 120; i++) { for (int i = 0; i < 120; i++) {
int age = (119 - i) * stride; // Alter der Probe (Sekunden) int ageStart = (119 - i) * stride; // juengstes Alter im Bucket (Sekunden)
int32_t vW = LV_CHART_POINT_NONE, vD = LV_CHART_POINT_NONE, vC = LV_CHART_POINT_NONE; int32_t 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; int32_t dW = LV_CHART_POINT_NONE, dD = LV_CHART_POINT_NONE;
if (age < (int)g_histFill) { 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; int idx = (int)g_histPos - 1 - age;
if (idx < 0) idx += CHART_HIST_MAX; if (idx < 0) idx += CHART_HIST_MAX;
vW = g_hist->w[idx]; sW += g_hist->w[idx];
vD = g_hist->d[idx]; sD += g_hist->d[idx];
if (g_hist->c[idx] != CHART_HIST_NONE) vC = g_hist->c[idx]; sDW += g_hist->dutyW[idx];
dW = g_hist->dutyW[idx]; sDD += g_hist->dutyD[idx];
dD = 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 (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 (vD > 300) { anyD = true; if (vD < lo) lo = vD; if (vD > hi) hi = vD; }
if (vC != LV_CHART_POINT_NONE && vC > 300) { if (vC < lo) lo = vC; if (vC > hi) hi = vC; } if (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_serW, i, vW);
lv_chart_set_value_by_id(g_chart, g_serD, i, vD); 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_serC, i, vC);
@@ -2356,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);
@@ -2840,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);
@@ -3418,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;
@@ -3610,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);
} }
} }