|
|
|
@@ -52,8 +52,17 @@
|
|
|
|
|
// Standard-Waagen-Typ (kann in /Sensoren geändert werden)
|
|
|
|
|
#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
|
|
|
|
@@ -80,6 +89,9 @@ bool executeDashboardAction(const String& action, const String& value,
|
|
|
|
|
bool& settingsChanged, bool& pidNeedsUpdate);
|
|
|
|
|
void touchUartRxTick();
|
|
|
|
|
void touchUartTick();
|
|
|
|
|
void initTouchUart();
|
|
|
|
|
bool initOledDisplay(bool showSplash);
|
|
|
|
|
void applyDisplayTypeChange(uint8_t newType);
|
|
|
|
|
void applyLightOutput();
|
|
|
|
|
void initResetDiagnostics();
|
|
|
|
|
void recordResetCheckpoint(uint16_t checkpoint);
|
|
|
|
@@ -91,10 +103,10 @@ void syncFullyKioskWithStandby(bool active);
|
|
|
|
|
************************************************************************************/
|
|
|
|
|
#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_SH110X.h>
|
|
|
|
|
#endif // ENABLE_DISPLAY
|
|
|
|
|
|
|
|
|
|
#include <EEPROM.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
|
|
|
|
|
unsigned long lastDashboardWsPushMs = 0;
|
|
|
|
|
|
|
|
|
|
// JC-Touch-Display (UART). Bewusst UNABHAENGIG von ENABLE_DISPLAY (OLED), damit alle
|
|
|
|
|
// Kombinationen baubar sind (nur OLED / nur JC / beide / keines). Auf 1 setzen, sobald
|
|
|
|
|
// das JC-Display an Serial1 (GPIO43/44) angeschlossen ist.
|
|
|
|
|
// JC-Touch-Display (UART). Ob die UART-Anbindung genutzt wird, entscheidet zur
|
|
|
|
|
// Laufzeit displayType (Web-UI /Sensoren); dieser Compile-Schalter bleibt als
|
|
|
|
|
// Notausstieg, um den Code komplett herauszubauen.
|
|
|
|
|
#define TOUCH_UART_ENABLED 1
|
|
|
|
|
#define TOUCH_UART_TX_PIN 43
|
|
|
|
|
#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;
|
|
|
|
|
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;
|
|
|
|
|
String touchUartRxLine;
|
|
|
|
|
bool touchUartRxOverflow = false;
|
|
|
|
@@ -342,19 +361,15 @@ MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO);
|
|
|
|
|
/************************************************************************************
|
|
|
|
|
* Display-Objekt (Adafruit SH1106G)
|
|
|
|
|
************************************************************************************/
|
|
|
|
|
#ifdef ENABLE_DISPLAY
|
|
|
|
|
/************************************************************************************
|
|
|
|
|
* Display-Objekt (Adafruit SH1106G)
|
|
|
|
|
************************************************************************************/
|
|
|
|
|
// Dieser Block muss vorhanden sein, wenn Display aktiviert!
|
|
|
|
|
// Objekt existiert immer; initialisiert wird es nur bei displayType == DISPLAY_TYPE_OLED
|
|
|
|
|
// und erfolgreicher I2C-Probe (siehe initOledDisplay()).
|
|
|
|
|
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
|
|
|
|
|
#endif // ENABLE_DISPLAY
|
|
|
|
|
|
|
|
|
|
/************************************************************************************
|
|
|
|
|
* Firmware-Informationen
|
|
|
|
|
************************************************************************************/
|
|
|
|
|
|
|
|
|
|
String version = "5.0.12";
|
|
|
|
|
String version = "5.1.1";
|
|
|
|
|
String versionHersteller = "Thomas Müller";
|
|
|
|
|
String versionHerstellerMail =
|
|
|
|
|
"<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_STANDBY_CLOCK_PERCENT = 909; // 1 Byte (uint8_t) -> Ende 909
|
|
|
|
|
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
|
|
|
|
@@ -1296,15 +1314,52 @@ char hostname[33] = "Dual-PID"; // Max 32 Zeichen + Nullterminator
|
|
|
|
|
String profileStatusMessage = ""; // für Nachrichten auf der Profil-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)
|
|
|
|
|
************************************************************************************/
|
|
|
|
|
#ifdef ENABLE_DISPLAY
|
|
|
|
|
static const unsigned char PROGMEM wifiSymbol[] = {
|
|
|
|
|
0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48,
|
|
|
|
|
0x27, 0x20, 0x08, 0x80, 0x02, 0x00
|
|
|
|
|
};
|
|
|
|
|
#endif // ENABLE_DISPLAY
|
|
|
|
|
|
|
|
|
|
/************************************************************************************
|
|
|
|
|
* Gemeinsame CSS-Styles (PROGMEM) - Unverändert
|
|
|
|
@@ -2451,12 +2506,10 @@ void checkWifiConnection() {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef ENABLE_DISPLAY
|
|
|
|
|
// WiFi-Symbol aufs Display zeichnen
|
|
|
|
|
void drawwifiSymbol(int16_t x, int16_t y) {
|
|
|
|
|
display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
|
|
|
|
|
}
|
|
|
|
|
#endif // ENABLE_DISPLAY
|
|
|
|
|
|
|
|
|
|
/************************************************************************************
|
|
|
|
|
* 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_STANDBY_CLOCK_PERCENT, defaultBacklightStandbyClockPercent);
|
|
|
|
|
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_SCALE_ENABLED, (uint8_t)(defaultScaleEnabled ? 1 : 0));
|
|
|
|
|
EEPROM.put(EEPROM_ADDR_SCALE_TYPE, defaultScaleType);
|
|
|
|
@@ -2912,6 +2966,9 @@ void resetToDefaults() {
|
|
|
|
|
// Alle Änderungen ins EEPROM schreiben
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
|
|
|
|
|
// Werksreset -> kein Profil mehr aktiv
|
|
|
|
|
clearActiveProfile();
|
|
|
|
|
|
|
|
|
|
// WICHTIG: Globale Variablen auch auf Default setzen
|
|
|
|
|
SetpointWasser = defaultSetpointWasser; SetpointDampf = defaultSetpointDampf;
|
|
|
|
|
OffsetWasser = defaultOffsetWasser; OffsetDampf = defaultOffsetDampf;
|
|
|
|
@@ -2960,6 +3017,7 @@ void resetToDefaults() {
|
|
|
|
|
xSwitchLongAction = defaultXSwitchLongAction;
|
|
|
|
|
buttonLongPressMs = (uint16_t)DEFAULT_BUTTON_LONG_PRESS_MS;
|
|
|
|
|
dashboardTempDisplayMode = defaultDashboardTempDisplayMode;
|
|
|
|
|
applyDisplayTypeChange(DEFAULT_DISPLAY_TYPE); // deaktiviert ein ggf. aktives Display sauber
|
|
|
|
|
xSwitchActive = false;
|
|
|
|
|
ecoForcedActive = false;
|
|
|
|
|
standbyOverriddenByWeb = false;
|
|
|
|
@@ -3866,7 +3924,7 @@ void stopBrewSequence(unsigned long durationMillis, bool manualStop = false, boo
|
|
|
|
|
|
|
|
|
|
/************************************************************************************
|
|
|
|
|
* 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
|
|
|
|
|
* - Zusätzliche Serial-Ausgaben für Headless-Betrieb
|
|
|
|
|
************************************************************************************/
|
|
|
|
@@ -3995,15 +4053,6 @@ void setup() {
|
|
|
|
|
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
|
|
|
|
|
loadFullyKioskConfig(fullyKioskConfig);
|
|
|
|
|
|
|
|
|
@@ -4253,6 +4302,34 @@ void setup() {
|
|
|
|
|
storedBacklightStandbyClock = defaultBacklightStandbyClockPercent;
|
|
|
|
|
}
|
|
|
|
|
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 storedScaleType = defaultScaleType;
|
|
|
|
|
EEPROM.get(EEPROM_ADDR_SCALE_ENABLED, storedScaleEnabled);
|
|
|
|
@@ -4356,11 +4433,6 @@ void setup() {
|
|
|
|
|
// Serial.println("---------------------------------------------");
|
|
|
|
|
// 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) {
|
|
|
|
|
// Serial.println(F("WiFi Modus: Access Point (AP)"));
|
|
|
|
|
// Serial.print(F("SSID: ")); Serial.println(WiFi.softAPSSID());
|
|
|
|
@@ -4393,51 +4465,13 @@ void setup() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Display-Initialisierung (nur wenn aktiviert) ---
|
|
|
|
|
#ifdef ENABLE_DISPLAY
|
|
|
|
|
// --- Display-Initialisierung gemaess Web-UI-Einstellung (displayType) ---
|
|
|
|
|
recordResetCheckpoint(RESET_CP_SETUP_DISPLAY);
|
|
|
|
|
if (!display.begin(0x3C, true)) { // display.begin benutzt das ggf. schon initialisierte Wire
|
|
|
|
|
Serial.println(F("Display konnte nicht initialisiert werden!"));
|
|
|
|
|
} else {
|
|
|
|
|
Serial.println(F("Display initialisiert."));
|
|
|
|
|
// 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
|
|
|
|
|
if (displayType == DISPLAY_TYPE_OLED) {
|
|
|
|
|
initOledDisplay(true);
|
|
|
|
|
} else if (displayType == DISPLAY_TYPE_TOUCH_UART) {
|
|
|
|
|
initTouchUart();
|
|
|
|
|
}
|
|
|
|
|
#endif // ENABLE_DISPLAY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// --- Waage Initialisierung (nur ESP32) ---
|
|
|
|
@@ -5177,10 +5211,10 @@ void loop() {
|
|
|
|
|
applyLightOutput();
|
|
|
|
|
|
|
|
|
|
// Display regelmaessig updaten, wenn aktiviert / angeschlossen
|
|
|
|
|
#ifdef ENABLE_DISPLAY
|
|
|
|
|
if (displayType == DISPLAY_TYPE_OLED && oledDisplayPresent) {
|
|
|
|
|
recordResetCheckpoint(RESET_CP_LOOP_DISPLAY);
|
|
|
|
|
updateDisplay();
|
|
|
|
|
#endif // ENABLE_DISPLAY
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Waagen-spezifische Logik in der Hauptschleife ---
|
|
|
|
|
recordResetCheckpoint(RESET_CP_LOOP_SCALE);
|
|
|
|
@@ -5350,8 +5384,108 @@ double readCaseTemperature() {
|
|
|
|
|
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() {
|
|
|
|
|
if (!oledDisplayPresent) { return; }
|
|
|
|
|
recordResetCheckpoint(RESET_CP_DISPLAY_RENDER);
|
|
|
|
|
double displayedWaterTemp = getDisplayedWaterTemperature();
|
|
|
|
|
double displayedSteamTemp = getDisplayedSteamTemperature();
|
|
|
|
@@ -5992,7 +6126,6 @@ if (scaleModeActive) {
|
|
|
|
|
yield();
|
|
|
|
|
display.display(); // Display *immer* am Ende aktualisieren
|
|
|
|
|
}
|
|
|
|
|
#endif // ENABLE_DISPLAY
|
|
|
|
|
|
|
|
|
|
void updateShotTimer() {
|
|
|
|
|
if (cleaningAssistantActive) {
|
|
|
|
@@ -6286,6 +6419,7 @@ void handleAutoTune(AsyncWebServerRequest *request) {
|
|
|
|
|
autoTuneWasserResultKi = (float)KiWasser;
|
|
|
|
|
autoTuneWasserResultKd = (float)KdWasser;
|
|
|
|
|
autoTuneWasserStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS;
|
|
|
|
|
markProfileDirty(); // PID-Werte weichen jetzt vom geladenen Profil ab
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Serial.println("AutoTune Wasser erfolgreich abgeschlossen.");
|
|
|
|
@@ -6344,6 +6478,7 @@ void handleAutoTune(AsyncWebServerRequest *request) {
|
|
|
|
|
autoTuneDampfResultKi = (float)KiDampf;
|
|
|
|
|
autoTuneDampfResultKd = (float)KdDampf;
|
|
|
|
|
autoTuneDampfStatus = (peaks > 9) ? AT_FAILSAFE_PEAKS : AT_SUCCESS;
|
|
|
|
|
markProfileDirty(); // PID-Werte weichen jetzt vom geladenen Profil ab
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Serial.println("AutoTune Dampf erfolgreich abgeschlossen.");
|
|
|
|
@@ -6640,7 +6775,6 @@ bool applyBrewControlUpdate(const BrewControlUpdate& update) {
|
|
|
|
|
if (update.hasFlowGuardEnabled && update.flowGuardEnabled != flowGuardEnabled) {
|
|
|
|
|
flowGuardEnabled = update.flowGuardEnabled;
|
|
|
|
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_ENABLED, flowGuardEnabled);
|
|
|
|
|
changed = true;
|
|
|
|
|
flowGuardConfigChanged = true;
|
|
|
|
|
}
|
|
|
|
|
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) {
|
|
|
|
|
flowGuardMinBrewSeconds = newFlowGuardMinSecs;
|
|
|
|
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_SECONDS, flowGuardMinBrewSeconds);
|
|
|
|
|
changed = 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) {
|
|
|
|
|
flowGuardPulsePeriodMs = newPulseMs;
|
|
|
|
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_PULSE_PERIOD_MS, flowGuardPulsePeriodMs);
|
|
|
|
|
changed = 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) {
|
|
|
|
|
flowGuardMinDutyPercent = newFlowGuardMinDuty;
|
|
|
|
|
EEPROM.put(EEPROM_ADDR_FLOWGUARD_MIN_DUTY, flowGuardMinDutyPercent);
|
|
|
|
|
changed = 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) {
|
|
|
|
|
markProfileDirty();
|
|
|
|
|
}
|
|
|
|
|
if (changed || flowGuardConfigChanged) {
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
}
|
|
|
|
|
return changed;
|
|
|
|
|
return changed || flowGuardConfigChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void handleSaveBrewControl(AsyncWebServerRequest *request) {
|
|
|
|
@@ -7652,6 +7789,25 @@ static const char sensorsHtmlHeadEnd[] PROGMEM = R"rawliteral(
|
|
|
|
|
static const char sensorsBodyStart[] PROGMEM = R"rawliteral(
|
|
|
|
|
<body>
|
|
|
|
|
<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'>
|
|
|
|
|
<input type='hidden' name='sensorSection' value='case'>
|
|
|
|
|
<h3>Zusatz-Temperatur-Sensor</h3>
|
|
|
|
@@ -7902,6 +8058,18 @@ void handleSensors(AsyncWebServerRequest *request) {
|
|
|
|
|
response->print(FPSTR(sensorsHtmlHeadEnd));
|
|
|
|
|
response->print(FPSTR(commonNav));
|
|
|
|
|
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) {
|
|
|
|
|
response->print(F(" checked"));
|
|
|
|
|
}
|
|
|
|
@@ -8409,6 +8577,7 @@ void handleFastHeatUpSettings(AsyncWebServerRequest *request) {
|
|
|
|
|
|
|
|
|
|
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
markProfileDirty(); // fastHeatUpAktiv ist Profilfeld
|
|
|
|
|
|
|
|
|
|
AsyncWebServerResponse *resp = request->beginResponse(303);
|
|
|
|
|
resp->addHeader(F("Location"), F("/Fast-Heat-Up"));
|
|
|
|
@@ -8901,6 +9070,7 @@ void handleSettingsUpdate(AsyncWebServerRequest *request) {
|
|
|
|
|
// --- EEPROM speichern und PID aktualisieren ---
|
|
|
|
|
if (valueChanged) { // Nur speichern, wenn sich mindestens ein Wert geändert hat
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
markProfileDirty(); // Setpoints/Offsets/PID/Boost/MaxTemp/Window sind Profilfelder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// PID-Parameter an die Regler übergeben
|
|
|
|
@@ -9288,6 +9458,7 @@ void handleUpdateAutotuneSettings(AsyncWebServerRequest *request) {
|
|
|
|
|
|
|
|
|
|
if (changed) {
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
markProfileDirty(); // AutoTune-Parameter sind Profilfelder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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_STANDBY_CLOCK_PERCENT, backlightStandbyClockPercent);
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
// Eco/Dampfverzoegerung/Wake-Verhalten sind Profilfelder -> Profil als geaendert markieren
|
|
|
|
|
markProfileDirty();
|
|
|
|
|
|
|
|
|
|
// Wenn Eco deaktiviert, Setpoints zurückladen
|
|
|
|
|
if (ecoModeMinutes == 0 && !ecoSwitchActive) {
|
|
|
|
@@ -9698,6 +9871,7 @@ void handleUpdatePiezoSettings(AsyncWebServerRequest *request) {
|
|
|
|
|
|
|
|
|
|
if (changed) {
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
markProfileDirty(); // piezoEnabled ist Teil des Profils
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AsyncWebServerResponse *resp = request->beginResponse(303);
|
|
|
|
@@ -9806,6 +9980,10 @@ struct SensorSettingsUpdate {
|
|
|
|
|
uint8_t xSwitchLongAction = 0;
|
|
|
|
|
bool hasButtonLongPressMs = false;
|
|
|
|
|
uint16_t buttonLongPressMs = 0;
|
|
|
|
|
|
|
|
|
|
bool updateDisplaySection = false;
|
|
|
|
|
bool hasDisplayType = false;
|
|
|
|
|
uint8_t displayType = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
}
|
|
|
|
@@ -9959,11 +10147,13 @@ void handleUpdateSensorSettings(AsyncWebServerRequest *request) {
|
|
|
|
|
bool isCaseSection = (section.length() == 0 || section == "case");
|
|
|
|
|
bool isScaleSection = (section.length() == 0 || section == "scale");
|
|
|
|
|
bool isXSwitchSection = (section.length() == 0 || section == "xSwitch");
|
|
|
|
|
bool isDisplaySection = (section.length() == 0 || section == "display");
|
|
|
|
|
|
|
|
|
|
SensorSettingsUpdate update;
|
|
|
|
|
update.updateCaseSection = isCaseSection;
|
|
|
|
|
update.updateScaleSection = isScaleSection;
|
|
|
|
|
update.updateXSwitchSection = isXSwitchSection;
|
|
|
|
|
update.updateDisplaySection = isDisplaySection;
|
|
|
|
|
|
|
|
|
|
if (isCaseSection) {
|
|
|
|
|
update.hasCaseEnabled = true;
|
|
|
|
@@ -9989,6 +10179,11 @@ void handleUpdateSensorSettings(AsyncWebServerRequest *request) {
|
|
|
|
|
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) {
|
|
|
|
|
update.hasXSwitchAction = request->hasArg("xSwitchAction");
|
|
|
|
|
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)) {
|
|
|
|
|
// Serial.println(" Profile saved successfully (Binary Format).");
|
|
|
|
|
// Gespeicherter Ist-Zustand entspricht ab jetzt diesem Profil -> als aktiv merken
|
|
|
|
|
setActiveProfile(displayName);
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
// 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
|
|
|
|
|
ecoModeAktiv = false; ecoModeActivatedTime = 0;
|
|
|
|
|
// 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.remove(filePath)) {
|
|
|
|
|
// Serial.println(" Profile deleted successfully.");
|
|
|
|
|
// War das geloeschte Profil das aktive -> Anzeige zuruecksetzen
|
|
|
|
|
if (activeProfileName.length() > 0 &&
|
|
|
|
|
sanitizeProfileName(activeProfileName) == sanitizedName) {
|
|
|
|
|
clearActiveProfile();
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
} else {
|
|
|
|
|
// Serial.println(" ERROR: Failed to delete profile file.");
|
|
|
|
@@ -11626,6 +11831,19 @@ void handleProfilesPage(AsyncWebServerRequest *request) {
|
|
|
|
|
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
|
|
|
|
|
response->print(FPSTR(profilesPageListStart)); // <ul>
|
|
|
|
|
if (profiles.empty()) {
|
|
|
|
@@ -11637,6 +11855,9 @@ void handleProfilesPage(AsyncWebServerRequest *request) {
|
|
|
|
|
String escapedConfirmJs = htmlEscape(confirmJs);
|
|
|
|
|
String listItem = FPSTR(profilesPageListItemStart);
|
|
|
|
|
listItem += escapedName;
|
|
|
|
|
if (name == activeProfileName) {
|
|
|
|
|
listItem += F(" <span style=\"color:#4CAF50;\" title=\"aktives Profil\">●</span>");
|
|
|
|
|
}
|
|
|
|
|
listItem += F("</span><div class=\"profile-actions\">");
|
|
|
|
|
listItem += F("<form action=\"/loadProfile\" method=\"POST\" class=\"profiles-form\"><input type=\"hidden\" name=\"profileName\" value=\"");
|
|
|
|
|
listItem += escapedName;
|
|
|
|
@@ -13440,6 +13661,7 @@ static const char dashboardControlPanelHTML[] PROGMEM = R"rawliteral(
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<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">
|
|
|
|
|
<option value="">Profil wählen...</option>
|
|
|
|
|
{PROFILE_OPTIONS} </select>
|
|
|
|
@@ -13747,6 +13969,16 @@ static const char dashboardJavaScript[] PROGMEM = R"rawliteral(
|
|
|
|
|
// Status Text
|
|
|
|
|
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) {
|
|
|
|
|
const showCaseTemp = (data.caseTempDashboard === true);
|
|
|
|
|
caseTempDisplay.style.display = showCaseTemp ? 'block' : 'none';
|
|
|
|
@@ -14620,6 +14852,14 @@ bool executeDashboardAction(const String& action, const String& value,
|
|
|
|
|
// --- Abschlussaktionen ---
|
|
|
|
|
if (settingsChanged) {
|
|
|
|
|
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) {
|
|
|
|
|
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, "\"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);
|
|
|
|
|
offset += snprintf(buffer + offset, bufferSize - offset, "\"caseTempDashboard\":%s,", caseTempDashboardActive ? "true" : "false");
|
|
|
|
|
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, "\"lightOn\":%s,", lightOn ? "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(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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
#if TOUCH_UART_ENABLED
|
|
|
|
|
if (displayType != DISPLAY_TYPE_TOUCH_UART) { return; }
|
|
|
|
|
touchUart.println(line);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
@@ -16126,6 +16396,7 @@ static bool touchUartHandleCommandLine(const String& line) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
EEPROM.commit();
|
|
|
|
|
markProfileDirty(); // PID-Werte sind Profilfelder
|
|
|
|
|
if (changedW) { pidWasser.SetTunings(KpWasser, KiWasser, KdWasser); }
|
|
|
|
|
if (changedD) { pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); }
|
|
|
|
|
touchUartSendAck(id, true, "PID gespeichert");
|
|
|
|
@@ -16283,6 +16554,7 @@ static bool touchUartHandleCommandLine(const String& line) {
|
|
|
|
|
|
|
|
|
|
void touchUartRxTick() {
|
|
|
|
|
#if TOUCH_UART_ENABLED
|
|
|
|
|
if (displayType != DISPLAY_TYPE_TOUCH_UART) return;
|
|
|
|
|
if (displayOtaActive) return; // waehrend OTA hat der Upload-Handler die UART exklusiv
|
|
|
|
|
recordResetCheckpoint(RESET_CP_TOUCH_UART_RX);
|
|
|
|
|
while (touchUart.available() > 0) {
|
|
|
|
@@ -16359,6 +16631,7 @@ static void touchUartPollWifiScan() {
|
|
|
|
|
|
|
|
|
|
void touchUartTick() {
|
|
|
|
|
#if TOUCH_UART_ENABLED
|
|
|
|
|
if (displayType != DISPLAY_TYPE_TOUCH_UART) return;
|
|
|
|
|
if (displayOtaActive) return; // waehrend OTA keine State-Pushes
|
|
|
|
|
recordResetCheckpoint(RESET_CP_TOUCH_UART_TX);
|
|
|
|
|
unsigned long now = millis();
|
|
|
|
|