feat(S3): kurze Sensoraussetzer überbrücken statt Sensorfehler (5.6.5) #94

Merged
thomas merged 1 commits from feat/sensor-aussetzer-ueberbruecken into main 2026-09-22 15:53:35 +02:00
2 changed files with 90 additions and 3 deletions

No files matched your search

+12
View File
@@ -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
+78 -3
View File
@@ -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 =
"<a href='mailto:thomas@mueller.black' class='info-link'>thomas@mueller.black</a>";
@@ -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