2481 lines
87 KiB
Arduino
2481 lines
87 KiB
Arduino
/************************************************************************************
|
|
* Dual-PID Controller Firmware
|
|
* Version 2.5.2
|
|
*
|
|
* Dieses Programm steuert Wasser- und Dampftemperatur über zwei unabhängige PID-Regler
|
|
* mithilfe eines MAX6675-Thermo-Elements (Dampf) und eines NTC-Sensors (Wasser).
|
|
* Zusätzlich werden diverse Funktionen wie Eco-Modus, Profile, Fast-Heat-Up und
|
|
* WLAN-Funktionen für eine Weboberfläche bereitgestellt.
|
|
*
|
|
* Compiler- und Hardware-Umgebung:
|
|
* - ESP8266: Wemos D1 mini, 4MB
|
|
* - Bibliotheken: Wire, Adafruit_GFX, Adafruit_SH110X, EEPROM, PID_v1, PID_AutoTune_v0,
|
|
* max6675, ESP8266WiFi, ESP8266WebServer
|
|
************************************************************************************/
|
|
|
|
#include <Wire.h>
|
|
#include <Adafruit_GFX.h>
|
|
#include <Adafruit_SH110X.h>
|
|
#include <EEPROM.h>
|
|
#include <PID_v1.h>
|
|
#include <PID_AutoTune_v0.h>
|
|
#include "max6675.h"
|
|
#include <ESP8266WiFi.h>
|
|
#include <ESP8266WebServer.h>
|
|
|
|
/************************************************************************************
|
|
* Hardware-Pinbelegung
|
|
************************************************************************************/
|
|
|
|
#define SSR_WASSER_PIN D1 // SSR für Wasser-Heizung
|
|
#define SSR_DAMPF_PIN D2 // SSR für Dampf-Heizung
|
|
#define SHOT_TIMER_PIN D8 // Input für Shot-Timer
|
|
#define OLED_SDA D3 // SDA-Leitung für OLED
|
|
#define OLED_SCK D4 // SCK-Leitung für OLED
|
|
#define ANALOG_NTC_PIN A0 // Analoger Eingang für den NTC-Sensor
|
|
|
|
// Display-Objekt (Adafruit SH1106G)
|
|
Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire);
|
|
|
|
/************************************************************************************
|
|
* MAX6675 (Dampf-Temperatursensor)
|
|
************************************************************************************/
|
|
|
|
#define MAX6675_SCK D5 // Serial Clock
|
|
#define MAX6675_CS D6 // Chip Select
|
|
#define MAX6675_SO D7 // Serial Out
|
|
MAX6675 thermocouple(MAX6675_SCK, MAX6675_CS, MAX6675_SO);
|
|
|
|
/************************************************************************************
|
|
* Firmware-Informationen
|
|
************************************************************************************/
|
|
|
|
String version = "2.5.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>";
|
|
|
|
// Demo-Modus: Hier können bestimmte Funktionen (WLAN-Anpassungen etc.) eingeschränkt werden.
|
|
bool demoModus = false;
|
|
|
|
// Dieses Kennwort muss in einer Firmware-Binärdatei vorhanden sein, damit ein Update gültig ist.
|
|
static const char FIRMWARE_PASSWORD[] = "FWKennwort123";
|
|
static bool passwordFound = false;
|
|
static int passMatchPos = 0;
|
|
const int passLength = sizeof(FIRMWARE_PASSWORD) - 1;
|
|
|
|
/************************************************************************************
|
|
* Geräte-Infos
|
|
************************************************************************************/
|
|
|
|
char infoHersteller[50];
|
|
char infoModell[50];
|
|
char infoZusatz[50];
|
|
|
|
/************************************************************************************
|
|
* NTC-Sensor-Parameter (Wasser)
|
|
* - A0 wird verwendet, um die Temperatur über einen Spannungsteiler zu messen
|
|
************************************************************************************/
|
|
|
|
const double V_SUPPLY = 3.3; // Versorgungsspannung
|
|
const double ADC_REF = 3.3; // ADC-Referenz
|
|
const double R_FIXED = 4700.0; // Festwiderstand 4,7 kΩ
|
|
|
|
// Daten des NTC
|
|
const double T0 = 298.15; // Referenztemperatur in Kelvin (25°C)
|
|
const double R0 = 50000.0; // NTC-Widerstand bei 25°C
|
|
const double beta = 3950.0; // Beta-Wert
|
|
|
|
/************************************************************************************
|
|
* Temperatur-Profil-Struktur
|
|
* - Speichert Wasser- und Dampf-Setpoint, Eco-Einstellungen und Profilname
|
|
************************************************************************************/
|
|
|
|
struct TemperatureProfile
|
|
{
|
|
double setpointWasser;
|
|
double setpointDampf;
|
|
int ecoModeMinutes;
|
|
bool dynamicEcoActive;
|
|
int dampfVerzoegerung;
|
|
char profileName[20];
|
|
};
|
|
|
|
/************************************************************************************
|
|
* PID-Regler-Konfiguration
|
|
************************************************************************************/
|
|
|
|
double SetpointDampf, InputDampf, OutputDampf;
|
|
double SetpointWasser, InputWasser, OutputWasser;
|
|
double OffsetDampf = 0.0, OffsetWasser = 0.0;
|
|
|
|
// PID-Variablen (Regelungsparameter)
|
|
double KpDampf = 2.0, KiDampf = 5.0, KdDampf = 1.0;
|
|
double KpWasser = 2.0, KiWasser = 5.0, KdWasser = 1.0;
|
|
|
|
// Instanziierung der PID-Regler
|
|
PID pidDampf(&InputDampf, &OutputDampf, &SetpointDampf, KpDampf, KiDampf, KdDampf, DIRECT);
|
|
PID pidWasser(&InputWasser, &OutputWasser, &SetpointWasser, KpWasser, KiWasser, KdWasser, DIRECT);
|
|
|
|
/************************************************************************************
|
|
* Standardwerte und Default-Einstellungen
|
|
************************************************************************************/
|
|
|
|
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 = "";
|
|
int defaultDampfVerzoegerung = 0;
|
|
int dampfVerzoegerung = 0; // Dampfverzögerung in Minuten
|
|
|
|
/************************************************************************************
|
|
* Webserver und zugehörige Objekte
|
|
************************************************************************************/
|
|
|
|
ESP8266WebServer server(80); // Webserver auf Port 80
|
|
|
|
// MagicValue zum Erkennen bereits gespeicherter EEPROM-Daten
|
|
const char storageMagicValue[5] = "MGVE";
|
|
|
|
/************************************************************************************
|
|
* EEPROM Adressen für das Speichern/Laden von Werten
|
|
************************************************************************************/
|
|
|
|
const int EEPROM_ADDR_MAGICVALUE = 0; // 5 Bytes (char[5])
|
|
const int EEPROM_ADDR_SETPOINT_DAMPF = 5; // 8 Bytes (double)
|
|
const int EEPROM_ADDR_SETPOINT_WASSER = 13; // 8 Bytes
|
|
const int EEPROM_ADDR_OFFSET_DAMPF = 21; // 8 Bytes
|
|
const int EEPROM_ADDR_OFFSET_WASSER = 29; // 8 Bytes
|
|
const int EEPROM_ADDR_KP_DAMPF = 37; // 8 Bytes
|
|
const int EEPROM_ADDR_KI_DAMPF = 45; // 8 Bytes
|
|
const int EEPROM_ADDR_KD_DAMPF = 53; // 8 Bytes
|
|
const int EEPROM_ADDR_KP_WASSER = 61; // 8 Bytes
|
|
const int EEPROM_ADDR_KI_WASSER = 69; // 8 Bytes
|
|
const int EEPROM_ADDR_KD_WASSER = 77; // 8 Bytes
|
|
const int EEPROM_ADDR_ECOMODE_MINUTES = 85; // 4 Bytes (int)
|
|
const int EEPROM_ADDR_ECOMODE_TEMP_WASSER = 89; // 4 Bytes
|
|
const int EEPROM_ADDR_ECOMODE_TEMP_DAMPF = 93; // 4 Bytes
|
|
const int EEPROM_ADDR_INFO_HERSTELLER = 97; // 50 Bytes (char[50])
|
|
const int EEPROM_ADDR_INFO_MODELL = 147; // 50 Bytes
|
|
const int EEPROM_ADDR_INFO_ZUSATZ = 197; // 50 Bytes
|
|
const int EEPROM_ADDR_FASTHEATUP_DATA = 247; // 1 Byte (bool)
|
|
const int EEPROM_ADDR_RUNTIME = 248; // 4 Bytes (unsigned long)
|
|
const int EEPROM_ADDR_SHOTCOUNTER = 252; // 4 Bytes
|
|
const int EEPROM_ADDR_DYNAMIC_ECO_MODE = 256; // 1 Byte (bool)
|
|
const int EEPROM_ADDR_PROFILE_1 = 257; // 50 Bytes für Profil 1
|
|
const int EEPROM_ADDR_PROFILE_2 = 307; // 50 Bytes für Profil 2
|
|
const int EEPROM_ADDR_TUNING_STEP = 357; // 8 Bytes (double)
|
|
const int EEPROM_ADDR_TUNING_NOISE = 365; // 8 Bytes (double)
|
|
const int EEPROM_ADDR_TUNING_STARTVALUE = 373; // 8 Bytes (double)
|
|
const int EEPROM_ADDR_TUNING_LOOKBACK = 381; // 4 Bytes (unsigned int)
|
|
const int EEPROM_ADDR_WIFI_CONFIG_MAGIC = 385;
|
|
const int EEPROM_ADDR_WIFI_CONFIG_DATA = 390;
|
|
const int EEPROM_ADDR_STEAM_DELAY = 527;
|
|
|
|
/************************************************************************************
|
|
* Eco-Mode Variablen
|
|
************************************************************************************/
|
|
|
|
unsigned long lastShotTime = 0; // Wann wurde zuletzt ein Shot beendet
|
|
int ecoModeMinutes = 0; // Zeit (Minuten) bis Eco-Modus
|
|
bool ecoModeAktiv = 0; // Flag, ob Eco läuft
|
|
int ecoModeTempWasser = 50; // Zieltemp Wasser im Eco
|
|
int ecoModeTempDampf = 50; // Zieltemp Dampf im Eco
|
|
bool dynamicEcoActive = false;
|
|
unsigned long ecoModeActivatedTime = 0;
|
|
|
|
/************************************************************************************
|
|
* Fast-Heat-Up Variablen
|
|
************************************************************************************/
|
|
|
|
bool fastHeatUpAktiv = 0; // Ist Fast-Heat-Up aktiviert?
|
|
bool fastHeatUpHeating = 0; // Wird gerade hochgeheizt (Wasser >130)?
|
|
int fastHeatUpSetpoint = 130;
|
|
|
|
/************************************************************************************
|
|
* Auto-Tune-Einstellungen
|
|
************************************************************************************/
|
|
|
|
double tuningStep;
|
|
double tuningNoise;
|
|
double tuningStartValue;
|
|
unsigned int tuningLookBack;
|
|
const double defaultTuningStep = 2.0;
|
|
const double defaultTuningNoise = 1.0;
|
|
const double defaultTuningStartValue = 2.0;
|
|
const unsigned int defaultTuningLookBack = 5;
|
|
|
|
PID_ATune* autoTuneWasser;
|
|
PID_ATune* autoTuneDampf;
|
|
bool autoTuneWasserActive = false;
|
|
bool autoTuneDampfActive = false;
|
|
|
|
/************************************************************************************
|
|
* Shot-Timer Variablen
|
|
************************************************************************************/
|
|
|
|
unsigned long shotStartTime = 0;
|
|
unsigned long shotEndTime = 0;
|
|
bool shotActive = false;
|
|
bool delayDisplayUpdate = false;
|
|
|
|
/************************************************************************************
|
|
* Shot-Zähler und Betriebszeit
|
|
************************************************************************************/
|
|
|
|
const int runtimeAddress = 500; // 4 Bytes (unsigned long)
|
|
const int shotCounterAddress = 504; // 4 Bytes (unsigned long)
|
|
unsigned long shotCounter = 0; // Shots > 20 Sekunden
|
|
unsigned long totalRuntime = 0; // Gesamt-Betriebszeit in Sekunden
|
|
unsigned long lastRuntimeSave = 0; // Wann zuletzt gespeichert
|
|
const unsigned long runtimeSaveInterval = 60000; // Speichern alle 60s
|
|
|
|
/************************************************************************************
|
|
* Verzögerungen für Anzeigen
|
|
************************************************************************************/
|
|
|
|
int delayInit1 = 1500; // Display-Bild am Anfang
|
|
int delayInit2 = 3000; // Warte für Anzeige
|
|
unsigned long previousMillis = 0; // für Intervall z.B. 250ms
|
|
const unsigned long interval = 250;
|
|
|
|
/************************************************************************************
|
|
* WiFi Signal Bitmap (Display-Anzeige)
|
|
************************************************************************************/
|
|
|
|
static const unsigned char PROGMEM wifiSymbol[] = {
|
|
0x1f, 0xc0, 0x20, 0x20, 0x4f, 0x90, 0x90, 0x48,
|
|
0x27, 0x20, 0x08, 0x80, 0x02, 0x00
|
|
};
|
|
|
|
/************************************************************************************
|
|
* Gemeinsame CSS-Styles (PROGMEM)
|
|
************************************************************************************/
|
|
|
|
static const char commonStyle[] PROGMEM = R"rawliteral(
|
|
<style>
|
|
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 (PROGMEM)
|
|
************************************************************************************/
|
|
|
|
static const char commonNav[] PROGMEM = R"rawliteral(
|
|
<nav>
|
|
<span class="hamburger" onclick="toggleMenu()">☰</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="/netzwerk">WiFi</a>
|
|
<a href="/updateFirmware">Firmware</a>
|
|
</nav>
|
|
)rawliteral";
|
|
|
|
/************************************************************************************
|
|
* WiFi-Konfigurationsstrukturen und Funktionen
|
|
************************************************************************************/
|
|
|
|
struct WiFiConfig
|
|
{
|
|
char ssid[32] = "";
|
|
char password[64] = "";
|
|
bool useStaticIP = false;
|
|
IPAddress staticIP = IPAddress(192, 168, 4, 1);
|
|
IPAddress gateway = IPAddress(192, 168, 4, 1);
|
|
IPAddress subnet = IPAddress(255, 255, 255, 0);
|
|
IPAddress dns = IPAddress(8, 8, 8, 8);
|
|
};
|
|
|
|
// Liest die WLAN-Konfiguration aus dem EEPROM
|
|
bool loadWiFiConfig(WiFiConfig& config)
|
|
{
|
|
char magic[5] = { 0 };
|
|
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
|
|
|
|
// Falls MagicValue nicht übereinstimmt, Standardwerte setzen
|
|
if (strncmp(magic, storageMagicValue, 4) != 0)
|
|
{
|
|
strcpy(config.ssid, "");
|
|
strcpy(config.password, "");
|
|
config.useStaticIP = false;
|
|
config.staticIP = IPAddress(192, 168, 4, 1);
|
|
config.gateway = IPAddress(192, 168, 4, 1);
|
|
config.subnet = IPAddress(255, 255, 255, 0);
|
|
config.dns = IPAddress(8, 8, 8, 8);
|
|
return false;
|
|
}
|
|
|
|
// Laden der gespeicherten Daten
|
|
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
|
|
return true;
|
|
}
|
|
|
|
// Schreibt die WLAN-Konfiguration ins EEPROM
|
|
void saveWiFiConfig(const WiFiConfig& config)
|
|
{
|
|
EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_MAGIC, storageMagicValue);
|
|
EEPROM.put(EEPROM_ADDR_WIFI_CONFIG_DATA, config);
|
|
EEPROM.commit();
|
|
}
|
|
|
|
// Aktiviert den Access Point Modus und zeigt IP an
|
|
void startAPMode()
|
|
{
|
|
WiFi.softAP(F("Dual PID-Controller"));
|
|
Serial.print(F("AP-Mode IP: "));
|
|
Serial.println(WiFi.softAPIP());
|
|
}
|
|
|
|
// Webserver-Handler: WLAN-Konfigurations-Seite
|
|
void handleWiFiConfig()
|
|
{
|
|
static const char wifiConfigForm[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>WLAN-Konfiguration</title><meta name='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";
|
|
|
|
WiFiConfig currentConfig;
|
|
char magic[5] = { 0 };
|
|
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_MAGIC, magic);
|
|
bool configValid = (strncmp(magic, storageMagicValue, 4) == 0);
|
|
|
|
// Wenn configValid, lädt die gespeicherten Daten, sonst Standardwerte
|
|
if (configValid)
|
|
{
|
|
EEPROM.get(EEPROM_ADDR_WIFI_CONFIG_DATA, currentConfig);
|
|
}
|
|
else
|
|
{
|
|
strcpy(currentConfig.ssid, "");
|
|
strcpy(currentConfig.password, "");
|
|
currentConfig.useStaticIP = false;
|
|
currentConfig.staticIP = IPAddress(192, 168, 4, 1);
|
|
currentConfig.gateway = IPAddress(192, 168, 4, 1);
|
|
currentConfig.subnet = IPAddress(255, 255, 255, 0);
|
|
}
|
|
|
|
// HTML zusammenbauen
|
|
String html = FPSTR(wifiConfigForm);
|
|
html += FPSTR(commonStyle);
|
|
html += FPSTR(wifiConfigForm2);
|
|
html += FPSTR(commonNav);
|
|
html += FPSTR(wifiConfigForm3);
|
|
|
|
html.replace("{SSID}", currentConfig.ssid);
|
|
|
|
// Im Demo-Modus wird das Passwort durch Sternchen ersetzt
|
|
if(!demoModus)
|
|
{
|
|
html.replace("{PASSWORD}", currentConfig.password);
|
|
}
|
|
else
|
|
{
|
|
html.replace("{PASSWORD}", "*** *** *** ***");
|
|
}
|
|
|
|
html.replace("{IP}", currentConfig.staticIP.toString());
|
|
html.replace("{GATEWAY}", currentConfig.gateway.toString());
|
|
html.replace("{SUBNET}", currentConfig.subnet.toString());
|
|
html.replace("{DHCP_CHECKED}", currentConfig.useStaticIP ? "" : "checked");
|
|
html.replace("{STATIC_CHECKED}", currentConfig.useStaticIP ? "checked" : "");
|
|
html.replace("{STATIC_DISPLAY}", currentConfig.useStaticIP ? "block" : "none");
|
|
|
|
server.send(200, F("text/html"), html);
|
|
}
|
|
|
|
// Handler zum Speichern der WLAN-Konfiguration
|
|
void handleSaveWiFiConfig()
|
|
{
|
|
if (!server.hasArg(F("ssid")))
|
|
{
|
|
server.send(500, F("text/plain"), F("Fehler: SSID nicht gefunden"));
|
|
return;
|
|
}
|
|
|
|
WiFiConfig newConfig;
|
|
strncpy(newConfig.ssid, server.arg(F("ssid")).c_str(), sizeof(newConfig.ssid));
|
|
strncpy(newConfig.password, server.arg(F("password")).c_str(), sizeof(newConfig.password));
|
|
newConfig.useStaticIP = (server.arg(F("ipType")) == "static");
|
|
|
|
if (newConfig.useStaticIP)
|
|
{
|
|
newConfig.staticIP.fromString(server.arg(F("ip")));
|
|
newConfig.gateway.fromString(server.arg(F("gateway")));
|
|
newConfig.subnet.fromString(server.arg(F("subnet")));
|
|
}
|
|
|
|
// Im Demo-Modus wird nichts gespeichert, sonst schon
|
|
if(!demoModus) { saveWiFiConfig(newConfig); }
|
|
|
|
static const char saveConfigHtml[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Einstellungen gespeichert</title><meta name='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();
|
|
}
|
|
|
|
// Handler, um den AP-Modus per Knopfdruck zu erzwingen
|
|
void handleForceAPMode()
|
|
{
|
|
// Leer-Konfiguration speichern, damit beim nächsten Start AP aktiv wird
|
|
WiFiConfig emptyConfig;
|
|
if(!demoModus) { saveWiFiConfig(emptyConfig); }
|
|
|
|
static const char forceAPHtml[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>AP-Modus aktivieren</title><meta name='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();
|
|
}
|
|
|
|
// Prüfung der WiFi-Verbindung (Reconnect falls getrennt)
|
|
void checkWifiConnection()
|
|
{
|
|
if (WiFi.status() != WL_CONNECTED)
|
|
{
|
|
WiFi.reconnect();
|
|
if (WiFi.waitForConnectResult() != WL_CONNECTED)
|
|
{
|
|
Serial.println(F("WiFi-Verbindung kann nicht hergestellt werden!"));
|
|
}
|
|
}
|
|
}
|
|
|
|
// WiFi-Symbol aufs Display zeichnen
|
|
void drawwifiSymbol(int16_t x, int16_t y)
|
|
{
|
|
display.drawBitmap(x, y, wifiSymbol, 13, 7, SH110X_WHITE);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler zum Rücksetzen der Betriebszeit
|
|
************************************************************************************/
|
|
|
|
void handleResetRuntime()
|
|
{
|
|
totalRuntime = 0;
|
|
EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime);
|
|
EEPROM.commit();
|
|
server.sendHeader(F("Location"), F("/info"));
|
|
server.send(303);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler zum Rücksetzen des Shotzählers
|
|
************************************************************************************/
|
|
|
|
void handleResetShots()
|
|
{
|
|
shotCounter = 0;
|
|
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
|
|
EEPROM.commit();
|
|
server.sendHeader(F("Location"), F("/info"));
|
|
server.send(303);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Variable für Systemstartzeit (relevant für die Dampf-Verzögerung)
|
|
************************************************************************************/
|
|
|
|
unsigned long startupTime;
|
|
|
|
/************************************************************************************
|
|
* setup(): Initialisierung des Systems
|
|
************************************************************************************/
|
|
|
|
void setup()
|
|
{
|
|
Serial.begin(115200);
|
|
EEPROM.begin(1024);
|
|
|
|
pinMode(SSR_DAMPF_PIN, OUTPUT);
|
|
pinMode(SSR_WASSER_PIN, OUTPUT);
|
|
pinMode(SHOT_TIMER_PIN, INPUT_PULLUP);
|
|
|
|
// Prüfen, ob MagicValue bereits im EEPROM liegt
|
|
char storedMagicValue[5] = { 0 };
|
|
bool magicValueVorhanden = false;
|
|
EEPROM.get(EEPROM_ADDR_MAGICVALUE, storedMagicValue);
|
|
|
|
if (strncmp(storedMagicValue, storageMagicValue, 4) == 0)
|
|
{
|
|
magicValueVorhanden = true;
|
|
}
|
|
else
|
|
{
|
|
magicValueVorhanden = false;
|
|
EEPROM.put(EEPROM_ADDR_MAGICVALUE, storageMagicValue);
|
|
EEPROM.commit();
|
|
}
|
|
|
|
// Werte aus dem EEPROM laden, wenn vorhanden, sonst mit Default-Werten initialisieren
|
|
if (magicValueVorhanden)
|
|
{
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
EEPROM.get(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
|
|
EEPROM.get(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
|
|
EEPROM.get(EEPROM_ADDR_KP_DAMPF, KpDampf);
|
|
EEPROM.get(EEPROM_ADDR_KI_DAMPF, KiDampf);
|
|
EEPROM.get(EEPROM_ADDR_KD_DAMPF, KdDampf);
|
|
EEPROM.get(EEPROM_ADDR_KP_WASSER, KpWasser);
|
|
EEPROM.get(EEPROM_ADDR_KI_WASSER, KiWasser);
|
|
EEPROM.get(EEPROM_ADDR_KD_WASSER, KdWasser);
|
|
EEPROM.get(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
|
|
EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
|
|
EEPROM.get(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
|
|
EEPROM.get(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
|
|
EEPROM.get(EEPROM_ADDR_INFO_MODELL, infoModell);
|
|
EEPROM.get(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
|
|
EEPROM.get(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
|
|
EEPROM.get(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
|
|
EEPROM.get(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
|
|
EEPROM.get(EEPROM_ADDR_TUNING_STEP, tuningStep);
|
|
EEPROM.get(EEPROM_ADDR_TUNING_NOISE, tuningNoise);
|
|
EEPROM.get(EEPROM_ADDR_TUNING_STARTVALUE, tuningStartValue);
|
|
EEPROM.get(EEPROM_ADDR_TUNING_LOOKBACK, tuningLookBack);
|
|
}
|
|
else
|
|
{
|
|
SetpointDampf = defaultSetpointDampf;
|
|
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
|
|
SetpointWasser = defaultSetpointWasser;
|
|
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
|
|
OffsetDampf = defaultOffsetDampf;
|
|
EEPROM.put(EEPROM_ADDR_OFFSET_DAMPF, OffsetDampf);
|
|
|
|
OffsetWasser = defaultOffsetWasser;
|
|
EEPROM.put(EEPROM_ADDR_OFFSET_WASSER, OffsetWasser);
|
|
|
|
KpDampf = defaultKpDampf;
|
|
EEPROM.put(EEPROM_ADDR_KP_DAMPF, KpDampf);
|
|
KiDampf = defaultKiDampf;
|
|
EEPROM.put(EEPROM_ADDR_KI_DAMPF, KiDampf);
|
|
KdDampf = defaultKdDampf;
|
|
EEPROM.put(EEPROM_ADDR_KD_DAMPF, KdDampf);
|
|
|
|
KpWasser = defaultKpWasser;
|
|
EEPROM.put(EEPROM_ADDR_KP_WASSER, KpWasser);
|
|
KiWasser = defaultKiWasser;
|
|
EEPROM.put(EEPROM_ADDR_KI_WASSER, KiWasser);
|
|
KdWasser = defaultKdWasser;
|
|
EEPROM.put(EEPROM_ADDR_KD_WASSER, KdWasser);
|
|
|
|
ecoModeMinutes = defaultEcoModeMinutes;
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
|
|
ecoModeTempWasser = defaultEcoModeTempWasser;
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
|
|
ecoModeTempDampf = defaultEcoModeTempDampf;
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
|
|
|
|
strcpy(infoHersteller, defaultInfoHersteller.c_str());
|
|
EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
|
|
strcpy(infoModell, defaultInfoModell.c_str());
|
|
EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell);
|
|
strcpy(infoZusatz, defaultInfoZusatz.c_str());
|
|
EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
|
|
|
|
dynamicEcoActive = false;
|
|
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
|
|
|
|
fastHeatUpAktiv = false;
|
|
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
|
|
|
|
dampfVerzoegerung = defaultDampfVerzoegerung;
|
|
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
|
|
|
|
TemperatureProfile defaultProfile;
|
|
defaultProfile.setpointWasser = defaultSetpointWasser;
|
|
defaultProfile.setpointDampf = defaultSetpointDampf;
|
|
defaultProfile.ecoModeMinutes = defaultEcoModeMinutes;
|
|
defaultProfile.dynamicEcoActive = false;
|
|
defaultProfile.dampfVerzoegerung = defaultDampfVerzoegerung;
|
|
strncpy(defaultProfile.profileName, "Unbenannt", sizeof(defaultProfile.profileName));
|
|
defaultProfile.profileName[sizeof(defaultProfile.profileName) - 1] = '\0';
|
|
EEPROM.put(EEPROM_ADDR_PROFILE_1, defaultProfile);
|
|
EEPROM.put(EEPROM_ADDR_PROFILE_2, defaultProfile);
|
|
|
|
tuningStep = defaultTuningStep;
|
|
tuningNoise = defaultTuningNoise;
|
|
tuningStartValue = defaultTuningStartValue;
|
|
tuningLookBack = defaultTuningLookBack;
|
|
EEPROM.put(EEPROM_ADDR_TUNING_STEP, tuningStep);
|
|
EEPROM.put(EEPROM_ADDR_TUNING_NOISE, tuningNoise);
|
|
EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE, tuningStartValue);
|
|
EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK, tuningLookBack);
|
|
|
|
EEPROM.commit();
|
|
}
|
|
|
|
// WiFi-Einstellungen laden und ggf. herstellen
|
|
WiFiConfig wifiConfig;
|
|
bool hasWiFiConfig = loadWiFiConfig(wifiConfig);
|
|
|
|
if (hasWiFiConfig)
|
|
{
|
|
WiFi.mode(WIFI_STA);
|
|
|
|
if (wifiConfig.useStaticIP)
|
|
{
|
|
WiFi.config(wifiConfig.staticIP, wifiConfig.gateway, wifiConfig.subnet);
|
|
}
|
|
|
|
WiFi.begin(wifiConfig.ssid, wifiConfig.password);
|
|
int retries = 0;
|
|
|
|
while (WiFi.status() != WL_CONNECTED && retries < 20)
|
|
{
|
|
delay(500);
|
|
Serial.print(F("."));
|
|
retries++;
|
|
}
|
|
|
|
if (WiFi.status() == WL_CONNECTED)
|
|
{
|
|
Serial.println(F("\nVerbunden!"));
|
|
}
|
|
else
|
|
{
|
|
Serial.println(F("\nKonnte keine Verbindung herstellen!"));
|
|
startAPMode();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
startAPMode();
|
|
}
|
|
|
|
// Betriebszeit und Shotcounter aus EEPROM laden
|
|
EEPROM.get(EEPROM_ADDR_RUNTIME, totalRuntime);
|
|
if (totalRuntime == ULONG_MAX) { totalRuntime = 0; }
|
|
|
|
EEPROM.get(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
|
|
if (shotCounter == ULONG_MAX)
|
|
{
|
|
shotCounter = 0;
|
|
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
|
|
}
|
|
|
|
EEPROM.commit();
|
|
|
|
if (fastHeatUpAktiv)
|
|
{
|
|
fastHeatUpHeating = true;
|
|
}
|
|
|
|
// OLED-Initialisierung
|
|
Wire.begin(OLED_SDA, OLED_SCK);
|
|
if (!display.begin(0x3C, true))
|
|
{
|
|
Serial.println(F("Display konnte nicht initialisiert werden!"));
|
|
}
|
|
|
|
display.clearDisplay();
|
|
display.setCursor(0, 0);
|
|
display.setTextColor(SH110X_WHITE);
|
|
display.setTextSize(1);
|
|
display.println(infoHersteller);
|
|
display.println(infoModell);
|
|
display.println(infoZusatz);
|
|
display.println("");
|
|
display.println(F("Dual-PID-Controller"));
|
|
display.print(F("Version: "));
|
|
display.println(version);
|
|
display.println(F("von Thomas M\201ller"));
|
|
display.display();
|
|
delay(delayInit1);
|
|
|
|
// PID-Regler aktivieren und Parameter setzen
|
|
pidDampf.SetMode(AUTOMATIC);
|
|
pidWasser.SetMode(AUTOMATIC);
|
|
pidDampf.SetTunings(KpDampf, KiDampf, KdDampf);
|
|
pidWasser.SetTunings(KpWasser, KiWasser, KdWasser);
|
|
|
|
// Anzeige zur WiFi-Verbindung
|
|
if (WiFi.status() != WL_CONNECTED)
|
|
{
|
|
display.clearDisplay();
|
|
display.setCursor(0, 0);
|
|
display.setTextColor(SH110X_WHITE);
|
|
display.setTextSize(1);
|
|
display.println(infoHersteller);
|
|
display.println(infoModell);
|
|
display.println(infoZusatz);
|
|
display.println("");
|
|
display.println(F("WiFi-Verbindung:"));
|
|
display.println(F("AP-Modus gestartet"));
|
|
display.println(WiFi.softAPIP());
|
|
}
|
|
else
|
|
{
|
|
display.clearDisplay();
|
|
display.setCursor(0, 0);
|
|
display.setTextColor(SH110X_WHITE);
|
|
display.setTextSize(1);
|
|
display.println(infoHersteller);
|
|
display.println(infoModell);
|
|
display.println(infoZusatz);
|
|
display.println("");
|
|
display.println(F("Webserver gestartet."));
|
|
display.println(F("IP-Adresse:"));
|
|
display.println(WiFi.localIP());
|
|
}
|
|
display.display();
|
|
delay(delayInit2);
|
|
|
|
// Routen für Webserver
|
|
server.on("/", handleRoot);
|
|
server.on("/info", handleInfo);
|
|
server.on("/eco", handleEco);
|
|
server.on("/fast-heat-up", handleFastHeatUp);
|
|
server.on("/updateFast-Heat-Up-Settings", handleFastHeatUpSettings);
|
|
server.on("/updateSettings", handleUpdate);
|
|
server.on("/updateInfoSettings", handleInfoUpdate);
|
|
server.on("/updateEcoSettings", handleEcoUpdate);
|
|
|
|
// AutoTune-Route (Wasser)
|
|
server.on("/autoTuneWasser", []() {
|
|
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>AutoTune Wasser</title><meta name='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);
|
|
});
|
|
|
|
// AutoTune-Route (Dampf)
|
|
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);
|
|
});
|
|
|
|
// Hauptseite PID-Tuning (Info, Starten etc.)
|
|
server.on("/PID-Tuning", HTTP_GET, []() {
|
|
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>PID-Tuning</title><meta name='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ür den PID-Regler (Proportional-, Integral- und Differentialanteil) selbständig zu ermitteln. Dabei analysiert das System mithilfe von Algorithmen das Regelverhalten (z. B. Reaktion auf einen Testimpuls) und passt Kp, Ki und Kd so an, dass gewünschte Kriterien wie kurze Einschwingzeit, geringe Überschwingung und stabile Regelung erreicht werden"
|
|
"<br><br>"
|
|
"Ein AutoTune-Vorgang kann durchaus 10-20 Minuten in Anspruch nehmen!</form>"
|
|
"<form action='/autoTuneWasser' method='POST' >\n"
|
|
"<h3>Automatisches PID-Tuning für Wasser</h3>\n"
|
|
"<input type='submit' value='Tuning starten'>\n"
|
|
"</form>\n"
|
|
"<form action='/autoTuneDampf' method='POST' >\n"
|
|
"<h3>Automatisches PID-Tuning für Dampf</h3>\n"
|
|
"<input type='submit' value='Tuning starten'>\n"
|
|
"</form>\n");
|
|
|
|
// Form für AutoTune-Parameter
|
|
page += F("<form action='/updateAutotuneSettings' method='POST'>\n"
|
|
"<h3>Autotune-Parameter</h3>\n"
|
|
"Hier können die AutoTune-Parameter geändert werden.<br>Änderungen sollten mit Vorsicht durchgeführt werden!<br>"
|
|
"<label for='tuningStep'>Step:</label>\n"
|
|
"<input type='text' id='tuningStep' name='tuningStep' value='{TUNING_STEP}'><br>\n"
|
|
"<label for='tuningNoise'>Noise:</label>\n"
|
|
"<input type='text' id='tuningNoise' name='tuningNoise' value='{TUNING_NOISE}'><br>\n"
|
|
"<label for='tuningStartValue'>StartValue:</label>\n"
|
|
"<input type='text' id='tuningStartValue' name='tuningStartValue' value='{TUNING_STARTVALUE}'><br>\n"
|
|
"<label for='tuningLookBack'>LookBack:</label>\n"
|
|
"<input type='text' id='tuningLookBack' name='tuningLookBack' value='{TUNING_LOOKBACK}'><br>\n"
|
|
"<input type='submit' value='AutoTune-Parameter speichern'>\n"
|
|
"</form>\n"
|
|
"<br><hr><br>"
|
|
"<form>"
|
|
"<h3>Erklärung der Parameter:</h3>\n"
|
|
"<p><strong>Step:</strong><br>\n"
|
|
"Dieser Wert bestimmt, wie stark der Ausgangswert während des Autotune-Vorgangs verändert wird. Mit anderen Worten, er gibt die Größe des Impulses an, der in das System eingespeist wird, um Oszillationen zu erzeugen. Ein zu hoher Wert kann zu übermaßigen Ausschlägen führen, während ein zu niedriger Wert möglicherweise nicht genug Dynamik erzeugt.</p>\n"
|
|
"<p><strong>Noise:</strong><br>\n"
|
|
"Dieser Parameter definiert einen Toleranzbereich (Noise-Band) für die gemessenen Eingangswerte. Kleine Schwankungen, die unterhalb dieses Wertes liegen, werden als Rauschen betrachtet und ignoriert. Das hilft, die Auswirkungen von Messrauschen zu minimieren und sicherzustellen, dass nur signifikante Änderungen in der Systemantwort zur Bestimmung der PID-Parameter herangezogen werden.</p>\n"
|
|
"<p><strong>StartValue:</strong><br>\n"
|
|
"Hier wird der Anfangswert für den Ausgang des Reglers festgelegt, wenn der Autotune-Vorgang startet. Dieser Wert sollte idealerweise im linear arbeitenden Bereich des Systems liegen, da von diesem Ausgangspunkt aus der tuningStep angewendet wird. Er beeinflusst also, in welchem Bereich die anschließenden Impulse liegen.</p>\n"
|
|
"<p><strong>LookBack:</strong><br>\n"
|
|
"Dieser Parameter legt fest, über welchen Zeitraum (in Sekunden) der Algorithmus die vergangene Systemantwort analysiert. Er bestimmt quasi das „Fenster“, über das die Schwingungsperiode und damit die Dynamik des Systems gemessen wird. Ein zu kurzes Fenster könnte zu einer ungenauen Erfassung der Oszillation führen, während ein zu langes Fenster unnötig viel Zeit benötigt.</p>\n"
|
|
"</form>"
|
|
"</body>\n</html>");
|
|
|
|
page.replace("{TUNING_STEP}", String(tuningStep, 2));
|
|
page.replace("{TUNING_NOISE}", String(tuningNoise, 2));
|
|
page.replace("{TUNING_STARTVALUE}", String(tuningStartValue, 2));
|
|
page.replace("{TUNING_LOOKBACK}", String(tuningLookBack));
|
|
|
|
server.send(200, F("text/html"), page);
|
|
});
|
|
|
|
// Speichern der AutoTune-Einstellungen
|
|
server.on("/updateAutotuneSettings", HTTP_POST, []() {
|
|
if (server.hasArg(F("tuningStep")))
|
|
{
|
|
tuningStep = server.arg(F("tuningStep")).toFloat();
|
|
EEPROM.put(EEPROM_ADDR_TUNING_STEP, tuningStep);
|
|
}
|
|
if (server.hasArg(F("tuningNoise")))
|
|
{
|
|
tuningNoise = server.arg(F("tuningNoise")).toFloat();
|
|
EEPROM.put(EEPROM_ADDR_TUNING_NOISE, tuningNoise);
|
|
}
|
|
if (server.hasArg(F("tuningStartValue")))
|
|
{
|
|
tuningStartValue = server.arg(F("tuningStartValue")).toFloat();
|
|
EEPROM.put(EEPROM_ADDR_TUNING_STARTVALUE, tuningStartValue);
|
|
}
|
|
if (server.hasArg(F("tuningLookBack")))
|
|
{
|
|
tuningLookBack = server.arg(F("tuningLookBack")).toInt();
|
|
EEPROM.put(EEPROM_ADDR_TUNING_LOOKBACK, tuningLookBack);
|
|
}
|
|
EEPROM.commit();
|
|
server.sendHeader(F("Location"), F("/PID-Tuning"));
|
|
server.send(303);
|
|
});
|
|
|
|
// Chart-Anzeige-Handler
|
|
server.on("/charts", handleCharts);
|
|
|
|
// JSON-Daten für Temperaturwerte
|
|
server.on("/data", []() {
|
|
String json = F("{");
|
|
json += F("\"wasser\":") + String(InputWasser) + F(",");
|
|
json += F("\"setpointwasser\":") + String(SetpointWasser) + F(",");
|
|
json += F("\"dampf\":") + String(InputDampf) + F(",");
|
|
json += F("\"setpointdampf\":") + String(SetpointDampf);
|
|
json += F("}");
|
|
server.send(200, F("application/json"), json);
|
|
});
|
|
|
|
// Firmware-Update-Seite
|
|
server.on("/updateFirmware", HTTP_GET, []() {
|
|
String page = F("<!DOCTYPE html>\n<html>\n<head>\n<title>Firmware-Update</title><meta name='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ü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);
|
|
});
|
|
|
|
// Firmware-Upload-Handler
|
|
server.on("/update", HTTP_POST, []() {
|
|
static const char updateDone[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Firmware-Update</title><meta name='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)
|
|
{
|
|
// Prüfen auf Firmware-Passwort
|
|
for (size_t i = 0; i < upload.currentSize; i++)
|
|
{
|
|
char c = (char)upload.buf[i];
|
|
if (c == FIRMWARE_PASSWORD[passMatchPos])
|
|
{
|
|
passMatchPos++;
|
|
if (passMatchPos == passLength)
|
|
{
|
|
passwordFound = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (c == FIRMWARE_PASSWORD[0])
|
|
{
|
|
passMatchPos = 1;
|
|
}
|
|
else
|
|
{
|
|
passMatchPos = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Schreibvorgang
|
|
if (Update.write(upload.buf, upload.currentSize) != upload.currentSize)
|
|
{
|
|
Update.printError(Serial);
|
|
}
|
|
}
|
|
else if (upload.status == UPLOAD_FILE_END)
|
|
{
|
|
if (!passwordFound)
|
|
{
|
|
Serial.println("Kennwort nicht gefunden -> Abbruch!");
|
|
Update.end(false);
|
|
}
|
|
else
|
|
{
|
|
if (Update.end(true))
|
|
{
|
|
Serial.printf("Update erfolgreich: %u Bytes\n", upload.totalSize);
|
|
}
|
|
else
|
|
{
|
|
Update.printError(Serial);
|
|
}
|
|
}
|
|
}
|
|
else if (upload.status == UPLOAD_FILE_ABORTED)
|
|
{
|
|
Update.end();
|
|
Serial.println(F("Update abgebrochen."));
|
|
}
|
|
yield();
|
|
});
|
|
|
|
// Handler zum Abbrechen des PID-AutoTune
|
|
server.on("/abbruch-pid-tuning", HTTP_POST, []() {
|
|
static const char abortTuning[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Info</title><meta name='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();
|
|
});
|
|
|
|
// Routen für WLAN-Einstellungen
|
|
server.on("/netzwerk", handleWiFiConfig);
|
|
server.on("/saveWiFiConfig", handleSaveWiFiConfig);
|
|
server.on("/forceAPMode", handleForceAPMode);
|
|
|
|
// Routen zum Zurücksetzen von Betriebszeit und ShotCounter
|
|
server.on("/resetRuntime", handleResetRuntime);
|
|
server.on("/resetShots", handleResetShots);
|
|
|
|
// Routen für Profile
|
|
server.on("/profiles", handleProfiles);
|
|
server.on("/saveProfile1", handleSaveProfile1);
|
|
server.on("/loadProfile1", handleLoadProfile1);
|
|
server.on("/saveProfile2", handleSaveProfile2);
|
|
server.on("/loadProfile2", handleLoadProfile2);
|
|
|
|
// Webserver starten
|
|
server.begin();
|
|
Serial.println(F("Webserver gestartet"));
|
|
startupTime = millis();
|
|
}
|
|
|
|
/************************************************************************************
|
|
* loop(): Hauptschleife
|
|
************************************************************************************/
|
|
|
|
void loop()
|
|
{
|
|
unsigned long currentMillis = millis();
|
|
|
|
// Intervall für Messung/SSR-Schaltung
|
|
if (currentMillis - previousMillis >= interval)
|
|
{
|
|
previousMillis = currentMillis;
|
|
|
|
// Temperaturmessung
|
|
InputDampf = round(thermocouple.readCelsius() + OffsetDampf);
|
|
InputWasser = round(readNTCTemperature() + OffsetWasser);
|
|
|
|
// Falls AutoTune nicht aktiv, normal regeln
|
|
handleAutoTune();
|
|
if (!autoTuneWasserActive) { pidWasser.Compute(); }
|
|
if (!autoTuneDampfActive) { pidDampf.Compute(); }
|
|
|
|
// SSR entsprechend der PID-Ausgänge schalten
|
|
digitalWrite(SSR_DAMPF_PIN, (int)OutputDampf);
|
|
digitalWrite(SSR_WASSER_PIN, (int)OutputWasser);
|
|
}
|
|
|
|
// Shot-Timer verarbeiten (z.B. ob der Bezug läuft)
|
|
updateShotTimer();
|
|
|
|
// Eco-Modus, wenn kein AutoTune läuft
|
|
if (!autoTuneWasserActive && !autoTuneDampfActive)
|
|
{
|
|
if (ecoModeMinutes > 0 && !shotActive)
|
|
{
|
|
unsigned long currentTimeEco = millis();
|
|
|
|
// Prüfen, ob Zeit für Eco
|
|
if (currentTimeEco - lastShotTime > (ecoModeMinutes * 60UL * 1000UL))
|
|
{
|
|
if (!ecoModeAktiv) { ecoModeActivatedTime = currentTimeEco; }
|
|
ecoModeAktiv = true;
|
|
|
|
// dynamischer Eco senkt Temp pro Minute weiter ab
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Dampf-Verzögerung (z.B. wenn die Maschine startet)
|
|
if (!autoTuneDampfActive && dampfVerzoegerung > 0 && (millis() - startupTime < (unsigned long)dampfVerzoegerung * 60000UL))
|
|
{
|
|
SetpointDampf = 0;
|
|
digitalWrite(SSR_DAMPF_PIN, LOW);
|
|
}
|
|
|
|
// Display regelmäßig updaten
|
|
updateDisplay();
|
|
server.handleClient();
|
|
|
|
// Jede Sekunde Betriebszeit hochzählen
|
|
static unsigned long lastRuntimeSecond = 0;
|
|
if (millis() - lastRuntimeSecond >= 1000)
|
|
{
|
|
totalRuntime++;
|
|
lastRuntimeSecond = millis();
|
|
}
|
|
|
|
// Alle 60s in EEPROM speichern und WiFi checken
|
|
if (millis() - lastRuntimeSave >= 60000)
|
|
{
|
|
EEPROM.put(EEPROM_ADDR_RUNTIME, totalRuntime);
|
|
EEPROM.commit();
|
|
lastRuntimeSave = millis();
|
|
checkWifiConnection();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Liest die Temperatur vom NTC-Sensor (Wasser) über den Spannungsteiler (ADC)
|
|
************************************************************************************/
|
|
|
|
double readNTCTemperature()
|
|
{
|
|
int adcValue = analogRead(A0);
|
|
if (adcValue <= 0)
|
|
{
|
|
return -273.15; // Fehler- fallback
|
|
}
|
|
|
|
double sensorVoltage = adcValue * (ADC_REF / 1023.0);
|
|
double R_ntc = R_FIXED * ((V_SUPPLY / sensorVoltage) - 1.0);
|
|
|
|
// Temperaturberechnung über die Beta-Gleichung
|
|
double temperatureK = 1.0 / ((1.0 / T0) + (1.0 / beta) * log(R_ntc / R0));
|
|
return temperatureK - 273.15;
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Aktualisiert das Display (Temperaturanzeige, Shot-Timer etc.)
|
|
************************************************************************************/
|
|
|
|
void updateDisplay()
|
|
{
|
|
// Wenn PID-Tuning läuft, wird das Display woanders beschrieben.
|
|
if (!autoTuneWasserActive && !autoTuneDampfActive)
|
|
{
|
|
// Warte 2 Sekunden nach Shot-Ende, bevor die Anzeige zurückgeht
|
|
if (delayDisplayUpdate && (millis() - shotEndTime < 2000))
|
|
{
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
delayDisplayUpdate = false;
|
|
}
|
|
|
|
display.clearDisplay();
|
|
|
|
if (WiFi.status() == WL_CONNECTED)
|
|
{
|
|
drawwifiSymbol(113, 0);
|
|
}
|
|
|
|
display.setCursor(0, 0);
|
|
display.setTextColor(SH110X_WHITE);
|
|
display.setTextSize(1);
|
|
|
|
// Fast-Heat-Up
|
|
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
|
|
{
|
|
// Anzeige, wenn Shot läuft
|
|
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();
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Verwaltet den Shot-Timer (wenn z.B. der Nutzer den Bezug aktiviert)
|
|
************************************************************************************/
|
|
|
|
void updateShotTimer()
|
|
{
|
|
if (digitalRead(SHOT_TIMER_PIN) == HIGH)
|
|
{
|
|
// Shot beginnt
|
|
if (!shotActive)
|
|
{
|
|
shotActive = true;
|
|
shotStartTime = millis();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Shot endet
|
|
if (shotActive)
|
|
{
|
|
unsigned long shotDuration = millis() - shotStartTime;
|
|
// nur Shots > 20 Sekunden zählen
|
|
if (shotDuration > 20000)
|
|
{
|
|
shotCounter++;
|
|
EEPROM.put(EEPROM_ADDR_SHOTCOUNTER, shotCounter);
|
|
EEPROM.commit();
|
|
Serial.print(F("Shots: "));
|
|
Serial.println(shotCounter);
|
|
}
|
|
shotActive = false;
|
|
lastShotTime = millis();
|
|
shotEndTime = millis();
|
|
delayDisplayUpdate = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Startet AutoTune für Wasser-PID
|
|
************************************************************************************/
|
|
|
|
void startAutoTuneWasser()
|
|
{
|
|
// Eco-Modus deaktivieren, falls aktiv
|
|
if(ecoModeAktiv)
|
|
{
|
|
ecoModeAktiv = false;
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
}
|
|
|
|
autoTuneWasser = new PID_ATune(&InputWasser, &OutputWasser);
|
|
autoTuneWasser->SetOutputStep(tuningStep);
|
|
autoTuneWasser->SetControlType(1);
|
|
autoTuneWasser->SetNoiseBand(tuningNoise);
|
|
autoTuneWasser->SetLookbackSec(tuningLookBack);
|
|
OutputWasser = tuningStartValue;
|
|
autoTuneWasserActive = true;
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Startet AutoTune für Dampf-PID
|
|
************************************************************************************/
|
|
|
|
void startAutoTuneDampf()
|
|
{
|
|
// Eco-Modus deaktivieren, falls aktiv
|
|
if(ecoModeAktiv)
|
|
{
|
|
ecoModeAktiv = false;
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
}
|
|
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
autoTuneDampf = new PID_ATune(&InputDampf, &OutputDampf);
|
|
autoTuneDampf->SetOutputStep(tuningStep);
|
|
autoTuneDampf->SetControlType(1);
|
|
autoTuneDampf->SetNoiseBand(tuningNoise);
|
|
autoTuneDampf->SetLookbackSec(tuningLookBack);
|
|
OutputDampf = tuningStartValue;
|
|
autoTuneDampfActive = true;
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Stoppt AutoTune für Wasser
|
|
************************************************************************************/
|
|
|
|
void stopAutoTuneWasser()
|
|
{
|
|
if (autoTuneWasserActive)
|
|
{
|
|
autoTuneWasserActive = false;
|
|
delete autoTuneWasser;
|
|
autoTuneWasser = nullptr;
|
|
OutputWasser = 0;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Stoppt AutoTune für Dampf
|
|
************************************************************************************/
|
|
|
|
void stopAutoTuneDampf()
|
|
{
|
|
if (autoTuneDampfActive)
|
|
{
|
|
autoTuneDampfActive = false;
|
|
delete autoTuneDampf;
|
|
autoTuneDampf = nullptr;
|
|
OutputDampf = 0;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Überwacht die AutoTune-Prozesse (Dampf/Wasser) und schreibt gefundene Werte ins EEPROM
|
|
************************************************************************************/
|
|
|
|
void handleAutoTune()
|
|
{
|
|
// AutoTune Wasser
|
|
if (autoTuneWasserActive)
|
|
{
|
|
// Wenn Runtime = 1, ist der AutoTune-Prozess abgeschlossen
|
|
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
|
|
{
|
|
// Anzeige während AutoTune
|
|
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();
|
|
}
|
|
}
|
|
|
|
// AutoTune Dampf
|
|
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
|
|
{
|
|
// Anzeige während AutoTune
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler für die Info-Seite
|
|
************************************************************************************/
|
|
|
|
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ä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);
|
|
|
|
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);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler für die Eco-Modus-Konfiguration
|
|
************************************************************************************/
|
|
|
|
void handleEco()
|
|
{
|
|
static const char ecoHtml[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Eco-Modus</title><meta name='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ält.<br>
|
|
Er senkt die Temperatur pro Minute um ein weiteres Grad ab, bis letzlich das Heizen vollständig beendet wird.<br>
|
|
Der dynamische Eco-Modus ist nur in Kombination mit dem Eco-Modus nutzbar!<br>
|
|
<br>
|
|
<b>Beispiel für eine praktische Anwendung:</b><br>
|
|
Peter hat an seiner Maschine den Eco-Modus auf 45 Minuten gestellt, mit Eco-Temperatur von 60 Grad. Nun kommt ihm etwas dazwischen und er kommt erst bei Minute 60 an die Maschine, um sich einen Espresso zuzubereiten. Die Maschine ist nun allerdings schon auf 60 Grad herabgekühlt und er muss erneut das Aufheizen abwarten ...<br>
|
|
Markus passiert das gleiche, doch er hat den dynamischen Eco-Modus mit einer initialen Eco-Temperatur von 95 Grad aktiviert - Seine Maschine ist nun zumindest noch bei 80 Grad.<br>
|
|
Markus muss zwar auch warten, kann jedoch noch vor Peter einen Espresso trinken.
|
|
</div>
|
|
<br><br>
|
|
<label for='dampfDelay'>Aufheizen für Dampf verzögern (Minuten, 0 = deaktiviert):</label>
|
|
<input type='text' id='dampfDelay' name='dampfDelay' value='{DAMPF_DELAY}'>
|
|
<p style='color:red;'>
|
|
<b>Achtung:</b><br>
|
|
Um Konflikte zu vermeiden, sollte der Wert geringer sein, als der des Eco-Modus, falls dieser aktiviert ist!
|
|
</p>
|
|
<br>
|
|
<input type='submit' value='Einstellungen speichern'>
|
|
</form>
|
|
</body>
|
|
</html>
|
|
)rawliteral";
|
|
|
|
String html = FPSTR(ecoHtml);
|
|
html += FPSTR(commonStyle);
|
|
html += FPSTR(ecoHtml2);
|
|
html += FPSTR(commonNav);
|
|
html += FPSTR(ecoHtml3);
|
|
|
|
html.replace(F("{ECO_MODE}"), String(ecoModeMinutes));
|
|
html.replace(F("{ECO_MODE_TEMP_WASSER}"), String(ecoModeTempWasser));
|
|
html.replace(F("{ECO_MODE_TEMP_DAMPF}"), String(ecoModeTempDampf));
|
|
html.replace(F("{DYNAMIC_ECO_MODE_CHECKBOX}"), dynamicEcoActive ? F("checked") : F(""));
|
|
html.replace(F("{DAMPF_DELAY}"), String(dampfVerzoegerung));
|
|
|
|
server.send(200, F("text/html"), html);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler für die Fast-Heat-Up Seite
|
|
************************************************************************************/
|
|
|
|
void handleFastHeatUp()
|
|
{
|
|
static const char fhuHtml[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Fast-Heat-Up-Modus</title><meta name='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ö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);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler zum Speichern der Fast-Heat-Up-Einstellung
|
|
************************************************************************************/
|
|
|
|
void handleFastHeatUpSettings()
|
|
{
|
|
if (server.hasArg(F("fastHeatUpAktiv")))
|
|
{
|
|
fastHeatUpAktiv = true;
|
|
}
|
|
else
|
|
{
|
|
fastHeatUpAktiv = false;
|
|
}
|
|
|
|
EEPROM.put(EEPROM_ADDR_FASTHEATUP_DATA, fastHeatUpAktiv);
|
|
EEPROM.commit();
|
|
|
|
server.sendHeader(F("Location"), F("/fast-heat-up"));
|
|
server.send(303);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler für Root (PID-Einstellungen)
|
|
************************************************************************************/
|
|
|
|
void handleRoot()
|
|
{
|
|
static const char rootHtml[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>PID-Einstellung</title><meta name='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 (°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);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Schreibt die Werte in den EEPROM (PID/Eco)
|
|
************************************************************************************/
|
|
|
|
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);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Speichert Eco-Einstellungen
|
|
************************************************************************************/
|
|
|
|
void handleEcoUpdate()
|
|
{
|
|
if (server.hasArg(F("ecoMode"))) ecoModeMinutes = server.arg(F("ecoMode")).toInt();
|
|
if (server.hasArg(F("ecoModeTempWasser"))) ecoModeTempWasser = server.arg(F("ecoModeTempWasser")).toInt();
|
|
if (server.hasArg(F("ecoModeTempDampf"))) ecoModeTempDampf = server.arg(F("ecoModeTempDampf")).toInt();
|
|
|
|
if (server.hasArg(F("dynamicEcoMode")))
|
|
{
|
|
dynamicEcoActive = true;
|
|
}
|
|
else
|
|
{
|
|
dynamicEcoActive = false;
|
|
}
|
|
|
|
if (server.hasArg(F("dampfDelay")))
|
|
{
|
|
dampfVerzoegerung = server.arg(F("dampfDelay")).toInt();
|
|
}
|
|
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_WASSER, ecoModeTempWasser);
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_TEMP_DAMPF, ecoModeTempDampf);
|
|
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
|
|
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
|
|
EEPROM.commit();
|
|
|
|
// Wenn Eco deaktiviert, Setpoints zurückladen
|
|
if (ecoModeMinutes == 0)
|
|
{
|
|
ecoModeAktiv = false;
|
|
ecoModeActivatedTime = 0;
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
}
|
|
|
|
server.sendHeader(F("Location"), F("/eco"));
|
|
server.send(303);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Speichert Geräte-Infos
|
|
************************************************************************************/
|
|
|
|
void handleInfoUpdate()
|
|
{
|
|
if (server.hasArg(F("hersteller")))
|
|
{
|
|
strncpy(infoHersteller, server.arg(F("hersteller")).c_str(), sizeof(infoHersteller));
|
|
}
|
|
if (server.hasArg(F("modell")))
|
|
{
|
|
strncpy(infoModell, server.arg(F("modell")).c_str(), sizeof(infoModell));
|
|
}
|
|
if (server.hasArg(F("zusatz")))
|
|
{
|
|
strncpy(infoZusatz, server.arg(F("zusatz")).c_str(), sizeof(infoZusatz));
|
|
}
|
|
|
|
EEPROM.put(EEPROM_ADDR_INFO_HERSTELLER, infoHersteller);
|
|
EEPROM.put(EEPROM_ADDR_INFO_MODELL, infoModell);
|
|
EEPROM.put(EEPROM_ADDR_INFO_ZUSATZ, infoZusatz);
|
|
EEPROM.commit();
|
|
|
|
server.sendHeader(F("Location"), F("/info"));
|
|
server.send(303);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Handler für Temperatur-Charts (Live)
|
|
************************************************************************************/
|
|
|
|
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);
|
|
}
|
|
|
|
/************************************************************************************
|
|
* Profil-Funktionen (Speichern/Laden von Profil 1 und 2)
|
|
************************************************************************************/
|
|
|
|
void handleProfiles()
|
|
{
|
|
TemperatureProfile p1;
|
|
TemperatureProfile p2;
|
|
EEPROM.get(EEPROM_ADDR_PROFILE_1, p1);
|
|
EEPROM.get(EEPROM_ADDR_PROFILE_2, p2);
|
|
|
|
static const char profilesHtml[] PROGMEM = R"rawliteral(
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Profile</title><meta name='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önnen zwei unabhängige Profile angelegt oder geladen werden.<br>
|
|
<br>
|
|
<b>In den Profilen werden gespeichert:</b><br>
|
|
- Setpoint Wasser<br>
|
|
- Setpoint Dampf<br>
|
|
- Eco-Modus-Einstellungen<br>
|
|
- Aufheizen für Dampf verzögern<br>
|
|
- Profilname<br>
|
|
</form>
|
|
<form action='/saveProfile1' method='POST'>
|
|
<h3>Profil 1 (Name: {PROFILE1NAME})</h3>
|
|
<label for='profileName1'>Neuen Profilnamen eingeben:</label>
|
|
<input type='text' id='profileName1' name='profileName' value='{PROFILE1NAME}' maxlength='19'><br><br>
|
|
Aktuelle Werte in <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 (Name: {PROFILE2NAME})</h3>
|
|
<label for='profileName2'>Neuen Profilnamen eingeben:</label>
|
|
<input type='text' id='profileName2' name='profileName' value='{PROFILE2NAME}' maxlength='19'><br><br>
|
|
Aktuelle Werte in <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);
|
|
|
|
String htmlBody = FPSTR(profilesHtml3);
|
|
htmlBody.replace("{PROFILE1NAME}", String(p1.profileName));
|
|
htmlBody.replace("{PROFILE2NAME}", String(p2.profileName));
|
|
page += htmlBody;
|
|
|
|
server.send(200, F("text/html"), page);
|
|
}
|
|
|
|
// Speichert aktuelles Setup in Profil 1
|
|
void handleSaveProfile1()
|
|
{
|
|
TemperatureProfile p;
|
|
EEPROM.get(EEPROM_ADDR_PROFILE_1, p);
|
|
|
|
if (server.hasArg("profileName"))
|
|
{
|
|
strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName));
|
|
}
|
|
|
|
// Wir verwenden die Werte aus dem EEPROM, damit nicht fälschlicherweise ECO-Werte genommen werden
|
|
p.setpointWasser = EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
|
|
p.ecoModeMinutes = ecoModeMinutes;
|
|
p.dynamicEcoActive = dynamicEcoActive;
|
|
p.dampfVerzoegerung = dampfVerzoegerung;
|
|
|
|
EEPROM.put(EEPROM_ADDR_PROFILE_1, p);
|
|
EEPROM.commit();
|
|
|
|
server.sendHeader(F("Location"), F("/profiles"));
|
|
server.send(303);
|
|
}
|
|
|
|
// Lädt Profil 1 in die aktuellen Parameter
|
|
void handleLoadProfile1()
|
|
{
|
|
TemperatureProfile p;
|
|
EEPROM.get(EEPROM_ADDR_PROFILE_1, p);
|
|
|
|
SetpointWasser = p.setpointWasser;
|
|
SetpointDampf = p.setpointDampf;
|
|
ecoModeMinutes = p.ecoModeMinutes;
|
|
dynamicEcoActive = p.dynamicEcoActive;
|
|
dampfVerzoegerung = p.dampfVerzoegerung;
|
|
|
|
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
|
|
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
|
|
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
|
|
EEPROM.commit();
|
|
|
|
server.sendHeader(F("Location"), F("/profiles"));
|
|
server.send(303);
|
|
}
|
|
|
|
// Speichert aktuelles Setup in Profil 2
|
|
void handleSaveProfile2()
|
|
{
|
|
TemperatureProfile p;
|
|
EEPROM.get(EEPROM_ADDR_PROFILE_2, p);
|
|
|
|
if (server.hasArg("profileName"))
|
|
{
|
|
strncpy(p.profileName, server.arg("profileName").c_str(), sizeof(p.profileName));
|
|
}
|
|
|
|
p.setpointWasser = EEPROM.get(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
p.setpointDampf = EEPROM.get(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
|
|
p.ecoModeMinutes = ecoModeMinutes;
|
|
p.dynamicEcoActive = dynamicEcoActive;
|
|
p.dampfVerzoegerung = dampfVerzoegerung;
|
|
|
|
EEPROM.put(EEPROM_ADDR_PROFILE_2, p);
|
|
EEPROM.commit();
|
|
|
|
server.sendHeader(F("Location"), F("/profiles"));
|
|
server.send(303);
|
|
}
|
|
|
|
// Lädt Profil 2 in die aktuellen Parameter
|
|
void handleLoadProfile2()
|
|
{
|
|
TemperatureProfile p;
|
|
EEPROM.get(EEPROM_ADDR_PROFILE_2, p);
|
|
|
|
SetpointWasser = p.setpointWasser;
|
|
SetpointDampf = p.setpointDampf;
|
|
ecoModeMinutes = p.ecoModeMinutes;
|
|
dynamicEcoActive = p.dynamicEcoActive;
|
|
dampfVerzoegerung = p.dampfVerzoegerung;
|
|
|
|
EEPROM.put(EEPROM_ADDR_SETPOINT_WASSER, SetpointWasser);
|
|
EEPROM.put(EEPROM_ADDR_SETPOINT_DAMPF, SetpointDampf);
|
|
EEPROM.put(EEPROM_ADDR_ECOMODE_MINUTES, ecoModeMinutes);
|
|
EEPROM.put(EEPROM_ADDR_DYNAMIC_ECO_MODE, dynamicEcoActive);
|
|
EEPROM.put(EEPROM_ADDR_STEAM_DELAY, dampfVerzoegerung);
|
|
EEPROM.commit();
|
|
|
|
server.sendHeader(F("Location"), F("/profiles"));
|
|
server.send(303);
|
|
}
|