tabs for sounds added

This commit is contained in:
oberon 2026-02-15 22:03:15 +01:00
parent 0eacdbf973
commit 702eba2370
6 changed files with 256 additions and 157 deletions

37
app.py
View File

@ -3,10 +3,16 @@ import os
import json import json
import logging import logging
import shutil import shutil
import threading
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from datetime import datetime, timedelta from datetime import datetime, timedelta
from flask import Flask, render_template, redirect, url_for, send_from_directory, request, jsonify from flask import Flask, render_template, redirect, url_for, send_from_directory, request, jsonify
import pygame
# pygame einmalig initialisieren (am besten global)
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
pygame.mixer.music.set_volume(0.8) # Standard 80 %
# Logging-Verzeichnis (muss VOR cleanup_old_log_dirs definiert werden!) # Logging-Verzeichnis (muss VOR cleanup_old_log_dirs definiert werden!)
LOG_DIR = 'logs' LOG_DIR = 'logs'
os.makedirs(LOG_DIR, exist_ok=True) os.makedirs(LOG_DIR, exist_ok=True)
@ -540,12 +546,6 @@ def api_reconnect():
return jsonify({"success": False, "message": str(e)}), 500 return jsonify({"success": False, "message": str(e)}), 500
import pygame
import threading
# pygame einmalig initialisieren (am besten global)
pygame.mixer.init()
@app.route('/api/play_sound', methods=['POST']) @app.route('/api/play_sound', methods=['POST'])
def api_play_sound(): def api_play_sound():
try: try:
@ -588,6 +588,31 @@ def api_play_sound():
return jsonify({"success": False, "message": str(e)}), 500 return jsonify({"success": False, "message": str(e)}), 500
@app.route('/api/set_volume', methods=['POST'])
def api_set_volume():
try:
data = request.get_json()
vol = float(data.get('volume', 0.8))
vol = max(0.0, min(1.0, vol))
pygame.mixer.music.set_volume(vol)
logger.info(f"Volume auf {vol} gesetzt")
return jsonify({"success": True})
except Exception as e:
logger.error(f"Volume-Fehler: {e}")
return jsonify({"success": False, "message": str(e)}), 500
@app.route('/api/stop_sound', methods=['POST'])
def api_stop_sound():
try:
pygame.mixer.music.stop()
logger.info("Aktueller Sound gestoppt")
return jsonify({"success": True})
except Exception as e:
logger.error(f"Stop-Sound-Fehler: {e}")
return jsonify({"success": False, "message": str(e)}), 500
if __name__ == '__main__': if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True) app.run(host='0.0.0.0', port=5000, debug=True)

View File

@ -1,22 +1,16 @@
{ {
"global_sounds": [ "global_sounds": [
{ {
"id": "pfeife_lang", "id": "dampflok-fahrt",
"file": "pfeife_lang.wav", "file": "Dampflok-mit-Waggons-schnauft-heran-und-rollt-vorüber.mp3",
"name": "Pfeife lang", "name": "Dampflok Ankunft und Vorbeifahrt",
"description": "Langgezogene Dampflok-Pfeife" "description": "Dampflok mit Waggons schnauft heran und rollt vorüber"
}, },
{ {
"id": "dampf_ausstoß", "id": "wind_western1",
"file": "dampf_ausstoß.mp3", "file": "freesound_community-wind-western-64661.mp3",
"name": "Dampfausstoß", "name": "Wind Western",
"description": "Kurzes Zischen" "description": "einfache Windgeräusche"
},
{
"id": "bahnhof_ankunft",
"file": "bahnhof_ankunft.mp3",
"name": "Bahnhofsankunft",
"description": "Stationsansage + Bremsquietschen"
} }
] ]
} }

Binary file not shown.

View File

