ki c2a44b80ea BUG-01+02: Resize via Pointer-Events + rAF (kein renderGrid-per-Move)
BUG-01: Resize-Handle griff nicht
  Items hatten `draggable=true` (HTML5-DnD). Der Resize-Handle war Kind des
  Items, also fired der Browser `dragstart` am Parent, sobald die Maus
  überhaupt auf irgendeinem Kind war. `e.preventDefault()` im
  `startResize()` mousedown-Handler kam zu spät — HTML5-DnD hatte den
  Move-Drag bereits übernommen.

BUG-02: Resize-Drag war zäh
  Pro mousemove rief der alte Code `renderGrid()` auf, das die komplette
  Cell- und Item-Struktur neu aufbaute. Bei 60 events/s × 100+ DOM-Ops
  pro Render entstand sichtbarer Jank.

Fix:
- HTML5-draggable komplett raus.
- Eigene Pointer-Events (`pointerdown`/`pointermove`/`pointerup`)
  für Move (startItemPointer) und Resize (startResizePointer).
- Resize-Handle: `pointer-events:auto` auf Handle,
  `pointer-events:none` auf `::before` (Dreieck-Deko), eigene
  Listener, `stopPropagation()` damit Item-Handler nicht mitfeuert.
- `touch-action:none` auf Handle und Item-Body → Touch-Devices
  scrollen nicht versehentlich beim Drag.
- Beide Aktionen nutzen requestAnimationFrame, und nur die
  CSS-Geometrie (grid-column/grid-row) wird aktualisiert — kein
  renderGrid() während Drag. Erst beim Drop / Resize-End kommt
  der volle re-render + save.
- Resize nutzt `setPointerCapture` damit der Move auch dann
  weiterläuft, wenn die Maus den Handle kurz verlässt.

Beweis: tests/test_pointer_events.js (Node-basiert, ohne Browser)
  1. Resize stopPropagation wird aufgerufen
  2. Item-Body pointerdown erreicht nur Item-Handler (nicht Resize)
  3. CSS pointer-events korrekt gesetzt
  4. renderGrid() in keiner onMove-Funktion (0/2)
  5. requestAnimationFrame wird genutzt (3 calls)
  6. draggable=true komplett entfernt (0 assignments)
  7. touch-action:none auf Resize-Handle
  ALLE 7 grün.

Manuelle Verifikation gegen laufenden Server (?demo=1):
  GET /?demo=1 → 200, 91685 bytes
  grep draggable=true: 0
  grep startItemPointer|startResizePointer: 6
  grep pointerdown: 3
  grep requestAnimationFrame: 3
  grep HTML5-drag handlers: 0 (nur 1 in Kommentar)
  grep renderGrid(): 12, davon 0 in onMove-Handlern

Closes #3 #4
2026-08-29 17:31:40 +04:00
2026-08-26 22:11:46 +04:00

epaper-dashboard

Plugin-based dashboard for the Waveshare 7.3" ACeP 7-Color e-Paper (F) HAT on Raspberry Pi 4.

Rendert alle 180s (oder konfigurierbar) ein 800×480px Vollbild auf dem Display.

Hardware

  • Display: Waveshare 7.3inch ACeP 7-Color e-Paper (F) HAT — 800×480, 7 Farben
  • Pi: Raspberry Pi 4 (oder Zero 2 W mit Einschränkungen)
  • Interface: SPI

Architektur

┌─────────────────────────────────────────────┐
│            epaper-dashboard                  │
├──────────────────┬──────────────────────────┤
│  dashboard.py    │   admin.py                │
│  (Renderer)      │   (Web Admin UI :8080)   │
├──────────────────┴──────────────────────────┤
│  layout.py · 4×4 Grid · Pack-Algorithmus     │
├────────────────────────────────────────────┤
│  plugins/  (eines pro Widget)               │
│  clock · weather · system · spotify         │
│  strava · gmail · minimax · hello          │
├────────────────────────────────────────────┤
│  network_watchdog.py                        │
│  (WiFi AP/Client Management)                 │
├────────────────────────────────────────────┤
│  waveshare_epd/  (Vendor-Treiber)           │
│  epd7in3f.py                                │
└─────────────────────────────────────────────┘

Quick Start

Pi vorbereiten

sudo apt update && sudo apt install -y python3-pil python3-numpy python3-flask git
sudo raspi-config  # → Interface Options → SPI → Enable
sudo reboot

Setup

