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

1726 lines
60 KiB
Arduino
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*****************************************************
* GESAMTER CODE: NEU GESCHRIEBEN MIT
* HTML-VEREINHEITLICHUNG & BUGFIX IM AUTO-TUNE
*****************************************************/
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <EEPROM.h>
#include <PID_v1.h>
#include <PID_AutoTune_v0.h>
#include "max6675.h"
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
/* Hardware definitions */
#define SSR_WASSER_PIN D1
#define SSR_DAMPF_PIN D2
#define SHOT_TIMER_PIN D8
#define OLED_SDA D3
#define OLED_SCK D4
#define ANALOG_NTC_PIN A0
// Display setup
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
// MAX6675 setup
#define MAX6675_SCK D5
#define MAX6675_CS D6
#define MAX6675_SO D7
MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO);
// Firmware-Version (wird auch auf dem Display angezeigt)
String version = "2.3.3";
String versionHersteller = "Thomas M&uuml;ller";
String versionHerstellerMail = "<a href='mailto:thomas@mueller.black'>thomas@mueller.black</a>";
String versionHerstellerWeb = "<a href='https://raw-designs.de/' target='_blank'>https://raw-designs.de/</a> | <a href='https://mueller.black/' target='_blank'>https://mueller.black/</a>";
// Geräteinfo
char infoHersteller[50];
char infoModell[50];
char infoZusatz[50];
// PID variables
double SetpointDampf, InputDampf, OutputDampf;
double SetpointWasser, InputWasser, OutputWasser;
double OffsetDampf = 0.0, OffsetWasser = 0.0;
double KpDampf = 2.0, KiDampf = 5.0, KdDampf = 1.0;
double KpWasser = 2.0, KiWasser = 5.0, KdWasser = 1.0;
PID pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT);
PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, DIRECT);
// Standardwerte definieren
double defaultSetpointDampf = 165.0;
double defaultSetpointWasser = 93.0;
double defaultOffsetDampf = 0.0;
double defaultOffsetWasser = 0.0;
double defaultKpDampf = 0.0;
double defaultKiDampf = 1.0;
double defaultKdDampf = 0.5;
double defaultKpWasser = 12.0;
double defaultKiWasser = 0.0;
double defaultKdWasser = 2.5;
int defaultEcoModeMinutes = 0;
int defaultEcoModeTempWasser = 60;
int defaultEcoModeTempDampf = 60;
String defaultInfoHersteller = "";
String defaultInfoModell = "";
String defaultInfoZusatz = "";
// Web server setup
ESP8266WebServer server(80);
// MagicValue
const char storageMagicValue[5] = "MGVE";
//------------------------------
// EEPROM Adressen als Konstanten
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
const int EEPROM_ADDR_DYNAMIC_ECO_MODE = 256; // 1 Byte (bool)
const int EEPROM_ADDR_WIFI_CONFIG_MAGIC = 261; // 5 Bytes
const int EEPROM_ADDR_WIFI_CONFIG_DATA = 266; // sizeof(WiFiConfig) ≈ 165 Bytes + Puffer
//------------------------------
// Eco-Mode
unsigned long lastShotTime = 0;
int ecoModeMinutes = 0;
bool ecoModeAktiv = 0;
int ecoModeTempWasser = 50;
int ecoModeTempDampf = 50;
bool dynamicEcoActive = false;
unsigned long ecoModeActivatedTime = 0;
// Fast-Heat-Up
bool fastHeatUpAktiv = 0;
bool fastHeatUpHeating = 0;
int fastHeatUpSetpoint = 130;
// AutoTune-Einstellungen
double tuningStep = 50; // Schrittweite für Output
double tuningNoise = 1; // Toleranz für Änderungen
double tuningStartValue = 50; // Startwert für AutoTune
unsigned int tuningLookBack = 20; // Anzahl der Lookback-Zyklen
PID_ATune* autoTuneWasser;
PID_ATune* autoTuneDampf;
bool autoTuneWasserActive = false;
bool autoTuneDampfActive = false;
// Shot timer
unsigned long shotStartTime = 0;
unsigned long shotEndTime = 0;
bool shotActive = false;
bool delayDisplayUpdate = false;
// Shot-Zähler
const int runtimeAddress = 500; // 4 Bytes (unsigned long)
const int shotCounterAddress = 504; // 4 Bytes (unsigned long) → für große Zahlen
unsigned long shotCounter = 0; // Mit unsigned long für große Zahlen
// Betriebszeit
unsigned long totalRuntime = 0;
unsigned long lastRuntimeSave = 0; // Zeitpunkt des letzten Speicherns
const unsigned long runtimeSaveInterval = 60000; // 1 Minute
// Delays für Anzeige beim Start
int delayInit1 = 1500;
int delayInit2 = 3000;
// Delays für den Loop, um SSRs korrekt zu schalten und Sensoren zu lesen
unsigned long previousMillis = 0; // vorheriger Zeitpunkt
const unsigned long interval = 250; // Intervall in Millisekunden
// WiFi Signal Bitmap
static const unsigned char PROGMEM wifi12x12[] = {
0x1f, 0x80, 0x7f, 0xe0, 0xf0, 0xf0, 0xc0, 0x30, 0x1f, 0x80, 0x7f, 0xe0,
0x60, 0x60, 0x0f, 0x00, 0x3f, 0xc0, 0x30, 0xc0, 0x06, 0x00, 0x06, 0x00
};
// Gemeinsame CSS-Styles als PROGMEM (unverändert)
static const char commonStyle[] PROGMEM = R"rawliteral(
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
h1 {
color: #333;
padding: 2px;
text-align: center;
}
nav {
background-color: #333;
overflow: hidden;
text-align: center;
padding: 10px 0;
}
nav a {
color: white;
text-decoration: none;
padding: 14px 20px;
display: inline-block;
}
nav a:hover {
background-color: #575757;
border-radius: 4px;
}
form {
width: 90%;
max-width: 600px;
margin: 20px auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
h3 {
color: #444;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
margin-bottom: 10px;
}
label {
display: block;
margin: 10px 0 5px;
font-weight: bold;
}
input[type="text"] {
width: calc(100% - 20px);
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
input[type="submit"] {
background-color: #333;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
input[type="submit"]:hover {
background-color: #555;
}
/* Media Queries */
@media (max-width: 768px) {
nav a {
display: block;
padding: 10px;
}
form {
width: 100%;
padding: 15px;
}
input[type="text"] {
width: calc(100% - 10px);
}
}
@media (max-width: 480px) {
nav a {
font-size: 14px;
padding: 8px;
}
h1 {
font-size: 24px;
}
input[type="submit"] {
width: 100%;
padding: 12px;
font-size: 16px;
}
}
</style>
)rawliteral";
// Gemeinsame Navigation als PROGMEM (unverändert)
static const char commonNav[] PROGMEM = R"rawliteral(
<nav>
<a href="/">PID-Einstellung</a>
<a href="/PID-Tuning">PID-Tuning</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="/wifi-config">WiFi</a>
<a href="/updateFirmware">Firmware</a>
</nav>
)rawliteral";
// WiFi
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);
};
bool loadWiFiConfig(WiFiConfig& config) {
char magic[5] = { 0 };
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
if (strncmp(magic, storageMagicValue, 4) != 0) {
// Setze Standardwerte
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;
}
// Lese Daten ab der KORREKTEN Adresse
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
return true;
}
void saveWiFiConfig(const WiFiConfig& config) {
EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, storageMagicValue);
EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
EEPROM.commit();
}
void startAPMode() {
WiFi.softAP(F("Dual PID-Controller"));
Serial.print(F("AP-Mode IP: "));
Serial.println(WiFi.softAPIP());
}
/*
* ----------------------------------------
* Gemeinsame Header- und Footer-Funktionen
* zur Vermeidung von HTML-Duplikaten.
* ----------------------------------------
*/
String createHtmlHeader(const String &pageTitle) {
// Baue dynamisch den Seitenkopf
String page = F("<!DOCTYPE html>\n<html>\n<head>\n");
page += F("<title>");
page += pageTitle;
page += F("</title>\n");
// Füge CSS aus PROGMEM an
page += FPSTR(commonStyle);
page += F("\n</head>\n<body>\n");
// Füge Navigation aus PROGMEM an
page += FPSTR(commonNav);
return page;
}
String createHtmlFooter() {
// Einfaches Footer-HTML
return F("\n</body>\n</html>\n");
}
/******************************************************
* Ab hier folgen die Handler für Webseiten (Routen).
* Der HTML-Code wurde "verschlankt" (kein Duplikat mehr).
******************************************************/
void handleWiFiConfig(); // Vorab-Deklarationen
void handleSaveWiFiConfig();
void handleForceAPMode();
void handleInfoUpdate();
void handleEcoUpdate();
void handleFastHeatUpSettings();
void handleResetRuntime();
void handleResetShots();
void handleRoot();
void handleUpdate();
void handleEco();
void handleFastHeatUp();
void handleCharts();
void handleInfo();
void handleAutoTune();
void checkWifiConnection() {
if (WiFi.status() != WL_CONNECTED) {
WiFi.reconnect();
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println(F("WiFi-Verbindung kann nicht hergestellt werden!"));
// Evtl. noch AP-Modus starten
}
}
}
void drawWiFi12x12(int16_t x, int16_t y) {
// x, y sind die Koordinaten in Pixeln, wo das Symbol oben links gezeichnet wird.
// 'wifi12x12' ist unser Array
// 12, 12 = Breite und Höhe
// SH110X_WHITE = Farbe (Pixel an)
display.drawBitmap(x, y, wifi12x12, 12, 12, SH110X_WHITE);
}
/************************
* Runtime/Shot Reset
************************/
void handleResetRuntime() {
totalRuntime = 0;
EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime);
EEPROM.commit();
server.sendHeader(F("Location"), F("/info"));
server.send(303);
}
void handleResetShots() {
shotCounter = 0;
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
EEPROM.commit();
server.sendHeader(F("Location"), F("/info"));
server.send(303);
}
/*************************************************************
* Firmware-Update (mit Kennwort-Suche im Binärfile)
* Festes Kennwort, das im Binärfile enthalten sein muss
*************************************************************/
static const char FIRMWARE_PASSWORD[] = "FWKennwort123";
static bool passwordFound = false;
static int passMatchPos = 0;
const int passLength = sizeof(FIRMWARE_PASSWORD) - 1;
/*
* setup() - Haupt-Initialisierung
*/
void setup() {
Serial.begin(115200);
EEPROM.begin(1024);
pinMode(SSR_DAMPF_PIN, OUTPUT);
pinMode(SSR_WASSER_PIN, OUTPUT);
pinMode(SHOT_TIMER_PIN, INPUT_PULLUP);
// Magic Value aus EEPROM lesen
char storedMagicValue[5] = { 0 }; // 4 Zeichen + Nullterminator
bool magicValueVorhanden = false;
EEPROM.get(EEPROM_ADDR_MAGICVALUE, storedMagicValue);
if (strncmp(storedMagicValue, storageMagicValue, 4) == 0) {
// Magic Value ist vorhanden gültige EEPROM-Daten
magicValueVorhanden = true;
} else {
// Magic Value fehlt oder stimmt nicht, also initialisieren
magicValueVorhanden = false;
EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue);
EEPROM.commit();
}
// Gelesene Werte prüfen, ggf. Standardwerte setzen
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
if (!magicValueVorhanden || SetpointDampf > 200) {
SetpointDampf = defaultSetpointDampf;
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
}
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
if (!magicValueVorhanden || SetpointWasser > 200) {
SetpointWasser = defaultSetpointWasser;
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
}
EEPROM.get(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
if (!magicValueVorhanden) {
OffsetDampf = defaultOffsetDampf;
EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
}
EEPROM.get(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
if (!magicValueVorhanden) {
OffsetWasser = defaultOffsetWasser;
EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
}
EEPROM.get(EEPROM_ADDR_KP_DAMPF, KpDampf);
if (!magicValueVorhanden) {
KpDampf = defaultKpDampf;
EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
}
EEPROM.get(EEPROM_ADDR_KI_DAMPF, KiDampf);
if (!magicValueVorhanden) {
KiDampf = defaultKiDampf;
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
}
EEPROM.get(EEPROM_ADDR_KD_DAMPF, KdDampf);
if (!magicValueVorhanden) {
KdDampf = defaultKdDampf;
EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
}
EEPROM.get(EEPROM_ADDR_KP_WASSER, KpWasser);
if (!magicValueVorhanden) {
KpWasser = defaultKpWasser;
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
}
EEPROM.get(EEPROM_ADDR_KI_WASSER, KiWasser);
if (!magicValueVorhanden) {
KiWasser = defaultKiWasser;
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
}
EEPROM.get(EEPROM_ADDR_KD_WASSER, KdWasser);
if (!magicValueVorhanden) {
KdWasser = defaultKdWasser;
EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
}
EEPROM.get(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
if (!magicValueVorhanden || ecoModeMinutes < 0) {
ecoModeMinutes = defaultEcoModeMinutes;
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
}
EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
if (!magicValueVorhanden || ecoModeTempWasser < 0) {
ecoModeTempWasser = defaultEcoModeTempWasser;
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
}
EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
if (!magicValueVorhanden || ecoModeTempDampf < 0) {
ecoModeTempDampf = defaultEcoModeTempDampf;
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
}
EEPROM.get(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
if (!magicValueVorhanden || strlen(infoHersteller) == 0) {
strcpy(infoHersteller, defaultInfoHersteller.c_str());
EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
}
EEPROM.get(EEPROM_ADDR_INFO_MODELL, infoModell);
if (!magicValueVorhanden || strlen(infoModell) == 0) {
strcpy(infoModell, defaultInfoModell.c_str());
EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell);
}
EEPROM.get(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
if (!magicValueVorhanden || strlen(infoZusatz) == 0) {
strcpy(infoZusatz, defaultInfoZusatz.c_str());
EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
}
if (!magicValueVorhanden) {
dynamicEcoActive = false;
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
} else {
EEPROM.get(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
}
if (!magicValueVorhanden) {
fastHeatUpAktiv = false;
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
} else {
EEPROM.get(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
}
// Versuche, aus dem EEPROM WLAN-Daten zu laden
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();
}
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);
}
// Werte schreiben
EEPROM.commit();
if (fastHeatUpAktiv) {
fastHeatUpHeating = true;
}
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 in AUTOMATIC (wird in loop bei AutoTune ggf. ausgesetzt)
pidDampf.SetMode(AUTOMATIC);
pidWasser.SetMode(AUTOMATIC);
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
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 registrieren
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);
server.on("/autoTuneWasser", []() {
String page = createHtmlHeader("AutoTune Wasser");
page += F("<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 += createHtmlFooter();
server.send(200, F("text/html"), page);
});
server.on("/autoTuneDampf", []() {
String page = createHtmlHeader("AutoTune Dampf");
page += F("<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 += createHtmlFooter();
server.send(200, F("text/html"), page);
});
server.on("/PID-Tuning", HTTP_GET, []() {
String page = createHtmlHeader("PID-Tuning");
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 action='/autoTuneWasser' method='POST' >\n"
"<h3>Automatisches PID-Tuning f&uuml;r Wasser</h3>\n"
"<input type='submit' value='Tuning starten'>\n"
"</form>\n"
"<form action='/autoTuneDampf' method='POST' >\n"
"<h3>Automatisches PID-Tuning f&uuml;r Dampf</h3>\n"
"<input type='submit' value='Tuning starten'>\n"
"</form>\n");
page += createHtmlFooter();
server.send(200, F("text/html"), page);
});
server.on("/charts", handleCharts);
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);
});
server.on("/updateFirmware", HTTP_GET, []() {
String page = createHtmlHeader("Firmware-Update");
page += F("<h1>Firmware</h1>\n"
"<form>\n"
"<h3>Firmware-Info</h3>\n"
"Firmware-Version: {FIRMWAREVERSION}<br>\n"
"Hersteller: {SWHERSTELLER}<br>\n"
"E-Mail: {SWHERSTELLERMAIL}<br>\n"
"Website: {SWHERSTELLERWEBSITE}<br>\n"
"</form>\n"
"<form method='POST' action='/update' enctype='multipart/form-data'>\n"
"<h3>Firmware-Update</h3>\n"
"Das Firmware-Update kann durch den Upload einer .bin-Datei durchgef&uuml;hrt werden.<br>\n"
"Nach dem Update wird ein automatischer Neustart durchgef&uuml;hrt.<br>\n"
"Sollte der Neustart nicht erfolgen, so kann dieser auch durch kurzzeitiges Trennen der Stromversorgung erfolgen.<br>\n"
"<br>\n"
"<input type='file' name='firmware'>\n"
"<br><br>\n"
"<button>Update starten</button>\n"
"</form>\n");
page.replace(F("{FIRMWAREVERSION}"), version);
page.replace(F("{SWHERSTELLER}"), versionHersteller);
page.replace(F("{SWHERSTELLERMAIL}"), versionHerstellerMail);
page.replace(F("{SWHERSTELLERWEBSITE}"), versionHerstellerWeb);
page += createHtmlFooter();
server.send(200, F("text/html"), page);
});
server.on(
"/update", HTTP_POST, []() {
// Antwortseite nach dem Upload
String htmlResponse = createHtmlHeader("Firmware-Update");
// Falls das Kennwort nicht gefunden wurde oder es einen Update-Fehler gab → Abbruch
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!");
}
}
// 30s Redirect
htmlResponse += F("<meta http-equiv=\"refresh\" content=\"30;url=/\" />\n"
"<script>\n"
" setTimeout(function() {\n"
" window.location.href = \"/\";\n"
" }, 30000);\n"
"</script>\n"
"<h1>");
htmlResponse += statusMessage;
htmlResponse += F("</h1>\n"
"<form><p>Sie werden in 30 Sekunden zur Startseite weitergeleitet...</p></form>");
htmlResponse += createHtmlFooter();
server.send(200, F("text/html"), htmlResponse);
delay(1000);
ESP.restart();
},
[]() {
// Upload-Callback
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) {
// Byte-für-Byte auf Kennwort prüfen
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;
}
}
}
// Das eigentliche Flashen
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();
});
server.on("/abbruch-pid-tuning", HTTP_POST, []() {
String html = createHtmlHeader("PID-Tuning Abbruch");
html += F("<h1>PID-Tuning Abbruch</h1>\n"
"<form>\n"
"<h3>Das PID-Tuning wurde erfolgreich abgebrochen.</h3>\n"
"<p>Der Normalbetrieb wird nun fortgesetzt.</p>\n"
"</form>\n");
html += createHtmlFooter();
server.send(200, F("text/html"), html);
stopAutoTuneWasser();
stopAutoTuneDampf();
});
server.on("/wifi-config", handleWiFiConfig);
server.on("/saveWiFiConfig", handleSaveWiFiConfig);
server.on("/forceAPMode", handleForceAPMode);
server.on("/resetRuntime", handleResetRuntime);
server.on("/resetShots", handleResetShots);
server.begin();
Serial.println(F("Webserver gestartet"));
}
/*******************************************
* Loop (Hauptschleife)
*******************************************/
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
// Beispielhafter Temperatur-Input
InputDampf = round(thermocouple.readCelsius() + OffsetDampf);
InputWasser = round((analogRead(ANALOG_NTC_PIN) * (3.3 / 1023.0)) + OffsetWasser);
// AutoTune ggf. ausführen
handleAutoTune();
if (!autoTuneWasserActive) pidWasser.Compute();
if (!autoTuneDampfActive) pidDampf.Compute();
digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf);
digitalWrite(SSR_WASSER_PIN, (int)OutputWasser);
}
updateShotTimer();
// Eco-Mode
if (!autoTuneWasserActive && !autoTuneDampfActive) {
if (ecoModeMinutes > 0 && !shotActive) {
unsigned long currentTimeEco = millis();
if (currentTimeEco - lastShotTime > (ecoModeMinutes * 60UL * 1000UL)) {
if (!ecoModeAktiv) {
// Eco-Modus wird gerade aktiviert Startzeit merken
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 {
ecoModeAktiv = false;
ecoModeActivatedTime = 0;
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
}
}
}
updateDisplay();
server.handleClient();
// Betriebszeit messen
static unsigned long lastRuntimeSecond = 0;
if (millis() - lastRuntimeSecond >= 1000) {
totalRuntime++;
lastRuntimeSecond = millis();
}
if (millis() - lastRuntimeSave >= 60000) {
EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime);
EEPROM.commit();
lastRuntimeSave = millis();
checkWifiConnection();
}
}
/*************************************************
* Display-Aktualisierung
*************************************************/
void updateDisplay() {
if (!autoTuneWasserActive && !autoTuneDampfActive) {
if (delayDisplayUpdate && (millis() - shotEndTime < 2000)) {
return;
} else {
delayDisplayUpdate = false;
}
display.clearDisplay();
// Optional: Zeichne WiFi-Symbol, falls verbunden
// if (WiFi.status() == WL_CONNECTED) { drawWiFi12x12(114, 0); }
display.setCursor(0, 0);
display.setTextColor(SH110X_WHITE);
display.setTextSize(1);
if (fastHeatUpHeating && !shotActive && ((int)InputWasser < fastHeatUpSetpoint)) {
SetpointWasser = fastHeatUpSetpoint;
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);
display.print(F(" "));
display.print((char)247);
display.print(F("C"));
} else {
if (shotActive) {
unsigned long elapsed = millis() - shotStartTime;
display.println(F("Shot-Timer:"));
display.println("");
display.setTextSize(4);
display.println(elapsed / 1000.0, 1);
display.setTextSize(1);
display.println(F("Sekunden"));
fastHeatUpHeating = false;
} else {
display.println(F("Temperaturen:"));
display.println("");
display.println(F("Wasser: "));
display.print((int)InputWasser);
display.print(F(" / "));
display.print((int)SetpointWasser);
display.print(F(" "));
display.print((char)247);
display.print(F("C"));
if (ecoModeAktiv) {
display.print(F(" (ECO)"));
}
display.println("");
display.println("");
display.println(F("Dampf: "));
display.print((int)InputDampf);
display.print(F(" / "));
display.print((int)SetpointDampf);
display.print(F(" "));
display.print((char)247);
display.print(F("C"));
if (ecoModeAktiv) {
display.print(F(" (ECO)"));
}
}
}
display.display();
}
}
/*********************************
* Shot-Timer
*********************************/
void updateShotTimer() {
if (digitalRead(SHOT_TIMER_PIN) == HIGH) {
if (!shotActive) {
shotActive = true;
shotStartTime = millis();
}
} else {
if (shotActive) {
unsigned long shotDuration = millis() - shotStartTime;
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;
}
}
}
/************************************************************
* AutoTune-Start/Stop
************************************************************/
void startAutoTuneWasser() {
autoTuneWasser = new PID_ATune(&InputWasser, &OutputWasser);
autoTuneWasser->SetOutputStep(tuningStep);
autoTuneWasser->SetControlType(1);
autoTuneWasser->SetNoiseBand(tuningNoise);
autoTuneWasser->SetLookbackSec(tuningLookBack);
OutputWasser = tuningStartValue;
autoTuneWasserActive = true;
}
void startAutoTuneDampf() {
autoTuneDampf = new PID_ATune(&InputDampf, &OutputDampf);
autoTuneDampf->SetOutputStep(tuningStep);
autoTuneDampf->SetControlType(1);
autoTuneDampf->SetNoiseBand(tuningNoise);
autoTuneDampf->SetLookbackSec(tuningLookBack);
OutputDampf = tuningStartValue;
autoTuneDampfActive = true;
}
void stopAutoTuneWasser() {
if (autoTuneWasserActive) {
autoTuneWasserActive = false;
delete autoTuneWasser;
autoTuneWasser = nullptr;
OutputWasser = 0;
}
}
void stopAutoTuneDampf() {
if (autoTuneDampfActive) {
autoTuneDampfActive = false;
delete autoTuneDampf;
autoTuneDampf = nullptr;
OutputDampf = 0;
}
}
/************************************************************
* AutoTune-Handling (laufend im loop() aufgerufen)
* Hier nun der KORREKTE Speicherort für die PID-Werte!
************************************************************/
void handleAutoTune() {
// Wasser
if (autoTuneWasserActive) {
if (autoTuneWasser->Runtime() == 1) {
KpWasser = autoTuneWasser->GetKp();
KiWasser = autoTuneWasser->GetKi();
KdWasser = autoTuneWasser->GetKd();
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
// Fix: Werte in *Wasser*-Adressen speichern!
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
EEPROM.commit();
autoTuneWasserActive = false;
delete autoTuneWasser;
} else {
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 verwenden!"));
display.println(F("Anzeige erlischt,"));
display.println(F("sobald der Vorgang"));
display.println(F("abgeschlossen ist!"));
display.display();
}
}
// Dampf
if (autoTuneDampfActive) {
if (autoTuneDampf->Runtime() == 1) {
KpDampf = autoTuneDampf->GetKp();
KiDampf = autoTuneDampf->GetKi();
KdDampf = autoTuneDampf->GetKd();
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
// Fix: Werte in *Dampf*-Adressen speichern!
EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
EEPROM.commit();
autoTuneDampfActive = false;
delete autoTuneDampf;
} else {
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 verwenden!"));
display.println(F("Anzeige erlischt,"));
display.println(F("sobald der Vorgang"));
display.println(F("abgeschlossen ist!"));
display.display();
}
}
}
/*********************************************************
* /info
*********************************************************/
void handleInfo() {
// Formulardaten dynamisch erstellen
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 = createHtmlHeader("Info");
html += F("<h1>Info</h1>\n"
"<form action='/updateInfoSettings' method='POST'>\n"
"<h3>Ger&auml;teinfo</h3>\n"
"Die Ger&auml;teinformationen werden beim Start der Maschine, bzw. PID-Controllers im Display angezeigt.<br><br>\n"
"<label for='hersteller'>Hersteller:</label>\n"
"<input type='text' id='hersteller' name='hersteller' value='{HERSTELLER}'>\n"
"<label for='modell'>Modell:</label>\n"
"<input type='text' id='modell' name='modell' value='{MODELL}'>\n"
"<label for='zusatz'>Zusatz (z.B. Limited Edition):</label>\n"
"<input type='text' id='zusatz' name='zusatz' value='{ZUSATZ}'>\n"
"</br></br>\n"
"<input type='submit' value='Einstellungen speichern'>\n"
"</form>\n"
"<form action='/resetRuntime' method='POST'>\n"
"<h3>Betriebszeit:</h3>\n"
"<p>{RUNTIME}</p>\n"
"<input type='submit' value='Zur&uuml;cksetzen'>\n"
"</form>\n"
"<form action='/resetShots' method='POST'>\n"
"<h3>Shots: (Bez&uuml;ge &uuml;ber 20 Sekunden)</h3>\n"
"<p>{SHOT_COUNT}</p>\n"
"<input type='submit' value='Zur&uuml;cksetzen'>\n"
"</form>\n"
"<form>\n"
"<h3>Systeminfo</h3>\n"
"Freier Heap: {FREE_HEAP} Bytes<br>\n"
"SDK-Version: {SDK_VERSION}<br>\n"
"Boot-Version: {BOOT_VERSION}<br>\n"
"CPU-Takt: {CPU_MHZ} MHz<br>\n"
"Sketch-Gr&ouml;&szlig;e: {SKETCH_SIZE} Bytes<br>\n"
"Freier Sketch-Speicher: {FREE_SKETCH} Bytes\n"
"</form>\n");
// Platzhalter ersetzen
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));
html.replace(F("{FREE_HEAP}"), String(ESP.getFreeHeap()));
html.replace(F("{SDK_VERSION}"), String(ESP.getSdkVersion()));
html.replace(F("{BOOT_VERSION}"), String(ESP.getBootVersion()));
html.replace(F("{CPU_MHZ}"), String(ESP.getCpuFreqMHz()));
html.replace(F("{SKETCH_SIZE}"), String(ESP.getSketchSize()));
html.replace(F("{FREE_SKETCH}"), String(ESP.getFreeSketchSpace()));
html += createHtmlFooter();
server.send(200, F("text/html"), html);
}
/*********************************************************
* /eco
*********************************************************/
void handleEco() {
String html = createHtmlHeader("Eco-Modus");
html += F("<h1>Eco-Modus</h1>\n"
"<form action='/updateEcoSettings' method='POST'>\n"
"<h3>Eco-Modus</h3>\n"
"<label for='ecoMode'>Eco-Modus (Minuten, 0 = deaktiviert):</label>\n"
"<input type='text' id='ecoMode' name='ecoMode' value='{ECO_MODE}'>\n"
"<label for='ecoModeTempWasser'>Eco-Temperatur Wasser:</label>\n"
"<input type='text' id='ecoModeTempWasser' name='ecoModeTempWasser' value='{ECO_MODE_TEMP_WASSER}'>\n"
"<label for='ecoModeTempDampf'>Eco-Temperatur Dampf:</label>\n"
"<input type='text' id='ecoModeTempDampf' name='ecoModeTempDampf' value='{ECO_MODE_TEMP_DAMPF}'>\n"
"<div style='margin-bottom:15px;'>\n"
"<label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>\n"
"Dynamischen Eco-Modus aktivieren:\n"
"<input type='checkbox' id='dynamicEcoMode' name='dynamicEcoMode' value='1' {DYNAMIC_ECO_MODE_CHECKBOX}>\n"
"</label>\n"
"</div>\n"
"<div style='margin-top:15px;'>\n"
"Der dynamische Eco-Modus verhindert, dass die Maschine bis in die Unendlichkeit eine gewisse Temperatur aufrecht erh&auml;lt.<br>\n"
"Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollst&auml;ndig beendet wird.<br>\n"
"<br>\n"
"Beispiel f&uuml;r eine praktische Anwendung:<br>\n"
"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 zubereiten zu k&ouml;nnen. Die Maschine ist nun allerdings schon auf 60 Grad herabgek&uuml;hlt und er muss erneut das Aufheizen abwarten ...<br>\n"
"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>\n"
"Markus muss zwar auch warten, kann jedoch noch vor Peter einen Espresso trinken.<br>\n"
"</div>\n"
"</br></br>\n"
"<input type='submit' value='Einstellungen speichern'>\n"
"</form>\n");
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 += createHtmlFooter();
server.send(200, F("text/html"), html);
}
/*********************************************************
* /fast-heat-up
*********************************************************/
void handleFastHeatUp() {
String html = createHtmlHeader("Fast-Heat-Up-Modus");
html += F("<h1>Fast-Heat-Up-Modus</h1>\n"
"<form action='/updateFast-Heat-Up-Settings' method='POST'>\n"
"<h3>Fast-Heat-Up-Modus</h3>\n"
"<div style='margin-bottom:15px;'>\n"
"<label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>\n"
" Fast-Heat-Up aktivieren:\n"
"</label>\n"
"<input type='checkbox' id='fast-heat-up-aktivieren' name='fastHeatUpAktiv' value='1' {FASTHEATUP_MODE_CHECKBOX}>\n"
"</div>\n"
"<div style='margin-top:15px;'>\n"
"Der Fast-Heat-Up-Modus erm&ouml;glicht es, die Maschine noch schneller aufzuheizen.<br>\n"
"Der Kessel wird beim Start auf 130 Grad Celsius erhitzt.<br>\n"
"Nachdem die Temperatur erreicht ist, muss ein Flush von ca. 20 Sekunden durchgef&uuml;hrt werden.\n"
"</div>\n"
"<input type='submit' value='Einstellungen speichern' style='margin-top:20px;'>\n"
"</form>\n");
html.replace(F("{FASTHEATUP_MODE_CHECKBOX}"), fastHeatUpAktiv ? F("checked") : F(""));
html += createHtmlFooter();
server.send(200, F("text/html"), html);
}
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);
}
/*********************************************************
* / (Root)
*********************************************************/
void handleRoot() {
String html = createHtmlHeader("PID-Einstellung");
html += F("<h1>PID-Einstellung</h1>\n"
"<form action='/updateSettings' method='POST'>\n"
"<h3>Zieltemperatur und Offset</h3>\n"
"<label for='wasser'>Wasser-Setpoint (&deg;C):</label>\n"
"<input type='text' id='wasser' name='wasser' value='{WASSER}'>\n"
"<label for='offsetWasser'>Wasser-Offset (&deg;C):</label>\n"
"<input type='text' id='offsetWasser' name='offsetWasser' value='{OFFSET_WASSER}'>\n"
"<label for='dampf'>Dampf-Setpoint (&deg;C):</label>\n"
"<input type='text' id='dampf' name='dampf' value='{DAMPF}'>\n"
"<label for='offsetDampf'>Dampf-Offset (&deg;C):</label>\n"
"<input type='text' id='offsetDampf' name='offsetDampf' value='{OFFSET_DAMPF}'>\n"
"</br></br></br>\n"
"<h3>PID Wasser</h3>\n"
"<label for='kpWasser'>Kp:</label>\n"
"<input type='text' id='kpWasser' name='kpWasser' value='{KP_WASSER}'>\n"
"<label for='kiWasser'>Ki:</label>\n"
"<input type='text' id='kiWasser' name='kiWasser' value='{KI_WASSER}'>\n"
"<label for='kdWasser'>Kd:</label>\n"
"<input type='text' id='kdWasser' name='kdWasser' value='{KD_WASSER}'>\n"
"</br></br></br>\n"
"<h3>PID Dampf</h3>\n"
"<label for='kpDampf'>Kp:</label>\n"
"<input type='text' id='kpDampf' name='kpDampf' value='{KP_DAMPF}'>\n"
"<label for='kiDampf'>Ki:</label>\n"
"<input type='text' id='kiDampf' name='kiDampf' value='{KI_DAMPF}'>\n"
"<label for='kdDampf'>Kd:</label>\n"
"<input type='text' id='kdDampf' name='kdDampf' value='{KD_DAMPF}'>\n"
"</br></br>\n"
"<input type='submit' value='Einstellungen speichern'>\n"
"</form>\n");
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));
html += createHtmlFooter();
server.send(200, F("text/html"), html);
}
/*********************************************************
* /updateSettings
*********************************************************/
void handleUpdate() {
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();
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();
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.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
EEPROM.commit();
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
server.sendHeader(F("Location"), F("/"));
server.send(303);
}
/*********************************************************
* /updateEcoSettings
*********************************************************/
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;
}
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.commit();
server.sendHeader(F("Location"), F("/eco"));
server.send(303);
}
/*********************************************************
* /updateInfoSettings
*********************************************************/
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);
}
/*********************************************************
* /charts
*********************************************************/
void handleCharts() {
// HTML+JS wird aus zwei Teilen zusammengesetzt
String html = createHtmlHeader("Live-Temperaturverlauf");
html += F("<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n"
"<style>\n"
" canvas {\n"
" width: 90%;\n"
" display: block;\n"
" margin: 20px auto;\n"
" }\n"
" .dropdown {\n"
" margin: 20px auto;\n"
" display: flex;\n"
" justify-content: center;\n"
" align-items: center;\n"
" }\n"
"</style>\n"
"<h1>Live-Temperaturverlauf</h1>\n"
"<div class=\"dropdown\">\n"
"<label for=\"updateRate\">Aktualisierungsrate: </label>\n"
"</br>\n"
"<select id=\"updateRate\">\n"
" <option value=\"1000\">1 Sekunde</option>\n"
" <option value=\"2000\">2 Sekunden</option>\n"
" <option value=\"3000\" selected>3 Sekunden</option>\n"
" <option value=\"4000\">4 Sekunden</option>\n"
" <option value=\"5000\">5 Sekunden</option>\n"
"</select>\n"
"</div>\n"
"<canvas id=\"combinedChart\" height=\"1000\"></canvas>\n"
"<script>\n"
" const ctx = document.getElementById('combinedChart').getContext('2d');\n"
" let setpointDampf = 100;\n"
" let setpointWasser = 80;\n"
" const combinedChart = new Chart(ctx, {\n"
" type: 'line',\n"
" data: {\n"
" labels: [],\n"
" datasets: [\n"
" {\n"
" label: 'Wasser-Temperatur',\n"
" data: [],\n"
" borderColor: 'rgba(75, 192, 192, 1)',\n"
" borderWidth: 2,\n"
" fill: false,\n"
" },\n"
" {\n"
" label: 'Dampf-Temperatur',\n"
" data: [],\n"
" borderColor: 'rgba(255, 99, 132, 1)',\n"
" borderWidth: 2,\n"
" fill: false,\n"
" },\n"
" {\n"
" label: 'Setpoint Wasser',\n"
" data: [],\n"
" borderColor: 'rgba(54, 162, 235, 0.7)',\n"
" borderDash: [10, 5],\n"
" borderWidth: 2,\n"
" fill: false,\n"
" },\n"
" {\n"
" label: 'Setpoint Dampf',\n"
" data: [],\n"
" borderColor: 'rgba(255, 159, 64, 0.7)',\n"
" borderDash: [10, 5],\n"
" borderWidth: 2,\n"
" fill: false,\n"
" }\n"
" ]\n"
" },\n"
" options: {\n"
" scales: {\n"
" y: { beginAtZero: true, max: 200 },\n"
" x: { title: { display: true, text: 'Zeit in Sekunden' } }\n"
" }\n"
" }\n"
" });\n\n"
" let time = 0;\n"
" let updateInterval = 3000;\n"
" let intervalId;\n\n"
" function fetchData() {\n"
" fetch('/data')\n"
" .then(response => response.json())\n"
" .then(data => {\n"
" setpointWasser = data.setpointwasser;\n"
" setpointDampf = data.setpointdampf;\n"
" time += updateInterval / 1000;\n"
" if (combinedChart.data.labels.length > 300) {\n"
" combinedChart.data.labels.shift();\n"
" combinedChart.data.datasets[0].data.shift();\n"
" combinedChart.data.datasets[1].data.shift();\n"
" combinedChart.data.datasets[2].data.shift();\n"
" combinedChart.data.datasets[3].data.shift();\n"
" }\n"
" combinedChart.data.labels.push(time);\n"
" combinedChart.data.datasets[0].data.push(data.wasser);\n"
" combinedChart.data.datasets[1].data.push(data.dampf);\n"
" combinedChart.data.datasets[2].data.push(setpointWasser);\n"
" combinedChart.data.datasets[3].data.push(setpointDampf);\n"
" combinedChart.update();\n"
" })\n"
" .catch(err => console.error(err));\n"
" }\n\n"
" function startFetching() {\n"
" if (intervalId) clearInterval(intervalId);\n"
" intervalId = setInterval(fetchData, updateInterval);\n"
" }\n\n"
" document.getElementById('updateRate').addEventListener('change', function(event) {\n"
" updateInterval = parseInt(event.target.value);\n"
" startFetching();\n"
" });\n\n"
" startFetching();\n"
"</script>\n"
);
html += createHtmlFooter();
server.send(200, F("text/html"), html);
}
/*********************************************************
* /wifi-config & /saveWiFiConfig & /forceAPMode
*********************************************************/
void handleWiFiConfig() {
Serial.print("Groesse der WiFiConfig-Struktur (Bytes): ");
Serial.println(sizeof(WiFiConfig));
// Aktuelle (oder Standard-)Werte laden
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);
} else {
// Ansonsten Standardwerte
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);
currentConfig.dns = IPAddress(8, 8, 8, 8);
}
String html = createHtmlHeader("WLAN-Konfiguration");
html += F("<h1>WLAN-Konfiguration</h1>\n"
"<form action='/saveWiFiConfig' method='POST'>\n"
"<h3>WLAN-Einstellungen</h3>\n"
"<label for='ssid'>SSID:</label>\n"
"<input type='text' id='ssid' name='ssid' value='{SSID}' required>\n"
"<label for='password'>Passwort:</label>\n"
"<input type='text' id='password' name='password' value='{PASSWORD}'>\n"
"<h3>IP-Einstellungen</h3>\n"
"<label>\n"
" <input type='radio' name='ipType' value='dhcp' {DHCP_CHECKED}> DHCP\n"
"</label>\n"
"<label>\n"
" <input type='radio' name='ipType' value='static' {STATIC_CHECKED}> Statische IP\n"
"</label>\n"
"<div id='staticFields' style='display: {STATIC_DISPLAY}'>\n"
" <label for='ip'>IP-Adresse:</label>\n"
" <input type='text' id='ip' name='ip' value='{IP}'>\n"
" <label for='gateway'>Gateway:</label>\n"
" <input type='text' id='gateway' name='gateway' value='{GATEWAY}'>\n"
" <label for='subnet'>Subnetzmaske:</label>\n"
" <input type='text' id='subnet' name='subnet' value='{SUBNET}'>\n"
"</div>\n"
"<input type='submit' value='Speichern'>\n"
"</form>\n"
"<form action='/forceAPMode' method='POST' style='margin-top: 20px;'>\n"
"<h3>AP-Modus</h3>\n"
"Verwendung im Access Point-Modus (AP).<br>\n"
"Erreichbarkeit unter IP-Adresse: 192.168.4.1<br><br>\n"
"<input type='submit' value='AP-Modus verwenden'>\n"
"</form>\n"
"<script>\n"
" document.querySelectorAll('input[name=\"ipType\"]').forEach(radio => {\n"
" radio.addEventListener('change', () => {\n"
" document.getElementById('staticFields').style.display = \n"
" radio.value === 'static' ? 'block' : 'none';\n"
" });\n"
" });\n"
"</script>\n"
);
// Felder ersetzen
html.replace("{SSID}", currentConfig.ssid);
html.replace("{PASSWORD}", currentConfig.password);
html.replace("{IP}", currentConfig.staticIP.toString());
html.replace("{GATEWAY}", currentConfig.gateway.toString());
html.replace("{SUBNET}", currentConfig.subnet.toString());
html.replace("{DHCP_CHECKED}", currentConfig.useStaticIP ? "" : "checked");
html.replace("{STATIC_CHECKED}", currentConfig.useStaticIP ? "checked" : "");
html.replace("{STATIC_DISPLAY}", currentConfig.useStaticIP ? "block" : "none");
html += createHtmlFooter();
server.send(200, F("text/html"), html);
}
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")));
}
saveWiFiConfig(newConfig);
String html = createHtmlHeader("Einstellungen gespeichert");
html += F("<h1>Einstellungen gespeichert</h1>\n"
"<form><p>Die Einstellungen wurden gespeichert. Neustart ...</p></form>\n");
html += createHtmlFooter();
server.send(200, F("text/html"), html);
delay(5000);
ESP.restart();
}
void handleForceAPMode() {
// Leere WLAN-Konfiguration abspeichern
WiFiConfig emptyConfig;
saveWiFiConfig(emptyConfig);
String html = createHtmlHeader("AP-Modus aktivieren");
html += F("<h1>AP-Modus wird aktiviert...</h1>\n"
"<form><p>Die Einstellungen wurden gespeichert. Neustart im AP-Modus ...</p></form>\n");
html += createHtmlFooter();
server.send(200, F("text/html"), html);
delay(1000);
ESP.restart();
}