MQTT-Display-LaserCutter/include/display_manager.h
MaPaLo76 9e3e06eb09 feat(state-machine): add grace time state after free time
Implements FR-020: Grace Time (buffer period after free time)

Problem: Fan relay signal continues ~5s after laser stops, causing
false billable time charges when user stops at countdown 1 second.

Solution: New GRACE state between GRATIS and NET_COUNTING with
configurable grace period (default 5s, range 0-30s). During grace
time, display shows blinking 0 (number of blinks = remaining seconds).
If laser stops during GRACE, no billable session is created.

State machine: INACTIVE -> GRATIS -> GRACE -> NET_COUNTING

Changes:
- config.h: Add DEFAULT_GRACE_TIME_S (5), MIN/MAX constants
- settings.h/cpp: Add graceTimeSeconds field, NVS persistence
- laser_tracker.h/cpp: Add GRACE state, transition logic
- display_manager.h/cpp: Add showGraceBlinking() with blink animation
- main.cpp: Add GRACE state handling in display update loop
- mqtt_client.cpp: Add grace_time_s field to session JSON
- web_server.cpp: Add Grace Time input field in config page
- README.md: Update documentation with GRACE state details
- Feature-Requests.md: Mark FR-020 as completed (v1.6.4)

Version: 1.6.4
Tested: Manual testing with logs and MQTT Explorer
2026-05-15 15:08:15 +02:00

163 lines
6.2 KiB
C++
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.

