From 7ffad8364b1f23989b59930c28973de9f6f34fbb Mon Sep 17 00:00:00 2001 From: Christian Mueller Date: Mon, 6 Apr 2026 10:35:01 +0200 Subject: [PATCH] docs: add design specification for ESP32 E-Paper display system Co-Authored-By: Claude Opus 4.6 (1M context) --- ...2026-04-06-epaper-display-system-design.md | 437 ++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 display/docs/superpowers/specs/2026-04-06-epaper-display-system-design.md diff --git a/display/docs/superpowers/specs/2026-04-06-epaper-display-system-design.md b/display/docs/superpowers/specs/2026-04-06-epaper-display-system-design.md new file mode 100644 index 0000000..85e3eed --- /dev/null +++ b/display/docs/superpowers/specs/2026-04-06-epaper-display-system-design.md @@ -0,0 +1,437 @@ +# 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