# Repository klonen
git clone https://git.pkop.de/Vibecode/epaper-dashboard.git
cd epaper-dashboard

# Config erstellen
cp config.example.json config.json
# → config.json editieren (Plugins + Layout)

#waveshare_epd Treiber
git clone https://github.com/waveshareteam/Waveshare_GPIO.git waveshare_epd_lib
# oder: den waveshare_epd Ordner vom vorherigen Setup übernehmen

# Services installieren
chmod +x install_services.sh
./install_services.sh

Config

config.json — Version 2 Format (empfohlen):

{
  "version": 2,
  "refresh_interval_s": 180,
  "layout": {
    "grid": { "cols": 4, "rows": 4 },
    "items": [
      { "id": "c1", "plugin": "clock",   "x": 0, "y": 0, "w": 2, "h": 2 },
      { "id": "w1", "plugin": "weather", "x": 2, "y": 0, "w": 2, "h": 2 },
      { "id": "st1", "plugin": "system", "x": 0, "y": 2, "w": 2, "h": 2 },
      { "id": "h1", "plugin": "hello",   "x": 2, "y": 2, "w": 2, "h": 2 }
    ]
  },
  "plugin_configs": {
    "hello": { "text": "Edit me!", "size": 40 }
  }
}

Grid: 4 Spalten × 4 Zeilen = 16 Zellen à 200×120px. x,y = Spalte/Zeile oben-links, w,h = Breite/Höhe in Zellen.

Web Admin

http://pi:8080/
  • Layout-Editor — Items per Klick hinzufügen, verschieben, Größe ändern, Auto-Pack
  • Plugin-Config — API-Keys, Locations, etc. pro Widget
  • Refresh Now — sofortiger Display-Refresh
  • Netzwerk — WLAN wechseln ohne SSH

Default Login: admin / admin — in .env mit EPAPER_ADMIN_PASSWORD setzen.

Plugins

Plugin Datenquelle Config-Felder
clock Systemzeit
weather Open-Meteo (kein API-Key) location (lat,lon), show_uv, show_forecast_hours
system Pi CPU/RAM/Uptime show_uptime, show_load, show_temp
spotify Spotify Web API client_id, client_secret (via Spotify Dev Portal)
strava Strava API access_token, club_id
gmail Gmail API credentials_json (OAuth2)
minimax MiniMax AI api_key, model, prompt
hello statisch text, color, size

Eigenes Plugin schreiben

# plugins/my_widget.py
from plugins.base import Widget
from palette import FG, BG

class Widget(Widget):
    name = "my_widget"
    label = "Mein Widget"
    description = "Zeigt etwas"
    category = "info"

    config_schema = [
        {"key": "api_key", "label": "API Key", "type": "secret"},
        {"key": "interval", "label": "Intervall (s)", "type": "int", "default": 60},
    ]
    default_config = {"api_key": "", "interval": 60}

    def fetch(self):
        return {"value": 42}

    def render(self, draw, fonts, x, y, w, h):
        d = self.fetch()
        # w, h sind Pixel (nicht Zellen!)
        draw.text((x + 10, y + 10), f"Value: {d['value']}",
                  font=fonts["28"], fill=FG)

Palette: BLACK, WHITE, RED, GREEN, BLUE, YELLOW, ORANGE + semantisch FG, BG, OK, WARN, ALERT, INFO, ACCENT

Services

systemctl --user enable --now epaper-dashboard
systemctl --user enable --now epaper-admin
journalctl --user -u epaper-dashboard -f   # Logs

Wichtig: PrivateTmp=no in den Service-Files — sonst sieht der Admin-Socket /tmp/epaper-dashboard.sock nicht.

Display-Einschränkungen

  • Nur Full-Refresh: ACeP-Displays vertragen keine schnellen Partial-Refreshes. Minimum 180s.
  • Refresh-Dauer: Ein Full-Refresh dauert ~35-65s. Währenddessen ist das Display leer/flackernd.
  • Ghosting: Leichtes Ghosting bei manchen Farben normal. Display nicht bei direktem Sonnenlicht ablesen.

Recovery

Falls das konfigurierte WLAN nicht erreichbar ist: der Pi startet automatisch einen Recovery AP.

Dort kannst du ein neues WLAN konfigurieren. Details in RECOVERY.md.

Lizenz

Apache 2.0

S
Description
Plugin-based ePaper dashboard for 7.3" ACeP 7-Color display on Raspberry Pi 4
Readme
664 KiB
Languages
Python 67.3%
HTML 27.8%
JavaScript 4.9%