diff --git a/Changelog.txt b/Changelog.txt index 4d5e87d..d6eef0b 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,15 @@ +Version 5.6.5: +- Kurze Sensoraussetzer lösen keinen Sensorfehler mehr aus. Liefert ein Temperaturfühler für einen + Moment keinen gültigen Wert, wird bis zu 3 Sekunden lang der letzte gültige Messwert weiterbenutzt - + die Regelung läuft normal weiter, Heizung, Anzeige und Piezo bleiben ruhig. Erst wenn der Ausfall + länger dauert, gilt er wie bisher als Sensorfehler mit Heizungsabschaltung und Warnung. +- Das gilt für alle drei Fühler: Wasserkessel (NTC), Dampfkessel (Thermoelement) und den Zusatzsensor + für Gehäuse/Tassenablage. Jede Überbrückung steht mit Dauer und gehaltenem Wert im seriellen Log. +- Der Warnton bei einem echten Sensorfehler ist kein Dauerton mehr. Bisher wurde er alle 250 ms neu + angestoßen, obwohl ein Ton eine Sekunde dauert. Jetzt piept es einmal beim Auftreten und danach als + Erinnerung alle 30 Sekunden, bis der Fühler wieder liefert. Der Ton bei Übertemperatur + (Sicherheitsabschaltung) bleibt unverändert. + Version 5.6.4: - Dampf-/Thermoblock-Temperatur wird jetzt mit einer Nachkommastelle angezeigt - im Webinterface, in den Verlaufskurven und auf dem Touch-Display. Bisher wurde der Messwert direkt nach dem Einlesen auf diff --git a/Dual-PID.ino b/Dual-PID.ino index 52f7a3f..516edca 100644 --- a/Dual-PID.ino +++ b/Dual-PID.ino @@ -78,6 +78,7 @@ struct SensorSettingsUpdate; struct SensorSettingsApplyResult; struct ServiceSettingsUpdate; struct FullyKioskConfig; +struct SensorGlitchBridge; class AsyncWebServerRequest; // Vorwaertsdeklarationen fuer Funktionen, die vor ihrer Definition genutzt werden @@ -369,7 +370,7 @@ Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire); * Firmware-Informationen ************************************************************************************/ -String version = "5.6.4"; +String version = "5.6.5"; String versionHersteller = "Thomas Müller"; String versionHerstellerMail = "thomas@mueller.black"; @@ -644,6 +645,58 @@ void updateSmoothedRawTemperature(double &smoothed, double measured) { smoothed += DISPLAY_TEMP_SMOOTHING * (measured - smoothed); } +// Kurzzeitige Sensoraussetzer ueberbruecken. Ein ADC-Ausreisser am NTC oder ein verpasster +// SPI-Frame am Thermoelement liefert fuer ein bis zwei Messtakte gar keinen Wert. Ohne +// Ueberbrueckung geht die Steuerung sofort in den Sensorfehler (Heizung aus, Warnung im +// Webinterface und auf dem Display, Ton am Piezo), obwohl der Fuehler kurz darauf wieder +// liefert. Waehrend der Haltezeit wird stattdessen der letzte gueltige Messwert +// weitergereicht; erst wenn der Ausfall laenger dauert, gilt er als echter Sensorfehler. +const unsigned long SENSOR_GLITCH_HOLD_MS = 3000; // maximale Ueberbrueckungsdauer + +struct SensorGlitchBridge { + double lastGoodValue = NAN; // letzter gueltiger Rohwert + unsigned long lastGoodMs = 0; // Zeitpunkt dieses Werts + bool holding = false; // wird gerade ueberbrueckt? + uint16_t holdEvents = 0; // Anzahl der Ueberbrueckungen seit dem Start +}; + +SensorGlitchBridge sensorBridgeWasser; +SensorGlitchBridge sensorBridgeDampf; +SensorGlitchBridge sensorBridgeCase; + +// Liefert den Messwert oder - bei ungueltiger Messung innerhalb der Haltezeit - den letzten +// gueltigen Wert. Die Gueltigkeitspruefung entspricht der spaeteren Sensorfehler-Pruefung, +// damit nicht ein Wert ueberbrueckt wird, den die Sicherheitslogik trotzdem verwirft. +double bridgeSensorGlitch(SensorGlitchBridge &bridge, double rawValue, double offsetValue, + unsigned long nowMs, const char *sensorName) { + bool valid = isfinite(rawValue) && (rawValue + offsetValue) >= 0.0; + if (valid) { + if (bridge.holding) { + Serial.printf("INFO: %s-Sensor nach %lu ms Ueberbrueckung wieder gueltig.\n", + sensorName, (unsigned long)(nowMs - bridge.lastGoodMs)); + bridge.holding = false; + } + bridge.lastGoodValue = rawValue; + bridge.lastGoodMs = nowMs; + return rawValue; + } + if (isfinite(bridge.lastGoodValue) && (nowMs - bridge.lastGoodMs) <= SENSOR_GLITCH_HOLD_MS) { + if (!bridge.holding) { + bridge.holding = true; + if (bridge.holdEvents < 0xFFFF) bridge.holdEvents++; + Serial.printf("INFO: %s-Sensor ohne Wert - Ueberbrueckung mit %.2f C.\n", + sensorName, bridge.lastGoodValue); + } + return bridge.lastGoodValue; + } + if (bridge.holding) { + Serial.printf("WARNUNG: %s-Sensor laenger als %lu ms ohne Wert - Sensorfehler.\n", + sensorName, SENSOR_GLITCH_HOLD_MS); + bridge.holding = false; + } + return rawValue; +} + // Anzeigewerte auf eine Nachkommastelle festzurren, damit Weboberflaeche, Display und // Verlaufskurven denselben Wert sehen und die letzte Stelle nicht zwischen den Kanaelen // auseinanderlaeuft. @@ -3341,6 +3394,22 @@ void beepMaintenanceAlert(int frequency = 3000, int duration = 3000) { // delay(duration + 10); // Kurze Pause danach, falls nötig, aber `tone` mit Dauer ist blockierend } +// Warnton bei Sensorfehler. Die Pruefung laeuft alle 250 ms, ein Ton dauert 1000 ms - +// unveraendert angestossen ergibt das einen Dauerton. Deshalb einmal beim Auftreten und +// danach nur noch als Erinnerung im Abstand von SENSOR_ALARM_REPEAT_MS. +const unsigned long SENSOR_ALARM_REPEAT_MS = 30000; +unsigned long lastSensorAlarmBeepMs = 0; +bool sensorAlarmBeepActive = false; + +void beepSensorAlarm() { + unsigned long nowMs = millis(); + if (!sensorAlarmBeepActive || (unsigned long)(nowMs - lastSensorAlarmBeepMs) >= SENSOR_ALARM_REPEAT_MS) { + beepShort(); + lastSensorAlarmBeepMs = nowMs; + sensorAlarmBeepActive = true; + } +} + /************************************************************************************ * ESP-NOW Empfangsfunktion @@ -5582,6 +5651,9 @@ void loop() { // Temperaturmessung RawInputDampf = thermocouple.readCelsius(); RawInputWasser = readNTCTemperature(); + // Kurze Aussetzer ueberbruecken, bevor irgendetwas mit den Werten passiert + RawInputDampf = bridgeSensorGlitch(sensorBridgeDampf, RawInputDampf, OffsetDampf, currentMillis, "Dampf"); + RawInputWasser = bridgeSensorGlitch(sensorBridgeWasser, RawInputWasser, OffsetWasser, currentMillis, "Wasser"); InputDampf = round(RawInputDampf + OffsetDampf); InputWasser = round(RawInputWasser + OffsetWasser); updateSmoothedRawTemperature(SmoothRawDampf, RawInputDampf); @@ -5589,6 +5661,7 @@ void loop() { updateOffsetCompensationReferences(); if (caseSensorEnabled) { double rawCase = readCaseTemperature(); + rawCase = bridgeSensorGlitch(sensorBridgeCase, rawCase, OffsetCase, currentMillis, "Gehaeuse"); InputCase = round(rawCase + OffsetCase); updateSmoothedRawTemperature(SmoothRawCase, rawCase); caseSensorError = (isnan(InputCase) || InputCase < 0.0); @@ -5613,7 +5686,7 @@ void loop() { wasserSafetyShutdown = false; OutputWasser = 0; // Ton ausgeben zur Warnung - beepShort(); + beepSensorAlarm(); // Serial.println("WARNUNG: Wassertemperatursensor liefert ungueltigen Wert!"); // Optional Log } else if (InputWasser > maxTempWasser) { wasserSensorError = false; @@ -5640,7 +5713,7 @@ void loop() { dampfSafetyShutdown = false; OutputDampf = 0; // Ton ausgeben zur Warnung - beepShort(); + beepSensorAlarm(); // Serial.println("WARNUNG: Dampftemperatursensor liefert ungueltigen Wert!"); // Optional Log } else if (InputDampf > maxTempDampf) { dampfSensorError = false; @@ -5658,6 +5731,8 @@ void loop() { if (activateBoostDampf) { OutputDampf = windowSizeDampf; } else if (!autoTuneDampfActive) { pidDampf.Compute(); } } } + // Warnton-Wiederholung erst zuruecksetzen, wenn beide Fuehler wieder liefern + if (!wasserSensorError && !dampfSensorError) { sensorAlarmBeepActive = false; } yield(); // Pruefen der Startverzoegerung fuer Dampf