Compare commits

...

11 Commits

Author SHA1 Message Date
Nick
deee299af5 meter 2026-03-02 20:14:16 +01:00
Nick
1975a6f1d0 fix requirements.txtx 2026-02-16 21:02:25 +01:00
Nick
729cb649fc add extra tools 2026-02-16 21:02:05 +01:00
Nick
14512aa3f4 tasks yaml 2026-02-16 20:50:34 +01:00
Nick
4d5119ab5a add service auto discovery 2026-02-08 20:22:50 +01:00
Nick
9173aed76f fix telegram ip 2026-02-08 20:14:29 +01:00
Nick
3f09ff2bd6 fix sync cache between bot and mixer 2025-11-10 19:56:47 +01:00
Nick
221e1ad318 fix setup process 2025-11-03 20:31:52 +01:00
Nick
4246d13339 dockerizzato 2025-11-03 20:15:23 +01:00
Nick
86fa13a1a0 analizza mix 2025-11-03 20:10:10 +01:00
Nick
77bd4fabb6 fixed -inf 2025-11-03 19:35:48 +01:00
28 changed files with 4501 additions and 1214 deletions

View File

@@ -18,7 +18,7 @@
"channel": 3, "channel": 3,
"name": "Talkback", "name": "Talkback",
"on": false, "on": false,
"level_db": null, "level_db": -60.0,
"pan": 0 "pan": 0
}, },
"4": { "4": {
@@ -31,15 +31,15 @@
"5": { "5": {
"channel": 5, "channel": 5,
"name": "Vox1", "name": "Vox1",
"on": true, "on": false,
"level_db": -1.1, "level_db": null,
"pan": 0 "pan": 0
}, },
"6": { "6": {
"channel": 6, "channel": 6,
"name": "Vox2", "name": "Vox2",
"on": true, "on": false,
"level_db": -0.15, "level_db": null,
"pan": 0 "pan": 0
}, },
"7": { "7": {
@@ -59,50 +59,50 @@
"9": { "9": {
"channel": 9, "channel": 9,
"name": "Kick", "name": "Kick",
"on": true, "on": false,
"level_db": -5.2, "level_db": null,
"pan": 0 "pan": 0
}, },
"10": { "10": {
"channel": 10, "channel": 10,
"name": "Snare", "name": "Snare",
"on": true, "on": false,
"level_db": -9.95, "level_db": null,
"pan": 0 "pan": 0
}, },
"11": { "11": {
"channel": 11, "channel": 11,
"name": "Tom 1", "name": "Tom 1",
"on": true, "on": false,
"level_db": -16.6, "level_db": null,
"pan": 0 "pan": 0
}, },
"12": { "12": {
"channel": 12, "channel": 12,
"name": "Tom 2", "name": "Tom 2",
"on": true, "on": false,
"level_db": -18.0, "level_db": null,
"pan": 0 "pan": 0
}, },
"13": { "13": {
"channel": 13, "channel": 13,
"name": "Tom3", "name": "Tom3",
"on": true, "on": false,
"level_db": -24.0, "level_db": null,
"pan": 0 "pan": 0
}, },
"14": { "14": {
"channel": 14, "channel": 14,
"name": "Pan SX", "name": "Pan SX",
"on": true, "on": false,
"level_db": -15.08, "level_db": null,
"pan": 0 "pan": 0
}, },
"15": { "15": {
"channel": 15, "channel": 15,
"name": "Pan dx", "name": "Pan dx",
"on": true, "on": false,
"level_db": -14.68, "level_db": null,
"pan": 0 "pan": 0
}, },
"16": { "16": {
@@ -123,21 +123,21 @@
"channel": 18, "channel": 18,
"name": "Archetto", "name": "Archetto",
"on": false, "on": false,
"level_db": null, "level_db": -76.0,
"pan": 0 "pan": 0
}, },
"19": { "19": {
"channel": 19, "channel": 19,
"name": "Vox 5", "name": "Vox 5",
"on": true, "on": false,
"level_db": -0.1, "level_db": null,
"pan": 0 "pan": 0
}, },
"20": { "20": {
"channel": 20, "channel": 20,
"name": "Tast", "name": "Tast",
"on": true, "on": false,
"level_db": 0.0, "level_db": null,
"pan": 0 "pan": 0
}, },
"21": { "21": {
@@ -157,15 +157,15 @@
"23": { "23": {
"channel": 23, "channel": 23,
"name": " Vox3", "name": " Vox3",
"on": true, "on": false,
"level_db": -0.2, "level_db": null,
"pan": 0 "pan": 0
}, },
"24": { "24": {
"channel": 24, "channel": 24,
"name": "Chit cnt", "name": "Chit cnt",
"on": true, "on": false,
"level_db": -1.75, "level_db": null,
"pan": 0 "pan": 0
}, },
"25": { "25": {
@@ -185,7 +185,7 @@
"27": { "27": {
"channel": 27, "channel": 27,
"name": "Vox 4", "name": "Vox 4",
"on": true, "on": false,
"level_db": null, "level_db": null,
"pan": 0 "pan": 0
}, },
@@ -228,14 +228,14 @@
"channel": 33, "channel": 33,
"name": "PC", "name": "PC",
"on": true, "on": true,
"level_db": -20.4, "level_db": -17.5,
"pan": -63 "pan": 0
}, },
"34": { "34": {
"channel": 34, "channel": 34,
"name": "PC", "name": "PC",
"on": true, "on": true,
"level_db": -20.4, "level_db": -17.5,
"pan": 63 "pan": 63
}, },
"35": { "35": {
@@ -286,7 +286,7 @@
"mix": 1, "mix": 1,
"name": "Aux 1", "name": "Aux 1",
"on": true, "on": true,
"level_db": -1.55 "level_db": -11.1
}, },
"2": { "2": {
"mix": 2, "mix": 2,
@@ -298,37 +298,37 @@
"mix": 3, "mix": 3,
"name": "Aux 3", "name": "Aux 3",
"on": true, "on": true,
"level_db": -0.5 "level_db": 0.35
}, },
"4": { "4": {
"mix": 4, "mix": 4,
"name": "Aux 4", "name": "Aux 4",
"on": true, "on": true,
"level_db": -0.6 "level_db": 0.0
}, },
"5": { "5": {
"mix": 5, "mix": 5,
"name": "batteria", "name": "batteria",
"on": true, "on": true,
"level_db": 1.35 "level_db": -4.9
}, },
"6": { "6": {
"mix": 6, "mix": 6,
"name": "Aux 6", "name": "Aux 6",
"on": true, "on": true,
"level_db": -6.65 "level_db": -9.3
}, },
"7": { "7": {
"mix": 7, "mix": 7,
"name": "Aux 7", "name": "Aux 7",
"on": true, "on": true,
"level_db": -2.7 "level_db": -6.35
}, },
"8": { "8": {
"mix": 8, "mix": 8,
"name": "Aux 8", "name": "Aux 8",
"on": true, "on": true,
"level_db": -2.65 "level_db": -9.0
}, },
"9": { "9": {
"mix": 9, "mix": 9,
@@ -382,13 +382,13 @@
"mix": 17, "mix": 17,
"name": "Diretta", "name": "Diretta",
"on": true, "on": true,
"level_db": 0.0 "level_db": -1.0
}, },
"18": { "18": {
"mix": 18, "mix": 18,
"name": "Diretta", "name": "Diretta",
"on": true, "on": true,
"level_db": 0.0 "level_db": -1.0
}, },
"19": { "19": {
"mix": 19, "mix": 19,
@@ -403,5 +403,12 @@
"level_db": 6.0 "level_db": 6.0
} }
}, },
"timestamp": 1762193941.3288717 "timestamp": 1770577942.7806144,
"steinch": {},
"fxrtn": {},
"dcas": {},
"matrices": {},
"stereo": {},
"mono": {},
"mute_masters": {}
} }

View File

@@ -0,0 +1,4 @@
{
"ip": "192.168.1.61",
"timestamp": 1772477899.4678419
}

19
Dockerfile Normal file
View File

@@ -0,0 +1,19 @@
# Dockerfile
# 1. Usa un'immagine Python ufficiale e leggera come base
FROM python:3.11-slim
# 2. Imposta una directory di lavoro all'interno del container
WORKDIR /app
# 3. Copia il file delle dipendenze e installale
# Questo passaggio viene fatto prima per sfruttare la cache di Docker.
# Se requirements.txt non cambia, questo strato non verrà ricostruito.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 4. Copia tutti i file del progetto nella directory di lavoro del container
COPY . .
# 5. Comando da eseguire quando il container viene avviato
CMD ["python", "telegram_bot.py"]

Binary file not shown.

407
cache/mixer_state.json vendored Normal file
View File

@@ -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": null,
"pan": 0
},
"3": {
"channel": 3,
"name": "Talkback",
"on": false,
"level_db": null,
"pan": 0
},
"4": {
"channel": 4,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"5": {
"channel": 5,
"name": "Vox1",
"on": true,
"level_db": -13.15,
"pan": 0
},
"6": {
"channel": 6,
"name": "Vox2",
"on": true,
"level_db": null,
"pan": 0
},
"7": {
"channel": 7,
"name": "Basso",
"on": false,
"level_db": null,
"pan": 0
},
"8": {
"channel": 8,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"9": {
"channel": 9,
"name": "Kick",
"on": true,
"level_db": -5.2,
"pan": 0
},
"10": {
"channel": 10,
"name": "Snare",
"on": true,
"level_db": -9.95,
"pan": 0
},
"11": {
"channel": 11,
"name": "Tom 1",
"on": true,
"level_db": -16.6,
"pan": 0
},
"12": {
"channel": 12,
"name": "Tom 2",
"on": true,
"level_db": -3.45,
"pan": 0
},
"13": {
"channel": 13,
"name": "Tom3",
"on": true,
"level_db": -24.0,
"pan": 0
},
"14": {
"channel": 14,
"name": "Pan SX",
"on": true,
"level_db": -15.08,
"pan": 0
},
"15": {
"channel": 15,
"name": "Pan dx",
"on": true,
"level_db": -14.68,
"pan": 0
},
"16": {
"channel": 16,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"17": {
"channel": 17,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"18": {
"channel": 18,
"name": "Archetto",
"on": false,
"level_db": null,
"pan": 0
},
"19": {
"channel": 19,
"name": "Vox 5",
"on": false,
"level_db": null,
"pan": 0
},
"20": {
"channel": 20,
"name": "Tast",
"on": true,
"level_db": -10.8,
"pan": 0
},
"21": {
"channel": 21,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"22": {
"channel": 22,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"23": {
"channel": 23,
"name": " Vox3",
"on": true,
"level_db": null,
"pan": 0
},
"24": {
"channel": 24,
"name": "Chit cnt",
"on": true,
"level_db": -0.5,
"pan": 0
},
"25": {
"channel": 25,
"name": "Chit dx",
"on": true,
"level_db": -22.7,
"pan": 0
},
"26": {
"channel": 26,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"27": {
"channel": 27,
"name": "Vox 4",
"on": false,
"level_db": null,
"pan": 0
},
"28": {
"channel": 28,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"29": {
"channel": 29,
"name": "Pad",
"on": false,
"level_db": null,
"pan": 0
},
"30": {
"channel": 30,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"31": {
"channel": 31,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"32": {
"channel": 32,
"name": "Sconosciuto",
"on": false,
"level_db": null,
"pan": 0
},
"33": {
"channel": 33,
"name": "PC",
"on": true,
"level_db": -17.5,
"pan": -63
},
"34": {
"channel": 34,
"name": "PC",
"on": true,
"level_db": -17.5,
"pan": 63
},
"35": {
"channel": 35,
"name": "ch35",
"on": true,
"level_db": null,
"pan": 0
},
"36": {
"channel": 36,
"name": "ch36",
"on": true,
"level_db": null,
"pan": 0
},
"37": {
"channel": 37,
"name": "ch37",
"on": true,
"level_db": null,
"pan": 0
},
"38": {
"channel": 38,
"name": "ch38",
"on": true,
"level_db": null,
"pan": 0
},
"39": {
"channel": 39,
"name": "ch39",
"on": true,
"level_db": null,
"pan": 0
},
"40": {
"channel": 40,
"name": "ch40",
"on": true,
"level_db": null,
"pan": 0
}
},
"mixes": {
"1": {
"mix": 1,
"name": "Aux 1",
"on": true,
"level_db": -1.55
},
"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": -6.65
},
"7": {
"mix": 7,
"name": "Aux 7",
"on": true,
"level_db": -2.7
},
"8": {
"mix": 8,
"name": "Aux 8",
"on": true,
"level_db": -2.65
},
"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": 1762800269.2657
}

249
config.py
View File

@@ -1,13 +1,252 @@
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 # Configurazione mixer
DEFAULT_HOST = "192.168.1.62"
DEFAULT_PORT = 49280 DEFAULT_PORT = 49280
TF5_INPUT_CHANNELS = 40 TF5_INPUT_CHANNELS = 40
TF5_MIX_BUSSES = 20 TF5_MIX_BUSSES = 20
from pathlib import Path
# Configurazione cache # Configurazione cache
CACHE_DIR = Path(".tf5_mixer_cache") CACHE_DIR = Path(".tf5_mixer_cache")
CACHE_FILE = CACHE_DIR / "channels_cache.json" CACHE_FILE = CACHE_DIR / "channels_cache.json"
CACHE_DURATION = 3600 # 60 minuti in secondi 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()

View File

@@ -0,0 +1,21 @@
{
"user_id": 1073121360,
"username": "Ingdavidfeliz",
"last_updated": 1762197549.52935,
"last_updated_str": "2025-11-03 19:19:09",
"message_count": 2,
"history": [
{
"role": "user",
"content": "Alzami il basso",
"timestamp": 1762197547.3495104,
"timestamp_str": "2025-11-03 19:19:07"
},
{
"role": "assistant",
"content": "Ciao David! Per \"alzami il basso\", intendi nel tuo monitor personale o in un altro mix? Ho bisogno di sapere in quale mix vuoi che alzi il basso.",
"timestamp": 1762197549.5293229,
"timestamp_str": "2025-11-03 19:19:09"
}
]
}

View File

@@ -0,0 +1,93 @@
{
"user_id": 309700430,
"username": "Cicciomt7",
"last_updated": 1762198991.2953947,
"last_updated_str": "2025-11-03 19:43:11",
"message_count": 14,
"history": [
{
"role": "user",
"content": "Chiudi vox 5",
"timestamp": 1762198915.897361,
"timestamp_str": "2025-11-03 19:41:55"
},
{
"role": "assistant",
"content": "Ok, Vox 5 (canale 19) è spento. 🎤❌",
"timestamp": 1762198920.8823922,
"timestamp_str": "2025-11-03 19:42:00"
},
{
"role": "user",
"content": "Apri vox 5",
"timestamp": 1762198929.1841967,
"timestamp_str": "2025-11-03 19:42:09"
},
{
"role": "assistant",
"content": "Ok, Vox 5 (canale 19) è acceso. 🎤✅",
"timestamp": 1762198931.5372686,
"timestamp_str": "2025-11-03 19:42:11"
},
{
"role": "user",
"content": "Chiusi vox 3",
"timestamp": 1762198935.3993735,
"timestamp_str": "2025-11-03 19:42:15"
},
{
"role": "assistant",
"content": "Non ho trovato nessun canale chiamato \"vox 3\". Sei sicuro del nome? Forse intendevi un altro canale o un numero?",
"timestamp": 1762198938.0440562,
"timestamp_str": "2025-11-03 19:42:18"
},
{
"role": "user",
"content": "Vox3",
"timestamp": 1762198948.152694,
"timestamp_str": "2025-11-03 19:42:28"
},
{
"role": "assistant",
"content": "Ok, \" Vox3\" (canale 23) è spento. 🎤❌",
"timestamp": 1762198953.3262546,
"timestamp_str": "2025-11-03 19:42:33"
},
{
"role": "user",
"content": "Apri vox3",
"timestamp": 1762198964.16481,
"timestamp_str": "2025-11-03 19:42:44"
},
{
"role": "assistant",
"content": "Ok, Vox3 (canale 23) è acceso. 🎤✅",
"timestamp": 1762198966.712192,
"timestamp_str": "2025-11-03 19:42:46"
},
{
"role": "user",
"content": "Chiudi il gelato",
"timestamp": 1762198972.835868,
"timestamp_str": "2025-11-03 19:42:52"
},
{
"role": "assistant",
"content": "Ok, \"Gelato\" (canale 2) è spento. 🎤❌",
"timestamp": 1762198975.563454,
"timestamp_str": "2025-11-03 19:42:55"
},
{
"role": "user",
"content": "Riaprilo",
"timestamp": 1762198989.0117595,
"timestamp_str": "2025-11-03 19:43:09"
},
{
"role": "assistant",
"content": "Ok, \"Gelato\" (canale 2) è acceso. 🎤✅",
"timestamp": 1762198991.295375,
"timestamp_str": "2025-11-03 19:43:11"
}
]
}

View File

@@ -0,0 +1,21 @@
{
"user_id": 436176546,
"username": "Gloriushhh",
"last_updated": 1762199858.7119617,
"last_updated_str": "2025-11-03 19:57:38",
"message_count": 2,
"history": [
{
"role": "user",
"content": "Alza il volume del mio canale",
"timestamp": 1762199853.7008705,
"timestamp_str": "2025-11-03 19:57:33"
},
{
"role": "assistant",
"content": "Non riesco a trovare il tuo canale 'VOX 2'. Sei sicuro che si chiami così? Potrebbe esserci un nome leggermente diverso o il canale non è presente.",
"timestamp": 1762199858.7119193,
"timestamp_str": "2025-11-03 19:57:38"
}
]
}

View File

@@ -1,87 +1,135 @@
{ {
"user_id": 4762864, "user_id": 4762864,
"username": "unnikked", "username": "unnikked",
"last_updated": 1762194065.7709692, "last_updated": 1770578542.7348943,
"last_updated_str": "2025-11-03 19:21:05", "last_updated_str": "2026-02-08 19:22:22",
"message_count": 13, "message_count": 21,
"history": [ "history": [
{ {
"role": "user", "role": "user",
"content": "Me lo spegni il gelato", "content": "Controlla ora",
"timestamp": 1761595234.0849943, "timestamp": 1769455908.2156205,
"timestamp_str": "2025-10-27 21:00:34" "timestamp_str": "2026-01-26 19:31:48"
}, },
{ {
"role": "assistant", "role": "assistant",
"content": "Fatto! Canale \"Gelato\" spento. ✅", "content": "Allora, Nicola, ho controllato di nuovo il mixer dopo il refresh! Ecco cosa vedo:\n\n**Canali:**\nCi sono un bel po' di canali attivi e nominati:\n* **Gelato (Canale 2):** Acceso, volume a -11.7 dB\n* **Vox1 (Canale 5):** Acceso, volume a 0 dB\n* **Vox2 (Canale 6):** Acceso, volume a 6.3 dB\n* **Basso (Canale 7):** Acceso, volume a -2.15 dB\n* **Kick (Canale 9):** Acceso, volume a -0.35 dB\n* **Snare (Canale 10):** Acceso, volume a -1.6 dB\n* **Tom 1 (Canale 11):** Acceso, volume a -2.8 dB\n* **Tom 2 (Canale 12):** Acceso, volume a -0.8 dB\n* **Tom3 (Canale 13):** Acceso, volume a -3.05 dB\n* **Pan SX (Canale 14):** Acceso, volume a -3.85 dB\n* **Pan dx (Canale 15):** Acceso, volume a -4.3 dB\n* **Archetto (Canale 18):** Acceso, volume a 4.6 dB\n* **Vox 5 (Canale 19):** Acceso, volume a -4.65 dB\n* **Tast (Canale 20):** Acceso, volume a -1.6 dB\n* **Vox3 (Canale 23):** Acceso, volume a 3.05 dB\n* **Chit cnt (Canale 24):** Acceso, volume a -2.35 dB\n* **Chit dx (Canale 25):** Acceso, volume a -0.85 dB\n* **PC (Canale 33 e 34):** Accesi, volume a -43.2 dB\n\n**Canali che sono accesi ma con volume bassissimo (-120 dB), quindi praticamente muti:**\n* **Vox 4 (Canale 27)**\n* **ch35 (Canale 35)**\n* **ch36 (Canale 36)**\n* **ch37 (Canale 37)**\n* **ch38 (Canale 38)**\n* **ch39 (Canale 39)**\n* **ch40 (Canale 40)**\n\n**Canali spenti o sconosciuti:**\n* **Canale 1:** Sconosciuto, spento, volume a 0.65 dB\n* **Talkback (Canale 3):** Spento, volume a -120 dB\n* **Canale 4:** Sconosciuto, spento, volume a -120 dB\n* **Canale 8:** Sconosciuto, spento, volume a -120 dB\n* **Canale 16:** Sconosciuto, spento, volume a -120 dB\n* **Canale 17:** Sconosciuto, spento, volume a -120 dB\n* **Canale 21:** Sconosciuto, spento, volume a -120 dB\n* **Canale 22:** Sconosciuto, spento, volume a -120 dB\n* **Canale 26:** Sconosciuto, spento, volume a -120 dB\n* **Canale 28:** Sconosciuto, spento, volume a -120 dB\n* **Pad (Canale 29):** Spento, volume a -120 dB\n* **Canali dal 30 al 32:** Sconosciuti, spenti, volume a -120 dB\n\n**Mix:**\nAnche i mix sono belli attivi:\n* **Aux 1 (Mix 1):** Acceso, volume a -11.1 dB\n* **Aux 2 (Mix 2):** Acceso, volume a -0.05 dB\n* **Aux 3 (Mix 3):** Acceso, volume a 0.35 dB\n* **Aux 4 (Mix 4):** Acceso, volume a 0 dB\n* **batteria (Mix 5):** Acceso, volume a 1.35 dB\n* **Aux 6 (Mix 6):** Acceso, volume a 10 dB\n* **Aux 7 (Mix 7):** Acceso, volume a -9.75 dB\n* **Aux 8 (Mix 8):** Acceso, volume a -4.4 dB\n* **Aux 9/10 (Mix 9 e 10):** Accesi, volume a -8.1 dB\n* **Aux Pulp (Mix 11 e 12):** Accesi, volume a -10.3 dB\n* **Aux15/16 (Mix 15 e 16):** Accesi, volume a 0.1 dB\n* **Diretta (Mix 17 e 18):** Accesi, volume a -2 dB\n* **Traduzio (Mix 19 e 20):** Accesi, volume a 6 dB\n\n**Mix spenti:**\n* **Aux13/14 (Mix 13 e 14):** Spenti, volume a -1.6 dB\n\nLa cache è stata aggiornata 5 secondi fa. Sembra che ci sia un buon movimento sul mixer! Fammi sapere se vuoi che faccia qualche modifica. 🎛️🎤",
"timestamp": 1761595239.1608367, "timestamp": 1769455917.246521,
"timestamp_str": "2025-10-27 21:00:39" "timestamp_str": "2026-01-26 19:31:57"
}, },
{ {
"role": "user", "role": "user",
"content": "Mi accendi il gelato.", "content": "ciao",
"timestamp": 1761595383.8880353, "timestamp": 1769766889.7665708,
"timestamp_str": "2025-10-27 21:03:03" "timestamp_str": "2026-01-30 09:54:49"
}, },
{ {
"role": "assistant", "role": "assistant",
"content": "Certo! Gelato acceso! 🍦✅", "content": "Ciao Nicola! Tutto bene? Dimmi pure se hai bisogno di qualcosa con il mixer! 🎛️🎤",
"timestamp": 1761595387.27931, "timestamp": 1769766891.7504349,
"timestamp_str": "2025-10-27 21:03:07" "timestamp_str": "2026-01-30 09:54:51"
}, },
{ {
"role": "user", "role": "user",
"content": "Apaga el helado. Cappuccino.", "content": "è acceso?",
"timestamp": 1761595426.0112157, "timestamp": 1769766900.066535,
"timestamp_str": "2025-10-27 21:03:46" "timestamp_str": "2026-01-30 09:55:00"
}, },
{ {
"role": "assistant", "role": "assistant",
"content": "Gelato spento! ✅\nNon ho trovato nessun canale chiamato \"Cappuccino\", sei sicuro del nome? 🤔", "content": "Cosa intendi? Un canale, un mix, o qualcos'altro? 🧐",
"timestamp": 1761595431.5373232, "timestamp": 1769766901.4737165,
"timestamp_str": "2025-10-27 21:03:51" "timestamp_str": "2026-01-30 09:55:01"
}, },
{ {
"role": "user", "role": "user",
"content": "Okay, aprimi il gelato allora!", "content": "proprio il mixer, lo riesci a raggiungere?",
"timestamp": 1762193935.418264, "timestamp": 1769766912.3007145,
"timestamp_str": "2025-11-03 19:18:55" "timestamp_str": "2026-01-30 09:55:12"
},
{
"role": "user",
"content": "Gli alzi il gelato?",
"timestamp": 1762193958.1497903,
"timestamp_str": "2025-11-03 19:19:18"
},
{
"role": "user",
"content": "Aprimi il giubbotto.",
"timestamp": 1762194045.3195145,
"timestamp_str": "2025-11-03 19:20:45"
}, },
{ {
"role": "assistant", "role": "assistant",
"content": "Non posso aprirti il giubbotto, non è un canale o un mix del mixer! 😄\n\nPosso aiutarti con i canali audio, i microfoni o i monitor, se vuoi.", "content": "✅ Certo, Nicola! Ho appena fatto un refresh e ho ricaricato tutte le informazioni dal mixer. Sembra che sia bello sveglio e raggiungibile! 🎛️✨",
"timestamp": 1762194046.8080468, "timestamp": 1769768014.9555686,
"timestamp_str": "2025-11-03 19:20:46" "timestamp_str": "2026-01-30 10:13:34"
}, },
{ {
"role": "user", "role": "user",
"content": "Aprimi il gelato", "content": "System check",
"timestamp": 1762194053.0858698, "timestamp": 1770577025.9354753,
"timestamp_str": "2025-11-03 19:20:53" "timestamp_str": "2026-02-08 18:57:05"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577350.9690044,
"timestamp_str": "2026-02-08 19:02:30"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577683.8676918,
"timestamp_str": "2026-02-08 19:08:03"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577775.3157346,
"timestamp_str": "2026-02-08 19:09:35"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577899.7554913,
"timestamp_str": "2026-02-08 20:11:39"
},
{
"role": "user",
"content": "System check",
"timestamp": 1770577940.3210933,
"timestamp_str": "2026-02-08 20:12:20"
}, },
{ {
"role": "assistant", "role": "assistant",
"content": "Certo! Gelato acceso! 🍦✅", "content": "✅ Fatto, Nicola! Ho appena fatto un controllo completo del sistema e ricaricato tutte le informazioni dal mixer. È bello sveglio e raggiungibile! 🎛️✨",
"timestamp": 1762194054.6322277, "timestamp": 1770577943.5131805,
"timestamp_str": "2025-11-03 19:20:54" "timestamp_str": "2026-02-08 20:12:23"
}, },
{ {
"role": "user", "role": "user",
"content": "È ancora chiuso", "content": "Ciao",
"timestamp": 1762194065.7709692, "timestamp": 1770577995.903267,
"timestamp_str": "2025-11-03 19:21:05" "timestamp_str": "2026-02-08 19:13:15"
},
{
"role": "assistant",
"content": "Ciao Nicola! Tutto bene? Dimmi pure se hai bisogno di qualcosa con il mixer! 🎛️🎤",
"timestamp": 1770577997.697525,
"timestamp_str": "2026-02-08 19:13:17"
},
{
"role": "user",
"content": "Ciao pupo",
"timestamp": 1770578526.691519,
"timestamp_str": "2026-02-08 19:22:06"
},
{
"role": "assistant",
"content": "Ciao Nicola! Tutto bene? Dimmi pure se hai bisogno di qualcosa con il mixer! 🎛️🎤",
"timestamp": 1770578528.1236699,
"timestamp_str": "2026-02-08 19:22:08"
},
{
"role": "user",
"content": "Check canali",
"timestamp": 1770578537.6022112,
"timestamp_str": "2026-02-08 19:22:17"
},
{
"role": "assistant",
"content": "Allora, Nicola, ho controllato tutti i canali! Ecco la situazione attuale:\n\n**Canali Attivi (o con volume udibile):**\n* **PC (Canale 33 e 34):** Accesi, volume a -17.5 dB\n\n**Canali che sono accesi ma con volume bassissimo (-120 dB), quindi praticamente muti:**\n* **Gelato (Canale 2)**\n* **Vox1 (Canale 5)**\n* **Vox2 (Canale 6)**\n* **Basso (Canale 7)**\n* **Kick (Canale 9)**\n* **Snare (Canale 10)**\n* **Tom 1 (Canale 11)**\n* **Tom 2 (Canale 12)**\n* **Tom3 (Canale 13)**\n* **Pan SX (Canale 14)**\n* **Pan dx (Canale 15)**\n* **Vox 5 (Canale 19)**\n* **Tast (Canale 20)**\n* **Vox3 (Canale 23)**\n* **Chit cnt (Canale 24)**\n* **Chit dx (Canale 25)**\n* **Vox 4 (Canale 27)**\n* **ch35 (Canale 35)**\n* **ch36 (Canale 36)**\n* **ch37 (Canale 37)**\n* **ch38 (Canale 38)**\n* **ch39 (Canale 39)**\n* **ch40 (Canale 40)**\n\n**Canali spenti o sconosciuti:**\n* **Canale 1:** Sconosciuto, spento, volume a 0.65 dB\n* **Talkback (Canale 3):** Spento, volume a -60 dB\n* **Canale 4:** Sconosciuto, spento, volume a -120 dB\n* **Canale 8:** Sconosciuto, spento, volume a -120 dB\n* **Canale 16:** Sconosciuto, spento, volume a -120 dB\n* **Canale 17:** Sconosciuto, spento, volume a -120 dB\n* **Archetto (Canale 18):** Spento, volume a -76 dB\n* **Canale 21:** Sconosciuto, spento, volume a -120 dB\n* **Canale 22:** Sconosciuto, spento, volume a -120 dB\n* **Canale 26:** Sconosciuto, spento, volume a -120 dB\n* **Canale 28:** Sconosciuto, spento, volume a -120 dB\n* **Pad (Canale 29):** Spento, volume a -120 dB\n* **Canali dal 30 al 32:** Sconosciuti, spenti, volume a -120 dB\n\nLa cache è stata aggiornata circa 10 minuti fa. Sembra che molti canali siano spenti o con volumi molto bassi. Fammi sapere se vuoi che accenda o alzi qualcosa! 🎛️🎤",
"timestamp": 1770578542.7348733,
"timestamp_str": "2026-02-08 19:22:22"
} }
] ]
} }

View File

@@ -1,94 +1,10 @@
{ {
"user_id": 6648463335, "user_id": 6648463335,
"username": "chiesasiloe", "username": "chiesasiloe",
"last_updated": 1761594892.8226538, "last_updated": 1771777427.5488307,
"last_updated_str": "2025-10-27 20:54:52", "last_updated_str": "2026-02-22 16:23:47",
"message_count": 21, "message_count": 21,
"history": [ "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", "role": "assistant",
"content": "Ok, ho spento la tastiera (canale 20). 🎹❌", "content": "Ok, ho spento la tastiera (canale 20). 🎹❌",
@@ -130,6 +46,90 @@
"content": "Ok, la tastiera (canale 20) è di nuovo aperta! 🎹✅", "content": "Ok, la tastiera (canale 20) è di nuovo aperta! 🎹✅",
"timestamp": 1761594892.8226538, "timestamp": 1761594892.8226538,
"timestamp_str": "2025-10-27 20:54:52" "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"
} }
] ]
} }

35
docker-compose.yaml Normal file
View File

@@ -0,0 +1,35 @@
# docker-compose.yml
version: '3.8'
services:
tf5-bot:
# Costruisce l'immagine usando il Dockerfile nella cartella corrente
build: .
# Nome del container per una facile identificazione
container_name: tf5-telegram-bot
# Carica le variabili d'ambiente dal file .env
env_file:
- .env
# Politica di riavvio:
# 'unless-stopped' riavvia il container automaticamente all'avvio di Docker
# (e in caso di crash), a meno che tu non lo abbia fermato manualmente.
restart: unless-stopped
# Modalità di rete "host":
# Questo è FONDAMENTALE. Permette al container di accedere direttamente
# alla rete del tuo computer. Senza questo, il bot non potrebbe
# "vedere" e connettersi al mixer TF5 che si trova sulla tua rete locale.
network_mode: "host"
# Volumi:
# Questo collega le cartelle del tuo computer (a sinistra dei due punti)
# alle cartelle all'interno del container (a destra).
# In questo modo, i profili, le conversazioni e la cache vengono salvati
# sul tuo disco e non vengono persi se il container viene riavviato.
volumes:
- ./profiles:/app/profiles
- ./conversations:/app/conversations
- ./cache:/app/cache

View File

@@ -0,0 +1,12 @@
{
"channels": {},
"mixes": {},
"stereo_ins": {},
"fx_returns": {},
"dcas": {},
"matrices": {},
"stereo_out": {},
"mono_out": {},
"mute_masters": {},
"timestamp": 0
}

File diff suppressed because it is too large Load Diff

17
profiles/1552084582.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 1552084582,
"username": "laissaantuness",
"role": "musicista",
"display_name": "Lai",
"channel_labels": [
"Chitarra"
],
"mix_labels": [
"Aux 7"
],
"setup_completed": true,
"created_at": 1762199748.9965794,
"created_at_str": "2025-11-03 19:55:48",
"last_updated": 1762199837.8484952,
"last_updated_str": "2025-11-03 19:57:17"
}

13
profiles/309700430.json Normal file
View File

@@ -0,0 +1,13 @@
{
"user_id": 309700430,
"username": "Cicciomt7",
"role": "mixerista",
"display_name": "Frank",
"channel_labels": [],
"mix_labels": [],
"setup_completed": true,
"created_at": 1762198859.8032317,
"created_at_str": "2025-11-03 19:40:59",
"last_updated": 1762198880.1237595,
"last_updated_str": "2025-11-03 19:41:20"
}

17
profiles/392578198.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 392578198,
"username": "Unknown",
"role": "cantante",
"display_name": "Rox",
"channel_labels": [
"Vox1"
],
"mix_labels": [
"Aux2"
],
"setup_completed": true,
"created_at": 1762199748.3117115,
"created_at_str": "2025-11-03 19:55:48",
"last_updated": 1762199831.8709695,
"last_updated_str": "2025-11-03 19:57:11"
}

17
profiles/436176546.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 436176546,
"username": "Gloriushhh",
"role": "cantante",
"display_name": "Gloriush",
"channel_labels": [
"VOX 2"
],
"mix_labels": [
"AUX 3"
],
"setup_completed": true,
"created_at": 1762199750.435028,
"created_at_str": "2025-11-03 19:55:50",
"last_updated": 1762199825.8800879,
"last_updated_str": "2025-11-03 19:57:05"
}

View File

@@ -3,11 +3,19 @@
"username": "unnikked", "username": "unnikked",
"role": "mixerista", "role": "mixerista",
"display_name": "Nicola", "display_name": "Nicola",
"channels": [], "channel_labels": [
"mixes": [], "I canali della batteria (kick",
"drum",
"pan",
"tom",
"etc)"
],
"mix_labels": [
"Aux batteria"
],
"setup_completed": true, "setup_completed": true,
"created_at": 1762193898.87283, "created_at": 1762198047.365003,
"created_at_str": "2025-11-03 19:18:18", "created_at_str": "2025-11-03 20:27:27",
"last_updated": 1762193916.4840386, "last_updated": 1762799485.4542227,
"last_updated_str": "2025-11-03 19:18:36" "last_updated_str": "2025-11-10 19:31:25"
} }

17
profiles/540754896.json Normal file
View File

@@ -0,0 +1,17 @@
{
"user_id": 540754896,
"username": "merymoon001",
"role": "musicista",
"display_name": "Moon",
"channel_labels": [
"Tastiera"
],
"mix_labels": [
"aux 8"
],
"setup_completed": true,
"created_at": 1762199744.617781,
"created_at_str": "2025-11-03 19:55:44",
"last_updated": 1762199821.1137702,
"last_updated_str": "2025-11-03 19:57:01"
}

13
profiles/6648463335.json Normal file
View File

@@ -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"
}

