epaper-display/display/docs/superpowers/specs/2026-04-06-epaper-display-system-design.md
Christian Mueller 7ffad8364b docs: add design specification for ESP32 E-Paper display system
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:35:01 +02:00

438 lines
16 KiB
Markdown
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.

# 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:
```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