import socket import ipaddress from pathlib import Path from typing import Optional import json import threading from concurrent.futures import ThreadPoolExecutor, as_completed import time # ========================================== # Specifiche Yamaha TF5 (FONDAMENTALI PER LA CACHE E IL CONTROLLER) # ========================================== DEFAULT_PORT = 49280 TF5_INPUT_CHANNELS = 40 # CH 1-40 TF5_ST_INPUT_CHANNELS = 2 # ST IN 1-2 TF5_FX_RETURN_CHANNELS = 4 # FX RTN 1-4 TF5_DCA_GROUPS = 8 # DCA 1-8 TF5_MIX_BUSSES = 20 # MIX 1-20 TF5_MATRIX_BUSSES = 4 # MATRIX 1-4 TF5_STEREO_BUSSES = 1 # 1 Bus Stereo (L/R) TF5_MUTE_GROUPS = 8 # Mute Master 1-8 TF5_FX_SLOTS = 8 # Processori FX (8 slot interni) # ========================================== # Configurazione Cache (Mixer State & IP) # ========================================== CACHE_DIR = Path(__file__).parent / "cache" CACHE_FILE = CACHE_DIR / "tf5_cache.json" IP_CACHE_FILE = CACHE_DIR / "mixer_ip.json" CACHE_DURATION = 60.0 # Secondi per la validità dello stato del mixer IP_CACHE_DURATION = 300 # 5 minuti per la validità dell'IP del mixer # Assicurati che la cartella esista CACHE_DIR.mkdir(parents=True, exist_ok=True) # ========================================== # Funzioni di Utilità per Auto-Discovery # ========================================== def log(message: str, level: str = "INFO"): """Stampa log formattato con timestamp""" timestamp = time.strftime("%H:%M:%S") symbols = { "INFO": "ℹ️", "SUCCESS": "✅", "ERROR": "❌", "WARNING": "⚠️", "SEARCH": "🔍", "NETWORK": "🌐", "CACHE": "📍", "TIME": "⏱️" } symbol = symbols.get(level, "•") print(f"[{timestamp}] {symbol} {message}") def get_local_network() -> str: """Ottiene la rete locale del sistema""" log("Rilevamento rete locale in corso...", "NETWORK") try: s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) local_ip = s.getsockname()[0] s.close() ip_parts = local_ip.split('.') network = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.0/24" log(f"IP locale rilevato: {local_ip}", "INFO") log(f"Rete da scansionare: {network}", "NETWORK") return network except Exception as e: log(f"Errore nel rilevare la rete locale: {e}", "ERROR") network = "192.168.1.0/24" log(f"Uso rete di fallback: {network}", "WARNING") return network def check_port(ip: str, port: int, timeout: float = 0.5) -> bool: """Controlla se una porta è aperta su un IP""" try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) result = sock.connect_ex((ip, port)) sock.close() return result == 0 except Exception: return False def scan_network_for_mixer(port: int = DEFAULT_PORT, max_workers: int = 50) -> Optional[str]: """Scansiona la rete locale per trovare il mixer""" log(f"Ricerca mixer sulla porta {port}...", "SEARCH") start_time = time.time() network = get_local_network() ip_network = ipaddress.ip_network(network, strict=False) total_hosts = sum(1 for _ in ip_network.hosts()) log(f"Indirizzi da scansionare: {total_hosts} (Thread: {max_workers})", "INFO") found_ip = None scanned = 0 with ThreadPoolExecutor(max_workers=max_workers) as executor: future_to_ip = {executor.submit(check_port, str(ip), port): str(ip) for ip in ip_network.hosts()} for future in as_completed(future_to_ip): ip = future_to_ip[future] scanned += 1 if scanned % 50 == 0: log(f"Progresso: {scanned}/{total_hosts} IP", "INFO") try: if future.result(): found_ip = ip elapsed = time.time() - start_time log(f"MIXER TROVATO su {ip}:{port}", "SUCCESS") log(f"Tempo di scansione: {elapsed:.2f}s", "TIME") executor.shutdown(wait=False, cancel_futures=True) break except Exception: pass if not found_ip: log(f"Nessun mixer trovato. Tempo: {time.time() - start_time:.2f}s", "ERROR") return found_ip def load_cached_ip() -> Optional[str]: """Carica l'IP dalla cache se valido""" if not IP_CACHE_FILE.exists(): return None try: with open(IP_CACHE_FILE, 'r') as f: data = json.load(f) cached_ip = data.get('ip') age = time.time() - data.get('timestamp', 0) if age < IP_CACHE_DURATION: log(f"Verifica raggiungibilità IP in cache: {cached_ip}...", "CACHE") if cached_ip and check_port(cached_ip, DEFAULT_PORT, timeout=1.0): log(f"IP dalla cache valido: {cached_ip}", "SUCCESS") return cached_ip else: log(f"IP dalla cache non raggiungibile.", "WARNING") else: log(f"Cache IP scaduta.", "WARNING") except Exception as e: log(f"Errore lettura cache IP: {e}", "ERROR") return None def save_ip_to_cache(ip: str): """Salva l'IP nella cache""" try: with open(IP_CACHE_FILE, 'w') as f: json.dump({'ip': ip, 'timestamp': time.time()}, f, indent=2) except Exception as e: log(f"Errore nel salvare cache IP: {e}", "ERROR") def get_mixer_ip() -> str: """Ottiene l'IP del mixer (Cache -> Discovery -> Fallback)""" log("=== AVVIO RICERCA YAMAHA TF5 ===", "INFO") cached_ip = load_cached_ip() if cached_ip: return cached_ip discovered_ip = scan_network_for_mixer() if discovered_ip: save_ip_to_cache(discovered_ip) return discovered_ip fallback_ip = "192.168.1.57" log(f"Uso IP predefinito di fallback: {fallback_ip}", "WARNING") return fallback_ip # ========================================== # ESECUZIONE AL CARICAMENTO DEL MODULO # ========================================== # Viene eseguito nel momento in cui mixer_controller.py fa "from config import *" DEFAULT_HOST = get_mixer_ip()