# 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. [Fehlerverhalten](#fehlerverhalten) 12. [Bibliotheken](#bibliotheken) 13. [Build & Flash](#build--flash) 14. [Tests](#tests) 15. [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](./doc/Screenshot1.svg) ![Konfiguration](./doc/Screenshot2.svg) ![OTA Firmware-Update](./doc/Screenshot3.svg) ## Hardware | Komponente | Modell / Beschreibung | |-------------------------|----------------------------------------------------| | Mikrocontroller | AZ-Delivery ESP32 DevKit V4 | | 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 │ Anzeige: Laserzeit (Minuten) │ links │ │ │ rechts │ ├─────────┼─────────┼─────────┼─────────┤ │ Modul 5 │ Modul 6 │ Modul 7 │ Modul 8 │ ← Zone 1 (unten) 0-idx: 4–7 │ unten │ │ │ unten │ Anzeige: Countdown / Status │ links │ │ │ rechts │ └─────────┴─────────┴─────────┴─────────┘ 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) | - Session-Minuten (Module 1–3) inkrementierten erst nach vollen **60 Netto-Sekunden** (harte Ganzzahl). - 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. - Eine **Session** beginnt, wenn der Laser aktiv wird, und endet, wenn er inaktiv wird. --- ## Zeit-Tracking & Gratiszeit ### Zwei getrennte Zeitkreise | Kreis | Speicher | Reset | Verwendung | |---|---|---|---| | **Session-Minuten** | RAM | bei Neustart / `resetSession()` | Display Module 1–3 | | **Gesamtzeit** | NVS (`total_min`) | nur per `resetTotal()` | MQTT, Web-UI, Wartungsstatistik | ### Burst-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` | `--` | läuft | – | | → Laser aus (aus `NET_COUNTING`) | `--` | wird addiert | gespeichert | | → Laser aus (aus `GRATIS`) | `--` | 0 addiert | Burst-Dauer 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). Inkrementiert hart bei 60 / 120 / 180 ... Netto-Sekunden. Wird bei Neustart und `resetSession()` auf 0 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** gesendet | | Publish | `lasercutter/status` | JSON (retained) | Heartbeat alle 60 Sekunden + LWT (online/offline) | | Subscribe | `lasercutter/reset` | `{"reset":true}` oder `"1"` | Setzt die akkumulierte Laserzeit auf 0 | ### JSON-Format `lasercutter/session` ```json { "session_s": 125, "total_min": "42.50", "gratiszeit_s": 20, "ip": "192.168.1.100" } ``` ### JSON-Format `lasercutter/status` ```json { "online": true, "total_min": "42.50", "ip": "192.168.1.100", "uptime_s": 3600 } ``` ### 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 | Funktion | |------------------|-------------|--------------------------------------------------------------| | Status | `/` | Aktuelle Laserzeit, letzter Session-Wert, Systemstatus | | Konfiguration | `/config` | MQTT-Broker (IP, Port, User, Passwort), Gratiszeit, Polarität | | Reset | `/reset` | Setzt akkumulierte Laserzeit zurück | | OTA Update | `/update` | Firmware-Update über Browser | --- ## 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) | --- ## 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). --- ## 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 ```bash # Haupt-Firmware bauen und flashen pio run -e az-delivery-devkit-v4 --target upload # Serieller Monitor pio device monitor ``` Ziel-Board: `az-delivery-devkit-v4` (ESP32), Upload-Port: `COM3` --- ## 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 │ ├── display_manager.h # Display-API (showLaserTime, showCountdown, ...) │ ├── laser_tracker.h # BurstState-Maschine, getSessionMinutes(), ... │ ├── 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 Alle Tests sind Hardware-Tests (kein Unit-Test-Framework). Sie werden als separate PlatformIO-Environments geflasht und über den Serial Monitor beobachtet. ### Übersicht | Nr. | Environment | Datei | Testet | |-----|-----------------------|------------------------------|-----------------------------------------------------| | 1.4 | `test-display` | `test_display.cpp` | GYMAX7219 Verkabelung, Modul-Nummerierung, Rotation | | 1.5 | `test-button` | `test_button.cpp` | Potentialfreier Schalter / Debounce an GPIO 4 | | 2.2 | `test-nvs` | `test_nvs.cpp` | NVS Lesen/Schreiben/Reset (SettingsManager) | | 3.3 | `test-wifi` | `test_wifi.cpp` | WiFiManager Captive Portal, WLAN-Verbindung | | 4.3 | `test-display-mgr` | `test_display_manager.cpp` | DisplayManager API (alle show*-Methoden) | | 5.6 | `test-laser-tracker` | `test_laser_tracker.cpp` | LaserTracker Burst-Logik, Gratiszeit, Session/NVS | | 6.5 | `test-mqtt` | `test_mqtt_client.cpp` | MqttClient TLS, Heartbeat, Session-Publish, JSON-Reset, LWT | | 7.7 | `test-web` | `test_web_server.cpp` | WebServer Routen, Config-Formular, Reset, ElegantOTA OTA | ### Test 1.4 – Display Verdrahtungstest ```bash pio run -e test-display --target upload pio device monitor -e test-display ``` Erwartetes Verhalten: Alle 8 Module zeigen nacheinander ihre Nummer, danach Laufschrift und Fülltest. ### Test 1.5 – Button / Laser-Eingang ```bash pio run -e test-button --target upload pio device monitor -e test-button ``` Erwartetes Verhalten: Serial-Ausgabe zeigt `HIGH`/`LOW` beim Betätigen des Schalters an GPIO 4. ### Test 2.2 – NVS Persistenz ```bash pio run -e test-nvs --target upload pio device monitor -e test-nvs ``` Erwartetes Verhalten: Schreibt Testwerte in NVS, liest sie zurück, prüft Übereinstimmung. Nach Neustart müssen die Werte erhalten bleiben. Alle Tests als `PASS` im Serial Monitor. ### Test 3.3 – WiFiManager ```bash pio run -e test-wifi --target upload pio device monitor -e test-wifi ``` Erwartetes Verhalten: Beim ersten Flash öffnet der ESP32 den AP `LaserCutter-Setup`. Nach Eingabe der WLAN-Credentials verbindet er sich und gibt die IP-Adresse aus. BOOT-Taste (GPIO 0) beim Start 3 s halten löscht gespeicherte Credentials. ### Test 4.3 – DisplayManager ```bash pio run -e test-display-mgr --target upload pio device monitor -e test-display-mgr ``` Erwartetes Verhalten: Durchläuft automatisch alle `show*`-Methoden: - `showWifiError(true/false)` → Modul 0 - `showLaserTime(n)` → Module 1–3 (ganze Minuten: 0, 1, 42, 999) - `showMqttError(true/false)` → Modul 4 - `showCountdown(n)` → Module 5–7 - `showIdle()` → Module 5–7 (`--`) ### Test 5.6 – LaserTracker ```bash pio run -e test-laser-tracker --target upload pio device monitor -e test-laser-tracker ``` **GPIO 4 Schalter** betätigen = Laser-AN simulieren. Erwartetes Verhalten: | Aktion | Module 1–3 | Module 5–7 | Serial | |---|---|---|---| | Idle | `0` | `--` | – | | Laser an (0–20 s) | `0` | Countdown `20`→`1` | `BurstStart -> GRATIS` | | Laser an (>20 s) | `0` | `--` | `GRATIS abgelaufen -> NET_COUNTING` | | Laser aus (nach 80 s netto) | `1` | `--` | `BurstEnd: gesamt=... netto=...` | | Nach 60 weiteren Netto-Sek. | `2` | `--` | – | **BOOT-Taste (GPIO 0) 3 s halten** → `settings.reset()` + `resetTotal()` → alle NVS-Werte auf Default, Session = 0. ### Test 6.5 – MqttClient (Phase 6) > ⚠️ Voraussetzung: `test_sketches/mqtt_test_secrets.h` anlegen (Vorlage: `mqtt_test_secrets.h.example`) ```bash pio run -e test-mqtt --target upload pio device monitor -e test-mqtt ``` **Erwartetes Verhalten:** | Aktion | Serial / MQTT Explorer | |---|---| | Start | Verbindung zu Broker, `lasercutter/status` retained `{"online":true,...}` | | Alle 30 s | Heartbeat auf `lasercutter/status` mit `uptime_s` | | Laser AN/AUS (>20 s) | Session-Publish auf `lasercutter/session` | | `{"reset":true}` senden an `lasercutter/reset` | `resetTotal()` → Session = 0 | | ESP32 stromlos | LWT `{"online":false}` erscheint nach Keep-Alive-Timeout | | Modul 4 | `E` blinkt bei MQTT-Verbindungsverlust | **BOOT-Taste (GPIO 0) 3 s halten** → `resetTotal()` → Gesamtzeit auf 0. **Status:** ✅ Verbindung, Heartbeat, Session-Publish, JSON-Reset, LWT getestet ⚠️ **Ungetestet:** MQTT-Error Modul 5 bei Verbindungsverlust, Reconnect nach WiFi-Ausfall ### Test 7.7 – WebServer (Phase 7) ```bash pio run -e test-web --target upload pio device monitor -e test-web ``` IP-Adresse aus Serial Monitor ablesen, dann im Browser: | URL | Erwartetes Verhalten | |---|---| | `http:///` | Statusseite: Session, Gesamtzeit (NVS), WLAN-IP, MQTT-Status, Broker | | `http:///config` | Formular: Broker, Port, User, PW, Gratiszeit, Polarität | | POST `/config` | Werte in NVS speichern, nach Neustart prüfen | | Button „Session zurücksetzen“ | `resetSession()` → Session = 0 min, Gesamtzeit (NVS) bleibt erhalten | | `http:///update` | ElegantOTA-Seite erreichbar | Statusseite aktualisiert sich automatisch alle 10 s. **Status:** ⏳ Hardware-Test ausstehend --- ## 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) | ✅ implementiert / ⚠️ 2 Punkte ungetestet | | 7 | WebServer (ESPAsyncWebServer, Config-UI, OTA) | ✅ implementiert / ⏳ Hardware-Test ausstehend | | 8 | Integrationstest (Display + MQTT + Web) | ⏳ ausstehend | | 9 | Gehäuse / Hardware-Finish | ⏳ ausstehend | | 10 | Produktiv-Deployment, Dokumentation final | ⏳ ausstehend | --- ## 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.