// EiPot V1.2.0 - Improved Code Quality Version (Wokwi-kompatibel)
// Implementiert alle Verbesserungsvorschläge außer WiFi-Passwort-Änderung
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <ArduinoJson.h>
// ==================== CONFIGURATION ====================
const int RELAY_PIN = 32;
const int STATUS_LED_PIN = 4;
// WiFi Access Point Einstellungen (unverändert wie gewünscht)
const char* WIFI_SSID = "EiPotV1.1.1";
const char* WIFI_PASSWORD = "123456789";
const char* MDNS_NAME = "eipot";
// Timing-Konstanten
const unsigned long SERVER_DELAY = 100;
// ==================== GLOBAL STATE ====================
struct CookingState {
bool cooking = false;
unsigned long cookStartTime = 0;
unsigned long totalCookTime = 0;
unsigned long remainingTime = 0;
String lastError = "";
} state;
WebServer server(80);
// ==================== INPUT VALIDATION ====================
bool isValidEggCount(const String& count) {
return (count == "1-2" || count == "3-4" || count == "5-6");
}
bool isValidEggSize(const String& size) {
return (size == "small" || size == "medium" || size == "big");
}
bool isValidEggDoneness(const String& doneness) {
return (doneness == "soft" || doneness == "medium" || doneness == "hard");
}
bool validateCookingParams(const String& count, const String& size, const String& doneness) {
return isValidEggCount(count) && isValidEggSize(size) && isValidEggDoneness(doneness);
}
// ==================== COOKING LOGIC ====================
struct CookingTime {
const char* count;
const char* size;
const char* doneness;
unsigned long timeSeconds;
};
// Lookup-Tabelle für spezifische Kombinationen
const CookingTime COOKING_TIMES[] = {
{"1-2", "medium", "soft", 8 * 60 + 50},
{"3-4", "medium", "soft", 9 * 60 + 30},
{"1-2", "big", "soft", 9 * 60 + 10},
{"3-4", "big", "soft", 9 * 60 + 50},
{"1-2", "medium", "hard", 12 * 60 + 10},
{"3-4", "medium", "hard", 12 * 60 + 50},
{"1-2", "big", "hard", 12 * 60 + 30},
{"3-4", "big", "hard", 13 * 60 + 10}
};
unsigned long calculateCookingTime(const String& count, const String& size, const String& doneness) {
// Prüfe spezifische Kombinationen zuerst
for (int i = 0; i < sizeof(COOKING_TIMES) / sizeof(COOKING_TIMES[0]); i++) {
if (count == COOKING_TIMES[i].count &&
size == COOKING_TIMES[i].size &&
doneness == COOKING_TIMES[i].doneness) {
return COOKING_TIMES[i].timeSeconds * 1000; // Konvertierung zu Millisekunden
}
}
// Fallback-Berechnungen für nicht spezifizierte Kombinationen
unsigned long baseTime;
if (count == "1-2") {
baseTime = 8 * 60 + 50; // 8:50 als neue angepasste Basis für 1-2 Eier
} else if (count == "3-4") {
baseTime = 9 * 60 + 30; // 9:30 als neue angepasste Basis für 3-4 Eier
} else if (count == "5-6") {
baseTime = 10 * 60 + 10; // 10:10 als Schätzung für 5-6 Eier
} else {
baseTime = 8 * 60 + 50; // Default
}
// Größenanpassungen
if (size == "small") {
baseTime -= 30; // -30 Sekunden für kleine Eier
} else if (size == "big") {
baseTime += 30; // +30 Sekunden für große Eier
}
// "medium" bleibt bei Basis-Zeit
// Härtegrad-Anpassungen
if (doneness == "medium") {
baseTime += 90; // +1:30 für mittlere Härte
} else if (doneness == "hard") {
baseTime += 3 * 60 + 20; // +3:20 für harte Eier
}
// "soft" bleibt bei Basis-Zeit
return baseTime * 1000; // Konvertierung zu Millisekunden
}
// ==================== TIMER UTILITIES ====================
bool isTimeElapsed(unsigned long startTime, unsigned long duration) {
// Overflow-sichere Zeit-Berechnung
return (millis() - startTime) >= duration;
}
unsigned long getRemainingTime(unsigned long startTime, unsigned long totalTime) {
unsigned long elapsed = millis() - startTime;
if (elapsed >= totalTime) {
return 0;
}
return (totalTime - elapsed) / 1000; // Sekunden
}
// ==================== HARDWARE CONTROL ====================
void initializePins() {
pinMode(RELAY_PIN, OUTPUT);
pinMode(STATUS_LED_PIN, OUTPUT);
digitalWrite(RELAY_PIN, HIGH); // INVERTED: HIGH = relay OFF
digitalWrite(STATUS_LED_PIN, LOW);
}
void startCooking() {
digitalWrite(RELAY_PIN, LOW); // INVERTED: LOW = relay ON
digitalWrite(STATUS_LED_PIN, HIGH);
}
void stopCooking() {
digitalWrite(RELAY_PIN, HIGH); // INVERTED: HIGH = relay OFF
digitalWrite(STATUS_LED_PIN, LOW);
}
// ==================== WIFI & MDNS SETUP ====================
bool initializeWiFi() {
WiFi.softAP(WIFI_SSID, WIFI_PASSWORD);
Serial.println("Access Point gestartet");
Serial.print("IP-Adresse: ");
Serial.println(WiFi.softAPIP());
return true;
}
bool initializeMDNS() {
if (MDNS.begin(MDNS_NAME)) {
Serial.println("mDNS Service gestartet");
Serial.print("Aufruf im Browser: http://");
Serial.print(MDNS_NAME);
Serial.println(".local");
MDNS.addService("http", "tcp", 80);
return true;
} else {
Serial.println("Fehler beim Starten des mDNS Service");
return false;
}
}
// ==================== HTML GENERATION ====================
String buildHTML() {
String html = "<!DOCTYPE html><html><head>";
html += "<meta charset=\"UTF-8\">";
html += "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">";
html += "<title>EiPot</title>";
html += "<style>";
// Braun Design Language CSS
html += "@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');";
html += "* { box-sizing: border-box; margin: 0; padding: 0; }";
html += "body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f8f8f8; color: #1a1a1a; line-height: 1.4; }";
html += ".container { max-width: 400px; margin: 0 auto; padding: 24px 16px; }";
html += ".device-header { text-align: center; margin-bottom: 40px; }";
html += ".brand { font-size: 32px; font-weight: 300; letter-spacing: 2px; color: #1a1a1a; margin-bottom: 8px; }";
html += ".model { font-size: 14px; font-weight: 400; color: #666; letter-spacing: 1px; }";
html += ".card { background: white; border-radius: 2px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); padding: 32px 24px; margin-bottom: 24px; }";
html += ".form-section { margin-bottom: 32px; }";
html += ".form-section:last-child { margin-bottom: 0; }";
html += ".section-label { font-size: 12px; font-weight: 500; color: #666; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 16px; }";
html += ".option-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }";
html += ".option-item input[type=radio] { display: none; }";
html += ".option-label { display: block; padding: 16px 8px; text-align: center; border: 1px solid #e0e0e0; background: white; color: #666; font-size: 13px; font-weight: 400; cursor: pointer; transition: all 0.2s ease; }";
html += ".option-item input[type=radio]:checked + .option-label { border-color: #1a1a1a; background: #1a1a1a; color: white; }";
html += ".option-label:hover { border-color: #999; }";
html += ".primary-button { width: 100%; padding: 18px; background: #1a1a1a; color: white; border: none; font-family: inherit; font-size: 14px; font-weight: 500; letter-spacing: 0.5px; cursor: pointer; transition: background 0.2s ease; margin-top: 24px; }";
html += ".primary-button:hover { background: #333; }";
html += ".primary-button:active { background: #000; }";
html += ".secondary-button { width: 100%; padding: 12px; background: white; color: #666; border: 1px solid #e0e0e0; font-family: inherit; font-size: 13px; font-weight: 400; cursor: pointer; transition: all 0.2s ease; margin-top: 12px; }";
html += ".secondary-button:hover { border-color: #999; color: #333; }";
html += ".status-card { background: #1a1a1a; color: white; text-align: center; }";
html += ".status-title { font-size: 14px; font-weight: 400; margin-bottom: 24px; color: #ccc; }";
html += ".timer-display { font-size: 48px; font-weight: 300; font-family: 'Courier New', monospace; margin: 24px 0; letter-spacing: 2px; }";
html += ".cooking .status-card { background: #333; }";
html += ".finished .status-card { background: #1a1a1a; animation: pulse 2s infinite; }";
html += "@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.8; } }";
html += ".hidden { display: none; }";
html += ".error-card { background: #ff4444; color: white; text-align: center; padding: 16px; margin-bottom: 16px; border-radius: 2px; }";
html += "</style></head><body>";
html += "<div class=\"container\">";
html += "<div class=\"device-header\">";
html += "<div class=\"brand\">BRAUN</div>";
html += "<div class=\"model\">EIPOT</div>";
html += "</div>";
html += "<div class=\"card error-card hidden\" id=\"errorCard\">";
html += "<div id=\"errorMessage\"></div>";
html += "</div>";
html += "<div class=\"card\" id=\"controlCard\">";
html += "<form id=\"cookingForm\">";
html += "<div class=\"form-section\">";
html += "<div class=\"section-label\">Anzahl</div>";
html += "<div class=\"option-grid\">";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"count1\" name=\"eggCount\" value=\"1-2\" required>";
html += "<label class=\"option-label\" for=\"count1\">1–2</label>";
html += "</div>";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"count2\" name=\"eggCount\" value=\"3-4\">";
html += "<label class=\"option-label\" for=\"count2\">3–4</label>";
html += "</div>";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"count3\" name=\"eggCount\" value=\"5-6\">";
html += "<label class=\"option-label\" for=\"count3\">5–6</label>";
html += "</div></div></div>";
html += "<div class=\"form-section\">";
html += "<div class=\"section-label\">Groesse</div>";
html += "<div class=\"option-grid\">";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"size1\" name=\"eggSize\" value=\"small\" required>";
html += "<label class=\"option-label\" for=\"size1\">S</label>";
html += "</div>";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"size2\" name=\"eggSize\" value=\"medium\">";
html += "<label class=\"option-label\" for=\"size2\">M</label>";
html += "</div>";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"size3\" name=\"eggSize\" value=\"big\">";
html += "<label class=\"option-label\" for=\"size3\">L</label>";
html += "</div></div></div>";
html += "<div class=\"form-section\">";
html += "<div class=\"section-label\">Haertegrad</div>";
html += "<div class=\"option-grid\">";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"done1\" name=\"eggDoneness\" value=\"soft\" required>";
html += "<label class=\"option-label\" for=\"done1\">Weich</label>";
html += "</div>";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"done2\" name=\"eggDoneness\" value=\"medium\">";
html += "<label class=\"option-label\" for=\"done2\">Mittel</label>";
html += "</div>";
html += "<div class=\"option-item\">";
html += "<input type=\"radio\" id=\"done3\" name=\"eggDoneness\" value=\"hard\">";
html += "<label class=\"option-label\" for=\"done3\">Hart</label>";
html += "</div></div></div>";
html += "<button type=\"submit\" class=\"primary-button\">START</button>";
html += "</form></div>";
html += "<div class=\"card status-card hidden\" id=\"statusCard\">";
html += "<div class=\"status-title\" id=\"statusTitle\">Wird gekocht</div>";
html += "<div class=\"timer-display\" id=\"timerDisplay\">00:00</div>";
html += "<button class=\"secondary-button\" onclick=\"stopCooking()\" id=\"stopButton\">STOP</button>";
html += "<button class=\"primary-button hidden\" onclick=\"confirmFinished()\" id=\"confirmButton\">OK</button>";
html += "</div></div>";
// JavaScript Teil
html += "<script>";
html += "let statusInterval;";
html += "function showError(message) {";
html += "const errorCard = document.getElementById('errorCard');";
html += "const errorMessage = document.getElementById('errorMessage');";
html += "errorMessage.textContent = message;";
html += "errorCard.classList.remove('hidden');";
html += "setTimeout(() => errorCard.classList.add('hidden'), 5000);";
html += "}";
html += "document.getElementById('cookingForm').addEventListener('submit', function(e) {";
html += "e.preventDefault();";
html += "const formData = new FormData(this);";
html += "const data = {";
html += "eggCount: formData.get('eggCount'),";
html += "eggSize: formData.get('eggSize'),";
html += "eggDoneness: formData.get('eggDoneness')";
html += "};";
html += "fetch('/start', {";
html += "method: 'POST',";
html += "headers: { 'Content-Type': 'application/json' },";
html += "body: JSON.stringify(data)";
html += "}).then(response => {";
html += "if (!response.ok) {";
html += "return response.text().then(text => { throw new Error(text); });";
html += "}";
html += "return response.text();";
html += "}).then(data => {";
html += "console.log(data); startStatusUpdates();";
html += "}).catch(error => {";
html += "showError('Fehler beim Starten: ' + error.message);";
html += "});});";
html += "function startStatusUpdates() {";
html += "document.getElementById('controlCard').classList.add('hidden');";
html += "document.getElementById('statusCard').classList.remove('hidden');";
html += "document.getElementById('statusCard').classList.add('cooking');";
html += "document.getElementById('stopButton').classList.remove('hidden');";
html += "document.getElementById('confirmButton').classList.add('hidden');";
html += "statusInterval = setInterval(updateStatus, 1000);";
html += "updateStatus();";
html += "}";
html += "function updateStatus() {";
html += "fetch('/status').then(response => response.json()).then(data => {";
html += "const timer = document.getElementById('timerDisplay');";
html += "const statusCard = document.getElementById('statusCard');";
html += "const statusTitle = document.getElementById('statusTitle');";
html += "const stopButton = document.getElementById('stopButton');";
html += "const confirmButton = document.getElementById('confirmButton');";
html += "if (data.cooking) {";
html += "const minutes = Math.floor(data.remainingTime / 60);";
html += "const seconds = data.remainingTime % 60;";
html += "timer.textContent = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');";
html += "statusTitle.textContent = 'Wird gekocht';";
html += "statusCard.classList.add('cooking');";
html += "statusCard.classList.remove('finished');";
html += "stopButton.classList.remove('hidden');";
html += "confirmButton.classList.add('hidden');";
html += "} else {";
html += "timer.textContent = '00:00';";
html += "statusTitle.textContent = 'Fertig';";
html += "statusCard.classList.remove('cooking');";
html += "statusCard.classList.add('finished');";
html += "stopButton.classList.add('hidden');";
html += "confirmButton.classList.remove('hidden');";
html += "clearInterval(statusInterval);";
html += "playNotificationSound();";
html += "}}).catch(error => {";
html += "showError('Verbindungsfehler: ' + error.message);";
html += "});";
html += "}";
html += "function stopCooking() {";
html += "fetch('/stop', { method: 'POST' }).then(() => {";
html += "clearInterval(statusInterval);";
html += "resetInterface();";
html += "}).catch(error => {";
html += "showError('Fehler beim Stoppen: ' + error.message);";
html += "});";
html += "}";
html += "function confirmFinished() {";
html += "resetInterface();";
html += "}";
html += "function resetInterface() {";
html += "document.getElementById('statusCard').classList.add('hidden');";
html += "document.getElementById('controlCard').classList.remove('hidden');";
html += "document.getElementById('cookingForm').reset();";
html += "}";
html += "function playNotificationSound() {";
html += "try {";
html += "if (navigator.vibrate) { navigator.vibrate([300, 100, 300]); }";
html += "const audioContext = new (window.AudioContext || window.webkitAudioContext)();";
html += "const oscillator = audioContext.createOscillator();";
html += "const gainNode = audioContext.createGain();";
html += "oscillator.connect(gainNode);";
html += "gainNode.connect(audioContext.destination);";
html += "oscillator.frequency.setValueAtTime(880, audioContext.currentTime);";
html += "gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);";
html += "oscillator.start(audioContext.currentTime);";
html += "oscillator.stop(audioContext.currentTime + 0.2);";
html += "} catch (e) { console.log('Audio nicht verfuegbar'); }";
html += "}";
html += "</script></body></html>";
return html;
}
// ==================== WEB HANDLERS ====================
void handleRoot() {
String html = buildHTML();
server.send(200, "text/html", html);
}
void handleStart() {
if (state.cooking) {
server.send(400, "text/plain", "Bereits am Kochen");
return;
}
// ArduinoJson für sicheres JSON-Parsing verwenden
StaticJsonDocument<200> doc;
DeserializationError error = deserializeJson(doc, server.arg("plain"));
if (error) {
Serial.print("JSON Parse Fehler: ");
Serial.println(error.c_str());
server.send(400, "text/plain", "Ungueltige JSON-Daten");
return;
}
String eggCount = doc["eggCount"].as<String>();
String eggSize = doc["eggSize"].as<String>();
String eggDoneness = doc["eggDoneness"].as<String>();
// Input-Validierung
if (!validateCookingParams(eggCount, eggSize, eggDoneness)) {
Serial.println("Ungueltige Parameter empfangen");
server.send(400, "text/plain", "Ungueltige Kochparameter");
return;
}
// Kochzeit berechnen
state.totalCookTime = calculateCookingTime(eggCount, eggSize, eggDoneness);
// Kochen starten
state.cooking = true;
state.cookStartTime = millis();
state.lastError = "";
startCooking();
Serial.println("Kochen gestartet via Web-Interface");
Serial.print("Gesamtzeit: ");
Serial.print(state.totalCookTime / 1000);
Serial.println(" Sekunden");
Serial.print("Kombination: ");
Serial.print(eggCount);
Serial.print(", ");
Serial.print(eggSize);
Serial.print(", ");
Serial.println(eggDoneness);
server.send(200, "text/plain", "Kochen gestartet");
}
void handleStatus() {
StaticJsonDocument<100> doc;
doc["cooking"] = state.cooking;
doc["remainingTime"] = state.remainingTime;
if (!state.lastError.isEmpty()) {
doc["error"] = state.lastError;
}
String response;
serializeJson(doc, response);
server.send(200, "application/json", response);
}
void handleStop() {
state.cooking = false;
state.remainingTime = 0;
state.lastError = "";
stopCooking();
Serial.println("Kochen gestoppt via Web-Interface");
server.send(200, "text/plain", "Kochen gestoppt");
}
// ==================== SETUP ====================
void setup() {
Serial.begin(115200);
Serial.println("EiPot V1.2.0 - Improved Version");
// Hardware initialisieren
initializePins();
// Netzwerk initialisieren
if (!initializeWiFi()) {
Serial.println("FEHLER: WiFi-Initialisierung fehlgeschlagen");
return;
}
if (!initializeMDNS()) {
Serial.println("WARNUNG: mDNS-Initialisierung fehlgeschlagen");
}
// Web-Server Routen definieren
server.on("/", handleRoot);
server.on("/start", HTTP_POST, handleStart);
server.on("/status", handleStatus);
server.on("/stop", HTTP_POST, handleStop);
// 404-Handler für bessere Fehlerbehandlung
server.onNotFound([]() {
server.send(404, "text/plain", "Seite nicht gefunden");
});
server.begin();
Serial.println("Web-Server gestartet - bereit fuer Eier!");
}
// ==================== MAIN LOOP ====================
void loop() {
server.handleClient();
// Kochvorgang überwachen mit overflow-sicherer Zeitberechnung
if (state.cooking) {
if (isTimeElapsed(state.cookStartTime, state.totalCookTime)) {
// Kochen beendet
state.cooking = false;
state.remainingTime = 0;
stopCooking();
Serial.println("Eier fertig!");
} else {
state.remainingTime = getRemainingTime(state.cookStartTime, state.totalCookTime);
}
}
delay(SERVER_DELAY);
}