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 <token> 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:
{
"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:
- ESP32 startet als Access Point ("EPaper-Setup")
- Captive Portal mit Konfigurationsseite
- Eingabefelder: WiFi SSID, Passwort, API-URL, Display-ID, optionaler Token
- Gespeichert im NVS (Non-Volatile Storage)
- 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