3699 lines
146 KiB
Arduino
3699 lines
146 KiB
Arduino
/************************************************************************************
|
|
* Dual-PID Controller Firmware (Zeitproportional)
|
|
* Version (Siehe unten im Code)
|
|
*
|
|
* Dieses Programm steuert Wasser- und Dampftemperatur über zwei unabhängige PID-Regler
|
|
* mit zeitproportionaler Ansteuerung der SSRs.
|
|
* Sensoren: MAX6675 (Dampf), NTC (Wasser).
|
|
* Funktionen: Eco-Modus, Profile, Fast-Heat-Up, WLAN-Weboberfläche, AutoTune.
|
|
*
|
|
* 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>
|
|
#include <math.h> // Für isnan()
|
|
|
|
/************************************************************************************
|
|
* 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.7.0";
|
|
String versionHersteller = "Thomas Mü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 = false;
|
|
|
|
// 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 & Zeitproportionale Steuerung
|
|
************************************************************************************/
|
|
|
|
// PID Input/Output/Setpoint Variablen
|
|
double SetpointDampf, InputDampf, OutputDampf;
|
|
double SetpointWasser, InputWasser, OutputWasser;
|
|
double OffsetDampf = 0.0, OffsetWasser = 0.0;
|
|
|
|
// PID-Konstanten (Regelungsparameter)
|
|
double KpDampf = 3.0, KiDampf = 0.1, KdDampf = 0.5;
|
|
double KpWasser = 5.0, KiWasser = 0.2, KdWasser = 1.0;
|
|
|
|
// --- NEU: Zeitproportionale Steuerung ---
|
|
unsigned long windowSizeWasser = 2000; // Fenstergröße für Wasser-SSR in ms (2 Sekunden)
|
|
unsigned long windowSizeDampf = 2000; // Fenstergröße für Dampf-SSR in ms (2 Sekunden)
|
|
unsigned long windowStartTimeWasser; // Startzeit des aktuellen Fensters für Wasser
|
|
unsigned long windowStartTimeDampf; // Startzeit des aktuellen Fensters für Dampf
|
|
|
|
// PID-Regler Instanzen
|
|
// WICHTIG: Die Output-Grenzen werden in setup() auf die Fenstergröße gesetzt!
|
|
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_SIZE = 1024; // Gesamtgröße des verwendeten EEPROM-Speichers
|
|
|
|
/************************************************************************************
|
|
* EEPROM Adressen für das Speichern/Laden von Werten
|
|
* (Adressen bleiben gleich wie im Originalcode)
|
|
************************************************************************************/
|
|
|
|
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 = 500.0; // Angepasst für Zeitproportional (Output 0-2000)
|
|
const double defaultTuningNoiseWasser = 1.0;
|
|
const double defaultTuningStartValueWasser = 1000.0; // Mittlerer Output als Startwert
|
|
const unsigned int defaultTuningLookBackWasser = 60;
|
|
|
|
// Parameter für Dampf
|
|
double tuningStepDampf;
|
|
double tuningNoiseDampf;
|
|
double tuningStartValueDampf;
|
|
unsigned int tuningLookBackDampf;
|
|
const double defaultTuningStepDampf = 300.0; // Angepasst für Zeitproportional
|
|
const double defaultTuningNoiseDampf = 1.0;
|
|
const double defaultTuningStartValueDampf = 1000.0; // Mittlerer Output als Startwert
|
|
const unsigned int defaultTuningLookBackDampf = 30;
|
|
|
|
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 initialRuntimeSaveDelay = 10 * 60 * 1000UL; // 10 Minuten
|
|
const unsigned long subsequentRuntimeSaveInterval = 5 * 60 * 1000UL; // 5 Minuten
|
|
bool initialRuntimeSaveDone = false;
|
|
|
|
/************************************************************************************
|
|
* 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; // PID-Berechnungsintervall
|
|
|
|
/************************************************************************************
|
|
* 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) - Unverändert
|
|
************************************************************************************/
|
|
|
|
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;
|
|
background: linear-gradient(120deg, #000000 0%, #1a1a1a 100%);
|
|
background-repeat: no-repeat;
|
|
background-attachment: fixed;
|
|
background-size: cover;
|
|
min-height: 100vh;
|
|
color: #ffffff;
|
|
}
|
|
|
|
/************************************************************************
|
|
* Links / Anker-Tags
|
|
************************************************************************/
|
|
a,
|
|
a:visited {
|
|
text-decoration: none;
|
|
transition: color 0.2s ease, background-color 0.2s ease;
|
|
}
|
|
a:hover,
|
|
a:focus {
|
|
text-decoration: none;
|
|
}
|
|
|
|
/************************************************************************
|
|
* Navigation (Sticky Top Bar)
|
|
************************************************************************/
|
|
nav {
|
|
background: rgba(0, 0, 0, 0.5);
|
|
backdrop-filter: blur(6px);
|
|
padding: 10px 0;
|
|
text-align: center;
|
|
position: sticky;
|
|
top: 0;
|
|
/* <b style='color: #FFCC00;'>WICHTIG:</b>: z-index erhöhen, damit das X-Icon klickbar bleibt */
|
|
z-index: 1000;
|
|
position: relative; /* Basis für absolute Positionierung von Kindern */
|
|
}
|
|
|
|
/* Wrapper für die Navigationslinks */
|
|
.nav-links {
|
|
display: inline;
|
|
}
|
|
|
|
/* Desktop Navigationslinks */
|
|
.nav-links a {
|
|
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;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Desktop Hover/Focus/Active States */
|
|
.nav-links a:hover,
|
|
.nav-links a:focus,
|
|
.nav-links a:active,
|
|
.nav-links a.active {
|
|
background: var(--accent-color);
|
|
color: #000000;
|
|
}
|
|
|
|
/* Hamburger-Icon / Close-Icon Styling */
|
|
.hamburger {
|
|
display: none;
|
|
font-size: 30px; /* Etwas größer für bessere Klickbarkeit */
|
|
font-weight: bold; /* Macht das 'X' oft etwas klarer */
|
|
line-height: 1;
|
|
position: absolute;
|
|
/* NEU: Anpassung - weiter nach unten verschoben */
|
|
top: calc(50% + 25px); /* Passe '25px' nach Bedarf an */
|
|
transform: translateY(-50%);
|
|
right: 20px;
|
|
color: var(--text-color);
|
|
cursor: pointer;
|
|
padding: 5px; /* Etwas Padding für größere Klickfläche */
|
|
z-index: 1001; /* Sicherstellen, dass der Button über dem Overlay liegt */
|
|
}
|
|
.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;
|
|
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: 10px 0 5px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/************************************************************************
|
|
* Eingabefelder (Inputs, Select, Textarea) MIT FOKUS-EFFEKT
|
|
************************************************************************/
|
|
input[type="text"],
|
|
input[type="password"],
|
|
select,
|
|
textarea {
|
|
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: 1px solid #555; /* Standard-Rand */
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
box-shadow: inset 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
|
}
|
|
|
|
/* Fokus-Effekt FÜR ALLE INPUTS/SELECT/TEXTAREA */
|
|
input[type="text"]:focus,
|
|
input[type="password"]:focus,
|
|
select:focus,
|
|
textarea:focus {
|
|
outline: none;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
/* Orangener Rand und Schein bei Fokus */
|
|
border-color: var(--accent-color, #d89904);
|
|
box-shadow: 0 0 8px rgba(216, 153, 4, 0.3);
|
|
}
|
|
|
|
/************************************************************************
|
|
* Buttons
|
|
************************************************************************/
|
|
input[type="submit"],
|
|
button {
|
|
background: var(--accent-color);
|
|
color: #000000;
|
|
border: none;
|
|
border-radius: 30px;
|
|
padding: 10px 20px;
|
|
margin-top: 12px;
|
|
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 - MOBILE OVERLAY HIER IMPLEMENTIERT
|
|
************************************************************************/
|
|
@media (max-width: 768px) {
|
|
.hamburger {
|
|
display: block; /* Hamburger anzeigen */
|
|
}
|
|
|
|
/* Der Link-Wrapper wird zum absolut positionierten Overlay */
|
|
.nav-links {
|
|
display: none;
|
|
position: absolute;
|
|
top: 100%; /* Unter der Nav-Leiste */
|
|
left: 0;
|
|
width: 100%;
|
|
background: rgba(26, 26, 26, 0.95);
|
|
backdrop-filter: blur(8px);
|
|
padding: 10px 0;
|
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
/* <b style='color: #FFCC00;'>WICHTIG:</b>: z-index niedriger als der Hamburger/Close-Button */
|
|
z-index: 1000;
|
|
}
|
|
|
|
/* Wenn Menü aktiv, Wrapper sichtbar */
|
|
nav.active .nav-links {
|
|
display: block;
|
|
}
|
|
|
|
/* Styling der Links im mobilen Overlay */
|
|
nav .nav-links a {
|
|
display: block;
|
|
margin: 0;
|
|
padding: 15px 25px;
|
|
text-align: center; /* Links zentrieren */
|
|
border-radius: 0;
|
|
color: #eee;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
nav .nav-links a:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* Hover/Active für mobile Links */
|
|
nav .nav-links a:hover,
|
|
nav .nav-links a:active,
|
|
nav .nav-links a.active {
|
|
background: var(--accent-color);
|
|
color: #000000;
|
|
}
|
|
|
|
} /* Ende @media (max-width: 768px) */
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
h1 {
|
|
font-size: 1.4rem;
|
|
}
|
|
/* Buttons auf 100% Breite */
|
|
input[type="submit"],
|
|
button {
|
|
width: 100%;
|
|
max-width: none;
|
|
}
|
|
/* Eingabefelder evtl. auch anpassen */
|
|
input[type="text"],
|
|
input[type="password"],
|
|
select,
|
|
textarea {
|
|
max-width: 90%;
|
|
min-width: 150px;
|
|
}
|
|
} /* Ende @media (max-width: 480px) */
|
|
|
|
</style>
|
|
<script>
|
|
// NEU: Angepasste JavaScript-Funktion für Icon-Wechsel
|
|
function toggleMenu() {
|
|
var nav = document.querySelector('nav');
|
|
var hamburger = document.querySelector('.hamburger');
|
|
nav.classList.toggle('active');
|
|
|
|
// Prüfen, ob das Menü JETZT aktiv ist und Icon entsprechend ändern
|
|
if (nav.classList.contains('active')) {
|
|
hamburger.innerHTML = '×'; // Ändert zu einem 'X' (Schließen-Symbol)
|
|
} else {
|
|
hamburger.innerHTML = '☰'; // Ändert zurück zum Hamburger-Symbol
|
|
}
|
|
}
|
|
|
|
// OPTIONAL: Schließen des Menüs bei Klick außerhalb (Verbessert Usability)
|
|
document.addEventListener('click', function(event) {
|
|
var nav = document.querySelector('nav');
|
|
var hamburger = document.querySelector('.hamburger');
|
|
var navLinks = document.querySelector('.nav-links');
|
|
|
|
// Prüfen ob Menü überhaupt offen ist
|
|
if (nav.classList.contains('active')) {
|
|
// Prüfen ob Klick außerhalb des Menüs UND außerhalb des Buttons war
|
|
var isClickInsideNavLinks = navLinks.contains(event.target);
|
|
var isClickOnHamburger = hamburger.contains(event.target);
|
|
|
|
if (!isClickInsideNavLinks && !isClickOnHamburger) {
|
|
toggleMenu(); // Schließe das Menü
|
|
}
|
|
}
|
|
});
|
|
|
|
</script>
|
|
)rawliteral";
|
|
|
|
|
|
/************************************************************************************
|
|
* Gemeinsame Navigation (PROGMEM) - Unverändert
|
|
************************************************************************************/
|
|
|
|
static const char commonNav[] PROGMEM = R"rawliteral(
|
|
<nav>
|
|
<span class="hamburger" onclick="toggleMenu()">☰</span>
|
|
<div class="nav-links"> <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>
|
|
</div> </nav>
|
|
)rawliteral";
|
|
|
|
/************************************************************************************
|
|
* WiFi-Konfigurationsstrukturen und Funktionen - Unverändert
|
|
************************************************************************************/
|
|
|
|
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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
// === PROGMEM Teile definieren (angepasst für Signalstärke) ===
|
|
static const char wifiPageChunk1[] 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 wifiPageChunk2[] PROGMEM = R"rawliteral(</head>)rawliteral"; // Head Ende
|
|
|
|
// Teil 3a: Body bis VOR Signalstärke-Wert
|
|
static const char wifiPageChunk3_a[] PROGMEM = R"rawliteral(
|
|
<body><h1>WLAN-Konfiguration</h1><form action='/saveWiFiConfig' method='POST'><h3>WLAN-Einstellungen</h3><div style='margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid rgba(255,255,255,0.1);'><label style='margin-right: 5px;'>Aktuelle Signalstärke:</label><span>)rawliteral"; // Endet vor dem Wert
|
|
|
|
// Teil 3b: NACH Signalstärke-Wert bis VOR SSID Value
|
|
static const char wifiPageChunk3_b[] PROGMEM = R"rawliteral(</span></div><label for='ssid'>SSID:</label><input type='text' id='ssid' name='ssid' value=')rawliteral"; // Endet mit value='
|
|
|
|
// Restliche Teile wie in der funktionierenden Version v2
|
|
static const char wifiPageChunk4[] PROGMEM = R"rawliteral('' required><label for='password'>Passwort:</label><input type='text' id='password' name='password' value=')rawliteral"; // Beginnt mit ', endet mit value='
|
|
static const char wifiPageChunk5_format[] PROGMEM = R"rawliteral(''><h3>IP-Einstellungen</h3><label><input type='radio' name='ipType' value='dhcp' %s> DHCP</label><label><input type='radio' name='ipType' value='static' %s> Statische IP</label><div id='staticFields' style='display:%s'>)rawliteral"; // Beginnt mit '', endet mit display:%s'>
|
|
static const char wifiPageChunk6[] PROGMEM = R"rawliteral(<label for='ip'>IP-Adresse:</label><input type='text' id='ip' name='ip' value=')rawliteral"; // Beginnt normal, endet mit value='
|
|
static const char wifiPageChunk7[] PROGMEM = R"rawliteral('><label for='gateway'>Gateway:</label><input type='text' id='gateway' name='gateway' value=')rawliteral"; // Beginnt mit ', endet mit value='
|
|
static const char wifiPageChunk8[] PROGMEM = R"rawliteral('><label for='subnet'>Subnetzmaske:</label><input type='text' id='subnet' name='subnet' value=')rawliteral"; // Beginnt mit ', endet mit value='
|
|
static const char wifiPageChunk9[] PROGMEM = R"rawliteral(''></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><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"; // Beginnt mit '', Rest bis Ende
|
|
|
|
// --- Hilfsfunktion zur Umrechnung von RSSI in Prozent (Approximation) ---
|
|
int calculateSignalPercentage(int rssi) {
|
|
int quality = 0;
|
|
if (rssi <= -100) {
|
|
quality = 0;
|
|
} else if (rssi >= -50) { // Guter Empfang
|
|
quality = 100;
|
|
} else {
|
|
// Lineare Skalierung zwischen -100dBm (0%) und -50dBm (100%)
|
|
quality = 2 * (rssi + 100);
|
|
}
|
|
return quality;
|
|
}
|
|
|
|
|
|
void handleWiFiConfig() {
|
|
Serial.println(F("Executing handleWiFiConfig (v3: Fewer Chunks, Handle Empty, Signal Strength)"));
|
|
|
|
// EEPROM Lese-Logik
|
|
WiFiConfig currentConfig;
|
|
char magic[5] = { 0 };
|
|
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
|
|
bool configValid = (strncmp(magic, storageMagicValue, 4) == 0);
|
|
if (configValid) {
|
|
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, currentConfig);
|
|
currentConfig.ssid[sizeof(currentConfig.ssid)-1] = '\0';
|
|
currentConfig.password[sizeof(currentConfig.password)-1] = '\0';
|
|
} 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);
|
|
}
|
|
Serial.println(F("EEPROM logic done."));
|
|
|
|
// --- Signalstärke ermitteln ---
|
|
char signalBuffer[50]; // Puffer für den Anzeigetext
|
|
if (WiFi.getMode() == WIFI_STA && WiFi.status() == WL_CONNECTED) {
|
|
long rssi = WiFi.RSSI(); // Signalstärke in dBm holen
|
|
int percentage = calculateSignalPercentage(rssi);
|
|
// Text formatieren (z.B. "-65 dBm (70%)")
|
|
snprintf(signalBuffer, sizeof(signalBuffer), "%d dBm (%d%%)", (int)rssi, percentage);
|
|
Serial.print(F("WiFi Signal: ")); Serial.println(signalBuffer);
|
|
} else if (WiFi.getMode() == WIFI_AP) {
|
|
strcpy(signalBuffer, "AP-Modus (keine Messung)");
|
|
Serial.println(F("WiFi Signal: AP Mode"));
|
|
} else {
|
|
strcpy(signalBuffer, "Nicht verbunden");
|
|
Serial.println(F("WiFi Signal: Not Connected"));
|
|
}
|
|
// --- Ende Signalstärke ---
|
|
|
|
|
|
// Temporäre Puffer für Senden
|
|
char formatBuffer[256]; // Für snprintf_P
|
|
char ipBuffer[16];
|
|
|
|
// ---- Beginn des Sendens in WENIGER Chunks ----
|
|
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
|
server.send(200, F("text/html"), "");
|
|
Serial.println("Sent header");
|
|
|
|
// Chunk 1: HTML-Start
|
|
server.sendContent_P(wifiPageChunk1);
|
|
// Chunk 1.5: Common Style (global)
|
|
server.sendContent_P(commonStyle);
|
|
// Chunk 2: Head Ende
|
|
server.sendContent_P(wifiPageChunk2);
|
|
// Chunk 2.5: Common Nav (global)
|
|
server.sendContent_P(commonNav);
|
|
// Chunk 3a: Body bis VOR Signalwert
|
|
server.sendContent_P(wifiPageChunk3_a); // Endet mit <span>
|
|
// Chunk 3.1: Signalwert senden
|
|
server.sendContent(signalBuffer); // Sendet den formatierten Signal-String
|
|
// Chunk 3b: NACH Signalwert bis VOR SSID Value
|
|
server.sendContent_P(wifiPageChunk3_b); // Endet mit value='
|
|
Serial.println("Sent initial chunks incl. Signal Strength");
|
|
|
|
// Chunk 4: SSID einfügen (Handle Empty)
|
|
if (strlen(currentConfig.ssid) == 0) {
|
|
server.sendContent(F("SSID eingeben"));
|
|
} else {
|
|
server.sendContent(currentConfig.ssid);
|
|
}
|
|
// Chunk 5: Nach SSID bis vor Passwort
|
|
server.sendContent_P(wifiPageChunk4); // Beginnt mit ', endet mit value='
|
|
// Chunk 6: Passwort einfügen (Handle Empty)
|
|
if (!demoModus) {
|
|
if (strlen(currentConfig.password) == 0) {
|
|
server.sendContent(F("Passwort eingeben"));
|
|
} else {
|
|
server.sendContent(currentConfig.password);
|
|
}
|
|
} else {
|
|
server.sendContent(F("*** *** *** ***"));
|
|
}
|
|
Serial.println("Sent SSID and Password chunks");
|
|
|
|
// Chunk 7: Radiobuttons und Div-Start (formatiert)
|
|
snprintf_P(formatBuffer, sizeof(formatBuffer), wifiPageChunk5_format,
|
|
currentConfig.useStaticIP ? "" : "checked",
|
|
currentConfig.useStaticIP ? "checked" : "",
|
|
currentConfig.useStaticIP ? "block" : "none"
|
|
);
|
|
server.sendContent(formatBuffer);
|
|
Serial.println("Sent combined Chunk 7 (IP Select Radios + Div Start)");
|
|
|
|
// === IP Felder (wie zuvor) ===
|
|
server.sendContent_P(wifiPageChunk6);
|
|
snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.staticIP[0], currentConfig.staticIP[1], currentConfig.staticIP[2], currentConfig.staticIP[3]);
|
|
server.sendContent(ipBuffer);
|
|
server.sendContent_P(wifiPageChunk7);
|
|
snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.gateway[0], currentConfig.gateway[1], currentConfig.gateway[2], currentConfig.gateway[3]);
|
|
server.sendContent(ipBuffer);
|
|
server.sendContent_P(wifiPageChunk8);
|
|
snprintf(ipBuffer, sizeof(ipBuffer), "%d.%d.%d.%d", currentConfig.subnet[0], currentConfig.subnet[1], currentConfig.subnet[2], currentConfig.subnet[3]);
|
|
server.sendContent(ipBuffer);
|
|
Serial.println("Sent IP fields");
|
|
|
|
// Chunk 9: Rest der Seite
|
|
server.sendContent_P(wifiPageChunk9);
|
|
Serial.println("Sent Rest of Page");
|
|
|
|
// Letzter Chunk zum Abschließen
|
|
server.sendContent(F(""));
|
|
Serial.println("Sent final empty chunk");
|
|
Serial.println(F("Finished sending handleWiFiConfig (v3 Final)"));
|
|
// ---- Ende des Sendens ----
|
|
}
|
|
|
|
// Handler zum Speichern der WLAN-Konfiguration - Unverändert
|
|
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 - Unverändert
|
|
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) - Unverändert
|
|
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 - Unverändert
|
|
void drawwifiSymbol(int16_t x, int16_t y)
|
|
{
|
|
display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler zum Rücksetzen der Betriebszeit - Unverändert
|
|
************************************************************************************/
|
|
|
|
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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
void handleResetShots()
|
|
{
|
|
shotCounter = 0;
|
|
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
|
|
EEPROM.commit();
|
|
server.sendHeader(F("Location"), F("/info"));
|
|
server.send(303);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler für den Export der EEPROM-Einstellungen - Unverändert
|
|
************************************************************************************/
|
|
void handleExportSettings() {
|
|
Serial.println("Anfrage zum Export der EEPROM-Einstellungen empfangen.");
|
|
|
|
// Header für Dateidownload setzen
|
|
server.sendHeader("Content-Disposition", "attachment; filename=\"dual_pid_settings.bin\"");
|
|
server.sendHeader("Content-Type", "application/octet-stream");
|
|
server.setContentLength(EEPROM_SIZE); // Wichtig: Korrekte Größe senden
|
|
|
|
// EEPROM-Inhalt byteweise senden
|
|
// Puffergröße klein halten, um Speicher zu sparen
|
|
const size_t bufferSize = 64;
|
|
byte buffer[bufferSize];
|
|
size_t bytesSent = 0;
|
|
|
|
// Start des Sendens (Status 200 OK)
|
|
server.send(200, "application/octet-stream", "");
|
|
|
|
// EEPROM lesen und in Chunks senden
|
|
for (int i = 0; i < EEPROM_SIZE; i += bufferSize) {
|
|
size_t currentChunkSize = (EEPROM_SIZE - i < bufferSize) ? (EEPROM_SIZE - i) : bufferSize;
|
|
for (size_t j = 0; j < currentChunkSize; j++) {
|
|
buffer[j] = EEPROM.read(i + j);
|
|
}
|
|
server.sendContent((const char*)buffer, currentChunkSize);
|
|
bytesSent += currentChunkSize;
|
|
yield(); // Wichtig für Netzwerkstabilität bei längeren Operationen
|
|
}
|
|
|
|
Serial.printf("EEPROM-Export abgeschlossen. %d Bytes gesendet.\n", bytesSent);
|
|
// Kein server.stop() hier, da die Verbindung automatisch geschlossen wird.
|
|
}
|
|
|
|
|
|
/************************************************************************************
|
|
* Handler für den Import der EEPROM-Einstellungen (Upload-Verarbeitung) - Unverändert
|
|
************************************************************************************/
|
|
// Globale Variablen bleiben gleich
|
|
static int eepromWriteAddress = 0;
|
|
static bool importSuccessful = false;
|
|
static String importStatusMessage = "";
|
|
|
|
void handleImportUpload() {
|
|
HTTPUpload& upload = server.upload();
|
|
|
|
// Demo-Modus Prüfung (Bleibt bestehen)
|
|
if (demoModus) {
|
|
if (upload.status == UPLOAD_FILE_START) {
|
|
Serial.println("FEHLER: EEPROM-Import im Demo-Modus nicht erlaubt!");
|
|
importStatusMessage = F("Import im Demo-Modus nicht verfügbar!");
|
|
importSuccessful = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (upload.status) {
|
|
case UPLOAD_FILE_START:
|
|
Serial.printf("EEPROM Import gestartet: %s\n", upload.filename.c_str());
|
|
// Optional: Debug-Ausgabe kann bleiben, um zu sehen, was gemeldet wird
|
|
Serial.printf("Info: Gemeldete upload.contentLength (kann Overhead enthalten): %u\n", upload.contentLength);
|
|
Serial.printf("Info: Erwartete Nutzdaten-Größe (EEPROM_SIZE): %d\n", EEPROM_SIZE);
|
|
|
|
eepromWriteAddress = 0; // Zähler zurücksetzen
|
|
importSuccessful = false; // Status zurücksetzen
|
|
importStatusMessage = ""; // Nachricht zurücksetzen
|
|
|
|
// !!!!! KEINE GRÖSSENPRÜFUNG MEHR HIER !!!!!
|
|
Serial.println("Starte Empfang der Upload-Daten...");
|
|
break; // Wichtig: break bleibt
|
|
|
|
case UPLOAD_FILE_WRITE:
|
|
// Prüfen, ob bereits ein Fehler aufgetreten ist (z.B. durch zu viele Daten im vorherigen Chunk)
|
|
if (importStatusMessage.length() > 0) {
|
|
Serial.println("Ignoriere weitere Schreibdaten wegen vorherigem Fehler.");
|
|
yield();
|
|
break; // Beende die Verarbeitung für diesen Status
|
|
}
|
|
|
|
// Schreibe die empfangenen Bytes, aber schütze vor Überlauf
|
|
// Serial.printf("Schreibe %d Bytes nach EEPROM Adresse %d\n", upload.currentSize, eepromWriteAddress);
|
|
for (size_t i = 0; i < upload.currentSize; i++) {
|
|
// Prüfen, ob das Schreiben das EEPROM_SIZE Limit überschreiten würde
|
|
if (eepromWriteAddress < EEPROM_SIZE) {
|
|
EEPROM.write(eepromWriteAddress, upload.buf[i]);
|
|
eepromWriteAddress++;
|
|
} else {
|
|
// Es kommen MEHR Daten als erwartet!
|
|
Serial.printf("FEHLER: Mehr als %d Bytes empfangen! Adresse: %d. Breche ab.\n", EEPROM_SIZE, eepromWriteAddress);
|
|
importStatusMessage = F("Importfehler: Empfangene Datei zu groß!"); // 'ß' für ß
|
|
// Breche die Verarbeitung dieses Chunks und zukünftiger Chunks ab
|
|
goto end_write_processing; // Springe aus der Schleife und dem Case
|
|
}
|
|
}
|
|
end_write_processing:; // Sprungziel für goto
|
|
yield();
|
|
break;
|
|
|
|
case UPLOAD_FILE_END:
|
|
Serial.println("EEPROM Import Upload-Ende erreicht.");
|
|
Serial.printf("Insgesamt verarbeitete Bytes: %d\n", eepromWriteAddress); // Wichtige Info
|
|
|
|
// FINALE GRÖSSENPRÜFUNG und Fehlerprüfung
|
|
|
|
// 1. Gab es einen Fehler während des Schreibens (z.B. Datei zu groß)?
|
|
if (importStatusMessage.length() > 0) {
|
|
Serial.println("Import nicht erfolgreich aufgrund vorheriger Fehler (z.B. Datei zu groß).");
|
|
importSuccessful = false; // Sicherstellen, dass Flag false ist
|
|
}
|
|
// 2. Wurden exakt EEPROM_SIZE Bytes verarbeitet?
|
|
else if (eepromWriteAddress == EEPROM_SIZE) {
|
|
Serial.println("Korrekte Anzahl Bytes empfangen. EEPROM Commit wird durchgeführt...");
|
|
if (EEPROM.commit()) {
|
|
Serial.println("EEPROM Commit erfolgreich.");
|
|
importSuccessful = true;
|
|
importStatusMessage = F("Einstellungen erfolgreich importiert! Neustart wird durchgeführt..."); // 'ü' für ü
|
|
} else {
|
|
Serial.println("FEHLER: EEPROM Commit fehlgeschlagen!");
|
|
importSuccessful = false;
|
|
importStatusMessage = F("Import fehlgeschlagen: EEPROM Commit Fehler!");
|
|
}
|
|
}
|
|
// 3. Wurden weniger oder mehr Bytes verarbeitet (und kein Fehler vorher gesetzt)?
|
|
else {
|
|
Serial.printf("FEHLER: Upload beendet, aber falsche Anzahl Bytes verarbeitet! Verarbeitet: %d, Erwartet: %d\n", eepromWriteAddress, EEPROM_SIZE);
|
|
importSuccessful = false;
|
|
importStatusMessage = F("Import fehlgeschlagen: Falsche Dateigröße!"); // 'ö', 'ß'
|
|
}
|
|
|
|
// Bereinigung für den nächsten Upload
|
|
eepromWriteAddress = 0;
|
|
break;
|
|
|
|
case UPLOAD_FILE_ABORTED:
|
|
Serial.println("EEPROM Import vom Client abgebrochen.");
|
|
importSuccessful = false;
|
|
// Nur Nachricht setzen, wenn nicht schon ein Fehler vorliegt
|
|
if (importStatusMessage.length() == 0) {
|
|
importStatusMessage = F("Import abgebrochen!");
|
|
}
|
|
eepromWriteAddress = 0;
|
|
break;
|
|
|
|
default:
|
|
Serial.printf("Unbekannter Upload-Status: %d\n", upload.status);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler für die Import-Seite (nach dem Upload) - Unverändert
|
|
************************************************************************************/
|
|
void handleImportSettings() {
|
|
// Verwende die Statusvariablen, die von handleImportUpload gesetzt wurden
|
|
|
|
// Diese HTML-Teile werden im Browser angezeigt -> Umlaute ersetzen
|
|
static const char importDoneHtmlStart[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html><html><head><title>Einstellungen Import</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 importDoneHtmlHeadEnd[] PROGMEM = R"rawliteral(</head>)rawliteral";
|
|
static const char importDoneHtmlBody[] PROGMEM = R"rawliteral(
|
|
<body><h1>Einstellungen Import</h1>
|
|
<form><p>{status_message}</p></form>
|
|
</body></html>
|
|
)rawliteral";
|
|
|
|
String htmlResponse = FPSTR(importDoneHtmlStart);
|
|
htmlResponse += FPSTR(commonStyle); // Annahme: commonStyle enthält keine Umlaute/ß für Browserausgabe oder diese sind bereits korrekt kodiert
|
|
htmlResponse += FPSTR(importDoneHtmlHeadEnd);
|
|
htmlResponse += FPSTR(commonNav); // Annahme: commonNav enthält keine Umlaute/ß für Browserausgabe oder diese sind bereits korrekt kodiert
|
|
String body = FPSTR(importDoneHtmlBody);
|
|
|
|
body.replace(F("{status_message}"), importStatusMessage); // Nachricht einfügen (Umlaute wurden bereits in handleImportUpload ersetzt)
|
|
|
|
htmlResponse += body;
|
|
|
|
server.send(200, F("text/html"), htmlResponse);
|
|
|
|
// Wenn erfolgreich, Neustart nach kurzer Verzögerung
|
|
if (importSuccessful) {
|
|
Serial.println("Neustart in 5 Sekunden nach EEPROM-Import...");
|
|
delay(5000);
|
|
ESP.restart();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Variable für Systemstartzeit (relevant für die Dampf-Verzögerung) - Unverändert
|
|
************************************************************************************/
|
|
|
|
unsigned long startupTime;
|
|
|
|
/************************************************************************************
|
|
* setup(): Initialisierung des Systems
|
|
************************************************************************************/
|
|
|
|
void setup()
|
|
{
|
|
Serial.begin(115200);
|
|
EEPROM.begin(EEPROM_SIZE); // Größe angepasst
|
|
|
|
pinMode(SSR_DAMPF_PIN, OUTPUT);
|
|
pinMode(SSR_WASSER_PIN, OUTPUT);
|
|
pinMode(SHOT_TIMER_PIN, INPUT_PULLUP);
|
|
|
|
// Initialisiere SSRs als AUS
|
|
digitalWrite(SSR_DAMPF_PIN, LOW);
|
|
digitalWrite(SSR_WASSER_PIN, LOW);
|
|
|
|
// 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 Konfiguration ---
|
|
// Setze Output-Grenzen auf die Fenstergröße für zeitproportionale Steuerung
|
|
pidWasser.SetOutputLimits(0, windowSizeWasser);
|
|
pidDampf.SetOutputLimits(0, windowSizeDampf);
|
|
|
|
// Setze PID-Parameter
|
|
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
|
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
|
|
|
// Setze PID-Modus auf AUTOMATIC
|
|
pidDampf.SetMode(AUTOMATIC);
|
|
pidWasser.SetMode(AUTOMATIC);
|
|
|
|
// Initialisiere Startzeiten für die Zeitfenster
|
|
windowStartTimeWasser = millis();
|
|
windowStartTimeDampf = millis();
|
|
|
|
// 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 (Unverändert, außer AutoTune-Parameter-Seite)
|
|
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ür den PID-Regler (Proportional-, Integral- und Differentialanteil) selbstä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ünschte Kriterien wie kurze Einschwingzeit, geringe Überschwingung und stabile Regelung erreicht werden"
|
|
"<br><br>"
|
|
"Ein AutoTune-Vorgang kann durchaus 10-20 Minuten in Anspruch nehmen!"
|
|
"<br><br>"
|
|
"<b style='color: #FFCC00;'>WICHTIG:</b><br>"
|
|
"Vor dem Start des PID-Tunings sollte die Maschine die gewünschte Betriebstemperatur erreicht haben und stabil sein.<br>"
|
|
"Andernfalls könnten die automatisch ermittelten PID-Werte für diesen Temperaturbereich ungenau oder nicht optimal sein."
|
|
"<br><br>"
|
|
"</form>"
|
|
"<form action='/autoTuneWasser' method='POST' >\n"
|
|
"<h3>Automatisches PID-Tuning für Wasser</h3>\n"
|
|
"<input type='submit' value='Tuning starten'>\n"
|
|
"</form>\n"
|
|
"<form action='/autoTuneDampf' method='POST' >\n"
|
|
"<h3>Automatisches PID-Tuning für Dampf</h3>\n"
|
|
"<input type='submit' value='Tuning starten'>\n"
|
|
"</form>\n");
|
|
|
|
// Form für AutoTune-Parameter
|
|
// HINWEIS: Die Bedeutung von 'Step' und 'StartValue' ändert sich bei Zeitproportional!
|
|
// 'Step' ist der Betrag, um den der Output (0-windowSize) geändert wird.
|
|
// 'StartValue' ist der initiale Output (0-windowSize).
|
|
page += F("<form action='/updateAutotuneSettings' method='POST'>\n"
|
|
"<h3>AutoTune-Parameter (Zeitproportional)</h3>\n" // Titel angepasst
|
|
"Hier können die AutoTune-Parameter geändert werden.<br>Änderungen sollten mit Vorsicht durchgeführt werden!<br><br>"
|
|
"<h3>Parameter Wasser / Kessel</h3>"
|
|
"<label for='tuningStepWasser'>Step (Output 0-{WINDOW_SIZE_WASSER}):</label>" // Beschreibung angepasst
|
|
"<input type='text' id='tuningStepWasser' name='tuningStepWasser' value='{TUNING_STEP_WASSER}'>"
|
|
"<label for='tuningNoiseWasser'>Noise (°C):</label>" // Einheit hinzugefügt
|
|
"<input type='text' id='tuningNoiseWasser' name='tuningNoiseWasser' value='{TUNING_NOISE_WASSER}'>"
|
|
"<label for='tuningStartValueWasser'>StartValue (Output 0-{WINDOW_SIZE_WASSER}):</label>" // Beschreibung angepasst
|
|
"<input type='text' id='tuningStartValueWasser' name='tuningStartValueWasser' value='{TUNING_STARTVALUE_WASSER}'>"
|
|
"<label for='tuningLookBackWasser'>LookBack (Sekunden):</label>"
|
|
"<input type='text' id='tuningLookBackWasser' name='tuningLookBackWasser' value='{TUNING_LOOKBACK_WASSER}'>"
|
|
|
|
"<br><h3>Parameter Dampf / Thermoblock</h3>"
|
|
"<label for='tuningStepDampf'>Step (Output 0-{WINDOW_SIZE_DAMPF}):</label>" // Beschreibung angepasst
|
|
"<input type='text' id='tuningStepDampf' name='tuningStepDampf' value='{TUNING_STEP_DAMPF}'>"
|
|
"<label for='tuningNoiseDampf'>Noise (°C):</label>" // Einheit hinzugefügt
|
|
"<input type='text' id='tuningNoiseDampf' name='tuningNoiseDampf' value='{TUNING_NOISE_DAMPF}'>"
|
|
"<label for='tuningStartValueDampf'>StartValue (Output 0-{WINDOW_SIZE_DAMPF}):</label>" // Beschreibung angepasst
|
|
"<input type='text' id='tuningStartValueDampf' name='tuningStartValueDampf' value='{TUNING_STARTVALUE_DAMPF}'>"
|
|
"<label for='tuningLookBackDampf'>LookBack (Sekunden):</label>"
|
|
"<input type='text' id='tuningLookBackDampf' name='tuningLookBackDampf' value='{TUNING_LOOKBACK_DAMPF}'>"
|
|
"<input type='submit' value='Parameter speichern'>\n"
|
|
"</form>\n"
|
|
"<br><hr><br>"
|
|
"<form>"
|
|
"<h3>Erklärung der Parameter (Zeitproportional):</h3>\n" // Titel angepasst
|
|
"<p><strong>Step:</strong><br>\n"
|
|
"Dieser Wert bestimmt, wie stark der Ausgangswert (innerhalb des Bereichs 0 bis 2000ms) während des AutoTune-Vorgangs verändert wird. Er gibt die Größe des Impulses an, der die Einschaltdauer des SSRs beeinflusst.</p>\n"
|
|
"<p><strong>Noise:</strong><br>\n"
|
|
"Dieser Parameter definiert einen Toleranzbereich (Noise-Band) für die gemessenen Eingangstemperaturen (°C). Kleine Schwankungen unterhalb dieses Wertes werden ignoriert.</p>\n"
|
|
"<p><strong>StartValue:</strong><br>\n"
|
|
"Hier wird der Anfangswert für den Ausgang des Reglers (0 bis 2000ms) festgelegt, wenn der AutoTune-Vorgang startet. Dieser Wert bestimmt die initiale Einschaltdauer des SSRs.</p>\n"
|
|
"<p><strong>LookBack:</strong><br>\n"
|
|
"Dieser Parameter legt fest, über welchen Zeitraum (in Sekunden) der Algorithmus die vergangene Systemantwort analysiert, um die Schwingungsperiode zu messen.</p>\n"
|
|
"</form>"
|
|
"</body>\n</html>");
|
|
|
|
// --- Placeholders ersetzen ---
|
|
page.replace("{WINDOW_SIZE_WASSER}", String(windowSizeWasser)); // NEU
|
|
page.replace("{WINDOW_SIZE_DAMPF}", String(windowSizeDampf)); // NEU
|
|
page.replace("{TUNING_STEP_WASSER}", String(tuningStepWasser, 1));
|
|
page.replace("{TUNING_NOISE_WASSER}", String(tuningNoiseWasser, 1));
|
|
page.replace("{TUNING_STARTVALUE_WASSER}", String(tuningStartValueWasser, 1));
|
|
page.replace("{TUNING_LOOKBACK_WASSER}", String(tuningLookBackWasser));
|
|
|
|
page.replace("{TUNING_STEP_DAMPF}", String(tuningStepDampf, 1));
|
|
page.replace("{TUNING_NOISE_DAMPF}", String(tuningNoiseDampf, 1));
|
|
page.replace("{TUNING_STARTVALUE_DAMPF}", String(tuningStartValueDampf, 1));
|
|
page.replace("{TUNING_LOOKBACK_DAMPF}", String(tuningLookBackDampf));
|
|
|
|
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 - Unverändert
|
|
server.on("/charts", handleCharts);
|
|
|
|
// JSON-Daten für Temperaturwerte - Unverändert
|
|
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 - Unverändert
|
|
server.on("/updateFirmware", HTTP_GET, []() {
|
|
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>Firmware & Einstellungen</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"); // Titel angepasst
|
|
page += FPSTR(commonStyle);
|
|
page += F("</head>\n");
|
|
page += FPSTR(commonNav);
|
|
// ----- HIER BEGINNT DER GROSSE F() BLOCK -----
|
|
page += F("<body>\n"
|
|
"<h1>Firmware & Einstellungen</h1>\n" // Titel angepasst
|
|
// --- Firmware Info Sektion (unverändert) ---
|
|
"<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"
|
|
// --- Firmware Update Sektion (unverändert) ---
|
|
"<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ührt werden.<br>\n"
|
|
"Nach dem Update wird ein automatischer Neustart durchgefü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' accept='.bin'>\n" // accept hinzugefügt
|
|
"<br>"
|
|
"<button>Update starten</button>\n"
|
|
"</form>\n"
|
|
|
|
// --- NEUE SEKTION: Einstellungen Import/Export ---
|
|
|
|
// Export
|
|
"<form>\n"
|
|
"<h3>Einstellungen exportieren</h3>"
|
|
"Sicherung aller aktuellen Einstellungen (gesamter EEPROM-Inhalt) in einer Datei.<br>\n"
|
|
"<a href='/exportSettings' download='dual_pid_settings.bin' style='display: inline-block; margin-top: 10px; background: var(--accent-color); color: #000; border-radius: 30px; padding: 10px 20px; text-decoration: none; font-weight: 600; box-shadow: 0 4px 12px rgba(216, 153, 4, 0.3); transition: all 0.3s ease;' onmouseover='this.style.opacity=\"0.85\"; this.style.transform=\"translateY(-2px)\";' onmouseout='this.style.opacity=\"1\"; this.style.transform=\"translateY(0)\";'>Einstellungen Exportieren (.bin)</a>\n"
|
|
"</form>\n"
|
|
|
|
// Import
|
|
"<form method='POST' action='/importSettings' enctype='multipart/form-data'>\n"
|
|
"<h3>Einstellungen importieren</h3>"
|
|
"Alle Einstellungen aus einer zuvor exportierten .bin-Datei wiederherstellen.<br><br>\n"
|
|
"<b style='color: #FFCC00;'>ACHTUNG:</b><br>"
|
|
"Dieser Vorgang überschreibt ALLE aktuellen Einstellungen im EEPROM!<br>\n"
|
|
"Die Datei muss exakt <b>{EEPROM_SIZE_BYTES} Bytes</b> groß sein.<br>\n"
|
|
// ******** ENDE DER ÄNDERUNG *********
|
|
"Nach dem Import wird das Gerät automatisch neu gestartet.<br>\n"
|
|
"<br>\n"
|
|
"<input type='file' name='settings' accept='.bin' required>\n" // accept und required hinzugefügt
|
|
"<br>\n"
|
|
"<button>Import & Neustart</button>\n"
|
|
"</form>\n"
|
|
// --- ENDE NEUE SEKTION ---
|
|
|
|
"</body>\n</html>");
|
|
// ----- HIER ENDET DER GROSSE F() BLOCK -----
|
|
|
|
// --- Platzhalter ersetzen (NACHDEM der F()-Block hinzugefügt wurde) ---
|
|
page.replace(F("{FIRMWAREVERSION}"), version);
|
|
page.replace(F("{SWHERSTELLER}"), versionHersteller);
|
|
page.replace(F("{SWHERSTELLERMAIL}"), versionHerstellerMail);
|
|
page.replace(F("{SWHERSTELLERWEBSITE}"), versionHerstellerWeb);
|
|
// ******** HIER DIE NEUE ERSETZUNG *********
|
|
page.replace(F("{EEPROM_SIZE_BYTES}"), String(EEPROM_SIZE));
|
|
// ******** ENDE DER NEUEN ERSETZUNG *********
|
|
|
|
server.send(200, F("text/html"), page);
|
|
});
|
|
|
|
// Firmware-Upload-Handler - Unverändert
|
|
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>Weiterleitung zur Startseite in 30 Sekunden ...</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 - Unverändert
|
|
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 - Unverändert
|
|
server.on("/netzwerk", handleWiFiConfig);
|
|
server.on("/saveWiFiConfig", handleSaveWiFiConfig);
|
|
server.on("/forceAPMode", handleForceAPMode);
|
|
|
|
// Routen zum Zurücksetzen von Betriebszeit und ShotCounter - Unverändert
|
|
server.on("/resetRuntime", handleResetRuntime);
|
|
server.on("/resetShots", handleResetShots);
|
|
|
|
// Routen für Profile - Unverändert
|
|
server.on("/profiles", handleProfiles);
|
|
server.on("/saveProfile1", handleSaveProfile1);
|
|
server.on("/loadProfile1", handleLoadProfile1);
|
|
server.on("/saveProfile2", handleSaveProfile2);
|
|
server.on("/loadProfile2", handleLoadProfile2);
|
|
|
|
// Routen für EEPROM Import/Export - Unverändert
|
|
server.on("/exportSettings", HTTP_GET, handleExportSettings);
|
|
server.on("/importSettings", HTTP_POST, handleImportSettings, handleImportUpload);
|
|
|
|
// Webserver starten
|
|
server.begin();
|
|
Serial.println(F("Webserver gestartet"));
|
|
startupTime = millis();
|
|
}
|
|
|
|
/************************************************************************************
|
|
* loop(): Hauptschleife - Implementierung der Zeitproportionalen Steuerung
|
|
************************************************************************************/
|
|
|
|
void loop()
|
|
{
|
|
unsigned long currentMillis = millis();
|
|
|
|
// --- Temperaturmessung und PID-Berechnung (im Intervall) ---
|
|
if (currentMillis - previousMillis >= interval)
|
|
{
|
|
previousMillis = currentMillis;
|
|
|
|
// Temperaturmessung
|
|
InputDampf = round(thermocouple.readCelsius() + OffsetDampf);
|
|
InputWasser = round(readNTCTemperature() + OffsetWasser);
|
|
|
|
// --- Sicherheitsüberprüfung Wasser ---
|
|
if (InputWasser < 0.0 || InputWasser > maxTempWasser || isnan(InputWasser)) // Auch NaN prüfen
|
|
{
|
|
wasserSensorError = true;
|
|
OutputWasser = 0; // PID-Output auf 0 setzen
|
|
Serial.println("WARNUNG: Wassertemperatur ungültig oder außerhalb des Bereichs!");
|
|
}
|
|
else
|
|
{
|
|
wasserSensorError = false;
|
|
// PID nur berechnen, wenn kein Fehler und kein AutoTune aktiv
|
|
if (!autoTuneWasserActive)
|
|
{
|
|
pidWasser.Compute();
|
|
}
|
|
// AutoTune hat seine eigene Logik zum Setzen von OutputWasser
|
|
}
|
|
|
|
// --- Sicherheitsüberprüfung Dampf ---
|
|
if (isnan(InputDampf) || InputDampf < 0.0 || InputDampf > maxTempDampf)
|
|
{
|
|
dampfSensorError = true;
|
|
OutputDampf = 0; // PID-Output auf 0 setzen
|
|
Serial.println("WARNUNG: Dampftemperatur ungültig oder außerhalb des Bereichs!");
|
|
}
|
|
else
|
|
{
|
|
dampfSensorError = false;
|
|
// PID nur berechnen, wenn kein Fehler, kein AutoTune und keine Startverzögerung aktiv
|
|
if (!autoTuneDampfActive)
|
|
{
|
|
// Dampf-Startverzögerung prüfen
|
|
if (dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL))
|
|
{
|
|
// Während der Verzögerung: Heizen verhindern
|
|
OutputDampf = 0; // Sicherstellen, dass PID Output 0 ist
|
|
}
|
|
else
|
|
{
|
|
// Normalbetrieb nach Verzögerung und ohne Sensorfehler
|
|
pidDampf.Compute();
|
|
}
|
|
}
|
|
// AutoTune hat seine eigene Logik zum Setzen von OutputDampf
|
|
}
|
|
|
|
// AutoTune-Logik (wird jetzt nur noch Output setzen, SSR-Steuerung unten)
|
|
handleAutoTune();
|
|
} // Ende des Temperaturmessungs-/PID-Berechnungsintervalls
|
|
|
|
// --- Zeitproportionale SSR-Ansteuerung (wird kontinuierlich geprüft) ---
|
|
unsigned long now = millis();
|
|
|
|
// --- Wasser SSR ---
|
|
if (wasserSensorError)
|
|
{
|
|
digitalWrite(SSR_WASSER_PIN, LOW); // Sicherheit: Bei Fehler immer AUS
|
|
}
|
|
else
|
|
{
|
|
// Fenster neu starten, wenn Zeit abgelaufen
|
|
if (now - windowStartTimeWasser > windowSizeWasser)
|
|
{
|
|
windowStartTimeWasser += windowSizeWasser;
|
|
}
|
|
// SSR einschalten, wenn aktuelle Zeit innerhalb der ON-Phase des Fensters liegt
|
|
// OutputWasser ist jetzt der ON-Zeitraum in ms (0 bis windowSizeWasser)
|
|
if (OutputWasser > 0 && (now < (windowStartTimeWasser + (unsigned long)OutputWasser)))
|
|
{
|
|
digitalWrite(SSR_WASSER_PIN, HIGH);
|
|
}
|
|
else
|
|
{
|
|
digitalWrite(SSR_WASSER_PIN, LOW);
|
|
}
|
|
}
|
|
|
|
// --- Dampf SSR ---
|
|
if (dampfSensorError)
|
|
{
|
|
digitalWrite(SSR_DAMPF_PIN, LOW); // Sicherheit: Bei Fehler immer AUS
|
|
}
|
|
else
|
|
{
|
|
// Fenster neu starten, wenn Zeit abgelaufen
|
|
if (now - windowStartTimeDampf > windowSizeDampf)
|
|
{
|
|
windowStartTimeDampf += windowSizeDampf;
|
|
}
|
|
// SSR einschalten, wenn aktuelle Zeit innerhalb der ON-Phase des Fensters liegt
|
|
// OutputDampf ist jetzt der ON-Zeitraum in ms (0 bis windowSizeDampf)
|
|
// Beachte: OutputDampf kann durch die Startverzögerung oder Fehler auf 0 gesetzt sein
|
|
if (OutputDampf > 0 && (now < (windowStartTimeDampf + (unsigned long)OutputDampf)))
|
|
{
|
|
digitalWrite(SSR_DAMPF_PIN, HIGH);
|
|
}
|
|
else
|
|
{
|
|
digitalWrite(SSR_DAMPF_PIN, LOW);
|
|
}
|
|
}
|
|
|
|
|
|
// 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 der Betriebszeit (NEUE LOGIK) und WiFi-Check - Unverändert
|
|
if (!demoModus) // Bedingung hinzugefügt: Nur speichern, wenn KEIN Demo-Modus aktiv ist
|
|
{
|
|
unsigned long currentMillisLoop = millis(); // Aktuelle Zeit für diese Prüfung holen
|
|
unsigned long intervalToUse;
|
|
|
|
// Bestimme, welches Intervall verwendet werden soll
|
|
if (!initialRuntimeSaveDone) {
|
|
// Wenn das erste Speichern noch nicht erfolgt ist, nutze die 10-Minuten-Verzögerung
|
|
intervalToUse = initialRuntimeSaveDelay;
|
|
} else {
|
|
// Wenn das erste Speichern bereits erfolgt ist, nutze das 5-Minuten-Intervall
|
|
intervalToUse = subsequentRuntimeSaveInterval;
|
|
}
|
|
|
|
// Prüfe, ob das ermittelte Intervall seit dem letzten Speichern abgelaufen ist
|
|
if (currentMillisLoop - lastRuntimeSave >= intervalToUse)
|
|
{
|
|
EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime); // Schreibe die aktuelle Betriebszeit ins EEPROM
|
|
EEPROM.commit(); // Führe das Schreiben physisch durch
|
|
|
|
lastRuntimeSave = currentMillisLoop; // Aktualisiere den Zeitpunkt des letzten Speicherns
|
|
|
|
// Wenn dies das erste Speichern war, setze das Flag
|
|
if (!initialRuntimeSaveDone) {
|
|
initialRuntimeSaveDone = true;
|
|
Serial.println("Betriebszeit: Erstes Speichern nach 10 Minuten erfolgt."); // Optional: Log-Ausgabe
|
|
} else {
|
|
Serial.println("Betriebszeit: Regelmäßiges Speichern (alle 5 Minuten) erfolgt."); // Optional: Log-Ausgabe
|
|
}
|
|
|
|
// Den WiFi-Check können wir auch hier durchführen, da er nun seltener aufgerufen wird
|
|
if (WiFi.getMode() == WIFI_STA) {
|
|
checkWifiConnection();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Liest die Temperatur vom NTC-Sensor (Wasser) über den Spannungsteiler (ADC) - Unverändert
|
|
************************************************************************************/
|
|
|
|
double readNTCTemperature()
|
|
{
|
|
int adcValue = analogRead(A0);
|
|
// Prüfen auf ungültige ADC-Werte (können bei Problemen auftreten)
|
|
if (adcValue <= 0 || adcValue >= 1023) // 1023 ist max. Wert, kann auf Kurzschluss hindeuten
|
|
{
|
|
Serial.print("WARNUNG: Ungültiger ADC-Wert vom NTC-Sensor: ");
|
|
Serial.println(adcValue);
|
|
return NAN; // Not-a-Number als Fehlerwert zurückgeben
|
|
}
|
|
|
|
double sensorVoltage = adcValue * (ADC_REF / 1023.0);
|
|
// Vermeide Division durch Null, falls sensorVoltage == V_SUPPLY
|
|
if (sensorVoltage >= V_SUPPLY) {
|
|
Serial.println("WARNUNG: NTC-Spannung entspricht Versorgungsspannung, R_ntc wäre unendlich.");
|
|
return NAN; // Fehler
|
|
}
|
|
// Vermeide Division durch Null, falls sensorVoltage == 0 (bereits oben abgefangen, aber zur Sicherheit)
|
|
if (sensorVoltage <= 0) {
|
|
Serial.println("WARNUNG: NTC-Spannung ist 0 oder negativ.");
|
|
return NAN; // Fehler
|
|
}
|
|
|
|
double R_ntc = R_FIXED * ((V_SUPPLY / sensorVoltage) - 1.0);
|
|
|
|
// Prüfen auf negativen oder sehr kleinen Widerstandswert
|
|
if (R_ntc <= 0) {
|
|
Serial.print("WARNUNG: Berechneter NTC-Widerstand ungültig: ");
|
|
Serial.println(R_ntc);
|
|
return NAN; // Fehler
|
|
}
|
|
|
|
// Temperaturberechnung über die Beta-Gleichung
|
|
// log() erwartet einen positiven Wert
|
|
double log_R_R0 = log(R_ntc / R0);
|
|
double temp_inv_K = (1.0 / T0) + (1.0 / beta) * log_R_R0;
|
|
|
|
// Prüfen auf Division durch Null bei der Invertierung
|
|
if (temp_inv_K == 0) {
|
|
Serial.println("WARNUNG: Kehrwert der Kelvin-Temperatur ist 0.");
|
|
return NAN; // Fehler
|
|
}
|
|
|
|
double temperatureK = 1.0 / temp_inv_K;
|
|
double temperatureC = temperatureK - 273.15;
|
|
|
|
// Optional: Plausibilitätsprüfung des Ergebnisses
|
|
// if (temperatureC < -50 || temperatureC > 200) { // Beispielgrenzen
|
|
// Serial.print("WARNUNG: Berechnete NTC-Temperatur unplausibel: ");
|
|
// Serial.println(temperatureC);
|
|
// return NAN;
|
|
// }
|
|
|
|
return temperatureC;
|
|
}
|
|
|
|
|
|
/************************************************************************************
|
|
* Aktualisiert das Display (Temperaturanzeige, Shot-Timer etc.) - Unverändert
|
|
************************************************************************************/
|
|
|
|
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) - Unverändert
|
|
************************************************************************************/
|
|
|
|
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 (Angepasst für Zeitproportional)
|
|
************************************************************************************/
|
|
|
|
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);
|
|
|
|
// Sicherstellen, dass die Output-Grenzen korrekt gesetzt sind
|
|
pidWasser.SetOutputLimits(0, windowSizeWasser);
|
|
|
|
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
|
|
// WICHTIG: AutoTune manipuliert OutputWasser direkt. Die zeitproportionale
|
|
// Logik in loop() wird diesen Wert verwenden, um den SSR zu steuern.
|
|
OutputWasser = tuningStartValueWasser; // Angepasst
|
|
|
|
// KEIN direktes digitalWrite mehr hier! Die loop() übernimmt das.
|
|
|
|
autoTuneWasserActive = true;
|
|
Serial.println("Starte AutoTune für Wasser (Zeitproportional)...");
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Startet AutoTune für Dampf-PID (Angepasst für Zeitproportional)
|
|
************************************************************************************/
|
|
|
|
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
|
|
pidDampf.SetMode(AUTOMATIC);
|
|
|
|
// Sicherstellen, dass die Output-Grenzen korrekt gesetzt sind
|
|
pidDampf.SetOutputLimits(0, windowSizeDampf);
|
|
|
|
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
|
|
// WICHTIG: AutoTune manipuliert OutputDampf direkt. Die zeitproportionale
|
|
// Logik in loop() wird diesen Wert verwenden, um den SSR zu steuern.
|
|
OutputDampf = tuningStartValueDampf; // Angepasst
|
|
|
|
// KEIN direktes digitalWrite mehr hier! Die loop() übernimmt das.
|
|
|
|
autoTuneDampfActive = true;
|
|
Serial.println("Starte AutoTune für Dampf (Zeitproportional)...");
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Stoppt AutoTune für Wasser - Unverändert
|
|
************************************************************************************/
|
|
|
|
void stopAutoTuneWasser()
|
|
{
|
|
if (autoTuneWasserActive)
|
|
{
|
|
autoTuneWasserActive = false;
|
|
delete autoTuneWasser;
|
|
autoTuneWasser = nullptr;
|
|
// Setze PID zurück in den Normalbetrieb (ggf. Output auf 0)
|
|
pidWasser.SetMode(AUTOMATIC); // Sicherstellen, dass PID wieder normal läuft
|
|
OutputWasser = 0; // Optional: Output zurücksetzen
|
|
// Stelle sicher, dass SSR ausgeschaltet ist
|
|
digitalWrite(SSR_WASSER_PIN, LOW);
|
|
Serial.println("AutoTune Wasser gestoppt.");
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Stoppt AutoTune für Dampf - Unverändert
|
|
************************************************************************************/
|
|
|
|
void stopAutoTuneDampf()
|
|
{
|
|
if (autoTuneDampfActive)
|
|
{
|
|
autoTuneDampfActive = false;
|
|
delete autoTuneDampf;
|
|
autoTuneDampf = nullptr;
|
|
// Setze PID zurück in den Normalbetrieb (ggf. Output auf 0)
|
|
pidDampf.SetMode(AUTOMATIC); // Sicherstellen, dass PID wieder normal läuft
|
|
OutputDampf = 0; // Optional: Output zurücksetzen
|
|
// Stelle sicher, dass SSR ausgeschaltet ist
|
|
digitalWrite(SSR_DAMPF_PIN, LOW);
|
|
Serial.println("AutoTune Dampf gestoppt.");
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Überwacht die AutoTune-Prozesse (Dampf/Wasser) - Unverändert in der Logik,
|
|
* aber die SSR-Steuerung erfolgt nun implizit über die loop()
|
|
************************************************************************************/
|
|
|
|
void handleAutoTune()
|
|
{
|
|
// AutoTune Wasser
|
|
if (autoTuneWasserActive)
|
|
{
|
|
// 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
|
|
// AutoTune->Runtime() berechnet und SETZT OutputWasser!
|
|
if (autoTuneWasser->Runtime() == 1)
|
|
{
|
|
KpWasser = autoTuneWasser->GetKp();
|
|
KiWasser = autoTuneWasser->GetKi();
|
|
KdWasser = autoTuneWasser->GetKd();
|
|
// WICHTIG: PID-Tunings sofort anwenden
|
|
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
|
// Speichern im EEPROM
|
|
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)
|
|
{
|
|
// 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
|
|
// AutoTune->Runtime() berechnet und SETZT OutputDampf!
|
|
if (autoTuneDampf->Runtime() == 1)
|
|
{
|
|
KpDampf = autoTuneDampf->GetKp();
|
|
KiDampf = autoTuneDampf->GetKi();
|
|
KdDampf = autoTuneDampf->GetKd();
|
|
// WICHTIG: PID-Tunings sofort anwenden
|
|
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
|
// Speichern im EEPROM
|
|
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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
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äteinfo</h3>
|
|
Die Gerä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ücksetzen'>
|
|
</form>
|
|
<form action='/resetShots' method='POST'>
|
|
<h3>Shots: (Bezüge über 20 Sekunden)</h3>
|
|
<p>{SHOT_COUNT}</p>
|
|
<input type='submit' value='Zurü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öße: {SKETCH_SIZE} Bytes<br>
|
|
Freier Sketch-Speicher: {FREE_SKETCH} Bytes<br>
|
|
Flash-Größe (Chip): {FLASH_SIZE} Bytes<br>
|
|
<br>
|
|
MAC-Adresse: {MAC_ADDRESS}<br>
|
|
WLAN-Signalstä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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
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ält.<br>
|
|
Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollständig beendet wird.<br>
|
|
Der dynamische Eco-Modus ist nur in Kombination mit dem Eco-Modus nutzbar!<br>
|
|
<br>
|
|
<b>Beispiel fü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ü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ögerung für Dampf</h3>
|
|
<label for='dampfDelay'>Aufheizen für Dampf verzögern (Minuten, 0 = deaktiviert):</label>
|
|
<input type='text' id='dampfDelay' name='dampfDelay' value='{DAMPF_DELAY}'>
|
|
<b style='color: #FFCC00;'>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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
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ö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ührt werden.<br>
|
|
<br>
|
|
<b style='color: #FFCC00;'>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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
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) - Unverändert
|
|
************************************************************************************/
|
|
|
|
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 & Offset: Wasser / Kessel</h3>
|
|
<label for='wasser'>Setpoint (°C):</label>
|
|
<input type='text' id='wasser' name='wasser' value='{WASSER}'>
|
|
<label for='offsetWasser'>Offset (°C):</label>
|
|
<input type='text' id='offsetWasser' name='offsetWasser' value='{OFFSET_WASSER}'>
|
|
</br>
|
|
<h3>Temperatur & Offset: Dampf / Thermoblock</h3>
|
|
<label for='dampf'>Setpoint (°C):</label>
|
|
<input type='text' id='dampf' name='dampf' value='{DAMPF}'>
|
|
<label for='offsetDampf'>Offset (°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 Übertemperatursicherung,<br>
|
|
ersetzt jedoch keine hardwareseitige Lösung durch z.B. Bimetall Temperaturschalter!<br>
|
|
Bei der Verwendung von Fast-Heat-Up muss der Wert für die Maximaltemperatur angepasst werden!<br>
|
|
<br>
|
|
<label for='maxTempWasser'>Max. Temp Wasser (°C):</label>
|
|
<input type='text' id='maxTempWasser' name='maxTempWasser' value='{MAX_TEMP_WASSER}'>
|
|
<label for='maxTempDampf'>Max. Temp Dampf (°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) - Unverändert
|
|
************************************************************************************/
|
|
|
|
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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
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 - Unverändert
|
|
************************************************************************************/
|
|
|
|
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) - Unverändert
|
|
************************************************************************************/
|
|
|
|
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='#000000'>
|
|
<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, // <b style='color: #FFCC00;'>WICHTIG:</b> 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ügbar</div>";
|
|
} else if (wasserSensorError) {
|
|
errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert für Wasser nicht verfügbar</div>";
|
|
} else if (dampfSensorError) {
|
|
errorMessageHtml = "<div class=\"sensor-error-message\">Temperaturwert für Dampf nicht verfü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) - Unverändert
|
|
************************************************************************************/
|
|
|
|
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önnen zwei unabhä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ür Dampf verzö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 - Unverändert
|
|
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
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, p.setpointWasser); // Korrigiert: Lade Wert aus EEPROM
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, p.setpointDampf); // Korrigiert: Lade Wert aus EEPROM
|
|
|
|
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 - Unverändert
|
|
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();
|
|
|
|
// PID-Parameter neu setzen, falls sie sich durch das Profil geändert haben
|
|
// (Obwohl Profile aktuell keine PID-Parameter speichern, ist dies gute Praxis)
|
|
// pidDampf.SetTunings(KpDampf, KiDampf, KdDampf); // Wenn Profile Kp/Ki/Kd enthalten würden
|
|
// pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
|
|
|
// Eco-Status zurücksetzen, falls nötig
|
|
ecoModeAktiv = false;
|
|
ecoModeActivatedTime = 0;
|
|
|
|
|
|
server.sendHeader(F("Location"), F("/profiles"));
|
|
server.send(303);
|
|
}
|
|
|
|
// Speichert aktuelles Setup in Profil 2 - Unverändert
|
|
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));
|
|
}
|
|
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, p.setpointWasser); // Korrigiert
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, p.setpointDampf); // Korrigiert
|
|
|
|
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 - Unverändert
|
|
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();
|
|
|
|
// PID-Parameter neu setzen, falls sie sich durch das Profil geändert haben
|
|
// pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
|
// pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
|
|
|
// Eco-Status zurücksetzen, falls nötig
|
|
ecoModeAktiv = false;
|
|
ecoModeActivatedTime = 0;
|
|
|
|
server.sendHeader(F("Location"), F("/profiles"));
|
|
server.send(303);
|
|
}
|