View File

@@ -1 +1,4 @@
dotenv dotenv
google-genai
pyyaml
readline

246
task_runner.py Normal file
View File

@@ -0,0 +1,246 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
task_runner.py
==============
Esegue file di task YAML sul Yamaha TF5 tramite TF5MixerController.
Uso da CLI:
python task_runner.py tasks_concerto_live.yaml # lista le cue disponibili
python task_runner.py tasks_concerto_live.yaml CUE_01 # esegue una cue specifica
python task_runner.py tasks_concerto_live.yaml --all # esegue tutte le cue in sequenza
"""
import sys
import time
import yaml
from pathlib import Path
from typing import Optional
from mixer_controller import TF5MixerController
# ──────────────────────────────────────────────────────────────
# DISPATCHER: mappa action → metodo del controller
# ──────────────────────────────────────────────────────────────
class TaskRunner:
def __init__(self, controller: TF5MixerController):
self.ctrl = controller
# Mappa nome azione → callable
self._action_map = {
"set_channel_level": self._set_channel_level,
"set_channel_on_off": self._set_channel_on_off,
"set_channel_pan": self._set_channel_pan,
"mute_channels": self._mute_channels,
"unmute_channels": self._unmute_channels,
"set_mix_level": self._set_mix_level,
"set_mix_on_off": self._set_mix_on_off,
"set_channel_to_mix_level": self._set_channel_to_mix_level,
"set_channel_to_mix_on_off":self._set_channel_to_mix_on_off,
"recall_scene": self._recall_scene,
"wait": self._wait,
"refresh_cache": self._refresh_cache,
}
# ── Handlers specifici ──────────────────────────────────
def _set_channel_level(self, step: dict) -> dict:
return self.ctrl.set_channel_level(step["channel"], float(step["level_db"]))
def _set_channel_on_off(self, step: dict) -> dict:
return self.ctrl.set_channel_on_off(step["channel"], bool(step["state"]))
def _set_channel_pan(self, step: dict) -> dict:
return self.ctrl.set_channel_pan(step["channel"], int(step["pan"]))
def _mute_channels(self, step: dict) -> dict:
return self.ctrl.mute_multiple_channels(list(step["channels"]))
def _unmute_channels(self, step: dict) -> dict:
return self.ctrl.unmute_multiple_channels(list(step["channels"]))
def _set_mix_level(self, step: dict) -> dict:
return self.ctrl.set_mix_level(step["mix"], float(step["level_db"]))
def _set_mix_on_off(self, step: dict) -> dict:
return self.ctrl.set_mix_on_off(step["mix"], bool(step["state"]))
def _set_channel_to_mix_level(self, step: dict) -> dict:
return self.ctrl.set_channel_to_mix_level(step["channel"], step["mix"], float(step["level_db"]))
def _set_channel_to_mix_on_off(self, step: dict) -> dict:
return self.ctrl.set_channel_to_mix_on_off(step["channel"], step["mix"], bool(step["state"]))
def _recall_scene(self, step: dict) -> dict:
return self.ctrl.recall_scene(str(step["bank"]), int(step["scene"]))
def _wait(self, step: dict) -> dict:
ms = int(step.get("ms", 0))
print(f" ⏱ Wait puro: {ms} ms")
time.sleep(ms / 1000.0)
return {"status": "success", "message": f"Atteso {ms} ms"}
def _refresh_cache(self, step: dict) -> dict:
return self.ctrl.refresh_cache()
# ── Esecuzione di un singolo step ───────────────────────
def _run_step(self, step_num: int, step: dict) -> bool:
"""Esegue un singolo step. Ritorna True se OK, False se errore."""
action = step.get("action")
delay_ms = int(step.get("delay_ms", 0))
wait_ms = int(step.get("wait_ms", 0))
if delay_ms > 0:
print(f" ⏳ delay_ms={delay_ms} prima dell'azione...")
time.sleep(delay_ms / 1000.0)
handler = self._action_map.get(action)
if handler is None:
print(f" ⚠️ Step {step_num}: azione '{action}' non riconosciuta — saltato.")
return False
print(f" ▶ Step {step_num}: {action} | params: { {k:v for k,v in step.items() if k not in ('action','delay_ms','wait_ms')} }")
try:
result = handler(step)
status = result.get("status", "unknown")
msg = result.get("message", "")
icon = "" if status in ("success", "partial") else ""
print(f" {icon} [{status}] {msg}")
if wait_ms > 0:
time.sleep(wait_ms / 1000.0)
return status in ("success", "partial")
except Exception as e:
print(f" ❌ Eccezione durante '{action}': {e}")
return False
# ── Esecuzione di una cue ────────────────────────────────
def run_cue(self, cue: dict) -> bool:
"""
Esegue tutti gli step di una cue.
Ritorna True se tutti gli step sono andati a buon fine.
"""
cue_id = cue.get("id", "???")
name = cue.get("name", "")
on_error = cue.get("on_error", "stop")
steps = cue.get("steps", [])
print(f"\n{''*60}")
print(f"🎬 CUE: {cue_id}{name}")
if cue.get("description"):
print(f" 📋 {cue['description']}")
print(f" Steps: {len(steps)} | on_error: {on_error}")
print(f"{''*60}")
all_ok = True
for i, step in enumerate(steps, start=1):
ok = self._run_step(i, step)
if not ok:
all_ok = False
if on_error == "stop":
print(f"\n 🛑 on_error=stop: cue {cue_id} interrotta allo step {i}.")
return False
# on_error == "continue": vai avanti
result_icon = "" if all_ok else "⚠️ (con errori)"
print(f"\n {result_icon} Cue {cue_id} completata.")
return all_ok
# ──────────────────────────────────────────────────────────────
# LOADER YAML
# ──────────────────────────────────────────────────────────────
def load_task_file(path: str) -> dict:
"""Carica e valida un file di task YAML."""
p = Path(path)
if not p.exists():
raise FileNotFoundError(f"File non trovato: {path}")
with open(p, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
if "cues" not in data or not isinstance(data["cues"], list):
raise ValueError("Il file YAML deve contenere una lista 'cues'.")
return data
def print_event_info(data: dict):
event = data.get("event", {})
if event:
print(f"\n{''*60}")
print(f"📅 Evento : {event.get('name', 'N/D')}")
print(f" Data : {event.get('date', 'N/D')}")
print(f" Venue : {event.get('venue', 'N/D')}")
if event.get("notes"):
print(f" Note : {event['notes']}")
def list_cues(data: dict):
print_event_info(data)
print(f"\n{''*60}")
print("📋 CUE DISPONIBILI:")
print(f"{''*60}")
for cue in data["cues"]:
steps_count = len(cue.get("steps", []))
print(f" {cue.get('id'):12s}{cue.get('name','')} ({steps_count} steps)")
print(f"{''*60}\n")
# ──────────────────────────────────────────────────────────────
# ENTRY POINT
# ──────────────────────────────────────────────────────────────
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
task_file = sys.argv[1]
cue_arg = sys.argv[2] if len(sys.argv) > 2 else None
try:
data = load_task_file(task_file)
except (FileNotFoundError, ValueError) as e:
print(f"❌ Errore nel caricamento del file: {e}")
sys.exit(1)
# Solo lista cue senza mixer
if cue_arg is None:
list_cues(data)
sys.exit(0)
# Connessione al mixer ed esecuzione
print("🔌 Connessione al mixer...")
import config
with TF5MixerController(host=config.get_host()) as ctrl:
runner = TaskRunner(ctrl)
if cue_arg == "--all":
print_event_info(data)
print("\n▶▶ Esecuzione di TUTTE le cue in sequenza...")
for cue in data["cues"]:
ok = runner.run_cue(cue)
if not ok and cue.get("on_error", "stop") == "stop":
print(f"\n🛑 Esecuzione interrotta a causa di un errore in {cue.get('id')}.")
sys.exit(1)
else:
# Cerca la cue per ID
cue_found = next((c for c in data["cues"] if c.get("id") == cue_arg), None)
if cue_found is None:
print(f"❌ Cue '{cue_arg}' non trovata nel file.")
list_cues(data)
sys.exit(1)
runner.run_cue(cue_found)
print("\n✅ Task runner completato.\n")
if __name__ == "__main__":
main()

71
task_schema.yaml Normal file
View File

@@ -0,0 +1,71 @@
# ============================================================
# SCHEMA DEI TASK - Yamaha TF5 Mixer Controller
# ============================================================
# Ogni file di task descrive UN evento (concerto, conferenza, ecc.)
# composto da N cues, ognuna con N steps eseguiti in sequenza.
#
# STRUTTURA GENERALE:
#
# event: → metadati dell'evento (opzionale)
# cues: → lista delle cue eseguibili
# - id: → identificatore univoco (es. "CUE_01")
# name: → nome leggibile
# description: → note operative (opzionale)
# on_error: → "stop" | "continue" (default: "stop")
# steps: → lista di azioni in sequenza
# - action: → tipo di azione (vedi sotto)
# ...params → parametri specifici dell'azione
# delay_ms: → attesa PRIMA di eseguire questo step (ms)
# wait_ms: → attesa DOPO l'esecuzione (ms)
#
# ============================================================
# TIPI DI AZIONE DISPONIBILI:
# ============================================================
#
# 1. set_channel_level
# channel: int (1-40)
# level_db: float (es. 0.0, -10.5, -inf → usa -999)
#
# 2. set_channel_on_off
# channel: int
# state: bool (true = ON, false = OFF / mute)
#
# 3. set_channel_pan
# channel: int
# pan: int (-63 = sinistra, 0 = centro, +63 = destra)
#
# 4. mute_channels
# channels: [int, int, ...]
#
# 5. unmute_channels
# channels: [int, int, ...]
#
# 6. set_mix_level
# mix: int (1-20)
# level_db: float
#
# 7. set_mix_on_off
# mix: int
# state: bool
#
# 8. set_channel_to_mix_level
# channel: int
# mix: int
# level_db: float
#
# 9. set_channel_to_mix_on_off
# channel: int
# mix: int
# state: bool
#
# 10. recall_scene
# bank: "a" | "b"
# scene: int (0-99)
#
# 11. wait
# ms: int → pausa pura senza azioni sul mixer
#
# 12. refresh_cache
# (nessun parametro) → forza un aggiornamento completo della cache
#
# ============================================================

238
tasks_concerto_live.yaml Normal file
View File

@@ -0,0 +1,238 @@
# ============================================================
# TASK FILE - Concerto Live "Band Example"
# Yamaha TF5 Mixer Controller
# ============================================================
event:
name: "Concerto Live - Band Example"
date: "2025-06-14"
venue: "Teatro Comunale"
notes: "FOH setup. Mix 1=IEM Vocalist, Mix 2=IEM Chitarra, Mix 3=Wedge Batteria"
cues:
# ----------------------------------------------------------
# PRE-SHOW: tutto silenzioso, scene base caricata
# ----------------------------------------------------------
- id: "CUE_01"
name: "Pre-Show - Carica scena base"
description: "Richiama la scena di partenza e porta tutto a zero"
on_error: "stop"
steps:
- action: recall_scene
bank: "a"
scene: 1
- action: wait
ms: 3000 # attendi che il mixer elabori la scena
- action: refresh_cache
wait_ms: 500
- action: mute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- action: set_mix_on_off
mix: 1
state: false
- action: set_mix_on_off
mix: 2
state: false
- action: set_mix_on_off
mix: 3
state: false
# ----------------------------------------------------------
# SOUNDCHECK: apri i canali uno alla volta con fade-in
# ----------------------------------------------------------
- id: "CUE_02"
name: "Soundcheck - Fade-in canali batteria"
description: "Canali 1-4 (kick, snare, OH sx, OH dx) portati a -10 dB gradualmente"
on_error: "continue"
steps:
# Kick
- action: set_channel_level
channel: 1
level_db: -40.0
- action: set_channel_on_off
channel: 1
state: true
wait_ms: 200
- action: set_channel_level
channel: 1
level_db: -20.0
wait_ms: 200
- action: set_channel_level
channel: 1
level_db: -10.0
wait_ms: 300
# Snare
- action: set_channel_level
channel: 2
level_db: -40.0
delay_ms: 500
- action: set_channel_on_off
channel: 2
state: true
wait_ms: 200
- action: set_channel_level
channel: 2
level_db: -10.0
wait_ms: 300
# Overhead Sx + Dx insieme
- action: set_channel_level
channel: 3
level_db: -40.0
delay_ms: 500
- action: set_channel_level
channel: 4
level_db: -40.0
- action: set_channel_on_off
channel: 3
state: true
- action: set_channel_on_off
channel: 4
state: true
wait_ms: 200
- action: set_channel_pan
channel: 3
pan: -30 # OH sx → sinistra
- action: set_channel_pan
channel: 4
pan: 30 # OH dx → destra
wait_ms: 200
- action: set_channel_level
channel: 3
level_db: -14.0
- action: set_channel_level
channel: 4
level_db: -14.0
# ----------------------------------------------------------
# IEM MIX 1 (Vocalist): setup invii
# ----------------------------------------------------------
- id: "CUE_03"
name: "IEM Mix 1 - Setup invii vocalist"
description: "Configura il mix 1 per il vocalist: voce principale alta, resto basso"
on_error: "continue"
steps:
- action: set_mix_level
mix: 1
level_db: 0.0
- action: set_mix_on_off
mix: 1
state: true
wait_ms: 200
# Voce principale (ch 9) → Mix 1 alta
- action: set_channel_to_mix_level
channel: 9
mix: 1
level_db: 0.0
- action: set_channel_to_mix_on_off
channel: 9
mix: 1
state: true
# Chitarra ritmica (ch 5) → Mix 1 media
- action: set_channel_to_mix_level
channel: 5
mix: 1
level_db: -6.0
delay_ms: 100
- action: set_channel_to_mix_on_off
channel: 5
mix: 1
state: true
# Kick (ch 1) → Mix 1 bassa (solo click per tempo)
- action: set_channel_to_mix_level
channel: 1
mix: 1
level_db: -12.0
delay_ms: 100
- action: set_channel_to_mix_on_off
channel: 1
mix: 1
state: true
# ----------------------------------------------------------
# SHOW START: tutto ON, master a 0 dB
# ----------------------------------------------------------
- id: "CUE_04"
name: "SHOW START - Apri tutto"
description: "Porta tutti i canali attivi e il master a nominal"
on_error: "stop"
steps:
- action: unmute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
- action: set_mix_on_off
mix: 1
state: true
- action: set_mix_on_off
mix: 2
state: true
- action: set_mix_on_off
mix: 3
state: true
- action: recall_scene
bank: "a"
scene: 10 # scena "SHOW" pre-salvata
wait_ms: 2000
- action: refresh_cache
# ----------------------------------------------------------
# PAUSA: muta rapidamente tutto il palco
# ----------------------------------------------------------
- id: "CUE_05"
name: "PAUSA - Muta palco"
description: "Muta tutti i canali di palco, lascia accesa la diffusione BG music"
on_error: "continue"
steps:
- action: mute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
- action: set_channel_on_off # ch 16 = BG music playback
channel: 16
state: true
delay_ms: 200
- action: set_channel_level
channel: 16
level_db: -6.0
# ----------------------------------------------------------
# FINE SHOW: fade-out master in 5 step da 1 secondo
# ----------------------------------------------------------
- id: "CUE_06"
name: "FINE SHOW - Fade out generale"
description: "Abbassa tutti i canali gradualmente in 5 secondi"
on_error: "continue"
steps:
- action: set_channel_level
channel: 9
level_db: -6.0
wait_ms: 1000
- action: set_channel_level
channel: 9
level_db: -12.0
wait_ms: 1000
- action: set_channel_level
channel: 9
level_db: -20.0
wait_ms: 1000
- action: set_channel_level
channel: 9
level_db: -40.0
wait_ms: 1000
- action: mute_channels
channels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 16]
- action: recall_scene
bank: "a"
scene: 0 # scena vuota / safe
delay_ms: 500

View File

@@ -63,8 +63,8 @@ class UserProfile:
self.username = username self.username = username
self.role: Optional[str] = None self.role: Optional[str] = None
self.display_name: Optional[str] = None self.display_name: Optional[str] = None
self.channels: List[int] = [] # Canali assegnati self.channel_labels: List[str] = [] # Etichette dei canali assegnati
self.mixes: List[int] = [] # Mix/aux assegnati self.mix_labels: List[str] = [] # Etichette dei mix/aux assegnati
self.setup_completed = False self.setup_completed = False
self.created_at = time.time() self.created_at = time.time()
self.last_updated = time.time() self.last_updated = time.time()
@@ -76,8 +76,8 @@ class UserProfile:
"username": self.username, "username": self.username,
"role": self.role, "role": self.role,
"display_name": self.display_name, "display_name": self.display_name,
"channels": self.channels, "channel_labels": self.channel_labels,
"mixes": self.mixes, "mix_labels": self.mix_labels,
"setup_completed": self.setup_completed, "setup_completed": self.setup_completed,
"created_at": self.created_at, "created_at": self.created_at,
"created_at_str": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.created_at)), "created_at_str": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.created_at)),
@@ -91,8 +91,8 @@ class UserProfile:
profile = cls(data["user_id"], data["username"]) profile = cls(data["user_id"], data["username"])
profile.role = data.get("role") profile.role = data.get("role")
profile.display_name = data.get("display_name") profile.display_name = data.get("display_name")
profile.channels = data.get("channels", []) profile.channel_labels = data.get("channel_labels", [])
profile.mixes = data.get("mixes", []) profile.mix_labels = data.get("mix_labels", [])
profile.setup_completed = data.get("setup_completed", False) profile.setup_completed = data.get("setup_completed", False)
profile.created_at = data.get("created_at", time.time()) profile.created_at = data.get("created_at", time.time())
profile.last_updated = data.get("last_updated", time.time()) profile.last_updated = data.get("last_updated", time.time())
@@ -102,36 +102,6 @@ class UserProfile:
"""Restituisce le info sul ruolo.""" """Restituisce le info sul ruolo."""
return self.ROLES.get(self.role, {}) return self.ROLES.get(self.role, {})
def can_access_channel(self, channel: int) -> bool:
"""Verifica se l'utente può accedere a un canale."""
if not self.role:
return False
permissions = self.get_role_info().get("permissions", [])
if "all" in permissions:
return True
if "own_channel" in permissions:
return channel in self.channels
return False
def can_access_mix(self, mix: int) -> bool:
"""Verifica se l'utente può accedere a un mix."""
if not self.role:
return False
permissions = self.get_role_info().get("permissions", [])
if "all" in permissions:
return True
if "own_mix" in permissions:
return mix in self.mixes
return False
def get_summary(self) -> str: def get_summary(self) -> str:
"""Restituisce un riepilogo del profilo.""" """Restituisce un riepilogo del profilo."""
if not self.setup_completed: if not self.setup_completed:
@@ -140,15 +110,14 @@ class UserProfile:
role_info = self.get_role_info() role_info = self.get_role_info()
msg = f"{role_info['emoji']} **{self.display_name}** ({role_info['name']})\n\n" msg = f"{role_info['emoji']} **{self.display_name}** ({role_info['name']})\n\n"
if self.channels: if self.channel_labels:
msg += f"🎚️ Canali: {', '.join(map(str, self.channels))}\n" msg += f"🎚️ Canali: {', '.join(self.channel_labels)}\n"
if self.mixes: if self.mix_labels:
msg += f"🔊 Mix: {', '.join(map(str, self.mixes))}\n" msg += f"🔊 Mix: {', '.join(self.mix_labels)}\n"
return msg return msg
class ProfileManager: class ProfileManager:
"""Gestisce i profili utente.""" """Gestisce i profili utente."""
@@ -221,7 +190,6 @@ class ProfileManager:
return profiles return profiles
class ConversationManager: class ConversationManager:
"""Gestisce il salvataggio delle conversazioni su file JSON.""" """Gestisce il salvataggio delle conversazioni su file JSON."""
@@ -289,7 +257,6 @@ class ConversationManager:
print(f"❌ Errore nell'eliminazione conversazione {user_id}: {e}") print(f"❌ Errore nell'eliminazione conversazione {user_id}: {e}")
return False return False
class TelegramBot: class TelegramBot:
"""Bot Telegram base con long polling.""" """Bot Telegram base con long polling."""
@@ -423,6 +390,7 @@ class TF5TelegramBot:
self.controller.set_channel_to_mix_level, self.controller.set_channel_to_mix_level,
self.controller.set_channel_to_mix_on_off, self.controller.set_channel_to_mix_on_off,
self.controller.get_channel_to_mix_info, self.controller.get_channel_to_mix_info,
self.controller.get_full_mix_details,
], ],
temperature=0, temperature=0,
) )
@@ -504,6 +472,37 @@ GRUPPI DI CANALI:
- "spegni tutto tranne..." → muta tutti gli altri - "spegni tutto tranne..." → muta tutti gli altri
- "solo i microfoni" → attiva solo quelli, spegni il resto - "solo i microfoni" → attiva solo quelli, spegni il resto
---
ANALISI DEL MIX (per utenti esperti)
Quando un utente chiede di "analizzare un mix", "controllare i volumi della diretta", o "dare suggerimenti per il mix X", devi usare la funzione `get_full_mix_details(mix_number)`.
Questa funzione ti darà la lista di tutti i canali inviati a quel mix, con i loro livelli.
Il tuo compito è analizzare questi dati e fornire suggerimenti pratici per migliorare l'equilibrio, specialmente per un mix destinato a una diretta streaming o registrazione.
Linee guida per l'analisi:
1. **Gerarchia dei Volumi**:
- **Predicatore/Oratore Principale**: Deve essere il canale più alto di tutti. Cerca nomi come "Predicatore", "Pastore", "Pulpito". Il suo livello deve essere il punto di riferimento.
- **Voce Solista (Lead Vocal)**: Subito dopo il predicatore. Deve essere chiaramente sopra gli strumenti e i cori. Cerca "Voce L", "Cantante P".
- **Cori/Voci Secondarie**: Dovrebbero essere a un livello inferiore rispetto alla voce solista, per supportarla senza coprirla.
- **Strumenti Melodici**: (Tastiere, Chitarre) Dovrebbero creare un "tappeto sonoro" sotto le voci. Il loro livello deve essere bilanciato per non competere con le voci.
- **Sezione Ritmica**: (Basso, Batteria) Devono essere presenti ma controllati. Il basso deve essere udibile ma non rimbombante, la batteria presente ma non invadente.
2. **Problemi Comuni da Segnalare**:
- **Canali Spenti ma Inviati**: Controlla il campo `channel_is_on`. Se è `False` per un canale con un livello di send significativo, è un errore comune. Segnalalo dicendo: "Attenzione, il canale X ('Nome Canale') è spento sul suo fader principale, ma stai cercando di mandarlo al mix. Non si sentirà nulla. Devi prima accenderlo."
- **Livelli Estremi**: Segnala i canali con send molto alti (sopra +5 dB, rischio di distorsione) o molto bassi (sotto -30 dB, forse inutili o dimenticati).
- **Equilibrio Voci/Strumenti**: Se il livello medio degli strumenti è uguale o superiore a quello della voce solista, suggerisci di abbassare gli strumenti per fare spazio alla voce. Esempio: "Le tastiere sono a -5dB e la voce a -6dB. Prova ad abbassare un po' le tastiere per far emergere meglio la voce."
3. **Formato della Risposta**:
- Inizia con un riepilogo generale del mix (es: "Ecco un'analisi del Mix 15 (Diretta Streaming):").
- Usa sezioni chiare con emoji per una lettura rapida:
- `✅ Punti di Forza`: Se vedi un buon equilibrio di base.
- `⚠️ Suggerimenti per Migliorare`: Elenco puntato con azioni concrete.
- `🤔 Problemi Rilevati`: Per i canali spenti o livelli anomali.
- Sii propositivo e offri soluzioni, non solo problemi.
Esempio di analisi: "Ho analizzato il mix 15. La voce del predicatore è correttamente il canale più forte. Tuttavia, la chitarra acustica è quasi allo stesso livello della voce solista. Ti suggerisco di abbassare la chitarra di circa 3dB per dare più risalto al cantante. Ho notato anche che il canale 12 ('Coro Dx') è spento, quindi non sta contribuendo al mix. Vuoi che lo accenda?"
---
CASI PARTICOLARI: CASI PARTICOLARI:
- Se la richiesta è ambigua, chiedi chiarimenti in modo colloquiale - Se la richiesta è ambigua, chiedi chiarimenti in modo colloquiale
- Se serve cercare un canale/mix, usa prima la cache (search_channels_by_name / search_mixes_by_name) - Se serve cercare un canale/mix, usa prima la cache (search_channels_by_name / search_mixes_by_name)
@@ -536,13 +535,17 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
instruction += f"Nome: {profile.display_name}\n" instruction += f"Nome: {profile.display_name}\n"
instruction += f"Ruolo: {role_info['name']} {role_info['emoji']}\n" instruction += f"Ruolo: {role_info['name']} {role_info['emoji']}\n"
if profile.channels: if profile.channel_labels:
instruction += f"\nCanali assegnati: {', '.join(map(str, profile.channels))}\n" labels_str = ', '.join([f"'{l}'" for l in profile.channel_labels])
instruction += "Quando l'utente dice 'il mio canale' o 'il mio mic/strumento', si riferisce a questi.\n" instruction += f"\nNomi dei canali assegnati: {labels_str}\n"
instruction += "Quando l'utente dice 'il mio canale' o 'il mio mic/strumento', si riferisce a questi. "
instruction += "**DEVI usare `search_channels_by_name` con questi nomi per trovare il numero di canale corretto prima di eseguire qualsiasi altra azione.**\n"
if profile.mixes: if profile.mix_labels:
instruction += f"\nMix assegnati: {', '.join(map(str, profile.mixes))}\n" labels_str = ', '.join([f"'{l}'" for l in profile.mix_labels])
instruction += "Quando l'utente dice 'le mie cuffie', 'il mio monitor', 'in cuffia', si riferisce a questi mix.\n" instruction += f"\nNomi dei mix assegnati: {labels_str}\n"
instruction += "Quando l'utente dice 'le mie cuffie', 'il mio monitor', 'in cuffia', si riferisce a questi mix. "
instruction += "**DEVI usare `search_mixes_by_name` con questi nomi per trovare il numero di mix corretto prima di eseguire qualsiasi altra azione.**\n"
# Istruzioni specifiche per ruolo # Istruzioni specifiche per ruolo
permissions = role_info.get("permissions", []) permissions = role_info.get("permissions", [])
@@ -555,77 +558,27 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
else: else:
instruction += f"\n⚠️ LIMITAZIONI DI ACCESSO ({role_info['name']}):\n" instruction += f"\n⚠️ LIMITAZIONI DI ACCESSO ({role_info['name']}):\n"
if "own_channel" in permissions and profile.channels: if "own_channel" in permissions and profile.channel_labels:
instruction += f"- Canali controllabili: SOLO {', '.join(map(str, profile.channels))}\n" instruction += f"- Canali controllabili: SOLO quelli che corrispondono ai nomi nel tuo profilo.\n"
if "own_mix" in permissions and profile.mixes: if "own_mix" in permissions and profile.mix_labels:
instruction += f"- Mix controllabili: SOLO {', '.join(map(str, profile.mixes))}\n" instruction += f"- Mix controllabili: SOLO quelli che corrispondono ai nomi nel tuo profilo.\n"
instruction += f"- Puoi gestire i send di QUALSIASI canale verso i TUOI mix ({', '.join(map(str, profile.mixes))})\n" instruction += f"- Puoi gestire i send di QUALSIASI canale verso i TUOI mix.\n"
instruction += "\n❌ NON puoi:\n" instruction += "\n❌ NON puoi:\n"
if "own_channel" not in permissions: instruction += "- Caricare scene (solo i mixeristi).\n"
instruction += "- Modificare il volume/stato dei canali\n" instruction += "- Accedere a canali/mix non associati al tuo profilo.\n"
if "own_mix" not in permissions:
instruction += "- Modificare il volume/stato dei mix\n"
instruction += "- Caricare scene (solo i mixeristi)\n"
instruction += "- Accedere a canali/mix non assegnati\n"
instruction += "\n💡 INTERPRETAZIONE RICHIESTE:\n" instruction += "\n💡 INTERPRETAZIONE RICHIESTE:\n"
instruction += "- 'alzami la chitarra' = alza il send della chitarra nel TUO mix\n" instruction += "- 'alzami la chitarra' = alza il send della chitarra nel TUO mix (identificato dal suo nome).\n"
instruction += "- 'più voce' = alza il send della voce nel TUO mix\n" instruction += "- 'più voce' = alza il send della voce nel TUO mix.\n"
instruction += "- 'meno batteria in cuffia' = abbassa il send della batteria nel TUO mix\n" instruction += "- 'meno batteria in cuffia' = abbassa il send della batteria nel TUO mix.\n"
instruction += "- Se l'utente chiede di fare qualcosa fuori dai suoi permessi, spiega gentilmente che non può e suggerisci di chiedere al mixerista\n" instruction += "- Se l'utente chiede di fare qualcosa fuori dai suoi permessi, spiega gentilmente che non può e suggerisci di chiedere al mixerista.\n"
instruction += f"\n{'='*70}\n" instruction += f"\n{'='*70}\n"
return instruction return instruction
def _check_permission(self, profile: UserProfile, operation: str,
channel: Optional[int] = None,
mix: Optional[int] = None) -> tuple[bool, str]:
"""
Verifica se l'utente ha i permessi per un'operazione.
Ritorna (permitted, error_message)
"""
if not profile.setup_completed:
return False, "⚠️ Devi prima configurare il tuo profilo con /setup"
role_info = profile.get_role_info()
permissions = role_info.get("permissions", [])
# Mixeristi possono fare tutto
if "all" in permissions:
return True, ""
# Verifica permessi specifici
if operation in ["set_channel_level", "set_channel_on_off", "set_channel_pan",
"mute_multiple_channels", "unmute_multiple_channels"]:
if "own_channel" not in permissions:
return False, "❌ Solo i mixeristi possono controllare i canali direttamente"
if channel and not profile.can_access_channel(channel):
return False, f"❌ Non hai accesso al canale {channel}. I tuoi canali: {', '.join(map(str, profile.channels))}"
if operation in ["set_mix_level", "set_mix_on_off"]:
if "own_mix" not in permissions:
return False, "❌ Non hai permessi per controllare i mix"
if mix and not profile.can_access_mix(mix):
return False, f"❌ Non hai accesso al mix {mix}. I tuoi mix: {', '.join(map(str, profile.mixes))}"
if operation == "recall_scene":
return False, "❌ Solo i mixeristi possono caricare scene"
# Le operazioni di send ai mix sono permesse se hai accesso al mix di destinazione
if operation in ["set_channel_to_mix_level", "set_channel_to_mix_on_off"]:
if "own_mix" not in permissions:
return False, "❌ Non hai permessi per controllare i send ai mix"
if mix and not profile.can_access_mix(mix):
return False, f"❌ Non hai accesso al mix {mix}. I tuoi mix: {', '.join(map(str, profile.mixes))}"
return True, ""
def get_profile(self, user_id: int, username: str) -> UserProfile: def get_profile(self, user_id: int, username: str) -> UserProfile:
"""Ottiene il profilo di un utente.""" """Ottiene il profilo di un utente."""
if user_id not in self.profiles: if user_id not in self.profiles:
@@ -694,7 +647,6 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
role_info = profile.get_role_info() role_info = profile.get_role_info()
permissions = role_info.get("permissions", []) permissions = role_info.get("permissions", [])
# Se è mixerista, ha finito
if "all" in permissions: if "all" in permissions:
profile.setup_completed = True profile.setup_completed = True
self.profile_manager.save_profile(profile) self.profile_manager.save_profile(profile)
@@ -708,59 +660,47 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
f"'Richiama la scena A10'\n" f"'Richiama la scena A10'\n"
f"'Mostrami tutti i canali'") f"'Mostrami tutti i canali'")
# Altrimenti, chiedi i canali (se necessario)
if "own_channel" in permissions: if "own_channel" in permissions:
self.bot.send_message( self.bot.send_message(
user_id, user_id,
f"🎚️ Quali canali usi?\n\n" f"🎚️ **Come si chiama il tuo canale/strumento?**\n\n"
f"Esempi:\n" f"Scrivi il nome esatto che vedi sull'etichetta del mixer (es: 'VOX Marco', 'CH Acustica').\n"
f"• Un canale: 5\n" f"Se hai più canali, separali con una virgola (es: 'Tastiera L', 'Tastiera R').\n\n"
f"• Più canali: 5, 7, 12\n" f"(Scrivi 'nessuno' se non hai un canale specifico)"
f"• Range: 5-8 (canali 5, 6, 7, 8)\n"
f"• Misti: 3, 5-7, 10\n\n"
f"(Scrivi 0 se non usi canali diretti)"
) )
self.setup_states[user_id] = {"step": "channels", "role": profile.role} self.setup_states[user_id] = {"step": "channels"}
return "" elif "own_mix" in permissions:
else:
# Salta direttamente ai mix
self.bot.send_message( self.bot.send_message(
user_id, user_id,
f"🔊 Quali mix/aux usi per le tue cuffie o uscita?\n\n" f"🔊 **Come si chiama il tuo mix per le cuffie/monitor?**\n\n"
f"Esempi:\n" f"Scrivi il nome esatto (es: 'InEar Marco', 'Cuffia Palco Sx').\n"
f"• Un mix: 3\n" f"Se usi un mix stereo, scrivi entrambi i nomi separati da virgola (es: 'IEM L', 'IEM R').\n\n"
f"• Più mix: 3, 5\n" f"(Scrivi 'nessuno' se non usi un mix specifico)"
f"• Range: 3-5\n"
f"• Mix stereo: 9, 10 (in-ear stereo)\n\n"
f"(Scrivi 0 se non usi mix specifici)"
) )
self.setup_states[user_id] = {"step": "mixes", "role": profile.role} self.setup_states[user_id] = {"step": "mixes"}
return ""
return ""
# Step: inserimento canali # Step: inserimento etichette canali
elif step == "channels": elif step == "channels":
channels = self._parse_channel_list(text) labels_text = text.strip()
profile.channels = channels if labels_text.lower() != 'nessuno':
profile.channel_labels = [label.strip() for label in labels_text.split(',')]
role_info = profile.get_role_info() role_info = profile.get_role_info()
permissions = role_info.get("permissions", []) permissions = role_info.get("permissions", [])
# Chiedi i mix se necessario
if "own_mix" in permissions: if "own_mix" in permissions:
self.bot.send_message( self.bot.send_message(
user_id, user_id,
f"🔊 Quali mix/aux usi per le tue cuffie o uscita?\n\n" f"🔊 **Come si chiama il tuo mix per le cuffie/monitor?**\n\n"
f"Esempi:\n" f"Scrivi il nome esatto (es: 'InEar Marco', 'Cuffia Palco Sx').\n"
f"• Un mix: 3\n" f"Se usi un mix stereo, scrivi entrambi i nomi separati da virgola (es: 'IEM L', 'IEM R').\n\n"
f"• Più mix: 3, 5\n" f"(Scrivi 'nessuno' se non usi un mix specifico)"
f"• Range: 3-5\n"
f"• Mix stereo: 9, 10 (in-ear stereo)\n\n"
f"(Scrivi 0 se non usi mix specifici)"
) )
self.setup_states[user_id] = {"step": "mixes", "role": profile.role} self.setup_states[user_id] = {"step": "mixes"}
return "" return ""
else: else:
# Finito
profile.setup_completed = True profile.setup_completed = True
self.profile_manager.save_profile(profile) self.profile_manager.save_profile(profile)
del self.setup_states[user_id] del self.setup_states[user_id]
@@ -771,59 +711,32 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
f"'Alza il mio canale'\n" f"'Alza il mio canale'\n"
f"'Spegni il mio mic'") f"'Spegni il mio mic'")
# Step: inserimento mix # Step: inserimento etichette mix
elif step == "mixes": elif step == "mixes":
mixes = self._parse_channel_list(text) # Stessa logica per i mix labels_text = text.strip()
profile.mixes = mixes if labels_text.lower() != 'nessuno':
profile.mix_labels = [label.strip() for label in labels_text.split(',')]
profile.setup_completed = True profile.setup_completed = True
self.profile_manager.save_profile(profile) self.profile_manager.save_profile(profile)
del self.setup_states[user_id] del self.setup_states[user_id]
role_info = profile.get_role_info()
example_commands = [] example_commands = []
if profile.channels: if profile.channel_labels:
example_commands.append("'Alza il mio canale'") example_commands.append("'Alza il volume del mio canale'")
if profile.mixes: if profile.mix_labels:
example_commands.extend([ example_commands.extend([
"'Alzami la chitarra' (nel tuo mix)", "'Alzami la chitarra nel mio monitor'",
"'Più voce in cuffia'", "'Più voce in cuffia'",
"'Meno batteria'" "'Meno batteria'"
]) ])
return (f"✅ Configurazione completata!\n\n" return (f"✅ Configurazione completata!\n\n"
f"{profile.get_summary()}\n" f"{profile.get_summary()}\n\n"
f"Esempi di comandi:\n" + "\n".join(example_commands)) f"Esempi di comandi che puoi usare:\n" + "\n".join(example_commands))
return "" return ""
def _parse_channel_list(self, text: str) -> List[int]:
"""Parse una lista di canali/mix dal testo utente."""
channels = []
# Rimuovi spazi e split per virgola
parts = [p.strip() for p in text.split(',')]
for part in parts:
# Range (es: 5-8)
if '-' in part:
try:
start, end = map(int, part.split('-'))
channels.extend(range(start, end + 1))
except:
pass
# Singolo numero
else:
try:
num = int(part)
if num > 0: # Ignora 0
channels.append(num)
except:
pass
return sorted(list(set(channels))) # Rimuovi duplicati e ordina
def _get_file_path(self, file_id: str) -> str: def _get_file_path(self, file_id: str) -> str:
"""Richiede a Telegram il percorso di un file.""" """Richiede a Telegram il percorso di un file."""
result = self.bot._make_request("getFile", {"file_id": file_id}) result = self.bot._make_request("getFile", {"file_id": file_id})
@@ -971,16 +884,16 @@ Devi essere veloce, chiaro e capire anche richieste approssimative.
msg += "'Alza il canale 5 a -10 dB'\n" msg += "'Alza il canale 5 a -10 dB'\n"
msg += "'Spegni i canali dal 1 al 5'\n" msg += "'Spegni i canali dal 1 al 5'\n"
msg += "'Richiama la scena A10'\n" msg += "'Richiama la scena A10'\n"
msg += "'Mostrami lo stato del canale 12'\n" msg += "'Analizza il mix 15 per la diretta'\n"
else: else:
msg += "**Esempi di comandi:**\n" msg += "**Esempi di comandi:**\n"
if profile.channels: if profile.channel_labels:
msg += "'Alza il mio canale'\n" msg += "'Alza il mio canale'\n"
msg += "'Spegni il mio mic'\n" msg += "'Spegni il mio mic'\n"
if profile.mixes: if profile.mix_labels:
msg += "'Alzami la chitarra' (nel tuo mix)\n" msg += "'Alzami la chitarra' (nel tuo mix)\n"
msg += "'Più voce in cuffia'\n" msg += "'Più voce in cuffia'\n"
msg += "'Meno batteria'\n" msg += "'Analizza il mio mix per la diretta'\n"
msg += "\n**Comandi speciali:**\n" msg += "\n**Comandi speciali:**\n"
msg += "/help - Mostra questo messaggio\n" msg += "/help - Mostra questo messaggio\n"
@@ -1170,7 +1083,7 @@ def main():
try: try:
bot = TF5TelegramBot( bot = TF5TelegramBot(
telegram_token=telegram_token, telegram_token=telegram_token,
mixer_host=args.mixer_host, mixer_host=get_host(),
mixer_port=args.mixer_port, mixer_port=args.mixer_port,
conversations_dir=args.conversations_dir, conversations_dir=args.conversations_dir,
profiles_dir=args.profiles_dir profiles_dir=args.profiles_dir

930
tf5_cly.py Normal file
View File

@@ -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", "", "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 <PATH> <IDX1> <IDX2> → legge un parametro
set <PATH> <IDX1> <IDX2> <V> → scrive un parametro
ssrecall_ex scene_a <N> → richiama scena banco A
Risposta OK: OK <valore>
Risposta ERR: ERROR <codice> <messaggio>
Notify: NOTIFY <PATH> <IDX1> <IDX2> <V>
""")
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: 0127 (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)