From 96508910b0cc79558ecf8a33e6a8e514f70c0cec Mon Sep 17 00:00:00 2001 From: MaPaLo76 <72209721+MaPaLo76@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:49:29 +0100 Subject: [PATCH] fix(FR-006): MQTT Session-Queue (128 Slots) statt volatile Einzelslot; Dequeue erst nach erfolgreichem Publish --- Feature-Requests.md | 23 +++++++++++------------ include/mqtt_client.h | 17 ++++++++++++----- src/mqtt_client.cpp | 36 ++++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 31 deletions(-) diff --git a/Feature-Requests.md b/Feature-Requests.md index 6fac368..1766543 100644 --- a/Feature-Requests.md +++ b/Feature-Requests.md @@ -19,15 +19,6 @@ Status: `[ ]` = offen · `[x]` = erledigt ## Offen -- [x] **FR-008** Bug: Reset-Befehle setzen Maschinenlaufzeit (NVS) ungewollt zurück ✅ - - **Bug A – MQTT `{"reset":true}`** ruft `resetTotal()` auf → `settings.saveTotalMinutes(0.0f)` → Maschinenlaufzeit (Gesamtbetriebszeit) wird **unwiderruflich aus dem NVS gelöscht** - - Gewünscht: `{"reset":true}` soll nur die Session-Summe (Tages-/Wochenzähler) zurücksetzen, nicht die Gesamtbetriebszeit - - `resetTotal()` sollte gar nicht per MQTT erreichbar sein (oder einen separaten, expliziten Befehl erfordern, z.B. `{"reset_total":true}`) - - **Bug B – Web-Button "Summe Laserzeit zurücksetzen"** ruft `resetSessionSum()` auf → NVS wird nicht angefasst, aber `_sessionNetSec = 0` → `getTotalMinutes()` sinkt sofort in der Anzeige (weil `_sessionNetSec`-Anteil verschwindet), obwohl der NVS-Wert korrekt bleibt. Täuscht einen Gesamt-Reset vor. - - Fix: `_totalMinutesBase` beim `resetSessionSum()` um den bereits akkumulierten `_sessionNetSec`-Anteil erhöhen, bevor `_sessionNetSec` auf 0 gesetzt wird → Kontinuität der Gesamtzeit sicherstellen - - Betroffene Dateien: `src/laser_tracker.cpp` (`resetSessionSum`, `resetTotal`), `src/mqtt_client.cpp` (MQTT-Reset-Handler) - - Commit: `c636add` - - [ ] **FR-007** Feature: Laufende Session-ID in MQTT Session-Payload - Jede Session erhält eine aufsteigende Ganzzahl (`session_id`), die im MQTT-Payload `MQTT_TOPIC_SESSION` mitgesendet wird - Empfänger (z. B. Home Assistant, Node-RED) kann fehlende Sessions sofort erkennen: Lücke zwischen `session_id` 47 und 49 → Session 48 ging verloren @@ -37,7 +28,7 @@ Status: `[ ]` = offen · `[x]` = erledigt - Betroffene Dateien: `include/mqtt_client.h` (Counter-Member), `src/mqtt_client.cpp` (`_doPublishSession`: `doc["session_id"] = _sessionCounter++`) - Commit: – -- [ ] **FR-006** Bug: MQTT Session-Publish nicht zuverlässig bei Verbindungsausfall +- [x] **FR-006** Bug: MQTT Session-Publish nicht zuverlässig bei Verbindungsausfall ✅ - **Bug A – Race Condition** (`mqtt_client.cpp`, `_doPublishSession`): `_pendingSession = false` wird gesetzt **bevor** `_client->publish()` ausgeführt wird. Scheitert der Publish (z. B. MQTT trennt in diesem Moment), ist das Flag bereits gelöscht → Session-Daten gehen still verloren. @@ -45,15 +36,23 @@ Status: `[ ]` = offen · `[x]` = erledigt - **Bug B – Nur 1 Slot** (`mqtt_client.h`, `volatile`-Felder): `_pendingSessionSec` / `_pendingGratisSec` / `_pendingSession` speichern genau eine Session. Enden zwei Sessions während einer Offline-Phase, überschreibt der zweite `publishSession()`-Aufruf die Daten des ersten → erste Session unwiederbringlich verloren. + - Fix: `QueueHandle_t` mit **128 Slots** (à `sizeof(SessionPayload)` = 8 Bytes = ~1 KB RAM); `xQueuePeek` + Dequeue erst nach erfolgreichem `_client->publish()` → kein Datenverlust bei Verbindungsabbruch - Betroffene Dateien: `src/mqtt_client.cpp`, `include/mqtt_client.h` - - Lösungsansatz: `QueueHandle_t` mit **mindestens 128 Slots** statt volatile Einzelslot; Publish erst nach erfolgreichem `_client->publish()` aus der Queue entfernen - Begründung: Theoretisch 1 Session/Minute × 2h Offline = **120 Sessions** im Worst Case → 10 Slots wären zu knapp; 128 Slots à ~16 Bytes = ~2 KB RAM (vertretbar) - Commit: – --- ## Erledigt +- [x] **FR-008** Bug: Reset-Befehle setzen Maschinenlaufzeit (NVS) ungewollt zurück ✅ + - **Bug A – MQTT `{"reset":true}`** ruft `resetTotal()` auf → `settings.saveTotalMinutes(0.0f)` → Maschinenlaufzeit (Gesamtbetriebszeit) wird **unwiderruflich aus dem NVS gelöscht** + - Gewünscht: `{"reset":true}` soll nur die Session-Summe (Tages-/Wochenzähler) zurücksetzen, nicht die Gesamtbetriebszeit + - `resetTotal()` sollte gar nicht per MQTT erreichbar sein (oder einen separaten, expliziten Befehl erfordern, z.B. `{"reset_total":true}`) + - **Bug B – Web-Button "Summe Laserzeit zurücksetzen"** ruft `resetSessionSum()` auf → NVS wird nicht angefasst, aber `_sessionNetSec = 0` → `getTotalMinutes()` sinkt sofort in der Anzeige (weil `_sessionNetSec`-Anteil verschwindet), obwohl der NVS-Wert korrekt bleibt. Täuscht einen Gesamt-Reset vor. + - Fix: `_totalMinutesBase` beim `resetSessionSum()` um den bereits akkumulierten `_sessionNetSec`-Anteil erhöhen, bevor `_sessionNetSec` auf 0 gesetzt wird → Kontinuität der Gesamtzeit sicherstellen + - Betroffene Dateien: `src/laser_tracker.cpp` (`resetSessionSum`, `resetTotal`), `src/mqtt_client.cpp` (MQTT-Reset-Handler) + - Commit: `c636add` + - [x] **FR-005** Bug: WDT-Crash + Display-/Browser-Freeze durch blockierenden TLS-Handshake ✅ - MQTT `reconnect()` mit TLS (Port 8883) blockiert `loop()` auf Core 1 bis zu 15 s - Folge: Task-Watchdog (30 s) feuert wenn zwei Reconnect-Versuche in einem 30s-Fenster → Neustart mit `WATCHDOG (Task)` diff --git a/include/mqtt_client.h b/include/mqtt_client.h index 5483ea0..baebb28 100644 --- a/include/mqtt_client.h +++ b/include/mqtt_client.h @@ -27,6 +27,13 @@ #include "settings.h" #include #include +#include + +// Session-Daten die per Queue von Core 1 -> Core 0 übergeben werden +struct SessionPayload { + int sessionSec; + int gratisSec; +}; class MqttClient { public: @@ -67,10 +74,9 @@ private: // FreeRTOS-Task: laeuft auf Core 0, erledigt Reconnect/Heartbeat/Publish TaskHandle_t _taskHandle; - // Pending-Session-Daten: von Core 1 (main) gesetzt, von Core 0 (Task) gelesen - volatile bool _pendingSession; - volatile int _pendingSessionSec; - volatile int _pendingGratisSec; + // Session-Queue: Core 1 schreibt per publishSession(), Core 0 liest und publiziert. + // 128 Slots = Worst Case ~2h Offline bei 1 Session/min. ~2 KB RAM. + QueueHandle_t _sessionQueue; // Verbindungsaufbau (blockierend - NUR aus MQTT-Task auf Core 0 aufrufen!) bool reconnect(); @@ -79,7 +85,8 @@ private: void publishHeartbeat(); // Eigentlicher Session-Publish (aus MQTT-Task auf Core 0) - void _doPublishSession(int lastSessionSec, int gratisSec); + // Gibt true zurück wenn Publish erfolgreich, false bei Fehler (Queue-Eintrag bleibt dann erhalten) + bool _doPublishSession(int lastSessionSec, int gratisSec); // FreeRTOS-Task static void _taskFn(void* param); diff --git a/src/mqtt_client.cpp b/src/mqtt_client.cpp index 00f773e..7f6e75c 100644 --- a/src/mqtt_client.cpp +++ b/src/mqtt_client.cpp @@ -22,11 +22,10 @@ MqttClient::MqttClient() , _lastReconnectMs(0) , _lastHeartbeatMs(0) , _taskHandle(nullptr) - , _pendingSession(false) - , _pendingSessionSec(0) - , _pendingGratisSec(0) + , _sessionQueue(nullptr) { _clientId[0] = '\0'; + _sessionQueue = xQueueCreate(128, sizeof(SessionPayload)); } // -------------------------------------------------------------------------- @@ -72,19 +71,22 @@ void MqttClient::loop() { // Daten werden via volatile Flag an den MQTT-Task auf Core 0 uebergeben. // -------------------------------------------------------------------------- void MqttClient::publishSession(int lastSessionSec, int gratisSec) { - _pendingSessionSec = lastSessionSec; - _pendingGratisSec = gratisSec; - _pendingSession = true; // Task auf Core 0 fuehrt den Publish aus - LOG_I("MQTT", "publishSession vorgemerkt: %d s (gratis %d s)", lastSessionSec, gratisSec); + SessionPayload p = { lastSessionSec, gratisSec }; + if (xQueueSend(_sessionQueue, &p, 0) != pdTRUE) { + LOG_E("MQTT", "publishSession: Queue voll! Session %d s verworfen", lastSessionSec); + } else { + LOG_I("MQTT", "publishSession eingereihrt: %d s (gratis %d s), Slots frei: %u", + lastSessionSec, gratisSec, (unsigned)uxQueueSpacesAvailable(_sessionQueue)); + } } // -------------------------------------------------------------------------- // _doPublishSession() - eigentlicher Publish, wird aus MQTT-Task aufgerufen // -------------------------------------------------------------------------- -void MqttClient::_doPublishSession(int lastSessionSec, int gratisSec) { +bool MqttClient::_doPublishSession(int lastSessionSec, int gratisSec) { if (!_client->connected()) { - LOG_E("MQTT", "_doPublishSession: nicht verbunden, uebersprungen"); - return; + LOG_E("MQTT", "_doPublishSession: nicht verbunden, Session bleibt in Queue"); + return false; } // session_minutes: Decken-Rundung (62 s = 2 min) @@ -110,6 +112,7 @@ void MqttClient::_doPublishSession(int lastSessionSec, int gratisSec) { bool ok = _client->publish(MQTT_TOPIC_SESSION, buf, /*retained=*/false); LOG_I("MQTT", "publishSession: %s -> %s", buf, ok ? "OK" : "FEHLER"); + return ok; } // -------------------------------------------------------------------------- @@ -224,10 +227,15 @@ void MqttClient::_taskLoop() { // PubSubClient-interne Verarbeitung (eingehende Nachrichten) _client->loop(); - // Pending Session-Publish (von Core 1 via volatile Flag gesetzt) - if (_pendingSession) { - _pendingSession = false; - _doPublishSession(_pendingSessionSec, _pendingGratisSec); + // Pending Sessions aus Queue publizieren (FIFO) + // Peek + Publish + nur bei Erfolg Dequeue → kein Datenverlust bei Verbindungsabbruch + SessionPayload pending; + while (xQueuePeek(_sessionQueue, &pending, 0) == pdTRUE) { + if (_doPublishSession(pending.sessionSec, pending.gratisSec)) { + xQueueReceive(_sessionQueue, &pending, 0); // erfolgreich: aus Queue entfernen + } else { + break; // nicht verbunden: beim nächsten Loop-Durchlauf erneut versuchen + } } // Heartbeat