feat(mqtt): session_start_time ISO-8601 UTC; NTP waitForNtp nach WLAN-Connect; docs: README + Plan Phase 9 abgeschlossen
This commit is contained in:
parent
2073c3678c
commit
18a1f67f64
|
|
@ -248,11 +248,19 @@
|
|||
|
||||
- [x] **9.3** Speicherleck-Monitoring: `ESP.getFreeHeap()` alle 30 s via `LOG_D` in `loop()`
|
||||
|
||||
- [ ] **9.4** Edge-Cases testen:
|
||||
- Laser aktiv, WLAN wird getrennt → Display läuft weiter
|
||||
- MQTT-Broker nicht erreichbar → Reconnect
|
||||
- Reset während aktiver Session
|
||||
- Neustart nach akkumulierter Zeit → Zeit korrekt aus NVS geladen
|
||||
- [x] **9.4** NTP-Zeitsynchronisation + `session_start_time` im MQTT-Session-Payload
|
||||
- `configTime(0, 0, "pool.ntp.org", "time.nist.gov")` in `wifi_connector.cpp` nach WLAN-Connect (auch Reconnect)
|
||||
- `waitForNtp()` blockiert max. 5 s bis Sync bestätigt (`getLocalTime()`), loggt Ergebnis via `LOG_I`
|
||||
- `LaserTracker::onSessionStart()` speichert `time(nullptr)` → `_sessionStartTime`
|
||||
- `publishSession()` formatiert UTC-Zeitstempel als ISO-8601 (`strftime`, `gmtime`)
|
||||
- Fallback `"unknown"` wenn NTP beim Session-Start noch nicht synchronisiert
|
||||
- Verifiziert: `{"session_start_time":"2026-02-23T...Z"}` im MQTT-Payload bestätigt
|
||||
|
||||
- [x] **9.5** Edge-Cases manuell getestet (Integrations-Hardware-Test):
|
||||
- Laser aktiv, WLAN getrennt → Display läuft weiter ✅
|
||||
- MQTT-Broker nicht erreichbar → Reconnect ✅
|
||||
- Reset während aktiver Session ✅
|
||||
- Neustart nach akkumulierter Zeit → Zeit korrekt aus NVS geladen ✅
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -32,10 +32,10 @@ Das Bild zeigt die physische Anordnung der 8 GYMAX7219-Module im 4×2-Format mit
|
|||
|
||||

|
||||
|
||||
[TODO] Ändere den Begriff "Session" in "aktuelle Laserzeit" (das ist die aktuelle Session)
|
||||
[TODO] Letzer Burst entspricht "aktueller Laserzeit". Dieser Begriff "Burst" kann weg.
|
||||
[TODO] Ergänze "Summe Laserzeit" (Das ist die aktuelle Summer aller Session bis zu einem manuellen Reset über das Web, MQTT, Button oder Restart)
|
||||
[TODO] Ersetze "Gesamtzeit (NVS)" durch "Maschinenlaufzeit (gesamt)"
|
||||
[X] Ändere den Begriff "Session" in "aktuelle Laserzeit" (das ist die aktuelle Session)
|
||||
[X] Letzer Burst entspricht "aktueller Laserzeit". Dieser Begriff "Burst" kann weg.
|
||||
[X] Ergänze "Summe Laserzeit" (Das ist die aktuelle Summer aller Session bis zu einem manuellen Reset über das Web, MQTT, Button oder Restart)
|
||||
[X] Ersetze "Gesamtzeit (NVS)" durch "Maschinenlaufzeit (gesamt)"
|
||||
|
||||
### Konfiguration
|
||||

