web console
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
# main.py
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
import uvicorn
|
||||
|
||||
# Importa la tua classe
|
||||
# Assicurati che il nome file sia corretto, lo rinomino per chiarezza
|
||||
from mixer_controller import TF5MixerController
|
||||
|
||||
app = FastAPI(title="Yamaha TF5 Web Interface")
|
||||
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
mixer_controller = None
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: list[WebSocket] = []
|
||||
|
||||
async def connect(self, websocket: WebSocket):
|
||||
await websocket.accept()
|
||||
self.active_connections.append(websocket)
|
||||
|
||||
def disconnect(self, websocket: WebSocket):
|
||||
self.active_connections.remove(websocket)
|
||||
|
||||
async def broadcast(self, message: str):
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_text(message)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
global mixer_controller
|
||||
mixer_controller = TF5MixerController(host="192.168.1.57")
|
||||
|
||||
asyncio.create_task(broadcast_mixer_state())
|
||||
# === NUOVA RIGA ===
|
||||
asyncio.create_task(broadcast_meter_data()) # Avvia il broadcast dei meter
|
||||
|
||||
@app.on_event("shutdown")
|
||||
def shutdown_event():
|
||||
if mixer_controller:
|
||||
mixer_controller.close()
|
||||
|
||||
# NUOVA FUNZIONE PER I METER
|
||||
async def broadcast_meter_data():
|
||||
"""Invia i dati dei meter in tempo reale via WebSocket."""
|
||||
while True:
|
||||
if mixer_controller and mixer_controller._is_connected.is_set() and manager.active_connections:
|
||||
try:
|
||||
# Esegui la funzione bloccante in un thread separato
|
||||
snapshot = await asyncio.to_thread(mixer_controller.get_meter_snapshot)
|
||||
await manager.broadcast(json.dumps({"type": "meter_update", "data": snapshot}))
|
||||
except Exception as e:
|
||||
print(f"Errore durante il broadcast dei meter: {e}")
|
||||
|
||||
await asyncio.sleep(0.1) # Aggiorna 10 volte al secondo
|
||||
|
||||
async def broadcast_mixer_state():
|
||||
"""Controlla se la cache del mixer è cambiata e invia lo stato al WebSocket."""
|
||||
last_timestamp = 0
|
||||
while True:
|
||||
if mixer_controller and mixer_controller._is_connected.is_set():
|
||||
with mixer_controller._cache_lock:
|
||||
current_timestamp = mixer_controller._cache.get("timestamp", 0)
|
||||
|
||||
if current_timestamp > last_timestamp:
|
||||
state = {
|
||||
"channels": mixer_controller.get_all_channels_summary().get("channels", []),
|
||||
"steinch": mixer_controller.get_all_steinch_summary().get("channels", []),
|
||||
"mixes": mixer_controller.get_all_mixes_summary().get("mixes", []),
|
||||
"dcas": mixer_controller.get_all_dca_summary().get("dcas", []),
|
||||
"fxrtn": mixer_controller.get_all_fxrtn_summary().get("channels", []),
|
||||
"stereo": mixer_controller.get_stereo_info(1).get("level_db", -120),
|
||||
"stereo_on": mixer_controller.get_stereo_info(1).get("on", False)
|
||||
}
|
||||
await manager.broadcast(json.dumps({"type": "state_update", "data": state}))
|
||||
last_timestamp = current_timestamp
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
@app.get("/")
|
||||
async def get():
|
||||
with open("static/index.html", "r", encoding="utf-8") as f:
|
||||
return HTMLResponse(f.read())
|
||||
|
||||
@app.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await manager.connect(websocket)
|
||||
# Invia lo stato iniziale appena si connette
|
||||
with mixer_controller._cache_lock:
|
||||
mixer_controller._cache['timestamp'] = time.time() # Forza l'invio
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
cmd = json.loads(data)
|
||||
await handle_command(cmd)
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
|
||||
def get_actual_level(response_str: str, fallback_db: float) -> float:
|
||||
try:
|
||||
parts = response_str.strip().split()
|
||||
for part in reversed(parts):
|
||||
try:
|
||||
val_int = int(part)
|
||||
return val_int / 100.0 if val_int > -32768 else float('-inf')
|
||||
except ValueError:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return fallback_db
|
||||
|
||||
async def handle_command(cmd: dict):
|
||||
action = cmd.get("action")
|
||||
target_type = cmd.get("type")
|
||||
value = cmd.get("value")
|
||||
|
||||
raw_id = cmd.get("id", 1)
|
||||
try:
|
||||
ch_id = int(raw_id)
|
||||
except ValueError:
|
||||
ch_id = 1
|
||||
|
||||
# ==========================================
|
||||
# 1. GESTIONE LIVELLI (MAIN FADERS)
|
||||
# ==========================================
|
||||
if action == "set_level":
|
||||
val_float = float(value)
|
||||
res = {}
|
||||
if target_type == "channel": res = await asyncio.to_thread(mixer_controller.set_channel_level, ch_id, val_float)
|
||||
elif target_type == "steinch": res = await asyncio.to_thread(mixer_controller.set_steinch_level, ch_id, val_float)
|
||||
elif target_type == "mix": res = await asyncio.to_thread(mixer_controller.set_mix_level, ch_id, val_float)
|
||||
elif target_type == "dca": res = await asyncio.to_thread(mixer_controller.set_dca_level, ch_id, val_float)
|
||||
elif target_type == "fxrtn": res = await asyncio.to_thread(mixer_controller.set_fxrtn_level, ch_id, val_float)
|
||||
elif target_type == "stereo": res = await asyncio.to_thread(mixer_controller.set_stereo_level, 1, val_float)
|
||||
|
||||
actual_db = get_actual_level(res.get("response", ""), val_float)
|
||||
|
||||
with mixer_controller._cache_lock:
|
||||
cache_key = "channels" if target_type == "channel" else target_type
|
||||
if target_type == "mix": cache_key = "mixes"
|
||||
if target_type == "dca": cache_key = "dcas"
|
||||
|
||||
str_id = str(ch_id)
|
||||
if target_type == "stereo":
|
||||
mixer_controller._cache.setdefault("stereo", {}).setdefault("1", {})["level_db"] = actual_db
|
||||
elif cache_key in mixer_controller._cache and str_id in mixer_controller._cache[cache_key]:
|
||||
mixer_controller._cache[cache_key][str_id]["level_db"] = actual_db
|
||||
mixer_controller._cache['timestamp'] = time.time()
|
||||
|
||||
# ==========================================
|
||||
# 2. GESTIONE ON/OFF (MAIN FADERS)
|
||||
# ==========================================
|
||||
elif action == "set_on":
|
||||
val_bool = bool(value)
|
||||
if target_type == "channel": await asyncio.to_thread(mixer_controller.set_channel_on_off, ch_id, val_bool)
|
||||
elif target_type == "steinch": await asyncio.to_thread(mixer_controller.set_steinch_on_off, ch_id, val_bool)
|
||||
elif target_type == "mix": await asyncio.to_thread(mixer_controller.set_mix_on_off, ch_id, val_bool)
|
||||
elif target_type == "dca": await asyncio.to_thread(mixer_controller.set_dca_on_off, ch_id, val_bool)
|
||||
elif target_type == "fxrtn": await asyncio.to_thread(mixer_controller.set_fxrtn_on_off, ch_id, val_bool)
|
||||
elif target_type == "stereo": await asyncio.to_thread(mixer_controller.set_stereo_on_off, 1, val_bool)
|
||||
|
||||
with mixer_controller._cache_lock:
|
||||
cache_key = "channels" if target_type == "channel" else target_type
|
||||
if target_type == "mix": cache_key = "mixes"
|
||||
if target_type == "dca": cache_key = "dcas"
|
||||
|
||||
str_id = str(ch_id)
|
||||
if target_type == "stereo":
|
||||
mixer_controller._cache.setdefault("stereo", {}).setdefault("1", {})["on"] = val_bool
|
||||
elif cache_key in mixer_controller._cache and str_id in mixer_controller._cache[cache_key]:
|
||||
mixer_controller._cache[cache_key][str_id]["on"] = val_bool
|
||||
mixer_controller._cache['timestamp'] = time.time()
|
||||
|
||||
# ==========================================
|
||||
# 3. GESTIONE "SENDS ON FADERS" (NUOVO!)
|
||||
# ==========================================
|
||||
elif action == "get_mix_sends":
|
||||
target_mix = int(cmd.get("mix", 1))
|
||||
|
||||
# Funzione per interrogare velocemente tutti i 40 canali per un mix specifico
|
||||
def fetch_sends():
|
||||
sends = []
|
||||
for ch in range(1, 41): # 40 Input Channels
|
||||
try:
|
||||
lvl_cmd = f"get MIXER:Current/InCh/ToMix/Level {ch-1} {target_mix-1}"
|
||||
on_cmd = f"get MIXER:Current/InCh/ToMix/On {ch-1} {target_mix-1}"
|
||||
|
||||
lvl_raw = mixer_controller._send_command(lvl_cmd)
|
||||
on_raw = mixer_controller._send_command(on_cmd)
|
||||
|
||||
lvl_db = mixer_controller._internal_to_level(mixer_controller._parse_value(lvl_raw))
|
||||
|
||||
# --- LA CORREZIONE È QUI ---
|
||||
# Usa il sanitizzatore per convertire -Infinity in -120.0
|
||||
lvl_db = mixer_controller._sanitize_value(lvl_db)
|
||||
|
||||
is_on = mixer_controller._parse_value(on_raw) == "1"
|
||||
|
||||
# Prendi il nome dal canale master nella cache
|
||||
name = f"CH {ch}"
|
||||
with mixer_controller._cache_lock:
|
||||
if str(ch) in mixer_controller._cache.get("channels", {}):
|
||||
name = mixer_controller._cache["channels"][str(ch)]["name"]
|
||||
|
||||
sends.append({
|
||||
"id": ch,
|
||||
"name": name,
|
||||
"level_db": lvl_db,
|
||||
"on": is_on
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
return sends
|
||||
|
||||
# Esegui in background per non bloccare FastAPI
|
||||
sends_data = await asyncio.to_thread(fetch_sends)
|
||||
await manager.broadcast(json.dumps({"type": "mix_sends_data", "mix": target_mix, "data": sends_data}))
|
||||
|
||||
elif action == "set_send_level":
|
||||
target_mix = int(cmd.get("mix", 1))
|
||||
val_float = float(value)
|
||||
# Invia e recupera la risposta (per i log e l'allineamento)
|
||||
res = await asyncio.to_thread(mixer_controller.set_channel_to_mix_level, ch_id, target_mix, val_float)
|
||||
|
||||
# Logghiamo l'effettivo valore applicato dal mixer (OKm)
|
||||
actual_db = get_actual_level(res.get("response", ""), val_float)
|
||||
print(f"DEBUG: Mandata CH{ch_id} -> MIX{target_mix} impostata a {actual_db} dB")
|
||||
|
||||
elif action == "set_send_on":
|
||||
target_mix = int(cmd.get("mix", 1))
|
||||
val_bool = bool(value)
|
||||
await asyncio.to_thread(mixer_controller.set_channel_to_mix_on_off, ch_id, target_mix, val_bool)
|
||||
print(f"DEBUG: Mandata CH{ch_id} -> MIX{target_mix} {'ACCESA' if val_bool else 'SPENTA'}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user