395 lines
14 KiB
Python
395 lines
14 KiB
Python
# 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
|
||
current_module = None # Module6_0 oder Module4_0
|
||
current_device = None # Device0/1/2 je nach hub_id
|
||
|
||
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/<filename>')
|
||
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/<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('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/<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)
|
||
|
||
|
||
@app.route('/admin/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
|
||
|
||
|
||
@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/<path:filename>')
|
||
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():
|
||
"""
|
||
Einfacher Status-Check: Ist ein Hub verbunden?
|
||
Wird von Frontend periodisch aufgerufen, um Verbindungsverlust zu erkennen.
|
||
"""
|
||
global current_device
|
||
|
||
if current_device is None:
|
||
logger.debug("Status-Check: Kein Device aktiv")
|
||
return jsonify({
|
||
"connected": False,
|
||
"message": "Keine aktive Verbindung"
|
||
}), 200 # 200 OK, damit der Check nicht als Fehler gilt
|
||
|
||
try:
|
||
# Optional: Hier könnte man später einen echten Test-Befehl machen
|
||
# z. B. current_device.CreateTelegram() oder nur prüfen, ob Instanz lebt
|
||
logger.debug("Status-Check: Device vorhanden → verbunden")
|
||
return jsonify({
|
||
"connected": True,
|
||
"message": "Verbunden",
|
||
"hub_id": current_config.get('hub_id') if current_config else None
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.exception("Status-Check-Fehler")
|
||
return jsonify({
|
||
"connected": False,
|
||
"message": f"Verbindungsfehler: {str(e)}"
|
||
}), 200 # immer 200, damit Frontend entscheidet
|
||
|
||
|
||
@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) |