1851 lines
59 KiB
Arduino
1851 lines
59 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.3.2";
|
||
String versionHersteller = "Thomas Mü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
|
||
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
|
||
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());
|
||
}
|
||
|
||
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>
|
||
)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";
|
||
|
||
// Notiz:
|
||
// Die Daten werden aus dem EEPROM gelesen, da das auslesen der aktuellen WiFi-Konfig
|
||
// auf manchen Wemos D1 Minis zu Problemen / Abstürzen geführt hat!
|
||
|
||
// 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 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>
|
||
)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>
|
||
)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!"));
|
||
// 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);
|
||
}
|
||
|
||
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 }; // 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);
|
||
}
|
||
|
||
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>\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: 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("/autoTuneDampf", []() {
|
||
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>AutoTune Dampf</title>\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>\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 action='/autoTuneWasser' method='POST' >\n"
|
||
"<h3>Automatisches PID-Tuning für Wasser</h3>\n"
|
||
"<input type='submit' value='Tuning starten'>\n"
|
||
"</form>\n"
|
||
"<form action='/autoTuneDampf' method='POST' >\n"
|
||
"<h3>Automatisches PID-Tuning für Dampf</h3>\n"
|
||
"<input type='submit' value='Tuning starten'>\n"
|
||
"</form>\n"
|
||
"</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>\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ührt werden.<br>\n"
|
||
"Nach dem Update wird ein automatischer Neustart durchgeführt.<br>\n"
|
||
"Sollte der Neustart nicht erfolgen, so kann dieser auch durch kurzzeitiges Trennen der Stromversorgung erfolgen.<br>\n"
|
||
"<br>\n"
|
||
"<input type='file' name='firmware'>\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, []() {
|
||
// Antwortseite
|
||
static const char updateDone[] PROGMEM = R"rawliteral(
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Firmware-Update</title>
|
||
)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);
|
||
|
||
// 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!");
|
||
}
|
||
}
|
||
|
||
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; // NEU: Reset
|
||
passMatchPos = 0; // NEU: Reset
|
||
if (!Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000)) {
|
||
Update.printError(Serial);
|
||
}
|
||
}
|
||
else if (upload.status == UPLOAD_FILE_WRITE) {
|
||
// NEU: 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 {
|
||
// Falls ein Teil passte, schauen wir, ob wir direkt wieder
|
||
// bei 0 anfangen oder evtl. der aktuelle char mit dem ersten
|
||
// Kennwort-Char matcht
|
||
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) {
|
||
// Am Ende nochmals prüfen, ob Kennwort vorhanden ist
|
||
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>
|
||
)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);
|
||
|
||
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) {
|
||
// 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();
|
||
|
||
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();
|
||
|
||
// Anschließend noch WiFi-Verbindung prüfen
|
||
checkWifiConnection();
|
||
}
|
||
}
|
||
|
||
void updateDisplay() {
|
||
if (!autoTuneWasserActive && !autoTuneDampfActive) {
|
||
if (delayDisplayUpdate && (millis() - shotEndTime < 2000)) {
|
||
return;
|
||
} else {
|
||
delayDisplayUpdate = false;
|
||
}
|
||
|
||
display.clearDisplay();
|
||
// Optional: Signalanzeige WiFi: 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();
|
||
}
|
||
}
|
||
|
||
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_DAMPF, KpWasser);
|
||
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiWasser);
|
||
EEPROM.put(EEPROM_ADDR_KD_DAMPF, 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_WASSER, KpDampf);
|
||
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiDampf);
|
||
EEPROM.put(EEPROM_ADDR_KD_WASSER, 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() {
|
||
// HTML für /info in PROGMEM
|
||
static const char infoHtml[] PROGMEM = R"rawliteral(
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>Info</title>
|
||
)rawliteral";
|
||
|
||
static const char infoHtml2[] PROGMEM = R"rawliteral(
|
||
</head>
|
||
)rawliteral";
|
||
|
||
static const char infoHtml3[] PROGMEM = R"rawliteral(
|
||
<body>
|
||
<h1>Info</h1>
|
||
<form action='/updateInfoSettings' method='POST'>
|
||
<h3>Geräteinfo</h3>
|
||
Die Geräteinformationen werden beim Start der Maschine, bzw. PID-Controllers im Display angezeigt.
|
||
<br><br>
|
||
<label for='hersteller'>Hersteller:</label>
|
||
<input type='text' id='hersteller' name='hersteller' value='{HERSTELLER}'>
|
||
|
||
<label for='modell'>Modell:</label>
|
||
<input type='text' id='modell' name='modell' value='{MODELL}'>
|
||
|
||
<label for='zusatz'>Zusatz (z.B. Limited Edition):</label>
|
||
<input type='text' id='zusatz' name='zusatz' value='{ZUSATZ}'>
|
||
</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ücksetzen'>
|
||
</form>
|
||
|
||
<form action='/resetShots' method='POST'>
|
||
<h3>Shots: (Bezüge über 20 Sekunden)</h3>
|
||
<p>{SHOT_COUNT}</p>
|
||
<input type='submit' value='Zurücksetzen'>
|
||
</form>
|
||
<form>
|
||
<h3>Systeminfo</h3>
|
||
Freier Heap: {FREE_HEAP} Bytes<br>
|
||
SDK-Version: {SDK_VERSION}<br>
|
||
Boot-Version: {BOOT_VERSION}<br>
|
||
CPU-Takt: {CPU_MHZ} MHz<br>
|
||
Sketch-Größ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);
|
||
|
||
// Strings 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));
|
||
|
||
// Systeminformationen
|
||
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>
|
||
)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>
|
||
<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:
|
||
<input type='checkbox' id='dynamicEcoMode' name='dynamicEcoMode' value='1' {DYNAMIC_ECO_MODE_CHECKBOX}>
|
||
</label>
|
||
</div>
|
||
<div style='margin-top:15px;'>
|
||
Der dynamische Eco-Modus verhindert, dass die Maschine bis in die Unendlichkeit eine gewisse Temperatur aufrecht erhält.<br>
|
||
Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollständig beendet wird.<br>
|
||
<br>
|
||
Beispiel fü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önnen. Die Maschine ist nun allerdings schon auf 60 Grad herabgekühlt und er muss erneut das Aufheizen abwarten ...<br>
|
||
Markus passiert das gleiche, doch er hat den dynamischen Eco-Modus mit einer initialen Eco-Temperatur von 95 Grad aktiviert - Seine Maschine ist nun zumindest noch bei 80 Grad.<br>
|
||
Markus muss zwar auch warten, kann jedoch noch vor Peter einen Espresso trinken.<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>
|
||
)rawliteral";
|
||
|
||
static const char fhuHtml2[] PROGMEM = R"rawliteral(
|
||
</head>
|
||
)rawliteral";
|
||
|
||
static const char fhuHtml3[] PROGMEM = R"rawliteral(
|
||
<body>
|
||
<h1>Fast-Heat-Up-Modus</h1>
|
||
<form action='/updateFast-Heat-Up-Settings' method='POST'>
|
||
<h3>Fast-Heat-Up-Modus</h3>
|
||
<div style='margin-bottom:15px;'>
|
||
<label for='fast-heat-up-aktivieren' style='display:inline-block; margin-right:10px;'>
|
||
Fast-Heat-Up aktivieren:
|
||
</label>
|
||
<input type='checkbox' id='fast-heat-up-aktivieren' name='fastHeatUpAktiv' value='1' {FASTHEATUP_MODE_CHECKBOX}>
|
||
</div>
|
||
<div style='margin-top:15px;'>
|
||
Der Fast-Heat-Up-Modus ermöglicht es, die Maschine noch schneller aufzuheizen.<br>
|
||
Der Kessel wird beim Start auf 130 Grad Celsius erhitzt.<br>
|
||
Nachdem die Temperatur erreicht ist, muss ein Flush von ca. 20 Sekunden durchgeführt werden.
|
||
</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>
|
||
)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 (°C):</label>
|
||
<input type='text' id='wasser' name='wasser' value='{WASSER}'>
|
||
|
||
<label for='offsetWasser'>Wasser-Offset (°C):</label>
|
||
<input type='text' id='offsetWasser' name='offsetWasser' value='{OFFSET_WASSER}'>
|
||
|
||
<label for='dampf'>Dampf-Setpoint (°C):</label>
|
||
<input type='text' id='dampf' name='dampf' value='{DAMPF}'>
|
||
|
||
<label for='offsetDampf'>Dampf-Offset (°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>
|
||
<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 => {
|
||
// Hier werden die Setpoint-Werte aus dem Response übernommen:
|
||
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);
|
||
}
|