#pragma once
// =============================================================================
// display_manager.h Dot-Matrix-Display Ausgabe (8× GYMAX7219, 4×2 Layout)
// Projekt: MQTT-Display LaserCutter
//
// Implementierung mit rohem MD_MAX72XX (kein MD_Parola), damit die physische
// 90°-CW Verdrehung der Module per Software (rotateCCW) zuverlässig kompensiert
// werden kann ohne dass MD_Parola Zonenlogik interferiert.
//
// Modul-Aufteilung (oben links = Index 0):
// Modul 0 : WiFi-Fehler-Indikator (showWifiError)
// Modul 13 (oben) : Laserzeit in ganzen Minuten, 3 Stellen rechtsbündig
// Modul 4 : MQTT-Fehler-Indikator (showMqttError)
// Modul 57 (unten) : Countdown-Sekunden, 3 Stellen rechtsbündig / showIdle
//
// Verwendung:
// display.begin(); // einmalig in setup()
// display.showLaserTime(42.5f); // Modul 1-3
// display.showCountdown(18); // Modul 5-7
// display.showIdle(); // Modul 5-7
// display.showWifiError(true); // Modul 0
// display.showMqttError(true); // Modul 4
// display.update(); // in loop() aufrufen (Pflicht)
// =============================================================================
#include <Arduino.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include "config.h"
// ---------------------------------------------------------------------------
// Maximale Textlänge je Zone (= Anzahl Module pro Zone)
// ---------------------------------------------------------------------------
#define DISP_CHARS_PER_ZONE DISPLAY_MODULES_PER_ZONE // 4
class DisplayManager {
public:
DisplayManager();
// Initialisierung (einmalig in setup())
void begin();
// Helligkeit setzen 015
void setBrightness(uint8_t level);
// ---- Anzeige-Methoden ---------------------------------------------------
// Modul 13 (oben): Laserzeit in ganzen Minuten, 3 Stellen rechtsbündig
// 09 → " x" z.B. " 7"
// 1099 → " xx" z.B. " 42"
// 100999 → "xxx" z.B. "123"
// ≥ 1000 → "!!!" (Überlauf, wird laut Anforderung nie erreicht)
// Modul 0 bleibt unberührt.
void showLaserTime(float minutes);
// Modul 57 (unten): Countdown-Sekunden, 3 Stellen rechtsbündig
// 09 → " x"
// 1099 → " xx"
// 100999 → "xxx"
// ≥ 1000 → "!!!"
// Modul 4 bleibt unberührt.
void showCountdown(int seconds);
// Modul 57 (unten): Leerlauf-Anzeige (" --")
// Modul 4 bleibt unberührt.
void showIdle();
// Modul 57 (unten): Grace Time Anzeige (blinkende '0')
// graceRemainingSec = 130: Anzahl Blinks entspricht den verbleibenden Sekunden
// update() muss in loop() aufgerufen werden, damit das Blinken funktioniert
// Modul 4 bleibt unberührt.
void showGraceBlinking(int graceRemainingSec);
// Modul 57 (unten): Sekunden-Kreisanzeige (wie analoge Uhr)
// seconds = 060: Rand der 3 Module wird sekündlich befüllt
// 60 LEDs = Perimeter des 24×8-Feldes (24 oben + 6 rechts + 24 unten + 6 links)
// Richtung: Uhrzeigersinn, Start oben-links
// seconds = 0 → alle LEDs aus (= Idle)
// seconds ≥ 60 → alle 60 Rand-LEDs an
// Modul 4 bleibt unberührt.
void showSessionRing(int seconds);
// Modul 57 (unten): Statustext (max. 3 Zeichen, wird abgeschnitten / aufgefüllt)
// Modul 4 bleibt unberührt.
void showStatus(const char* msg);
// Modul 0 (oben links): WiFi-Fehler-Indikator
// error=true → 'W' Symbol
// error=false → leer
void showWifiError(bool error);
// Modul 4 (unten links): MQTT-Fehler-Indikator
// error=true → 'M' Symbol
// error=false → leer
void showMqttError(bool error);
// Beide Zonen löschen
void clear();
// MAX7219 neu initialisieren (einmaliges initMx, kein Delay)
// Aufrufen nach Relais-Umschaltung um durch EMV-Störungen korrumpierte
// MAX7219-Register (Decode-Mode, Scan-Limit) zurückzusetzen
void reinit();
// Muss in loop() aufgerufen werden (für Blink-Animation bei Grace Time)
void update();
// Alle LEDs ein / aus (Testfunktion)
void allLedsOn();
void allLedsOff();
// Display ein-/ausschalten (SHUTDOWN-Modus des MAX7219)
// setEnabled(false) → alle LEDs aus (MAX7219 Shutdown)
// setEnabled(true) → normaler Betrieb
void setEnabled(bool enabled);
bool isEnabled() const { return _enabled; }
// Debug-Ausgabe auf Serial
void printToSerial() const;
private:
MD_MAX72XX _mx;
bool _enabled = true;
// ---- Letzter angezeigter Zustand (für redraw nach reinit) ----------------
enum class BottomMode : uint8_t { IDLE, COUNTDOWN, RING, STATUS, GRACE_BLINK };
float _lastLaserMinutes = 0.0f;
BottomMode _lastBottomMode = BottomMode::IDLE;
int _lastBottomValue = 0;
char _lastStatusMsg[4] = {' ', ' ', ' ', '\0'};
bool _lastWifiError = false;
bool _lastMqttError = false;
// ---- Grace Time Blink-Animation -----------------------------------------
int _graceBlinkTarget = 0; // Ziel-Anzahl Blinks
int _graceBlinkCount = 0; // Aktuelle Blink-Anzahl
uint32_t _graceBlinkLastMs = 0; // millis() des letzten Blink-Wechsels
bool _graceBlinking = false; // true = gerade im Blink-Modus
// Zeichnet den zuletzt gespeicherten Zustand neu auf die Hardware
void redraw();
// Schreibt genau DISP_CHARS_PER_ZONE Zeichen auf eine Zone
// zone 0 → Module 0..3 (oben)
// zone 1 → Module 4..7 (unten)
// str muss exakt DISP_CHARS_PER_ZONE Zeichen enthalten (kein Null-Terminator nötig)
void writeZone(uint8_t zone, const char* str);
// Schreibt ein Zeichen auf ein einzelnes Modul
void writeChar(uint8_t moduleIdx, char c);
// 90° CCW Rotation kompensiert physische 90° CW Verdrehung der Module
static void rotateCCW(const uint8_t src[8], uint8_t dst[8]);
// Gibt Zeiger auf 8-Byte-Bitmap für ASCII-Zeichen zurück
// Unterstützt: '0''9', ' ', '-', '.', '!', 'E', 'r', 'o', 'n', 'A', 'P'
static const uint8_t* charBitmap(char c);
};
// Globale Instanz
extern DisplayManager display;