commit ce1095bb24ac57d91b59d47398e87ad59db23964 Author: rainer Date: Tue Apr 29 23:36:37 2025 +0200 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/app/init.py b/app/init.py new file mode 100644 index 0000000..9337bd8 --- /dev/null +++ b/app/init.py @@ -0,0 +1,10 @@ +from flask import Flask + +def create_app(): + app = Flask(__name__) + app.secret_key = "rennstopuhr2025" + + from app.routes import main as main_blueprint + app.register_blueprint(main_blueprint) + + return app diff --git a/app/routes.py b/app/routes.py new file mode 100644 index 0000000..ecc0ea4 --- /dev/null +++ b/app/routes.py @@ -0,0 +1,99 @@ +from flask import Blueprint, render_template, request, redirect, url_for, jsonify +import json +import os +from app.utils import gpio_handler, countdown, zeitmessung, system_tools + +main = Blueprint("main", __name__) + +@main.route("/") +def index(): + return render_template("index.html") + +@main.route("/rennen_starten") +def rennen_starten(): + with open("config/fahrer.json") as f: + fahrer = json.load(f) + return render_template("rennen_starten.html", fahrer=fahrer) + +@main.route("/countdown_starten", methods=["POST"]) +def countdown_starten(): + countdown.start() + return redirect(url_for("main.rennansicht")) + +@main.route("/rennansicht") +def rennansicht(): + with open("config/fahrer.json") as f: + fahrer = json.load(f) + return render_template("rennbildschirm.html", fahrer=fahrer) + +@main.route("/fahrerverwaltung") +def fahrerverwaltung(): + with open("config/fahrer.json") as f: + fahrer = json.load(f) + return render_template("fahrerverwaltung.html", fahrer=fahrer) + +@main.route("/fahrerverwaltung/speichern", methods=["POST"]) +def fahrer_speichern(): + data = request.get_json() + with open("config/fahrer.json", "w") as f: + json.dump(data, f, indent=2) + return jsonify({"status": "ok"}) + +@main.route("/admin") +def admin(): + return render_template("admin.html") + +@main.route("/zeit_zuordnen") +def zeit_zuordnen(): + with open("data/zeiten.json") as f: + zeiten = json.load(f) + with open("config/fahrer.json") as f: + fahrer = json.load(f) + return render_template("zeit_zuordnen.html", zeiten=zeiten, fahrer=fahrer) + +@main.route("/zuordnung/speichern", methods=["POST"]) +def zuordnung_speichern(): + data = request.get_json() + with open("data/zuordnung.json", "w") as f: + json.dump(data, f, indent=2) + return jsonify({"status": "ok"}) + +@main.route("/ergebnis") +def ergebnis(): + zuordnung_path = "data/zuordnung.json" + fahrer_path = "config/fahrer.json" + + with open(fahrer_path) as f: + fahrer_list = json.load(f) + + if os.path.exists(zuordnung_path): + with open(zuordnung_path) as f: + zuordnungen = json.load(f) + else: + zuordnungen = [] + + fahrer_dict = {str(f["nummer"]): {"name": f["name"], "zeiten": [], "farbe": f["farbe"]} for f in fahrer_list} + + for eintrag in zuordnungen: + fahrer_id = str(eintrag["fahrer"]) + zeit = float(eintrag["zeit"]) + if fahrer_id in fahrer_dict: + fahrer_dict[fahrer_id]["zeiten"].append(zeit) + + ergebnisse = [] + for nummer, daten in fahrer_dict.items(): + zeiten = daten["zeiten"] + if zeiten: + beste = min(zeiten) + durchschnitt = sum(zeiten) / len(zeiten) + gesamt = sum(zeiten) + ergebnisse.append({ + "nummer": nummer, + "name": daten["name"], + "farbe": daten["farbe"], + "beste": f"{beste:.3f}", + "durchschnitt": f"{durchschnitt:.3f}", + "gesamt": f"{gesamt:.3f}" + }) + + return render_template("ergebnis.html", ergebnisse=ergebnisse) diff --git a/app/static/css/styles.css b/app/static/css/styles.css new file mode 100644 index 0000000..0d61fa3 --- /dev/null +++ b/app/static/css/styles.css @@ -0,0 +1,102 @@ +body { + font-family: Arial, sans-serif; + margin: 20px; + background-color: #f7f7f7; + color: #333; +} + +h1, h2, h3 { + color: #222; +} + +form { + margin-bottom: 20px; +} + +input, select, button { + padding: 8px; + margin: 5px; + font-size: 1em; +} + +button { + border: none; + border-radius: 6px; + padding: 10px 20px; + cursor: pointer; +} + +button.green { + background-color: #4CAF50; + color: white; +} + +button.red { + background-color: #e53935; + color: white; +} + +button.orange { + background-color: orange; + color: white; +} + +.fahrer-eintrag { + margin: 5px 0; +} + +.licht { + display: inline-block; + width: 80px; + height: 80px; + margin: 10px; + border-radius: 50%; + background-color: grey; + opacity: 0.4; +} + +.licht.rot.aktiv { + background-color: red; + opacity: 1; +} + +.licht.gruen { + background-color: green !important; + opacity: 1 !important; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(10, 10, 10, 0.9); + z-index: 1000; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + color: white; + font-size: 2em; +} + +.ampel-klein { + display: flex; + justify-content: center; + margin: 10px auto; + height: 100px; +} + +.ergebnis { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.fahrerbox { + padding: 20px; + border-radius: 10px; + color: white; + flex: 1 1 200px; +} diff --git a/app/static/js/admin.js b/app/static/js/admin.js new file mode 100644 index 0000000..7c58fe8 --- /dev/null +++ b/app/static/js/admin.js @@ -0,0 +1,30 @@ +function systemCommand(cmd) { + fetch(`/admin/${cmd}`, { method: "POST" }) + .then(res => res.text()) + .then(alert); +} + +function fetchLogs() { + fetch("/admin/logs") + .then(res => res.text()) + .then(data => { + document.getElementById("logs").innerText = data; + }); +} + +document.getElementById("adminForm").addEventListener("submit", function(e) { + e.preventDefault(); + const data = new FormData(e.target); + const json = {}; + for (let [k, v] of data.entries()) { + json[k] = v; + } + json["ziel_aktiviert"] = data.get("ziel_aktiviert") === "on"; + + fetch("/admin/save", { + method: "POST", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(json) + }).then(res => res.text()) + .then(alert); +}); diff --git a/app/static/js/fahrer.js b/app/static/js/fahrer.js new file mode 100644 index 0000000..00806f0 --- /dev/null +++ b/app/static/js/fahrer.js @@ -0,0 +1,17 @@ +function addFahrer() { + const fahrerListe = document.getElementById("fahrerListe"); + const index = fahrerListe.children.length + 1; + const farben = ["#ff0000", "#00ff00", "#0000ff", "#ffa500"]; + const neueFarbe = farben[index % farben.length]; + + const div = document.createElement("div"); + div.className = "fahrer-eintrag"; + div.innerHTML = ` + + `; + fahrerListe.appendChild(div); +} diff --git a/app/static/js/main.js b/app/static/js/main.js new file mode 100644 index 0000000..b6d9447 --- /dev/null +++ b/app/static/js/main.js @@ -0,0 +1,31 @@ +function startCountdown() { + const beep = new Audio("/static/sounds/beep.wav"); + const go = new Audio("/static/sounds/go.wav"); + + setTimeout(() => { + document.getElementById("licht1").classList.add("aktiv"); + beep.play(); + }, 1000); + + setTimeout(() => { + document.getElementById("licht2").classList.add("aktiv"); + beep.play(); + }, 2000); + + setTimeout(() => { + document.getElementById("licht3").classList.add("aktiv"); + beep.play(); + }, 3000); + + setTimeout(() => { + document.querySelectorAll(".licht").forEach(el => { + el.classList.remove("rot"); + el.classList.add("gruen"); + }); + go.play(); + }, 4000); + + setTimeout(() => { + window.location.href = "/rennen"; + }, 7000); +} diff --git a/app/static/js/zuordnen.js b/app/static/js/zuordnen.js new file mode 100644 index 0000000..2285de2 --- /dev/null +++ b/app/static/js/zuordnen.js @@ -0,0 +1,20 @@ +document.getElementById("zuordnungForm").addEventListener("submit", function(e) { + e.preventDefault(); + const zuordnungen = []; + + document.querySelectorAll(".zuordnung").forEach(row => { + const zeit = row.querySelector("span").innerText; + const fahrer = row.querySelector("select").value; + if (fahrer) { + zuordnungen.push({ zeit: zeit, fahrer: fahrer }); + } + }); + + fetch("/zeit_zuordnen", { + method: "POST", + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ zuordnungen: zuordnungen }) + }).then(() => { + window.location.href = "/ergebnis"; + }); +}); diff --git a/app/static/sounds/beep.wav b/app/static/sounds/beep.wav new file mode 100644 index 0000000..934b53f Binary files /dev/null and b/app/static/sounds/beep.wav differ diff --git a/app/static/sounds/go.wav b/app/static/sounds/go.wav new file mode 100644 index 0000000..ecc8dac Binary files /dev/null and b/app/static/sounds/go.wav differ diff --git a/app/templates/admin.html b/app/templates/admin.html new file mode 100644 index 0000000..1fbb8a6 --- /dev/null +++ b/app/templates/admin.html @@ -0,0 +1,37 @@ + + + + Admin + + + + +

Admin-Einstellungen

+
+
+
+
+

+ +
+
+

Systemsteuerung

+ + + + +

+
+
diff --git a/app/templates/countdown.html b/app/templates/countdown.html
new file mode 100644
index 0000000..74180f8
--- /dev/null
+++ b/app/templates/countdown.html
@@ -0,0 +1,18 @@
+
+
+
+    Countdown
+    
+    
+
+
+    
+
+
+
+
+ + + diff --git a/app/templates/ergebnis.html b/app/templates/ergebnis.html new file mode 100644 index 0000000..0e652b5 --- /dev/null +++ b/app/templates/ergebnis.html @@ -0,0 +1,20 @@ + + + + Ergebnis + + + +

🏁 Ergebnis

+
+ {% for e in ergebnisse %} +
+

#{{ e.nummer }} - {{ e.name }}

+

Beste Zeit: {{ e.beste }} s

+

Durchschnitt: {{ e.durchschnitt }} s

+

Gesamtzeit: {{ e.gesamt }} s

+
+ {% endfor %} +
+ + diff --git a/app/templates/fahrerverwaltung.html b/app/templates/fahrerverwaltung.html new file mode 100644 index 0000000..e6b82f3 --- /dev/null +++ b/app/templates/fahrerverwaltung.html @@ -0,0 +1,27 @@ + + + + Fahrerverwaltung + + + + +

Fahrer verwalten

+
+
+ {% for f in fahrer %} +
+ +
+ {% endfor %} +
+ + +
+ Zurück + + diff --git a/app/templates/index.html b/app/templates/index.html new file mode 100644 index 0000000..14120c9 --- /dev/null +++ b/app/templates/index.html @@ -0,0 +1,15 @@ + + + + Rennstoppuhr + + + +

Rennstoppuhr

+ + + diff --git a/app/templates/rennbildschirm.html b/app/templates/rennbildschirm.html new file mode 100644 index 0000000..e8af3ba --- /dev/null +++ b/app/templates/rennbildschirm.html @@ -0,0 +1,29 @@ + + + + Rennbildschirm + + + +
+
+

Aktuelle Runde: --.--

+

Beste Runde: --.--

+

Durchschnitt: --.--

+
+
+ Runde 1 / 3 +
+
+ {% for f in fahrer %} +
+ #{{ f.nummer }} - {{ f.name }} +
+ {% endfor %} +
+
+ + +
+ + diff --git a/app/templates/rennen_starten.html b/app/templates/rennen_starten.html new file mode 100644 index 0000000..4ce1b93 --- /dev/null +++ b/app/templates/rennen_starten.html @@ -0,0 +1,17 @@ + + + + Rennen starten + + + +

Rennen starten

+
+ + +
+

Zur Fahrerverwaltung

+ + diff --git a/app/templates/zeit_zuordnen.html b/app/templates/zeit_zuordnen.html new file mode 100644 index 0000000..6f28974 --- /dev/null +++ b/app/templates/zeit_zuordnen.html @@ -0,0 +1,25 @@ + + + + Zeit zuordnen + + + + +

Zeit zuordnen

+
+ {% for z in zeiten %} +
+ {{ z.zeit }} + +
+ {% endfor %} + +
+ + diff --git a/app/utils/countdown.py b/app/utils/countdown.py new file mode 100644 index 0000000..d1337e2 --- /dev/null +++ b/app/utils/countdown.py @@ -0,0 +1,17 @@ +import time +import threading +import pygame + +def play_sound(path): + pygame.mixer.init() + sound = pygame.mixer.Sound(path) + sound.play() + time.sleep(sound.get_length()) + +def start(): + def countdown_thread(): + for i in range(3): + play_sound("app/static/sounds/beep.wav") + time.sleep(1) + play_sound("app/static/sounds/go.wav") + threading.Thread(target=countdown_thread).start() diff --git a/app/utils/gpio_handler.py b/app/utils/gpio_handler.py new file mode 100644 index 0000000..8e740a8 --- /dev/null +++ b/app/utils/gpio_handler.py @@ -0,0 +1,21 @@ +from gpiozero import Button +from signal import pause +import time + +class Lichtschranke: + def __init__(self, gpio_pin, callback, schutzzeit=3): + self.pin = gpio_pin + self.callback = callback + self.last_trigger = 0 + self.button = Button(gpio_pin, pull_up=False, bounce_time=0.05) + self.button.when_pressed = self._ausgeloest + self.schutzzeit = schutzzeit + + def _ausgeloest(self): + jetzt = time.time() + if jetzt - self.last_trigger > self.schutzzeit: + self.last_trigger = jetzt + self.callback() + + def stop(self): + self.button.close() diff --git a/app/utils/init.py b/app/utils/init.py new file mode 100644 index 0000000..86212e3 --- /dev/null +++ b/app/utils/init.py @@ -0,0 +1 @@ +# leer diff --git a/app/utils/system_tools.py b/app/utils/system_tools.py new file mode 100644 index 0000000..bf2381b --- /dev/null +++ b/app/utils/system_tools.py @@ -0,0 +1,14 @@ +import subprocess + +def reboot(): + subprocess.run(["sudo", "reboot"]) + +def shutdown(): + subprocess.run(["sudo", "shutdown", "now"]) + +def restart_service(): + subprocess.run(["sudo", "systemctl", "restart", "rennstopuhr.service"]) + +def get_logs(): + result = subprocess.run(["journalctl", "-u", "rennstopuhr.service", "--no-pager"], capture_output=True, text=True) + return result.stdout diff --git a/app/utils/zeitmessung.py b/app/utils/zeitmessung.py new file mode 100644 index 0000000..c2befaf --- /dev/null +++ b/app/utils/zeitmessung.py @@ -0,0 +1,22 @@ +import time + +class Zeitmesser: + def __init__(self): + self.startzeit = None + self.runden = [] + + def start(self): + self.startzeit = time.perf_counter() + + def runde(self): + if self.startzeit is None: + return None + jetzt = time.perf_counter() + rundenzeit = jetzt - self.startzeit + self.runden.append(rundenzeit) + self.startzeit = jetzt + return rundenzeit + + def reset(self): + self.startzeit = None + self.runden = [] diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..2387685 --- /dev/null +++ b/install.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +echo "📦 Projekt wird installiert..." + +# System aktualisieren +sudo apt update && sudo apt upgrade -y +sudo apt install python3-venv git -y + +# Projekt klonen +git clone https://github.com/DEIN_GIT_REPO/pi-race-timer.git /home/pi/pi-race-timer +cd /home/pi/pi-race-timer + +# Virtuelle Umgebung einrichten +python3 -m venv venv +source venv/bin/activate + +# Python-Abhängigkeiten installieren +pip install -U pip +pip install -r requirements.txt + +# systemd-Dienst einrichten +sudo cp pi-race-timer.service /etc/systemd/system/ +sudo systemctl daemon-reexec +sudo systemctl enable pi-race-timer +sudo systemctl start pi-race-timer + +echo "✅ Installation abgeschlossen. Webinterface erreichbar unter: http://:5000" diff --git a/pi-race-timer.service b/pi-race-timer.service new file mode 100644 index 0000000..38ba75f --- /dev/null +++ b/pi-race-timer.service @@ -0,0 +1,12 @@ +[Unit] +Description=Race Timer Webserver +After=network.target + +[Service] +ExecStart=/home/pi/pi-race-timer/venv/bin/python3 /home/pi/pi-race-timer/main.py +WorkingDirectory=/home/pi/pi-race-timer +Restart=always +User=pi + +[Install] +WantedBy=multi-user.target diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0af968d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +setuptools +wheel +Flask==2.3.3 +gpiozero==1.6.2 diff --git a/run.py b/run.py new file mode 100644 index 0000000..8a342e8 --- /dev/null +++ b/run.py @@ -0,0 +1,6 @@ +from app import create_app + +app = create_app() + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=False)