177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
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() |