MQTT-Display-LaserCutter/README.md
MaPaLo76 c61a67f812 feat(web+mqtt): FR-010 Webinterface-Redesign + MQTT-Steuerung
Webinterface:
- /: oeffentlich, Laser Cutter Status, m:ss, an/aus, Display-Toggle (fetch/204)
- /config: H2-Abschnitte (Status, MQTT, Webzugang, Geraet, Tools)
  Maschinenlaufzeit h:mm:ss, Reboot-Button (disabled bei aktivem Laser)
- /reset-total: neue POST-Route fuer NVS-Maschinenlaufzeit-Reset (Auth)
- /display-toggle: POST, 204, kein Seitenwechsel
- /reboot: POST, Auth, ESP.restart()

DisplayManager:
- setEnabled()/isEnabled() via MAX7219 SHUTDOWN-Modus

MQTT:
- lasercutter/reset -> lasercutter/cmd (MQTT_TOPIC_CMD)
- Kommandos: reset_session_nvs, reset_session_ram, display, reboot
- Heartbeat lasercutter/status: display_on-Feld hinzugefuegt
2026-03-01 17:28:47 +01:00

518 lines
25 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Laser Cutter Dot Matrix Display und MQTT Client
Dieses Projekt implementiert einen ESP32-basierten MQTT-Client mit Dot-Matrix-Display für einen Laser Cutter. Es misst und visualisiert die aktive Laserzeit, sendet Session-Daten an einen MQTT-Broker und bietet ein Browser-Webinterface zur Konfiguration und Steuerung.
---
## Inhaltsverzeichnis
1. [Hardware](#hardware)
2. [Pinbelegung](#pinbelegung)
3. [Display](#display)
4. [Laser-Signaldetektion](#laser-signaldetektion)
5. [Zeit-Tracking & Gratiszeit](#zeit-tracking--gratiszeit)
6. [MQTT Client](#mqtt-client)
7. [Webinterface](#webinterface)
8. [Konfiguration & Persistenz](#konfiguration--persistenz)
9. [WiFi-Setup (WiFiManager)](#wifi-setup-wifimanager)
10. [OTA Firmware-Update](#ota-firmware-update)
11. [Remote-Debugging (Web Console)](#remote-debugging-web-console)
12. [Fehlerverhalten](#fehlerverhalten)
13. [Bibliotheken](#bibliotheken)
14. [Build & Flash](#build--flash)
15. [Tests](#tests)
16. [Beitragen / Commits](#beitragen--commits)
---
## Skizze und Screenshots
![Front-Ansicht des Laser Cutter Displays](./doc/Front.svg)
Das Bild zeigt die physische Anordnung der 8 GYMAX7219-Module im 4×2-Format mit LED-Positionen und Beschriftung der Anzeigebereiche.
### LaserCutter Display
![LaserCutter Display](./doc/screenshot1.png)
[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
![Konfiguration](./doc/screenshot2.png)
### OTA Firmware-Update Seite
![OTA Firmware-Update](./doc/screenshot3.png)
## Hardware
| Komponente | Modell / Beschreibung |
|-------------------------|----------------------------------------------------|
| Mikrocontroller | AZ-Delivery ESP32 DevKit V4 EPS32-WROOM (Dual Core) |
| Dot-Matrix-Display | 8× GYMAX7219 Module (kompatibel zu MAX7219), 8×8 LEDs je Modul, Typ: `GENERIC_HW` |
| Display-Anordnung | 4 Module nebeneinander × 2 Reihen = 32×16 LEDs |
| Display-Kaskadierung | Alle 8 Module in einer einzigen SPI-Kette |
| Stromversorgung Display | **Externes 5 V-Netzteil erforderlich** (gemessen: ~0,5 A / ~2,5 W bei 8 Modulen, Vollast bis ~2 A möglich) ESP32 USB-Port reicht nicht aus |
| Laser-Aktivsignal | Potentialfreier Ausgang des Laser Cutters (Optokoppler empfohlen) |
| Optional | Shelly PM Mini G3 als externer Leistungszähler (separater MQTT-Service) |
---
## Pinbelegung
| Signal | ESP32 GPIO | Beschreibung |
|---------------|-----------|-----------------------------------------------------------|
| MAX7219 MOSI | GPIO 23 | SPI Data (VSPI) → DIN Modul 1 |
| MAX7219 CLK | GPIO 18 | SPI Clock (VSPI) |
| MAX7219 CS | GPIO 5 | SPI Chip Select (alle Module) |
| Laser-Signal | GPIO 4 | Potentialfreier Eingang (INPUT_PULLUP, konfigurierbar) |
> Die Polarität des Laser-Signals (`LOW_ACTIVE` oder `HIGH_ACTIVE`) ist über das Webinterface konfigurierbar.
### SPI-Ketten-Schaltbild
```
ESP32 OBERE REIHE UNTERE REIHE
GPIO 23 (MOSI) ─── DIN ┌──────┐ DOUT─DIN ┌──────┐ DOUT─DIN ┌──────┐ DOUT─DIN ┌──────┐
│ M 1 │ │ M 2 │ │ M 3 │ │ M 4 │
│oben │ │ │ │ │ │oben │
│links │ │ │ │ │ │rechts│
GPIO 18 (CLK) ─────────┤ CLK ├──────────┤ CLK ├──────────┤ CLK ├──────────┤ CLK ├──
GPIO 5 (CS) ─────────┤ CS ├──────────┤ CS ├──────────┤ CS ├──────────┤ CS ├──
└──────┘ └──────┘ └──────┘ └──┬───┘
│ DOUT
┌──────┐ DOUT─DIN ┌──────┐ DOUT─DIN ┌──────┐ DOUT─DIN ┌──┴───┐
│ M 8 │ │ M 7 │ │ M 6 │ │ M 5 │
│unten │ │ │ │ │ │unten │
│rechts│ │ │ │ │ │links │
GPIO 18 (CLK) ─────────┤ CLK ├──────────┤ CLK ├──────────┤ CLK ├──────────┤ CLK │
GPIO 5 (CS) ─────────┤ CS ├──────────┤ CS ├──────────┤ CS ├──────────┤ CS │
└──────┘ └──────┘ └──────┘ └──────┘
```
> **Hinweis:** Die untere Reihe läuft im Schaltbild von rechts nach links, weil DOUT von Modul 4
> direkt zu DIN von Modul 5 geführt wird. MD_Parola berücksichtigt das automatisch.
---
## Display
Das Display besteht aus 8 GYMAX7219-Modulen in einer **4×2-Anordnung** (32×16 LEDs gesamt).
Es ist in **zwei Zonen** aufgeteilt, die unabhängig voneinander beschrieben werden.
### Physisches Layout
```
DIN ← ESP32 GPIO 23
┌─────────┬─────────┬─────────┬─────────┐
│ Modul 1 │ Modul 2 │ Modul 3 │ Modul 4 │ ← Zone 0 (oben) 0-idx: 03
│ oben │ │ │ oben │ idx 0 -> Anzeige: WiFi-Fehler (E = Fehler, leer = OK)
│ links │ │ │ rechts │ idx 1 -3 -> Anzeige: Summe Laserzeit (Minuten) -> Summe aller Sessions bis zum Reset
├─────────┼─────────┼─────────┼─────────┤
│ Modul 5 │ Modul 6 │ Modul 7 │ Modul 8 │ ← Zone 1 (unten) 0-idx: 47
│ unten │ │ │ unten │ idx 4 -> Anzeige: MQTT-Fehler (E = kein Broker, leer = OK)
│ links │ │ │ rechts │ idx 5 -7 -> Anzeige: Countdown / Status
└─────────┴─────────┴─────────┴─────────┘
SPI-Kette: ESP32 → M1 → M2 → M3 → M4 → M5 → M6 → M7 → M8
```
**Hardware-Typ:** `GENERIC_HW` (verifiziert durch Hardware-Test; physische Ausrichtung erfordert 90° CCW Software-Rotation)
> **Stromversorgung:** Die 8 Module müssen über ein **externes 5 V-Netzteil** versorgt werden. GND des Netzteils mit ESP32-GND verbinden. Gemessene Leistungsaufnahme im Betrieb: ca. **0,5 A / 2,5 W**; bei allen LEDs EIN (Testmuster) bis ca. 2 A möglich.
### Modul-Belegung
| Modul(e) | 0-Index | Physisch | Anzeige-Inhalt |
|----------|---------|-------------------|---------------------------------------------------------------|
| 0 | 0 | Oben links | **WiFi-Fehler** (`E` = kein WLAN, leer = OK) |
| 13 | 13 | Oben Mitterechts | **Session-Minuten** ganzzahlig (0 bei Neustart, RAM-only) |
| 4 | 4 | Unten links | **MQTT-Fehler** (`E` = kein Broker, leer = OK) |
| 57 | 57 | Unten Mitterechts| **Countdown** (Gratiszeit in Sek.) oder `--` (Idle/Netto) |
- Summe Session (Module 13) inkrementiert mit **jeder angefangenen Minute pro Session** (Ceiling). Beispiel: 10 s Netto = +1 Min, 65 s Netto = +2 Min (1 bei Sekunde 1, 2 ab Sekunde 61).
- Die Netto-Zeit beginnt erst nach Ablauf der Gratiszeit (Laser muss länger als `gratisSeconds` aktiv bleiben).
- Die Anzeige aktualisiert sich im `loop()` ohne Blocking-Delays.
- Bibliotheken: `MD_MAX72XX` (direkte Puffer-Steuerung, keine MD_Parola)
---
## Laser-Signaldetektion
Der ESP32 überwacht einen **digitalen GPIO-Eingang** mit internem Pull-Up-Widerstand (`INPUT_PULLUP`).
Der potentialfreie Ausgang des Laser Cutters (z.B. Schließer-Kontakt über Optokoppler) wird an diesen Pin angeschlossen.
- **Polarität konfigurierbar**: Im Webinterface einstellbar, ob `LOW` oder `HIGH` den aktiven Zustand bedeutet.
- **Debounce**: Software-Entprellung, um Fehlmessungen bei Schaltflanken zu vermeiden. [TODO] Parameter für Debounce-Zeit (z.B. 50 ms) konfigurierbar machen.
- Eine **Session** beginnt, wenn der Laser aktiv wird, und endet, wenn er inaktiv wird.
- Eine Session besteht aus Abglaufener Gratizeit + Laser-AN-Zeit (Netto-Zeit). Eine "Session" ohne Laser Zeit ist keine Session und wird auch nicht als MQTT Session veröffentlicht. Unabhängig von einer vollständigen Session wird die Laser-AN-Zeit immer in der NVS-Gesamtzeit akkumuliert (für Wartungsintervalle).
---
## Zeit-Tracking & Gratiszeit
### Drei getrennte Zeitkreise
| Kreis | Speicher | Reset | Verwendung |
|---|---|---|---|
| **Session-Minuten** | RAM | bei Neustart / `resetSessionSum()` | MQTT Session in Minuten, Web-UI |
| **Summe Session** | RAM | bei Neustart / `resetSessionSum()` | Display Module 13, Web-UI, nicht in MQTT |
| **Gesamtzeit** | NVS (`total_min`) | nur per `resetTotal()` | MQTT, Web-UI, Wartungsstatistik |
### Session-Zustandsmaschine
Jedes Laser-AN-Ereignis durchläuft drei Zustände:
```
INACTIVE ──(Laser an)──► GRATIS ──(Gratiszeit abgelaufen)──► NET_COUNTING
▲ │ │
└───────────────────────┴──────────(Laser aus)─────────────────┘
```
| Zustand | Display unten | Netto-Zeit | NVS |
|---|---|---|---|
| `INACTIVE` | `--` | | |
| `GRATIS` | Countdown (z.B. `19`, `18`, ...) | läuft nicht | |
| `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 |
### Gratiszeit
- Konfigurierbar: 0120 Sekunden (Standard: **20 s**)
- Startet **neu bei jedem** Laser-AN-Ereignis
- Geht der Laser während der Gratiszeit wieder aus: keine Session-Zeit, aber NVS zählt die Einschaltdauer
- Einstellbar über das Webinterface
### NVS-Gesamtzeit
Zählt **jede Sekunde Laser-AN**, inklusive Gratiszeit. Eignet sich für Wartungsintervalle (tatsächliche Einschaltdauer des Lasers).
### Session-Minuten (Display)
Zählt nur die **Netto-Zeit** (nach Ablauf der Gratiszeit). Jede **angefangene Minute pro Session** wird aufgerundet gezählt (Ceiling). Beispiele:
- Session mit 10 s Netto → +1 Minute
- Session mit 65 s Netto → live +1 bei Sekunde 160, dann +2 ab Sekunde 61
- Session mit 120 s Netto → live +1 bei Sek. 160, +2 ab Sek. 61120
Wird bei Neustart und `resetSessionSum()` auf 0 zurückgesetzt.
### Session-Sekunden
Dies wird in Form eines umlaufenden Kreis um die Module 57 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 13) wird inkrementiert. Bei Laser-Aus wird diese Anzeige wieder auf "--" zurückgesetzt.
---
## MQTT Client
Der ESP32 verbindet sich mit einem MQTT-Broker (konfigurierbar über Webinterface).
### Topics
| Richtung | Topic | Format | Beschreibung |
|------------|---------------------------|------------------------------|-----------------------------------------|
| Publish | `lasercutter/session` | JSON | Wird beim **Ende eines Laser-Bursts** oder nach **Session-Reset** gesendet |
| Publish | `lasercutter/status` | JSON (retained) | Heartbeat alle 60 Sekunden + LWT (online/offline) |
| Subscribe | `lasercutter/cmd` | JSON | Steuerkommandos: Reset, Display, Reboot |
### JSON-Format `lasercutter/session`
```json
{
"session_minutes": 2,
"session_seconds": 95,
"session_start_time": "2026-02-23T13:34:56",
"freetime_s": 20,
"ip": "192.168.2.62"
}
```
> `session_start_time` ist ein Lokalzeit-Zeitstempel (ISO 8601, CET/CEST), synchronisiert via NTP (`pool.ntp.org`) unmittelbar nach WLAN-Connect. Zeitzone wird automatisch zwischen CET (UTC+1) und CEST (UTC+2) umgeschaltet.
> Wert `"unknown"` wenn die NTP-Synchronisation beim Session-Start noch nicht abgeschlossen war.
> Bei einem **Session-Reset** (Web oder MQTT `{"reset_session_ram":true}`) wird ebenfalls ein `lasercutter/session`-Publish ausgelöst mit den bis dahin akkumulierten Werten identisches Format wie beim normalen Session-Ende.
### JSON-Format `lasercutter/status`
```json
{
"online": true,
"session_sum": "42.50",
"machine_running_time_min": "1234.75",
"display_on": true,
"ip": "192.168.1.100",
"uptime_s": 3600,
"firmware_version": "1.2.1 (Mar 1 2026)",
"reset_reason": "POWERON"
}
```
> `reset_reason` mögliche Werte: `POWERON`, `SOFTWARE`, `PANIC`, `TASK_WDT`, `INT_WDT`, `WDT`, `BROWNOUT`, `EXT_PIN`, `DEEPSLEEP`, `SDIO`, `UNKNOWN`. Der Wert wird einmalig beim Start gespeichert und bleibt für die gesamte Laufzeit konstant.
### JSON-Format `lasercutter/cmd`
Alle Steuerkommandos werden als JSON an `lasercutter/cmd` gesendet:
```json
{ "reset_session_nvs": true } // Session-Summe + NVS-Maschinenlaufzeit auf 0
{ "reset_session_ram": true } // nur RAM-Session-Summe auf 0, NVS bleibt
{ "display": true } // Display einschalten
{ "display": false } // Display ausschalten
{ "reboot": true } // ESP32 neu starten
```
> Mehrere Kommandos können in einem JSON kombiniert werden, z. B. `{"reset_session_ram":true, "display":false}`.
### LWT (Last Will and Testament)
Bei Verbindungsabbruch sendet der Broker automatisch auf `lasercutter/status`:
```json
{"online": false}
```
### Verhalten
- Non-Blocking Reconnect alle 10 s, falls MQTT-Broker nicht erreichbar.
- Bei aktivem Laser läuft die Zeitmessung unabhängig vom MQTT-Status weiter.
- Client-ID: `lasercutter-display-XXXXXX` (mit MAC-Suffix, eindeutig auf Public Broker).
- TLS (Port 8883): wird automatisch aktiviert wenn Port 8883 konfiguriert ist (`WiFiClientSecure`, `setInsecure`).
---
## Webinterface
Das Webinterface ist über die IP-Adresse des ESP32 im Browser erreichbar.
| Seite | URL | Auth | Funktion |
|--------------------------|-------------------|------|-----------------------------------------------------------------------|
| Laser Cutter Status | `/` | | Laserzeit, Laserstatus, Display-Toggle, Session-Reset |
| Laser Cutter Setup | `/config` | ✔ | MQTT, Web-Auth, WLAN, Maschinenlaufzeit-Reset, Gratiszeit, Polarität, Reboot |
| Session-Reset | `/reset` | | Setzt die **Session-Summe** (RAM) auf 0 NVS bleibt erhalten |
| Maschinenlaufzeit-Reset | `/reset-total` | ✔ | Setzt Maschinenlaufzeit im NVS auf 0 |
| Display-Toggle | `/display-toggle` | | Display ein-/ausschalten (POST, antwortet 204) |
| Reboot | `/reboot` | ✔ | ESP32 sofort neu starten |
| OTA Update | `/update` | | Firmware-Update über Browser (ElegantOTA) |
| Log Console | `/log` | ✔ | Live-Log über WebSocket |
> `/` ist **öffentlich** (kein Passwortschutz) der Display-Toggle und Session-Reset sind bewusst ohne Auth erreichbar. Alle sicherheitsrelevanten Seiten (Setup, Reboot, Maschinenlaufzeit-Reset) erfordern HTTP Basic Auth, sofern ein Web-Passwort gesetzt ist.
---
## Konfiguration & Persistenz
Alle Einstellungen werden im **NVS (Non-Volatile Storage)** des ESP32 gespeichert und sind über das Webinterface änderbar:
| Einstellung | Standard | Beschreibung |
|-----------------------|---------------|-----------------------------------------|
| MQTT Broker | `broker.hivemq.com` | Hostname oder IP des MQTT-Brokers |
| MQTT Port | `1883` | Port des MQTT-Brokers (8883 = TLS auto) |
| MQTT User | *(leer)* | MQTT Benutzername (optional) |
| MQTT Passwort | *(leer)* | MQTT Passwort (optional) |
| Gratiszeit | `20` | Sekunden, 0120 |
| Signal-Polarität | `LOW_ACTIVE` | `LOW_ACTIVE` oder `HIGH_ACTIVE` |
| Akkumulierte Zeit | `0.0` | Gespeicherte Laserzeit in Minuten (NVS) |
| Web-Benutzername | `admin` | HTTP-Basic-Auth Benutzername |
| Web-Passwort | *(leer)* | Leer = kein Passwortschutz; setzen über `/config` |
---
## WiFi-Setup (WiFiManager)
Beim ersten Start (oder wenn keine gespeicherten WLAN-Daten vorhanden) öffnet das Gerät einen **Access Point** mit Captive Portal:
- SSID: `LaserCutter-Setup`
- Im Browser öffnet sich automatisch die Konfigurationsseite.
- WLAN-Credentials werden nach erfolgreicher Verbindung im NVS gespeichert.
---
## OTA Firmware-Update
Firmware-Updates können kabellos über die Weboberfläche unter `/update` eingespielt werden (ElegantOTA).
---
## Remote-Debugging (Web Console)
Geplant (FR-002): Neue Route `/log` im bestehenden Webinterface zeigt alle Log-Ausgaben live im Browser über WebSocket.
- Kein extra Library nötig nutzt den bestehenden `ESPAsyncWebServer`
- Aufruf: `http://172.30.30.90/log`
- Noch nicht implementiert.
---
## Fehlerverhalten
| Zustand | Modul 0 (oben links) | Modul 4 (unten links) | Verhalten |
|-----------------------|---------------------|-----------------------|---------------------------------------|
| WLAN getrennt | `E` (blinkt) | | Zeiterfassung läuft weiter, Reconnect |
| MQTT nicht erreichbar | | `E` (blinkt) | Zeiterfassung läuft weiter, Reconnect |
| WLAN + MQTT OK | leer | leer | Normaler Betrieb |
---
## Bibliotheken
| Bibliothek | Zweck |
|-----------------------|---------------------------------------------------------------------|
| `MD_MAX72XX` | Treiber für MAX7219/GYMAX7219, direkte Puffer-Steuerung |
| `MD_Parola` | (Dependency von MD_MAX72XX, nicht direkt genutzt) |
| `PubSubClient` | MQTT Client |
| `WiFiManager` | WiFi Captive Portal |
| `ESPAsyncWebServer` | Asynchroner Webserver |
| `AsyncTCP` | TCP-Basis für ESPAsyncWebServer |
| `ArduinoJson` | JSON Serialisierung/Deserialisierung |
| `Preferences` | NVS-Zugriff (built-in ESP32 Arduino) |
| `ElegantOTA` | OTA-Update über Webinterface |
---
## Build & Flash
### Via USB (Standard)
```bash
# Haupt-Firmware bauen und flashen
pio run -e az-delivery-devkit-v4 --target upload
# Serieller Monitor
pio device monitor -e az-delivery-devkit-v4
```
Ziel-Board: `az-delivery-devkit-v4` (ESP32), Upload-Port: `COM3`
---
### Via WiFi (OTA)
Sobald das Gerät einmal per USB geflasht wurde und im WLAN erreichbar ist, kann jeder weitere Flash-Vorgang kabellos erfolgen.
**Voraussetzung:** ESP32 läuft und ist im WLAN verbunden.
**1. IP-Adresse in `platformio.ini` eintragen** (einmalig):
```ini
[env:az-delivery-devkit-v4-ota]
upload_port = 192.168.2.62 ; <-- hier die IP des ESP32 eintragen
```
Die aktuelle IP ist im Router (DHCP-Tabelle) oder über mDNS (`lasercutter-display.local`) im Browser erreichbar.
**2. Flashen via PlatformIO:**
```bash
pio run -e az-delivery-devkit-v4-ota --target upload
```
**3. Flashen via Arduino IDE:**
Im Menü **Tools → Port** erscheint nach ein paar Sekunden automatisch ein Netzwerk-Port:
```
lasercutter-display at 192.168.x.x
```
Diesen auswählen und normal über **Sketch → Hochladen** flashen.
> **Hinweis:** Falls ein Web-Passwort gesetzt ist, verwendet ArduinoOTA dasselbe Passwort. Es muss in `platformio.ini` per `upload_flags` übergeben werden:
> ```ini
> [env:az-delivery-devkit-v4-ota]
> upload_flags = --auth=DEIN_WEBPASSWORT
> ```
> Arduino IDE fragt das Passwort beim Upload automatisch ab.
> **Wichtig (Erstinbetriebnahme):** Der erste OTA-Flash setzt voraus, dass die aktuelle Firmware bereits ArduinoOTA enthält. Nach einem Neuflash via USB (`az-delivery-devkit-v4`) ist OTA dauerhaft verfügbar.
---
## Projektstruktur
```
MQTT-Display-LaserCutter/
├── src/
│ ├── main.cpp # Hauptprogramm
│ ├── display_manager.cpp # Display-Implementierung (MD_MAX72XX)
│ ├── laser_tracker.cpp # Signal-Detektion, Burst-Logik, Zeiterfassung
│ ├── settings.cpp # NVS-Persistenz (Preferences)
│ ├── wifi_connector.cpp # WiFiManager-Wrapper
│ ├── mqtt_client.cpp # MQTT-Wrapper (PubSubClient, TLS, Phase 6)
│ └── web_server.cpp # Webinterface (ESPAsyncWebServer, Phase 7)
├── include/
│ ├── config.h # Pin-Definitionen, Konstanten, LOG-Makros
│ ├── display_manager.h # Display-API (showLaserTime, showCountdown, ...)
│ ├── laser_tracker.h # SessionState-Maschine, getAllSessionsSumMinutes(), ...
│ ├── settings.h # Settings-Struct, SettingsManager
│ ├── wifi_connector.h # WiFi-Verbindungsmanagement
│ ├── mqtt_client.h # (Phase 6) MQTT-Wrapper (PubSubClient)
│ └── web_server.h # (Phase 7) Webinterface (ESPAsyncWebServer)
├── test_sketches/
│ ├── test_display.cpp # 1.4 - GYMAX7219 Moduldignose
│ ├── test_button.cpp # 1.5 - Potentialfreier Schalter
│ ├── test_nvs.cpp # 2.2 - NVS Persistenz
│ ├── test_wifi.cpp # 3.3 - WiFiManager
│ ├── test_display_manager.cpp # 4.3 - DisplayManager
│ ├── test_laser_tracker.cpp # 5.6 - LaserTracker
│ ├── test_mqtt_client.cpp # 6.5 - MqttClient (TLS, Session, Heartbeat)
│ ├── test_web_server.cpp # 7.7 - WebServer (Routen, Config, OTA)
│ └── mqtt_test_secrets.h # (gitignoriert) persönliche Broker-Credentials
├── platformio.ini
└── README.md
```
---
## Tests
Die tests sind in `test_sketches/` als eigenständige Sketches organisiert, die jeweils spezifische Funktionalitäten der Hardware oder Software testen.
Auch die Beschreibung der tests befindet sich in der README.md in dem Verzeichneis `test_sketches/` um die Übersicht zu behalten.
---
## Implementierungsstand
| Phase | Beschreibung | Status |
|-------|-------------|--------|
| 1 | Hardware-Grundtest (Display, Button) | ✅ abgeschlossen |
| 2 | NVS-Persistenz (Settings) | ✅ abgeschlossen |
| 3 | WiFiManager | ✅ abgeschlossen |
| 4 | DisplayManager | ✅ abgeschlossen |
| 5 | LaserTracker (Burst-Logik, Zeiterfassung) | ✅ abgeschlossen |
| 6 | MqttClient (PubSubClient, TLS, Heartbeat, Session, Reset) | ✅ abgeschlossen |
| 7 | WebServer (ESPAsyncWebServer, Config-UI, Auth, OTA) | ✅ abgeschlossen |
| 8 | Integrationstest (Display + MQTT + Web) | ✅ abgeschlossen |
| 9 | OTA & Stabilisierung (ArduinoOTA, non-blocking WiFi, Auth) | ✅ abgeschlossen |
| 10 | Produktiv-Deployment, Dokumentation final | ✅ abgeschlossen |
---
## Beitragen / Commits
Dieses Projekt verwendet **[Conventional Commits](https://www.conventionalcommits.org/)** für alle Git-Commit-Nachrichten.
### Format
```
<type>(<scope>): <kurze Beschreibung>
[optionaler Body]
[optionaler Footer]
```
### Wichtigste Typen
| Typ | Verwendung |
|---|---|
| `feat` | Neues Feature |
| `fix` | Bugfix |
| `docs` | Nur Dokumentation |
| `refactor` | Code-Umstrukturierung ohne Feature/Fix |
| `test` | Test-Sketches / Test-Code |
| `chore` | Build-System, Abhängigkeiten, Konfiguration |
### Beispiele
```
feat(display): add GENERIC_HW rotation compensation
fix(nvs): prevent crash on empty broker string
docs(readme): add power supply requirements
chore(platformio): reduce SPI clock to 1 MHz for stability
```
Breaking Changes werden mit `!` nach dem Typ markiert oder im Footer mit `BREAKING CHANGE:` beschrieben.