# ESP32 E-Paper Anzeigesystem — Design-Spezifikation ## 1. Übersicht Modulares System zur Anzeige von Daten aus MQTT-Brokern und PostgreSQL-Datenbanken auf einem ESP32-basierten E-Paper-Display (Waveshare 7.5" V2, 800×480, schwarz/weiß). **Komponenten:** - **Middleware** (Node.js/Express/TypeScript) — Datensammlung, Verarbeitung, API - **Webinterface** (React/TypeScript) — Konfiguration und Verwaltung - **ESP32 Firmware** (PlatformIO/Arduino) — Display-Rendering **Architektur:** Modularer Monolith — ein Express-Server mit klar getrennten internen Modulen. React SPA wird als Static Files ausgeliefert. **Datenbank:** SQLite (Config, Datenpunkte, Layouts) **Deployment:** Proxmox LXC-Container, Deploy via Git Clone ## 2. Systemarchitektur ``` Datenquellen (MQTT, PostgreSQL) │ ▼ Middleware (Node.js/Express/TS) ┌─────────────────────────────┐ │ MQTT Module │ │ PostgreSQL Module │ │ Datapoint Manager │ │ Display Module │ │ REST API (JWT Auth) │ │ SQLite DB │ └──────────┬──────────────────┘ │ ├──► Webinterface (React SPA) │ └──► ESP32 (E-Paper) ``` ### 2.1 Module | Modul | Verantwortung | |-------|---------------| | **MQTT Module** | Broker-Verbindungen, Topic Discovery (Wildcard + Prefix), Payload-Parsing (JSON-Pfade), Werte an Datapoint Manager weiterleiten | | **PostgreSQL Module** | DB-Connections verwalten, konfigurierte Queries periodisch ausführen, Ergebnisse auf Datenpunkte mappen | | **Datapoint Manager** | Zentrale Registry aller Datenpunkte mit Name, Einheit, Wert, Timestamp, Status. Transformationen (Runden, Skalieren, Mapping). SQLite-Persistenz | | **Display Module** | Layout-Templates, Zuordnung Datenpunkte → Layout-Elemente, JSON-Builder für ESP32 API. Unterstützt freie Positionierung und Grid-Modus | | **REST API** | Express-Router mit JWT-Auth, CRUD für alle Entitäten, Display-Endpoint für ESP32 | ## 3. Datenmodell (SQLite) ### 3.1 mqtt_brokers | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | name | TEXT NOT NULL | Anzeigename | | host | TEXT NOT NULL | Broker-Hostname | | port | INTEGER DEFAULT 1883 | Port | | username | TEXT NULL | Auth | | password | TEXT NULL | Auth (verschlüsselt gespeichert) | | use_tls | BOOLEAN DEFAULT false | TLS aktivieren | | enabled | BOOLEAN DEFAULT true | Broker aktiv | | status | TEXT DEFAULT 'disconnected' | Verbindungsstatus | | created_at | DATETIME | Erstellzeitpunkt | | updated_at | DATETIME | Letzte Änderung | ### 3.2 mqtt_topics | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | broker_id | INTEGER FK | Referenz auf mqtt_brokers | | topic | TEXT NOT NULL | Topic-Pfad | | json_path | TEXT NULL | JSON-Pfad für Wert-Extraktion (z.B. `$.value`) | | selected | BOOLEAN DEFAULT false | Vom User ausgewählt | | ignored | BOOLEAN DEFAULT false | Vom User ignoriert | | last_payload | TEXT NULL | Letzte empfangene Payload | | discovered_at | DATETIME | Entdeckungszeitpunkt | ### 3.3 pg_sources | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | name | TEXT NOT NULL | Anzeigename | | host | TEXT NOT NULL | DB-Host | | port | INTEGER DEFAULT 5432 | Port | | database | TEXT NOT NULL | Datenbankname | | username | TEXT NOT NULL | DB-User | | password | TEXT NOT NULL | DB-Passwort (verschlüsselt) | | use_tls | BOOLEAN DEFAULT false | TLS | | enabled | BOOLEAN DEFAULT true | Aktiv | | status | TEXT DEFAULT 'disconnected' | Verbindungsstatus | | created_at | DATETIME | Erstellzeitpunkt | ### 3.4 pg_queries | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | source_id | INTEGER FK | Referenz auf pg_sources | | name | TEXT NOT NULL | Anzeigename | | query | TEXT NOT NULL | SQL-Abfrage (read-only) | | interval_sec | INTEGER DEFAULT 60 | Abfrage-Intervall | | result_column | TEXT NOT NULL | Spaltenname für den Wert | | last_result | TEXT NULL | Letztes Ergebnis | | last_run_at | DATETIME NULL | Letzte Ausführung | ### 3.5 datapoints | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | name | TEXT NOT NULL | Technischer Name | | label | TEXT NOT NULL | Anzeigename | | unit | TEXT NULL | Einheit (°C, %, ppm, ...) | | value | TEXT NULL | Letzter Wert | | status | TEXT DEFAULT 'unknown' | ok / error / stale | | source_type | TEXT NOT NULL | 'mqtt' oder 'postgresql' | | source_id | INTEGER NOT NULL | FK auf Broker oder PG-Source | | topic_id | INTEGER NULL | FK auf mqtt_topics (nur bei MQTT) | | query_id | INTEGER NULL | FK auf pg_queries (nur bei PG) | | transform | TEXT NULL | JSON: `{"round": 1, "scale": 1.0, "map": {}}` | | enabled | BOOLEAN DEFAULT true | Aktiv | | updated_at | DATETIME | Letztes Update | ### 3.6 displays | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | name | TEXT NOT NULL | Anzeigename | | description | TEXT NULL | Beschreibung | | layout_id | INTEGER FK | Referenz auf layouts | | api_token | TEXT NULL | Optionaler API-Token für ESP32 | | last_seen | DATETIME NULL | Letzter ESP32-Abruf | | refresh_sec | INTEGER DEFAULT 300 | Update-Intervall (5 Min) | | created_at | DATETIME | Erstellzeitpunkt | ### 3.7 layouts | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | name | TEXT NOT NULL | Layout-Name | | type | TEXT DEFAULT 'free' | 'free' (freie Positionierung) oder 'grid' | | width | INTEGER DEFAULT 800 | Display-Breite | | height | INTEGER DEFAULT 480 | Display-Höhe | | grid_cols | INTEGER NULL | Spalten bei Grid-Modus | | grid_rows | INTEGER NULL | Zeilen bei Grid-Modus | | created_at | DATETIME | Erstellzeitpunkt | ### 3.8 layout_elements | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | layout_id | INTEGER FK | Referenz auf layouts | | datapoint_id | INTEGER FK NULL | Zugewiesener Datenpunkt | | type | TEXT NOT NULL | 'value', 'label', 'line', 'rect' | | x | INTEGER NOT NULL | X-Position | | y | INTEGER NOT NULL | Y-Position | | width | INTEGER NULL | Breite (für Karten, Linien, Rects) | | height | INTEGER NULL | Höhe | | font_size | INTEGER DEFAULT 24 | Schriftgröße | | font_weight | TEXT DEFAULT 'normal' | 'normal' oder 'bold' | | text_align | TEXT DEFAULT 'left' | 'left', 'center', 'right' | | icon | TEXT NULL | Icon-Name (thermometer, droplet, ...) | | style | TEXT DEFAULT 'plain' | 'plain' (nur Text) oder 'card' (mit Rahmen) | | static_text | TEXT NULL | Fester Text für Labels/Titel | | grid_col | INTEGER NULL | Spalte im Grid-Modus | | grid_row | INTEGER NULL | Zeile im Grid-Modus | | sort_order | INTEGER DEFAULT 0 | Reihenfolge | ### 3.9 users | Spalte | Typ | Beschreibung | |--------|-----|--------------| | id | INTEGER PK | Auto-Increment | | username | TEXT UNIQUE NOT NULL | Benutzername | | password_hash | TEXT NOT NULL | bcrypt-Hash | | created_at | DATETIME | Erstellzeitpunkt | ## 4. REST API ### 4.1 Authentifizierung - Login: `POST /api/auth/login` → JWT Token - Alle `/api/*` Endpunkte (außer Display-Endpoint) erfordern JWT im `Authorization: Bearer ` Header - Display-Endpoint optional per Query-Param: `?token=xxx` ### 4.2 Endpunkte | Methode | Pfad | Beschreibung | |---------|------|--------------| | **Auth** | | | | POST | `/api/auth/login` | Login → JWT | | **MQTT Broker** | | | | GET | `/api/mqtt/brokers` | Alle Broker auflisten | | POST | `/api/mqtt/brokers` | Broker hinzufügen | | PUT | `/api/mqtt/brokers/:id` | Broker bearbeiten | | DELETE | `/api/mqtt/brokers/:id` | Broker löschen | | POST | `/api/mqtt/brokers/:id/test` | Verbindung testen | | **MQTT Topics** | | | | GET | `/api/mqtt/brokers/:id/topics` | Topics eines Brokers | | PUT | `/api/mqtt/topics/:id` | Topic bearbeiten | | POST | `/api/mqtt/brokers/:id/discover` | Discovery starten | | **PostgreSQL** | | | | GET/POST | `/api/pg/sources` | PG-Verbindungen | | PUT/DELETE | `/api/pg/sources/:id` | PG-Verbindung bearbeiten/löschen | | POST | `/api/pg/sources/:id/test` | Verbindung testen | | GET/POST | `/api/pg/sources/:id/queries` | Queries verwalten | | PUT/DELETE | `/api/pg/queries/:id` | Query bearbeiten/löschen | | **Datenpunkte** | | | | GET | `/api/datapoints` | Alle Datenpunkte | | PUT | `/api/datapoints/:id` | Datenpunkt bearbeiten | | **Displays** | | | | GET/POST | `/api/displays` | Displays verwalten | | PUT/DELETE | `/api/displays/:id` | Display bearbeiten/löschen | | **Layouts** | | | | GET/POST | `/api/layouts` | Layouts verwalten | | GET | `/api/layouts/:id` | Layout mit Elementen | | PUT/DELETE | `/api/layouts/:id` | Layout bearbeiten/löschen | | POST | `/api/layouts/:id/elements` | Element hinzufügen | | PUT/DELETE | `/api/layouts/elements/:id` | Element bearbeiten/löschen | | **ESP32 Display** | | | | **GET** | **`/api/display/:id`** | **Display-Daten für ESP32** | ### 4.3 ESP32 JSON-Format `GET /api/display/1` Response: ```json { "display": { "id": 1, "name": "Werkstatt", "width": 800, "height": 480, "refresh_sec": 300 }, "layout": { "type": "free" }, "elements": [ { "type": "label", "x": 400, "y": 20, "font_size": 28, "font_weight": "bold", "text_align": "center", "text": "Werkstatt Monitor" }, { "type": "value", "x": 20, "y": 80, "width": 380, "height": 90, "font_size": 32, "font_weight": "bold", "icon": "thermometer", "style": "card", "label": "Temperatur", "value": "23.5", "unit": "°C" }, { "type": "value", "x": 410, "y": 80, "width": 380, "height": 90, "font_size": 32, "font_weight": "bold", "icon": "droplet", "style": "card", "label": "Luftfeuchte", "value": "48", "unit": "%" }, { "type": "line", "x": 0, "y": 60, "width": 800, "height": 2 }, { "type": "label", "x": 400, "y": 465, "font_size": 12, "text_align": "center", "text": "Aktualisiert: 06.04.2026 10:15" } ] } ``` Für Grid-Layouts enthält `layout` zusätzlich `grid_cols` und `grid_rows`, und die Elemente werden automatisch positioniert. ## 5. Webinterface (React SPA) ### 5.1 Seiten | Seite | Funktionalität | |-------|---------------| | **Dashboard** | Status-Kacheln (Broker, Datenpunkte, Displays), Event-Log | | **MQTT Broker** | CRUD für Broker, Verbindungstest, Status | | **Topics** | Topic-Liste mit Checkbox, Filter/Suche, Payload-Vorschau, JSON-Pfad | | **PostgreSQL** | CRUD für Connections + Queries, Test-Ausführung | | **Datenpunkte** | Tabelle aller Punkte, Label/Einheit/Transform bearbeiten | | **Displays** | Display-Verwaltung, Layout-Zuweisung, API-Token, Refresh-Intervall | | **Layouts** | Layout-Editor: Typ wählen (free/grid), Elemente positionieren, Datenpunkte zuweisen, Icons wählen, Style (card/plain) | | **Vorschau** | E-Paper-Simulation (schwarz/weiß, 800×480 proportional) | ### 5.2 UI-Design - Sidebar-Navigation (fixiert, links) - Responsives Layout - Dunkle Sidebar (#1a1a2e), heller Content-Bereich - Status-Farben: Grün (verbunden/ok), Orange (warning), Rot (error) ### 5.3 Topic Discovery - Prefix-basiert als Default: User gibt Prefix ein (z.B. `sensor/`), System subscribed auf `sensor/#` - Optional: volle Discovery mit `#` (Wildcard) - Entdeckte Topics werden in Liste angezeigt mit Checkbox (auswählen/ignorieren) - Payload-Vorschau zeigt letzte Message + JSON-Pfad-Konfiguration ## 6. ESP32 Firmware ### 6.1 Hardware - **Board:** Waveshare e-Paper ESP32 Driver Board - **Display:** Waveshare 7.5" V2 (800×480, schwarz/weiß) - **Interface:** SPI ### 6.2 PlatformIO-Projekt **Libraries:** - `GxEPD2` — E-Paper Display-Treiber - `ArduinoJson` — JSON-Parsing - `Adafruit GFX` — Grafik-Primitives - `WiFi` (built-in) — WiFi-Stack - `HTTPClient` (built-in) — HTTP-Requests ### 6.3 Ablauf ``` 1. Boot / Wake from Deep Sleep 2. WiFi verbinden (3 Versuche, sonst Fehlermeldung auf Display) 3. HTTP GET /api/display/{id}?token=xxx 4. JSON parsen 5. Display rendern: - Elemente iterieren - Je nach type: Text, Linie, Rechteck, Icon zeichnen - Card-Style: Rahmen mit abgerundeten Ecken - Grid-Mode: automatische Positionierung 6. Deep Sleep für refresh_sec Sekunden ``` ### 6.4 Renderer Der Renderer iteriert über das `elements[]`-Array und zeichnet je nach `type`: - **label**: Text an x/y mit font_size, font_weight, text_align - **value**: Label + Wert + Einheit, optional mit Icon und Card-Rahmen - **line**: Horizontale/vertikale Linie - **rect**: Rechteck (gefüllt oder Outline) **Icons:** 12 vordefinierte Bitmaps (24×24 px) im Flash: thermometer, droplet, wind, pressure, sun, bolt, gauge, wifi, battery, check, warning, clock ### 6.5 Konfiguration (Captive Portal) Beim ersten Start oder wenn kein WiFi konfiguriert: 1. ESP32 startet als Access Point ("EPaper-Setup") 2. Captive Portal mit Konfigurationsseite 3. Eingabefelder: WiFi SSID, Passwort, API-URL, Display-ID, optionaler Token 4. Gespeichert im NVS (Non-Volatile Storage) 5. Reboot und Verbindung ### 6.6 Fehlerbehandlung | Situation | Verhalten | |-----------|-----------| | WiFi-Fehler | 3 Versuche, dann Fehlermeldung auf Display, Deep Sleep 60s | | API-Fehler | Letzte gültige Anzeige behalten, "Offline" im Footer, Deep Sleep 60s | | JSON-Fehler | Fehlermeldung auf Display, letzte Anzeige beibehalten | ## 7. Deployment - **Ziel:** Neuer Proxmox LXC-Container - **Provisionierung:** Per `/proxmox-lxc` Skill - **Deploy:** Git Clone auf LXC, `npm install`, `npm run build`, systemd Service - **Webinterface:** React Build als Static Files im Express-Server - **Port:** 4000 (Backend + Frontend) ## 8. Projektstruktur ``` display/ ├── middleware/ # Node.js Backend │ ├── src/ │ │ ├── modules/ │ │ │ ├── mqtt/ # MQTT Module │ │ │ ├── postgres/ # PostgreSQL Module │ │ │ ├── datapoints/ # Datapoint Manager │ │ │ └── display/ # Display/Layout Module │ │ ├── api/ │ │ │ ├── routes/ # Express Routes │ │ │ ├── middleware/ # Auth, Error Handler │ │ │ └── index.ts # API Setup │ │ ├── db/ │ │ │ ├── migrations/ # SQLite Migrations │ │ │ └── index.ts # DB Connection │ │ └── index.ts # Entry Point │ ├── package.json │ └── tsconfig.json ├── frontend/ # React SPA │ ├── src/ │ │ ├── pages/ # Dashboard, Broker, Topics, ... │ │ ├── components/ # Shared Components │ │ ├── api/ # API Client │ │ └── App.tsx │ ├── package.json │ └── tsconfig.json ├── esp32/ # PlatformIO Projekt │ ├── src/ │ │ └── main.cpp │ ├── include/ │ │ └── icons.h # Icon Bitmaps │ └── platformio.ini └── docs/ ``` ## 9. MVP-Scope Für den ersten Test: - 1 MQTT-Broker anbinden - 5 Datenpunkte konfigurieren - 1 Layout mit 5 Werten (gemischt: 2er-Reihen + 1er-Reihe) - 1 Display - ESP32 ruft Daten ab und zeigt sie an - Webinterface: Dashboard, Broker-Config, Topic-Auswahl, Datenpunkte, Layout-Editor, Vorschau