# 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: 0–3 │ 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: 4–7 │ 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) | | 1–3 | 1–3 | Oben Mitte–rechts | **Session-Minuten** ganzzahlig (0 bei Neustart, RAM-only) | | 4 | 4 | Unten links | **MQTT-Fehler** (`E` = kein Broker, leer = OK) | | 5–7 | 5–7 | Unten Mitte–rechts| **Countdown** (Gratiszeit in Sek.) oder `--` (Idle/Netto) | - Summe Session (Module 1–3) 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 1–3, 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: 0–120 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 1–60, dann +2 ab Sekunde 61 - Session mit 120 s Netto → live +1 bei Sek. 1–60, +2 ab Sek. 61–120 Wird bei Neustart und `resetSessionSum()` auf 0 zurückgesetzt. ### Session-Sekunden 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 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, 0–120 | | 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 ``` (): [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.