MQTT-Display-LaserCutter/README.md

439 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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. [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
![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 |
| 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 / `resetSession()` | MQTT Session in Minuten, Web-UI |
| **Summe Session** | RAM | bei Neustart / `resetSession()` | 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 `resetSession()` 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** 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_minutes": 2,
"session_seconds": 95,
"session_start_time": "2026-02-23T12:34:56Z",
"freetime_s": 20,
"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",
"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`:
```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, 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, 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).
---
## 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 # 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](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.