Add files via upload
This commit is contained in:
1 parent
1c89edd796
commit
7a64cf3610
6 files changed
+973
-1
No files matched your search
@@ -1 +1,504 @@
|
||||
# rpi-ubahn
|
||||
# 🚇 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 ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
**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! 🚀
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"device": "/dev/fb0",
|
||||
"format": "BGRA8888",
|
||||
"width": 480,
|
||||
"height": 360
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
+93
@@ -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();
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user