Added comments and readme
This commit is contained in:
parent
1d632252bf
commit
e25c8606d9
8 changed files with 252 additions and 16 deletions
211
README.md
211
README.md
|
|
@ -1,3 +1,212 @@
|
||||||
# Scoreboard-JS
|
# Scoreboard-JS
|
||||||
|
|
||||||
Implements a score- and timeboard for various sports
|
Echtzeit-Anzeigetafel für Sportveranstaltungen — zeigt Timer und Spielstand auf einem separaten Monitor an und wird über ein Admin-Panel gesteuert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Das System besteht aus zwei Teilen:
|
||||||
|
|
||||||
|
| Teil | URL | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| **Monitor** | `http://<host>:3000/` | Vollbildanzeige für den Beamer/Monitor (Timer + Score) |
|
||||||
|
| **Admin-Panel** | `http://<host>:3000/admin` | Steuerung von Timer und Scoreboard |
|
||||||
|
|
||||||
|
Alle Änderungen vom Admin-Panel werden über **WebSockets (Socket.IO)** in Echtzeit auf dem Monitor angezeigt — ohne Seiten-Reload.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
- Node.js (v18 oder neuer empfohlen)
|
||||||
|
- npm
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation & Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scoreboard
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Server startet mit `nodemon` und ist anschließend unter Port **3000** erreichbar.
|
||||||
|
Der Socket.IO-Server läuft separat auf Port **3001**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
scoreboard/
|
||||||
|
├── app.js # Express-App, Middleware, Routen
|
||||||
|
├── bin/www # HTTP-Server, startet die App
|
||||||
|
├── controllers/
|
||||||
|
│ ├── socketio.js # Socket.IO-Server (Port 3001)
|
||||||
|
│ ├── timer.js # Timer-Logik (Start, Pause, Reset, IncDec)
|
||||||
|
│ ├── score.js # Score-Logik (Punkte, Teams, Seitenwechsel)
|
||||||
|
│ └── cli.js # CLI-Controller (aktuell Testcode)
|
||||||
|
├── routes/
|
||||||
|
│ ├── admin.js # REST-Endpunkte für das Admin-Panel
|
||||||
|
│ └── index.js # Monitor-Ansicht + WebSocket-Verbindungshandler
|
||||||
|
├── views/
|
||||||
|
│ ├── admin.hbs # Admin-Panel (Handlebars-Template)
|
||||||
|
│ ├── index.hbs # Monitor ohne Scoreboard (nur Timer)
|
||||||
|
│ └── indexScore.hbs # Monitor mit Scoreboard (Timer + Score + Teamnamen)
|
||||||
|
└── public/
|
||||||
|
├── javascripts/
|
||||||
|
│ ├── admin.js # Frontend-Logik Admin-Panel
|
||||||
|
│ ├── index.js # Frontend-Logik Monitor (ohne Score)
|
||||||
|
│ └── indexScore.js # Frontend-Logik Monitor (mit Score)
|
||||||
|
└── stylesheets/
|
||||||
|
├── admin.css
|
||||||
|
├── index.css
|
||||||
|
├── indexScore.css
|
||||||
|
└── seven-segment.css # Seven-Segment-Display-Schrift für den Timer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funktionen
|
||||||
|
|
||||||
|
### Timer
|
||||||
|
|
||||||
|
- Countdown-Timer mit konfigurierbarer Startzeit (Standard: **7:00 Minuten**)
|
||||||
|
- Start / Pause
|
||||||
|
- Zurücksetzen auf eine neue Zeit (0:00 bis 10:00, in 15-Sekunden-Schritten)
|
||||||
|
- Feineinstellung im laufenden Betrieb: ±1s, ±5s, ±10s
|
||||||
|
- Nach Ablauf wird `timerEnded` an alle Clients gesendet und "ENDE" auf dem Monitor angezeigt
|
||||||
|
|
||||||
|
### Scoreboard
|
||||||
|
|
||||||
|
- Punktestand für **Team A** und **Team B**
|
||||||
|
- Punkte einzeln erhöhen (+1) oder verringern (-1)
|
||||||
|
- Score direkt auf einen bestimmten Wert setzen
|
||||||
|
- Score zurücksetzen (beide Teams auf 0)
|
||||||
|
- **Scoreboard ein-/ausblenden:** Der Monitor wechselt zwischen Timer-only-Ansicht (`index.hbs`) und Timer+Score-Ansicht (`indexScore.hbs`)
|
||||||
|
|
||||||
|
### Teams konfigurieren
|
||||||
|
|
||||||
|
Jedes Team hat folgende Felder:
|
||||||
|
|
||||||
|
| Feld | Beschreibung |
|
||||||
|
|---|---|
|
||||||
|
| `name` | Hauptname des Teams |
|
||||||
|
| `name2` | Zweiter Name (nur bei Spielgemeinschaft) |
|
||||||
|
| `isSpielgemeinschaft` | Wenn aktiv, werden beide Namen untereinander angezeigt |
|
||||||
|
|
||||||
|
### Seitenwechsel (Halbzeit)
|
||||||
|
|
||||||
|
Über "Seitenwechsel" werden Team A und Team B auf dem Monitor die Seiten tauschen — der Spielstand bleibt unverändert, nur die Anzeigepositionen (links/rechts) wechseln.
|
||||||
|
|
||||||
|
### Monitor neu laden
|
||||||
|
|
||||||
|
Der Button "Monitor neu laden" im Admin-Panel sendet ein `refresh`-Event an alle verbundenen Clients und löst einen Seiten-Reload des Monitors aus. Nützlich z.B. nach dem Ein-/Ausblenden des Scoreboards.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur / Datenfluss
|
||||||
|
|
||||||
|
```
|
||||||
|
Admin-Panel (Browser)
|
||||||
|
│
|
||||||
|
│ HTTP REST (fetch)
|
||||||
|
▼
|
||||||
|
Express-Server :3000
|
||||||
|
routes/admin.js
|
||||||
|
│
|
||||||
|
├── controllers/timer.js ──┐
|
||||||
|
└── controllers/score.js ──┤
|
||||||
|
│ io.sockets.emit(event, data)
|
||||||
|
▼
|
||||||
|
Socket.IO-Server :3001
|
||||||
|
│
|
||||||
|
WebSocket-Verbindung
|
||||||
|
│
|
||||||
|
┌───────────────┴───────────────┐
|
||||||
|
▼ ▼
|
||||||
|
Monitor (index.hbs) Monitor (indexScore.hbs)
|
||||||
|
index.js indexScore.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket-Events
|
||||||
|
|
||||||
|
| Event | Richtung | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `timerDurationLeft` | Server → Client | Aktuelle Restzeit als String `MM:SS` |
|
||||||
|
| `timerEnded` | Server → Client | Timer abgelaufen |
|
||||||
|
| `score` | Server → Client | Aktueller Spielstand als String `X:Y` |
|
||||||
|
| `scoreSideswitch` | Server → Client | Seitenwechsel ausgelöst — Frontend neu laden |
|
||||||
|
| `refresh` | Server → Client | Kompletten Seiten-Reload auslösen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## REST-API (Admin-Endpunkte)
|
||||||
|
|
||||||
|
Alle Endpunkte sind unter `/admin` erreichbar.
|
||||||
|
|
||||||
|
### Allgemein
|
||||||
|
|
||||||
|
| Methode | Endpunkt | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/admin/refreshMonitor` | Sendet `refresh` an alle Monitors |
|
||||||
|
|
||||||
|
### Timer
|
||||||
|
|
||||||
|
| Methode | Endpunkt | Body | Beschreibung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/admin/timerStart` | — | Timer starten |
|
||||||
|
| GET | `/admin/timerPause` | — | Timer pausieren |
|
||||||
|
| POST | `/admin/timerReset` | `{ duration: <Sekunden> }` | Timer zurücksetzen |
|
||||||
|
| POST | `/admin/timerIncDec` | `{ value: <Sekunden> }` | Timer erhöhen (positiv) / verringern (negativ) |
|
||||||
|
| GET | `/admin/timerGetValues` | — | Aktuelle Timer-Werte abrufen |
|
||||||
|
|
||||||
|
Antwortformat `timerGetValues`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"isPaused": true,
|
||||||
|
"duration": "<ISO 8601>",
|
||||||
|
"durationLeft": "<ISO 8601>",
|
||||||
|
"print": "07:00"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scoreboard
|
||||||
|
|
||||||
|
| Methode | Endpunkt | Body | Beschreibung |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/admin/scoreToggle` | — | Scoreboard ein-/ausblenden |
|
||||||
|
| GET | `/admin/scoreGetValues` | — | Aktuelle Score-Werte abrufen |
|
||||||
|
| GET | `/admin/scoreClearScore` | — | Spielstand beider Teams auf 0 |
|
||||||
|
| GET | `/admin/scoreToggleSideswitch` | — | Seiten der Teams tauschen |
|
||||||
|
| POST | `/admin/scoreSetScore` | `{ team, score }` | Score direkt setzen |
|
||||||
|
| POST | `/admin/scoreAlterScore` | `{ team, dir }` | Score um 1 ändern (`dir`: `"inc"` / `"dec"`) |
|
||||||
|
| POST | `/admin/scoreConfigTeams` | `{ teamA: { name, name2, isSpielgemeinschaft }, teamB: { ... } }` | Teams konfigurieren |
|
||||||
|
|
||||||
|
`team` ist jeweils `"teamA"` oder `"teamB"`.
|
||||||
|
|
||||||
|
Antwortformat `scoreGetValues`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"teamA": { "name": "Team A", "name2": "", "score": 0, "isSpielgemeinschaft": 0 },
|
||||||
|
"teamB": { "name": "Team B", "name2": "", "score": 0, "isSpielgemeinschaft": 0 },
|
||||||
|
"sideswitch": false,
|
||||||
|
"print": "0:0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Abhängigkeiten
|
||||||
|
|
||||||
|
| Paket | Verwendung |
|
||||||
|
|---|---|
|
||||||
|
| `express` | HTTP-Server und Routing |
|
||||||
|
| `socket.io` | WebSocket-Kommunikation |
|
||||||
|
| `moment` | Timer-Berechnung und Zeitformatierung |
|
||||||
|
| `hbs` | Handlebars als View-Engine |
|
||||||
|
| `morgan` | HTTP-Request-Logging |
|
||||||
|
| `nodemon` | Automatischer Server-Neustart bei Dateiänderungen |
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
|
// Einstiegspunkt der Express-Applikation.
|
||||||
|
// Registriert Middleware, View-Engine und Routen, und exportiert die App für bin/www.
|
||||||
|
|
||||||
var createError = require('http-errors');
|
var createError = require('http-errors');
|
||||||
var express = require('express');
|
var express = require('express');
|
||||||
var path = require('path');
|
var path = require('path');
|
||||||
var cookieParser = require('cookie-parser');
|
var cookieParser = require('cookie-parser');
|
||||||
var logger = require('morgan');
|
var logger = require('morgan');
|
||||||
|
|
||||||
var indexRouter = require('./routes/index');
|
var indexRouter = require('./routes/index'); // Öffentliche Monitor-Ansicht
|
||||||
var adminRouter = require('./routes/admin');
|
var adminRouter = require('./routes/admin'); // Admin-Panel (Timer & Scoreboard steuern)
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,23 @@
|
||||||
|
// CLI-Controller (Testdatei)
|
||||||
|
// Zuständig für die Verarbeitung von Kommandozeilen-Befehlen des Scoreboards.
|
||||||
|
// Aktuell nur ein Funktionstest mit einem einfachen `ls`-Befehl.
|
||||||
|
|
||||||
const exec = require('node:child_process');
|
const exec = require('node:child_process');
|
||||||
|
|
||||||
// run the `ls` command using exec
|
// Testaufruf: Listet den Inhalt des aktuellen Verzeichnisses auf,
|
||||||
|
// um zu prüfen, ob child_process.exec korrekt funktioniert.
|
||||||
exec('ls ./', (err, output) => {
|
exec('ls ./', (err, output) => {
|
||||||
// once the command has completed, the callback function is called
|
|
||||||
if (err) {
|
if (err) {
|
||||||
// log and return if we encounter an error
|
|
||||||
console.error("could not execute command: ", err)
|
console.error("could not execute command: ", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// log the output received from the command
|
|
||||||
console.log("Output: \n", output)
|
console.log("Output: \n", output)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ let teamB = {
|
||||||
};
|
};
|
||||||
let sideswitch = false;
|
let sideswitch = false;
|
||||||
|
|
||||||
// Function prototypes
|
// Funktionsübersicht (Prototypen ohne Implementierung — nur zur Übersicht)
|
||||||
function setEnabled(status) {};
|
function setEnabled(status) {};
|
||||||
function configTeam(team, name, name2, isSpielgemeinschaft) {};
|
function configTeam(team, name, name2, isSpielgemeinschaft) {};
|
||||||
function setScore(team, score) {};
|
function setScore(team, score) {};
|
||||||
|
|
@ -30,6 +30,7 @@ function setEnabled(status) {
|
||||||
enabled = status;
|
enabled = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setzt Namen und Spielgemeinschafts-Status eines Teams
|
||||||
function configTeam(team, name, name2, isSpielgemeinschaft) {
|
function configTeam(team, name, name2, isSpielgemeinschaft) {
|
||||||
if(team == "teamA") {
|
if(team == "teamA") {
|
||||||
teamA.name = name;
|
teamA.name = name;
|
||||||
|
|
@ -51,7 +52,7 @@ function setScore(team, score) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment score by one
|
// Ändert den Score eines Teams um 1 — Richtung "inc" (erhöhen) oder "dec" (verringern)
|
||||||
function alterScore(team, dir) {
|
function alterScore(team, dir) {
|
||||||
if(team == "teamA") {
|
if(team == "teamA") {
|
||||||
if(dir == "inc") {
|
if(dir == "inc") {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
// Erstellt den zentralen Socket.IO-Server auf Port 3001.
|
||||||
|
// Wird von timer.js, score.js und den Routen importiert,
|
||||||
|
// um Events (Timer, Score, Refresh) an alle verbundenen Clients zu senden.
|
||||||
|
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
|
|
||||||
|
// CORS auf "*" gesetzt, da Admin und Monitor auf unterschiedlichen Ports laufen können
|
||||||
const io = new Server(3001, {
|
const io = new Server(3001, {
|
||||||
cors: { origin: "*" }
|
cors: { origin: "*" }
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ function updateScoreFrontend(values) {
|
||||||
document.getElementById("score").innerHTML = values.print; // Set score on admin interface
|
document.getElementById("score").innerHTML = values.print; // Set score on admin interface
|
||||||
|
|
||||||
if(!values.sideswitch) {
|
if(!values.sideswitch) {
|
||||||
|
// Normale Seite: teamA links, teamB rechts
|
||||||
if(values.teamA.isSpielgemeinschaft) {
|
if(values.teamA.isSpielgemeinschaft) {
|
||||||
document.getElementById("teamA").innerHTML = values.teamA.name + '<br>' + values.teamA.name2;
|
document.getElementById("teamA").innerHTML = values.teamA.name + '<br>' + values.teamA.name2;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -57,6 +58,7 @@ function updateScoreFrontend(values) {
|
||||||
document.getElementById("teamB").innerHTML = values.teamB.name
|
document.getElementById("teamB").innerHTML = values.teamB.name
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Seitenwechsel nach Halbzeit: teamA und teamB tauschen ihre Anzeigeseite
|
||||||
if(values.teamA.isSpielgemeinschaft) {
|
if(values.teamA.isSpielgemeinschaft) {
|
||||||
document.getElementById("teamB").innerHTML = values.teamA.name + '<br>' + values.teamA.name2;
|
document.getElementById("teamB").innerHTML = values.teamA.name + '<br>' + values.teamA.name2;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -72,6 +74,8 @@ function updateScoreFrontend(values) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wird beim Laden der Seite aufgerufen, um Timer und Score sofort mit dem aktuellen
|
||||||
|
// Serverstand zu synchronisieren (verhindert leere Anzeige nach Seiten-Reload)
|
||||||
async function initialUpdate() {
|
async function initialUpdate() {
|
||||||
timerGetValues(); // Request new values for timer
|
timerGetValues(); // Request new values for timer
|
||||||
scoreGetValues(); // Request new values for score
|
scoreGetValues(); // Request new values for score
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,9 @@ router.post('/timerReset', function(req, res, next) {
|
||||||
res.json(timer.getValues()); // Respond with all important values to update the frontend
|
res.json(timer.getValues()); // Respond with all important values to update the frontend
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Express router endpoint to increase or decrease the timer by a value in seconds
|
||||||
router.post('/timerIncDec', function(req, res, next) {
|
router.post('/timerIncDec', function(req, res, next) {
|
||||||
timer.incDec(req.body.value);
|
timer.incDec(req.body.value); // Positive Werte = erhöhen, negative Werte = verringern
|
||||||
res.json(timer.getValues());
|
res.json(timer.getValues());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -116,6 +117,7 @@ router.get('/scoreClearScore', function(req, res, next) {
|
||||||
res.json(score.getValues()); // Respond with important values for frontend
|
res.json(score.getValues()); // Respond with important values for frontend
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Express router endpoint to configure both teams (name, name2, Spielgemeinschaft-flag)
|
||||||
router.post('/scoreConfigTeams', function(req, res, next) {
|
router.post('/scoreConfigTeams', function(req, res, next) {
|
||||||
score.configTeam("teamA", req.body.teamA.name, req.body.teamA.name2, req.body.teamA.isSpielgemeinschaft);
|
score.configTeam("teamA", req.body.teamA.name, req.body.teamA.name2, req.body.teamA.isSpielgemeinschaft);
|
||||||
score.configTeam("teamB", req.body.teamB.name, req.body.teamB.name2, req.body.teamB.isSpielgemeinschaft);
|
score.configTeam("teamB", req.body.teamB.name, req.body.teamB.name2, req.body.teamB.isSpielgemeinschaft);
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@ router.get('/', function(req, res, next) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Websocket-Verbindungshandler: wird ausgelöst, sobald ein Client (Monitor) sich verbindet
|
||||||
io.on('connection', (socket) => {
|
io.on('connection', (socket) => {
|
||||||
console.log("A user connected");
|
console.log("A user connected");
|
||||||
socket.emit("Hello user from server");
|
socket.emit("Hello user from server"); // Begrüßungsnachricht an den neuen Client
|
||||||
|
|
||||||
|
// Eingehende Nachrichten vom Client loggen (aktuell nur für Debugging)
|
||||||
socket.on('message', (message) => {
|
socket.on('message', (message) => {
|
||||||
console.log(message)
|
console.log(message)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue