2026-02-16 21:46:32 +01:00

334 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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__)
audio_lock = threading.Lock()
# Sound-Cache: path -> pygame.mixer.Sound
loaded_sounds = {}
def _ensure_mixer():
if not pygame.mixer.get_init():
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
pygame.mixer.set_num_channels(16)
def _load_sound(file_path):
if file_path in loaded_sounds:
return loaded_sounds[file_path]
snd = pygame.mixer.Sound(file_path)
loaded_sounds[file_path] = snd
return snd
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')
channel_req = data.get('channel') # optional: spezifischer Channel
loop_req = data.get('loop', 0) # optional: -1 oder true für Endlosschleife
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 (serialisiert, aber mehrere Channels erlaubt)
with audio_lock:
_ensure_mixer()
# Sound laden (Cache)
snd = _load_sound(file_path)
# Ziel-Channel bestimmen
ch = None
if channel_req not in [None, ""]:
try:
ch_id = int(channel_req)
ch = pygame.mixer.Channel(ch_id)
except Exception as e:
logger.error(f"Ungültiger channel-Wert '{channel_req}': {e}")
return jsonify({"success": False, "message": "Ungültiger Channel"}), 400
else:
ch = pygame.mixer.find_channel(True) # zwingend freien nehmen
if ch is None:
return jsonify({"success": False, "message": "Kein freier Audio-Channel verfügbar"}), 503
# Loop-Wert interpretieren
loops = -1 if loop_req in [True, "true", "True", -1, "loop", "1", 1] else 0
try:
ch.play(snd, loops=loops)
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:
data = request.get_json(silent=True) or {}
channel_req = data.get('channel')
with audio_lock:
_ensure_mixer()
if channel_req not in [None, ""]:
try:
ch_id = int(channel_req)
pygame.mixer.Channel(ch_id).stop()
except Exception as e:
logger.error(f"Stop-Sound-Fehler (channel {channel_req}): {e}")
return jsonify({"success": False, "message": "Ungültiger Channel"}), 400
else:
# Alle Kanäle stoppen
for ch_id in range(pygame.mixer.get_num_channels()):
pygame.mixer.Channel(ch_id).stop()
# Zur Sicherheit auch music stoppen
pygame.mixer.music.stop()
logger.info("Sound(s) 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))
channel_req = data.get('channel')
with audio_lock:
_ensure_mixer()
if channel_req not in [None, ""]:
ch_id = int(channel_req)
pygame.mixer.Channel(ch_id).set_volume(vol)
else:
# global: alle Kanäle + music
for ch_id in range(pygame.mixer.get_num_channels()):
pygame.mixer.Channel(ch_id).set_volume(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