@ -11,6 +11,9 @@ document.addEventListener('DOMContentLoaded', () => {
const stopAllBtn = document.getElementById('stop-all-btn'); const stopAllBtn = document.getElementById('stop-all-btn');
const reconnectSection = document.getElementById('reconnect-section'); const reconnectSection = document.getElementById('reconnect-section');
const reconnectBtn = document.getElementById('reconnect-btn'); const reconnectBtn = document.getElementById('reconnect-btn');
const statusBadge = document.getElementById('connection-status');
const volumeSlider = document.getElementById('volume-slider');
const volumeDisplay = document.getElementById('volume-display');
if (!connectBtn) { if (!connectBtn) {
console.warn('Nicht auf der Steuerseite Connect-Button nicht gefunden'); console.warn('Nicht auf der Steuerseite Connect-Button nicht gefunden');
@ -18,8 +21,68 @@ document.addEventListener('DOMContentLoaded', () => {
} }
// ── Config aus Template ──────────────────────────────────────────────────── // ── Config aus Template ────────────────────────────────────────────────────
const config = window.mkConfig || {}; const config = {{ config | tojson | safe }};
console.log("app.js verwendet config:", config); const globalSounds = {{ global_sounds | tojson | safe }};
window.mkConfig = config; // global für Konsistenz
console.log("Config im JS:", config);
console.log("Globale Sounds:", globalSounds);
// ── Status-Anzeige ─────────────────────────────────────────────────────────
function updateStatus(connected, message = '') {
if (!statusBadge) return;
if (connected) {
statusBadge.className = 'badge bg-success px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Verbunden' + (message ? ` ${message}` : '');
if (reconnectSection) reconnectSection.style.display = 'none';
} else {
statusBadge.className = 'badge bg-danger px-3 py-2 fs-6';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Getrennt' + (message ? ` ${message}` : '');
if (reconnectSection) reconnectSection.style.display = 'block';
}
}
// Initialer Status
updateStatus(false);
// ── Automatische Verbindungsprüfung ────────────────────────────────────────
let connectionCheckInterval = null;
let failedChecks = 0;
const MAX_FAILED_CHECKS = 3;
function startConnectionCheck() {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
connectionCheckInterval = setInterval(async () => {
try {
console.log("→ Status-Check ...");
const res = await fetch('/api/status');
const data = await res.json();
if (data.connected) {
failedChecks = 0;
updateStatus(true);
} else {
failedChecks++;
console.warn(`Status-Check fehlgeschlagen (${failedChecks}/${MAX_FAILED_CHECKS}):`, data.message);
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, data.message || 'Mehrere Checks fehlgeschlagen');
}
}
} catch (err) {
failedChecks++;
console.warn(`Status-Check Netzwerkfehler (${failedChecks}/${MAX_FAILED_CHECKS}):`, err);
if (failedChecks >= MAX_FAILED_CHECKS) {
updateStatus(false, 'Keine Antwort vom Hub/Server');
}
}
}, 6000); // 6 Sekunden
}
window.addEventListener('beforeunload', () => {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
});
// ── Connect-Button ──────────────────────────────────────────────────────── // ── Connect-Button ────────────────────────────────────────────────────────
connectBtn.addEventListener('click', async () => { connectBtn.addEventListener('click', async () => {
@ -43,16 +106,13 @@ document.addEventListener('DOMContentLoaded', () => {
controlSection.style.display = 'block'; controlSection.style.display = 'block';
reconnectSection.style.display = 'none'; reconnectSection.style.display = 'none';
// Status aktualisieren
updateStatus(true); updateStatus(true);
startConnectionCheck();
console.log("→ Rufe renderChannels() auf"); console.log("→ Rufe renderChannels() auf");
renderChannels(); renderChannels();
console.log("→ renderChannels() abgeschlossen"); console.log("→ renderChannels() abgeschlossen");
// Automatische Prüfung starten
startConnectionCheck();
} else { } else {
console.warn("→ Connect fehlgeschlagen:", result.message); console.warn("→ Connect fehlgeschlagen:", result.message);
alert('Verbindung fehlgeschlagen:\n' + (result.message || 'Unbekannter Fehler')); alert('Verbindung fehlgeschlagen:\n' + (result.message || 'Unbekannter Fehler'));
@ -179,7 +239,6 @@ document.addEventListener('DOMContentLoaded', () => {
</div> </div>
`; `;
} else { } else {
// Licht, Sound, Fogger etc. → Toggle
controlHTML = ` controlHTML = `
<label class="form-label fw-bold">${channel.name} (${channel.port})</label> <label class="form-label fw-bold">${channel.name} (${channel.port})</label>
<button class="btn btn-outline-secondary w-100 toggle-btn" <button class="btn btn-outline-secondary w-100 toggle-btn"
@ -275,11 +334,9 @@ document.addEventListener('DOMContentLoaded', () => {
throw new Error(errData.message || 'Steuerbefehl fehlgeschlagen'); throw new Error(errData.message || 'Steuerbefehl fehlgeschlagen');
} }
} catch (err) { } catch (err) {
console.error('sendControl Fehler:', err); console.error('sendControl Fehler:', err);
failedChecks++; // Bei Fehler: Verbindungsprüfung triggern
if (failedChecks >= MAX_FAILED_CHECKS) { updateStatus(false, 'Fehler beim Senden');
updateStatus(false, 'Mehrere Steuerbefehle fehlgeschlagen');
}
} }
} }
@ -291,110 +348,93 @@ document.addEventListener('DOMContentLoaded', () => {
} }
} }
// ── Status-Anzeige ───────────────────────────────────────────────────────── // ── Soundboard Buttons ─────────────────────────────────────────────────────
const statusBadge = document.getElementById('connection-status'); let currentSoundButton = null;
function updateStatus(connected, message = '') { document.querySelectorAll('.play-sound-btn').forEach(btn => {
if (!statusBadge) return; btn.addEventListener('click', async () => {
const soundId = btn.dataset.soundId;
if (connected) { // Alten Play-Button zurücksetzen
statusBadge.className = 'badge bg-success px-3 py-2 fs-6'; if (currentSoundButton && currentSoundButton !== btn) {
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Verbunden'; currentSoundButton.classList.remove('btn-success');
} else { currentSoundButton.classList.add('btn-outline-primary', 'btn-outline-secondary');
statusBadge.className = 'badge bg-danger px-3 py-2 fs-6'; currentSoundButton.innerHTML = currentSoundButton.dataset.originalText || '<i class="bi bi-play-fill me-2"></i> Abspielen';
statusBadge.innerHTML = '<i class="bi bi-circle-fill me-2"></i> Getrennt' + (message ? ` ${message}` : '');
showReconnect();
}
}
// ── Soundboard Buttons ─────────────────────────────────────────────────────
document.querySelectorAll('.play-sound-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const soundId = btn.dataset.soundId;
btn.disabled = true;
const originalText = btn.innerHTML;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Spielt...';
try {
const res = await fetch('/api/play_sound', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sound_id: soundId })
});
const data = await res.json();
if (data.success) {
btn.classList.remove('btn-outline-primary', 'btn-outline-secondary');
btn.classList.add('btn-success');
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i> Gespielt';
setTimeout(() => {
btn.classList.remove('btn-success');
btn.classList.add('btn-outline-primary'); // oder secondary
btn.innerHTML = originalText;
btn.disabled = false;
}, 2000);
} else {
alert('Fehler: ' + (data.message || 'Unbekannt'));
btn.disabled = false;
btn.innerHTML = originalText;
} }
} catch (err) {
console.error('Sound-Play-Fehler:', err);
alert('Netzwerkfehler beim Abspielen');
btn.disabled = false;
btn.innerHTML = originalText;
}
});
});
// Initialer Status btn.disabled = true;
updateStatus(false); const originalText = btn.innerHTML;
btn.dataset.originalText = originalText;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Spielt...';
// ── Automatische Verbindungsprüfung ──────────────────────────────────────── try {
let connectionCheckInterval = null; const res = await fetch('/api/play_sound', {
let failedChecks = 0; method: 'POST',
const MAX_FAILED_CHECKS = 3; // erst nach 3 Fehlschlägen in Folge rot headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sound_id: soundId })
});
function startConnectionCheck() {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
connectionCheckInterval = setInterval(async () => {
try {
console.log("→ Status-Check ...");
const res = await fetch('/api/status');
const data = await res.json(); const data = await res.json();
if (data.connected) { if (data.success) {
failedChecks = 0; btn.classList.remove('btn-outline-primary', 'btn-outline-secondary');
updateStatus(true); btn.classList.add('btn-success');
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i> Gespielt';
currentSoundButton = btn;
} else { } else {
failedChecks++; alert('Fehler: ' + (data.message || 'Unbekannt'));
console.warn(`Status-Check fehlgeschlagen (${failedChecks}/${MAX_FAILED_CHECKS}):`, data.message); btn.innerHTML = originalText;
if (failedChecks >= MAX_FAILED_CHECKS) { btn.disabled = false;
updateStatus(false, data.message || 'Mehrere Checks fehlgeschlagen');
}
} }
} catch (err) { } catch (err) {
failedChecks++; console.error('Sound-Play-Fehler:', err);
console.warn(`Status-Check Netzwerkfehler (${failedChecks}/${MAX_FAILED_CHECKS}):`, err); alert('Netzwerkfehler beim Abspielen');
if (failedChecks >= MAX_FAILED_CHECKS) { btn.innerHTML = originalText;
updateStatus(false, 'Keine Antwort vom Hub/Server'); btn.disabled = false;
} }
} });
}, 6000); // 6 Sekunden aggressiver als 8 s
}
// In Connect- und Reconnect-Handler nach erfolgreichem Connect/Reconnect:
startConnectionCheck();
updateStatus(true);
window.addEventListener('beforeunload', () => {
if (connectionCheckInterval) clearInterval(connectionCheckInterval);
}); });
// Optional: Bei Seitenlade den Check starten (falls schon verbunden) // ── Stop-Button für aktuellen Sound
// startConnectionCheck(); // ← auskommentiert, da nach Connect gestartet document.querySelectorAll('.stop-sound-btn').forEach(btn => {
btn.addEventListener('click', async () => {
try {
const res = await fetch('/api/stop_sound', { method: 'POST' });
const data = await res.json();
if (data.success) {
if (currentSoundButton) {
currentSoundButton.classList.remove('btn-success');
currentSoundButton.classList.add('btn-outline-primary', 'btn-outline-secondary');
currentSoundButton.innerHTML = currentSoundButton.dataset.originalText || '<i class="bi bi-play-fill me-2"></i> Abspielen';
currentSoundButton.disabled = false;
currentSoundButton = null;
}
console.log('Sound gestoppt');
} else {
alert('Fehler: ' + data.message);
}
} catch (err) {
console.error('Stop-Sound-Fehler:', err);
alert('Netzwerkfehler beim Stoppen');
}
});
});
// ── Volume-Regler
if (volumeSlider && volumeDisplay) {
volumeSlider.addEventListener('input', () => {
const vol = volumeSlider.value / 100;
volumeDisplay.textContent = `${volumeSlider.value} %`;
try {
fetch('/api/set_volume', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ volume: vol })
}).catch(err => console.error('Volume-Set-Fehler:', err));
} catch (err) {
console.error('Volume-Fehler:', err);
}
});
}
}); });

