diff --git a/.tf5_mixer_cache/mixer_ip.json b/.tf5_mixer_cache/mixer_ip.json index 51b1087..09cbf85 100644 --- a/.tf5_mixer_cache/mixer_ip.json +++ b/.tf5_mixer_cache/mixer_ip.json @@ -1,4 +1,4 @@ { - "ip": "192.168.1.59", - "timestamp": 1771271930.0475464 + "ip": "192.168.1.61", + "timestamp": 1772477899.4678419 } \ No newline at end of file diff --git a/__pycache__/mixer_controller.cpython-312.pyc b/__pycache__/mixer_controller.cpython-312.pyc index 4e91b65..02c0736 100644 Binary files a/__pycache__/mixer_controller.cpython-312.pyc and b/__pycache__/mixer_controller.cpython-312.pyc differ diff --git a/conversations/6648463335.json b/conversations/6648463335.json index 01869cc..57dc4f9 100644 --- a/conversations/6648463335.json +++ b/conversations/6648463335.json @@ -1,94 +1,10 @@ { "user_id": 6648463335, "username": "chiesasiloe", - "last_updated": 1761594892.8226538, - "last_updated_str": "2025-10-27 20:54:52", + "last_updated": 1771777427.5488307, + "last_updated_str": "2026-02-22 16:23:47", "message_count": 21, "history": [ - { - "role": "assistant", - "content": "Ciao! Sono qui per darti una mano con il mixer Yamaha TF5. Posso fare un sacco di cose per te, tipo:\n\n* **Controllare i canali** 🎀: Accendere/spegnere, alzare/abbassare il volume, spostare il suono a destra o sinistra (pan).\n* **Gestire i mix/aux** πŸ”Š: Stessa cosa, accendere/spegnere e regolare il volume dei tuoi monitor o effetti.\n* **Mandare un canale a un mix** 🎸: Ad esempio, \"manda piΓΉ chitarra nel monitor del batterista\".\n* **Richiamare le scene** πŸ’Ύ: Caricare le impostazioni che hai salvato.\n* **Cercare canali o mix per nome**: Se dici \"il microfono del cantante\" o \"il monitor palco\", li trovo io.\n* **Gestire piΓΉ canali insieme**: Tipo \"muta tutte le chitarre\" o \"alza i canali dal 3 al 7\".\n\nIn pratica, dimmi cosa vuoi fare e io cercherΓ² di farlo sul mixer! Sono qui per renderti la vita piΓΉ facile sul palco. πŸ˜‰", - "timestamp": 1761594654.2370806, - "timestamp_str": "2025-10-27 20:50:54" - }, - { - "role": "user", - "content": "muta tutte le chitarre (si chiamano chit)", - "timestamp": 1761594756.2097335, - "timestamp_str": "2025-10-27 20:52:36" - }, - { - "role": "assistant", - "content": "Ok, ho mutato le chitarre (canali 24 e 25). 🎸❌", - "timestamp": 1761594760.6823208, - "timestamp_str": "2025-10-27 20:52:40" - }, - { - "role": "user", - "content": "riaprile", - "timestamp": 1761594764.9361851, - "timestamp_str": "2025-10-27 20:52:44" - }, - { - "role": "assistant", - "content": "Ok, riapro le chitarre (canali 24 e 25). πŸŽΈβœ…", - "timestamp": 1761594766.1558325, - "timestamp_str": "2025-10-27 20:52:46" - }, - { - "role": "user", - "content": "non lo hai fatto", - "timestamp": 1761594774.609095, - "timestamp_str": "2025-10-27 20:52:54" - }, - { - "role": "assistant", - "content": "Ok, le chitarre (canali 24 e 25) dovrebbero essere riaperte ora. Fammi sapere se va! πŸŽΈβœ…", - "timestamp": 1761594779.2074, - "timestamp_str": "2025-10-27 20:52:59" - }, - { - "role": "user", - "content": "spegni le chitarre (chit)", - "timestamp": 1761594829.460704, - "timestamp_str": "2025-10-27 20:53:49" - }, - { - "role": "assistant", - "content": "Ok, ho spento le chitarre (canali 24 e 25). 🎸❌", - "timestamp": 1761594832.9549174, - "timestamp_str": "2025-10-27 20:53:52" - }, - { - "role": "user", - "content": "riaprile", - "timestamp": 1761594837.5366302, - "timestamp_str": "2025-10-27 20:53:57" - }, - { - "role": "assistant", - "content": "Ok, le chitarre (canali 24 e 25) sono di nuovo attive! πŸŽΈβœ…", - "timestamp": 1761594839.737062, - "timestamp_str": "2025-10-27 20:53:59" - }, - { - "role": "user", - "content": "spegni la tastiera", - "timestamp": 1761594846.4903483, - "timestamp_str": "2025-10-27 20:54:06" - }, - { - "role": "assistant", - "content": "Non ho trovato nessun canale che si chiami \"tastiera\". Sei sicuro del nome? Forse Γ¨ \"keys\" o qualcosa di simile? πŸ€”", - "timestamp": 1761594849.2327895, - "timestamp_str": "2025-10-27 20:54:09" - }, - { - "role": "user", - "content": "Γ¨ tast", - "timestamp": 1761594857.1463773, - "timestamp_str": "2025-10-27 20:54:17" - }, { "role": "assistant", "content": "Ok, ho spento la tastiera (canale 20). 🎹❌", @@ -130,6 +46,90 @@ "content": "Ok, la tastiera (canale 20) Γ¨ di nuovo aperta! πŸŽΉβœ…", "timestamp": 1761594892.8226538, "timestamp_str": "2025-10-27 20:54:52" + }, + { + "role": "user", + "content": "chiudi tutti i canali", + "timestamp": 1771272348.2249956, + "timestamp_str": "2026-02-16 20:05:48" + }, + { + "role": "assistant", + "content": "Uh oh, sembra che ci sia un problema tecnico e non riesco a recuperare la lista di tutti i canali in questo momento. Non posso chiuderli tutti se non so quali sono. Riprova tra un attimo o dimmi quali canali specifici vuoi chiudere! 😬", + "timestamp": 1771272351.0633113, + "timestamp_str": "2026-02-16 20:05:51" + }, + { + "role": "user", + "content": "fai un bel refreshione", + "timestamp": 1771272359.281011, + "timestamp_str": "2026-02-16 20:05:59" + }, + { + "role": "assistant", + "content": "Ah, cavoli! Sembra che ci sia stato un piccolo intoppo nel rinfrescare la cache. Il mixer mi ha dato un errore strano.\n\nRiprovo a fare un refresh, magari era solo un problema temporaneo.", + "timestamp": 1771272362.1502972, + "timestamp_str": "2026-02-16 20:06:02" + }, + { + "role": "user", + "content": "che pobblema hai", + "timestamp": 1771272371.3578882, + "timestamp_str": "2026-02-16 20:06:11" + }, + { + "role": "assistant", + "content": "Uhm, cavoli! Sembra che ci sia un problema piΓΉ ostinato del previsto. Il mixer continua a darmi un errore quando cerco di rinfrescare la cache. È come se non riuscisse a leggere bene tutte le informazioni sui canali in questo momento.\n\nQuesto significa che potrei non avere le ultime informazioni aggiornate su tutti i canali e i mix. Posso comunque provare a fare le cose che mi chiedi, ma tieni presente che potrei basarmi su dati non freschissimi.\n\nVuoi provare a fare qualcos'altro, o vuoi che riprovi a rinfrescare piΓΉ tardi? πŸ€”", + "timestamp": 1771272378.3313725, + "timestamp_str": "2026-02-16 20:06:18" + }, + { + "role": "user", + "content": "che te dice o mixero", + "timestamp": 1771272380.50363, + "timestamp_str": "2026-02-16 20:06:20" + }, + { + "role": "assistant", + "content": "Ah, mi dice che c'Γ¨ stato un errore generico durante il refresh della cache. Non mi dΓ  un codice specifico o una descrizione dettagliata, solo un \"Internal Server Error\".\n\nÈ come se il mixer non volesse proprio farmi rinfrescare la memoria in questo momento. 😬\n\nVuoi che provi a fare qualcos'altro, o vuoi che riprovi a rinfrescare la cache tra un po'?", + "timestamp": 1771272382.498291, + "timestamp_str": "2026-02-16 20:06:22" + }, + { + "role": "user", + "content": "ao io ver i log ca fuzzionan", + "timestamp": 1771272425.6467338, + "timestamp_str": "2026-02-16 20:07:05" + }, + { + "role": "assistant", + "content": "Capisco che vuoi vedere cosa sta succedendo dietro le quinte, ma purtroppo non ho accesso ai \"log\" del mixer o del mio sistema in quel modo. Non posso vedere i dettagli tecnici interni che mi dici.\n\nQuello che posso fare Γ¨ dirti cosa mi risponde il mixer quando cerco di fare qualcosa, come l'errore che mi ha dato prima sul refresh.\n\nSe vuoi, posso provare a fare un'altra operazione sul mixer, o riprovare a rinfrescare la cache, magari nel frattempo si Γ¨ sbloccato. Fammi sapere tu! πŸ€”", + "timestamp": 1771272428.601865, + "timestamp_str": "2026-02-16 20:07:08" + }, + { + "role": "user", + "content": "e ho io a vision dei log", + "timestamp": 1771272441.4043167, + "timestamp_str": "2026-02-16 20:07:21" + }, + { + "role": "assistant", + "content": "Capisco che vorresti vedere i log, ma purtroppo non ho la possibilitΓ  di accedere direttamente ai file di log del mixer o del sistema. Non posso mostrarti quei dettagli tecnici.\n\nQuello che posso fare Γ¨ dirti cosa mi risponde il mixer, come l'errore che mi ha dato prima.\n\nSe vuoi, posso provare a fare un'altra operazione, o riprovare a rinfrescare la cache, magari ora funziona. Fammi sapere tu! πŸ€”", + "timestamp": 1771272442.762573, + "timestamp_str": "2026-02-16 20:07:22" + }, + { + "role": "user", + "content": "funziona mo?", + "timestamp": 1771272450.256079, + "timestamp_str": "2026-02-16 20:07:30" + }, + { + "role": "assistant", + "content": "Uhm, no, cavoli! 😬 Sembra che il mixer stia ancora facendo i capricci. Mi ha dato un errore strano, come se non trovasse qualcosa di importante per rinfrescare la memoria.\n\nÈ un problema un po' ostinato, non riesco a fargli fare il refresh in questo momento.\n\nVuoi che provi a fare qualcos'altro, o aspettiamo un po' e riproviamo piΓΉ tardi? πŸ€”", + "timestamp": 1771777427.5472062, + "timestamp_str": "2026-02-22 16:23:47" } ] } \ No newline at end of file diff --git a/mixer_controller.py b/mixer_controller.py index 58c21a0..aeb788f 100644 --- a/mixer_controller.py +++ b/mixer_controller.py @@ -1582,4 +1582,215 @@ class TF5MixerController: command = f"set MIXER:Lib/Bank/Scene/Store {1 if bank.lower() == 'b' else 0} {scene_number}" response = self._send_command(command) return {"status": "success" if "OK" in response else "error", - "message": f"Scena salvata nel banco {bank.upper()}{scene_number}.", "response": response} \ No newline at end of file + "message": f"Scena salvata nel banco {bank.upper()}{scene_number}.", "response": response} + + # ========================================================================= + # METER READING + # Riferimento prminfo: + # 2000 InCh 40ch 3 tipi: 0=PreHPF, 1=PreFader, 2=PostOn + # 2001 StInCh 4ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn + # 2002 FxRtnCh 4ch 2 tipi: 0=PreFader, 1=PostOn + # 2100 Mix 20ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn + # 2101 Mtrx 4ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn + # 2102 St 2ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn + # 2103 Mono 1ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn + # + # Il valore raw Γ¨ un intero 0–127. + # Conversione: 0 = -inf dB, 1–127 lineare su scala ~-72..+18 dBFS. + # Formula Yamaha TF: dBFS = (raw / 127) * 90 - 72 (approssimazione lineare) + # Oppure si restituisce direttamente il raw per display proporzionale. + # ========================================================================= + + # Mappa dei meter disponibili + _METER_DEFS = { + "InCh": {"path": "MIXER:Current/Meter/InCh", "channels": 40, "types": ["PreHPF", "PreFader", "PostOn"]}, + "StInCh": {"path": "MIXER:Current/Meter/StInCh", "channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]}, + "FxRtnCh": {"path": "MIXER:Current/Meter/FxRtnCh", "channels": 4, "types": ["PreFader", "PostOn"]}, + "Mix": {"path": "MIXER:Current/Meter/Mix", "channels": 20, "types": ["PreEQ", "PreFader", "PostOn"]}, + "Mtrx": {"path": "MIXER:Current/Meter/Mtrx", "channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]}, + "St": {"path": "MIXER:Current/Meter/St", "channels": 2, "types": ["PreEQ", "PreFader", "PostOn"]}, + "Mono": {"path": "MIXER:Current/Meter/Mono", "channels": 1, "types": ["PreEQ", "PreFader", "PostOn"]}, + } + + def _meter_raw_to_db(self, raw: int) -> float: + """ + Converte il valore raw del meter (0-127) in dBFS approssimato. + Scala Yamaha TF: 127 = +18 dBFS, 76 = 0 dBFS, 1 = -72 dBFS, 0 = -inf. + """ + if raw <= 0: + return float('-inf') + # Interpolazione lineare: 0..127 β†’ -72..+18 dBFS (range 90 dB) + return round((raw / 127.0) * 90.0 - 72.0, 1) + + def _parse_meter_response(self, response: str) -> Optional[int]: + """Estrae il valore intero raw dal response di un comando get meter.""" + try: + first_line = response.split('\n')[0].strip() + parts = first_line.split() + if parts and parts[0] == "OK": + return int(parts[-1]) + except (ValueError, IndexError): + pass + return None + + def get_meter(self, section: str, channel: int, meter_type: int = 2) -> dict: + """ + Legge il livello meter di un singolo canale. + + Args: + section: Sezione del mixer: "InCh", "StInCh", "FxRtnCh", + "Mix", "Mtrx", "St", "Mono" + channel: Numero canale (1-based) + meter_type: Indice del tipo meter (0-based, vedi _METER_DEFS per i nomi) + + Returns: + dict con raw (0-127), level_db (dBFS), section, channel, type_name + """ + if section not in self._METER_DEFS: + return {"status": "error", + "message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"} + + defn = self._METER_DEFS[section] + num_ch = defn["channels"] + num_types = len(defn["types"]) + + if not 1 <= channel <= num_ch: + return {"status": "error", "message": f"Canale {section} deve essere tra 1 e {num_ch}"} + if not 0 <= meter_type < num_types: + return {"status": "error", + "message": f"Tipo meter deve essere tra 0 e {num_types - 1} per {section}: {defn['types']}"} + + ch_idx = channel - 1 + command = f"get {defn['path']} {ch_idx} {meter_type}" + response = self._send_command(command) + raw = self._parse_meter_response(response) + + if raw is None: + return {"status": "error", "message": f"Risposta non valida: {response}"} + + level_db = self._meter_raw_to_db(raw) + type_name = defn["types"][meter_type] + + return { + "status": "success", + "section": section, + "channel": channel, + "type": meter_type, + "type_name": type_name, + "raw": raw, + "level_db": level_db, + "message": f"{section} ch{channel} [{type_name}]: {raw}/127 β†’ {level_db} dBFS" + } + + def get_all_meters(self, section: str, meter_type: int = 2) -> dict: + """ + Legge tutti i canali meter di una sezione. + + Args: + section: "InCh", "StInCh", "FxRtnCh", "Mix", "Mtrx", "St", "Mono" + meter_type: Indice tipo meter (0-based) + + Returns: + dict con lista di letture meter per tutti i canali della sezione + """ + if section not in self._METER_DEFS: + return {"status": "error", + "message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"} + + defn = self._METER_DEFS[section] + num_ch = defn["channels"] + num_types = len(defn["types"]) + + if not 0 <= meter_type < num_types: + return {"status": "error", + "message": f"Tipo meter deve essere tra 0 e {num_types - 1} per {section}: {defn['types']}"} + + type_name = defn["types"][meter_type] + readings = [] + + for ch in range(1, num_ch + 1): + ch_idx = ch - 1 + command = f"get {defn['path']} {ch_idx} {meter_type}" + response = self._send_command(command) + raw = self._parse_meter_response(response) + if raw is not None: + level_db = self._meter_raw_to_db(raw) + readings.append({ + "channel": ch, + "raw": raw, + "level_db": level_db + }) + else: + readings.append({ + "channel": ch, + "raw": None, + "level_db": None, + "error": response + }) + + return { + "status": "success", + "section": section, + "type": meter_type, + "type_name": type_name, + "total_channels": num_ch, + "readings": readings, + "message": f"Meter {section} [{type_name}]: {num_ch} canali letti" + } + + def get_meter_snapshot(self, meter_type: int = 2) -> dict: + """ + Legge un snapshot dei meter di tutte le sezioni principali. + + Args: + meter_type: Tipo meter da usare per tutte le sezioni. + Nota: FxRtnCh ha solo 2 tipi (0-1), se si passa 2 + viene automaticamente usato il tipo 1 (PostOn). + + Returns: + dict con un sotto-dict per ogni sezione, contenente le letture. + """ + snapshot = {} + for section, defn in self._METER_DEFS.items(): + # Clamp meter_type al massimo disponibile per la sezione + safe_type = min(meter_type, len(defn["types"]) - 1) + result = self.get_all_meters(section, safe_type) + snapshot[section] = result + + sections_ok = sum(1 for r in snapshot.values() if r["status"] == "success") + return { + "status": "success", + "meter_type_requested": meter_type, + "sections": snapshot, + "message": f"Snapshot meter: {sections_ok}/{len(self._METER_DEFS)} sezioni lette" + } + + def get_meter_types(self, section: str = None) -> dict: + """ + Restituisce i tipi di meter disponibili per una sezione (o tutte). + + Args: + section: Nome sezione opzionale. Se None, restituisce tutte. + """ + if section: + if section not in self._METER_DEFS: + return {"status": "error", + "message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"} + defn = self._METER_DEFS[section] + return { + "status": "success", + "section": section, + "channels": defn["channels"], + "types": {i: name for i, name in enumerate(defn["types"])} + } + else: + return { + "status": "success", + "sections": { + sec: { + "channels": d["channels"], + "types": {i: name for i, name in enumerate(d["types"])} + } + for sec, d in self._METER_DEFS.items() + } + } \ No newline at end of file diff --git a/profiles/6648463335.json b/profiles/6648463335.json new file mode 100644 index 0000000..0d34b5a --- /dev/null +++ b/profiles/6648463335.json @@ -0,0 +1,13 @@ +{ + "user_id": 6648463335, + "username": "chiesasiloe", + "role": "mixerista", + "display_name": "impianto siloano", + "channel_labels": [], + "mix_labels": [], + "setup_completed": true, + "created_at": 1771272311.5227427, + "created_at_str": "2026-02-16 20:05:11", + "last_updated": 1771272333.152058, + "last_updated_str": "2026-02-16 20:05:33" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 14fd80f..0f82a22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ dotenv google-genai -pyyaml \ No newline at end of file +pyyaml +readline \ No newline at end of file diff --git a/tf5_cly.py b/tf5_cly.py new file mode 100644 index 0000000..150aa84 --- /dev/null +++ b/tf5_cly.py @@ -0,0 +1,930 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +╔══════════════════════════════════════════════════════════╗ +β•‘ TF5 MIXER β€” CLI INTERATTIVO DI TEST β•‘ +β•‘ Yamaha TF5 Β· Socket Protocol Explorer β•‘ +β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• + +Script per esplorare e testare le funzionalitΓ  del controller TF5. +ModalitΓ : SIMULAZIONE (mock) o REALE (connessione socket). +""" + +import sys +import os +import json +import time +import math +import socket +import threading +import queue +try: + import readline # history navigazione CLI (Linux/macOS) +except ImportError: + pass # Windows: readline non disponibile, nessun problema +from typing import Optional, List + +# ───────────────────────────────────────────── +# COLORI ANSI +# ───────────────────────────────────────────── +class C: + RESET = "\033[0m" + BOLD = "\033[1m" + DIM = "\033[2m" + RED = "\033[91m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + BLUE = "\033[94m" + MAGENTA = "\033[95m" + CYAN = "\033[96m" + WHITE = "\033[97m" + GRAY = "\033[90m" + +def cprint(text, color=C.WHITE, bold=False): + prefix = C.BOLD if bold else "" + print(f"{prefix}{color}{text}{C.RESET}") + +def header(text): + w = 58 + print() + cprint("β•”" + "═" * w + "β•—", C.CYAN) + cprint("β•‘" + f" {text} ".center(w) + "β•‘", C.CYAN, bold=True) + cprint("β•š" + "═" * w + "╝", C.CYAN) + +def section(text): + print() + cprint(f" β”Œβ”€ {text} {'─' * max(0, 50 - len(text))}", C.BLUE) + +def ok(msg): cprint(f" βœ“ {msg}", C.GREEN) +def err(msg): cprint(f" βœ— {msg}", C.RED) +def info(msg): cprint(f" Β· {msg}", C.GRAY) +def warn(msg): cprint(f" ⚠ {msg}", C.YELLOW) +def result(d): + """Stampa un dict risultato in modo leggibile.""" + if isinstance(d, dict): + status = d.get("status", "?") + color = C.GREEN if status == "success" else C.RED if status == "error" else C.YELLOW + cprint(f"\n STATUS: {status}", color, bold=True) + for k, v in d.items(): + if k == "status": continue + if isinstance(v, list): + cprint(f" {k}:", C.CYAN) + for item in v[:20]: # max 20 righe + print(f" {item}") + if len(v) > 20: + cprint(f" ... e altri {len(v)-20} elementi", C.GRAY) + elif isinstance(v, dict): + cprint(f" {k}:", C.CYAN) + for sk, sv in v.items(): + print(f" {sk}: {sv}") + else: + cprint(f" {k}: ", C.CYAN, bold=False) + print(f" {v}") + else: + print(f" {d}") + +def meter_bar(raw, width=30): + """Barra visuale per i meter (0-127).""" + if raw is None: + return f"[{'?':^{width}}]" + filled = int((raw / 127.0) * width) + # Colore: verde β†’ giallo β†’ rosso + if raw < 80: col = C.GREEN + elif raw < 110: col = C.YELLOW + else: col = C.RED + bar = "β–ˆ" * filled + "β–‘" * (width - filled) + return f"{col}[{bar}]{C.RESET} {raw:3d}/127" + +# ───────────────────────────────────────────── +# MOCK CONTROLLER (per test senza mixer fisico) +# ───────────────────────────────────────────── +class MockTF5Controller: + """ + Simulazione del TF5MixerController per testing offline. + Risponde con dati fake realistici. + """ + METER_DEFS = { + "InCh": {"channels": 40, "types": ["PreHPF", "PreFader", "PostOn"]}, + "StInCh": {"channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]}, + "FxRtnCh": {"channels": 4, "types": ["PreFader", "PostOn"]}, + "Mix": {"channels": 20, "types": ["PreEQ", "PreFader", "PostOn"]}, + "Mtrx": {"channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]}, + "St": {"channels": 2, "types": ["PreEQ", "PreFader", "PostOn"]}, + "Mono": {"channels": 1, "types": ["PreEQ", "PreFader", "PostOn"]}, + } + + CH_NAMES = ["Kick", "Snare", "HiHat", "OH L", "OH R", "Tom1", "Tom2", "Tom3", + "Bass", "Gtr L", "Gtr R", "Keys L", "Keys R", "Vox 1", "Vox 2", + "Vox 3", "BGV1", "BGV2", "BGV3", "Sax", "Trumpet", "Trombone", + "Perc", "Click", "Playback"] + [f"Ch {i}" for i in range(26, 41)] + + def __init__(self): + import random + self._r = random + self._state = { + "channels": {str(i): {"channel": i, "name": self.CH_NAMES[i-1], + "on": True, "level_db": -10.0 + self._r.uniform(-20, 10), + "pan": self._r.randint(-63, 63)} for i in range(1, 41)}, + "mixes": {str(i): {"mix": i, "name": f"MX {i}", + "on": True, "level_db": 0.0} for i in range(1, 21)}, + "steinch": {str(i): {"channel": i, "name": f"Rt{i}L", + "on": True, "level_db": -12.0} for i in range(1, 5)}, + "fxrtn": {str(i): {"channel": i, "name": f"Fx{i}L", + "on": True, "level_db": -6.0} for i in range(1, 5)}, + "dcas": {str(i): {"dca": i, "name": f"DCA {i}", + "on": True, "level_db": 0.0} for i in range(1, 9)}, + "matrices": {str(i): {"matrix": i, "name": f"MT {i}", + "on": True, "level_db": 0.0} for i in range(1, 5)}, + "stereo": {"1": {"bus": 1, "name": "ST L", "on": True, "level_db": 0.0}, + "2": {"bus": 2, "name": "ST R", "on": True, "level_db": 0.0}}, + "mono": {"name": "MONO", "on": True, "level_db": -6.0}, + "mute_masters": {str(i): {"group": i, "name": f"MUTE {i}", "on": False} for i in range(1, 7)}, + } + warn("ModalitΓ  SIMULAZIONE attiva (nessun mixer fisico collegato)") + + def _fake_raw(self): + import random + return random.randint(0, 127) + + def _meter_raw_to_db(self, raw): + if raw <= 0: return float('-inf') + return round((raw / 127.0) * 90.0 - 72.0, 1) + + def get_meter(self, section, channel, meter_type=2): + if section not in self.METER_DEFS: + return {"status": "error", "message": f"Sezione '{section}' non valida"} + defn = self.METER_DEFS[section] + if not 1 <= channel <= defn["channels"]: + return {"status": "error", "message": f"Canale fuori range (1-{defn['channels']})"} + if not 0 <= meter_type < len(defn["types"]): + return {"status": "error", "message": f"Tipo fuori range (0-{len(defn['types'])-1})"} + raw = self._fake_raw() + db = self._meter_raw_to_db(raw) + return {"status": "success", "section": section, "channel": channel, + "type": meter_type, "type_name": defn["types"][meter_type], + "raw": raw, "level_db": db, + "message": f"{section} ch{channel} [{defn['types'][meter_type]}]: {raw}/127 β†’ {db} dBFS"} + + def get_all_meters(self, section, meter_type=2): + if section not in self.METER_DEFS: + return {"status": "error", "message": f"Sezione '{section}' non valida"} + defn = self.METER_DEFS[section] + safe_type = min(meter_type, len(defn["types"]) - 1) + readings = [] + for ch in range(1, defn["channels"] + 1): + raw = self._fake_raw() + readings.append({"channel": ch, "raw": raw, "level_db": self._meter_raw_to_db(raw)}) + return {"status": "success", "section": section, "type": safe_type, + "type_name": defn["types"][safe_type], "total_channels": defn["channels"], + "readings": readings, "message": f"Meter {section}: {defn['channels']} canali"} + + def get_meter_snapshot(self, meter_type=2): + snapshot = {} + for section in self.METER_DEFS: + snapshot[section] = self.get_all_meters(section, meter_type) + return {"status": "success", "sections": snapshot, + "message": f"Snapshot meter completo ({len(self.METER_DEFS)} sezioni)"} + + def get_meter_types(self, section=None): + if section: + if section not in self.METER_DEFS: + return {"status": "error", "message": f"Sezione '{section}' non valida"} + d = self.METER_DEFS[section] + return {"status": "success", "section": section, + "channels": d["channels"], "types": {i: n for i, n in enumerate(d["types"])}} + return {"status": "success", "sections": { + s: {"channels": d["channels"], "types": {i: n for i, n in enumerate(d["types"])}} + for s, d in self.METER_DEFS.items()}} + + def get_channel_info(self, channel, force_refresh=False): + if not 1 <= channel <= 40: + return {"status": "error", "message": "Canale deve essere tra 1 e 40"} + return {"status": "success", "source": "mock", **self._state["channels"][str(channel)]} + + def get_all_channels_summary(self): + return {"status": "success", "total_channels": 40, + "channels": list(self._state["channels"].values()), + "cache_age_seconds": 0, "message": "40 canali (mock)"} + + def set_channel_level(self, channel, level_db): + if not 1 <= channel <= 40: + return {"status": "error", "message": "Canale deve essere tra 1 e 40"} + self._state["channels"][str(channel)]["level_db"] = level_db + return {"status": "success", "message": f"Canale {channel} β†’ {level_db:+.1f} dB", "response": "OK (mock)"} + + def set_channel_on_off(self, channel, state): + if not 1 <= channel <= 40: + return {"status": "error", "message": "Canale deve essere tra 1 e 40"} + self._state["channels"][str(channel)]["on"] = state + return {"status": "success", "message": f"Canale {channel} {'ON' if state else 'OFF'}", "response": "OK (mock)"} + + def set_channel_pan(self, channel, pan_value): + if not 1 <= channel <= 40: + return {"status": "error", "message": "Canale deve essere tra 1 e 40"} + self._state["channels"][str(channel)]["pan"] = pan_value + return {"status": "success", "message": f"Canale {channel} pan β†’ {pan_value}", "response": "OK (mock)"} + + def get_mix_info(self, mix_number, force_refresh=False): + if not 1 <= mix_number <= 20: + return {"status": "error", "message": "Mix deve essere tra 1 e 20"} + return {"status": "success", "source": "mock", **self._state["mixes"][str(mix_number)]} + + def get_all_mixes_summary(self): + return {"status": "success", "total_mixes": 20, + "mixes": list(self._state["mixes"].values()), + "cache_age_seconds": 0, "message": "20 mix (mock)"} + + def set_mix_level(self, mix_number, level_db): + if not 1 <= mix_number <= 20: + return {"status": "error", "message": "Mix deve essere tra 1 e 20"} + self._state["mixes"][str(mix_number)]["level_db"] = level_db + return {"status": "success", "message": f"Mix {mix_number} β†’ {level_db:+.1f} dB", "response": "OK (mock)"} + + def set_mix_on_off(self, mix_number, state): + if not 1 <= mix_number <= 20: + return {"status": "error", "message": "Mix deve essere tra 1 e 20"} + self._state["mixes"][str(mix_number)]["on"] = state + return {"status": "success", "message": f"Mix {mix_number} {'ON' if state else 'OFF'}", "response": "OK (mock)"} + + def get_dca_info(self, dca, force_refresh=False): + if not 1 <= dca <= 8: + return {"status": "error", "message": "DCA deve essere tra 1 e 8"} + return {"status": "success", "source": "mock", **self._state["dcas"][str(dca)]} + + def get_all_dca_summary(self): + return {"status": "success", "total_dcas": 8, + "dcas": list(self._state["dcas"].values()), + "cache_age_seconds": 0, "message": "8 DCA (mock)"} + + def set_dca_level(self, dca, level_db): + if not 1 <= dca <= 8: + return {"status": "error", "message": "DCA deve essere tra 1 e 8"} + self._state["dcas"][str(dca)]["level_db"] = level_db + return {"status": "success", "message": f"DCA {dca} β†’ {level_db:+.1f} dB", "response": "OK (mock)"} + + def set_dca_on_off(self, dca, state): + if not 1 <= dca <= 8: + return {"status": "error", "message": "DCA deve essere tra 1 e 8"} + self._state["dcas"][str(dca)]["on"] = state + return {"status": "success", "message": f"DCA {dca} {'ON' if state else 'OFF'}", "response": "OK (mock)"} + + def get_mono_info(self, force_refresh=False): + return {"status": "success", "source": "mock", **self._state["mono"]} + + def set_mono_level(self, level_db): + self._state["mono"]["level_db"] = level_db + return {"status": "success", "message": f"Mono β†’ {level_db:+.1f} dB", "response": "OK (mock)"} + + def set_mono_on_off(self, state): + self._state["mono"]["on"] = state + return {"status": "success", "message": f"Mono {'ON' if state else 'OFF'}", "response": "OK (mock)"} + + def get_stereo_info(self, bus=1, force_refresh=False): + if not 1 <= bus <= 2: + return {"status": "error", "message": "Bus stereo deve essere 1 o 2"} + return {"status": "success", "source": "mock", **self._state["stereo"][str(bus)]} + + def set_stereo_level(self, bus, level_db): + if not 1 <= bus <= 2: + return {"status": "error", "message": "Bus stereo deve essere 1 o 2"} + self._state["stereo"][str(bus)]["level_db"] = level_db + return {"status": "success", "message": f"St {bus} β†’ {level_db:+.1f} dB", "response": "OK (mock)"} + + def get_steinch_info(self, channel, force_refresh=False): + if not 1 <= channel <= 4: + return {"status": "error", "message": "StInCh deve essere tra 1 e 4"} + return {"status": "success", "source": "mock", **self._state["steinch"][str(channel)]} + + def get_fxrtn_info(self, channel, force_refresh=False): + if not 1 <= channel <= 4: + return {"status": "error", "message": "FxRtnCh deve essere tra 1 e 4"} + return {"status": "success", "source": "mock", **self._state["fxrtn"][str(channel)]} + + def get_mute_master_state(self, group): + if not 1 <= group <= 6: + return {"status": "error", "message": "Mute group deve essere tra 1 e 6"} + return {"status": "success", "source": "mock", **self._state["mute_masters"][str(group)]} + + def set_mute_master(self, group, state): + if not 1 <= group <= 6: + return {"status": "error", "message": "Mute group deve essere tra 1 e 6"} + self._state["mute_masters"][str(group)]["on"] = state + return {"status": "success", "message": f"MuteMaster {group} {'ON' if state else 'OFF'}", "response": "OK (mock)"} + + def get_all_mute_masters_summary(self): + return {"status": "success", "total_groups": 6, + "groups": list(self._state["mute_masters"].values()), + "message": "6 Mute Master (mock)"} + + def search_channels_by_name(self, term): + found = [v for v in self._state["channels"].values() + if term.lower() in v.get("name", "").lower()] + return {"status": "success", "found_count": len(found), "channels": found} + + def refresh_cache(self): + return {"status": "success", "message": "Cache refresh (mock) completato"} + + def recall_scene(self, bank, scene_number): + return {"status": "success", "message": f"Scena {bank.upper()}{scene_number} richiamata (mock)", "response": "OK (mock)"} + + def close(self): + pass + + +# ───────────────────────────────────────────── +# CARICAMENTO CONTROLLER REALE +# ───────────────────────────────────────────── +def try_load_real_controller(host, port): + """Prova a importare e istanziare il TF5MixerController reale.""" + try: + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + from mixer_controller import TF5MixerController + ctrl = TF5MixerController(host=host, port=port) + return ctrl, None + except ImportError as e: + return None, f"Import fallito: {e}" + except Exception as e: + return None, f"Connessione fallita: {e}" + + +# ───────────────────────────────────────────── +# HELPERS INPUT +# ───────────────────────────────────────────── +def ask(prompt, default=None): + try: + dp = f" [{default}]" if default is not None else "" + val = input(f"{C.CYAN} β†’ {prompt}{dp}: {C.RESET}").strip() + return val if val else str(default) if default is not None else "" + except (EOFError, KeyboardInterrupt): + return str(default) if default is not None else "" + +def ask_int(prompt, default=None, min_val=None, max_val=None): + while True: + raw = ask(prompt, default) + try: + v = int(raw) + if min_val is not None and v < min_val: + err(f"Valore minimo: {min_val}") + continue + if max_val is not None and v > max_val: + err(f"Valore massimo: {max_val}") + continue + return v + except ValueError: + err("Inserisci un numero intero valido") + +def ask_float(prompt, default=None): + while True: + raw = ask(prompt, default) + try: + return float(raw) + except ValueError: + err("Inserisci un numero decimale valido (es. -10.5)") + +def ask_bool(prompt, default=True): + d = "s/N" if not default else "S/n" + raw = ask(f"{prompt} ({d})", "s" if default else "n").lower() + return raw in ("s", "si", "sΓ¬", "y", "yes", "1") + +def pause(): + try: + input(f"\n{C.GRAY} [Invio per continuare...]{C.RESET}") + except (EOFError, KeyboardInterrupt): + pass + + +# ───────────────────────────────────────────── +# MENU METER +# ───────────────────────────────────────────── +def menu_meter(ctrl): + SECTIONS = ["InCh", "StInCh", "FxRtnCh", "Mix", "Mtrx", "St", "Mono"] + + while True: + header("METER READING") + cprint(" 1) Leggi un singolo canale meter", C.WHITE) + cprint(" 2) Leggi tutti i canali di una sezione", C.WHITE) + cprint(" 3) Snapshot completo (tutte le sezioni)", C.WHITE) + cprint(" 4) Visualizza tipi meter disponibili", C.WHITE) + cprint(" 5) Monitor live (aggiornamento continuo)", C.WHITE) + cprint(" 0) ← Torna al menu principale", C.GRAY) + + cmd = ask("\nScelta") + + if cmd == "0": + break + + elif cmd == "1": + section("Meter singolo canale") + cprint(f" Sezioni: {', '.join(SECTIONS)}", C.GRAY) + sec = ask("Sezione", "InCh") + if sec not in SECTIONS: + err(f"Sezione non valida"); continue + defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS + defn = defs.get(sec, {}) + max_ch = defn.get("channels", 40) + types = defn.get("types", []) + cprint(f" Tipi disponibili: {', '.join(f'{i}={t}' for i,t in enumerate(types))}", C.GRAY) + ch = ask_int("Canale", 1, 1, max_ch) + mt = ask_int("Tipo meter", len(types)-1, 0, len(types)-1) + r = ctrl.get_meter(sec, ch, mt) + result(r) + if r.get("status") == "success": + raw = r.get("raw", 0) + print(f"\n {meter_bar(raw)}") + pause() + + elif cmd == "2": + section("Meter sezione completa") + cprint(f" Sezioni: {', '.join(SECTIONS)}", C.GRAY) + sec = ask("Sezione", "InCh") + if sec not in SECTIONS: + err(f"Sezione non valida"); continue + defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS + defn = defs.get(sec, {}) + types = defn.get("types", []) + cprint(f" Tipi disponibili: {', '.join(f'{i}={t}' for i,t in enumerate(types))}", C.GRAY) + mt = ask_int("Tipo meter", len(types)-1, 0, len(types)-1) + r = ctrl.get_all_meters(sec, mt) + if r.get("status") == "success": + print() + cprint(f" {'CH':>4} {'RAW':>5} {'dBFS':>8} LIVELLO", C.CYAN, bold=True) + cprint(" " + "─" * 52, C.GRAY) + for rd in r.get("readings", []): + ch = rd["channel"] + raw = rd.get("raw") + db = rd.get("level_db") + db_str = f"{db:+6.1f}" if db is not None and db != float('-inf') else " -inf" + bar = meter_bar(raw, 20) + print(f" {ch:>4} {(raw or 0):>5} {db_str} dB {bar}") + else: + result(r) + pause() + + elif cmd == "3": + section("Snapshot completo") + defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS + mt = ask_int("Tipo meter preferito (verrΓ  clampato per sezione)", 2, 0, 2) + r = ctrl.get_meter_snapshot(mt) + if r.get("status") == "success": + for sec_name, sec_data in r.get("sections", {}).items(): + readings = sec_data.get("readings", []) + type_name = sec_data.get("type_name", "?") + if readings: + avg_raw = sum(rd.get("raw") or 0 for rd in readings) / len(readings) + peak_raw = max((rd.get("raw") or 0 for rd in readings), default=0) + print(f"\n {C.CYAN}{C.BOLD}{sec_name:10}{C.RESET} [{type_name}] " + f"ch:{len(readings)} avg:{avg_raw:5.1f} peak:{meter_bar(peak_raw, 15)}") + else: + result(r) + pause() + + elif cmd == "4": + section("Tipi meter disponibili") + defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS + r = ctrl.get_meter_types() + print() + cprint(f" {'SEZIONE':12} {'CH':>5} TIPI DISPONIBILI", C.CYAN, bold=True) + cprint(" " + "─" * 50, C.GRAY) + if r.get("status") == "success": + for sec_name, data in r.get("sections", {}).items(): + types_str = " ".join(f"{i}={t}" for i, t in data.get("types", {}).items()) + print(f" {sec_name:12} {data.get('channels', '?'):>5} {types_str}") + pause() + + elif cmd == "5": + section("Monitor live meter") + cprint(" Premi CTRL+C per fermare", C.GRAY) + defs = ctrl.METER_DEFS if hasattr(ctrl, 'METER_DEFS') else MockTF5Controller.METER_DEFS + sec = ask("Sezione da monitorare", "InCh") + if sec not in SECTIONS: + err("Sezione non valida"); continue + mt = len(defs.get(sec, {}).get("types", [])) - 1 + interval = ask_float("Intervallo secondi", 1.0) + try: + iteration = 0 + while True: + r = ctrl.get_all_meters(sec, mt) + os.system("clear" if os.name != "nt" else "cls") + header(f"LIVE METER β€” {sec} [{r.get('type_name','?')}]") + cprint(f" Aggiornamento #{iteration+1} Β· {time.strftime('%H:%M:%S')}", C.GRAY) + print() + if r.get("status") == "success": + for rd in r.get("readings", []): + ch = rd["channel"] + raw = rd.get("raw") + db = rd.get("level_db") + db_str = f"{db:+6.1f}" if db is not None and db != float('-inf') else " -inf" + bar = meter_bar(raw, 25) + print(f" ch{ch:02d} {db_str} dBFS {bar}") + time.sleep(interval) + iteration += 1 + except KeyboardInterrupt: + ok("Monitor fermato") + pause() + + +# ───────────────────────────────────────────── +# MENU CANALI +# ───────────────────────────────────────────── +def menu_channels(ctrl): + while True: + header("CANALI INPUT (InCh)") + cprint(" 1) Info canale", C.WHITE) + cprint(" 2) Imposta livello fader", C.WHITE) + cprint(" 3) Abilita / Disabilita canale", C.WHITE) + cprint(" 4) Imposta pan", C.WHITE) + cprint(" 5) Riepilogo tutti i canali", C.WHITE) + cprint(" 6) Cerca canale per nome", C.WHITE) + cprint(" 0) ← Torna al menu principale", C.GRAY) + + cmd = ask("\nScelta") + + if cmd == "0": + break + elif cmd == "1": + ch = ask_int("Numero canale", 1, 1, 40) + result(ctrl.get_channel_info(ch)) + pause() + elif cmd == "2": + ch = ask_int("Numero canale", 1, 1, 40) + db = ask_float("Livello dB (es. -10, 0, +5)", -10) + result(ctrl.set_channel_level(ch, db)) + pause() + elif cmd == "3": + ch = ask_int("Numero canale", 1, 1, 40) + state = ask_bool("Abilitare il canale?", True) + result(ctrl.set_channel_on_off(ch, state)) + pause() + elif cmd == "4": + ch = ask_int("Numero canale", 1, 1, 40) + pan = ask_int("Pan (-63=L, 0=C, +63=R)", 0, -63, 63) + result(ctrl.set_channel_pan(ch, pan)) + pause() + elif cmd == "5": + r = ctrl.get_all_channels_summary() + if r.get("status") == "success": + print() + cprint(f" {'CH':>4} {'NOME':15} {'ON':>4} {'dB':>8}", C.CYAN, bold=True) + cprint(" " + "─" * 40, C.GRAY) + for ch in r.get("channels", []): + on_str = f"{C.GREEN}ON {C.RESET}" if ch["on"] else f"{C.RED}OFF{C.RESET}" + db = ch.get("level_db", -120) + db_str = f"{db:+6.1f}" if db > -120 else " -inf" + print(f" {ch['channel']:>4} {ch['name']:15} {on_str} {db_str} dB") + pause() + elif cmd == "6": + term = ask("Nome da cercare") + result(ctrl.search_channels_by_name(term)) + pause() + + +# ───────────────────────────────────────────── +# MENU MIX BUS +# ───────────────────────────────────────────── +def menu_mixes(ctrl): + while True: + header("MIX BUSES") + cprint(" 1) Info mix bus", C.WHITE) + cprint(" 2) Imposta livello fader", C.WHITE) + cprint(" 3) Abilita / Disabilita mix bus", C.WHITE) + cprint(" 4) Riepilogo tutti i mix", C.WHITE) + cprint(" 0) ← Torna al menu principale", C.GRAY) + + cmd = ask("\nScelta") + + if cmd == "0": + break + elif cmd == "1": + m = ask_int("Numero mix", 1, 1, 20) + result(ctrl.get_mix_info(m)) + pause() + elif cmd == "2": + m = ask_int("Numero mix", 1, 1, 20) + db = ask_float("Livello dB", 0) + result(ctrl.set_mix_level(m, db)) + pause() + elif cmd == "3": + m = ask_int("Numero mix", 1, 1, 20) + state = ask_bool("Abilitare?", True) + result(ctrl.set_mix_on_off(m, state)) + pause() + elif cmd == "4": + r = ctrl.get_all_mixes_summary() + if r.get("status") == "success": + print() + cprint(f" {'MIX':>5} {'NOME':15} {'ON':>4} {'dB':>8}", C.CYAN, bold=True) + cprint(" " + "─" * 40, C.GRAY) + for m in r.get("mixes", []): + on_str = f"{C.GREEN}ON {C.RESET}" if m["on"] else f"{C.RED}OFF{C.RESET}" + db = m.get("level_db", -120) + db_str = f"{db:+6.1f}" if db > -120 else " -inf" + print(f" {m['mix']:>5} {m['name']:15} {on_str} {db_str} dB") + pause() + + +# ───────────────────────────────────────────── +# MENU DCA +# ───────────────────────────────────────────── +def menu_dca(ctrl): + while True: + header("DCA GROUPS") + cprint(" 1) Info DCA", C.WHITE) + cprint(" 2) Imposta livello", C.WHITE) + cprint(" 3) Abilita / Disabilita DCA", C.WHITE) + cprint(" 4) Riepilogo tutti i DCA", C.WHITE) + cprint(" 0) ← Torna al menu principale", C.GRAY) + + cmd = ask("\nScelta") + + if cmd == "0": + break + elif cmd == "1": + d = ask_int("Numero DCA", 1, 1, 8) + result(ctrl.get_dca_info(d)) + pause() + elif cmd == "2": + d = ask_int("Numero DCA", 1, 1, 8) + db = ask_float("Livello dB", 0) + result(ctrl.set_dca_level(d, db)) + pause() + elif cmd == "3": + d = ask_int("Numero DCA", 1, 1, 8) + state = ask_bool("Abilitare?", True) + result(ctrl.set_dca_on_off(d, state)) + pause() + elif cmd == "4": + r = ctrl.get_all_dca_summary() + if r.get("status") == "success": + print() + cprint(f" {'DCA':>5} {'NOME':15} {'ON':>4} {'dB':>8}", C.CYAN, bold=True) + cprint(" " + "─" * 40, C.GRAY) + for d in r.get("dcas", []): + on_str = f"{C.GREEN}ON {C.RESET}" if d["on"] else f"{C.RED}OFF{C.RESET}" + db = d.get("level_db", 0) + print(f" {d['dca']:>5} {d['name']:15} {on_str} {db:+6.1f} dB") + pause() + + +# ───────────────────────────────────────────── +# MENU BUS MASTER +# ───────────────────────────────────────────── +def menu_masters(ctrl): + while True: + header("BUS MASTER (Stereo / Mono)") + cprint(" 1) Info Stereo bus", C.WHITE) + cprint(" 2) Imposta livello Stereo", C.WHITE) + cprint(" 3) Info Mono bus", C.WHITE) + cprint(" 4) Imposta livello Mono", C.WHITE) + cprint(" 5) Mute Masters β€” stato", C.WHITE) + cprint(" 6) Attiva / Disattiva Mute Master group", C.WHITE) + cprint(" 0) ← Torna al menu principale", C.GRAY) + + cmd = ask("\nScelta") + + if cmd == "0": + break + elif cmd == "1": + bus = ask_int("Bus stereo (1=L, 2=R)", 1, 1, 2) + result(ctrl.get_stereo_info(bus)) + pause() + elif cmd == "2": + bus = ask_int("Bus stereo (1=L, 2=R)", 1, 1, 2) + db = ask_float("Livello dB", 0) + result(ctrl.set_stereo_level(bus, db)) + pause() + elif cmd == "3": + result(ctrl.get_mono_info()) + pause() + elif cmd == "4": + db = ask_float("Livello dB", -6) + result(ctrl.set_mono_level(db)) + pause() + elif cmd == "5": + result(ctrl.get_all_mute_masters_summary()) + pause() + elif cmd == "6": + g = ask_int("Numero gruppo (1-6)", 1, 1, 6) + state = ask_bool("Attivare il mute?", False) + result(ctrl.set_mute_master(g, state)) + pause() + + +# ───────────────────────────────────────────── +# MENU COMANDI RAW +# ───────────────────────────────────────────── +def menu_raw(ctrl): + """Invia comandi raw al mixer (solo modalitΓ  reale).""" + if isinstance(ctrl, MockTF5Controller): + warn("I comandi raw non sono disponibili in modalitΓ  simulazione.") + pause() + return + + header("COMANDI RAW") + cprint(" Invia comandi testuali diretti al mixer.", C.GRAY) + cprint(" Esempi:", C.GRAY) + cprint(" get MIXER:Current/InCh/Fader/Level 0 0", C.GRAY) + cprint(" set MIXER:Current/InCh/Fader/On 0 0 0", C.GRAY) + cprint(" get MIXER:Current/Meter/InCh 0 2", C.GRAY) + cprint(" Digita 'exit' per uscire.", C.GRAY) + print() + + while True: + try: + cmd = input(f"{C.MAGENTA} RAW> {C.RESET}").strip() + if not cmd: + continue + if cmd.lower() in ("exit", "quit", "0"): + break + response = ctrl._send_command(cmd) + cprint(f" ← {response}", C.YELLOW) + except KeyboardInterrupt: + break + + +# ───────────────────────────────────────────── +# MENU SCENE +# ───────────────────────────────────────────── +def menu_scenes(ctrl): + while True: + header("SCENE") + cprint(" 1) Richiama scena (banco A o B)", C.WHITE) + cprint(" 2) Refresh cache", C.WHITE) + cprint(" 0) ← Torna al menu principale", C.GRAY) + + cmd = ask("\nScelta") + + if cmd == "0": + break + elif cmd == "1": + bank = ask("Banco (a/b)", "a").lower() + num = ask_int("Numero scena (0-99)", 0, 0, 99) + result(ctrl.recall_scene(bank, num)) + pause() + elif cmd == "2": + ok("Avvio refresh cache...") + result(ctrl.refresh_cache()) + pause() + + +# ───────────────────────────────────────────── +# MENU ALTRI CANALI +# ───────────────────────────────────────────── +def menu_other_channels(ctrl): + while True: + header("ALTRI CANALI") + cprint(" 1) Stereo Input (StInCh) β€” info", C.WHITE) + cprint(" 2) FX Return (FxRtnCh) β€” info", C.WHITE) + cprint(" 0) ← Torna al menu principale", C.GRAY) + + cmd = ask("\nScelta") + + if cmd == "0": + break + elif cmd == "1": + ch = ask_int("Canale StInCh (1-4)", 1, 1, 4) + result(ctrl.get_steinch_info(ch)) + pause() + elif cmd == "2": + ch = ask_int("Canale FxRtnCh (1-4)", 1, 1, 4) + result(ctrl.get_fxrtn_info(ch)) + pause() + + +# ───────────────────────────────────────────── +# HELP / REFERENCE +# ───────────────────────────────────────────── +def show_help(): + header("REFERENCE β€” PROTOCOLLO TF5") + section("Struttura comandi") + print(""" + get β†’ legge un parametro + set β†’ scrive un parametro + ssrecall_ex scene_a β†’ richiama scena banco A + + Risposta OK: OK + Risposta ERR: ERROR + Notify: NOTIFY +""") + section("Livelli fader") + print(""" + Valore interno: intero Γ— 100 (es. -1000 = -10.0 dB) + Minimo: -32768 = -∞ (fader giΓΉ) + Massimo: +1000 = +10.0 dB + Unity gain: 0 = 0.0 dB +""") + section("Meter") + print(""" + Valore raw: 0–127 (intero) + Conversione: dBFS = (raw/127) Γ— 90 - 72 + 0 β†’ -inf dBFS + 76 β†’ ~ 0 dBFS (approssimato) + 127β†’ +18 dBFS + + SEZIONI METER: + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ InCh β”‚ 40 β”‚ 0=PreHPF 1=PreFader 2=PostOn β”‚ + β”‚ StInCh β”‚ 4 β”‚ 0=PreEQ 1=PreFader 2=PostOn β”‚ + β”‚ FxRtnCh β”‚ 4 β”‚ 0=PreFader 1=PostOn β”‚ + β”‚ Mix β”‚ 20 β”‚ 0=PreEQ 1=PreFader 2=PostOn β”‚ + β”‚ Mtrx β”‚ 4 β”‚ 0=PreEQ 1=PreFader 2=PostOn β”‚ + β”‚ St β”‚ 2 β”‚ 0=PreEQ 1=PreFader 2=PostOn β”‚ + β”‚ Mono β”‚ 1 β”‚ 0=PreEQ 1=PreFader 2=PostOn β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +""") + section("Pan") + print(""" + Valore: -63 (full left) β†’ 0 (center) β†’ +63 (full right) +""") + pause() + + +# ───────────────────────────────────────────── +# MAIN / SETUP +# ───────────────────────────────────────────── +def startup_screen(): + os.system("clear" if os.name != "nt" else "cls") + print() + cprint("╔══════════════════════════════════════════════════════════╗", C.CYAN) + cprint("β•‘ β•‘", C.CYAN) + cprint("β•‘ YAMAHA TF5 β€” CLI INTERATTIVO DI TEST β•‘", C.CYAN, bold=True) + cprint("β•‘ Socket Protocol Explorer β•‘", C.CYAN) + cprint("β•‘ β•‘", C.CYAN) + cprint("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•", C.CYAN) + print() + +def choose_mode(): + cprint(" ModalitΓ  di connessione:", C.WHITE, bold=True) + cprint(" 1) SIMULAZIONE (mock β€” nessun mixer necessario)", C.GREEN) + cprint(" 2) REALE (connessione socket al TF5)", C.YELLOW) + print() + choice = ask("Scelta", "1") + + if choice == "2": + host = ask("Indirizzo IP del mixer", "192.168.0.128") + port = ask_int("Porta", 49280, 1, 65535) + info(f"Connessione a {host}:{port}...") + ctrl, error = try_load_real_controller(host, port) + if ctrl is None: + err(f"Impossibile connettersi: {error}") + warn("Avvio in modalitΓ  simulazione come fallback.") + return MockTF5Controller() + ok(f"Connesso a {host}:{port}") + return ctrl + else: + return MockTF5Controller() + + +def main(): + startup_screen() + ctrl = choose_mode() + + while True: + header("MENU PRINCIPALE") + cprint(f" ModalitΓ : {'SIMULAZIONE (mock)' if isinstance(ctrl, MockTF5Controller) else 'REALE'}", C.GRAY) + print() + cprint(" M) Meter reading ──────────── leggi livelli meter", C.MAGENTA, bold=True) + cprint(" C) Canali input ────────────── InCh 1-40", C.WHITE) + cprint(" X) Mix buses ───────────────── MX 1-20", C.WHITE) + cprint(" D) DCA groups ──────────────── DCA 1-8", C.WHITE) + cprint(" B) Bus master ──────────────── Stereo / Mono / Mute", C.WHITE) + cprint(" A) Altri canali ────────────── StInCh / FxRtnCh", C.WHITE) + cprint(" S) Scene ───────────────────── recall / store", C.WHITE) + cprint(" R) Comandi RAW ─────────────── protocollo diretto", C.WHITE) + cprint(" H) Help / Reference ────────── documentazione", C.GRAY) + cprint(" Q) Esci", C.RED) + + cmd = ask("\nScelta").upper() + + if cmd == "Q": + cprint("\n Chiusura in corso...", C.GRAY) + ctrl.close() + cprint(" Arrivederci! 🎚️\n", C.CYAN) + break + elif cmd == "M": + menu_meter(ctrl) + elif cmd == "C": + menu_channels(ctrl) + elif cmd == "X": + menu_mixes(ctrl) + elif cmd == "D": + menu_dca(ctrl) + elif cmd == "B": + menu_masters(ctrl) + elif cmd == "A": + menu_other_channels(ctrl) + elif cmd == "S": + menu_scenes(ctrl) + elif cmd == "R": + menu_raw(ctrl) + elif cmd == "H": + show_help() + else: + warn("Scelta non valida") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print(f"\n\n{C.GRAY} Interrotto dall'utente.{C.RESET}\n") + sys.exit(0) \ No newline at end of file