# app/routes/api.py import os import time import threading import logging import pygame from flask import Blueprint, jsonify, request, current_app import app.state as state from app.bluetooth.manager import MouldKing, advertiser, tracer from app.utils.helpers import load_default_sounds logger = logging.getLogger(__name__) api_bp = Blueprint('api', __name__) @api_bp.route('/connect', methods=['POST']) def api_connect(): if state.current_config is None: return jsonify({"success": False, "message": "Keine Konfiguration geladen"}), 400 try: hub_id = int(state.current_config.get('hub_id', 0)) hub_type = state.current_config.get('hub_type', '6channel') # '4channel' oder '6channel' # Alte Verbindung sauber trennen if state.current_device is not None: try: state.current_device.Disconnect() except: pass if state.current_module is not None: # ggf. Stop vor Disconnect try: state.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']: state.current_module = mk.Module6_0() else: state.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(state.current_module, device_attr): raise ValueError(f"Hub-ID {hub_id} nicht unterstützt (max 0-2)") state.current_device = getattr(state.current_module, device_attr) # Verbinden state.current_device.Connect() state.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 @api_bp.route('/reconnect', methods=['POST']) def api_reconnect(): if state.current_config is None: return jsonify({"success": False, "message": "Keine Konfiguration geladen"}), 400 try: # Alte Verbindung sauber beenden, falls nötig if state.current_device is not None: try: state.current_device.Disconnect() except: pass # Neu verbinden (kopierter Code aus api_connect) hub_id = int(state.current_config.get('hub_id', 0)) hub_type = state.current_config.get('hub_type', '6channel') mk = MouldKing() mk.SetAdvertiser(advertiser) try: mk.SetTracer(tracer) except: pass if hub_type.lower() in ['6channel', '6']: state.current_module = mk.Module6_0() else: state.current_module = mk.Module4_0() device_attr = f'Device{hub_id}' if not hasattr(state.current_module, device_attr): raise ValueError(f"Hub-ID {hub_id} nicht unterstützt") state.current_device = getattr(state.current_module, device_attr) state.current_device.Connect() state.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 @api_bp.route('/control', methods=['POST']) def api_control(): if state.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 state.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! state.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 @api_bp.route('/stop_all', methods=['POST']) def api_stop_all(): if state.current_device is None: return jsonify({"success": False, "message": "Nicht verbunden"}), 400 try: # Nur stoppen – KEIN Disconnect! state.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 @api_bp.route('/status', methods=['GET']) def api_status(): if state.current_device is not None: return jsonify({ "connected": True, "message": "Verbunden" }) else: return jsonify({ "connected": False, "message": "Keine aktive Verbindung" }), 200 pass @api_bp.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 # Sounds zusammenstellen: lokalspezifisch + globale Default-Sounds config_sounds = state.current_config.get('sounds', []) if state.current_config else [] global_sounds = load_default_sounds(current_app.config['CONFIG_DIR']) all_sounds = list(config_sounds) + list(global_sounds) if not all_sounds: return jsonify({"success": False, "message": "Keine Sounds konfiguriert"}), 400 sound_entry = next((s for s in all_sounds if s.get('id') == sound_id), None) if not sound_entry: return jsonify({"success": False, "message": f"Sound mit ID '{sound_id}' nicht gefunden"}), 404 sounds_dir = current_app.config['SOUNDS_DIR'] file_path = os.path.join(sounds_dir, 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 # Abspielen (non-blocking, kein eigener Thread nötig) try: pygame.mixer.music.load(file_path) pygame.mixer.music.play() except Exception as e: logger.exception(f"Fehler beim Abspielen von {file_path}") return jsonify({"success": False, "message": str(e)}), 500 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 @api_bp.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 @api_bp.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