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

2000 lines
65 KiB
Arduino

#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.4.0";
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_PROFILE_1 = 257; // 50 Bytes für Profile 1
const int EEPROM_ADDR_PROFILE_2 = 307; // 50 Bytes für Profile 2
const int EEPROM_ADDR_WIFI_CONFIG_MAGIC = 357; // 5 Bytes
const int EEPROM_ADDR_WIFI_CONFIG_DATA = 362; // 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 wifiSymbol[] = {
0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48, 0x27, 0x20, 0x08, 0x80, 0x02, 0x00
};
// Gemeinsame CSS-Styles als PROGMEM
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;
}
/* Navigation und Hamburger-Menü */
nav {
background-color: #333;
overflow: hidden;
text-align: center;
padding: 25px 0; /* Höhe der Leiste */
position: relative;
}
nav a {
color: white;
text-decoration: none;
padding: 14px 20px;
display: inline-block;
}
nav a:hover {
background-color: #575757;
border-radius: 4px;
}
.hamburger {
display: none;
font-size: 28px;
line-height: 1;
position: absolute;
top: 10px;
right: 20px;
color: white;
cursor: pointer;
}
.hamburger:hover {
color: #ddd;
}
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;
}
/* Ab hier Media Queries */
@media (max-width: 768px) {
.hamburger {
display: block;
}
nav a {
display: none;
}
nav.active a {
display: block;
margin: 10px 0;
}
}
@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>
<script>
function toggleMenu() {
var nav = document.querySelector('nav');
nav.classList.toggle('active');
}
</script>
)rawliteral";
// Gemeinsame Navigation als PROGMEM
static const char commonNav[] PROGMEM = R"rawliteral(
<nav>
<span class="hamburger" onclick="toggleMenu()">&#9776;</span>
<a href="/">PID-Einstellung</a>
<a href="/profiles">Profile</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());
}
void handleWiFiConfig() {
// HTML-Roh-Strings fürs Formular in PROGMEM
Serial.print("Groesse der WiFiConfig-Struktur (Bytes): ");
Serial.println(sizeof(WiFiConfig));
static const char wifiConfigForm[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>WLAN-Konfiguration</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char wifiConfigForm2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char wifiConfigForm3[] PROGMEM = R"rawliteral(
<body>
<h1>WLAN-Konfiguration</h1>
<form action='/saveWiFiConfig' method='POST'>
<h3>WLAN-Einstellungen</h3>
<label for='ssid'>SSID:</label>
<input type='text' id='ssid' name='ssid' value='{SSID}' required>
<label for='password'>Passwort:</label>
<input type='text' id='password' name='password' value='{PASSWORD}'>
<h3>IP-Einstellungen</h3>
<label>
<input type='radio' name='ipType' value='dhcp' {DHCP_CHECKED}> DHCP
</label>
<label>
<input type='radio' name='ipType' value='static' {STATIC_CHECKED}> Statische IP
</label>
<div id='staticFields' style='display: {STATIC_DISPLAY}'>
<label for='ip'>IP-Adresse:</label>
<input type='text' id='ip' name='ip' value='{IP}'>
<label for='gateway'>Gateway:</label>
<input type='text' id='gateway' name='gateway' value='{GATEWAY}'>
<label for='subnet'>Subnetzmaske:</label>
<input type='text' id='subnet' name='subnet' value='{SUBNET}'>
</div>
<input type='submit' value='Speichern'>
</form>
<form action='/forceAPMode' method='POST' style='margin-top: 20px;'>
<h3>AP-Modus</h3>
Verwendung im Access Point-Modus (AP).<br>
Erreichbarkeit unter IP-Adresse: 192.168.4.1<br><br>
<input type='submit' value='AP-Modus verwenden'>
</form>
<script>
document.querySelectorAll('input[name="ipType"]').forEach(radio => {
radio.addEventListener('change', () => {
document.getElementById('staticFields').style.display =
radio.value === 'static' ? 'block' : 'none';
});
});
</script>
</body>
</html>
)rawliteral";
// Zuerst unsere Struktur für die WLAN-Einstellungen anlegen
WiFiConfig currentConfig;
// Magic-Value aus dem EEPROM lesen, um zu prüfen, ob gültige WiFi-Daten vorliegen
char magic[5] = { 0 };
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
bool configValid = (strncmp(magic, storageMagicValue, 4) == 0);
if (configValid) {
// Wenn gültig, die eigentlichen Konfigurationsdaten lesen
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, currentConfig);
} else {
// Ansonsten Standardwerte setzen
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);
}
// Nun das HTML dynamisch zusammenbauen
String html = FPSTR(wifiConfigForm);
html += FPSTR(commonStyle); // CSS + JS einbinden
html += FPSTR(wifiConfigForm2);
html += FPSTR(commonNav); // Navigation einbinden
html += FPSTR(wifiConfigForm3);
// Felder mit den gelesenen bzw. Default-Werten 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");
// Und an den Client senden
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);
// Kurze HTML-Antwort in PROGMEM
static const char saveConfigHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Einstellungen gespeichert</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><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();
}
void handleForceAPMode() {
// Leere WLAN-Konfiguration abspeichern
WiFiConfig emptyConfig;
saveWiFiConfig(emptyConfig);
static const char forceAPHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>AP-Modus aktivieren</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><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();
}
void checkWifiConnection() {
if (WiFi.status() != WL_CONNECTED) {
WiFi.reconnect();
if (WiFi.waitForConnectResult() != WL_CONNECTED) {
Serial.println(F("WiFi-Verbindung kann nicht hergestellt werden!"));
}
}
}
void drawwifiSymbol(int16_t x, int16_t y) {
display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
}
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);
}
// ------------------------------------------------------
// Festes Kennwort, das im Binärfile enthalten sein muss - Für FW-Update
static const char FIRMWARE_PASSWORD[] = "FWKennwort123";
static bool passwordFound = false;
static int passMatchPos = 0;
const int passLength = sizeof(FIRMWARE_PASSWORD) - 1;
// ------------------------------------------------------
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 };
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();
}
// 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);
}
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);
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);
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 = F("<!DOCTYPE html>\n<html>\n<head>\n<title>AutoTune Wasser</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><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);
});
server.on("/autoTuneDampf", []() {
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>AutoTune Dampf</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><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);
});
server.on("/PID-Tuning", HTTP_GET, []() {
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>PID-Tuning</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>\n");
page += FPSTR(commonStyle);
page += F("</head>\n");
page += FPSTR(commonNav);
page += F("<body>\n");
if (autoTuneWasserActive || autoTuneDampfActive) {
page += F("<form action='/abbruch-pid-tuning' method='POST' >\n"
"<h3>Laufendes PID-Tuning abbrechen?</h3>\n"
"<input type='submit' value='Abbrechen'>\n"
"</form></br>\n");
}
page += F("<h1>PID-Tuning</h1>\n"
"<form>Das automatische PID-Tuning dient dazu, die optimalen Parameter f&uuml;r den PID-Regler (Proportional-, Integral- und Differentialanteil) selbst&auml;ndig zu ermitteln. Dabei analysiert das System mithilfe von Algorithmen das Regelverhalten (z. B. Reaktion auf einen Testimpuls) und passt Kp, Ki und Kd so an, dass gew&uuml;nschte Kriterien wie kurze Einschwingzeit, geringe &Uuml;berschwingung und stabile Regelung erreicht werden.</form>"
"<form action='/autoTuneWasser' method='POST' >\n"
"<h3>Automatisches PID-Tuning f&uuml;r Wasser</h3>\n"
"<input type='submit' value='Tuning starten'>\n"
"</form>\n"
"<form action='/autoTuneDampf' method='POST' >\n"
"<h3>Automatisches PID-Tuning f&uuml;r Dampf</h3>\n"
"<input type='submit' value='Tuning starten'>\n"
"</form>\n"
"</body>\n</html>");
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 = F("<!DOCTYPE html>\n<html>\n<head>\n<title>Firmware-Update</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>\n");
page += FPSTR(commonStyle);
page += F("</head>\n");
page += FPSTR(commonNav);
page += F("<body>\n"
"<h1>Firmware</h1>\n"
"<form>\n"
"<h3>Firmware-Info</h3>\n"
"Firmware-Version: {FIRMWAREVERSION}<br>\n"
"Hersteller: {SWHERSTELLER}<br>\n"
"E-Mail: {SWHERSTELLERMAIL}<br>\n"
"Website: {SWHERSTELLERWEBSITE}<br>\n"
"</form>\n"
"<form method='POST' action='/update' enctype='multipart/form-data'>\n"
"<h3>Firmware-Update</h3>\n"
"Das Firmware-Update kann durch den Upload einer .bin-Datei durchgef&uuml;hrt werden.<br>\n"
"Nach dem Update wird ein automatischer Neustart durchgef&uuml;hrt.<br>\n"
"Sollte der Neustart nicht erfolgen, so kann dieser auch durch kurzzeitiges Trennen der Stromversorgung erfolgen.<br>\n"
"<br>\n"
"<input type='file' name='firmware'>\n"
"<br><br>\n"
"<button>Update starten</button>\n"
"</form>\n"
"</body>\n</html>");
page.replace(F("{FIRMWAREVERSION}"), version);
page.replace(F("{SWHERSTELLER}"), versionHersteller);
page.replace(F("{SWHERSTELLERMAIL}"), versionHerstellerMail);
page.replace(F("{SWHERSTELLERWEBSITE}"), versionHerstellerWeb);
server.send(200, F("text/html"), page);
});
server.on(
"/update", HTTP_POST, []() {
static const char updateDone[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Firmware-Update</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char updateDone2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char updateDone3[] PROGMEM = R"rawliteral(
<head>
<meta http-equiv="refresh" content="30;url=/" />
<script>
setTimeout(function() {
window.location.href = "/";
}, 30000);
</script>
<body>
<h1>{status_message}</h1>
<form><p>Sie werden in 30 Sekunden zur Startseite weitergeleitet...</p></form>
</body>
</html>
)rawliteral";
String htmlResponse = FPSTR(updateDone);
htmlResponse += FPSTR(commonStyle);
htmlResponse += FPSTR(updateDone2);
htmlResponse += FPSTR(commonNav);
htmlResponse += FPSTR(updateDone3);
String statusMessage;
if (!passwordFound) {
statusMessage = F("Update abgebrochen! Die Firmware konnte nicht validiert werden.");
} else {
if (Update.hasError()) {
statusMessage = F("Update fehlgeschlagen!");
} else {
statusMessage = F("Update erfolgreich!");
}
}
htmlResponse.replace(F("{status_message}"), statusMessage);
server.send(200, F("text/html"), htmlResponse);
delay(1000);
ESP.restart();
},
[]() {
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
Serial.printf("Update gestartet: %s\n", upload.filename.c_str());
passwordFound = false;
passMatchPos = 0;
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
Update.printError(Serial);
}
}
else if (upload.status == UPLOAD_FILE_WRITE) {
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;
}
}
}
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, []() {
static const char abortTuning[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Info</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><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();
});
server.on("/wifi-config", handleWiFiConfig);
server.on("/saveWiFiConfig", handleSaveWiFiConfig);
server.on("/forceAPMode", handleForceAPMode);
server.on("/resetRuntime", handleResetRuntime);
server.on("/resetShots", handleResetShots);
// Routen für die Profile
server.on("/profiles", handleProfiles);
server.on("/saveProfile1", handleSaveProfile1);
server.on("/loadProfile1", handleLoadProfile1);
server.on("/saveProfile2", handleSaveProfile2);
server.on("/loadProfile2", handleLoadProfile2);
server.begin();
Serial.println(F("Webserver gestartet"));
}
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);
handleAutoTune();
if (!autoTuneWasserActive) pidWasser.Compute();
if (!autoTuneDampfActive) pidDampf.Compute();
digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf);
digitalWrite(SSR_WASSER_PIN, (int)OutputWasser);
}
updateShotTimer();
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 {
ecoModeAktiv = false;
ecoModeActivatedTime = 0;
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
}
}
}
updateDisplay();
server.handleClient();
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();
}
}
void updateDisplay() {
if (!autoTuneWasserActive && !autoTuneDampfActive) {
if (delayDisplayUpdate && (millis() - shotEndTime < 2000)) {
return;
} else {
delayDisplayUpdate = false;
}
display.clearDisplay();
if (WiFi.status() == WL_CONNECTED) {drawwifiSymbol(113, 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 && dynamicEcoActive) {
display.print(F(" (ECO+)"));
}
if (ecoModeAktiv && !dynamicEcoActive) {
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 && dynamicEcoActive) {
display.print(F(" (ECO+)"));
}
if (ecoModeAktiv && !dynamicEcoActive) {
display.print(F(" (ECO)"));
}
}
}
display.display();
}
}
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;
}
}
}
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;
}
}
void handleAutoTune() {
if (autoTuneWasserActive) {
if (autoTuneWasser->Runtime() == 1) {
KpWasser = autoTuneWasser->GetKp();
KiWasser = autoTuneWasser->GetKi();
KdWasser = autoTuneWasser->GetKd();
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
EEPROM.commit();
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();
}
}
if (autoTuneDampfActive) {
if (autoTuneDampf->Runtime() == 1) {
KpDampf = autoTuneDampf->GetKp();
KiDampf = autoTuneDampf->GetKi();
KdDampf = autoTuneDampf->GetKd();
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
EEPROM.commit();
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();
}
}
}
void handleInfo() {
static const char infoHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Info</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char infoHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char infoHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Info</h1>
<form action='/updateInfoSettings' method='POST'>
<h3>Ger&auml;teinfo</h3>
Die Ger&auml;teinformationen werden beim Start der Maschine, bzw. PID-Controllers im Display angezeigt.
<br><br>
<label for='hersteller'>Hersteller:</label>
<input type='text' id='hersteller' name='hersteller' value='{HERSTELLER}'>
<label for='modell'>Modell:</label>
<input type='text' id='modell' name='modell' value='{MODELL}'>
<label for='zusatz'>Zusatz (z.B. Limited Edition):</label>
<input type='text' id='zusatz' name='zusatz' value='{ZUSATZ}'>
</br></br>
<input type='submit' value='Einstellungen speichern'>
</form>
<form action='/resetRuntime' method='POST'>
<h3>Betriebszeit:</h3>
<p>{RUNTIME}</p>
<input type='submit' value='Zur&uuml;cksetzen'>
</form>
<form action='/resetShots' method='POST'>
<h3>Shots: (Bez&uuml;ge &uuml;ber 20 Sekunden)</h3>
<p>{SHOT_COUNT}</p>
<input type='submit' value='Zur&uuml;cksetzen'>
</form>
<form>
<h3>Systeminfo</h3>
Freier Heap: {FREE_HEAP} Bytes<br>
SDK-Version: {SDK_VERSION}<br>
Boot-Version: {BOOT_VERSION}<br>
CPU-Takt: {CPU_MHZ} MHz<br>
Sketch-Gr&ouml;&szlig;e: {SKETCH_SIZE} Bytes<br>
Freier Sketch-Speicher: {FREE_SKETCH} Bytes
</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);
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()));
server.send(200, F("text/html"), html);
}
void handleEco() {
static const char ecoHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Eco-Modus</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><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;'>
<label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>
Dynamischen Eco-Modus aktivieren (ECO+):
<input type='checkbox' id='dynamicEcoMode' name='dynamicEcoMode' value='1' {DYNAMIC_ECO_MODE_CHECKBOX}>
</label>
</div>
<div style='margin-top:15px;'>
Der dynamische Eco-Modus verhindert, dass die Maschine bis in die Unendlichkeit eine gewisse Temperatur aufrecht erh&auml;lt.<br>
Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollst&auml;ndig beendet wird.<br>
Der dynamische Eco-Modus ist nur in Kombination mit dem Eco-Modus nutzbar!<br>
<br>
Beispiel f&uuml;r eine praktische Anwendung:<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 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>
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.<br>
</div>
</br></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(""));
server.send(200, F("text/html"), html);
}
void handleFastHeatUp() {
static const char fhuHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Fast-Heat-Up-Modus</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char fhuHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char fhuHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Fast-Heat-Up-Modus</h1>
<form action='/updateFast-Heat-Up-Settings' method='POST'>
<h3>Fast-Heat-Up-Modus</h3>
<div style='margin-bottom:15px;'>
<label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>
Fast-Heat-Up aktivieren:
</label>
<input type='checkbox' id='fast-heat-up-aktivieren' name='fastHeatUpAktiv' value='1' {FASTHEATUP_MODE_CHECKBOX}>
</div>
<div style='margin-top:15px;'>
Der Fast-Heat-Up-Modus erm&ouml;glicht es, die Maschine noch schneller aufzuheizen.<br>
Der Kessel wird beim Start auf 130 Grad Celsius erhitzt.<br>
Nachdem die Temperatur erreicht ist, muss ein Flush von ca. 20 Sekunden durchgef&uuml;hrt werden.
</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);
}
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);
}
void handleRoot() {
static const char rootHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>PID-Einstellung</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><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>Zieltemperatur und Offset</h3>
<label for='wasser'>Wasser-Setpoint (&deg;C):</label>
<input type='text' id='wasser' name='wasser' value='{WASSER}'>
<label for='offsetWasser'>Wasser-Offset (&deg;C):</label>
<input type='text' id='offsetWasser' name='offsetWasser' value='{OFFSET_WASSER}'>
<label for='dampf'>Dampf-Setpoint (&deg;C):</label>
<input type='text' id='dampf' name='dampf' value='{DAMPF}'>
<label for='offsetDampf'>Dampf-Offset (&deg;C):</label>
<input type='text' id='offsetDampf' name='offsetDampf' value='{OFFSET_DAMPF}'>
</br></br></br>
<h3>PID Wasser</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></br></br>
<h3>PID Dampf</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></br>
<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("{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);
}
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);
}
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);
}
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);
}
void handleCharts() {
static const char chartsHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Live-Temperaturverlauf</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
canvas {
width: 90%;
display: block;
margin: 20px auto;
}
.dropdown {
margin: 20px auto;
display: flex;
justify-content: center;
align-items: center;
}
</style>
)rawliteral";
static const char chartsHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char chartsHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Live-Temperaturverlauf</h1>
<div class="dropdown">
<label for="updateRate">Aktualisierungsrate: </label>
</br>
<select id="updateRate">
<option value="1000">1 Sekunde</option>
<option value="2000">2 Sekunden</option>
<option value="3000" selected>3 Sekunden</option>
<option value="4000">4 Sekunden</option>
<option value="5000">5 Sekunden</option>
</select>
</div>
<canvas id="combinedChart" height="1000"></canvas>
<script>
const ctx = document.getElementById('combinedChart').getContext('2d');
let setpointDampf = 100;
let setpointWasser = 80;
const combinedChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Wasser-Temperatur',
data: [],
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 2,
fill: false,
},
{
label: 'Dampf-Temperatur',
data: [],
borderColor: 'rgba(255, 99, 132, 1)',
borderWidth: 2,
fill: false,
},
{
label: 'Setpoint Wasser',
data: [],
borderColor: 'rgba(54, 162, 235, 0.7)',
borderDash: [10, 5],
borderWidth: 2,
fill: false,
},
{
label: 'Setpoint Dampf',
data: [],
borderColor: 'rgba(255, 159, 64, 0.7)',
borderDash: [10, 5],
borderWidth: 2,
fill: false,
}
]
},
options: {
scales: {
y: { beginAtZero: true, max: 200 },
x: { title: { display: true, text: 'Zeit in Sekunden' } }
}
}
});
let time = 0;
let updateInterval = 3000;
let intervalId;
function fetchData() {
fetch('/data')
.then(response => response.json())
.then(data => {
setpointWasser = data.setpointwasser;
setpointDampf = data.setpointdampf;
time += updateInterval / 1000;
if (combinedChart.data.labels.length > 300) {
combinedChart.data.labels.shift();
combinedChart.data.datasets[0].data.shift();
combinedChart.data.datasets[1].data.shift();
combinedChart.data.datasets[2].data.shift();
combinedChart.data.datasets[3].data.shift();
}
combinedChart.data.labels.push(time);
combinedChart.data.datasets[0].data.push(data.wasser);
combinedChart.data.datasets[1].data.push(data.dampf);
combinedChart.data.datasets[2].data.push(setpointWasser);
combinedChart.data.datasets[3].data.push(setpointDampf);
combinedChart.update();
})
.catch(err => console.error(err));
}
function startFetching() {
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(fetchData, updateInterval);
}
document.getElementById('updateRate').addEventListener('change', function(event) {
updateInterval = parseInt(event.target.value);
startFetching();
});
startFetching();
</script>
</body>
</html>
)rawliteral";
String html = FPSTR(chartsHtml);
html += FPSTR(commonStyle);
html += FPSTR(chartsHtml2);
html += FPSTR(commonNav);
html += FPSTR(chartsHtml3);
server.send(200, F("text/html"), html);
}
/*
---------------------------------------------------
AB HIER: NEUE FUNKTIONEN FÜR ZWEI TEMPERATURPROFILE
---------------------------------------------------
*/
// Struktur für ein Profil mit 4 Werten
struct TemperatureProfile {
double setpointWasser;
double offsetWasser;
double setpointDampf;
double offsetDampf;
};
void handleProfiles() {
static const char profilesHtml[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<title>Profile</title><meta name='viewport' content='width=device-width, initial-scale=1.0'><meta name='viewport' content='width=device-width, initial-scale=1.0'>
)rawliteral";
static const char profilesHtml2[] PROGMEM = R"rawliteral(
</head>
)rawliteral";
static const char profilesHtml3[] PROGMEM = R"rawliteral(
<body>
<h1>Temperaturprofile</h1>
<form>
Es k&ouml;nnen zwei unabh&auml;ngige Profile angelegt, bzw. verwaltet werden.<br>
In den Profilen werden Temperaturen und Offsets gespeichert.
</form>
<form action='/saveProfile1' method='POST'>
<h3>Profil 1</h3>
Aktuelle Temperatureinstellungen im <b>Profil 1</b> speichern.<br><br>
<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><br>
<input type='submit' value='Profil 1 laden'>
</form>
<form action='/saveProfile2' method='POST'>
<h3>Profil 2</h3>
Aktuelle Temperatureinstellungen im <b>Profil 2</b> speichern.<br><br>
<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><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);
page += FPSTR(profilesHtml3);
server.send(200, F("text/html"), page);
}
void handleSaveProfile1() {
TemperatureProfile p;
p.setpointWasser = SetpointWasser;
p.offsetWasser = OffsetWasser;
p.setpointDampf = SetpointDampf;
p.offsetDampf = OffsetDampf;
EEPROM.put(EEPROM_ADDR_PROFILE_1, p);
EEPROM.commit();
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}
void handleLoadProfile1() {
TemperatureProfile p;
EEPROM.get(EEPROM_ADDR_PROFILE_1, p);
SetpointWasser = p.setpointWasser;
OffsetWasser = p.offsetWasser;
SetpointDampf = p.setpointDampf;
OffsetDampf = p.offsetDampf;
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
EEPROM.commit();
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}
void handleSaveProfile2() {
TemperatureProfile p;
p.setpointWasser = SetpointWasser;
p.offsetWasser = OffsetWasser;
p.setpointDampf = SetpointDampf;
p.offsetDampf = OffsetDampf;
EEPROM.put(EEPROM_ADDR_PROFILE_2, p);
EEPROM.commit();
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}
void handleLoadProfile2() {
TemperatureProfile p;
EEPROM.get(EEPROM_ADDR_PROFILE_2, p);
SetpointWasser = p.setpointWasser;
OffsetWasser = p.offsetWasser;
SetpointDampf = p.setpointDampf;
OffsetDampf = p.offsetDampf;
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
EEPROM.commit();
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
server.sendHeader(F("Location"), F("/profiles"));
server.send(303);
}