fix(FR-006): MQTT Session-Queue (128 Slots) statt volatile Einzelslot; Dequeue erst nach erfolgreichem Publish

This commit is contained in:
MaPaLo76 2026-02-28 23:49:29 +01:00
parent abde7fb154
commit 96508910b0
3 changed files with 45 additions and 31 deletions

View File

@ -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)`

View File

@ -27,6 +27,13 @@
#include "settings.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
// 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);

View File

@ -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