# app.py import os import json import logging 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.basicConfig( filename='app.log', level=logging.INFO, format='%(asctime)s | %(levelname)-8s | %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger(__name__) # 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 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('/control') def control_page(): if current_config is None: return redirect(url_for('index')) 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 if current_config is None: return jsonify({"success": False, "message": "Keine Konfiguration geladen"}), 400 try: hub_id = int(current_config.get('hub_id', 0)) # Falls schon verbunden → erstmal trennen (sauberer Neustart) if current_hub is not None: try: current_hub.disconnect() # oder .close() – je nach Methode except: pass current_hub = MouldKing( hub_id=hub_id, advertiser=advertiser, tracer=tracer # ← falls das doch geht; sonst entfernen ) current_hub.connect() # ← das ist der entscheidende Aufruf logger.info(f"Erfolgreich verbunden mit Hub-ID {hub_id}") return jsonify({"success": True, "message": f"Hub {hub_id} verbunden"}) except Exception as e: logger.exception("Connect-Fehler") return jsonify({ "success": False, "message": f"Verbindungsfehler: {str(e)}" }), 500 @app.route('/api/control', methods=['POST']) def api_control(): global current_hub if current_hub is None: return jsonify({"success": False, "message": "Nicht verbunden"}), 400 try: data = request.get_json() port = data['port'] value = float(data['value']) # Config-spezifische Anpassungen channel_config = next((c for c in current_config['channels'] if c['port'] == port), None) if channel_config: if channel_config.get('invert', False): value = -value if channel_config.get('negative_only', False) and value >= 0: value = -abs(value) if channel_config['type'] != 'motor': value = channel_config.get('on_value', 1.0) if value != 0 else channel_config.get('off_value', 0.0) # Hier kommt der echte Aufruf current_hub.set_motor(channel=port, power=value) # ← ← ← Kernaufruf logger.info(f"Steuerung: {port} → {value:.2f}") 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_hub if current_hub is None: return jsonify({"success": False, "message": "Nicht verbunden"}), 400 try: current_hub.stop_all() # ← oder alle Kanäle manuell auf 0 setzen logger.info("Alle Kanäle gestoppt") return jsonify({"success": True}) except Exception as e: logger.exception("Stop-Fehler") return jsonify({"success": False, "message": str(e)}), 500 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)