From 0ed2435536ba7b1a647a842acb1dd504dbe92242 Mon Sep 17 00:00:00 2001 From: Nick Date: Mon, 27 Oct 2025 20:13:23 +0100 Subject: [PATCH] refactoring --- .tf5_mixer_cache/channels_cache.json | 407 ++++++++++ __pycache__/config.cpython-312.pyc | Bin 0 -> 458 bytes __pycache__/mixer_controller.cpython-312.pyc | Bin 2055 -> 31355 bytes config.py | 13 + mixer_agent.py | 541 +------------ mixer_controller.py | 809 ++++++++++++++++++- 6 files changed, 1227 insertions(+), 543 deletions(-) create mode 100644 .tf5_mixer_cache/channels_cache.json create mode 100644 __pycache__/config.cpython-312.pyc create mode 100644 config.py diff --git a/.tf5_mixer_cache/channels_cache.json b/.tf5_mixer_cache/channels_cache.json new file mode 100644 index 0000000..be269e5 --- /dev/null +++ b/.tf5_mixer_cache/channels_cache.json @@ -0,0 +1,407 @@ +{ + "channels": { + "1": { + "channel": 1, + "name": "Sconosciuto", + "on": false, + "level_db": 0.65, + "pan": 0 + }, + "2": { + "channel": 2, + "name": "Gelato", + "on": true, + "level_db": -6.25, + "pan": 0 + }, + "3": { + "channel": 3, + "name": "Talkback", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "4": { + "channel": 4, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "5": { + "channel": 5, + "name": "Vox1", + "on": true, + "level_db": 0.65, + "pan": 0 + }, + "6": { + "channel": 6, + "name": "Vox2", + "on": true, + "level_db": -1.1, + "pan": 0 + }, + "7": { + "channel": 7, + "name": "Basso", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "8": { + "channel": 8, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "9": { + "channel": 9, + "name": "Kick", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "10": { + "channel": 10, + "name": "Snare", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "11": { + "channel": 11, + "name": "Tom 1", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "12": { + "channel": 12, + "name": "Tom 2", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "13": { + "channel": 13, + "name": "Tom3", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "14": { + "channel": 14, + "name": "Pan SX", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "15": { + "channel": 15, + "name": "Pan dx", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "16": { + "channel": 16, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "17": { + "channel": 17, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "18": { + "channel": 18, + "name": "Archetto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "19": { + "channel": 19, + "name": "Vox 5", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "20": { + "channel": 20, + "name": "Tast", + "on": true, + "level_db": 1.0, + "pan": 0 + }, + "21": { + "channel": 21, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "22": { + "channel": 22, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "23": { + "channel": 23, + "name": " Vox3", + "on": true, + "level_db": 3.8, + "pan": 0 + }, + "24": { + "channel": 24, + "name": "Chit cnt", + "on": true, + "level_db": -2.1, + "pan": 0 + }, + "25": { + "channel": 25, + "name": "Chit dx", + "on": true, + "level_db": -7.7, + "pan": 0 + }, + "26": { + "channel": 26, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "27": { + "channel": 27, + "name": "Vox 4", + "on": false, + "level_db": -132.0, + "pan": 0 + }, + "28": { + "channel": 28, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "29": { + "channel": 29, + "name": "Pad", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "30": { + "channel": 30, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "31": { + "channel": 31, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "32": { + "channel": 32, + "name": "Sconosciuto", + "on": false, + "level_db": -Infinity, + "pan": 0 + }, + "33": { + "channel": 33, + "name": "PC", + "on": true, + "level_db": -19.8, + "pan": -63 + }, + "34": { + "channel": 34, + "name": "PC", + "on": true, + "level_db": -19.8, + "pan": 63 + }, + "35": { + "channel": 35, + "name": "ch35", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "36": { + "channel": 36, + "name": "ch36", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "37": { + "channel": 37, + "name": "ch37", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "38": { + "channel": 38, + "name": "ch38", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "39": { + "channel": 39, + "name": "ch39", + "on": true, + "level_db": -Infinity, + "pan": 0 + }, + "40": { + "channel": 40, + "name": "ch40", + "on": true, + "level_db": -Infinity, + "pan": 0 + } + }, + "mixes": { + "1": { + "mix": 1, + "name": "Sinistro", + "on": true, + "level_db": -Infinity + }, + "2": { + "mix": 2, + "name": "Aux 2", + "on": true, + "level_db": -0.05 + }, + "3": { + "mix": 3, + "name": "Aux 3", + "on": true, + "level_db": -0.5 + }, + "4": { + "mix": 4, + "name": "Aux 4", + "on": true, + "level_db": -0.6 + }, + "5": { + "mix": 5, + "name": "batteria", + "on": true, + "level_db": 1.35 + }, + "6": { + "mix": 6, + "name": "Aux 6", + "on": true, + "level_db": 0.9 + }, + "7": { + "mix": 7, + "name": "Destro", + "on": true, + "level_db": -2.7 + }, + "8": { + "mix": 8, + "name": "Aux 8", + "on": true, + "level_db": -0.15 + }, + "9": { + "mix": 9, + "name": "Aux 9/10", + "on": true, + "level_db": -8.1 + }, + "10": { + "mix": 10, + "name": "Aux 9/10", + "on": true, + "level_db": -8.1 + }, + "11": { + "mix": 11, + "name": "Aux Pulp", + "on": true, + "level_db": -10.3 + }, + "12": { + "mix": 12, + "name": "Aux Pulp", + "on": true, + "level_db": -10.3 + }, + "13": { + "mix": 13, + "name": "Aux13/14", + "on": false, + "level_db": -1.6 + }, + "14": { + "mix": 14, + "name": "Aux13/14", + "on": false, + "level_db": -1.6 + }, + "15": { + "mix": 15, + "name": "Aux15/16", + "on": true, + "level_db": 0.1 + }, + "16": { + "mix": 16, + "name": "Aux15/16", + "on": true, + "level_db": 0.1 + }, + "17": { + "mix": 17, + "name": "Diretta", + "on": true, + "level_db": 0.0 + }, + "18": { + "mix": 18, + "name": "Diretta", + "on": true, + "level_db": 0.0 + }, + "19": { + "mix": 19, + "name": "Traduzio", + "on": true, + "level_db": 6.0 + }, + "20": { + "mix": 20, + "name": "Traduzio", + "on": true, + "level_db": 6.0 + } + }, + "timestamp": 1761592356.5132928 +} \ No newline at end of file diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..674be36b5410dd87dd065f1b410bf3069737fef0 GIT binary patch literal 458 zcmX@j%ge<81P>4Y&oEv6vdRn9L1c%lERw9 zlFJ&!%E-XPkjk3Ewi;*x2t=`^vZb(BaRXWGDI8H8m7JPfRXm24MtX*37J7zyW=5F} z2N)P$Y5>)~6af)H%%I75izOhjB%?||uO!VhJ~y)>wJ1J0F*zf(N;o+qF)uGQrx?Q5 z%PP*#%M{>aVDQuAyv5_<>gE{g6B6&?9~^?<1^5Sr+!6|LGmZE33kVH~clL1f^Kf33Qi&yPI>2OMY@`ZfaghaSYHeN%Ble5V|2K s+`#vgLE<9^1Fzr(LFLOlDh-@Bc*QPA>Rsg3Yv8&eDB8$Z#1GUC0Ivajw*UYD literal 0 HcmV?d00001 diff --git a/__pycache__/mixer_controller.cpython-312.pyc b/__pycache__/mixer_controller.cpython-312.pyc index 826c57f2c202a833bd5811c3ab031716f576a6e9..21741aba802e0706e769971ff02a8f4f0c36fae0 100644 GIT binary patch literal 31355 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 literal 2055 zcmZ`)PfQb87=JUSyj%?u2!yY)2sHeTOYYk+U!zP}*5jAU~m;GkifvWLM^1k2u-tYVO zzL__Fxm->JcfNL_%N1&i}&g-dbal1~|!*(^k75HO6~J{l<|#m=%Hu*yE7z%qp}n*kuqqB8v-C%DvjA6Y zMbS%Bwojtl!rmh*3t^`fT`k9DDH|3ZvJ#73#$!cg&;-KepWkah?T9jqEQ2Ny1JAN+ ziJL@=Oha)WYpsY!@-$K#SNNi=-6;mLu1K*kSEl#}=+WWXFbG$f8g|7Fx!Ci0P9p4D z;cxPgpD7m^?YQM1yr22Ndv)I4suw(#YbPvrinsKi^Sp@iRrx&0Ri_AJP$eNT9t(+* z5{nT^WhF*hL^+m_4Yf$+*qjnkU^g0yVWmY>Wr-w+7zqKQ3kuajqPddhSS~&hF(m3tZ_*}0iplzsC=^cR8*&8r#<6J{QK?*%Y_huGPL3|9 zBq38x=@or$h_6UL5KS)`7@irq5(w&L6JvvyXC{K8AxAKl}H4dy|AncMiHPhBde#uuW_5v0Nih@=)hUU zckR2XZ(m=#{;>0R|8M@S&fVtzY;%9k<=?5>6I{1P*GBJG{yb*Dk+qRK+Wp|2NKR<5 z){E<&yEk`hTC+8+8*|TUI&(tTzEGL*bfmk|zU}r*Rew(K?|EzQCGIBf<6l#|-lnX# zX`^eqJL7G7<{jGgUdnneJ@by1I@0>|F_Hk{!I-Ms&4nK^}RwI-@ET{Wh%Z* zd(!rFXxo-fYzKGhcB(Vwft+J_ucmvua+}L|`fio(9k1J1&boV^J9^&lmw8?x7S6@6 zZCD>zKbI*#n{zY*XN#uWHxpTR=W|Er`vV?G@8~2P=Z_sj9QyM@^I#YE_*5z2$3B~Z zTTR@>522FBeH`eYAamxb1I^r%niE4k+>>*RfqMj)pYmS>&TvmfL-X_u53`R~jb8%1 zLCaJZh62L_*G7Xg7sn=oN2{x2<3Y2rf1uaEMQVd#;pmGhI36K>wF#ES-;CD5bigqT i^8&g4LCym^syMkS?72Ow!oR1Q(=Bh1m+4Y{!1^EJiTB0; diff --git a/config.py b/config.py new file mode 100644 index 0000000..2271daa --- /dev/null +++ b/config.py @@ -0,0 +1,13 @@ +# Configurazione mixer +DEFAULT_HOST = "192.168.1.62" +DEFAULT_PORT = 49280 +TF5_INPUT_CHANNELS = 40 +TF5_MIX_BUSSES = 20 + +from pathlib import Path + + +# Configurazione cache +CACHE_DIR = Path(".tf5_mixer_cache") +CACHE_FILE = CACHE_DIR / "channels_cache.json" +CACHE_DURATION = 3600 # 60 minuti in secondi \ No newline at end of file diff --git a/mixer_agent.py b/mixer_agent.py index b467eb5..7fd84b6 100644 --- a/mixer_agent.py +++ b/mixer_agent.py @@ -13,513 +13,12 @@ from pathlib import Path from typing import List, Optional from google import genai from google.genai import types - from dotenv import load_dotenv +from mixer_controller import TF5MixerController + load_dotenv() -# Configurazione mixer -DEFAULT_HOST = "192.168.1.62" -DEFAULT_PORT = 49280 -TF5_INPUT_CHANNELS = 40 -TF5_MIX_BUSSES = 20 - -# Configurazione cache -CACHE_DIR = Path.home() / ".tf5_mixer_cache" -CACHE_FILE = CACHE_DIR / "channels_cache.json" -CACHE_DURATION = 3600 # 60 minuti in secondi - - -class TF5MixerController: - def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT): - self.host = host - self.port = port - self.socket = None - self._ensure_cache_dir() - self._cache = self._load_cache() - - def _connect(self): - """Stabilisce la connessione se non già connesso.""" - if self.socket is None: - print('Inizializzazione socket...') - try: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.socket.settimeout(5) - self.socket.connect((self.host, self.port)) - except socket.error as e: - self.socket = None - raise ConnectionError(f"Impossibile connettersi al mixer: {e}") - - def _disconnect(self): - """Chiude la connessione.""" - if self.socket: - print('Chiusura socket...') - try: - self.socket.close() - print('Socket chiuso!') - except: - print('Errore durante chiusura socket!') - pass - finally: - self.socket = None - - def _send_command(self, command: str) -> str: - """Invia un comando al mixer e restituisce la risposta.""" - max_retries = 2 - - for attempt in range(max_retries): - try: - print(f'Tentativo di connessione {attempt} per {command}') - self._connect() - self.socket.sendall((command + '\n').encode('utf-8')) - response = self.socket.recv(4096) - decoded = response.decode('utf-8', errors='ignore').strip() - print(f'Risposta {decoded}') - return decoded - - except socket.error as e: - print(f'Errore di connessione dopo {max_retries} tentativi: {e}') - self._disconnect() # Forza riconnessione al prossimo tentativo - - if attempt < max_retries - 1: - time.sleep(0.1) - continue - else: - return f"Errore di connessione dopo {max_retries} tentativi: {e}" - - def close(self): - """Chiude la connessione (da chiamare alla fine).""" - self._disconnect() - - def __enter__(self): - """Context manager entry.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Context manager exit.""" - self.close() - - def _ensure_cache_dir(self): - """Crea la directory di cache se non esiste.""" - CACHE_DIR.mkdir(parents=True, exist_ok=True) - - def _load_cache(self) -> dict: - """Carica la cache dal file.""" - if CACHE_FILE.exists(): - try: - with open(CACHE_FILE, 'r', encoding='utf-8') as f: - return json.load(f) - except Exception as e: - print(f"⚠️ Errore nel caricamento della cache: {e}") - return {"channels": {}, "timestamp": 0} - - def _save_cache(self): - """Salva la cache nel file.""" - try: - with open(CACHE_FILE, 'w', encoding='utf-8') as f: - json.dump(self._cache, f, indent=2, ensure_ascii=False) - except Exception as e: - print(f"⚠️ Errore nel salvataggio della cache: {e}") - - def _is_cache_valid(self) -> bool: - """Verifica se la cache è ancora valida.""" - if not self._cache.get("channels"): - return False - cache_age = time.time() - self._cache.get("timestamp", 0) - return cache_age < CACHE_DURATION - - def _parse_name(self, response: str) -> str: - """Estrae il nome tra virgolette dalla risposta.""" - try: - start = response.find('"') + 1 - end = response.rfind('"') - if start > 0 and end > start: - return response[start:end] - return "Sconosciuto" - except: - return "Errore" - - def _parse_value(self, response: str) -> str: - """Estrae l'ultimo valore da una risposta OK.""" - parts = response.split() - if len(parts) > 0 and parts[0] == "OK": - return parts[-1] - return "N/A" - - def refresh_cache(self) -> dict: - """Aggiorna la cache leggendo tutti i canali dal mixer. - - Returns: - Un dizionario con lo stato dell'operazione - """ - print("🔄 Aggiornamento cache in corso...") - channels_data = {} - - for ch in range(1, TF5_INPUT_CHANNELS + 1): - ch_idx = ch - 1 - - # Leggi nome - resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0") - name = self._parse_name(resp_name) - - # Leggi stato ON/OFF - resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0") - is_on = self._parse_value(resp_on) == "1" - - # Leggi livello - resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0") - level_raw = self._parse_value(resp_level) - try: - level_int = int(level_raw) - level_db = level_int / 100.0 if level_int > -32768 else float('-inf') - except: - level_db = None - - # Leggi pan - resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0") - pan_raw = self._parse_value(resp_pan) - try: - pan_value = int(pan_raw) - except: - pan_value = None - - channels_data[str(ch)] = { - "channel": ch, - "name": name, - "on": is_on, - "level_db": level_db, - "pan": pan_value - } - - # Piccolo delay per non sovraccaricare il mixer - time.sleep(0.05) - - self._cache = { - "channels": channels_data, - "timestamp": time.time() - } - self._save_cache() - - print(f"✅ Cache aggiornata con {len(channels_data)} canali") - return { - "status": "success", - "message": f"Cache aggiornata con {len(channels_data)} canali", - "channels_count": len(channels_data) - } - - def recall_scene(self, bank: str, scene_number: int) -> dict: - """Richiama una scena dal banco A o B. - - Args: - bank: Il banco della scena ('a' o 'b') - scene_number: Il numero della scena (0-99) - - Returns: - Un dizionario con lo stato dell'operazione - """ - if bank.lower() not in ['a', 'b']: - return {"status": "error", "message": "Il banco deve essere 'a' o 'b'"} - if not 0 <= scene_number <= 99: - return {"status": "error", "message": "Il numero scena deve essere tra 0 e 99"} - - command = f"ssrecall_ex scene_{bank.lower()} {scene_number}" - response = self._send_command(command) - - # Invalida la cache dopo il cambio scena - self._cache["timestamp"] = 0 - - return { - "status": "success" if "OK" in response else "error", - "message": f"Scena {bank.upper()}{scene_number} richiamata. Cache invalidata.", - "response": response - } - - def set_channel_level(self, channel: int, level_db: float) -> dict: - """Imposta il livello del fader di un canale in dB. - - Args: - channel: Numero del canale (1-40) - level_db: Livello in dB (da -inf a +10.0) - - Returns: - Un dizionario con lo stato dell'operazione - """ - if not 1 <= channel <= TF5_INPUT_CHANNELS: - return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} - - # Converti dB in valore interno (moltiplicato per 100) - if level_db <= -138: - internal_value = -32768 # -inf - else: - internal_value = int(level_db * 100) - - command = f"set MIXER:Current/InCh/Fader/Level {channel-1} 0 {internal_value}" - response = self._send_command(command) - - # Aggiorna cache locale - if str(channel) in self._cache.get("channels", {}): - self._cache["channels"][str(channel)]["level_db"] = level_db - - return { - "status": "success" if "OK" in response else "error", - "message": f"Canale {channel} impostato a {level_db:+.1f} dB", - "response": response - } - - def set_channel_on_off(self, channel: int, state: bool) -> dict: - """Accende o spegne un canale. - - Args: - channel: Numero del canale (1-40) - state: True per accendere, False per spegnere - - Returns: - Un dizionario con lo stato dell'operazione - """ - if not 1 <= channel <= TF5_INPUT_CHANNELS: - return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} - - value = 1 if state else 0 - command = f"set MIXER:Current/InCh/Fader/On {channel-1} 0 {value}" - response = self._send_command(command) - - # Aggiorna cache locale - if str(channel) in self._cache.get("channels", {}): - self._cache["channels"][str(channel)]["on"] = state - - return { - "status": "success" if "OK" in response else "error", - "message": f"Canale {channel} {'acceso' if state else 'spento'}", - "response": response - } - - def set_channel_pan(self, channel: int, pan_value: int) -> dict: - """Imposta il pan di un canale. - - Args: - channel: Numero del canale (1-40) - pan_value: Valore pan da -63 (sinistra) a +63 (destra), 0 è centro - - Returns: - Un dizionario con lo stato dell'operazione - """ - if not 1 <= channel <= TF5_INPUT_CHANNELS: - return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} - if not -63 <= pan_value <= 63: - return {"status": "error", "message": "Il pan deve essere tra -63 e +63"} - - command = f"set MIXER:Current/InCh/ToSt/Pan {channel-1} 0 {pan_value}" - response = self._send_command(command) - - # Aggiorna cache locale - if str(channel) in self._cache.get("channels", {}): - self._cache["channels"][str(channel)]["pan"] = pan_value - - pan_desc = "centro" - if pan_value < 0: - pan_desc = f"sinistra {abs(pan_value)}" - elif pan_value > 0: - pan_desc = f"destra {pan_value}" - - return { - "status": "success" if "OK" in response else "error", - "message": f"Canale {channel} pan impostato a {pan_desc}", - "response": response - } - - def set_mix_level(self, mix_number: int, level_db: float) -> dict: - """Imposta il livello di un mix/aux. - - Args: - mix_number: Numero del mix (1-20) - level_db: Livello in dB (da -inf a +10.0) - - Returns: - Un dizionario con lo stato dell'operazione - """ - if not 1 <= mix_number <= TF5_MIX_BUSSES: - return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"} - - if level_db <= -138: - internal_value = -32768 - else: - internal_value = int(level_db * 100) - - command = f"set MIXER:Current/Mix/Fader/Level {mix_number-1} 0 {internal_value}" - response = self._send_command(command) - return { - "status": "success" if "OK" in response else "error", - "message": f"Mix {mix_number} impostato a {level_db:+.1f} dB", - "response": response - } - - def mute_multiple_channels(self, channels: List[int]) -> dict: - """Muta più canali contemporaneamente. - - Args: - channels: Lista di numeri di canale da mutare (es: [1, 2, 5, 8]) - - Returns: - Un dizionario con lo stato dell'operazione - """ - results = [] - for ch in channels: - result = self.set_channel_on_off(ch, False) - results.append(result) - - success_count = sum(1 for r in results if r["status"] == "success") - return { - "status": "success" if success_count == len(channels) else "partial", - "message": f"Mutati {success_count}/{len(channels)} canali: {channels}", - "details": results - } - - def unmute_multiple_channels(self, channels: List[int]) -> dict: - """Riattiva più canali contemporaneamente. - - Args: - channels: Lista di numeri di canale da riattivare (es: [1, 2, 5, 8]) - - Returns: - Un dizionario con lo stato dell'operazione - """ - results = [] - for ch in channels: - result = self.set_channel_on_off(ch, True) - results.append(result) - - success_count = sum(1 for r in results if r["status"] == "success") - return { - "status": "success" if success_count == len(channels) else "partial", - "message": f"Riattivati {success_count}/{len(channels)} canali: {channels}", - "details": results - } - - def get_channel_info(self, channel: int, force_refresh: bool = False) -> dict: - """Legge le informazioni di un canale (nome, livello, stato, pan). - Usa la cache se disponibile e valida. - - Args: - channel: Numero del canale (1-40) - force_refresh: Se True, ignora la cache e legge dal mixer - - Returns: - Un dizionario con tutte le informazioni del canale - """ - if not 1 <= channel <= TF5_INPUT_CHANNELS: - return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} - - # Usa cache se valida e non forzato il refresh - if not force_refresh and self._is_cache_valid(): - cached_data = self._cache.get("channels", {}).get(str(channel)) - if cached_data: - return { - "status": "success", - "source": "cache", - **cached_data - } - - # Altrimenti leggi dal mixer - ch_idx = channel - 1 - - resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0") - name = self._parse_name(resp_name) - - resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0") - is_on = self._parse_value(resp_on) == "1" - - resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0") - level_raw = self._parse_value(resp_level) - try: - level_int = int(level_raw) - level_db = level_int / 100.0 if level_int > -32768 else float('-inf') - except: - level_db = None - - resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0") - pan_raw = self._parse_value(resp_pan) - try: - pan_value = int(pan_raw) - except: - pan_value = None - - return { - "status": "success", - "source": "mixer", - "channel": channel, - "name": name, - "on": is_on, - "level_db": level_db, - "pan": pan_value - } - - def search_channels_by_name(self, search_term: str) -> dict: - """Cerca canali il cui nome contiene un determinato termine. - Usa la cache per velocizzare la ricerca. - - Args: - search_term: Il termine da cercare nei nomi dei canali (case-insensitive) - - Returns: - Un dizionario con la lista dei canali trovati - """ - # Aggiorna cache se non valida - if not self._is_cache_valid(): - self.refresh_cache() - - search_lower = search_term.lower() - found_channels = [] - - for ch_str, info in self._cache.get("channels", {}).items(): - if search_lower in info.get("name", "").lower(): - found_channels.append({ - "channel": info["channel"], - "name": info["name"], - "on": info["on"], - "level_db": info["level_db"] - }) - - # Ordina per numero canale - found_channels.sort(key=lambda x: x["channel"]) - - return { - "status": "success", - "search_term": search_term, - "found_count": len(found_channels), - "channels": found_channels, - "message": f"Trovati {len(found_channels)} canali contenenti '{search_term}'" - } - - def get_all_channels_summary(self) -> dict: - """Ottiene un riepilogo di tutti i canali con nome e stato. - Usa la cache per velocizzare. - - Returns: - Un dizionario con il riepilogo di tutti i canali - """ - # Aggiorna cache se non valida - if not self._is_cache_valid(): - self.refresh_cache() - - channels = [] - for ch_str, info in self._cache.get("channels", {}).items(): - channels.append({ - "channel": info["channel"], - "name": info["name"], - "on": info["on"] - }) - - # Ordina per numero canale - channels.sort(key=lambda x: x["channel"]) - - cache_age = time.time() - self._cache.get("timestamp", 0) - - return { - "status": "success", - "total_channels": len(channels), - "channels": channels, - "cache_age_seconds": int(cache_age), - "message": f"Riepilogo di {len(channels)} canali (cache: {int(cache_age)}s fa)" - } +from config import * class TF5AIAgent: @@ -543,11 +42,15 @@ class TF5AIAgent: self.controller.set_channel_on_off, self.controller.set_channel_pan, self.controller.set_mix_level, + self.controller.set_mix_on_off, self.controller.mute_multiple_channels, self.controller.unmute_multiple_channels, self.controller.get_channel_info, + self.controller.get_mix_info, self.controller.search_channels_by_name, + self.controller.search_mixes_by_name, self.controller.get_all_channels_summary, + self.controller.get_all_mixes_summary, self.controller.refresh_cache, ], temperature=0, @@ -564,9 +67,11 @@ Il mixer ha: - Scene memorizzate nei banchi A e B (da 0 a 99) IMPORTANTE - Sistema di Cache: -- Le info sui canali sono salvate per 5 minuti per non sovraccaricare il mixer -- Usa search_channels_by_name e get_all_channels_summary per cercare velocemente -- Quando si carica una scena, i dati vengono aggiornati automaticamente +- Le info sui canali e mix sono salvate per 60 minuti per non sovraccaricare il mixer +- Usa search_channels_by_name e search_mixes_by_name per cercare velocemente +- Usa get_all_channels_summary e get_all_mixes_summary per vedere tutto +- Quando modifichi un canale/mix, la cache viene aggiornata automaticamente +- Quando si carica una scena, i dati vengono invalidati e aggiornati alla prossima richiesta - Puoi fare refresh_cache solo se l'utente lo chiede esplicitamente Come interpretare le richieste: @@ -582,8 +87,8 @@ VOLUME/LIVELLO: - "silenzio/muto" → spegni il canale ON/OFF: -- "accendi/attiva/apri" → canale ON -- "spegni/muta/chiudi/stacca" → canale OFF +- "accendi/attiva/apri" → canale/mix ON +- "spegni/muta/chiudi/stacca" → canale/mix OFF - "muto" può significare sia spegnere che abbassare molto BILANCIAMENTO (PAN): @@ -592,12 +97,13 @@ BILANCIAMENTO (PAN): - "al centro" → pan 0 - "un po' a sinistra" → pan -30 circa -IDENTIFICAZIONE CANALI: -- Accetta sia numeri ("canale 5") che nomi ("il microfono del cantante") -- Se non trovi un canale per nome, cerca usando search_channels_by_name +IDENTIFICAZIONE CANALI E MIX: +- Accetta sia numeri ("canale 5", "mix 3") che nomi ("il microfono del cantante", "monitor palco") +- Se non trovi un canale/mix per nome, cerca usando search_channels_by_name o search_mixes_by_name - "il mio mic/microfono" → cerca tra i canali chi è sul palco - "le chitarre/i vox/le tastiere" → cerca per strumento - "tutti i mic/tutte le chitarre" → cerca e gestisci multipli +- "il monitor" / "l'aux 2" → cerca tra i mix SCENE: - "carica/richiama/vai alla scena X" → recall_scene @@ -610,7 +116,7 @@ GRUPPI DI CANALI: CASI PARTICOLARI: - Se la richiesta è ambigua, chiedi chiarimenti in modo colloquiale -- Se serve cercare un canale, usa prima la cache (search_channels_by_name) +- Se serve cercare un canale/mix, usa prima la cache (search_channels_by_name / search_mixes_by_name) - Conferma sempre cosa hai fatto con un messaggio breve e chiaro - Usa emoji occasionalmente per rendere le risposte più amichevoli (✅ ❌ 🎤 🎸 🔊) - Se qualcosa non funziona, spiega il problema in modo semplice @@ -620,7 +126,7 @@ ESEMPI DI INTERPRETAZIONE: "abbassa un po' le chitarre" → cerca canali chitarra, riduci di 3-5 dB "muto tutto" → spegni tutti i 40 canali "solo voce" → cerca canali voce, accendi quelli e spegni gli altri -"mettimi più forte nel monitor" → NON puoi (sono gli aux), spiega che serve il tecnico +"alza il monitor 2" → cerca mix 2, aumenta volume "carica la scena del soundcheck" → cerca nel nome o chiedi numero scena "troppo forte, abbassa" → riduci di 5-8 dB "spegni questo canale" → se non specifica numero, chiedi quale @@ -646,7 +152,7 @@ Devi essere veloce, chiaro e capire anche richieste approssimative. def close(self): """Chiude le connessioni.""" self.controller.close() - + def chat(self, user_message: str) -> str: """Invia un messaggio all'agente e riceve la risposta. @@ -678,7 +184,10 @@ Devi essere veloce, chiaro e capire anche richieste approssimative. # Mostra stato cache cache_age = time.time() - self.controller._cache.get("timestamp", 0) if self.controller._is_cache_valid(): + channels_count = len(self.controller._cache.get("channels", {})) + mixes_count = len(self.controller._cache.get("mixes", {})) print(f"\n💾 Cache disponibile (età: {int(cache_age)}s)") + print(f" 📊 {channels_count} canali, {mixes_count} mix") else: print("\n💾 Cache non disponibile, verrà creata al primo utilizzo") @@ -688,8 +197,10 @@ Devi essere veloce, chiaro e capire anche richieste approssimative. print(" - 'Richiama la scena A10'") print(" - 'Imposta il pan del canale 3 a sinistra'") print(" - 'Muta i canali 2, 4, 6 e 8'") + print(" - 'Alza il mix 3 di 5 dB'") print(" - 'Quali canali sono associati ai vox?'") print(" - 'Mostrami lo stato del canale 12'") + print(" - 'Cerca i monitor'") print(" - 'Aggiorna i dati dal mixer'") print("\nDigita 'esci' o 'quit' per terminare\n") diff --git a/mixer_controller.py b/mixer_controller.py index b5a29a0..8c1b4b5 100644 --- a/mixer_controller.py +++ b/mixer_controller.py @@ -1,32 +1,785 @@ -# mixer_controller.py import socket import sys +import os +import json +import time +from pathlib import Path +from typing import List, Optional +from google import genai +from google.genai import types +from dotenv import load_dotenv -# Impostazioni di connessione predefinite -DEFAULT_HOST = "192.168.1.62" # Modifica con l'IP del tuo mixer -DEFAULT_PORT = 49280 +load_dotenv() + +from config import * + + +class TF5MixerController: + def __init__(self, host=DEFAULT_HOST, port=DEFAULT_PORT): + self.host = host + self.port = port + self.socket = None + self._ensure_cache_dir() + self._cache = self._load_cache() + + def _connect(self): + """Stabilisce la connessione se non già connesso.""" + if self.socket is None: + print('Inizializzazione socket...') + try: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(5) + self.socket.connect((self.host, self.port)) + except socket.error as e: + self.socket = None + raise ConnectionError(f"Impossibile connettersi al mixer: {e}") + + def _disconnect(self): + """Chiude la connessione.""" + if self.socket: + print('Chiusura socket...') + try: + self.socket.close() + print('Socket chiuso!') + except: + print('Errore durante chiusura socket!') + pass + finally: + self.socket = None + + def _send_command(self, command: str) -> str: + """Invia un comando al mixer e restituisce la risposta.""" + max_retries = 2 + + for attempt in range(max_retries): + try: + print(f'Tentativo di connessione {attempt} per {command}') + self._connect() + self.socket.sendall((command + '\n').encode('utf-8')) + response = self.socket.recv(4096) + decoded = response.decode('utf-8', errors='ignore').strip() + print(f'Risposta {decoded}') + return decoded + + except socket.error as e: + print(f'Errore di connessione dopo {max_retries} tentativi: {e}') + self._disconnect() # Forza riconnessione al prossimo tentativo + + if attempt < max_retries - 1: + time.sleep(0.1) + continue + else: + return f"Errore di connessione dopo {max_retries} tentativi: {e}" + + def close(self): + """Chiude la connessione (da chiamare alla fine).""" + self._disconnect() + + def __enter__(self): + """Context manager entry.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Context manager exit.""" + self.close() + + def _ensure_cache_dir(self): + """Crea la directory di cache se non esiste.""" + CACHE_DIR.mkdir(parents=True, exist_ok=True) + + def _load_cache(self) -> dict: + """Carica la cache dal file.""" + if CACHE_FILE.exists(): + try: + with open(CACHE_FILE, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + print(f"⚠️ Errore nel caricamento della cache: {e}") + return {"channels": {}, "mixes": {}, "timestamp": 0} + + def _save_cache(self): + """Salva la cache nel file.""" + try: + with open(CACHE_FILE, 'w', encoding='utf-8') as f: + json.dump(self._cache, f, indent=2, ensure_ascii=False) + except Exception as e: + print(f"⚠️ Errore nel salvataggio della cache: {e}") + + def _is_cache_valid(self) -> bool: + """Verifica se la cache è ancora valida.""" + if not self._cache.get("channels"): + return False + cache_age = time.time() - self._cache.get("timestamp", 0) + return cache_age < CACHE_DURATION + + def _update_channel_cache(self, channel: int): + """Aggiorna la cache per un singolo canale leggendo dal mixer. + + Args: + channel: Numero del canale (1-40) + """ + if not 1 <= channel <= TF5_INPUT_CHANNELS: + return + + ch_idx = channel - 1 + + # Leggi nome + resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0") + name = self._parse_name(resp_name) + + # Leggi stato ON/OFF + resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0") + is_on = self._parse_value(resp_on) == "1" + + # Leggi livello + resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0") + level_raw = self._parse_value(resp_level) + try: + level_int = int(level_raw) + level_db = level_int / 100.0 if level_int > -32768 else float('-inf') + except: + level_db = None + + # Leggi pan + resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0") + pan_raw = self._parse_value(resp_pan) + try: + pan_value = int(pan_raw) + except: + pan_value = None + + # Inizializza la struttura channels se non esiste + if "channels" not in self._cache: + self._cache["channels"] = {} + + # Aggiorna la cache + self._cache["channels"][str(channel)] = { + "channel": channel, + "name": name, + "on": is_on, + "level_db": level_db, + "pan": pan_value + } + + self._save_cache() + + def _update_mix_cache(self, mix_number: int): + """Aggiorna la cache per un singolo mix/aux leggendo dal mixer. + + Args: + mix_number: Numero del mix (1-20) + """ + if not 1 <= mix_number <= TF5_MIX_BUSSES: + return + + mix_idx = mix_number - 1 + + # Leggi nome + resp_name = self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0") + name = self._parse_name(resp_name) + + # Leggi stato ON/OFF + resp_on = self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0") + is_on = self._parse_value(resp_on) == "1" + + # Leggi livello + resp_level = self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0") + level_raw = self._parse_value(resp_level) + try: + level_int = int(level_raw) + level_db = level_int / 100.0 if level_int > -32768 else float('-inf') + except: + level_db = None + + # Inizializza la struttura mixes se non esiste + if "mixes" not in self._cache: + self._cache["mixes"] = {} + + # Aggiorna la cache + self._cache["mixes"][str(mix_number)] = { + "mix": mix_number, + "name": name, + "on": is_on, + "level_db": level_db + } + + self._save_cache() + + def _parse_name(self, response: str) -> str: + """Estrae il nome tra virgolette dalla risposta.""" + try: + start = response.find('"') + 1 + end = response.rfind('"') + if start > 0 and end > start: + return response[start:end] + return "Sconosciuto" + except: + return "Errore" + + def _parse_value(self, response: str) -> str: + """Estrae l'ultimo valore da una risposta OK.""" + parts = response.split() + if len(parts) > 0 and parts[0] == "OK": + return parts[-1] + return "N/A" + + def refresh_cache(self) -> dict: + """Aggiorna la cache leggendo tutti i canali e mix dal mixer. + + Returns: + Un dizionario con lo stato dell'operazione + """ + print("🔄 Aggiornamento cache completo in corso...") + channels_data = {} + mixes_data = {} + + # Aggiorna canali + for ch in range(1, TF5_INPUT_CHANNELS + 1): + ch_idx = ch - 1 + + # Leggi nome + resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0") + name = self._parse_name(resp_name) + + # Leggi stato ON/OFF + resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0") + is_on = self._parse_value(resp_on) == "1" + + # Leggi livello + resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0") + level_raw = self._parse_value(resp_level) + try: + level_int = int(level_raw) + level_db = level_int / 100.0 if level_int > -32768 else float('-inf') + except: + level_db = None + + # Leggi pan + resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0") + pan_raw = self._parse_value(resp_pan) + try: + pan_value = int(pan_raw) + except: + pan_value = None + + channels_data[str(ch)] = { + "channel": ch, + "name": name, + "on": is_on, + "level_db": level_db, + "pan": pan_value + } + + time.sleep(0.05) + + # Aggiorna mix + for mix in range(1, TF5_MIX_BUSSES + 1): + mix_idx = mix - 1 + + # Leggi nome + resp_name = self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0") + name = self._parse_name(resp_name) + + # Leggi stato ON/OFF + resp_on = self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0") + is_on = self._parse_value(resp_on) == "1" + + # Leggi livello + resp_level = self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0") + level_raw = self._parse_value(resp_level) + try: + level_int = int(level_raw) + level_db = level_int / 100.0 if level_int > -32768 else float('-inf') + except: + level_db = None + + mixes_data[str(mix)] = { + "mix": mix, + "name": name, + "on": is_on, + "level_db": level_db + } + + time.sleep(0.05) + + self._cache = { + "channels": channels_data, + "mixes": mixes_data, + "timestamp": time.time() + } + self._save_cache() + + print(f"✅ Cache aggiornata con {len(channels_data)} canali e {len(mixes_data)} mix") + return { + "status": "success", + "message": f"Cache aggiornata con {len(channels_data)} canali e {len(mixes_data)} mix", + "channels_count": len(channels_data), + "mixes_count": len(mixes_data) + } + + def recall_scene(self, bank: str, scene_number: int) -> dict: + """Richiama una scena dal banco A o B. + + Args: + bank: Il banco della scena ('a' o 'b') + scene_number: Il numero della scena (0-99) + + Returns: + Un dizionario con lo stato dell'operazione + """ + if bank.lower() not in ['a', 'b']: + return {"status": "error", "message": "Il banco deve essere 'a' o 'b'"} + if not 0 <= scene_number <= 99: + return {"status": "error", "message": "Il numero scena deve essere tra 0 e 99"} + + command = f"ssrecall_ex scene_{bank.lower()} {scene_number}" + response = self._send_command(command) + + # Invalida la cache dopo il cambio scena + self._cache["timestamp"] = 0 + + return { + "status": "success" if "OK" in response else "error", + "message": f"Scena {bank.upper()}{scene_number} richiamata. Cache invalidata.", + "response": response + } + + def set_channel_level(self, channel: int, level_db: float) -> dict: + """Imposta il livello del fader di un canale in dB. + + Args: + channel: Numero del canale (1-40) + level_db: Livello in dB (da -inf a +10.0) + + Returns: + Un dizionario con lo stato dell'operazione + """ + if not 1 <= channel <= TF5_INPUT_CHANNELS: + return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} + + # Converti dB in valore interno (moltiplicato per 100) + if level_db <= -138: + internal_value = -32768 # -inf + else: + internal_value = int(level_db * 100) + + command = f"set MIXER:Current/InCh/Fader/Level {channel-1} 0 {internal_value}" + response = self._send_command(command) + + # Aggiorna cache dal mixer per avere dati precisi + if "OK" in response: + self._update_channel_cache(channel) + + return { + "status": "success" if "OK" in response else "error", + "message": f"Canale {channel} impostato a {level_db:+.1f} dB", + "response": response + } + + def set_channel_on_off(self, channel: int, state: bool) -> dict: + """Accende o spegne un canale. + + Args: + channel: Numero del canale (1-40) + state: True per accendere, False per spegnere + + Returns: + Un dizionario con lo stato dell'operazione + """ + if not 1 <= channel <= TF5_INPUT_CHANNELS: + return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} + + value = 1 if state else 0 + command = f"set MIXER:Current/InCh/Fader/On {channel-1} 0 {value}" + response = self._send_command(command) + + # Aggiorna cache dal mixer per avere dati precisi + if "OK" in response: + self._update_channel_cache(channel) + + return { + "status": "success" if "OK" in response else "error", + "message": f"Canale {channel} {'acceso' if state else 'spento'}", + "response": response + } + + def set_channel_pan(self, channel: int, pan_value: int) -> dict: + """Imposta il pan di un canale. + + Args: + channel: Numero del canale (1-40) + pan_value: Valore pan da -63 (sinistra) a +63 (destra), 0 è centro + + Returns: + Un dizionario con lo stato dell'operazione + """ + if not 1 <= channel <= TF5_INPUT_CHANNELS: + return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} + if not -63 <= pan_value <= 63: + return {"status": "error", "message": "Il pan deve essere tra -63 e +63"} + + command = f"set MIXER:Current/InCh/ToSt/Pan {channel-1} 0 {pan_value}" + response = self._send_command(command) + + # Aggiorna cache dal mixer per avere dati precisi + if "OK" in response: + self._update_channel_cache(channel) + + pan_desc = "centro" + if pan_value < 0: + pan_desc = f"sinistra {abs(pan_value)}" + elif pan_value > 0: + pan_desc = f"destra {pan_value}" + + return { + "status": "success" if "OK" in response else "error", + "message": f"Canale {channel} pan impostato a {pan_desc}", + "response": response + } + + def set_mix_level(self, mix_number: int, level_db: float) -> dict: + """Imposta il livello di un mix/aux. + + Args: + mix_number: Numero del mix (1-20) + level_db: Livello in dB (da -inf a +10.0) + + Returns: + Un dizionario con lo stato dell'operazione + """ + if not 1 <= mix_number <= TF5_MIX_BUSSES: + return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"} + + if level_db <= -138: + internal_value = -32768 + else: + internal_value = int(level_db * 100) + + command = f"set MIXER:Current/Mix/Fader/Level {mix_number-1} 0 {internal_value}" + response = self._send_command(command) + + # Aggiorna cache dal mixer per avere dati precisi + if "OK" in response: + self._update_mix_cache(mix_number) + + return { + "status": "success" if "OK" in response else "error", + "message": f"Mix {mix_number} impostato a {level_db:+.1f} dB", + "response": response + } + + def set_mix_on_off(self, mix_number: int, state: bool) -> dict: + """Accende o spegne un mix/aux. + + Args: + mix_number: Numero del mix (1-20) + state: True per accendere, False per spegnere + + Returns: + Un dizionario con lo stato dell'operazione + """ + if not 1 <= mix_number <= TF5_MIX_BUSSES: + return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"} + + value = 1 if state else 0 + command = f"set MIXER:Current/Mix/Fader/On {mix_number-1} 0 {value}" + response = self._send_command(command) + + # Aggiorna cache dal mixer per avere dati precisi + if "OK" in response: + self._update_mix_cache(mix_number) + + return { + "status": "success" if "OK" in response else "error", + "message": f"Mix {mix_number} {'acceso' if state else 'spento'}", + "response": response + } + + def mute_multiple_channels(self, channels: List[int]) -> dict: + """Muta più canali contemporaneamente. + + Args: + channels: Lista di numeri di canale da mutare (es: [1, 2, 5, 8]) + + Returns: + Un dizionario con lo stato dell'operazione + """ + results = [] + for ch in channels: + result = self.set_channel_on_off(ch, False) + results.append(result) + + success_count = sum(1 for r in results if r["status"] == "success") + return { + "status": "success" if success_count == len(channels) else "partial", + "message": f"Mutati {success_count}/{len(channels)} canali: {channels}", + "details": results + } + + def unmute_multiple_channels(self, channels: List[int]) -> dict: + """Riattiva più canali contemporaneamente. + + Args: + channels: Lista di numeri di canale da riattivare (es: [1, 2, 5, 8]) + + Returns: + Un dizionario con lo stato dell'operazione + """ + results = [] + for ch in channels: + result = self.set_channel_on_off(ch, True) + results.append(result) + + success_count = sum(1 for r in results if r["status"] == "success") + return { + "status": "success" if success_count == len(channels) else "partial", + "message": f"Riattivati {success_count}/{len(channels)} canali: {channels}", + "details": results + } + + def get_channel_info(self, channel: int, force_refresh: bool = False) -> dict: + """Legge le informazioni di un canale (nome, livello, stato, pan). + Usa la cache se disponibile e valida. + + Args: + channel: Numero del canale (1-40) + force_refresh: Se True, ignora la cache e legge dal mixer + + Returns: + Un dizionario con tutte le informazioni del canale + """ + if not 1 <= channel <= TF5_INPUT_CHANNELS: + return {"status": "error", "message": f"Il canale deve essere tra 1 e {TF5_INPUT_CHANNELS}"} + + # Usa cache se valida e non forzato il refresh + if not force_refresh and self._is_cache_valid(): + cached_data = self._cache.get("channels", {}).get(str(channel)) + if cached_data: + return { + "status": "success", + "source": "cache", + **cached_data + } + + # Altrimenti leggi dal mixer + ch_idx = channel - 1 + + resp_name = self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0") + name = self._parse_name(resp_name) + + resp_on = self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0") + is_on = self._parse_value(resp_on) == "1" + + resp_level = self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0") + level_raw = self._parse_value(resp_level) + try: + level_int = int(level_raw) + level_db = level_int / 100.0 if level_int > -32768 else float('-inf') + except: + level_db = None + + resp_pan = self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0") + pan_raw = self._parse_value(resp_pan) + try: + pan_value = int(pan_raw) + except: + pan_value = None + + return { + "status": "success", + "source": "mixer", + "channel": channel, + "name": name, + "on": is_on, + "level_db": level_db, + "pan": pan_value + } + + def get_mix_info(self, mix_number: int, force_refresh: bool = False) -> dict: + """Legge le informazioni di un mix/aux (nome, livello, stato). + Usa la cache se disponibile e valida. + + Args: + mix_number: Numero del mix (1-20) + force_refresh: Se True, ignora la cache e legge dal mixer + + Returns: + Un dizionario con tutte le informazioni del mix + """ + if not 1 <= mix_number <= TF5_MIX_BUSSES: + return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"} + + # Usa cache se valida e non forzato il refresh + if not force_refresh and self._is_cache_valid(): + cached_data = self._cache.get("mixes", {}).get(str(mix_number)) + if cached_data: + return { + "status": "success", + "source": "cache", + **cached_data + } + + # Altrimenti leggi dal mixer + mix_idx = mix_number - 1 + + resp_name = self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0") + name = self._parse_name(resp_name) + + resp_on = self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0") + is_on = self._parse_value(resp_on) == "1" + + resp_level = self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0") + level_raw = self._parse_value(resp_level) + try: + level_int = int(level_raw) + level_db = level_int / 100.0 if level_int > -32768 else float('-inf') + except: + level_db = None + + return { + "status": "success", + "source": "mixer", + "mix": mix_number, + "name": name, + "on": is_on, + "level_db": level_db + } + + def search_channels_by_name(self, search_term: str) -> dict: + """Cerca canali il cui nome contiene un determinato termine. + Usa la cache per velocizzare la ricerca. + + Args: + search_term: Il termine da cercare nei nomi dei canali (case-insensitive) + + Returns: + Un dizionario con la lista dei canali trovati + """ + # Aggiorna cache se non valida + if not self._is_cache_valid(): + self.refresh_cache() + + search_lower = search_term.lower() + found_channels = [] + + for ch_str, info in self._cache.get("channels", {}).items(): + if search_lower in info.get("name", "").lower(): + found_channels.append({ + "channel": info["channel"], + "name": info["name"], + "on": info["on"], + "level_db": info["level_db"] + }) + + # Ordina per numero canale + found_channels.sort(key=lambda x: x["channel"]) + + return { + "status": "success", + "search_term": search_term, + "found_count": len(found_channels), + "channels": found_channels, + "message": f"Trovati {len(found_channels)} canali contenenti '{search_term}'" + } + + def search_mixes_by_name(self, search_term: str) -> dict: + """Cerca mix/aux il cui nome contiene un determinato termine. + Usa la cache per velocizzare la ricerca. + + Args: + search_term: Il termine da cercare nei nomi dei mix (case-insensitive) + + Returns: + Un dizionario con la lista dei mix trovati + """ + # Aggiorna cache se non valida + if not self._is_cache_valid(): + self.refresh_cache() + + search_lower = search_term.lower() + found_mixes = [] + + for mix_str, info in self._cache.get("mixes", {}).items(): + if search_lower in info.get("name", "").lower(): + found_mixes.append({ + "mix": info["mix"], + "name": info["name"], + "on": info["on"], + "level_db": info["level_db"] + }) + + # Ordina per numero mix + found_mixes.sort(key=lambda x: x["mix"]) + + return { + "status": "success", + "search_term": search_term, + "found_count": len(found_mixes), + "mixes": found_mixes, + "message": f"Trovati {len(found_mixes)} mix contenenti '{search_term}'" + } + + def get_all_channels_summary(self) -> dict: + """Ottiene un riepilogo di tutti i canali con nome e stato. + Usa la cache per velocizzare. + + Returns: + Un dizionario con il riepilogo di tutti i canali + """ + # Aggiorna cache se non valida + if not self._is_cache_valid(): + self.refresh_cache() + + channels = [] + for ch_str, info in self._cache.get("channels", {}).items(): + channels.append({ + "channel": info["channel"], + "name": info["name"], + "on": info["on"] + }) + + # Ordina per numero canale + channels.sort(key=lambda x: x["channel"]) + + cache_age = time.time() - self._cache.get("timestamp", 0) + + return { + "status": "success", + "total_channels": len(channels), + "channels": channels, + "cache_age_seconds": int(cache_age), + "message": f"Riepilogo di {len(channels)} canali (cache: {int(cache_age)}s fa)" + } + + def get_all_mixes_summary(self) -> dict: + """Ottiene un riepilogo di tutti i mix/aux con nome e stato. + Usa la cache per velocizzare. + + Returns: + Un dizionario con il riepilogo di tutti i mix + """ + # Aggiorna cache se non valida + if not self._is_cache_valid(): + self.refresh_cache() + + mixes = [] + for mix_str, info in self._cache.get("mixes", {}).items(): + mixes.append({ + "mix": info["mix"], + "name": info["name"], + "on": info["on"] + }) + + # Ordina per numero mix + mixes.sort(key=lambda x: x["mix"]) + + cache_age = time.time() - self._cache.get("timestamp", 0) + + return { + "status": "success", + "total_mixes": len(mixes), + "mixes": mixes, + "cache_age_seconds": int(cache_age), + "message": f"Riepilogo di {len(mixes)} mix (cache: {int(cache_age)}s fa)" + } -def send_command(command, host=DEFAULT_HOST, port=DEFAULT_PORT): - """ - Crea una connessione, invia un singolo comando, riceve la risposta e la chiude. - Restituisce la risposta del mixer. - """ - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.settimeout(5) - s.connect((host, port)) - s.sendall((command + '\n').encode('utf-8')) - print(f"-> Comando inviato al mixer: '{command}'") - response = s.recv(4096).decode('utf-8', errors='ignore').strip() - print(f"<- Risposta dal mixer: '{response}'") - if response.startswith("OK"): - return {"status": "success", "response": response} - else: - return {"status": "error", "response": response} - except socket.error as e: - print(f"Errore critico di connessione a {host}:{port} -> {e}") - error_msg = f"Impossibile connettersi al mixer: {e}" - return {"status": "error", "response": error_msg} - except Exception as e: - print(f"Errore imprevisto: {e}") - return {"status": "error", "response": str(e)} \ No newline at end of file