| .github | ||
| .vscode | ||
| doc | ||
| include | ||
| lib | ||
| parser | ||
| src | ||
| test | ||
| test_sketches | ||
| .gitignore | ||
| Implementation-Plan.md | ||
| platformio.ini | ||
| README.md | ||
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
- Hardware
- Pinbelegung
- Display
- Laser-Signaldetektion
- Zeit-Tracking & Gratiszeit
- MQTT Client
- Webinterface
- Konfiguration & Persistenz
- WiFi-Setup (WiFiManager)
- OTA Firmware-Update
- Fehlerverhalten
- Bibliotheken
- Build & Flash
- Tests
- Beitragen / Commits
Skizze und Screenshots
Das Bild zeigt die physische Anordnung der 8 GYMAX7219-Module im 4×2-Format mit LED-Positionen und Beschriftung der Anzeigebereiche.
LaserCutter Display
[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
OTA Firmware-Update Seite
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_ACTIVEoderHIGH_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
gratisSecondsaktiv 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
LOWoderHIGHden 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 / resetSession() |
MQTT Session in Minuten, Web-UI |
| Summe Session | RAM | bei Neustart / resetSession() |
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 resetSession() 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 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
{
"session_minutes": 2,
"session_seconds": 95,
"session_start_time": "2026-02-23T12:34:56Z",
"freetime_s": 20,
"ip": "192.168.2.62"
}
session_start_timeist 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
{
"online": true,
"session_sum": "42.50",
"machine_running_time_min": "1234.75",
"ip": "192.168.1.100",
"uptime_s": 3600
}
LWT (Last Will and Testament)
Bei Verbindungsabbruch sendet der Broker automatisch auf lasercutter/status:
{"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, Web-Passwort |
| Reset | /reset |
Setzt akkumulierte Laserzeit zurück |
| OTA Update | /update |
Firmware-Update über Browser |
Alle Seiten sind per HTTP Basic Auth geschützt, wenn in der Konfiguration 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).
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
# 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 # 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) | ✅ 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 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.


