modularisierung von app.py und app.js - erster versuch

This commit is contained in:
2026-02-16 13:57:57 +01:00
parent f6fba252e4
commit 174e64e480
17 changed files with 1341 additions and 629 deletions
View File
+108
View File
@@ -0,0 +1,108 @@
# app/routes/admin.py
from flask import Blueprint, render_template, request, jsonify, redirect, url_for
from .. import current_config
from app.utils.helpers import load_configs, load_default_sounds
admin_bp = Blueprint('admin', __name__)
@admin_bp.route('/')
def admin():
configs = load_configs()
return render_template('admin.html', configs=configs)
@app.route('/edit/<filename>', methods=['GET', 'POST'])
def admin_edit_config(filename):
path = os.path.join(app.config['CONFIG_DIR'], filename)
if request.method == 'POST':
try:
new_content = request.form.get('config_content')
if not new_content:
raise ValueError("Kein Inhalt übermittelt")
# Validierung: versuche zu parsen
json.loads(new_content)
with open(path, 'w', encoding='utf-8') as f:
f.write(new_content)
logger.info(f"Config {filename} erfolgreich gespeichert")
return redirect(url_for('admin'))
except json.JSONDecodeError as e:
logger.error(f"Ungültiges JSON in {filename}: {e}")
error_msg = f"Ungültiges JSON: {str(e)}"
except Exception as e:
logger.error(f"Speicherfehler {filename}: {e}")
error_msg = f"Fehler beim Speichern: {str(e)}"
# Bei Fehler: zurück zum Formular mit Fehlermeldung
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
return render_template('admin_edit.html', filename=filename, content=content, error=error_msg)
# GET: Formular laden
if not os.path.exists(path):
return "Konfiguration nicht gefunden", 404
with open(path, 'r', encoding='utf-8') as f:
content = f.read()
return render_template('admin_edit.html', filename=filename, content=content)
pass
@app.route('/delete/<filename>', methods=['POST'])
def admin_delete_config(filename):
path = os.path.join(app.config['CONFIG_DIR'], filename)
if os.path.exists(path):
try:
os.remove(path)
logger.info(f"Config gelöscht: {filename}")
return jsonify({"success": True, "message": f"{filename} gelöscht"})
except Exception as e:
logger.error(f"Löschfehler {filename}: {e}")
return jsonify({"success": False, "message": str(e)}), 500
return jsonify({"success": False, "message": "Datei nicht gefunden"}), 404
pass
@app.route('/logs')
@app.route('/logs/<date>')
def admin_logs(date=None):
if date is None:
date = datetime.now().strftime('%d')
today = datetime.now().strftime('%Y-%m') # ← Hier definieren!
current_month = datetime.now().strftime('%Y-%m')
log_dir = os.path.join(LOG_DIR, today)
if not os.path.exists(log_dir):
return render_template('admin_logs.html', logs="Keine Logs für diesen Tag.", date=date, dates=[], today=today)
# Verfügbare Tage ...
dates = sorted([
f.split('-')[0] for f in os.listdir(log_dir)
if f.endswith('-info.log') or f.endswith('-error.log')
], reverse=True)
# Logs laden ...
info_path = os.path.join(log_dir, f"{date}-info.log")
error_path = os.path.join(log_dir, f"{date}-error.log")
logs = []
if os.path.exists(info_path):
with open(info_path, 'r', encoding='utf-8') as f:
logs.append("=== INFO-LOG ===\n" + f.read())
if os.path.exists(error_path):
with open(error_path, 'r', encoding='utf-8') as f:
logs.append("=== ERROR-LOG ===\n" + f.read())
log_content = "\n\n".join(logs) if logs else "Keine Logs für diesen Tag."
return render_template('admin_logs.html',
logs=log_content,
date=date,
dates=dates,
today=current_month)
pass
+258
View File
@@ -0,0 +1,258 @@
# app/routes/api.py
from flask import Blueprint, jsonify, request
from .. import current_device, current_config, pygame
api_bp = Blueprint('api', __name__)
@app.route('/connect', methods=['POST'])
def api_connect():
global current_hub, current_module, current_device
if current_config is None:
return jsonify({"success": False, "message": "Keine Konfiguration geladen"}), 400
try:
hub_id = int(current_config.get('hub_id', 0))
hub_type = current_config.get('hub_type', '6channel') # '4channel' oder '6channel'
# Alte Verbindung sauber trennen
if current_device is not None:
try:
current_device.Disconnect()
except:
pass
if current_module is not None:
# ggf. Stop vor Disconnect
try:
current_device.Stop()
except:
pass
# MouldKing neu initialisieren
mk = MouldKing()
mk.SetAdvertiser(advertiser)
try:
mk.SetTracer(tracer)
except:
pass
# Modul holen
if hub_type.lower() in ['6channel', '6']:
current_module = mk.Module6_0()
else:
current_module = mk.Module4_0()
# Device auswählen (hub_id = 0,1,2 → Device0,1,2)
device_attr = f'Device{hub_id}'
if not hasattr(current_module, device_attr):
raise ValueError(f"Hub-ID {hub_id} nicht unterstützt (max 0-2)")
current_device = getattr(current_module, device_attr)
# Verbinden
current_device.Connect()
current_hub = mk # falls später noch gebraucht
logger.info(f"Verbunden: {hub_type} Hub-ID {hub_id}{device_attr}")
return jsonify({"success": True, "message": f"Hub {hub_id} ({hub_type}) verbunden"})
except Exception as e:
logger.exception("Connect-Fehler")
return jsonify({"success": False, "message": f"Verbindungsfehler: {str(e)}"}), 500
pass
@app.route('/reconnect', methods=['POST'])
def api_reconnect():
global current_device, current_module, current_hub
if current_config is None:
return jsonify({"success": False, "message": "Keine Konfiguration geladen"}), 400
try:
# Alte Verbindung sauber beenden, falls nötig
if current_device is not None:
try:
current_device.Disconnect()
except:
pass
# Neu verbinden (kopierter Code aus api_connect)
hub_id = int(current_config.get('hub_id', 0))
hub_type = current_config.get('hub_type', '6channel')
mk = MouldKing()
mk.SetAdvertiser(advertiser)
try:
mk.SetTracer(tracer)
except:
pass
if hub_type.lower() in ['6channel', '6']:
current_module = mk.Module6_0()
else:
current_module = mk.Module4_0()
device_attr = f'Device{hub_id}'
if not hasattr(current_module, device_attr):
raise ValueError(f"Hub-ID {hub_id} nicht unterstützt")
current_device = getattr(current_module, device_attr)
current_device.Connect()
current_hub = mk
logger.info(f"Re-Connect erfolgreich: Hub {hub_id}")
return jsonify({"success": True, "message": "Verbindung wiederhergestellt"})
except Exception as e:
logger.exception("Re-Connect-Fehler")
return jsonify({"success": False, "message": str(e)}), 500
pass
@app.route('/control', methods=['POST'])
def api_control():
global current_device
if current_device is None:
return jsonify({"success": False, "message": "Nicht verbunden"}), 400
try:
data = request.get_json()
port = data['port'] # 'A', 'B', ...
value = float(data['value'])
# Port → channelId mappen (A=0, B=1, ...)
channel_map = {'A':0, 'B':1, 'C':2, 'D':3, 'E':4, 'F':5}
if isinstance(port, str):
port_upper = port.upper()
if port_upper in channel_map:
channel_id = channel_map[port_upper]
else:
raise ValueError(f"Ungültiger Port: {port}")
else:
channel_id = int(port)
# Config-spezifische Anpassungen (invert, negative_only, on/off-Werte)
ch_cfg = next((c for c in current_config['channels'] if c['port'].upper() == port.upper()), None)
if ch_cfg:
if ch_cfg.get('invert', False):
value = -value
if ch_cfg.get('negative_only', False) and value >= 0:
value = -abs(value)
if ch_cfg['type'] != 'motor':
value = ch_cfg.get('on_value', 1.0) if value != 0 else ch_cfg.get('off_value', 0.0)
# Echter Aufruf!
current_device.SetChannel(channel_id, value)
logger.info(f"SetChannel({channel_id}, {value:.2f}) → Port {port}")
return jsonify({"success": True})
except Exception as e:
logger.exception("Control-Fehler")
return jsonify({"success": False, "message": str(e)}), 500
pass
@app.route('/stop_all', methods=['POST'])
def api_stop_all():
global current_device
if current_device is None:
return jsonify({"success": False, "message": "Nicht verbunden"}), 400
try:
# Nur stoppen KEIN Disconnect!
current_device.Stop()
logger.info("Alle Kanäle gestoppt (Verbindung bleibt bestehen)")
return jsonify({"success": True, "message": "Alle Kanäle gestoppt"})
except Exception as e:
logger.exception("Stop-Fehler")
return jsonify({"success": False, "message": str(e)}), 500
pass
@app.route('/status', methods=['GET'])
def api_status():
global current_device
if current_device is not None:
return jsonify({
"connected": True,
"message": "Verbunden"
})
else:
return jsonify({
"connected": False,
"message": "Keine aktive Verbindung"
}), 200
pass
@app.route('/play_sound', methods=['POST'])
def api_play_sound():
try:
data = request.get_json()
sound_id = data.get('sound_id')
if not sound_id:
return jsonify({"success": False, "message": "Kein sound_id angegeben"}), 400
# Sound aus aktueller Config suchen
if current_config is None or 'sounds' not in current_config:
return jsonify({"success": False, "message": "Keine Sounds in der aktuellen Konfiguration"}), 400
sound_entry = next((s for s in current_config['sounds'] if s['id'] == sound_id), None)
if not sound_entry:
return jsonify({"success": False, "message": f"Sound mit ID '{sound_id}' nicht gefunden"}), 404
file_path = os.path.join('sounds', sound_entry['file'])
if not os.path.exists(file_path):
logger.error(f"Sound-Datei nicht gefunden: {file_path}")
return jsonify({"success": False, "message": "Sound-Datei nicht gefunden"}), 404
# Sound in separatem Thread abspielen (blockiert nicht)
def play():
try:
pygame.mixer.music.load(file_path)
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
time.sleep(0.1)
except Exception as e:
logger.error(f"Fehler beim Abspielen von {file_path}: {e}")
threading.Thread(target=play, daemon=True).start()
logger.info(f"Spiele Sound: {sound_entry['name']} ({sound_id})")
return jsonify({"success": True, "message": f"Spiele: {sound_entry['name']}"})
except Exception as e:
logger.exception("Play-Sound-Fehler")
return jsonify({"success": False, "message": str(e)}), 500
pass
@app.route('/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
pass
@app.route('/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
pass
+68
View File
@@ -0,0 +1,68 @@
# app/routes/main.py
from flask import Blueprint, render_template, redirect, url_for, request, jsonify
from .. import current_config
from app.utils.helpers import load_configs, load_default_sounds
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
configs = load_configs()
return render_template('index.html', configs=configs)
"""
@app.route('/')
def index():
return render_template('index.html', configs=load_configs())
"""
@app.route('/load_config/<filename>')
def load_config(filename):
global current_config
path = os.path.join(app.config['CONFIG_DIR'], filename)
if not os.path.exists(path):
logger.error(f"Config nicht gefunden: {path}")
return "Konfiguration nicht gefunden", 404
try:
with open(path, 'r', encoding='utf-8') as f:
current_config = json.load(f)
current_config['filename'] = filename
logger.info(f"Config erfolgreich geladen: {filename} mit {len(current_config.get('channels', []))} Kanälen")
return redirect(url_for('main.control_page'))
except json.JSONDecodeError as e:
logger.error(f"Ungültiges JSON in {filename}: {e}")
return "Ungültiges JSON-Format in der Konfigurationsdatei", 500
except Exception as e:
logger.error(f"Ladefehler {filename}: {e}")
return "Fehler beim Laden der Konfiguration", 500
@app.route('/control')
def control_page():
if current_config is None:
logger.warning("current_config ist None → Redirect zu index")
return redirect(url_for('main.index'))
# Globale Sounds immer laden
global_sounds = load_default_sounds()
logger.info(f"Übergebe config an Template: {current_config}")
print("DEBUG: config hat channels?", 'channels' in current_config, len(current_config.get('channels', [])))
return render_template(
'control.html',
config=current_config,
global_sounds=global_sounds
)
@app.route('/soundboard')
def soundboard():
if current_config is None or 'sounds' not in current_config:
return redirect(url_for('index'))
sounds = current_config.get('sounds', [])
return render_template('soundboard.html', sounds=sounds, config=current_config)
pass