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