diff --git a/README.md b/README.md
index d98bb42..9e392d8 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,504 @@
-# rpi-ubahn
\ No newline at end of file
+# π Berliner U-Bahn Anzeige fΓΌr Raspberry Pi
+
+Eine authentische Berliner U-Bahn Bahnsteiganzeige fΓΌr den **Raspberry Pi 3B** mit **joy-it RB-TFT3.2V2** Display (480x360). Zeigt Echtzeit-Abfahrten der **U9** an der Station **Amrumer StraΓe**.
+
+
+
+
+
+---
+
+## πΈ Screenshot
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β π΅ U Amrumer StraΓe 14:23 β β Gelber Header
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
+β β
+β β Osloer StraΓe β
+β π§ U9 5 min β β Orange U9-Badge
+β 8 min β
+β β
+β βββββββββββββββββββββββββββββββββββββββββββββββββββ β
+β β
+β β Rathaus Steglitz β
+β π§ U9 3 min β
+β 12 min β
+β β
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+---
+
+## β¨ Features
+
+- β
**Echtzeit-Daten** ΓΌber VBB REST API
+- β
**U9-spezifisch** fΓΌr Amrumer StraΓe
+- β
**Beide Richtungen**: Osloer StraΓe β Rathaus Steglitz
+- β
**Authentisches BVG-Design** mit U-Bahn Symbol
+- β
**Autostart** via systemd Service
+- β
**Dynamische Display-Konfiguration** mit `display_config.json`
+- β
**480x360 AuflΓΆsung** optimiert fΓΌr joy-it TFT Display
+- β
**Node.js basiert** - schnell und ressourcenschonend
+
+---
+
+## π§ Hardware-Anforderungen
+
+| Komponente | Beschreibung |
+|------------|--------------|
+| **Raspberry Pi** | Raspberry Pi 3B (oder neuer) |
+| **Display** | joy-it RB-TFT3.2V2 (480x360, BGRA8888) |
+| **OS** | Raspberry Pi OS (Lite oder Desktop) |
+| **Internet** | WLAN/Ethernet fΓΌr API-Zugriff |
+
+---
+
+## π¦ Installation
+
+### 1οΈβ£ System vorbereiten
+
+```bash
+# System aktualisieren
+sudo apt update && sudo apt upgrade -y
+
+# Node.js und npm installieren
+curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
+sudo apt install -y nodejs
+
+# Git installieren
+sudo apt install -y git
+
+# Projekt klonen
+git clone https://github.com/Serverlele30/rpi-ubahn.git
+cd rpi-ubahn
+```
+
+### 2οΈβ£ Dependencies installieren
+
+```bash
+npm install
+```
+
+**Wichtig:** Falls `canvas` Build-Fehler auftreten:
+
+```bash
+sudo apt install -y build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
+npm install canvas --build-from-source
+```
+
+### 3οΈβ£ Display konfigurieren
+
+Das Projekt enthΓ€lt bereits eine `display_config.json`:
+
+```json
+{
+ "device": "/dev/fb0",
+ "format": "BGRA8888",
+ "width": 480,
+ "height": 360
+}
+```
+
+**Display-Berechtigungen setzen:**
+
+```bash
+sudo chmod 666 /dev/fb0
+```
+
+---
+
+## π Verwendung
+
+### Manueller Start (Test)
+
+```bash
+node ubahn_anzeige.js
+```
+
+**Erwartete Ausgabe:**
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββ
+β U-BAHN ANZEIGE AMRUMER STR. β
+βββββββββββββββββββββββββββββββββββββββββββββ
+
+Device: /dev/fb0
+Format: BGRA8888
+AuflΓΆsung: 480x360
+
+β Display-Konfiguration geladen
+2024-11-10T... INFO: Display init gestartet
+2024-11-10T... INFO: Font geladen
+2024-11-10T... INFO: Framebuffer geΓΆffnet: /dev/fb0
+2024-11-10T... INFO: Display bereit
+2024-11-10T... INFO: API-Update: 12 U9-Bahnen
+```
+
+### API testen (ohne Display)
+
+```bash
+node test_api.js
+```
+
+Zeigt die nΓ€chsten U9-Abfahrten in der Konsole an.
+
+---
+
+## π Autostart einrichten
+
+### systemd Service installieren
+
+```bash
+# Service-Datei kopieren
+sudo cp ubahn-anzeige.service /etc/systemd/system/
+
+# Service aktivieren
+sudo systemctl daemon-reload
+sudo systemctl enable ubahn-anzeige.service
+sudo systemctl start ubahn-anzeige.service
+```
+
+### Service verwalten
+
+```bash
+# Status prΓΌfen
+sudo systemctl status ubahn-anzeige
+
+# Logs anzeigen
+sudo journalctl -u ubahn-anzeige -f
+
+# Neustarten
+sudo systemctl restart ubahn-anzeige
+
+# Stoppen
+sudo systemctl stop ubahn-anzeige
+```
+
+---
+
+## π Projektstruktur
+
+```
+rpi-ubahn/
+βββ ubahn_anzeige.js # Hauptprogramm
+βββ test_api.js # API-Test-Script
+βββ package.json # npm Dependencies
+βββ display_config.json # Display-Konfiguration
+βββ ubahn-anzeige.service # systemd Service-Datei
+βββ README.md # Diese Datei
+```
+
+### Datei-Beschreibungen
+
+| Datei | Beschreibung |
+|-------|--------------|
+| `ubahn_anzeige.js` | Hauptprogramm - holt API-Daten und zeichnet Display |
+| `test_api.js` | Test-Tool fΓΌr VBB API ohne Display |
+| `display_config.json` | Display-Einstellungen (AuflΓΆsung, Format, Device) |
+| `ubahn-anzeige.service` | Systemd Service fΓΌr Autostart |
+| `package.json` | Node.js AbhΓ€ngigkeiten (axios, canvas) |
+
+---
+
+## βοΈ Konfiguration
+
+### Station Γ€ndern
+
+Im Code `ubahn_anzeige.js` die `STATION_ID` Γ€ndern:
+
+```javascript
+const STATION_ID = "900009101"; // Amrumer StraΓe
+```
+
+**Andere Stationen finden:**
+
+```bash
+# Beispiel: Alexanderplatz suchen
+curl "https://v6.vbb.transport.rest/locations?query=Alexanderplatz&results=5"
+```
+
+### Display-AuflΓΆsung anpassen
+
+In `display_config.json`:
+
+```json
+{
+ "device": "/dev/fb0",
+ "format": "BGRA8888",
+ "width": 320, β Anpassen
+ "height": 240 β Anpassen
+}
+```
+
+### Update-Intervall Γ€ndern
+
+In `ubahn_anzeige.js` Zeile ~107:
+
+```javascript
+setInterval(fetchData, 20000); // 20 Sekunden (20000ms)
+```
+
+---
+
+## π¨ Design-Details
+
+### Farben (BVG-konform)
+
+| Element | Farbe | RGB |
+|---------|-------|-----|
+| Hintergrund | Dunkelblau-Grau | `rgb(45, 55, 70)` |
+| Header | BVG Gelb | `rgb(255, 220, 0)` |
+| U9 Badge | Orange | `rgb(255, 110, 0)` |
+| U-Symbol | Blau | `rgb(0, 80, 160)` |
+| Text | WeiΓ | `rgb(255, 255, 255)` |
+
+### Layout
+
+- **Header (70px)**: U-Bahn Symbol + Stationsname + Uhrzeit
+- **Richtung 1**: β Osloer StraΓe (2 Abfahrten)
+- **Trennlinie**: Gelbe Linie
+- **Richtung 2**: β Rathaus Steglitz (2 Abfahrten)
+
+---
+
+## π API-Details
+
+### VBB REST API
+
+- **Base URL**: `https://v6.vbb.transport.rest/`
+- **Dokumentation**: [v6.vbb.transport.rest](https://v6.vbb.transport.rest/)
+- **Endpoint**: `/stops/{id}/departures`
+- **Parameter**:
+ - `duration=60` - Abfahrten in nΓ€chsten 60 Minuten
+ - `results=50` - Max. 50 Ergebnisse
+
+**Beispiel-Request:**
+
+```bash
+curl "https://v6.vbb.transport.rest/stops/900009101/departures?duration=60"
+```
+
+---
+
+## π Troubleshooting
+
+### Problem: Display bleibt schwarz
+
+**LΓΆsung:**
+
+```bash
+# Framebuffer-Berechtigungen prΓΌfen
+ls -l /dev/fb0
+
+# Berechtigungen setzen
+sudo chmod 666 /dev/fb0
+
+# Oder dauerhaft mit udev-Regel:
+echo 'SUBSYSTEM=="graphics", KERNEL=="fb0", MODE="0666"' | sudo tee /etc/udev/rules.d/99-fbdev.rules
+sudo udevadm control --reload-rules
+```
+
+### Problem: Canvas Build-Fehler
+
+**LΓΆsung:**
+
+```bash
+# Build-Tools installieren
+sudo apt install -y build-essential libcairo2-dev libpango1.0-dev \
+ libjpeg-dev libgif-dev librsvg2-dev
+
+# Canvas neu kompilieren
+npm rebuild canvas --build-from-source
+```
+
+### Problem: "Font nicht gefunden"
+
+**LΓΆsung:**
+
+```bash
+# DejaVu Fonts installieren
+sudo apt install -y fonts-dejavu-core
+
+# Font-Cache aktualisieren
+fc-cache -fv
+```
+
+### Problem: API liefert keine Daten
+
+**PrΓΌfen:**
+
+```bash
+# Test-Script ausfΓΌhren
+node test_api.js
+
+# Netzwerk prΓΌfen
+ping -c 4 v6.vbb.transport.rest
+
+# API manuell testen
+curl "https://v6.vbb.transport.rest/stops/900009101/departures?duration=60"
+```
+
+---
+
+## π Logs analysieren
+
+```bash
+# Service-Logs (Live)
+sudo journalctl -u ubahn-anzeige -f
+
+# Letzte 50 Zeilen
+sudo journalctl -u ubahn-anzeige -n 50
+
+# Fehler filtern
+sudo journalctl -u ubahn-anzeige -p err
+```
+
+---
+
+## π Performance
+
+- **CPU-Nutzung**: ~5-10% (Raspberry Pi 3B)
+- **RAM**: ~50-80 MB
+- **API-Requests**: Alle 20 Sekunden
+- **Display-Refresh**: Jede Sekunde
+
+---
+
+## π€ Beitragen
+
+BeitrΓ€ge sind herzlich willkommen! π
+
+1. **Fork** das Repository
+2. **Clone** deinen Fork: `git clone https://github.com/DEIN-NAME/rpi-ubahn.git`
+3. **Branch** erstellen: `git checkout -b feature/neue-funktion`
+4. **Commit** Γnderungen: `git commit -m "Neue Funktion hinzugefΓΌgt"`
+5. **Push**: `git push origin feature/neue-funktion`
+6. **Pull Request** ΓΆffnen
+
+---
+
+## π TODO / Roadmap
+
+- [ ] **Mehrere Linien** unterstΓΌtzen (U1, U2, U3, etc.)
+- [ ] **Stationswechsel** per GPIO-Button
+- [ ] **Web-Interface** fΓΌr Konfiguration
+- [ ] **StΓΆrungsmeldungen** anzeigen
+- [ ] **Nacht-Modus** (Display aus 23:00-05:00)
+- [ ] **Warnung** bei VerspΓ€tungen
+- [ ] **Historische Daten** / Statistiken
+- [ ] **Docker-Support**
+
+---
+
+## π Features in Arbeit
+
+### Geplante Updates
+
+- **Multi-Station Support**: Wechsel zwischen mehreren Stationen
+- **Touch-Support**: Touch-Bedienung auf dem Display
+- **API-Caching**: Offline-Fallback bei API-Ausfall
+- **Custom Themes**: Verschiedene Farbschemata
+
+---
+
+## π Lizenz
+
+**MIT License**
+
+```
+Copyright (c) 2024 Serverlele30
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+---
+
+## π Credits & Danksagungen
+
+- **VBB API**: [v6.vbb.transport.rest](https://v6.vbb.transport.rest/) - Γffentliche Verkehrsdaten Berlin
+- **Node Canvas**: [node-canvas](https://github.com/Automattic/node-canvas) - HTML5 Canvas fΓΌr Node.js
+- **joy-it**: [RB-TFT3.2V2](https://joy-it.net/en/products/RB-TFT3.2V2) - TFT Display Hardware
+- **BVG**: Design inspiriert von echten Berliner U-Bahn Anzeigen
+
+---
+
+## π Support & Kontakt
+
+- **GitHub Issues**: [github.com/Serverlele30/rpi-ubahn/issues](https://github.com/Serverlele30/rpi-ubahn/issues)
+- **Diskussionen**: [github.com/Serverlele30/rpi-ubahn/discussions](https://github.com/Serverlele30/rpi-ubahn/discussions)
+
+---
+
+## π NΓΌtzliche Links
+
+| Link | Beschreibung |
+|------|--------------|
+| [VBB API Docs](https://v6.vbb.transport.rest/) | API-Dokumentation |
+| [joy-it Display](https://joy-it.net/en/products/RB-TFT3.2V2) | Display-Dokumentation |
+| [Raspberry Pi](https://www.raspberrypi.com/) | Offizielle Raspberry Pi Seite |
+| [BVG](https://www.bvg.de/) | Berliner Verkehrsbetriebe |
+
+---
+
+## π Star History
+
+Wenn dir dieses Projekt gefΓ€llt, gib ihm einen β auf GitHub!
+
+---
+
+## πΈ Galerie
+
+### Real-Life Setup
+
+*FΓΌge hier Fotos von deinem Setup hinzu!*
+
+### Display-Ansicht
+
+```
+ββββββββββββββββββββββββββββββββββββββββββββββββββ
+β π΅ U Amrumer StraΓe 14:23 β
+β βββββββββββββββββββββββββββββββββββββββββββββββββ£
+β β
+β β Osloer StraΓe β
+β π§ U9 5 min β
+β 8 min β
+β β
+β ββββββββββββββββββββββββββββββββββββββββββββ β
+β β
+β β Rathaus Steglitz β
+β π§ U9 3 min β
+β 12 min β
+β β
+ββββββββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+---
+
+
+
+**Made with β€οΈ in Berlin** π»
+
+[](https://github.com/Serverlele30/rpi-ubahn)
+[](LICENSE)
+[](https://nodejs.org/)
+
+**Bei Fragen oder Problemen:** [Issues](https://github.com/Serverlele30/rpi-ubahn/issues) ΓΆffnen! π
+
+
diff --git a/display_config.json b/display_config.json
new file mode 100644
index 0000000..d217697
--- /dev/null
+++ b/display_config.json
@@ -0,0 +1,6 @@
+{
+ "device": "/dev/fb0",
+ "format": "BGRA8888",
+ "width": 480,
+ "height": 360
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..87e4fcc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "ubahn-anzeige",
+ "version": "1.0.0",
+ "description": "Berliner U-Bahn Bahnsteig Anzeige fΓΌr Raspberry Pi mit TFT Display",
+ "main": "ubahn_anzeige.js",
+ "scripts": {
+ "start": "node ubahn_anzeige.js",
+ "dev": "node ubahn_anzeige.js",
+ "install-service": "sudo cp ubahn-anzeige.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable ubahn-anzeige"
+ },
+ "keywords": [
+ "ubahn",
+ "berlin",
+ "vbb",
+ "raspberry-pi",
+ "display"
+ ],
+ "author": "",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "^1.6.2",
+ "canvas": "^2.11.2",
+ "onoff": "^6.0.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+}
diff --git a/test_api.js b/test_api.js
new file mode 100644
index 0000000..bf1ca5a
--- /dev/null
+++ b/test_api.js
@@ -0,0 +1,93 @@
+#!/usr/bin/env node
+// API Test-Script - Testen ohne Display
+
+const axios = require('axios');
+
+const STATION_ID = "900009101"; // Amrumer StraΓe
+const API_URL = `https://v6.vbb.transport.rest/stops/${STATION_ID}/departures`;
+
+async function testAPI() {
+ console.log("=".repeat(50));
+ console.log("U-Bahn API Test");
+ console.log("=".repeat(50));
+ console.log("");
+ console.log(`Station ID: ${STATION_ID}`);
+ console.log(`API URL: ${API_URL}`);
+ console.log("");
+
+ try {
+ console.log("Fetching departures...");
+ const response = await axios.get(`${API_URL}?duration=60&results=50`, {
+ timeout: 10000
+ });
+
+ const data = response.data;
+ let departures = [];
+
+ // Handle different API response formats
+ if (typeof data === 'object' && data.departures) {
+ departures = data.departures;
+ } else if (Array.isArray(data)) {
+ departures = data;
+ }
+
+ console.log(`β Received ${departures.length} departures`);
+ console.log("");
+
+ // Filter U9
+ const u9 = departures.filter(d => d.line?.name === 'U9');
+ console.log(`U9 Departures: ${u9.length}`);
+ console.log("");
+
+ // Split by direction
+ const osloer = u9.filter(d => d.direction?.includes('Osloer'));
+ const steglitz = u9.filter(d =>
+ d.direction?.includes('Steglitz') || d.direction?.includes('Rathaus')
+ );
+
+ console.log("β".repeat(50));
+ console.log("β Osloer Str.");
+ console.log("β".repeat(50));
+ osloer.slice(0, 3).forEach((dep, i) => {
+ const when = new Date(dep.when);
+ const now = new Date();
+ const mins = Math.floor((when - now) / 1000 / 60);
+ console.log(` ${i + 1}. U9 in ${mins} min (${when.toLocaleTimeString('de-DE')})`);
+ });
+
+ console.log("");
+ console.log("β".repeat(50));
+ console.log("β Rathaus Steglitz");
+ console.log("β".repeat(50));
+ steglitz.slice(0, 3).forEach((dep, i) => {
+ const when = new Date(dep.when);
+ const now = new Date();
+ const mins = Math.floor((when - now) / 1000 / 60);
+ console.log(` ${i + 1}. U9 in ${mins} min (${when.toLocaleTimeString('de-DE')})`);
+ });
+
+ console.log("");
+ console.log("=".repeat(50));
+ console.log("β API Test erfolgreich!");
+ console.log("=".repeat(50));
+
+ } catch (error) {
+ console.error("");
+ console.error("β API Test fehlgeschlagen!");
+ console.error("");
+
+ if (error.code === 'ENOTFOUND') {
+ console.error("Fehler: Keine Internetverbindung");
+ } else if (error.code === 'ETIMEDOUT') {
+ console.error("Fehler: Timeout - API antwortet nicht");
+ } else {
+ console.error(`Fehler: ${error.message}`);
+ }
+
+ console.error("");
+ process.exit(1);
+ }
+}
+
+// Run test
+testAPI();
diff --git a/ubahn-anzeige.service b/ubahn-anzeige.service
new file mode 100644
index 0000000..6ce60fc
--- /dev/null
+++ b/ubahn-anzeige.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=U-Bahn Anzeige fΓΌr Raspberry Pi
+After=network.target
+
+[Service]
+Type=simple
+User=pi
+WorkingDirectory=/home/pi/ubahn
+Environment="NODE_ENV=production"
+ExecStart=/usr/bin/sudo /usr/bin/node /home/pi/ubahn/ubahn_anzeige.js
+Restart=always
+RestartSec=10
+StandardOutput=journal
+StandardError=journal
+SyslogIdentifier=ubahn-anzeige
+
+[Install]
+WantedBy=multi-user.target
diff --git a/ubahn_anzeige.js b/ubahn_anzeige.js
new file mode 100644
index 0000000..27a4a4f
--- /dev/null
+++ b/ubahn_anzeige.js
@@ -0,0 +1,324 @@
+#!/usr/bin/env node
+// -*- coding: utf-8 -*-
+// U-Bahn Anzeige β angepasst fΓΌr 480x360 (BGRA8888)
+
+const axios = require("axios");
+const fs = require("fs");
+const { createCanvas, registerFont } = require("canvas");
+
+// ---- Konfiguration ----
+let config = {
+ device: "/dev/fb0",
+ format: "BGRA8888",
+ width: 480,
+ height: 360
+};
+
+try {
+ if (fs.existsSync("display_config.json")) {
+ const savedConfig = JSON.parse(fs.readFileSync("display_config.json", "utf8"));
+ config = { ...config, ...savedConfig };
+ console.log("β Display-Konfiguration geladen");
+ } else {
+ console.log("! Keine display_config.json gefunden, nutze Defaults");
+ }
+} catch (e) {
+ console.log("! Fehler beim Laden der Config:", e.message);
+}
+
+// Die Stations ID findest du unter
+// "https://v6.vbb.transport.rest/locations?query=U_amrummer_stra%C3%9Fe&results=1"
+// Das ist fΓΌr die Amrumer StraΓe. Siehe hier: ^^^^^^^^^^^^^
+const STATION_ID = "900009101";
+
+const API_URL = `https://v6.vbb.transport.rest/stops/${STATION_ID}/departures`;
+
+const FB_DEVICE = config.device;
+const WIDTH = config.width;
+const HEIGHT = config.height;
+const FORMAT = config.format;
+
+// Farben
+const BG = { r: 45, g: 55, b: 70 };
+const YELLOW = { r: 255, g: 220, b: 0 };
+const U9_ORANGE = { r: 255, g: 110, b: 0 };
+const WHITE = { r: 255, g: 255, b: 255 };
+const GRAY = { r: 130, g: 130, b: 130 };
+
+console.log("\nβββββββββββββββββββββββββββββββββββββββββββββ");
+console.log("β U-BAHN ANZEIGE AMRUMER STR. β");
+console.log("βββββββββββββββββββββββββββββββββββββββββββββ\n");
+
+console.log(`Device: ${FB_DEVICE}`);
+console.log(`Format: ${FORMAT}`);
+console.log(`AuflΓΆsung: ${WIDTH}x${HEIGHT}\n`);
+
+function log(level, message) {
+ const timestamp = new Date().toISOString();
+ console.log(`${timestamp} ${level}: ${message}`);
+}
+
+class Display {
+ constructor() {
+ log("INFO", "Display init gestartet");
+ this.departures_cache = [];
+ this.running = true;
+ this.fb = null;
+ this.draw_count = 0;
+
+ process.stdout.write("\x1B[?25l"); // Cursor ausblenden
+
+ try {
+ registerFont("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", { family: "DejaVuSans" });
+ log("INFO", "Font geladen");
+ } catch (e) {
+ log("WARNING", `Font-Fehler: ${e.message}`);
+ }
+
+ this.openFramebuffer();
+
+ process.on("SIGTERM", () => this.onSignal("SIGTERM"));
+ process.on("SIGINT", () => this.onSignal("SIGINT"));
+
+ this.startApiLoop();
+ }
+
+ openFramebuffer() {
+ try {
+ if (!fs.existsSync(FB_DEVICE)) {
+ log("ERROR", `${FB_DEVICE} existiert nicht!`);
+ return;
+ }
+ this.fb = fs.openSync(FB_DEVICE, "r+");
+ log("INFO", `Framebuffer geΓΆffnet: ${FB_DEVICE}`);
+
+ const clearBuf = Buffer.alloc(WIDTH * HEIGHT * 4);
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
+ const offset = i * 4;
+ clearBuf[offset] = BG.b;
+ clearBuf[offset + 1] = BG.g;
+ clearBuf[offset + 2] = BG.r;
+ clearBuf[offset + 3] = 255;
+ }
+ fs.writeSync(this.fb, clearBuf, 0, clearBuf.length, 0);
+ log("INFO", "Display bereit");
+ } catch (e) {
+ log("ERROR", `Framebuffer-Fehler: ${e.message}`);
+ if (e.code === "EACCES") log("ERROR", `LΓΆsung: sudo chmod 666 ${FB_DEVICE}`);
+ }
+ }
+
+ onSignal(signum) {
+ log("INFO", `Signal ${signum} empfangen, beende...`);
+ this.running = false;
+ process.stdout.write("\x1B[?25h");
+ if (this.fb !== null) fs.closeSync(this.fb);
+ process.exit(0);
+ }
+
+ startApiLoop() {
+ const fetchData = async () => {
+ if (!this.running) return;
+ try {
+ const response = await axios.get(`${API_URL}?duration=60&results=50`, { timeout: 10000 });
+ const data = response.data;
+ let u9 = [];
+
+ if (data?.departures) u9 = data.departures.filter(d => d.line?.name === "U9");
+ else if (Array.isArray(data)) u9 = data.filter(d => d.line?.name === "U9");
+
+ this.departures_cache = u9;
+ log("INFO", `API-Update: ${u9.length} U9-Bahnen`);
+ } catch (e) {
+ log("WARNING", `API-Fehler: ${e.message}`);
+ }
+ };
+
+ fetchData();
+ setInterval(fetchData, 20000);
+ }
+
+ mins(when) {
+ try {
+ let whenStr = when;
+ if (whenStr.endsWith("Z")) whenStr = whenStr.slice(0, -1) + "+00:00";
+ const dep = new Date(whenStr);
+ const diff = (dep - new Date()) / 60000;
+ return diff >= 0.5 ? String(Math.floor(diff)) : "0";
+ } catch {
+ return "--";
+ }
+ }
+
+ roundRect(ctx, x, y, w, h, r) {
+ ctx.beginPath();
+ ctx.moveTo(x + r, y);
+ ctx.lineTo(x + w - r, y);
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
+ ctx.lineTo(x + w, y + h - r);
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
+ ctx.lineTo(x + r, y + h);
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
+ ctx.lineTo(x, y + r);
+ ctx.quadraticCurveTo(x, y, x + r, y);
+ ctx.closePath();
+ ctx.fill();
+ }
+
+draw() {
+ if (!this.fb) return;
+
+ const canvas = createCanvas(WIDTH, HEIGHT);
+ const ctx = canvas.getContext("2d");
+
+ // Hintergrund
+ ctx.fillStyle = `rgb(${BG.r},${BG.g},${BG.b})`;
+ ctx.fillRect(0, 0, WIDTH, HEIGHT);
+
+ // Header
+ ctx.fillStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.fillRect(0, 0, WIDTH, 70);
+
+ // --- U-Bahn Symbol (blau mit weiΓem U) ---
+ const uBoxX = 15;
+ const uBoxY = 10;
+ const uBoxSize = 50;
+
+ // Hintergrund des Symbols (blau)
+ ctx.fillStyle = "rgb(0, 80, 160)";
+ this.roundRect(ctx, uBoxX, uBoxY, uBoxSize, uBoxSize, 6);
+
+ // Buchstabe βUβ mittig & weiΓ
+ ctx.fillStyle = "white";
+ ctx.font = "bold 38px DejaVuSans";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText("U", uBoxX + uBoxSize / 2, uBoxY + uBoxSize / 2 + 2);
+
+ // Stationsname rechts neben dem Symbol
+ ctx.textAlign = "left";
+ ctx.textBaseline = "alphabetic";
+ ctx.fillStyle = `rgb(${BG.r},${BG.g},${BG.b})`;
+ ctx.font = "bold 28px DejaVuSans";
+ ctx.fillText("Amrumer StraΓe", uBoxX + uBoxSize + 25, 50);
+
+ // Uhrzeit
+ const now = new Date().toTimeString().slice(0, 5);
+ ctx.font = "bold 26px DejaVuSans";
+ ctx.fillText(now, WIDTH - 90, 50);
+
+ // --- Abfahrten ---
+ const deps = [...this.departures_cache];
+ const osloer = deps.filter(x => x.direction?.includes("Osloer")).slice(0, 2);
+ const steglitz = deps.filter(x => x.direction?.includes("Steglitz") || x.direction?.includes("Rathaus")).slice(0, 2);
+
+ // Verschiebung nach unten
+ const offsetY = 120;
+
+ // Abschnitt Osloer
+ ctx.fillStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.font = "bold 26px DejaVuSans";
+ ctx.fillText("β Osloer StraΓe", 15, offsetY);
+
+ if (osloer.length) {
+ const mins = this.mins(osloer[0].when);
+ ctx.fillStyle = `rgb(${WHITE.r},${WHITE.g},${WHITE.b})`;
+ this.roundRect(ctx, 15, offsetY + 10, 75, 45, 6);
+ ctx.fillStyle = `rgb(${U9_ORANGE.r},${U9_ORANGE.g},${U9_ORANGE.b})`;
+ this.roundRect(ctx, 18, offsetY + 13, 69, 39, 5);
+ ctx.fillStyle = `rgb(${WHITE.r},${WHITE.g},${WHITE.b})`;
+ ctx.font = "bold 32px DejaVuSans";
+ ctx.fillText("U9", 25, offsetY + 45);
+
+ ctx.fillStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.font = "bold 52px DejaVuSans";
+ ctx.fillText(mins, WIDTH - 145, offsetY + 45);
+ ctx.fillStyle = `rgb(${WHITE.r},${WHITE.g},${WHITE.b})`;
+ ctx.font = "bold 36px DejaVuSans";
+ ctx.fillText("min", WIDTH - 85, offsetY + 45);
+ }
+
+ if (osloer.length >= 2) {
+ const mins2 = this.mins(osloer[1].when);
+ ctx.fillStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.font = "bold 36px DejaVuSans";
+ ctx.fillText(mins2, WIDTH - 125, offsetY + 80);
+ ctx.fillStyle = `rgb(${GRAY.r},${GRAY.g},${GRAY.b})`;
+ ctx.font = "bold 24px DejaVuSans";
+ ctx.fillText("min", WIDTH - 85, offsetY + 80);
+ }
+
+ // Trennlinie
+ ctx.strokeStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.lineWidth = 2;
+ ctx.beginPath();
+ ctx.moveTo(10, offsetY + 100);
+ ctx.lineTo(WIDTH - 10, offsetY + 100);
+ ctx.stroke();
+
+ // Abschnitt Steglitz
+ ctx.fillStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.font = "bold 26px DejaVuSans";
+ ctx.fillText("β Rathaus Steglitz", 15, offsetY + 130);
+
+ if (steglitz.length) {
+ const mins = this.mins(steglitz[0].when);
+ ctx.fillStyle = `rgb(${WHITE.r},${WHITE.g},${WHITE.b})`;
+ this.roundRect(ctx, 15, offsetY + 140, 75, 45, 6);
+ ctx.fillStyle = `rgb(${U9_ORANGE.r},${U9_ORANGE.g},${U9_ORANGE.b})`;
+ this.roundRect(ctx, 18, offsetY + 143, 69, 39, 5);
+ ctx.fillStyle = `rgb(${WHITE.r},${WHITE.g},${WHITE.b})`;
+ ctx.font = "bold 32px DejaVuSans";
+ ctx.fillText("U9", 25, offsetY + 175);
+
+ ctx.fillStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.font = "bold 52px DejaVuSans";
+ ctx.fillText(mins, WIDTH - 145, offsetY + 175);
+ ctx.fillStyle = `rgb(${WHITE.r},${WHITE.g},${WHITE.b})`;
+ ctx.font = "bold 36px DejaVuSans";
+ ctx.fillText("min", WIDTH - 85, offsetY + 175);
+ }
+
+ if (steglitz.length >= 2) {
+ const mins2 = this.mins(steglitz[1].when);
+ ctx.fillStyle = `rgb(${YELLOW.r},${YELLOW.g},${YELLOW.b})`;
+ ctx.font = "bold 36px DejaVuSans";
+ ctx.fillText(mins2, WIDTH - 125, offsetY + 210);
+ ctx.fillStyle = `rgb(${GRAY.r},${GRAY.g},${GRAY.b})`;
+ ctx.font = "bold 24px DejaVuSans";
+ ctx.fillText("min", WIDTH - 85, offsetY + 210);
+ }
+
+ this.write(canvas);
+}
+
+ write(canvas) {
+ if (!this.fb) return;
+ const ctx = canvas.getContext("2d");
+ const img = ctx.getImageData(0, 0, WIDTH, HEIGHT);
+ const buffer = Buffer.alloc(WIDTH * HEIGHT * 4);
+ for (let i = 0; i < WIDTH * HEIGHT; i++) {
+ const s = i * 4;
+ buffer[s] = img.data[s + 2]; // BGRA
+ buffer[s + 1] = img.data[s + 1];
+ buffer[s + 2] = img.data[s];
+ buffer[s + 3] = img.data[s + 3];
+ }
+ fs.writeSync(this.fb, buffer, 0, buffer.length, 0);
+ }
+
+ run() {
+ const loop = () => {
+ if (!this.running) return;
+ this.draw();
+ setTimeout(loop, 1000);
+ };
+ loop();
+ }
+}
+
+// Main
+if (require.main === module) {
+ const disp = new Display();
+ disp.run();
+}