1. Vorschlag Implementierung

This commit is contained in:
MaPaLo76 2026-02-22 11:13:16 +01:00
parent 5a3a4e40bd
commit d84dfd2119
3 changed files with 537 additions and 1 deletions

229
Implementation-Plan.md Normal file
View File

@ -0,0 +1,229 @@
# Implementation Plan Laser Cutter MQTT Display
> Dieses Dokument beschreibt die Entwicklungsreihenfolge des Projekts.
> Tasks werden während der Entwicklung mit `[x]` abgehakt.
---
## Phase 1 Projekt-Setup & Konfiguration
- [ ] **1.1** `platformio.ini` mit allen benötigten Bibliotheken erweitern
- `MD_Parola`, `MD_MAX72XX`
- `PubSubClient`
- `WiFiManager` (tzapu/WiFiManager)
- `ESPAsyncWebServer` + `AsyncTCP`
- `ArduinoJson`
- `ElegantOTA`
- `Preferences` (ESP32 built-in, kein Extra-Eintrag nötig)
- [ ] **1.2** `include/config.h` erstellen zentrale Pin-Definitionen und Konstanten
- GPIO-Pins (MAX7219 SPI, Laser-Signal)
- Anzahl der Module (8), Zonen-Aufteilung
- Standard-Werte (Gratiszeit 20s, MQTT Port 1883, etc.)
- MQTT-Topic-Konstanten
- [ ] **1.3** Build prüfen (leeres Projekt kompiliert fehlerfrei)
---
## Phase 2 NVS Persistenz (`Settings`)
- [ ] **2.1** `include/settings.h` + `src/settings.cpp` erstellen
Speichert und lädt alle Konfigurationswerte über ESP32 `Preferences` (NVS):
- MQTT Broker IP, Port, User, Passwort
- Gratiszeit (0120 s)
- Signal-Polarität (`LOW_ACTIVE` / `HIGH_ACTIVE`)
- Akkumulierte Laserzeit (float, Minuten)
- [ ] **2.2** Unit-Test (Serial-Output): Werte schreiben, ESP32 neu starten, Werte lesen und verifizieren
---
## Phase 3 WiFi & WiFiManager
- [ ] **3.1** WiFiManager einbinden
- AP-Name: `LaserCutter-Setup`
- Captive-Portal-Seite für WLAN-Credentials
- Timeout: 120 s, danach Neustart
- [ ] **3.2** Verbindungsstatus-Callback implementieren (für spätere Fehleranzeige)
- [ ] **3.3** Test: Erstverbindung mit neuem AP, gespeichertes WLAN nach Neustart automatisch verbinden
---
## Phase 4 Dot-Matrix-Display (`DisplayManager`)
- [ ] **4.1** `include/display_manager.h` + `src/display_manager.cpp` erstellen
Wrapper um `MD_Parola` mit zwei Zonen:
- Zone 0 (Module 03, obere Reihe): Laserzeit in Minuten
- Zone 1 (Module 47, untere Reihe): Countdown / Statusmeldungen
- [ ] **4.2** Methoden implementieren:
- `showLaserTime(float minutes)` obere Zeile
- `showCountdown(int seconds)` untere Zeile (während Gratiszeit)
- `showIdle()` `---` in unterer Zeile wenn kein Countdown
- `showError(const char* msg)` Fehlermeldung in unterer Zeile, Modul 1
- `update()` in `loop()` aufrufen (MD_Parola benötigt regelmäßigen Aufruf)
- [ ] **4.3** Test: Statische Zahlen und Scrolltext auf beiden Zonen anzeigen
---
## Phase 5 Laser-Signaldetektion & Zeit-Tracking (`LaserTracker`)
- [ ] **5.1** `include/laser_tracker.h` + `src/laser_tracker.cpp` erstellen
- [ ] **5.2** GPIO-Eingang konfigurieren:
- `INPUT_PULLUP` auf GPIO 4
- Software-Debounce (min. 50 ms Entprellzeit)
- Polarität über `Settings` auslesbar
- [ ] **5.3** Session-Logik implementieren:
- `onLaserActive()` → Session-Start, Timer starten
- `onLaserInactive()` → Session-Ende, akkumulierte Zeit aktualisieren, NVS speichern
- [ ] **5.4** Gratiszeit-Logik:
- Countdown läuft ab Session-Start
- Zeit wird erst nach Ablauf der Gratiszeit zur Gesamtzeit addiert
- `getCountdownRemaining()` → Rückgabe verbleibende Sekunden (0 wenn abgelaufen)
- [ ] **5.5** Öffentliche Getter:
- `getTotalMinutes()` → float
- `getSessionSeconds()` → int (aktuelle Session ohne Gratiszeit)
- `isActive()` → bool
- `getLastSessionSeconds()` → int (für MQTT Publish)
- [ ] **5.6** Test: Laser-Signal simulieren (GPIO auf GND ziehen), Zeitmessung verifizieren
---
## Phase 6 MQTT Client (`MqttClient`)
- [ ] **6.1** `include/mqtt_client.h` + `src/mqtt_client.cpp` erstellen
Wrapper um `PubSubClient`
- [ ] **6.2** Verbindungsaufbau mit Credentials aus `Settings`
- [ ] **6.3** Publish-Methode `publishSession(sessionSec, totalMin, gratisSec)`:
- Topic: `lasercutter/session`
- JSON mit `session_s`, `total_min`, `gratiszeit_s`, `ts`
- Wird von `LaserTracker` beim Session-Ende aufgerufen
- [ ] **6.4** Publish-Methode `publishStatus(totalMin, ipStr, uptimeSec)`:
- Topic: `lasercutter/status`
- Heartbeat alle 60 Sekunden
- QoS 0
- [ ] **6.5** Subscribe auf `lasercutter/reset`:
- Payload `"1"` oder `{"action":"reset"}``LaserTracker::resetTotal()`
- QoS 1
- [ ] **6.6** Reconnect-Logik (Non-blocking, max. alle 10 Sekunden versuchen)
- [ ] **6.7** Test: Session-Daten mit MQTT Explorer empfangen, Reset auslösen und verifizieren
---
## Phase 7 Webinterface (`WebServer`)
- [ ] **7.1** `include/web_server.h` + `src/web_server.cpp` erstellen
Basierend auf `ESPAsyncWebServer`
- [ ] **7.2** Route `GET /` Statusseite (HTML):
- Aktuelle Laserzeit (Minuten)
- Letzter Session-Wert (Sekunden)
- WLAN-Status, MQTT-Status
- IP-Adresse
- [ ] **7.3** Route `GET /config` Konfigurationsformular (HTML):
- MQTT Broker IP, Port, User, Passwort
- Gratiszeit (Slider 0120 s)
- Signal-Polarität (Radio-Button)
- [ ] **7.4** Route `POST /config` Konfiguration speichern (via `Settings`, NVS)
- [ ] **7.5** Route `POST /reset` Laserzeit zurücksetzen (Button auf Statusseite)
- [ ] **7.6** ElegantOTA unter `/update` einbinden
- [ ] **7.7** Test: Alle Seiten im Browser aufrufen, Konfiguration ändern und nach Neustart prüfen
---
## Phase 8 Integration & Hauptprogramm (`main.cpp`)
- [ ] **8.1** `main.cpp` aufräumen und alle Module initialisieren:
- Reihenfolge: `Settings``DisplayManager``WiFiManager``MqttClient``WebServer``LaserTracker`
- [ ] **8.2** `loop()` implementieren:
- `displayManager.update()`
- `laserTracker.update()` (GPIO-Polling + Session-Logik)
- `mqttClient.loop()` (Reconnect + Heartbeat)
- `displayManager.showLaserTime(laserTracker.getTotalMinutes())`
- `displayManager.showCountdown(...)` oder `showIdle()` oder `showError(...)`
- [ ] **8.3** Callback von `LaserTracker``MqttClient.publishSession(...)` verdrahten
- [ ] **8.4** WLAN/MQTT-Fehlerzustand auf Display spiegeln
- [ ] **8.5** Watchdog Timer aktivieren (ESP32 WDT, 30 s)
---
## Phase 9 OTA & Stabilisierung
- [ ] **9.1** ElegantOTA in `main.cpp` initialisieren und in `loop()` einbinden
- [ ] **9.2** Serielles Logging vereinheitlichen (Log-Level: INFO / DEBUG per `#define`)
- [ ] **9.3** Langzeit-Test: 24h Betrieb, Speicherleck-Check via Serial Monitor (`ESP.getFreeHeap()`)
- [ ] **9.4** Edge-Cases testen:
- Laser aktiv, WLAN wird getrennt → Display läuft weiter
- MQTT-Broker nicht erreichbar → Reconnect
- Reset während aktiver Session
- Neustart nach akkumulierter Zeit → Zeit korrekt aus NVS geladen
---
## Phase 10 Dokumentation & Abschluss
- [ ] **10.1** README.md finalisieren (Screenshots, Schaltplan-Skizze ggf. ergänzen)
- [ ] **10.2** `platformio.ini` mit finalen Library-Versionen dokumentieren
- [ ] **10.3** MQTT-Topics in README.md validieren (mit realem Broker testen)
- [ ] **10.4** Optionaler Schaltplan (Fritzing / ASCII) für Laser-Signal-Anschluss (Optokoppler)
---
## Abhängigkeitsdiagramm (Reihenfolge)
```
Phase 1 (Setup)
└── Phase 2 (NVS/Settings)
├── Phase 3 (WiFi)
├── Phase 4 (Display)
└── Phase 5 (LaserTracker)
└── Phase 6 (MQTT)
└── Phase 7 (Webinterface)
└── Phase 8 (Integration)
└── Phase 9 (OTA/Stabilisierung)
└── Phase 10 (Doku)
```
---
## Verwendete MQTT-Topics (Überblick)
| Topic | Richtung | QoS | Auslöser |
|------------------------|-----------|-----|-------------------------------|
| `lasercutter/session` | Publish | 1 | Session-Ende (Laser inaktiv) |
| `lasercutter/status` | Publish | 0 | Heartbeat alle 60 s |
| `lasercutter/reset` | Subscribe | 1 | Externer Reset-Befehl |
---
*Erstellt: 22. Februar 2026*

