From 7a64cf3610b0c9f72573b6c4506b567c0834adab Mon Sep 17 00:00:00 2001 From: Serverlele30 <46974281+Serverlele30@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:37:00 +0100 Subject: [PATCH] Add files via upload --- README.md | 505 +++++++++++++++++++++++++++++++++++++++++- display_config.json | 6 + package.json | 28 +++ test_api.js | 93 ++++++++ ubahn-anzeige.service | 18 ++ ubahn_anzeige.js | 324 +++++++++++++++++++++++++++ 6 files changed, 973 insertions(+), 1 deletion(-) create mode 100644 display_config.json create mode 100644 package.json create mode 100644 test_api.js create mode 100644 ubahn-anzeige.service create mode 100644 ubahn_anzeige.js 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**. + +![U-Bahn Anzeige](https://img.shields.io/badge/U--Bahn-U9-orange?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiByeD0iNCIgZmlsbD0iIzAwNTBBMCIvPgo8dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIxOCIgZm9udC13ZWlnaHQ9ImJvbGQiIGZpbGw9IndoaXRlIj5VPC90ZXh0Pgo8L3N2Zz4K) +![Station](https://img.shields.io/badge/Station-Amrumer_Straße-yellow?style=for-the-badge) +![Platform](https://img.shields.io/badge/Platform-Raspberry_Pi_3B-c51a4a?style=for-the-badge&logo=raspberry-pi) + +--- + +## πŸ“Έ 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** 🐻 + +[![GitHub](https://img.shields.io/badge/GitHub-Serverlele30-181717?style=for-the-badge&logo=github)](https://github.com/Serverlele30/rpi-ubahn) +[![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)](LICENSE) +[![Node.js](https://img.shields.io/badge/Node.js-18+-339933?style=for-the-badge&logo=node.js)](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(); +}