Files
Dual-PID/Sicherungen/Dual_PID_FastHeatUp - 31.03.2025.ino
T
raw-designs 453644816f Initiale Bereitstellung
Initiale Bereitstellung der aktuellen Version auf Gitea
2026-07-10 18:13:43 +02:00

3176 lines
121 KiB
Arduino

/************************************************************************************
* Dual-PID Controller Firmware
* Version 2.5.2
*
* Dieses Programm steuert Wasser- und Dampftemperatur über zwei unabhängige PID-Regler
* mithilfe eines MAX6675-Thermo-Elements (Dampf) und eines NTC-Sensors (Wasser).
* Zusätzlich werden diverse Funktionen wie Eco-Modus, Profile, Fast-Heat-Up und
* WLAN-Funktionen für eine Weboberfläche bereitgestellt.
*
* Compiler- und Hardware-Umgebung:
* - ESP8266: Wemos D1 mini, 4MB
* - Bibliotheken: Wire, Adafruit_GFX, Adafruit_SH110X, EEPROM, PID_v1, PID_AutoTune_v0,
* max6675, ESP8266WiFi, ESP8266WebServer
************************************************************************************/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <EEPROM.h>
#include <PID_v1.h>
#include <PID_AutoTune_v0.h>
#include "max6675.h"
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
/************************************************************************************
* Hardware-Pinbelegung
************************************************************************************/
#define SSR_WASSER_PIN D2 // SSR für Wasser-Heizung
#define SSR_DAMPF_PIN D1 // SSR für Dampf-Heizung
#define SHOT_TIMER_PIN D8 // Input für Shot-Timer
#define OLED_SDA D3 // SDA-Leitung für OLED
#define OLED_SCK D4 // SCK-Leitung für OLED
#define ANALOG_NTC_PIN A0 // Analoger Eingang für den NTC-Sensor
// Display-Objekt (Adafruit SH1106G)
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
/************************************************************************************
* MAX6675 (Dampf-Temperatursensor)
************************************************************************************/
#define MAX6675_SCK D7 // Serial Clock
#define MAX6675_CS D6 // Chip Select
#define MAX6675_SO D5 // Serial Out
MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO);
/************************************************************************************
* Firmware-Informationen
************************************************************************************/
String version = "2.5.7";
String versionHersteller = "Thomas M&uuml;ller";
String versionHerstellerMail =
"<a href='mailto:thomas@mueller.black' "
"style='color: #d89904 !important; text-decoration: none !important;'>"
"thomas@mueller.black</a>";
String versionHerstellerWeb =
"<a href='https://raw-designs.de/' target='_blank' "
"style='color: #d89904 !important; text-decoration: none !important;'>"
"https://raw-designs.de/</a> | "
"<a href='https://mueller.black/' target='_blank' "
"style='color: #d89904 !important; text-decoration: none !important;'>"
"https://mueller.black</a>";
// Demo-Modus: Hier können bestimmte Funktionen (WLAN-Anpassungen etc.) eingeschränkt werden.
bool demoModus = true;
// Dieses Kennwort muss in einer Firmware-Binärdatei vorhanden sein, damit ein Update gültig ist.
static const char FIRMWARE_PASSWORD[] = "FWKennwort123";
static bool passwordFound = false;
static int passMatchPos = 0;
const int passLength = sizeof(FIRMWARE_PASSWORD) - 1;
/************************************************************************************
* Geräte-Infos
************************************************************************************/
char infoHersteller[50];
char infoModell[50];
char infoZusatz[50];
/************************************************************************************
* NTC-Sensor-Parameter (Wasser)
* - A0 wird verwendet, um die Temperatur über einen Spannungsteiler zu messen
************************************************************************************/
const double V_SUPPLY = 3.3; // Versorgungsspannung
const double ADC_REF = 3.3; // ADC-Referenz
const double R_FIXED = 3000.0; // Festwiderstand 3 kΩ
// Daten des NTC
const double T0 = 298.15; // Referenztemperatur in Kelvin (25°C)
const double R0 = 50000.0; // NTC-Widerstand bei 25°C
const double beta = 3950.0; // Beta-Wert
/************************************************************************************
* Temperatur-Profil-Struktur
* - Speichert Wasser- und Dampf-Setpoint, Eco-Einstellungen und Profilname
************************************************************************************/
struct TemperatureProfile
{
double setpointWasser;
double setpointDampf;
int ecoModeMinutes;
bool dynamicEcoActive;
int dampfVerzoegerung;
char profileName[20];
};
/************************************************************************************
* PID-Regler-Konfiguration
************************************************************************************/
double SetpointDampf, InputDampf, OutputDampf;
double SetpointWasser, InputWasser, OutputWasser;
double OffsetDampf = 0.0, OffsetWasser = 0.0;
// PID-Variablen (Regelungsparameter)
double KpDampf = 3.0, KiDampf = 0.1, KdDampf = 0.5;
double KpWasser = 5.0, KiWasser = 0.2, KdWasser = 1.0;
// Instanziierung der PID-Regler
PID pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT);
PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, DIRECT);
// Variablen für Sicherheitsabschaltung
double maxTempWasser = 110.0; // Standardwert, wie bisher hardcoded
double maxTempDampf = 180.0; // Standardwert, wie bisher hardcoded
/************************************************************************************
* Standardwerte und Default-Einstellungen
************************************************************************************/
double defaultSetpointDampf = 165.0;
double defaultSetpointWasser = 93.0;
double defaultOffsetDampf = 0.0;
double defaultOffsetWasser = 0.0;
double defaultKpDampf = 3.0;
double defaultKiDampf = 0.1;
double defaultKdDampf = 0.5;
double defaultKpWasser = 5.0;
double defaultKiWasser = 0.2;
double defaultKdWasser = 1.0;
int defaultEcoModeMinutes = 0;
int defaultEcoModeTempWasser = 60;
int defaultEcoModeTempDampf = 60;
String defaultInfoHersteller = "";
String defaultInfoModell = "";
String defaultInfoZusatz = "";
int defaultDampfVerzoegerung = 0;
int dampfVerzoegerung = 0; // Dampfverzögerung in Minuten
/************************************************************************************
* Webserver und zugehörige Objekte
************************************************************************************/
ESP8266WebServer server(80); // Webserver auf Port 80
// MagicValue zum Erkennen bereits gespeicherter EEPROM-Daten
const char storageMagicValue[5] = "MGVE";
/************************************************************************************
* EEPROM Adressen für das Speichern/Laden von Werten
************************************************************************************/
const int EEPROM_ADDR_MAGICVALUE = 0; // 5 Bytes (char[5])
const int EEPROM_ADDR_SETPOINT_DAMPF = 5; // 8 Bytes (double)
const int EEPROM_ADDR_SETPOINT_WASSER = 13; // 8 Bytes
const int EEPROM_ADDR_OFFSET_DAMPF = 21; // 8 Bytes
const int EEPROM_ADDR_OFFSET_WASSER = 29; // 8 Bytes
const int EEPROM_ADDR_KP_DAMPF = 37; // 8 Bytes
const int EEPROM_ADDR_KI_DAMPF = 45; // 8 Bytes
const int EEPROM_ADDR_KD_DAMPF = 53; // 8 Bytes
const int EEPROM_ADDR_KP_WASSER = 61; // 8 Bytes
const int EEPROM_ADDR_KI_WASSER = 69; // 8 Bytes
const int EEPROM_ADDR_KD_WASSER = 77; // 8 Bytes
const int EEPROM_ADDR_ECOMODE_MINUTES = 85; // 4 Bytes (int)
const int EEPROM_ADDR_ECOMODE_TEMP_WASSER = 89; // 4 Bytes
const int EEPROM_ADDR_ECOMODE_TEMP_DAMPF = 93; // 4 Bytes
const int EEPROM_ADDR_INFO_HERSTELLER = 97; // 50 Bytes (char[50])
const int EEPROM_ADDR_INFO_MODELL = 147; // 50 Bytes
const int EEPROM_ADDR_INFO_ZUSATZ = 197; // 50 Bytes
const int EEPROM_ADDR_FASTHEATUP_DATA = 247; // 1 Byte (bool)
const int EEPROM_ADDR_RUNTIME = 248; // 4 Bytes (unsigned long)
const int EEPROM_ADDR_SHOTCOUNTER = 252; // 4 Bytes (unsigned long)
const int EEPROM_ADDR_DYNAMIC_ECO_MODE = 256; // 1 Byte (bool)
const int EEPROM_ADDR_PROFILE_1 = 257; // 50 Bytes für Profil 1 (struct TemperatureProfile)
const int EEPROM_ADDR_PROFILE_2 = 307; // 50 Bytes für Profil 2 (struct TemperatureProfile)
// --- AutoTune Parameter Wasser ---
const int EEPROM_ADDR_TUNING_STEP_WASSER = 357; // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_NOISE_WASSER = 365; // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_STARTVALUE_WASSER = 373; // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_LOOKBACK_WASSER = 381; // 4 Bytes (unsigned int) -> Ende 384
// --- WiFi Konfiguration ---
const int EEPROM_ADDR_WIFI_CONFIG_MAGIC = 385; // 5 Bytes (char[5])
const int EEPROM_ADDR_WIFI_CONFIG_DATA = 390; // 113 Bytes (struct WiFiConfig) -> Ende ca. 503
// --- Sonstige Einstellungen ---
const int EEPROM_ADDR_STEAM_DELAY = 527; // 4 Bytes (int) -> Ende 530
// --- AutoTune Parameter Dampf --- (NEU)
const int EEPROM_ADDR_TUNING_STEP_DAMPF = 531; // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_NOISE_DAMPF = 539; // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_STARTVALUE_DAMPF = 547; // 8 Bytes (double)
const int EEPROM_ADDR_TUNING_LOOKBACK_DAMPF = 555; // 4 Bytes (unsigned int) -> Ende 558
// --- Adressen für Übertemperatursicherung / Sicherheitsabschaltung ---Adressen für Sicherheitslimits
const int EEPROM_ADDR_MAX_TEMP_WASSER = 559; // 8 Bytes (double) -> Ende 566
const int EEPROM_ADDR_MAX_TEMP_DAMPF = 567; // 8 Bytes (double) -> Ende 574
/************************************************************************************
* Eco-Mode Variablen
************************************************************************************/
unsigned long lastShotTime = 0; // Wann wurde zuletzt ein Shot beendet
int ecoModeMinutes = 0; // Zeit (Minuten) bis Eco-Modus
bool ecoModeAktiv = 0; // Flag, ob Eco läuft
int ecoModeTempWasser = 50; // Zieltemp Wasser im Eco
int ecoModeTempDampf = 50; // Zieltemp Dampf im Eco
bool dynamicEcoActive = false;
unsigned long ecoModeActivatedTime = 0;
/************************************************************************************
* Fast-Heat-Up Variablen
************************************************************************************/
bool fastHeatUpAktiv = 0; // Ist Fast-Heat-Up aktiviert?
bool fastHeatUpHeating = 0; // Wird gerade hochgeheizt (Wasser >130)?
int fastHeatUpSetpoint = 130;
/************************************************************************************
* Sicherheits- und Fehlerstatus
************************************************************************************/
bool wasserSensorError = false; // Flag für Fehler am Wassertemperatursensor
bool dampfSensorError = false; // Flag für Fehler am Dampftemperatursensor
/************************************************************************************
* Auto-Tune-Einstellungen
************************************************************************************/
// Parameter für Wasser
double tuningStepWasser;
double tuningNoiseWasser;
double tuningStartValueWasser;
unsigned int tuningLookBackWasser;
const double defaultTuningStepWasser = 25.0;
const double defaultTuningNoiseWasser = 1.0;
const double defaultTuningStartValueWasser = 95.0;
const unsigned int defaultTuningLookBackWasser = 60;
// Parameter für Dampf (NEU)
double tuningStepDampf;
double tuningNoiseDampf;
double tuningStartValueDampf;
unsigned int tuningLookBackDampf;
const double defaultTuningStepDampf = 15.0; // Höher wegen mehr Leistung
const double defaultTuningNoiseDampf = 1.0; // Thermoblock könnte "rauschen"
const double defaultTuningStartValueDampf = 95.0; // Höherer Startpunkt im Dampfbereich
const unsigned int defaultTuningLookBackDampf = 30; // Evtl. längere Beobachtung nötig
PID_ATune* autoTuneWasser;
PID_ATune* autoTuneDampf;
bool autoTuneWasserActive = false;
bool autoTuneDampfActive = false;
/************************************************************************************
* Shot-Timer Variablen
************************************************************************************/
unsigned long shotStartTime = 0;
unsigned long shotEndTime = 0;
bool shotActive = false;
bool delayDisplayUpdate = false;
/************************************************************************************
* Shot-Zähler und Betriebszeit
************************************************************************************/
unsigned long shotCounter = 0; // Shots > 20 Sekunden
unsigned long totalRuntime = 0; // Gesamt-Betriebszeit in Sekunden
unsigned long lastRuntimeSave = 0; // Wann zuletzt gespeichert
const unsigned long runtimeSaveInterval = 60000; // Speichern alle 60s
/************************************************************************************
* Verzögerungen für Anzeigen
************************************************************************************/
int delayInit1 = 1500; // Display-Bild am Anfang
int delayInit2 = 3000; // Warte für Anzeige
unsigned long previousMillis = 0; // für Intervall z.B. 250ms
const unsigned long interval = 250;
/************************************************************************************
* WiFi Signal Bitmap (Display-Anzeige)
************************************************************************************/
static const unsigned char PROGMEM wifiSymbol[] = {
0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48,
0x27, 0x20, 0x08, 0x80, 0x02, 0x00
};
/************************************************************************************
* Gemeinsame CSS-Styles (PROGMEM)
************************************************************************************/
static const char commonStyle[] PROGMEM = R"rawliteral(
<style>
/* Farben und Grundvariablen */
:root {
--primary-bg: #000000; /* Schwarz */
--secondary-bg: #1a1a1a; /* Dunkles Grau/Schwarz */
--accent-color: #d89904; /* Gold-/Kupferton */
--text-color: #ffffff; /* Weiß für Schrift */
--card-bg-opacity: 0.15; /* Opazität für Form-Karten-Hintergrund */
}
/* Seitenhintergrund: Farbverlauf */
body {
margin: 0;
padding: 0;
font-family: "Segoe UI", Tahoma, Arial, sans-serif;
/* Der Verlauf in Schwarz (#000000) zu etwas hellerem Schwarz (#1a1a1a) */
background: linear-gradient(120deg, #000000 0%, #1a1a1a 100%);
/* Keine Wiederholung, fester Hintergrund, ganzseitige Abdeckung: */
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
/* Damit der Verlauf die gesamte Höhe füllt: */
min-height: 100vh;
color: #ffffff;
}
/************************************************************************
* Links / Anker-Tags
* -> Alle klickbaren Links standardmäßig in der Akzentfarbe
************************************************************************/
a,
a:visited {
text-decoration: none;
transition: color 0.2s ease, background-color 0.2s ease;
}
a:hover,
a:focus {
text-decoration: none;
}
/************************************************************************
* Navigation (am oberen Seitenrand, semi-transparent)
************************************************************************/
nav {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
padding: 20px 0;
text-align: center;
position: sticky;
top: 0;
z-index: 999;
}
/* Navigationslinks */
nav a {
/* Standardzustand (nicht aktiv, nicht hover) -> weißer Text */
color: #fff;
font-weight: 500;
text-decoration: none;
margin: 0 10px;
padding: 10px 20px;
border-radius: 8px;
transition: background 0.3s ease, color 0.3s ease;
}
/* Hover-Effekt: Hintergrund in Akzentfarbe, Schrift schwarz */
nav a:hover {
background: var(--accent-color);
color: #000000;
}
/* Optional: Fokus (z. B. per Tastatur) gleich wie Hover */
nav a:focus {
background: var(--accent-color);
color: #000000;
}
/* Klick / active-State */
nav a:active {
background: var(--accent-color);
color: #000000;
}
/* Wenn du eine „active“-Klasse für die aktuell geladene Seite nutzt,
kannst du sie so einfärben: */
nav a.active {
background: var(--accent-color);
color: #000000;
}
/* Hamburger-Icon (für Mobile) */
.hamburger {
display: none;
font-size: 28px;
line-height: 1;
position: absolute;
top: 10px;
right: 20px;
color: var(--text-color);
cursor: pointer;
}
.hamburger:hover {
color: #ddd;
}
/************************************************************************
* Formulare (Card-Design, Glas-Effekt)
************************************************************************/
form {
background: rgba(255, 255, 255, var(--card-bg-opacity));
backdrop-filter: blur(8px);
border-radius: 12px;
padding: 20px;
margin: 20px auto;
width: 90%;
max-width: 600px; /* Hier kannst du es noch weiter einschränken, z.B. 500px */
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
color: var(--text-color);
}
/* Überschriften */
h1 {
text-align: center;
margin-top: 20px;
font-size: 2rem;
}
h3 {
margin-top: 0;
font-weight: 600;
padding-bottom: 5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
/* Labels */
label {
display: block;
margin: 0px 0 5px;
font-weight: 600;
}
/************************************************************************
* Eingabefelder kleiner machen
************************************************************************/
input[type="text"],
input[type="password"],
select,
textarea {
/* nicht mehr 100%, sondern ein fixes max-Width */
width: auto;
min-width: 200px;
max-width: 300px;
display: block;
margin-bottom: 15px;
background: rgba(0, 0, 0, 0.4);
color: var(--text-color);
border: none;
border-radius: 8px;
padding: 10px;
box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.2);
transition: background 0.2s ease;
}
/* Fokus-Effekt */
input[type="text"]:focus,
input[type="password"]:focus,
select:focus,
textarea:focus {
outline: none;
background: rgba(0, 0, 0, 0.6);
}
/************************************************************************
* Buttons ebenfalls schmaler
************************************************************************/
input[type="submit"],
button {
background: var(--accent-color);
color: #000000; /* Schwarz auf Gold */
border: none;
border-radius: 30px;
padding: 10px 20px;
margin-top: 12px;
/* statt großer Breite: feste Limits */
width: auto;
min-width: 100px;
max-width: 200px;
cursor: pointer;
font-weight: 600;
box-shadow: 0 4px 12px rgba(216, 153, 4, 0.3);
transition: all 0.3s ease;
}
input[type="submit"]:hover,
button:hover {
opacity: 0.85;
transform: translateY(-2px);
}
/************************************************************************
* Responsive Design
************************************************************************/
@media (max-width: 768px) {
/* Nav-Links einklappen/hamburger */
.hamburger {
display: block;
}
nav a {
display: none;
}
nav.active a {
display: block;
margin: 10px 0;
}
nav {
text-align: left;
}
}
@media (max-width: 480px) {
h1 {
font-size: 1.4rem;
}
/* Buttons auf 100% bei ganz kleinen Displays, wenn gewünscht */
input[type="submit"] {
width: 100%;
max-width: none;
}
}
</style>
<script>
function toggleMenu() {
var nav = document.querySelector('nav');
nav.classList.toggle('active');
}
</script>
)rawliteral";
/************************************************************************************
* Gemeinsame Navigation (PROGMEM)
************************************************************************************/
static const char commonNav[] PROGMEM = R"rawliteral(
<nav>
<span class="hamburger" onclick="toggleMenu()">&#9776;</span>
<a href="/">PID-Einstellung</a>
<a href="/PID-Tuning">PID-Tuning</a>
<a href="/profiles">Profile</a>
<a href="/charts">Temperaturverlauf</a>
<a href="/fast-heat-up">Fast-Heat-Up</a>
<a href="/eco">Eco</a>
<a href="/info">Info</a>
<a href="/netzwerk">WiFi</a>
<a href="/updateFirmware">Firmware</a>
</nav>
)rawliteral";
/************************************************************************************
* WiFi-Konfigurationsstrukturen und Funktionen
************************************************************************************/
struct WiFiConfig
{
char ssid[32] = "";
char password[64] = "";
bool useStaticIP = false;
IPAddress staticIP = IPAddress(192, 168, 4, 1);
IPAddress gateway = IPAddress(192, 168, 4, 1);
IPAddress subnet = IPAddress(255, 255, 255, 0);
IPAddress dns = IPAddress(8, 8, 8, 8);
};
// Liest die WLAN-Konfiguration aus dem EEPROM
bool loadWiFiConfig(WiFiConfig& config)
{
char magic[5] = { 0 };
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
// Falls MagicValue nicht übereinstimmt, Standardwerte setzen
if (strncmp(magic, storageMagicValue, 4) != 0)
{
strcpy(config.ssid, "");
strcpy(config.password, "");
config.useStaticIP = false;
config.staticIP = IPAddress(192, 168, 4, 1);
config.gateway = IPAddress(192, 168, 4, 1);
config.subnet = IPAddress(255, 255, 255, 0);
config.dns = IPAddress(8, 8, 8, 8);
return false;
}
// Laden der gespeicherten Daten
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
return true;
}
// Schreibt die WLAN-Konfiguration ins EEPROM
void saveWiFiConfig(const WiFiConfig& config)
{
EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, storageMagicValue);
EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
EEPROM.commit();
}
// Aktiviert den Access Point Modus und zeigt IP an
void startAPMode()
{
WiFi.softAP(F("Dual PID-Controller"));
Serial.print(F("AP-Mode IP: "));
Serial.println(WiFi.softAPIP());
}
// Webserver-Handler: WLAN-Konfigurations-Seite
void handleWiFiConfig()
{
static const char wifiConfigForm[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>WLAN-Konfiguration</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char wifiConfigForm2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char wifiConfigForm3[] PROGMEM = R"rawliteral(
<body>
<h1>WLAN-Konfiguration</h1>
<form action='/saveWiFiConfig' method='POST'>
<h3>WLAN-Einstellungen</h3>
<label for='ssid'>SSID:</label>
<input type='text' id='ssid' name='ssid' value='{SSID}' required>
<label for='password'>Passwort:</label>
<input type='text' id='password' name='password' value='{PASSWORD}'>
<h3>IP-Einstellungen</h3>
<label>
<input type='radio' name='ipType' value='dhcp' {DHCP_CHECKED}> DHCP
</label>
<label>
<input type='radio' name='ipType' value='static' {STATIC_CHECKED}> Statische IP
</label>
<div id='staticFields' style='display: {STATIC_DISPLAY}'>
<label for='ip'>IP-Adresse:</label>
<input type='text' id='ip' name='ip' value='{IP}'>
<label for='gateway'>Gateway:</label>
<input type='text' id='gateway' name='gateway' value='{GATEWAY}'>
<label for='subnet'>Subnetzmaske:</label>
<input type='text' id='subnet' name='subnet' value='{SUBNET}'>
</div>
<input type='submit' value='Speichern'>
</form>
<form action='/forceAPMode' method='POST' style='margin-top: 20px;'>
<h3>AP-Modus</h3>
Verwendung im Access Point-Modus (AP).<br>
Erreichbarkeit unter IP-Adresse: 192.168.4.1<br><br>
<input type='submit' value='AP-Modus verwenden'>
</form>
<script>
document.querySelectorAll('input[name="ipType"]').forEach(radio => {
radio.addEventListener('change', () => {
document.getElementById('staticFields').style.display =
radio.value === 'static' ? 'block' : 'none';
});
});
</script>
</body>
</html>
)rawliteral";
WiFiConfig currentConfig;
char magic[5] = { 0 };
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
bool configValid = (strncmp(magic, storageMagicValue, 4) == 0);
// Wenn configValid, lädt die gespeicherten Daten, sonst Standardwerte
if (configValid)
{
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, currentConfig);
}
else
{
strcpy(currentConfig.ssid, "");
strcpy(currentConfig.password, "");
currentConfig.useStaticIP = false;
currentConfig.staticIP = IPAddress(192, 168, 4, 1);
currentConfig.gateway = IPAddress(192, 168, 4, 1);
currentConfig.subnet = IPAddress(255, 255, 255, 0);
}
// HTML zusammenbauen
String html = FPSTR(wifiConfigForm);
html += FPSTR(commonStyle);
html += FPSTR(wifiConfigForm2);
html += FPSTR(commonNav);
html += FPSTR(wifiConfigForm3);
html.replace("{SSID}", currentConfig.ssid);
// Im Demo-Modus wird das Passwort durch Sternchen ersetzt
if(!demoModus)
{
html.replace("{PASSWORD}", currentConfig.password);
}
else
{
html.replace("{PASSWORD}", "*** *** *** ***");
}
html.replace("{IP}", currentConfig.staticIP.toString());
html.replace("{GATEWAY}", currentConfig.gateway.toString());
html.replace("{SUBNET}", currentConfig.subnet.toString());
html.replace("{DHCP_CHECKED}", currentConfig.useStaticIP ? "" : "checked");
html.replace("{STATIC_CHECKED}", currentConfig.useStaticIP ? "checked" : "");
html.replace("{STATIC_DISPLAY}", currentConfig.useStaticIP ? "block" : "none");
server.send(200, F("text/html"), html);
}
// Handler zum Speichern der WLAN-Konfiguration
void handleSaveWiFiConfig()
{
if (!server.hasArg(F("ssid")))
{
server.send(500, F("text/plain"), F("Fehler: SSID nicht gefunden"));
return;
}
WiFiConfig newConfig;
strncpy(newConfig.ssid, server.arg(F("ssid")).c_str(), sizeof(newConfig.ssid));
strncpy(newConfig.password, server.arg(F("password")).c_str(), sizeof(newConfig.password));
newConfig.useStaticIP = (server.arg(F("ipType")) == "static");
if (newConfig.useStaticIP)
{
newConfig.staticIP.fromString(server.arg(F("ip")));
newConfig.gateway.fromString(server.arg(F("gateway")));
newConfig.subnet.fromString(server.arg(F("subnet")));
}
// Im Demo-Modus wird nichts gespeichert, sonst schon
if(!demoModus) { saveWiFiConfig(newConfig); }
static const char saveConfigHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Einstellungen gespeichert</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char saveConfigHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char saveConfigHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Einstellungen gespeichert</h1>
<form><p>Die Einstellungen wurden gespeichert. Neustart ...</p></form>
</body>
</html>
)rawliteral";
String html = FPSTR(saveConfigHtml);
html += FPSTR(commonStyle);
html += FPSTR(saveConfigHtml2);
html += FPSTR(commonNav);
html += FPSTR(saveConfigHtml3);
server.send(200, F("text/html"), html);
delay(5000);
ESP.restart();
}
// Handler, um den AP-Modus per Knopfdruck zu erzwingen
void handleForceAPMode()
{
// Leer-Konfiguration speichern, damit beim nächsten Start AP aktiv wird
WiFiConfig emptyConfig;
if(!demoModus) { saveWiFiConfig(emptyConfig); }
static const char forceAPHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>AP-Modus aktivieren</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char forceAPHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char forceAPHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>AP-Modus wird aktiviert...</h1>
<form><p>Die Einstellungen wurden gespeichert. Neustart im AP-Modus ...</p></form>
</body>
</html>
)rawliteral";
String html = FPSTR(forceAPHtml);
html += FPSTR(commonStyle);
html += FPSTR(forceAPHtml2);
html += FPSTR(commonNav);
html += FPSTR(forceAPHtml3);
server.send(200, F("text/html"), html);
delay(1000);
ESP.restart();
}
// Prüfung der WiFi-Verbindung (Reconnect falls getrennt)
void checkWifiConnection()
{
if (WiFi.status() != WL_CONNECTED)
{
WiFi.reconnect();
if (WiFi.waitForConnectResult() != WL_CONNECTED)
{
Serial.println(F("WiFi-Verbindung kann nicht hergestellt werden!"));
}
}
}
// WiFi-Symbol aufs Display zeichnen
void drawwifiSymbol(int16_t x, int16_t y)
{
display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
}
/************************************************************************************
* Handler zum Rücksetzen der Betriebszeit
************************************************************************************/
void handleResetRuntime()
{
totalRuntime = 0;
EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime);
EEPROM.commit();
server.sendHeader(F("Location"), F("/info"));
server.send(303);
}
/************************************************************************************
* Handler zum Rücksetzen des Shotzählers
************************************************************************************/
void handleResetShots()
{
shotCounter = 0;
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
EEPROM.commit();
server.sendHeader(F("Location"), F("/info"));
server.send(303);
}
/************************************************************************************
* Variable für Systemstartzeit (relevant für die Dampf-Verzögerung)
************************************************************************************/
unsigned long startupTime;
/************************************************************************************
* setup(): Initialisierung des Systems
************************************************************************************/
void setup()
{
Serial.begin(115200);
EEPROM.begin(1024);
pinMode(SSR_DAMPF_PIN, OUTPUT);
pinMode(SSR_WASSER_PIN, OUTPUT);
pinMode(SHOT_TIMER_PIN, INPUT_PULLUP);
// Prüfen, ob MagicValue bereits im EEPROM liegt
char storedMagicValue[5] = { 0 };
bool magicValueVorhanden = false;
EEPROM.get(EEPROM_ADDR_MAGICVALUE, storedMagicValue);
if (strncmp(storedMagicValue, storageMagicValue, 4) == 0)
{
magicValueVorhanden = true;
}
else
{
magicValueVorhanden = false;
EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue);
EEPROM.commit();
}
// Werte aus dem EEPROM laden, wenn vorhanden, sonst mit Default-Werten initialisieren
if (magicValueVorhanden)
{
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.get(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
EEPROM.get(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
EEPROM.get(EEPROM_ADDR_KP_DAMPF, KpDampf);
EEPROM.get(EEPROM_ADDR_KI_DAMPF, KiDampf);
EEPROM.get(EEPROM_ADDR_KD_DAMPF, KdDampf);
EEPROM.get(EEPROM_ADDR_KP_WASSER, KpWasser);
EEPROM.get(EEPROM_ADDR_KI_WASSER, KiWasser);
EEPROM.get(EEPROM_ADDR_KD_WASSER, KdWasser);
EEPROM.get(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
EEPROM.get(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
EEPROM.get(EEPROM_ADDR_INFO_MODELL, infoModell);
EEPROM.get(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
EEPROM.get(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
EEPROM.get(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
EEPROM.get(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
// --- Bestehende Tuning-Parameter laden (jetzt Wasser) ---
EEPROM.get(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser); // Name geändert
EEPROM.get(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser); // Name geändert
EEPROM.get(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser); // Name geändert
EEPROM.get(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser); // Name geändert
// --- NEU: Dampf-Tuning-Parameter laden ---
EEPROM.get(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf);
EEPROM.get(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf);
EEPROM.get(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf);
EEPROM.get(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf);
// NEU: Sicherheitslimits laden
EEPROM.get(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser);
EEPROM.get(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf);
}
else
{
SetpointDampf = defaultSetpointDampf;
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
SetpointWasser = defaultSetpointWasser;
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
OffsetDampf = defaultOffsetDampf;
EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
OffsetWasser = defaultOffsetWasser;
EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
KpDampf = defaultKpDampf;
EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
KiDampf = defaultKiDampf;
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
KdDampf = defaultKdDampf;
EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
KpWasser = defaultKpWasser;
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
KiWasser = defaultKiWasser;
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
KdWasser = defaultKdWasser;
EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
ecoModeMinutes = defaultEcoModeMinutes;
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
ecoModeTempWasser = defaultEcoModeTempWasser;
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
ecoModeTempDampf = defaultEcoModeTempDampf;
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
strcpy(infoHersteller, defaultInfoHersteller.c_str());
EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
strcpy(infoModell, defaultInfoModell.c_str());
EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell);
strcpy(infoZusatz, defaultInfoZusatz.c_str());
EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
dynamicEcoActive = false;
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
fastHeatUpAktiv = false;
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
dampfVerzoegerung = defaultDampfVerzoegerung;
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
TemperatureProfile defaultProfile;
defaultProfile.setpointWasser = defaultSetpointWasser;
defaultProfile.setpointDampf = defaultSetpointDampf;
defaultProfile.ecoModeMinutes = defaultEcoModeMinutes;
defaultProfile.dynamicEcoActive = false;
defaultProfile.dampfVerzoegerung = defaultDampfVerzoegerung;
strncpy(defaultProfile.profileName, "Unbenannt", sizeof(defaultProfile.profileName));
defaultProfile.profileName[sizeof(defaultProfile.profileName) - 1] = '\0';
EEPROM.put(EEPROM_ADDR_PROFILE_1, defaultProfile);
EEPROM.put(EEPROM_ADDR_PROFILE_2, defaultProfile);
// --- Bestehende Tuning-Parameter als Wasser-Defaults speichern ---
tuningStepWasser = defaultTuningStepWasser; // Name geändert
tuningNoiseWasser = defaultTuningNoiseWasser; // Name geändert
tuningStartValueWasser = defaultTuningStartValueWasser; // Name geändert
tuningLookBackWasser = defaultTuningLookBackWasser; // Name geändert
EEPROM.put(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser);
EEPROM.put(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser);
EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser);
EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser);
// --- NEU: Dampf-Tuning-Parameter als Defaults speichern ---
tuningStepDampf = defaultTuningStepDampf;
tuningNoiseDampf = defaultTuningNoiseDampf;
tuningStartValueDampf = defaultTuningStartValueDampf;
tuningLookBackDampf = defaultTuningLookBackDampf;
EEPROM.put(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf);
EEPROM.put(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf);
EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf);
EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf);
// NEU: Default-Sicherheitslimits speichern (nutzt die initialisierten globalen Variablen)
EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser);
EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf);
EEPROM.commit();
}
// WiFi-Einstellungen laden und ggf. herstellen
WiFiConfig wifiConfig;
bool hasWiFiConfig = loadWiFiConfig(wifiConfig);
if (hasWiFiConfig)
{
WiFi.mode(WIFI_STA);
if (wifiConfig.useStaticIP)
{
WiFi.config(wifiConfig.staticIP, wifiConfig.gateway, wifiConfig.subnet);
}
WiFi.begin(wifiConfig.ssid, wifiConfig.password);
int retries = 0;
while (WiFi.status() != WL_CONNECTED && retries < 20)
{
delay(500);
Serial.print(F("."));
retries++;
}
if (WiFi.status() == WL_CONNECTED)
{
Serial.println(F("\nVerbunden!"));
}
else
{
Serial.println(F("\nKonnte keine Verbindung herstellen!"));
startAPMode();
}
}
else
{
startAPMode();
}
// Betriebszeit und Shotcounter aus EEPROM laden
EEPROM.get(EEPROM_ADDR_RUNTIME, totalRuntime);
if (totalRuntime == ULONG_MAX) { totalRuntime = 0; }
EEPROM.get(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
if (shotCounter == ULONG_MAX)
{
shotCounter = 0;
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
}
EEPROM.commit();
if (fastHeatUpAktiv)
{
fastHeatUpHeating = true;
}
// OLED-Initialisierung
Wire.begin(OLED_SDA, OLED_SCK);
if (!display.begin(0x3C, true))
{
Serial.println(F("Display konnte nicht initialisiert werden!"));
}
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-Controller"));
display.print(F("Version: "));
display.println(version);
display.println(F("von Thomas M\201ller"));
display.display();
delay(delayInit1);
// PID-Regler aktivieren und Parameter setzen
pidDampf.SetMode(AUTOMATIC);
pidWasser.SetMode(AUTOMATIC);
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
// Anzeige zur WiFi-Verbindung
if (WiFi.status() != WL_CONNECTED)
{
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("WiFi-Verbindung:"));
display.println(F("AP-Modus gestartet"));
display.println(WiFi.softAPIP());
}
else
{
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("Webserver gestartet."));
display.println(F("IP-Adresse:"));
display.println(WiFi.localIP());
}
display.display();
delay(delayInit2);
// Routen für Webserver
server.on("/", handleRoot);
server.on("/info", handleInfo);
server.on("/eco", handleEco);
server.on("/fast-heat-up", handleFastHeatUp);
server.on("/updateFast-Heat-Up-Settings", handleFastHeatUpSettings);
server.on("/updateSettings", handleUpdate);
server.on("/updateInfoSettings", handleInfoUpdate);
server.on("/updateEcoSettings", handleEcoUpdate);
// AutoTune-Route (Wasser)
server.on("/autoTuneWasser", []() {
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>AutoTune Wasser</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>\n");
page += FPSTR(commonStyle);
page += F("</head>\n");
page += FPSTR(commonNav);
page += F("<body>\n<h1>PID-Tuning</h1>\n");
if (!autoTuneWasserActive && !autoTuneDampfActive)
{
startAutoTuneWasser();
page += F("<form action='/abbruch-pid-tuning' method='POST' >\n"
"<p>AutoTune gestartet: Wasser-PID</p>\n"
"<br>\n"
"<input type='submit' value='PID-Tuning abbrechen'>\n"
"</form>\n");
}
else
{
page += F("<form action='/abbruch-pid-tuning' method='POST' >\n"
"<p style=\"color: red;\">AutoTune ist bereits aktiv!</p>\n"
"<br>\n"
"<input type='submit' value='PID-Tuning abbrechen'>\n"
"</form>\n");
}
page += F("</body>\n</html>");
server.send(200, F("text/html"), page);
});
// AutoTune-Route (Dampf)
server.on("/autoTuneDampf", []() {
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>AutoTune Dampf</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>\n");
page += FPSTR(commonStyle);
page += F("</head>\n");
page += FPSTR(commonNav);
page += F("<body>\n<h1>PID-Tuning</h1>\n");
if (!autoTuneWasserActive && !autoTuneDampfActive)
{
startAutoTuneDampf();
page += F("<form action='/abbruch-pid-tuning' method='POST' >\n"
"<p>AutoTune gestartet: Dampf-PID</p>\n"
"<br>\n"
"<input type='submit' value='PID-Tuning abbrechen'>\n"
"</form>\n");
}
else
{
page += F("<form action='/abbruch-pid-tuning' method='POST' >\n"
"<p style=\"color: red;\">AutoTune ist bereits aktiv!</p>\n"
"<br>\n"
"<input type='submit' value='PID-Tuning abbrechen'>\n"
"</form>\n");
}
page += F("</body>\n</html>");
server.send(200, F("text/html"), page);
});
// Hauptseite PID-Tuning (Info, Starten etc.)
server.on("/PID-Tuning", HTTP_GET, []() {
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>PID-Tuning</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>\n");
page += FPSTR(commonStyle);
page += F("</head>\n");
page += FPSTR(commonNav);
page += F("<body>\n");
if (autoTuneWasserActive || autoTuneDampfActive)
{
page += F("<form action='/abbruch-pid-tuning' method='POST' >\n"
"<h3>Laufendes PID-Tuning abbrechen?</h3>\n"
"<input type='submit' value='Abbrechen'>\n"
"</form></br>\n");
}
page += F("<h1>PID-Tuning</h1>\n"
"<form>Das automatische PID-Tuning dient dazu, die optimalen Parameter f&uuml;r den PID-Regler (Proportional-, Integral- und Differentialanteil) selbst&auml;ndig zu ermitteln. Dabei analysiert das System mithilfe von Algorithmen das Regelverhalten (z. B. Reaktion auf einen Testimpuls) und passt Kp, Ki und Kd so an, dass gew&uuml;nschte Kriterien wie kurze Einschwingzeit, geringe &Uuml;berschwingung und stabile Regelung erreicht werden"
"<br><br>"
"Ein AutoTune-Vorgang kann durchaus 10-20 Minuten in Anspruch nehmen!"
"<br><br>"
"<b>WICHTIG!</b><br>"
"Vor dem Start des PID-Tunings sollte die Maschine die gew&uuml;nschte Betriebstemperatur erreicht haben und stabil sein.<br>"
"Andernfalls k&ouml;nnten die automatisch ermittelten PID-Werte f&uuml;r diesen Temperaturbereich ungenau oder nicht optimal sein."
"<br><br>"
"</form>"
"<form action='/autoTuneWasser' method='POST' >\n"
"<h3>Automatisches PID-Tuning f&uuml;r Wasser</h3>\n"
"<input type='submit' value='Tuning starten'>\n"
"</form>\n"
"<form action='/autoTuneDampf' method='POST' >\n"
"<h3>Automatisches PID-Tuning f&uuml;r Dampf</h3>\n"
"<input type='submit' value='Tuning starten'>\n"
"</form>\n");
// Form für AutoTune-Parameter
page += F("<form action='/updateAutotuneSettings' method='POST'>\n"
"<h3>AutoTune-Parameter</h3>\n"
"Hier k&ouml;nnen die AutoTune-Parameter ge&auml;ndert werden.<br>&Auml;nderungen sollten mit Vorsicht durchgef&uuml;hrt werden!<br><br>"
// --- HIER BEGINNEN DIE ÄNDERUNGEN ---
"<h3>Parameter Wasser / Kessel</h3>" // Überschrift für Wasser
"<label for='tuningStepWasser'>Step:</label>"
"<input type='text' id='tuningStepWasser' name='tuningStepWasser' value='{TUNING_STEP_WASSER}'>" // ID, Name, Placeholder angepasst
"<label for='tuningNoiseWasser'>Noise:</label>"
"<input type='text' id='tuningNoiseWasser' name='tuningNoiseWasser' value='{TUNING_NOISE_WASSER}'>" // ID, Name, Placeholder angepasst
"<label for='tuningStartValueWasser'>StartValue:</label>"
"<input type='text' id='tuningStartValueWasser' name='tuningStartValueWasser' value='{TUNING_STARTVALUE_WASSER}'>" // ID, Name, Placeholder angepasst
"<label for='tuningLookBackWasser'>LookBack (Sekunden):</label>"
"<input type='text' id='tuningLookBackWasser' name='tuningLookBackWasser' value='{TUNING_LOOKBACK_WASSER}'>" // ID, Name, Placeholder angepasst
"<br><h3>Parameter Dampf / Thermoblock</h3>" // Überschrift für Dampf
"<label for='tuningStepDampf'>Step:</label>"
"<input type='text' id='tuningStepDampf' name='tuningStepDampf' value='{TUNING_STEP_DAMPF}'>" // NEUE Felder
"<label for='tuningNoiseDampf'>Noise:</label>"
"<input type='text' id='tuningNoiseDampf' name='tuningNoiseDampf' value='{TUNING_NOISE_DAMPF}'>" // NEUE Felder
"<label for='tuningStartValueDampf'>StartValue:</label>"
"<input type='text' id='tuningStartValueDampf' name='tuningStartValueDampf' value='{TUNING_STARTVALUE_DAMPF}'>" // NEUE Felder
"<label for='tuningLookBackDampf'>LookBack (Sekunden):</label>"
"<input type='text' id='tuningLookBackDampf' name='tuningLookBackDampf' value='{TUNING_LOOKBACK_DAMPF}'>" // NEUE Felder
// --- HIER ENDEN DIE ÄNDERUNGEN ---
"<input type='submit' value='Parameter speichern'>\n"
"</form>\n"
"<br><hr><br>"
"<form>"
"<h3>Erkl&auml;rung der Parameter:</h3>\n"
"<p><strong>Step:</strong><br>\n"
"Dieser Wert bestimmt, wie stark der Ausgangswert w&auml;hrend des AutoTune-Vorgangs ver&auml;ndert wird. Mit anderen Worten, er gibt die Gr&ouml;&szlig;e des Impulses an, der in das System eingespeist wird, um Oszillationen zu erzeugen. Ein zu hoher Wert kann zu &uuml;berma&szlig;igen Ausschl&auml;gen f&uuml;hren, w&auml;hrend ein zu niedriger Wert m&ouml;glicherweise nicht genug Dynamik erzeugt.</p>\n"
"<p><strong>Noise:</strong><br>\n"
"Dieser Parameter definiert einen Toleranzbereich (Noise-Band) f&uuml;r die gemessenen Eingangswerte. Kleine Schwankungen, die unterhalb dieses Wertes liegen, werden als Rauschen betrachtet und ignoriert. Das hilft, die Auswirkungen von Messrauschen zu minimieren und sicherzustellen, dass nur signifikante &Auml;nderungen in der Systemantwort zur Bestimmung der PID-Parameter herangezogen werden.</p>\n"
"<p><strong>StartValue:</strong><br>\n"
"Hier wird der Anfangswert f&uuml;r den Ausgang des Reglers festgelegt, wenn der AutoTune-Vorgang startet. Dieser Wert sollte idealerweise im linear arbeitenden Bereich des Systems liegen, da von diesem Ausgangspunkt aus der tuningStep angewendet wird. Er beeinflusst also, in welchem Bereich die anschlie&szlig;enden Impulse liegen.</p>\n"
"<p><strong>LookBack:</strong><br>\n"
"Dieser Parameter legt fest, &uuml;ber welchen Zeitraum (in Sekunden) der Algorithmus die vergangene Systemantwort analysiert. Er bestimmt quasi das &bdquo;Fenster&ldquo;, &uuml;ber das die Schwingungsperiode und damit die Dynamik des Systems gemessen wird. Ein zu kurzes Fenster k&ouml;nnte zu einer ungenauen Erfassung der Oszillation f&uuml;hren, w&auml;hrend ein zu langes Fenster unn&ouml;tig viel Zeit ben&ouml;tigt.</p>\n"
"</form>"
"</body>\n</html>");
// --- Placeholders ersetzen ---
page.replace("{TUNING_STEP_WASSER}", String(tuningStepWasser, 2)); // Angepasst
page.replace("{TUNING_NOISE_WASSER}", String(tuningNoiseWasser, 2)); // Angepasst
page.replace("{TUNING_STARTVALUE_WASSER}", String(tuningStartValueWasser, 2)); // Angepasst
page.replace("{TUNING_LOOKBACK_WASSER}", String(tuningLookBackWasser)); // Angepasst
page.replace("{TUNING_STEP_DAMPF}", String(tuningStepDampf, 2)); // NEU
page.replace("{TUNING_NOISE_DAMPF}", String(tuningNoiseDampf, 2)); // NEU
page.replace("{TUNING_STARTVALUE_DAMPF}", String(tuningStartValueDampf, 2)); // NEU
page.replace("{TUNING_LOOKBACK_DAMPF}", String(tuningLookBackDampf)); // NEU
server.send(200, F("text/html"), page);
});
// Speichern der AutoTune-Einstellungen
server.on("/updateAutotuneSettings", HTTP_POST, []() {
bool changed = false; // Flag, um zu prüfen, ob EEPROM.commit() nötig ist
// --- Wasser Parameter ---
if (server.hasArg(F("tuningStepWasser")))
{
tuningStepWasser = server.arg(F("tuningStepWasser")).toFloat();
EEPROM.put(EEPROM_ADDR_TUNING_STEP_WASSER, tuningStepWasser);
changed = true;
}
if (server.hasArg(F("tuningNoiseWasser")))
{
tuningNoiseWasser = server.arg(F("tuningNoiseWasser")).toFloat();
EEPROM.put(EEPROM_ADDR_TUNING_NOISE_WASSER, tuningNoiseWasser);
changed = true;
}
if (server.hasArg(F("tuningStartValueWasser")))
{
tuningStartValueWasser = server.arg(F("tuningStartValueWasser")).toFloat();
EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_WASSER, tuningStartValueWasser);
changed = true;
}
if (server.hasArg(F("tuningLookBackWasser")))
{
tuningLookBackWasser = server.arg(F("tuningLookBackWasser")).toInt();
EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_WASSER, tuningLookBackWasser);
changed = true;
}
// --- Dampf Parameter (NEU) ---
if (server.hasArg(F("tuningStepDampf")))
{
tuningStepDampf = server.arg(F("tuningStepDampf")).toFloat();
EEPROM.put(EEPROM_ADDR_TUNING_STEP_DAMPF, tuningStepDampf);
changed = true;
}
if (server.hasArg(F("tuningNoiseDampf")))
{
tuningNoiseDampf = server.arg(F("tuningNoiseDampf")).toFloat();
EEPROM.put(EEPROM_ADDR_TUNING_NOISE_DAMPF, tuningNoiseDampf);
changed = true;
}
if (server.hasArg(F("tuningStartValueDampf")))
{
tuningStartValueDampf = server.arg(F("tuningStartValueDampf")).toFloat();
EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE_DAMPF, tuningStartValueDampf);
changed = true;
}
if (server.hasArg(F("tuningLookBackDampf")))
{
tuningLookBackDampf = server.arg(F("tuningLookBackDampf")).toInt();
EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK_DAMPF, tuningLookBackDampf);
changed = true;
}
// Nur speichern, wenn sich etwas geändert hat
if (changed) {
EEPROM.commit();
}
server.sendHeader(F("Location"), F("/PID-Tuning"));
server.send(303);
});
// Chart-Anzeige-Handler
server.on("/charts", handleCharts);
// JSON-Daten für Temperaturwerte
server.on("/data", []() {
String json = F("{");
json += F("\"wasser\":") + String(InputWasser) + F(",");
json += F("\"setpointwasser\":") + String(SetpointWasser) + F(",");
json += F("\"dampf\":") + String(InputDampf) + F(",");
json += F("\"setpointdampf\":") + String(SetpointDampf);
json += F("}");
server.send(200, F("application/json"), json);
});
// Firmware-Update-Seite
server.on("/updateFirmware", HTTP_GET, []() {
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>Firmware-Update</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>\n");
page += FPSTR(commonStyle);
page += F("</head>\n");
page += FPSTR(commonNav);
page += F("<body>\n"
"<h1>Firmware</h1>\n"
"<form>\n"
"<h3>Firmware-Info</h3>\n"
"Firmware-Version: {FIRMWAREVERSION}<br>\n"
"Hersteller: {SWHERSTELLER}<br>\n"
"E-Mail: {SWHERSTELLERMAIL}<br>\n"
"Website: {SWHERSTELLERWEBSITE}<br>\n"
"</form>\n"
"<form method='POST' action='/update' enctype='multipart/form-data'>\n"
"<h3>Firmware-Update</h3>\n"
"Das Firmware-Update kann durch den Upload einer .bin-Datei durchgef&uuml;hrt werden.<br>\n"
"Nach dem Update wird ein automatischer Neustart durchgef&uuml;hrt.<br>\n"
"Sollte der Neustart nicht erfolgen, so kann dieser auch durch kurzzeitiges Trennen der Stromversorgung erfolgen.<br>\n"
"<br>\n"
"<input type='file' name='firmware'>\n"
"<br>"
"<button>Update starten</button>\n"
"</form>\n"
"</body>\n</html>");
page.replace(F("{FIRMWAREVERSION}"), version);
page.replace(F("{SWHERSTELLER}"), versionHersteller);
page.replace(F("{SWHERSTELLERMAIL}"), versionHerstellerMail);
page.replace(F("{SWHERSTELLERWEBSITE}"), versionHerstellerWeb);
server.send(200, F("text/html"), page);
});
// Firmware-Upload-Handler
server.on("/update", HTTP_POST, []() {
static const char updateDone[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Firmware-Update</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char updateDone2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char updateDone3[] PROGMEM = R"rawliteral(
<head>
<meta http-equiv="refresh" content="30;url=/" />
<script>
setTimeout(function() {
window.location.href = "/";
}, 30000);
</script>
<body>
<h1>{status_message}</h1>
<form><p>Sie werden in 30 Sekunden zur Startseite weitergeleitet...</p></form>
</body>
</html>
)rawliteral";
String htmlResponse = FPSTR(updateDone);
htmlResponse += FPSTR(commonStyle);
htmlResponse += FPSTR(updateDone2);
htmlResponse += FPSTR(commonNav);
htmlResponse += FPSTR(updateDone3);
String statusMessage;
if (!passwordFound)
{
statusMessage = F("Update abgebrochen! Die Firmware konnte nicht validiert werden.");
}
else
{
if (Update.hasError())
{
statusMessage = F("Update fehlgeschlagen!");
}
else
{
statusMessage = F("Update erfolgreich!");
}
}
htmlResponse.replace(F("{status_message}"), statusMessage);
server.send(200, F("text/html"), htmlResponse);
delay(1000);
ESP.restart();
},
[]() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START)
{
Serial.printf("Update gestartet: %s\n", upload.filename.c_str());
passwordFound = false;
passMatchPos = 0;
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000))
{
Update.printError(Serial);
}
}
else if (upload.status == UPLOAD_FILE_WRITE)
{
// Prüfen auf Firmware-Passwort
for (size_t i = 0; i < upload.currentSize; i++)
{
char c = (char)upload.buf[i];
if (c == FIRMWARE_PASSWORD[passMatchPos])
{
passMatchPos++;
if (passMatchPos == passLength)
{
passwordFound = true;
}
}
else
{
if (c == FIRMWARE_PASSWORD[0])
{
passMatchPos = 1;
}
else
{
passMatchPos = 0;
}
}
}
// Schreibvorgang
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize)
{
Update.printError(Serial);
}
}
else if (upload.status == UPLOAD_FILE_END)
{
if (!passwordFound)
{
Serial.println("Kennwort nicht gefunden -> Abbruch!");
Update.end(false);
}
else
{
if (Update.end(true))
{
Serial.printf("Update erfolgreich: %u Bytes\n", upload.totalSize);
}
else
{
Update.printError(Serial);
}
}
}
else if (upload.status == UPLOAD_FILE_ABORTED)
{
Update.end();
Serial.println(F("Update abgebrochen."));
}
yield();
});
// Handler zum Abbrechen des PID-AutoTune
server.on("/abbruch-pid-tuning", HTTP_POST, []() {
static const char abortTuning[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Info</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char abortTuning2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char abortTuning3[] PROGMEM = R"rawliteral(
<body>
<h1>PID-Tuning Abbruch</h1>
<form>
<h3>Das PID-Tuning wurde erfolgreich abgebrochen.</h3>
<p>Der Normalbetrieb wird nun fortgesetzt.</p>
</form>
</body>
</html>
)rawliteral";
String html = FPSTR(abortTuning);
html += FPSTR(commonStyle);
html += FPSTR(abortTuning2);
html += FPSTR(commonNav);
html += FPSTR(abortTuning3);
server.send(200, F("text/html"), html);
stopAutoTuneWasser();
stopAutoTuneDampf();
});
// Routen für WLAN-Einstellungen
server.on("/netzwerk", handleWiFiConfig);
server.on("/saveWiFiConfig", handleSaveWiFiConfig);
server.on("/forceAPMode", handleForceAPMode);
// Routen zum Zurücksetzen von Betriebszeit und ShotCounter
server.on("/resetRuntime", handleResetRuntime);
server.on("/resetShots", handleResetShots);
// Routen für Profile
server.on("/profiles", handleProfiles);
server.on("/saveProfile1", handleSaveProfile1);
server.on("/loadProfile1", handleLoadProfile1);
server.on("/saveProfile2", handleSaveProfile2);
server.on("/loadProfile2", handleLoadProfile2);
// Webserver starten
server.begin();
Serial.println(F("Webserver gestartet"));
startupTime = millis();
}
/************************************************************************************
* loop(): Hauptschleife
************************************************************************************/
void loop()
{
unsigned long currentMillis = millis();
// Intervall für Messung/SSR-Schaltung
if (currentMillis - previousMillis >= interval)
{
previousMillis = currentMillis;
// Temperaturmessung
InputDampf = round(thermocouple.readCelsius() + OffsetDampf);
InputWasser = round(readNTCTemperature() + OffsetWasser);
// --- Sicherheitsüberprüfung Wasser ---
// Prüft, ob der Messwert im gültigen Bereich (0°C - Sicherheitsabschaltungs-Wert) liegt.
// Beinhaltet auch die Prüfung auf den Fehlerwert -273.15 aus readNTCTemperature.
if (InputWasser < 0.0 || InputWasser > maxTempWasser)
{
wasserSensorError = true; // Fehlerflag setzen
digitalWrite(SSR_WASSER_PIN, LOW); // Heizung SOFORT ausschalten
OutputWasser = 0; // PID-Output zur Sicherheit auf 0 setzen
Serial.println("WARNUNG: Wassertemperatur außerhalb des gültigen Bereichs <0°C oder >maxTempWasser!");
}
else
{
wasserSensorError = false; // Kein Fehler, Flag zurücksetzen
// PID nur berechnen und SSR schalten, wenn kein Fehler und kein AutoTune aktiv
if (!autoTuneWasserActive)
{
pidWasser.Compute();
digitalWrite(SSR_WASSER_PIN, (int)OutputWasser);
}
// AutoTune hat seine eigene Logik zum Setzen von OutputWasser, SSR wird dort indirekt gesteuert
}
// --- Sicherheitsüberprüfung Dampf ---
// Prüft, ob der Messwert gültig ist (kein NaN) und im Bereich >0°C und <maxTempDampf liegt.
// isnan() benötigt ggf. #include <math.h> (oft schon in Arduino.h enthalten)
if (isnan(InputDampf) || InputDampf < 0.0 || InputDampf > maxTempDampf)
{
dampfSensorError = true; // Fehlerflag setzen
digitalWrite(SSR_DAMPF_PIN, LOW); // Heizung SOFORT ausschalten
OutputDampf = 0; // PID-Output zur Sicherheit auf 0 setzen
Serial.println("WARNUNG: Dampftemperatur ungültig oder außerhalb des Bereichs <0°C oder >maxTempDampf!");
}
else
{
dampfSensorError = false; // Kein Fehler, Flag zurücksetzen
// PID nur berechnen und SSR schalten, wenn kein Fehler, kein AutoTune und keine Startverzögerung aktiv
if (!autoTuneDampfActive)
{
// Dampf-Startverzögerung prüfen (innerhalb der 'kein Fehler'-Logik)
if (dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL))
{
// Während der Verzögerung: Heizen verhindern, auch wenn Sensor OK ist
// SetpointDampf = 0; // Setpoint temporär ändern ist nicht ideal, besser direkt SSR steuern
digitalWrite(SSR_DAMPF_PIN, LOW); // Heizung aus während Verzögerung
OutputDampf = 0; // Sicherstellen, dass PID Output 0 ist
}
else
{
// Normalbetrieb nach Verzögerung und ohne Sensorfehler
pidDampf.Compute();
digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf);
}
}
// AutoTune hat seine eigene Logik zum Setzen von OutputDampf
}
// AutoTune-Logik (unverändert - beachte: AutoTune stoppt nicht automatisch bei Sensorfehler!)
handleAutoTune();
// Die SSR-digitalWrites sind jetzt innerhalb der Sicherheits-if/else-Blöcke
}
// Shot-Timer verarbeiten (unverändert)
updateShotTimer();
// Eco-Modus (unverändert - Sicherheitschecks haben Vorrang vor Eco-Sollwerten)
if (!autoTuneWasserActive && !autoTuneDampfActive)
{
if (ecoModeMinutes > 0 && !shotActive)
{
unsigned long currentTimeEco = millis();
if (currentTimeEco - lastShotTime > (ecoModeMinutes * 60UL * 1000UL))
{
if (!ecoModeAktiv) { ecoModeActivatedTime = currentTimeEco; }
ecoModeAktiv = true;
if (dynamicEcoActive)
{
int minutesPassed = (currentTimeEco - ecoModeActivatedTime) / (60UL * 1000UL);
SetpointWasser = (ecoModeTempWasser - minutesPassed > 0 ? ecoModeTempWasser - minutesPassed : 0);
SetpointDampf = (ecoModeTempDampf - minutesPassed > 0 ? ecoModeTempDampf - minutesPassed : 0);
}
else
{
SetpointWasser = ecoModeTempWasser;
SetpointDampf = ecoModeTempDampf;
}
}
else
{
// Wenn Eco-Zeit noch nicht erreicht ODER Shot aktiv war, normale Sollwerte laden
// Nur laden, wenn Eco gerade deaktiviert wird, um ständiges EEPROM-Lesen zu vermeiden
if (ecoModeAktiv) {
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
}
ecoModeAktiv = false;
ecoModeActivatedTime = 0;
}
} else if (ecoModeAktiv) {
// Sicherstellen, dass Eco deaktiviert wird, wenn ecoModeMinutes=0 gesetzt wird o.ä.
ecoModeAktiv = false;
ecoModeActivatedTime = 0;
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
}
}
// Display regelmäßig updaten (unverändert im Aufruf)
updateDisplay();
server.handleClient(); // Webserver (unverändert)
// Betriebszeit zählen (unverändert)
static unsigned long lastRuntimeSecond = 0;
if (millis() - lastRuntimeSecond >= 1000)
{
totalRuntime++;
lastRuntimeSecond = millis();
}
// Speichern und WiFi-Check (unverändert)
if (millis() - lastRuntimeSave >= runtimeSaveInterval) // 60000 ms = 1 Minute
{
EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime);
// Optional: Nur committen, wenn sich Wert geändert hat (spart Schreibzyklen)
// EEPROM.commit(); // Commit kann auch seltener erfolgen, wenn nötig
lastRuntimeSave = millis();
// Prüfe WiFi Verbindung nur wenn nicht im AP Modus
if (WiFi.getMode() == WIFI_STA) {
checkWifiConnection();
}
// Commit hier, da ShotCounter etc. auch gespeichert werden könnten
EEPROM.commit();
}
}
/************************************************************************************
* Liest die Temperatur vom NTC-Sensor (Wasser) über den Spannungsteiler (ADC)
************************************************************************************/
double readNTCTemperature()
{
int adcValue = analogRead(A0);
if (adcValue <= 0)
{
return -273.15; // Fehler- fallback
}
double sensorVoltage = adcValue * (ADC_REF / 1023.0);
double R_ntc = R_FIXED * ((V_SUPPLY / sensorVoltage) - 1.0);
// Temperaturberechnung über die Beta-Gleichung
double temperatureK = 1.0 / ((1.0 / T0) + (1.0 / beta) * log(R_ntc / R0));
return temperatureK - 273.15;
}
/************************************************************************************
* Aktualisiert das Display (Temperaturanzeige, Shot-Timer etc.)
************************************************************************************/
void updateDisplay()
{
// Wenn PID-Tuning läuft, wird das Display woanders beschrieben.
if (!autoTuneWasserActive && !autoTuneDampfActive)
{
// Warte 2 Sekunden nach Shot-Ende, bevor die Anzeige zurückgeht
if (delayDisplayUpdate && (millis() - shotEndTime < 2000))
{
return;
}
else
{
delayDisplayUpdate = false;
}
display.clearDisplay();
if (WiFi.status() == WL_CONNECTED)
{
drawwifiSymbol(113, 0); // Position anpassen, falls nötig
}
display.setCursor(0, 0);
display.setTextColor(SH110X_WHITE);
display.setTextSize(1);
// Fast-Heat-Up Anzeige
// Zeige F-H-U nur an, wenn *kein* Sensorfehler vorliegt
if (fastHeatUpHeating && !shotActive && !wasserSensorError && ((int)InputWasser < fastHeatUpSetpoint))
{
SetpointWasser = fastHeatUpSetpoint; // Setpoint setzen (Sicherheit prüft Ist-Wert!)
display.println(F("Fast-Heat-Up aktiv:"));
display.println("");
display.println(F("20 Sek. Flushen,"));
display.println(F("wenn Temp. erreicht!"));
display.println(F(" "));
display.println(F("Wasser: "));
display.print((int)InputWasser);
display.print(F(" / "));
display.print((int)SetpointWasser); // Zeige Zieltemp für F-H-U
display.print(F(" "));
display.print((char)247); // Grad-Symbol
display.print(F("C"));
}
else
{
// Anzeige, wenn Shot läuft
if (shotActive)
{
unsigned long elapsed = millis() - shotStartTime;
display.println(F("Shot-Timer:"));
display.println("");
display.setTextSize(3); // Größere Schrift für Timer
display.println(elapsed / 1000.0, 1); // Zeit in Sekunden mit einer Nachkommastelle
display.setTextSize(1); // Zurück zur normalen Schriftgröße
display.println(F("Sekunden"));
fastHeatUpHeating = false; // Shot unterbricht FastHeatUp Anzeige/Logik
}
else // Normalanzeige (kein Shot, kein Fast-Heat-Up)
{
display.println(F("Temperaturen:"));
display.println("");
// Wasser Temperatur / Fehleranzeige
display.println(F("Wasser: ")); // Text geändert für Konsistenz
if (wasserSensorError) {
display.print(F("Sensorfehler!"));
} else {
display.print((int)InputWasser);
display.print(F(" / "));
display.print((int)SetpointWasser); // Zeige aktuellen Sollwert
display.print(F(" "));
display.print((char)247); // Grad-Symbol
display.print(F("C"));
// ECO Status nur anzeigen, wenn kein Fehler vorliegt
if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)"));
else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)"));
}
display.println(""); // Leerzeile
display.println(""); // Zusätzliche Leerzeile für Abstand
// Dampf Temperatur / Fehleranzeige
display.println(F("Dampf: ")); // Text geändert für Konsistenz
if (dampfSensorError) {
display.print(F("Sensorfehler!"));
} else {
// Anzeige nur, wenn keine Startverzögerung aktiv ist ODER Verzögerung abgelaufen
if (!(dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL)))
{
display.print((int)InputDampf);
display.print(F(" / "));
display.print((int)SetpointDampf); // Zeige aktuellen Sollwert
display.print(F(" "));
display.print((char)247); // Grad-Symbol
display.print(F("C"));
// ECO Status nur anzeigen, wenn kein Fehler vorliegt
if (ecoModeAktiv && dynamicEcoActive) display.print(F(" (ECO+)"));
else if (ecoModeAktiv && !dynamicEcoActive) display.print(F(" (ECO)"));
} else {
display.print(F("Startverz\x94gerung")); // Anzeige während der Verzögerung
}
}
// Keine zusätzliche Leerzeile am Ende nötig
}
}
display.display(); // Display aktualisieren
}
// Wenn AutoTune aktiv ist, wird das Display in handleAutoTune() beschrieben (unverändert)
}
/************************************************************************************
* Verwaltet den Shot-Timer (wenn z.B. der Nutzer den Bezug aktiviert)
************************************************************************************/
void updateShotTimer()
{
if (digitalRead(SHOT_TIMER_PIN) == HIGH)
{
// Shot beginnt
if (!shotActive)
{
shotActive = true;
shotStartTime = millis();
}
}
else
{
// Shot endet
if (shotActive)
{
unsigned long shotDuration = millis() - shotStartTime;
// nur Shots > 20 Sekunden zählen
if (shotDuration > 20000)
{
shotCounter++;
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
EEPROM.commit();
Serial.print(F("Shots: "));
Serial.println(shotCounter);
}
shotActive = false;
lastShotTime = millis();
shotEndTime = millis();
delayDisplayUpdate = true;
}
}
}
/************************************************************************************
* Startet AutoTune für Wasser-PID
************************************************************************************/
void startAutoTuneWasser()
{
// Eco-Modus deaktivieren, falls aktiv
if(ecoModeAktiv)
{
ecoModeAktiv = false;
// Ursprünglichen Setpoint wiederherstellen (aus EEPROM oder Profil)
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
// Optional: Auch Dampf-Setpoint wiederherstellen, falls Eco beide beeinflusst
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
}
// Stelle sicher, dass der PID im Automatikmodus ist bevor Tuning beginnt
pidWasser.SetMode(AUTOMATIC);
autoTuneWasser = new PID_ATune(&InputWasser, &OutputWasser);
// Verwende die Wasser-spezifischen Parameter
autoTuneWasser->SetOutputStep(tuningStepWasser); // Angepasst
autoTuneWasser->SetControlType(1); // DIRECT control (heating)
autoTuneWasser->SetNoiseBand(tuningNoiseWasser); // Angepasst
autoTuneWasser->SetLookbackSec((int)tuningLookBackWasser); // Angepasst (Cast zu int für Funktion)
// Setze den Startwert für den Output
OutputWasser = tuningStartValueWasser; // Angepasst
// Wichtig: Den SSR Pin direkt mit dem Startwert ansteuern, bevor AutoTune übernimmt
digitalWrite(SSR_WASSER_PIN, (int)OutputWasser);
autoTuneWasserActive = true;
Serial.println("Starte AutoTune für Wasser...");
}
/************************************************************************************
* Startet AutoTune für Dampf-PID
************************************************************************************/
void startAutoTuneDampf()
{
// Eco-Modus deaktivieren, falls aktiv
if(ecoModeAktiv)
{
ecoModeAktiv = false;
// Ursprünglichen Setpoint wiederherstellen (aus EEPROM oder Profil)
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
// Optional: Auch Wasser-Setpoint wiederherstellen, falls Eco beide beeinflusst
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
}
// Stelle sicher, dass der PID im Automatikmodus ist bevor Tuning beginnt
// (Setpoint wird hier nicht geändert, da Tuning oft bei Betriebstemperatur startet)
pidDampf.SetMode(AUTOMATIC);
autoTuneDampf = new PID_ATune(&InputDampf, &OutputDampf);
// Verwende die Dampf-spezifischen Parameter
autoTuneDampf->SetOutputStep(tuningStepDampf); // Angepasst
autoTuneDampf->SetControlType(1); // DIRECT control (heating)
autoTuneDampf->SetNoiseBand(tuningNoiseDampf); // Angepasst
autoTuneDampf->SetLookbackSec((int)tuningLookBackDampf); // Angepasst (Cast zu int für Funktion)
// Setze den Startwert für den Output
OutputDampf = tuningStartValueDampf; // Angepasst
// Wichtig: Den SSR Pin direkt mit dem Startwert ansteuern, bevor AutoTune übernimmt
digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf);
autoTuneDampfActive = true;
Serial.println("Starte AutoTune für Dampf...");
}
/************************************************************************************
* Stoppt AutoTune für Wasser
************************************************************************************/
void stopAutoTuneWasser()
{
if (autoTuneWasserActive)
{
autoTuneWasserActive = false;
delete autoTuneWasser;
autoTuneWasser = nullptr;
OutputWasser = 0;
}
}
/************************************************************************************
* Stoppt AutoTune für Dampf
************************************************************************************/
void stopAutoTuneDampf()
{
if (autoTuneDampfActive)
{
autoTuneDampfActive = false;
delete autoTuneDampf;
autoTuneDampf = nullptr;
OutputDampf = 0;
}
}
/************************************************************************************
* Überwacht die AutoTune-Prozesse (Dampf/Wasser) und schreibt gefundene Werte ins EEPROM
************************************************************************************/
void handleAutoTune()
{
// AutoTune Wasser
if (autoTuneWasserActive)
{
// NEU: Prüfen auf Sensorfehler während des Tunings
if (wasserSensorError) {
Serial.println("FEHLER: Wassertemperatursensor während AutoTune ausgefallen! Breche Tuning ab.");
stopAutoTuneWasser(); // Ruft Funktion auf, die Flag zurücksetzt und Speicher freigibt
// Optional: Fehlermeldung auf Display ausgeben
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("PID-Tuning Fehler:"));
display.println(F("Wasser-Sensor!"));
display.display();
delay(3000); // Nachricht kurz anzeigen
return; // Funktion für diesen Durchlauf verlassen
}
// Wenn Runtime() 1 zurückgibt, ist der AutoTune-Prozess abgeschlossen
// (Original-Logik, jetzt nur ausgeführt, wenn kein Sensorfehler vorliegt)
if (autoTuneWasser->Runtime() == 1)
{
KpWasser = autoTuneWasser->GetKp();
KiWasser = autoTuneWasser->GetKi();
KdWasser = autoTuneWasser->GetKd();
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
EEPROM.commit();
Serial.println("AutoTune Wasser erfolgreich abgeschlossen.");
Serial.printf("Neue Werte: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", KpWasser, KiWasser, KdWasser);
stopAutoTuneWasser(); // Korrekt beenden
}
else
{
// Anzeige während AutoTune (unverändert)
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("PID-Tuning aktiv:"));
display.println(F("Wasser-PID"));
display.println("");
display.println(F("Bitte Maschine nicht"));
display.println(F("verwenden!")); // Textumbruch für Lesbarkeit
display.display();
}
}
// AutoTune Dampf
if (autoTuneDampfActive)
{
// NEU: Prüfen auf Sensorfehler während des Tunings
if (dampfSensorError) {
Serial.println("FEHLER: Dampftemperatursensor während AutoTune ausgefallen! Breche Tuning ab.");
stopAutoTuneDampf(); // Ruft Funktion auf, die Flag zurücksetzt und Speicher freigibt
// Optional: Fehlermeldung auf Display ausgeben
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("PID-Tuning Fehler:"));
display.println(F("Dampf-Sensor!"));
display.display();
delay(3000); // Nachricht kurz anzeigen
return; // Funktion für diesen Durchlauf verlassen
}
// Wenn Runtime() 1 zurückgibt, ist der AutoTune-Prozess abgeschlossen
// (Original-Logik, jetzt nur ausgeführt, wenn kein Sensorfehler vorliegt)
if (autoTuneDampf->Runtime() == 1)
{
KpDampf = autoTuneDampf->GetKp();
KiDampf = autoTuneDampf->GetKi();
KdDampf = autoTuneDampf->GetKd();
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
EEPROM.commit();
Serial.println("AutoTune Dampf erfolgreich abgeschlossen.");
Serial.printf("Neue Werte: Kp=%.2f, Ki=%.2f, Kd=%.2f\n", KpDampf, KiDampf, KdDampf);
stopAutoTuneDampf(); // Korrekt beenden
}
else
{
// Anzeige während AutoTune (unverändert)
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("PID-Tuning aktiv:"));
display.println(F("Dampf-PID"));
display.println("");
display.println(F("Bitte Maschine nicht"));
display.println(F("verwenden!"));
display.display();
}
}
}
/************************************************************************************
* Handler für die Info-Seite
************************************************************************************/
void handleInfo()
{
static const char infoHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Info</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char infoHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char infoHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Info</h1>
<form action='/updateInfoSettings' method='POST'>
<h3>Ger&auml;teinfo</h3>
Die Ger&auml;teinformationen werden beim Start der Maschine, bzw. PID-Controllers im Display angezeigt.
<br><br>
<label for='hersteller'>Hersteller:</label>
<input type='text' id='hersteller' name='hersteller' value='{HERSTELLER}'>
<label for='modell'>Modell:</label>
<input type='text' id='modell' name='modell' value='{MODELL}'>
<label for='zusatz'>Zusatz (z.B. Limited Edition):</label>
<input type='text' id='zusatz' name='zusatz' value='{ZUSATZ}'>
<input type='submit' value='Einstellungen speichern'>
</form>
<form action='/resetRuntime' method='POST'>
<h3>Betriebszeit:</h3>
<p>{RUNTIME}</p>
<input type='submit' value='Zur&uuml;cksetzen'>
</form>
<form action='/resetShots' method='POST'>
<h3>Shots: (Bez&uuml;ge &uuml;ber 20 Sekunden)</h3>
<p>{SHOT_COUNT}</p>
<input type='submit' value='Zur&uuml;cksetzen'>
</form>
<form>
<h3>Systeminfo</h3>
Freier Heap: {FREE_HEAP} Bytes<br>
SDK-Version: {SDK_VERSION}<br>
Core-Version: {CORE_VERSION}<br>
Boot-Version: {BOOT_VERSION}<br>
Chip-ID: {CHIP_ID}<br>
CPU-Takt: {CPU_MHZ} MHz<br>
Letzter Reset-Grund: {RESET_REASON}<br>
<br>
Sketch-Gr&ouml;&szlig;e: {SKETCH_SIZE} Bytes<br>
Freier Sketch-Speicher: {FREE_SKETCH} Bytes<br>
Flash-Gr&ouml;&szlig;e (Chip): {FLASH_SIZE} Bytes<br>
<br>
MAC-Adresse: {MAC_ADDRESS}<br>
WLAN-Signalst&auml;rke (RSSI): {WIFI_RSSI} dBm<br>
</form>
</body>
</html>
)rawliteral";
unsigned long totalSeconds = totalRuntime;
unsigned long days = totalSeconds / 86400;
unsigned long hours = (totalSeconds % 86400) / 3600;
unsigned long minutes = (totalSeconds % 3600) / 60;
String runtimeStr = String(days) + F(" Tag(e), ") +
String(hours) + F(" Stunde(n) und ") +
String(minutes) + F(" Minuten");
String html = FPSTR(infoHtml);
html += FPSTR(commonStyle);
html += FPSTR(infoHtml2);
html += FPSTR(commonNav);
html += FPSTR(infoHtml3); // <-- Stelle sicher, dass hier das angepasste HTML verwendet wird
html.replace(F("{HERSTELLER}"), String(infoHersteller));
html.replace(F("{MODELL}"), String(infoModell));
html.replace(F("{ZUSATZ}"), String(infoZusatz));
html.replace(F("{RUNTIME}"), runtimeStr);
html.replace(F("{SHOT_COUNT}"), String(shotCounter));
// Systeminfo-Ersetzungen
html.replace(F("{FREE_HEAP}"), String(ESP.getFreeHeap()));
html.replace(F("{SDK_VERSION}"), String(ESP.getSdkVersion()));
html.replace(F("{CORE_VERSION}"), ESP.getCoreVersion()); // NEU - Ist schon String
html.replace(F("{BOOT_VERSION}"), String(ESP.getBootVersion()));
html.replace(F("{CHIP_ID}"), String(ESP.getChipId(), HEX)); // NEU - ChipID oft als HEX angezeigt
html.replace(F("{CPU_MHZ}"), String(ESP.getCpuFreqMHz()));
html.replace(F("{RESET_REASON}"), ESP.getResetReason()); // NEU - Ist schon String
html.replace(F("{SKETCH_SIZE}"), String(ESP.getSketchSize()));
html.replace(F("{FREE_SKETCH}"), String(ESP.getFreeSketchSpace()));
html.replace(F("{FLASH_SIZE}"), String(ESP.getFlashChipSize())); // NEU
html.replace(F("{MAC_ADDRESS}"), WiFi.macAddress()); // NEU - Ist schon String
// RSSI nur anzeigen, wenn verbunden, sonst "N/A"
String rssi_str = F("N/A");
if (WiFi.status() == WL_CONNECTED) {
rssi_str = String(WiFi.RSSI());
}
html.replace(F("{WIFI_RSSI}"), rssi_str); // NEU
server.send(200, F("text/html"), html);
}
/************************************************************************************
* Handler für die Eco-Modus-Konfiguration
************************************************************************************/
void handleEco()
{
static const char ecoHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Eco-Modus</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char ecoHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char ecoHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Eco-Modus</h1>
<form action='/updateEcoSettings' method='POST'>
<h3>Eco-Modus</h3>
Der Eco-Modus senkt die voreingestellten Temperaturen nach der angegebenen Zeit auf die Eco-Temperaturen ab.<br>
<label for='ecoMode'>Eco-Modus (Minuten, 0 = deaktiviert):</label>
<input type='text' id='ecoMode' name='ecoMode' value='{ECO_MODE}'>
<label for='ecoModeTempWasser'>Eco-Temperatur Wasser:</label>
<input type='text' id='ecoModeTempWasser' name='ecoModeTempWasser' value='{ECO_MODE_TEMP_WASSER}'>
<label for='ecoModeTempDampf'>Eco-Temperatur Dampf:</label>
<input type='text' id='ecoModeTempDampf' name='ecoModeTempDampf' value='{ECO_MODE_TEMP_DAMPF}'>
<div style='margin-bottom:15px;'>
<br>
<h3>Dynamischer Eco-Modus</h3>
<label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>
Dynamischen Eco-Modus aktivieren (ECO+):
<input type='checkbox' id='dynamicEcoMode' name='dynamicEcoMode' value='1' {DYNAMIC_ECO_MODE_CHECKBOX}>
</label>
</div>
<div style='margin-top:15px;'>
Der dynamische Eco-Modus verhindert, dass die Maschine bis in die Unendlichkeit eine gewisse Temperatur aufrecht erh&auml;lt.<br>
Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollst&auml;ndig beendet wird.<br>
Der dynamische Eco-Modus ist nur in Kombination mit dem Eco-Modus nutzbar!<br>
<br>
<b>Beispiel f&uuml;r eine praktische Anwendung:</b><br>
Peter hat an seiner Maschine den Eco-Modus auf 45 Minuten gestellt, mit Eco-Temperatur von 60 Grad. Nun kommt ihm etwas dazwischen und er kommt erst bei Minute 60 an die Maschine, um sich einen Espresso zuzubereiten. Die Maschine ist nun allerdings schon auf 60 Grad herabgek&uuml;hlt und er muss erneut das Aufheizen abwarten ...<br>
Markus passiert das gleiche, doch er hat den dynamischen Eco-Modus mit einer initialen Eco-Temperatur von 95 Grad aktiviert - Seine Maschine ist nun zumindest noch bei 80 Grad.<br>
Markus muss zwar auch warten, kann jedoch noch vor Peter einen Espresso trinken.
</div><br>
<h3>Verz&ouml;gerung f&uuml;r Dampf</h3>
<label for='dampfDelay'>Aufheizen f&uuml;r Dampf verz&ouml;gern (Minuten, 0 = deaktiviert):</label>
<input type='text' id='dampfDelay' name='dampfDelay' value='{DAMPF_DELAY}'>
<b>Achtung:</b><br>
Um Konflikte zu vermeiden, sollte der Wert geringer sein, als der des Eco-Modus, falls dieser aktiviert ist!
<br>
<input type='submit' value='Einstellungen speichern'>
</form>
</body>
</html>
)rawliteral";
String html = FPSTR(ecoHtml);
html += FPSTR(commonStyle);
html += FPSTR(ecoHtml2);
html += FPSTR(commonNav);
html += FPSTR(ecoHtml3);
html.replace(F("{ECO_MODE}"), String(ecoModeMinutes));
html.replace(F("{ECO_MODE_TEMP_WASSER}"), String(ecoModeTempWasser));
html.replace(F("{ECO_MODE_TEMP_DAMPF}"), String(ecoModeTempDampf));
html.replace(F("{DYNAMIC_ECO_MODE_CHECKBOX}"), dynamicEcoActive ? F("checked") : F(""));
html.replace(F("{DAMPF_DELAY}"), String(dampfVerzoegerung));
server.send(200, F("text/html"), html);
}
/************************************************************************************
* Handler für die Fast-Heat-Up Seite
************************************************************************************/
void handleFastHeatUp()
{
static const char fhuHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Fast-Heat-Up-Modus</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char fhuHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char fhuHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Fast-Heat-Up-Modus</h1>
<form action='/updateFast-Heat-Up-Settings' method='POST'>
<h3>Fast-Heat-Up-Modus</h3>
<div style='margin-bottom:15px;'>
<label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>
Fast-Heat-Up aktivieren:
</label>
<input type='checkbox' id='fast-heat-up-aktivieren' name='fastHeatUpAktiv' value='1' {FASTHEATUP_MODE_CHECKBOX}>
</div>
<div style='margin-top:15px;'>
Der Fast-Heat-Up-Modus erm&ouml;glicht es, die Maschine noch schneller aufzuheizen.<br>
Der Kessel wird beim Start auf 130 Grad Celsius erhitzt.<br>
Nachdem die Temperatur erreicht ist, muss ein Flush von ca. 20 Sekunden durchgef&uuml;hrt werden.<br>
<br>
<b>ACHTUNG:</b><br>
Bitte beachten, dass auf der Seite PID-Einstellung eventuell die max. Wassertemperatur der Sicherheitsabschaltung angepasst werden muss!<br>
</div>
<input type='submit' value='Einstellungen speichern' style='margin-top:20px;'>
</form>
</body>
</html>
)rawliteral";
String html = FPSTR(fhuHtml);
html += FPSTR(commonStyle);
html += FPSTR(fhuHtml2);
html += FPSTR(commonNav);
html += FPSTR(fhuHtml3);
html.replace(F("{FASTHEATUP_MODE_CHECKBOX}"), fastHeatUpAktiv ? F("checked") : F(""));
server.send(200, F("text/html"), html);
}
/************************************************************************************
* Handler zum Speichern der Fast-Heat-Up-Einstellung
************************************************************************************/
void handleFastHeatUpSettings()
{
if (server.hasArg(F("fastHeatUpAktiv")))
{
fastHeatUpAktiv = true;
}
else
{
fastHeatUpAktiv = false;
}
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
EEPROM.commit();
server.sendHeader(F("Location"), F("/fast-heat-up"));
server.send(303);
}
/************************************************************************************
* Handler für Root (PID-Einstellungen)
************************************************************************************/
void handleRoot()
{
static const char rootHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>PID-Einstellung</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char rootHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char rootHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>PID-Einstellung</h1>
<form action='/updateSettings' method='POST'>
<h3>Temperatur &amp; Offset: Wasser / Kessel</h3>
<label for='wasser'>Setpoint (&deg;C):</label>
<input type='text' id='wasser' name='wasser' value='{WASSER}'>
<label for='offsetWasser'>Offset (&deg;C):</label>
<input type='text' id='offsetWasser' name='offsetWasser' value='{OFFSET_WASSER}'>
</br>
<h3>Temperatur &amp; Offset: Dampf / Thermoblock</h3>
<label for='dampf'>Setpoint (&deg;C):</label>
<input type='text' id='dampf' name='dampf' value='{DAMPF}'>
<label for='offsetDampf'>Offset (&deg;C):</label>
<input type='text' id='offsetDampf' name='offsetDampf' value='{OFFSET_DAMPF}'>
</br>
<h3>PID-Parameter: Wasser / Kessel</h3>
<label for='kpWasser'>Kp:</label>
<input type='text' id='kpWasser' name='kpWasser' value='{KP_WASSER}'>
<label for='kiWasser'>Ki:</label>
<input type='text' id='kiWasser' name='kiWasser' value='{KI_WASSER}'>
<label for='kdWasser'>Kd:</label>
<input type='text' id='kdWasser' name='kdWasser' value='{KD_WASSER}'>
</br>
<h3>PID-Parameter: Dampf / Thermoblock</h3>
<label for='kpDampf'>Kp:</label>
<input type='text' id='kpDampf' name='kpDampf' value='{KP_DAMPF}'>
<label for='kiDampf'>Ki:</label>
<input type='text' id='kiDampf' name='kiDampf' value='{KI_DAMPF}'>
<label for='kdDampf'>Kd:</label>
<input type='text' id='kdDampf' name='kdDampf' value='{KD_DAMPF}'>
</br>
<h3>Sicherheitsabschaltung</h3>
Die Sicherheitsabschaltung bietet eine softwareseitige &Uuml;bertemperatursicherung,<br>
ersetzt jedoch keine hardwareseitige L&ouml;sung durch z.B. Bimetall Temperaturschalter!<br>
Bei der Verwendung von Fast-Heat-Up muss der Wert f&uuml;r die Maximaltemperatur angepasst werden!<br>
<br>
<label for='maxTempWasser'>Max. Temp Wasser (&deg;C):</label>
<input type='text' id='maxTempWasser' name='maxTempWasser' value='{MAX_TEMP_WASSER}'>
<label for='maxTempDampf'>Max. Temp Dampf (&deg;C):</label>
<input type='text' id='maxTempDampf' name='maxTempDampf' value='{MAX_TEMP_DAMPF}'>
<input type='submit' value='Einstellungen speichern'>
</form>
</body>
</html>
)rawliteral";
String html = FPSTR(rootHtml);
html += FPSTR(commonStyle);
html += FPSTR(rootHtml2);
html += FPSTR(commonNav);
html += FPSTR(rootHtml3);
html.replace(F("{MAX_TEMP_WASSER}"), String(maxTempWasser, 1)); // Mit einer Nachkommastelle
html.replace(F("{MAX_TEMP_DAMPF}"), String(maxTempDampf, 1)); // Mit einer Nachkommastelle
html.replace(F("{WASSER}"), String((int)SetpointWasser));
html.replace(F("{OFFSET_WASSER}"), String(OffsetWasser, 1));
html.replace(F("{DAMPF}"), String((int)SetpointDampf));
html.replace(F("{OFFSET_DAMPF}"), String(OffsetDampf, 1));
html.replace(F("{KP_WASSER}"), String(KpWasser, 1));
html.replace(F("{KI_WASSER}"), String(KiWasser, 1));
html.replace(F("{KD_WASSER}"), String(KdWasser, 1));
html.replace(F("{KP_DAMPF}"), String(KpDampf, 1));
html.replace(F("{KI_DAMPF}"), String(KiDampf, 1));
html.replace(F("{KD_DAMPF}"), String(KdDampf, 1));
server.send(200, F("text/html"), html);
}
/************************************************************************************
* Schreibt die Werte in den EEPROM (PID/Eco)
************************************************************************************/
void handleUpdate()
{
if (server.hasArg(F("maxTempWasser"))) maxTempWasser = server.arg(F("maxTempWasser")).toFloat();
if (server.hasArg(F("maxTempDampf"))) maxTempDampf = server.arg(F("maxTempDampf")).toFloat();
if (server.hasArg(F("wasser"))) SetpointWasser = server.arg(F("wasser")).toFloat();
if (server.hasArg(F("dampf"))) SetpointDampf = server.arg(F("dampf")).toFloat();
if (server.hasArg(F("offsetWasser"))) OffsetWasser = server.arg(F("offsetWasser")).toFloat();
if (server.hasArg(F("offsetDampf"))) OffsetDampf = server.arg(F("offsetDampf")).toFloat();
if (server.hasArg(F("kpWasser"))) KpWasser = server.arg(F("kpWasser")).toFloat();
if (server.hasArg(F("kiWasser"))) KiWasser = server.arg(F("kiWasser")).toFloat();
if (server.hasArg(F("kdWasser"))) KdWasser = server.arg(F("kdWasser")).toFloat();
if (server.hasArg(F("kpDampf"))) KpDampf = server.arg(F("kpDampf")).toFloat();
if (server.hasArg(F("kiDampf"))) KiDampf = server.arg(F("kiDampf")).toFloat();
if (server.hasArg(F("kdDampf"))) KdDampf = server.arg(F("kdDampf")).toFloat();
EEPROM.put(EEPROM_ADDR_MAX_TEMP_WASSER, maxTempWasser);
EEPROM.put(EEPROM_ADDR_MAX_TEMP_DAMPF, maxTempDampf);
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
EEPROM.commit();
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
server.sendHeader(F("Location"), F("/"));
server.send(303);
}
/************************************************************************************
* Speichert Eco-Einstellungen
************************************************************************************/
void handleEcoUpdate()
{
if (server.hasArg(F("ecoMode"))) ecoModeMinutes = server.arg(F("ecoMode")).toInt();
if (server.hasArg(F("ecoModeTempWasser"))) ecoModeTempWasser = server.arg(F("ecoModeTempWasser")).toInt();
if (server.hasArg(F("ecoModeTempDampf"))) ecoModeTempDampf = server.arg(F("ecoModeTempDampf")).toInt();
if (server.hasArg(F("dynamicEcoMode")))
{
dynamicEcoActive = true;
}
else
{
dynamicEcoActive = false;
}
if (server.hasArg(F("dampfDelay")))
{
dampfVerzoegerung = server.arg(F("dampfDelay")).toInt();
}
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
EEPROM.commit();
// Wenn Eco deaktiviert, Setpoints zurückladen
if (ecoModeMinutes == 0)
{
ecoModeAktiv = false;
ecoModeActivatedTime = 0;
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
}
server.sendHeader(F("Location"), F("/eco"));
server.send(303);
}
/************************************************************************************
* Speichert Geräte-Infos
************************************************************************************/
void handleInfoUpdate()
{
if (server.hasArg(F("hersteller")))
{
strncpy(infoHersteller, server.arg(F("hersteller")).c_str(), sizeof(infoHersteller));
}
if (server.hasArg(F("modell")))
{
strncpy(infoModell, server.arg(F("modell")).c_str(), sizeof(infoModell));
}
if (server.hasArg(F("zusatz")))
{
strncpy(infoZusatz, server.arg(F("zusatz")).c_str(), sizeof(infoZusatz));
}
EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell);
EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
EEPROM.commit();
server.sendHeader(F("Location"), F("/info"));
server.send(303);
}
/************************************************************************************
* Handler für Temperatur-Charts (Live) - Modernisierte Version mit Fehlermeldungen
************************************************************************************/
void handleCharts()
{
// HTML Teil 1: Head, Chart.js CDN, CSS-Styles für die Chart-Seite
static const char chartsHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="de">
<head>
<title>Live-Temperaturverlauf</title>
<meta charset="UTF-8">
<meta name='theme-color' content='#111111'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>
<style>
/* Spezifische Styles für die Chart-Seite (Ergänzung zu commonStyle) */
h1.chart-title { /* Eigene Klasse um Konflikte zu vermeiden */
text-align: center;
color: #efefef;
margin-top: 20px;
margin-bottom: 10px;
font-weight: 300; /* Leichtere Schrift */
font-size: 1.8rem; /* Etwas kleiner als Standard H1 */
}
/* Container für das Diagramm */
.chart-container {
width: 95%; /* Etwas breiter */
max-width: 1200px; /* Max Breite für große Bildschirme */
height: 65vh; /* Höhe relativ zur Fensterhöhe, etwas weniger als vorher */
min-height: 350px; /* Mindesthöhe für kleine Bildschirme */
margin: 15px auto; /* Oben/Unten Abstand reduziert */
background: rgba(0, 0, 0, 0.2); /* Dunkler, leicht transparenter Hintergrund */
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4); /* Angepasster Schatten */
border-radius: 15px; /* Stärkere Abrundung */
position: relative; /* Wichtig für Chart.js */
border: 1px solid rgba(255, 255, 255, 0.1); /* Subtiler heller Rand */
padding: 20px; /* Mehr Innenabstand */
box-sizing: border-box;
}
/* Canvas Styling (nimmt Größe des Containers an) */
.chart-container canvas {
width: 100% !important;
height: 100% !important;
}
/* Dropdown für Aktualisierungsrate */
.update-rate-selector {
margin: 10px auto 20px auto; /* Abstand angepasst */
display: flex;
justify-content: center;
align-items: center;
gap: 12px; /* Mehr Abstand zwischen Label und Select */
}
.update-rate-selector label {
color: #ccc; /* Helles Grau für Label */
font-size: 0.95em;
}
.update-rate-selector select {
background: #252525; /* Dunkler Hintergrund für Select */
color: #e0e0e0;
border: 1px solid #555; /* Dunklerer Rand */
padding: 8px 12px;
border-radius: 8px; /* Stärkere Abrundung */
font-size: 0.9em;
cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.update-rate-selector select:focus {
outline: none;
border-color: var(--accent-color, #d89904); /* Fokus mit Akzentfarbe */
box-shadow: 0 0 8px rgba(216, 153, 4, 0.3); /* Passender Schatten */
}
/* --- NEU: Styling für Sensor-Fehlermeldungen --- */
.sensor-error-message {
color: #FFCC00; /* Auffälliges Gelb/Gold für Warnungen im Dark Mode */
text-align: center;
margin-top: 5px; /* Weniger Abstand nach oben */
margin-bottom: 15px; /* Abstand zum Chart-Container */
padding: 8px 10px;
background-color: rgba(255, 204, 0, 0.1); /* Sehr dezenter gelber Hintergrund */
border: 1px solid rgba(255, 204, 0, 0.2); /* Sehr dezenter gelber Rand */
border-radius: 8px;
font-weight: 500; /* Etwas fetter als normal */
font-size: 0.9em;
max-width: 600px; /* Verhindert zu breite Box auf großen Screens */
margin-left: auto; /* Zentriert die Box selbst */
margin-right: auto;
}
/* Responsive Anpassungen für kleinere Bildschirme */
@media (max-width: 600px) {
h1.chart-title {
font-size: 1.5rem;
}
.chart-container {
width: 98%;
padding: 10px;
height: 60vh; /* Evtl. Höhe anpassen */
}
.update-rate-selector {
flex-direction: column; /* Untereinander auf kleinen Screens */
gap: 5px;
margin-bottom: 15px;
}
.sensor-error-message {
width: 90%; /* Error-Box auf kleinen Screens */
font-size: 0.85em;
}
}
</style>
)rawliteral";
// HTML Teil 2: Schließendes Head-Tag
static const char chartsHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
// HTML Teil 3: Body, Navigation, Chart-Container und JavaScript
static const char chartsHtml3[] PROGMEM = R"rawliteral(
<body>
<h1 class="chart-title">Live-Temperaturverlauf</h1>
<div class="update-rate-selector">
<label for="updateRate">Aktualisierungsrate:</label>
<select id="updateRate">
<option value="1000">1 Sekunde</option>
<option value="2000">2 Sekunden</option>
<option value="3000" selected>3 Sekunden</option>
<option value="5000">5 Sekunden</option>
<option value="10000">10 Sekunden</option>
</select>
</div>
{SENSOR_ERROR_MESSAGES}
<div class="chart-container">
<canvas id="combinedChart"></canvas>
</div>
<script>
const ctx = document.getElementById('combinedChart').getContext('2d');
let currentSetpointDampf = 0; // Startwerte, werden sofort überschrieben
let currentSetpointWasser = 0;
// --- Modernere Farbpalette (Dark-Theme-freundlich) ---
const wasserColor = '#00AEEF'; // Helles Blau (Corporate Blue ähnlich)
const dampfColor = '#F7941D'; // Kräftiges Orange
const setpointWasserColor = 'rgba(0, 174, 239, 0.7)'; // Passendes Blau, transparenter
const setpointDampfColor = 'rgba(247, 148, 29, 0.7)'; // Passendes Orange, transparenter
const gridColor = 'rgba(255, 255, 255, 0.1)'; // Sehr dezentes Gitter
const textColor = '#E0E0E0'; // Helle Textfarbe für Achsen/Legende
// Chart.js Instanz
const combinedChart = new Chart(ctx, {
type: 'line',
data: {
labels: [], // Werden dynamisch gefüllt (Zeiten)
datasets: [
{
label: 'Wasser IST', // Kürzerer Name
data: [],
borderColor: wasserColor,
backgroundColor: 'rgba(0, 174, 239, 0.1)', // Füllfarbe mit niedriger Opazität
borderWidth: 2,
fill: 'start', // Fläche unter der Linie füllen bis zur X-Achse
tension: 0.4, // Linien glätten
pointRadius: 0, // Punkte standardmäßig ausblenden
pointHoverRadius: 6, // Punkte bei Hover größer machen
pointHitRadius: 10, // Größerer Bereich für Tooltip-Trigger
pointHoverBackgroundColor: wasserColor, // Punktfarbe bei Hover
},
{
label: 'Dampf IST', // Kürzerer Name
data: [],
borderColor: dampfColor,
backgroundColor: 'rgba(247, 148, 29, 0.1)', // Füllfarbe
borderWidth: 2,
fill: 'start',
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 6,
pointHitRadius: 10,
pointHoverBackgroundColor: dampfColor,
},
{
label: 'Wasser SOLL', // Kürzerer Name
data: [],
borderColor: setpointWasserColor,
borderWidth: 1.5, // Etwas dünner
borderDash: [6, 3], // Angepasstes Strichmuster (kürzer)
fill: false, // Keine Füllung für Setpoints
tension: 0.1, // Nur leichte Glättung für Setpoints
pointRadius: 0, // Keine Punkte für Setpoints
pointHoverRadius: 0,
},
{
label: 'Dampf SOLL', // Kürzerer Name
data: [],
borderColor: setpointDampfColor,
borderWidth: 1.5,
borderDash: [6, 3],
fill: false,
tension: 0.1,
pointRadius: 0,
pointHoverRadius: 0,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false, // WICHTIG bei fester Container-Höhe
animation: false, // Keine Startanimation
interaction: {
intersect: false, // Tooltip auch anzeigen, wenn nicht genau getroffen
mode: 'index', // Tooltip für alle Datenpunkte am selben Zeitindex anzeigen
},
scales: {
x: {
type: 'category', // Besser für Zeit-Labels als 'linear'
title: {
display: true,
text: 'Uhrzeit', // Aussagekräftiger als "Zeitverlauf"
color: textColor,
font: { size: 13, weight: '300' }
},
ticks: {
color: textColor,
font: { size: 11 },
maxRotation: 0, // Verhindert Drehung der Labels
autoSkip: true, // Überspringt Labels bei Platzmangel
maxTicksLimit: 8 // Begrenzt die Anzahl sichtbarer Ticks (anpassen bei Bedarf)
},
grid: {
color: gridColor, // Subtiles Gitter
drawBorder: false // Keine dicke Achsenlinie am Rand
}
},
y: {
// beginAtZero: false, // Nur wenn negative Temps möglich wären
suggestedMin: 0, // Startet nahe 0, passt sich aber an
suggestedMax: 180, // Schlägt Maximum vor (etwas über Dampf-Setpoint), passt sich an
title: {
display: true,
text: 'Temperatur (°C)', // Achsentitel hinzugefügt
color: textColor,
font: { size: 13, weight: '300' }
},
ticks: {
color: textColor,
font: { size: 11 },
stepSize: 20, // Feste Schrittweite für bessere Lesbarkeit
// Callback um '°C' hinzuzufügen
callback: function(value, index, values) {
return value + ' °C';
}
},
grid: {
color: gridColor, // Subtiles Gitter
drawBorder: false
}
}
},
plugins: {
legend: {
position: 'bottom', // Legende unten platziert
align: 'center', // Zentriert
labels: {
color: textColor,
font: { size: 12 },
usePointStyle: true, // Kleine Symbole statt Linien
pointStyle: 'rectRounded', // Form der Symbole (oder 'circle', 'rect' etc.)
boxWidth: 15, // Breite der Symbole
padding: 15 // Abstand der Legendenelemente
}
},
tooltip: {
enabled: true,
mode: 'index', // Stellt sicher, dass alle Werte für den Index angezeigt werden
intersect: false,
backgroundColor: 'rgba(0, 0, 0, 0.85)', // Dunkler Tooltip Hintergrund
titleColor: '#ffffff', // Titel in Weiß
titleFont: { size: 13, weight: 'bold' },
bodyColor: '#dddddd', // Textfarbe im Body
bodyFont: { size: 12 },
bodySpacing: 4, // Abstand zwischen Tooltip-Zeilen
borderColor: 'rgba(255, 255, 255, 0.1)', // Feiner Rand
borderWidth: 1,
padding: 10, // Innenabstand
cornerRadius: 8, // Abgerundete Ecken
displayColors: true, // Farbige Quadrate im Tooltip anzeigen
boxPadding: 4, // Abstand der Farbquadrate zum Text
// Callback um °C im Tooltip hinzuzufügen und Formatierung
callbacks: {
label: function(context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
// Runden auf eine Nachkommastelle für bessere Anzeige
label += context.parsed.y.toFixed(1) + ' °C';
}
return label;
}
}
}
}
}
});
// --- Daten abrufen und Chart aktualisieren ---
let updateInterval = 3000; // Standard-Intervall (aus Dropdown)
let intervalId = null;
const MAX_DATA_POINTS = 100; // Maximale Anzahl Datenpunkte im Chart (anpassen bei Bedarf)
// Holt Daten vom Server und aktualisiert das Chart
function fetchData() {
fetch('/data')
.then(response => {
if (!response.ok) {
console.error('Netzwerkantwort war nicht ok:', response.statusText);
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
// Aktuelle Setpoints speichern für die Anzeige
if (data.setpointwasser !== undefined) currentSetpointWasser = data.setpointwasser;
if (data.setpointdampf !== undefined) currentSetpointDampf = data.setpointdampf;
// Zeitstempel für die X-Achse generieren
const now = new Date();
// Format HH:MM:SS
const timeLabel = now.toLocaleTimeString('de-DE');
const labels = combinedChart.data.labels;
const datasets = combinedChart.data.datasets;
// Daten hinzufügen
labels.push(timeLabel);
// --- Pruefen ob Sensorfehler vorliegt ---
// HINWEIS: Diese Info kommt nicht vom /data endpoint.
// Die Anzeige wird serverseitig gerendert.
// Wir fuegen hier nur die normalen Werte hinzu.
datasets[0].data.push(data.wasser); // Wasser IST
datasets[1].data.push(data.dampf); // Dampf IST
datasets[2].data.push(currentSetpointWasser); // Wasser SOLL
datasets[3].data.push(currentSetpointDampf); // Dampf SOLL
// Alte Daten entfernen, wenn Limit erreicht
while (labels.length > MAX_DATA_POINTS) {
labels.shift(); // Ältestes Label entfernen
datasets.forEach(dataset => {
dataset.data.shift(); // Ältesten Datenpunkt entfernen
});
}
// Chart aktualisieren ohne Animation (verhindert Flackern)
combinedChart.update('none');
})
.catch(err => {
console.error("Fehler beim Abrufen der Daten:", err);
// Optional: Stoppe das Intervall bei wiederholten Fehlern
// if (intervalId) clearInterval(intervalId);
});
}
// Startet und stoppt das regelmäßige Abrufen der Daten
function startFetching() {
if (intervalId) {
clearInterval(intervalId); // Bestehendes Intervall sicher löschen
intervalId = null;
}
fetchData(); // Sofort einmal Daten holen, um Chart zu initialisieren
intervalId = setInterval(fetchData, updateInterval); // Neues Intervall starten
console.log(`Chart-Aktualisierung gestartet: Intervall ${updateInterval / 1000} Sekunden.`);
}
// Event Listener für das Dropdown der Aktualisierungsrate
document.getElementById('updateRate').addEventListener('change', function(event) {
const newInterval = parseInt(event.target.value, 10);
if (!isNaN(newInterval) && newInterval > 0) {
updateInterval = newInterval;
startFetching(); // Fetching neu starten mit neuem Intervall
}
});
// Initialen Fetch starten, wenn die Seite geladen ist
startFetching();
</script>
</body>
</html>
)rawliteral";
// --- HTML zusammensetzen ---
String html = FPSTR(chartsHtml); // Teil 1 (Head + CSS Start)
html += FPSTR(commonStyle); // Deine allgemeinen Styles hier einfügen
html += FPSTR(chartsHtml2); // Teil 2 (Head Ende)
html += FPSTR(commonNav); // Deine Navigation hier einfügen
html += FPSTR(chartsHtml3); // Teil 3 (Body + JS inkl. Platzhalter)
// --- NEU: Dynamische Fehlermeldungen generieren ---
String errorMessageHtml = ""; // Standardmäßig leer
// Extern deklarierte Flags `wasserSensorError` und `dampfSensorError` verwenden
if (wasserSensorError && dampfSensorError) {
errorMessageHtml = "<div class=\"sensor-error-message\">Die Temperaturwerte sind aktuell nicht verf&uuml;gbar</div>";
} else if (wasserSensorError) {
errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert f&uuml;r Wasser nicht verf&uuml;gbar</div>";
} else if (dampfSensorError) {
errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert f&uuml;r Dampf nicht verf&uuml;gbar</div>";
}
// Platzhalter im HTML durch die generierte Fehlermeldung ersetzen (oder durch Leerstring, falls kein Fehler)
html.replace(F("{SENSOR_ERROR_MESSAGES}"), errorMessageHtml);
// --- Senden der Seite ---
server.send(200, F("text/html"), html);
}
/************************************************************************************
* Profil-Funktionen (Speichern/Laden von Profil 1 und 2)
************************************************************************************/
void handleProfiles()
{
TemperatureProfile p1;
TemperatureProfile p2;
EEPROM.get(EEPROM_ADDR_PROFILE_1, p1);
EEPROM.get(EEPROM_ADDR_PROFILE_2, p2);
static const char profilesHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Profile</title><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='theme-color' content='#000000'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char profilesHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char profilesHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Temperaturprofile</h1>
<form>
Es k&ouml;nnen zwei unabh&auml;ngige Profile angelegt oder geladen werden.<br>
<br>
<b>In den Profilen werden gespeichert:</b><br>
- Setpoint Wasser<br>
- Setpoint Dampf<br>
- Eco-Modus-Einstellungen<br>
- Aufheizen f&uuml;r Dampf verz&ouml;gern<br>
- Profilname<br>
</form>
<form action='/saveProfile1' method='POST'>
<h3>Profil 1 (Name: {PROFILE1NAME})</h3>
<label for='profileName1'>Neuen Profilnamen eingeben:</label>
<input type='text' id='profileName1' name='profileName' value='{PROFILE1NAME}' maxlength='19'>
<input type='submit' value='Speichern in Profil 1'>
</form>
<form action='/loadProfile1' method='POST'>
<h3>Profil 1 laden</h3>
Temperatureinstellungen aus <b>Profil 1</b> laden.<br>
<input type='submit' value='Profil 1 laden'>
</form>
<form action='/saveProfile2' method='POST'>
<h3>Profil 2 (Name: {PROFILE2NAME})</h3>
<label for='profileName2'>Neuen Profilnamen eingeben:</label>
<input type='text' id='profileName2' name='profileName' value='{PROFILE2NAME}' maxlength='19'>
<input type='submit' value='Speichern in Profil 2'>
</form>
<form action='/loadProfile2' method='POST'>
<h3>Profil 2 laden</h3>
Temperatureinstellungen aus <b>Profil 2</b> laden<br>
<input type='submit' value='Profil 2 laden'>
</form>
</body>
</html>
)rawliteral";
String page = FPSTR(profilesHtml);
page += FPSTR(commonStyle);
page += FPSTR(profilesHtml2);
page += FPSTR(commonNav);
String htmlBody = FPSTR(profilesHtml3);
htmlBody.replace("{PROFILE1NAME}", String(p1.profileName));
htmlBody.replace("{PROFILE2NAME}", String(p2.profileName));
page += htmlBody;
server.send(200, F("text/html"), page);
}
// Speichert aktuelles Setup in Profil 1
void handleSaveProfile1()
{
TemperatureProfile p;
EEPROM.get(EEPROM_ADDR_PROFILE_1, p);
if (server.hasArg("profileName"))
{
strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName));
}
// Wir verwenden die Werte aus dem EEPROM, damit nicht fälschlicherweise ECO-Werte genommen werden
p.setpointWasser = EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
p.ecoModeMinutes = ecoModeMinutes;
p.dynamicEcoActive = dynamicEcoActive;
p.dampfVerzoegerung = dampfVerzoegerung;
EEPROM.put(EEPROM_ADDR_PROFILE_1, p);
EEPROM.commit();
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}
// Lädt Profil 1 in die aktuellen Parameter
void handleLoadProfile1()
{
TemperatureProfile p;
EEPROM.get(EEPROM_ADDR_PROFILE_1, p);
SetpointWasser = p.setpointWasser;
SetpointDampf = p.setpointDampf;
ecoModeMinutes = p.ecoModeMinutes;
dynamicEcoActive = p.dynamicEcoActive;
dampfVerzoegerung = p.dampfVerzoegerung;
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
EEPROM.commit();
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}
// Speichert aktuelles Setup in Profil 2
void handleSaveProfile2()
{
TemperatureProfile p;
EEPROM.get(EEPROM_ADDR_PROFILE_2, p);
if (server.hasArg("profileName"))
{
strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName));
}
p.setpointWasser = EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
p.ecoModeMinutes = ecoModeMinutes;
p.dynamicEcoActive = dynamicEcoActive;
p.dampfVerzoegerung = dampfVerzoegerung;
EEPROM.put(EEPROM_ADDR_PROFILE_2, p);
EEPROM.commit();
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}
// Lädt Profil 2 in die aktuellen Parameter
void handleLoadProfile2()
{
TemperatureProfile p;
EEPROM.get(EEPROM_ADDR_PROFILE_2, p);
SetpointWasser = p.setpointWasser;
SetpointDampf = p.setpointDampf;
ecoModeMinutes = p.ecoModeMinutes;
dynamicEcoActive = p.dynamicEcoActive;
dampfVerzoegerung = p.dampfVerzoegerung;
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
EEPROM.commit();
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}