228
README.md
View File

@ -1,4 +1,230 @@
# Laser Cutter Dot Matrix Display und MQTT Client
Dieses Projekt ist ein Dotmatrix-Display und MQTT-Client für einen Laser Cutter. Es ist darauf ausgelegt, die aktive Lasercutter Zeit auf einem Dotmatrix-Display anzuzeigen und Nachrichten über die aktive Laserzeit an einen MQTT-Broker zu senden und Nachrichten zum Zurücksetzen der Lasercutter-Zeit zu empfangen.
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)
---
## Hardware
| Komponente | Modell / Beschreibung |
|-------------------------|----------------------------------------------------|
| Mikrocontroller | AZ-Delivery ESP32 DevKit V4 |
| Dot-Matrix-Display | 8× GYMAX7219 Module (kompatibel zu MAX7219), 8×8 LEDs je Modul |
| Display-Anordnung | 4 Module nebeneinander × 2 Reihen = 32×16 LEDs |
| Display-Kaskadierung | Alle 8 Module in einer einzigen SPI-Kette |
| Laser-Aktivsignal | Potentialfreier Ausgang des Laser Cutters (Optokoppler empfohlen) |
| Optional | Shelly PM Mini G3 als externer Leistungsmesser (separater MQTT-Service) |
---
## Pinbelegung
| Signal | ESP32 GPIO | Beschreibung |
|---------------|-----------|------------------------------------------------|
| MAX7219 MOSI | GPIO 23 | SPI Data (VSPI) |
| MAX7219 CLK | GPIO 18 | SPI Clock (VSPI) |
| MAX7219 CS | GPIO 5 | SPI Chip Select |
| Laser-Signal | GPIO 4 | Potentialfreier Eingang (INPUT_PULLUP, konfigurierbar invertierbar) |
> Die Polarität des Laser-Signals (HIGH = aktiv oder LOW = aktiv) ist über das Webinterface konfigurierbar.
---
## 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:
| Zone | Module (Kette) | Anzeige-Inhalt |
|------|----------------|----------------------------------------|
| Oben | Module 03 | Akkumulierte aktive Laserzeit in **Minuten** (z.B. `42.5 min`) |
| Unten | Module 47 | Laufender **Countdown** in Sekunden der Gratiszeit, danach `---` |
- Die Anzeige aktualisiert sich sekündlich, solange der Laser aktiv ist.
- Bei inaktivem Laser bleibt die letzte gemessene Zeit dauerhaft sichtbar.
- Fehlerzustände (WLAN, MQTT) werden in der **unteren Zeile, erstes Modul** angezeigt.
- Die Bibliotheken `MD_Parola` + `MD_MAX72XX` werden für die Ansteuerung verwendet.
---
## 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
- Die Laserzeit wird **akkumulativ** in Minuten gezählt und über Neustarts hinaus im **NVS (Non-Volatile Storage)** des ESP32 gespeichert.
- **Gratiszeit**: Eine konfigurierbare Zeitspanne (0120 Sekunden) am Anfang jeder Session wird **nicht** zur akkumulierten Zeit gezählt. Dies erlaubt kurze Testläufe zum Einstellen des Laser Cutters ohne Kosten.
- Standard: 20 Sekunden
- Einstellbar über das Webinterface
- Die untere Display-Zeile zeigt während der Gratiszeit einen Countdown an.
---
## 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 einer Session** gesendet |
| Subscribe | `lasercutter/reset` | `"1"` | Setzt die akkumulierte Laserzeit auf 0 |
| Publish | `lasercutter/status` | JSON | Heartbeat alle 60 Sekunden (online/offline)|
### JSON-Format `lasercutter/session`
```json
{
"session_s": 125,
"total_min": 42.5,
"gratiszeit_s": 20,
"ts": "2026-02-22T10:30:00Z"
}
```
### JSON-Format `lasercutter/status`
```json
{
"online": true,
"total_min": 42.5,
"ip": "192.168.1.100",
"uptime_s": 3600
}
```
### Verhalten
- Reconnect im Hintergrund, falls MQTT-Broker nicht erreichbar.
- Bei aktivem Laser läuft die Zeitmessung unabhängig vom MQTT-Status weiter.
- QoS 1 für Session- und Reset-Topics.
---
## 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 IP | `192.168.1.1` | IP-Adresse des MQTT-Brokers |
| MQTT Port | `1883` | Port des MQTT-Brokers |
| 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) |
---
## 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 | Display-Anzeige (untere Zeile, Modul 1) | Verhalten |
|----------------------|----------------------------------------|----------------------------------------|
| WLAN getrennt | `WiFi ERR` | Zeiterfassung läuft weiter, Reconnect |
| MQTT nicht erreichbar| `MQTT ERR` | Zeiterfassung läuft weiter, Reconnect |
| WLAN + MQTT OK | Normaler Betrieb (Countdown/`---`) | |
---
## Bibliotheken
| Bibliothek | Zweck |
|-----------------------|--------------------------------------|
| `MD_Parola` | Dot-Matrix-Display Textausgabe |
| `MD_MAX72XX` | Treiber für MAX7219/GYMAX7219 |
| `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
# PlatformIO CLI
pio run --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
├── include/
│ ├── config.h # Pin-Definitionen, Konstanten
│ ├── display_manager.h # Display-Logik (MD_Parola)
│ ├── laser_tracker.h # Signal-Detektion & Zeiterfassung
│ ├── mqtt_client.h # MQTT-Wrapper (PubSubClient)
│ ├── web_server.h # Webinterface (ESPAsyncWebServer)
│ └── settings.h # NVS-Persistenz (Preferences)
├── parser/
│ └── shelly_parser.py # Separater Python-Parser für Shelly PM G3
├── platformio.ini
└── README.md
```

