# app.py import os import json import logging from logging.handlers import TimedRotatingFileHandler from datetime import datetime from flask import Flask, render_template, redirect, url_for, send_from_directory, request, jsonify # ── Bluetooth ──────────────────────────────────────────────────────────────── from mkconnect.mouldking.MouldKing import MouldKing from mkconnect.tracer.TracerConsole import TracerConsole from mkconnect.advertiser.AdvertiserBTSocket import AdvertiserBTSocket app = Flask(__name__) app.config['CONFIG_DIR'] = 'configs' os.makedirs(app.config['CONFIG_DIR'], exist_ok=True) # ── Logging ──────────────────────────────────────────────────────────────── # Logging-Verzeichnis LOG_DIR = 'logs' os.makedirs(LOG_DIR, exist_ok=True) # Täglich neuer Ordner (z. B. logs/2026-02) today = datetime.now().strftime('%Y-%m') current_log_subdir = os.path.join(LOG_DIR, today) os.makedirs(current_log_subdir, exist_ok=True) # Log-Datei: z. B. logs/2026-02/12.log log_filename = os.path.join(current_log_subdir, datetime.now().strftime('%d.log')) # Logging konfigurieren logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # Handler: schreibt in die tagesaktuelle Datei handler = logging.FileHandler(log_filename) handler.setLevel(logging.INFO) # Format formatter = logging.Formatter('%(asctime)s | %(levelname)-8s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S') handler.setFormatter(formatter) # Alten Handler entfernen (falls schon gesetzt) logger.handlers.clear() logger.addHandler(handler) # Optional: Auch in Konsole loggen (für Entwicklung) console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) logger.addHandler(console_handler) logger.info("Logging neu initialisiert – Tages-Log: " + log_filename) # Bluetooth-Komponenten (einmalig initialisieren) tracer = TracerConsole() advertiser = AdvertiserBTSocket() # ← ohne tracer, wie korrigiert # Globale Zustände (für Einzelbenutzer / Entwicklung – später Session/DB) current_config = None current_hub: MouldKing | None = None current_module = None # Module6_0 oder Module4_0 current_device = None # Device0/1/2 je nach hub_id import threading import time # Globale Variable für den letzten erfolgreichen Check (optional) last_successful_check = time.time() def connection_monitor(): """Background-Thread: Prüft alle 5 Sekunden, ob der Hub noch antwortet""" global current_device, current_module, current_hub, last_successful_check while True: if current_device is not None: try: # Harter Test: Kanal 0 kurz auf 0.1 und zurück auf 0.0 setzen current_device.SetChannel(0, 0.1) time.sleep(0.05) # winzige Pause current_device.SetChannel(0, 0.0) last_successful_check = time.time() logger.debug("Monitor: Hub antwortet → SetChannel-Test OK") except Exception as e: logger.warning(f"Monitor: Hub scheint weg zu sein: {str(e)}") # Sauber trennen try: current_device.Disconnect() except: pass current_device = None current_module = None current_hub = None # Optional: Frontend benachrichtigen (z. B. über WebSocket später) time.sleep(5) # Prüfintervall: 5 Sekunden # Thread starten (daemon=True → beendet sich mit der App) monitor_thread = threading.Thread(target=connection_monitor, daemon=True) monitor_thread.start() logger.info("Background-Monitor für Hub-Verbindung gestartet") def load_configs(): configs = [] for filename in os.listdir(app.config['CONFIG_DIR']): if filename.lower().endswith('.json'): path = os.path.join(app.config['CONFIG_DIR'], filename) try: with open(path, 'r', encoding='utf-8') as f: data = json.load(f) data['filename'] = filename data.setdefault('name', filename.replace('.json', '').replace('_', ' ').title()) configs.append(data) except Exception as e: logger.error(f"Fehler beim Laden {filename}: {e}") return sorted(configs, key=lambda x: x.get('name', '')) @app.route('/') def index(): return render_template('index.html', configs=load_configs()) """ @app.route('/load_config/') def load_config(filename): global current_config path = os.path.join(app.config['CONFIG_DIR'], filename) if not os.path.exists(path): return "Datei 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 geladen: {filename}") return redirect(url_for('control_page')) except Exception as e: logger.error(f"Config-Ladefehler {filename}: {e}") return "Fehler beim Laden", 500 """ @app.route('/load_config/') 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('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: return redirect(url_for('index')) return render_template('control.html', config=current_config) """ @app.route('/control') def control_page(): if current_config is None: logger.warning("current_config ist None → Redirect zu index") return redirect(url_for('index')) 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) # ── Admin ──────────────────────────────────────────────────────────────────── @app.route('/admin') def admin(): configs = load_configs() return render_template('admin.html', configs=configs) @app.route('/admin/edit/', 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) @app.route('/admin/delete/', 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 @app.route('/admin/logs') def admin_logs(): log_path = 'app.log' if os.path.exists(log_path): with open(log_path, 'r', encoding='utf-8') as f: logs = f.read() else: logs = "Keine Logs vorhanden." return render_template('admin_logs.html', logs=logs) @app.route('/configs/') def serve_config_file(filename): return send_from_directory(app.config['CONFIG_DIR'], filename) # ── API ────────────────────────────────────────────────────────────────────── @app.route('/api/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 @app.route('/api/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 @app.route('/api/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 @app.route('/api/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 @app.route('/api/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 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)