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 # Configurazione mixer DEFAULT_PORT = 49280 TF5_INPUT_CHANNELS = 40 TF5_MIX_BUSSES = 20 # Configurazione cache CACHE_DIR = Path(".tf5_mixer_cache") CACHE_FILE = CACHE_DIR / "channels_cache.json" IP_CACHE_FILE = CACHE_DIR / "mixer_ip.json" CACHE_DURATION = 3600 # 60 minuti in secondi IP_CACHE_DURATION = 300 # 5 minuti per l'IP 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: # Crea un socket per ottenere l'IP locale s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) local_ip = s.getsockname()[0] s.close() # Converte in network (es: 192.168.1.0/24) 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 as e: # Log solo per debug, commentabile se troppo verbose # log(f"Errore su {ip}: {e}", "ERROR") 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() # Genera lista di IP da scansionare ip_network = ipaddress.ip_network(network, strict=False) total_hosts = sum(1 for _ in ip_network.hosts()) log(f"Indirizzi da scansionare: {total_hosts}", "INFO") log(f"Thread paralleli: {max_workers}", "INFO") found_ip = None scanned = 0 # Scansione parallela per velocità 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() } log("Scansione in corso...", "SEARCH") for future in as_completed(future_to_ip): ip = future_to_ip[future] scanned += 1 # Log di progresso ogni 50 IP if scanned % 50 == 0: log(f"Progresso: {scanned}/{total_hosts} IP scansionati", "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") log(f"IP scansionati: {scanned}/{total_hosts}", "INFO") # Cancella i task rimanenti executor.shutdown(wait=False, cancel_futures=True) break except Exception as e: log(f"Errore durante scansione di {ip}: {e}", "ERROR") if not found_ip: elapsed = time.time() - start_time log(f"Nessun mixer trovato sulla porta {port}", "ERROR") log(f"Tempo totale di scansione: {elapsed:.2f}s", "TIME") log(f"IP scansionati: {scanned}/{total_hosts}", "INFO") return found_ip def load_cached_ip() -> Optional[str]: """Carica l'IP dalla cache se valido""" log("Controllo cache IP...", "CACHE") if not IP_CACHE_FILE.exists(): log("Nessuna cache trovata", "INFO") return None try: with open(IP_CACHE_FILE, 'r') as f: data = json.load(f) cached_ip = data.get('ip') cache_time = data.get('timestamp', 0) age = time.time() - cache_time log(f"Cache trovata: {cached_ip} (età: {int(age)}s)", "CACHE") # Controlla se la cache è ancora valida if age < IP_CACHE_DURATION: log(f"Verifica raggiungibilità di {cached_ip}...", "CACHE") # Verifica che l'IP sia ancora valido 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 scaduta (max {IP_CACHE_DURATION}s)", "WARNING") except Exception as e: log(f"Errore nel leggere cache IP: {e}", "ERROR") return None def save_ip_to_cache(ip: str): """Salva l'IP nella cache""" log(f"Salvataggio IP in cache: {ip}", "CACHE") CACHE_DIR.mkdir(exist_ok=True) try: with open(IP_CACHE_FILE, 'w') as f: json.dump({ 'ip': ip, 'timestamp': time.time() }, f, indent=2) log(f"Cache salvata: {IP_CACHE_FILE}", "SUCCESS") except Exception as e: log(f"Errore nel salvare cache IP: {e}", "ERROR") def get_mixer_ip() -> str: """ Ottiene l'IP del mixer con auto-discovery: 1. Controlla cache 2. Scansiona la rete 3. Fallback su IP predefinito """ log("=== INIZIO RICERCA MIXER ===", "INFO") # Prova con la cache cached_ip = load_cached_ip() if cached_ip: log("=== RICERCA COMPLETATA (CACHE) ===", "SUCCESS") return cached_ip # Scansiona la rete log("Avvio scansione di rete...", "SEARCH") discovered_ip = scan_network_for_mixer() if discovered_ip: save_ip_to_cache(discovered_ip) log("=== RICERCA COMPLETATA (DISCOVERY) ===", "SUCCESS") return discovered_ip # Fallback fallback_ip = "192.168.1.57" log(f"Uso IP predefinito di fallback: {fallback_ip}", "WARNING") log("=== RICERCA COMPLETATA (FALLBACK) ===", "WARNING") return fallback_ip # Variabile globale per l'host (si aggiorna automaticamente) DEFAULT_HOST = None def get_host() -> str: """Ottiene l'host del mixer (con lazy loading)""" global DEFAULT_HOST if DEFAULT_HOST is None: log("Host mixer non inizializzato, avvio discovery...", "INFO") DEFAULT_HOST = get_mixer_ip() else: log(f"Host mixer già inizializzato: {DEFAULT_HOST}", "INFO") return DEFAULT_HOST def refresh_mixer_ip(): """Forza un refresh dell'IP del mixer""" global DEFAULT_HOST log("=== REFRESH FORZATO IP MIXER ===", "INFO") log("Cancellazione cache esistente...", "CACHE") # Rimuovi cache if IP_CACHE_FILE.exists(): try: IP_CACHE_FILE.unlink() log("Cache rimossa", "SUCCESS") except Exception as e: log(f"Errore rimozione cache: {e}", "ERROR") # Nuova scansione DEFAULT_HOST = scan_network_for_mixer() if DEFAULT_HOST: save_ip_to_cache(DEFAULT_HOST) log(f"Nuovo IP impostato: {DEFAULT_HOST}", "SUCCESS") else: log("Refresh fallito, mantengo IP precedente", "ERROR") log("=== REFRESH COMPLETATO ===", "INFO") return DEFAULT_HOST # Test rapido if __name__ == "__main__": log("=== TEST AUTO-DISCOVERY MIXER ===", "INFO") host = get_host() log(f"Host finale: {host}", "SUCCESS") # Test refresh # log("\n=== TEST REFRESH ===", "INFO") # refresh_mixer_ip()