81
parser/shelly_parser.py Normal file
View File

@ -0,0 +1,81 @@
"""Shelly PM Mini G3 Parser - extracts power data from MQTT messages."""
from datetime import datetime
import structlog
from exceptions import ParserError
logger = structlog.get_logger()
class ShellyParser:
"""Parser for Shelly PM Mini G3 MQTT Messages."""
def parse_message(self, topic: str, payload: dict) -> dict | None:
"""
Parse Shelly MQTT message.
Args:
topic: MQTT topic
payload: Message payload (already JSON parsed)
Returns:
Dict with parsed data or None
"""
try:
# Only parse status messages
if "/status/pm1:0" in topic:
return self._parse_status_message(topic, payload)
return None
except Exception as e:
logger.debug("shelly_parse_error", topic=topic, error=str(e))
return None
def _parse_status_message(self, topic: str, data: dict) -> dict | None:
"""
Parse Shelly PM status message.
Topic: shaperorigin/status/pm1:0
Payload format:
{
"id": 0,
"voltage": 230.0,
"current": 0.217,
"apower": 50.0,
"freq": 50.0,
"aenergy": {"total": 12345.6},
"temperature": {"tC": 35.2}
}
"""
try:
device_id = self._extract_device_id(topic)
result = {
"message_type": "status",
"device_id": device_id,
"timestamp": datetime.utcnow().isoformat() + "Z",
"voltage": data.get("voltage"),
"current": data.get("current"),
"apower": data.get("apower", 0), # Active Power in Watts
"frequency": data.get("freq"),
"total_energy": data.get("aenergy", {}).get("total"),
"temperature": data.get("temperature", {}).get("tC"),
}
logger.debug("shelly_parsed_status", device_id=device_id, apower=result["apower"])
return result
except Exception as e:
logger.error("shelly_status_parse_error", error=str(e))
return None
def _extract_device_id(self, topic: str) -> str:
"""Extract device ID from topic path."""
# Example: shaperorigin/status/pm1:0 -> shaperorigin
parts = topic.split("/")
if len(parts) > 0:
return parts[0]
return "unknown"