docs: add design specification for ESP32 E-Paper display system

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christian Mueller 2026-04-06 10:35:01 +02:00
parent 8d5e4106d6
commit 7ffad8364b

View File

@ -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 <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