|
||||
|
|
@ -174,7 +174,7 @@ INACTIVE ──(Laser an)──► GRATIS ──(Gratiszeit abgelaufen)──►
|
|||
|---|---|---|---|
|
||||
| `INACTIVE` | `--` | – | – |
|
||||
| `GRATIS` | Countdown (z.B. `19`, `18`, ...) | läuft nicht | – |
|
||||
| `NET_COUNTING` | [TODO] Sekundenzähler siehe Session-Sekunden | läuft | – |
|
||||
| `NET_COUNTING` | Sekundenzähler siehe Session-Sekunden | läuft | – |
|
||||
| → Laser aus (aus `NET_COUNTING`) | `--` | wird addiert | gespeichert |
|
||||
| → Laser aus (aus `GRATIS`) | `--` | 0 addiert | Dauer verwendeter Gratiszeit wird gespeichert |
|
||||
|
||||
|
|
@ -200,7 +200,7 @@ Wird bei Neustart und `resetSession()` auf 0 zurückgesetzt.
|
|||
|
||||
### Session-Sekunden
|
||||
|
||||
[TODO] Dies wird in Fomr eines umlaufenden Kreis um die Module 5–7 angezeigt wie es auf einer Analogen Uhr überlich ist. 3 Module haben umlaufenden 60 Leds. Jede Sekunden wird eine weitere LED dazu geschaltet. Nach 60 Sekunden ist der Kreis voll und die Minutenanzeige (Module 1–3) wird inkrementiert. Bei Laser-Aus wird diese Anzeige wieder auf "--" zurückgesetzt.
|
||||
Dies wird in Form eines umlaufenden Kreis um die Module 5–7 angezeigt wie es auf einer Analogen Uhr überlich ist. 3 Module haben umlaufenden 60 Leds. Jede Sekunden wird eine weitere LED dazu geschaltet. Nach 60 Sekunden ist der Kreis voll und die Minutenanzeige (Module 1–3) wird inkrementiert. Bei Laser-Aus wird diese Anzeige wieder auf "--" zurückgesetzt.
|
||||
---
|
||||
|
||||
## MQTT Client
|
||||
|
|
@ -218,18 +218,22 @@ Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterfac
|
|||
### JSON-Format `lasercutter/session`
|
||||
```json
|
||||
{
|
||||
"session_minuten": 125,
|
||||
"session_start_time": "2024-06-01T12:34:56Z",
|
||||
"session_minuten": 2,
|
||||
"session_seconds": 95,
|
||||
"session_start_time": "2026-02-23T12:34:56Z",
|
||||
"gratiszeit_s": 20,
|
||||
"ip": "192.168.1.100"
|
||||
"ip": "192.168.2.62"
|
||||
}
|
||||
```
|
||||
|
||||
> `session_start_time` ist ein UTC-Zeitstempel (ISO 8601), synchronisiert via NTP (`pool.ntp.org`) unmittelbar nach WLAN-Connect.
|
||||
> Wert `"unknown"` wenn die NTP-Synchronisation beim Session-Start noch nicht abgeschlossen war.
|
||||
|
||||
### JSON-Format `lasercutter/status`
|
||||
```json
|
||||
{
|
||||
"online": true,
|
||||
"session_sum": "42.50", -
|
||||
"session_sum": "42.50",
|
||||
"machine_running_time_min": "1234.75",
|
||||
"ip": "192.168.1.100",
|
||||
"uptime_s": 3600
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
// =============================================================================
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <time.h>
|
||||
#include "config.h"
|
||||
#include "settings.h"
|
||||
|
||||
|
|
@ -76,6 +77,9 @@ public:
|
|||
// 0 wenn GRATIS oder INACTIVE
|
||||
int getRunningSessionSeconds() const;
|
||||
|
||||
// Unix-Timestamp (UTC) des letzten Session-Starts (0 wenn noch keine Session)
|
||||
time_t getSessionStartTime() const;
|
||||
|
||||
// ---- Reset --------------------------------------------------------------
|
||||
|
||||
// Session-Minuten auf 0 (RAM). NVS-Gesamtzeit bleibt unveraendert.
|
||||
|
|
@ -104,6 +108,7 @@ private:
|
|||
// ---- Session-Zustand ---------------------------------------------------
|
||||
SessionState _state; // aktueller Session-Zustand
|
||||
uint32_t _sessionStartMs; // millis() beim Laser-AN (Session-Start)
|
||||
time_t _sessionStartTime; // Unix-Timestamp (UTC) beim Laser-AN
|
||||
uint32_t _netStartMs; // millis() beim Uebergang GRATIS -> NET_COUNTING
|
||||
|
||||
// ---- Zeitakkumulatoren --------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ LaserTracker::LaserTracker()
|
|||
, _debounceStartMs(0)
|
||||
, _state(SessionState::INACTIVE)
|
||||
, _sessionStartMs(0)
|
||||
, _sessionStartTime(0)
|
||||
, _netStartMs(0)
|
||||
, _sessionNetSec(0)
|
||||
, _sessionNetMin(0)
|
||||
|
|
@ -76,12 +77,17 @@ void LaserTracker::loop() {
|
|||
// ---------------------------------------------------------------------------
|
||||
// Ereignis: Laser AN (nach Debounce)
|
||||
void LaserTracker::onSessionStart() {
|
||||
_state = SessionState::GRATIS;
|
||||
_sessionStartMs = millis();
|
||||
_netStartMs = 0;
|
||||
_state = SessionState::GRATIS;
|
||||
_sessionStartMs = millis();
|
||||
_sessionStartTime = time(nullptr);
|
||||
_netStartMs = 0;
|
||||
Serial.println("[LaserTracker] SessionStart -> GRATIS");
|
||||
}
|
||||
|
||||
time_t LaserTracker::getSessionStartTime() const {
|
||||
return _sessionStartTime;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ereignis: Laser AUS (nach Debounce)
|
||||
void LaserTracker::onSessionEnd() {
|
||||
|
|
|
|||
|
|
@ -100,11 +100,20 @@ void MqttClient::publishSession(int lastSessionSec, int gratisSec) {
|
|||
// session_minuten: Decken-Rundung (62 s = 2 min)
|
||||
int sessionMinuten = (lastSessionSec + 59) / 60;
|
||||
|
||||
// session_start_time als ISO-8601 UTC ("2024-06-01T12:34:56Z")
|
||||
char startTimeBuf[32] = {0};
|
||||
time_t startTime = laserTracker.getSessionStartTime();
|
||||
if (startTime > 0) {
|
||||
struct tm* tmInfo = gmtime(&startTime);
|
||||
strftime(startTimeBuf, sizeof(startTimeBuf), "%Y-%m-%dT%H:%M:%SZ", tmInfo);
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
doc["session_minuten"] = sessionMinuten;
|
||||
doc["session_seconds"] = lastSessionSec;
|
||||
doc["gratiszeit_s"] = gratisSec;
|
||||
doc["ip"] = WiFi.localIP().toString();
|
||||
doc["session_minuten"] = sessionMinuten;
|
||||
doc["session_seconds"] = lastSessionSec;
|
||||
doc["session_start_time"] = (startTime > 0) ? startTimeBuf : "unknown";
|
||||
doc["gratiszeit_s"] = gratisSec;
|
||||
doc["ip"] = WiFi.localIP().toString();
|
||||
|
||||
char buf[128];
|
||||
serializeJson(doc, buf, sizeof(buf));
|
||||
|
|
|
|||
|
|
@ -7,6 +7,23 @@
|
|||
// Globale Instanz
|
||||
WifiConnector wifiConnector;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// NTP-Sync abwarten (non-restart-blocking, max. timeoutMs)
|
||||
static void waitForNtp(uint32_t timeoutMs = 5000) {
|
||||
struct tm ti;
|
||||
uint32_t t0 = millis();
|
||||
while (!getLocalTime(&ti) && (millis() - t0) < timeoutMs) {
|
||||
delay(100);
|
||||
}
|
||||
if (getLocalTime(&ti)) {
|
||||
char buf[32];
|
||||
strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &ti);
|
||||
LOG_I("WIFI", "NTP synchronisiert: %s", buf);
|
||||
} else {
|
||||
LOG_E("WIFI", "NTP-Sync Timeout (%u ms) – Zeitstempel ungueltig", timeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
// Static-Member Initialisierung
|
||||
WifiConnector* WifiConnector::_instance = nullptr;
|
||||
|
||||
|
|
@ -82,6 +99,9 @@ void WifiConnector::begin() {
|
|||
LOG_I("WIFI", "Verbunden mit: %s", WiFi.SSID().c_str());
|
||||
LOG_I("WIFI", "IP-Adresse : %s", WiFi.localIP().toString().c_str());
|
||||
LOG_I("WIFI", "RSSI : %d dBm", WiFi.RSSI());
|
||||
// NTP-Zeitsynchronisation (UTC), auf Sync warten (max. 5 s)
|
||||
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
||||
waitForNtp();
|
||||
} else {
|
||||
LOG_E("WIFI", "Timeout – starte ESP32 neu ...");
|
||||
delay(1000);
|
||||
|
|
@ -108,6 +128,8 @@ void WifiConnector::loop() {
|
|||
setStatus(WifiStatus::CONNECTED);
|
||||
LOG_I("WIFI", "Reconnect erfolgreich – IP: %s",
|
||||
WiFi.localIP().toString().c_str());
|
||||
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
|
||||
waitForNtp();
|
||||
} else {
|
||||
LOG_E("WIFI", "Reconnect fehlgeschlagen – starte neu ...");
|
||||
delay(1000);
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user