View File

@ -14,7 +14,7 @@
</div> </div>
<div class="row"> <div class="row">
<!-- Linke Spalte: Kanal-Steuerung (8/12 auf lg) --> <!-- Linke Spalte: Kanal-Steuerung (8/12) -->
<div class="col-lg-8"> <div class="col-lg-8">
<div class="row mb-4 align-items-center"> <div class="row mb-4 align-items-center">
<div class="col-md-8"> <div class="col-md-8">
@ -65,7 +65,7 @@
</div> </div>
</div> </div>
<!-- Reconnect-Bereich wird nur bei Verbindungsverlust eingeblendet --> <!-- Reconnect-Bereich -->
<div id="reconnect-section" class="text-center mt-5" style="display: none;"> <div id="reconnect-section" class="text-center mt-5" style="display: none;">
<div class="alert alert-warning"> <div class="alert alert-warning">
<strong>Verbindung unterbrochen</strong><br> <strong>Verbindung unterbrochen</strong><br>
@ -77,45 +77,82 @@
</div> </div>
</div> </div>
<!-- Rechte Spalte: Soundboard (4/12 auf lg) --> <!-- Rechte Spalte: Soundboard (4/12) -->
<div class="col-lg-4"> <div class="col-lg-4">
<div class="card shadow sticky-top" style="top: 20px;"> <div class="card shadow sticky-top" style="top: 20px;">
<div class="card-header bg-info text-white"> <div class="card-header bg-info text-white">
<h5 class="mb-0">Soundboard</h5> <h5 class="mb-0">Soundboard</h5>
</div> </div>
<div class="card-body" style="max-height: 70vh; overflow-y: auto;"> <div class="card-body">
<!-- Lok-spezifische Sounds --> <!-- Volume-Regler (global) -->
{% if config.sounds and config.sounds|length > 0 %} <div class="mb-4">
<h6 class="mt-0 mb-3">Lok-spezifisch</h6> <label for="volume-slider" class="form-label fw-bold">Lautstärke</label>
<div class="d-grid gap-2 mb-4"> <input type="range" class="form-range" id="volume-slider" min="0" max="100" value="80" step="5">
{% for sound in config.sounds %} <div class="text-center mt-1">
<button class="btn btn-outline-primary play-sound-btn" <span id="volume-display">80 %</span>
data-sound-id="{{ sound.id }}">
{{ sound.name }}
{% if sound.description %}
<small class="d-block text-muted">{{ sound.description }}</small>
{% endif %}
</button>
{% endfor %}
</div> </div>
{% endif %} </div>
<!-- Globale / Standard-Sounds --> <!-- Tabs -->
{% if global_sounds and global_sounds|length > 0 %} <ul class="nav nav-tabs mb-3" id="soundTabs" role="tablist">
<h6 class="mt-4 mb-3">Standard-Sounds</h6> {% if config.sounds and config.sounds|length > 0 %}
<div class="d-grid gap-2"> <li class="nav-item" role="presentation">
{% for sound in global_sounds %} <button class="nav-link active" id="lok-tab" data-bs-toggle="tab" data-bs-target="#lok-sounds" type="button" role="tab">
<button class="btn btn-outline-secondary play-sound-btn" Lok-spezifisch
data-sound-id="{{ sound.id }}">
{{ sound.name }}
{% if sound.description %}
<small class="d-block text-muted">{{ sound.description }}</small>
{% endif %}
</button> </button>
{% endfor %} </li>
{% endif %}
<li class="nav-item" role="presentation">
<button class="nav-link {% if not config.sounds or config.sounds|length == 0 %}active{% endif %}" id="global-tab" data-bs-toggle="tab" data-bs-target="#global-sounds" type="button" role="tab">
Standard-Sounds
</button>
</li>
</ul>
<div class="tab-content" id="soundTabContent" style="max-height: 50vh; overflow-y: auto;">
<!-- Lok-spezifische Sounds -->
{% if config.sounds and config.sounds|length > 0 %}
<div class="tab-pane fade show active" id="lok-sounds" role="tabpanel">
<div class="d-grid gap-2">
{% for sound in config.sounds %}
<div class="d-flex gap-2">
<button class="btn btn-outline-primary flex-grow-1 play-sound-btn"
data-sound-id="{{ sound.id }}">
{{ sound.name }}
{% if sound.description %}
<small class="d-block text-muted">{{ sound.description }}</small>
{% endif %}
</button>
<button class="btn btn-outline-danger stop-sound-btn" title="Aktuellen Sound stoppen">
<i class="bi bi-stop-fill"></i>
</button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Globale Standard-Sounds -->
<div class="tab-pane fade {% if not config.sounds or config.sounds|length == 0 %}show active{% endif %}" id="global-sounds" role="tabpanel">
<div class="d-grid gap-2">
{% for sound in global_sounds %}
<div class="d-flex gap-2">
<button class="btn btn-outline-secondary flex-grow-1 play-sound-btn"
data-sound-id="{{ sound.id }}">
{{ sound.name }}
{% if sound.description %}
<small class="d-block text-muted">{{ sound.description }}</small>
{% endif %}
</button>
<button class="btn btn-outline-danger stop-sound-btn" title="Aktuellen Sound stoppen">
<i class="bi bi-stop-fill"></i>
</button>
</div>
{% endfor %}
</div>
</div> </div>
{% endif %} </div>
{% if (not config.sounds or config.sounds|length == 0) and (not global_sounds or global_sounds|length == 0) %} {% if (not config.sounds or config.sounds|length == 0) and (not global_sounds or global_sounds|length == 0) %}
<p class="text-muted text-center py-4">Keine Sounds konfiguriert</p> <p class="text-muted text-center py-4">Keine Sounds konfiguriert</p>
@ -134,5 +171,8 @@
const config = {{ config | tojson | safe }}; const config = {{ config | tojson | safe }};
const globalSounds = {{ global_sounds | tojson | safe }}; const globalSounds = {{ global_sounds | tojson | safe }};
window.mkConfig = config; // falls du es global brauchst window.mkConfig = config; // falls du es global brauchst
console.log("Config im JS:", config);
console.log("Globale Sounds:", globalSounds);
</script> </script>
{% endblock %} {% endblock %}