meter
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"ip": "192.168.1.59",
|
||||
"timestamp": 1771271930.0475464
|
||||
"ip": "192.168.1.61",
|
||||
"timestamp": 1772477899.4678419
|
||||
}
|
||||
Binary file not shown.
@@ -1,94 +1,10 @@
|
||||
{
|
||||
"user_id": 6648463335,
|
||||
"username": "chiesasiloe",
|
||||
"last_updated": 1761594892.8226538,
|
||||
"last_updated_str": "2025-10-27 20:54:52",
|
||||
"last_updated": 1771777427.5488307,
|
||||
"last_updated_str": "2026-02-22 16:23:47",
|
||||
"message_count": 21,
|
||||
"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",
|
||||
"content": "Ok, ho spento la tastiera (canale 20). 🎹❌",
|
||||
@@ -130,6 +46,90 @@
|
||||
"content": "Ok, la tastiera (canale 20) è di nuovo aperta! 🎹✅",
|
||||
"timestamp": 1761594892.8226538,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1582,4 +1582,215 @@ class TF5MixerController:
|
||||
command = f"set MIXER:Lib/Bank/Scene/Store {1 if bank.lower() == 'b' else 0} {scene_number}"
|
||||
response = self._send_command(command)
|
||||
return {"status": "success" if "OK" in response else "error",
|
||||
"message": f"Scena salvata nel banco {bank.upper()}{scene_number}.", "response": response}
|
||||
"message": f"Scena salvata nel banco {bank.upper()}{scene_number}.", "response": response}
|
||||
|
||||
# =========================================================================
|
||||
# METER READING
|
||||
# Riferimento prminfo:
|
||||
# 2000 InCh 40ch 3 tipi: 0=PreHPF, 1=PreFader, 2=PostOn
|
||||
# 2001 StInCh 4ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
|
||||
# 2002 FxRtnCh 4ch 2 tipi: 0=PreFader, 1=PostOn
|
||||
# 2100 Mix 20ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
|
||||
# 2101 Mtrx 4ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
|
||||
# 2102 St 2ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
|
||||
# 2103 Mono 1ch 3 tipi: 0=PreEQ, 1=PreFader, 2=PostOn
|
||||
#
|
||||
# Il valore raw è un intero 0–127.
|
||||
# Conversione: 0 = -inf dB, 1–127 lineare su scala ~-72..+18 dBFS.
|
||||
# Formula Yamaha TF: dBFS = (raw / 127) * 90 - 72 (approssimazione lineare)
|
||||
# Oppure si restituisce direttamente il raw per display proporzionale.
|
||||
# =========================================================================
|
||||
|
||||
# Mappa dei meter disponibili
|
||||
_METER_DEFS = {
|
||||
"InCh": {"path": "MIXER:Current/Meter/InCh", "channels": 40, "types": ["PreHPF", "PreFader", "PostOn"]},
|
||||
"StInCh": {"path": "MIXER:Current/Meter/StInCh", "channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]},
|
||||
"FxRtnCh": {"path": "MIXER:Current/Meter/FxRtnCh", "channels": 4, "types": ["PreFader", "PostOn"]},
|
||||
"Mix": {"path": "MIXER:Current/Meter/Mix", "channels": 20, "types": ["PreEQ", "PreFader", "PostOn"]},
|
||||
"Mtrx": {"path": "MIXER:Current/Meter/Mtrx", "channels": 4, "types": ["PreEQ", "PreFader", "PostOn"]},
|
||||
"St": {"path": "MIXER:Current/Meter/St", "channels": 2, "types": ["PreEQ", "PreFader", "PostOn"]},
|
||||
"Mono": {"path": "MIXER:Current/Meter/Mono", "channels": 1, "types": ["PreEQ", "PreFader", "PostOn"]},
|
||||
}
|
||||
|
||||
def _meter_raw_to_db(self, raw: int) -> float:
|
||||
"""
|
||||
Converte il valore raw del meter (0-127) in dBFS approssimato.
|
||||
Scala Yamaha TF: 127 = +18 dBFS, 76 = 0 dBFS, 1 = -72 dBFS, 0 = -inf.
|
||||
"""
|
||||
if raw <= 0:
|
||||
return float('-inf')
|
||||
# Interpolazione lineare: 0..127 → -72..+18 dBFS (range 90 dB)
|
||||
return round((raw / 127.0) * 90.0 - 72.0, 1)
|
||||
|
||||
def _parse_meter_response(self, response: str) -> Optional[int]:
|
||||
"""Estrae il valore intero raw dal response di un comando get meter."""
|
||||
try:
|
||||
first_line = response.split('\n')[0].strip()
|
||||
parts = first_line.split()
|
||||
if parts and parts[0] == "OK":
|
||||
return int(parts[-1])
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return None
|
||||
|
||||
def get_meter(self, section: str, channel: int, meter_type: int = 2) -> dict:
|
||||
"""
|
||||
Legge il livello meter di un singolo canale.
|
||||
|
||||
Args:
|
||||
section: Sezione del mixer: "InCh", "StInCh", "FxRtnCh",
|
||||
"Mix", "Mtrx", "St", "Mono"
|
||||
channel: Numero canale (1-based)
|
||||
meter_type: Indice del tipo meter (0-based, vedi _METER_DEFS per i nomi)
|
||||
|
||||
Returns:
|
||||
dict con raw (0-127), level_db (dBFS), section, channel, type_name
|
||||
"""
|
||||
if section not in self._METER_DEFS:
|
||||
return {"status": "error",
|
||||
"message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"}
|
||||
|
||||
defn = self._METER_DEFS[section]
|
||||
num_ch = defn["channels"]
|
||||
num_types = len(defn["types"])
|
||||
|
||||
if not 1 <= channel <= num_ch:
|
||||
return {"status": "error", "message": f"Canale {section} deve essere tra 1 e {num_ch}"}
|
||||
if not 0 <= meter_type < num_types:
|
||||
return {"status": "error",
|
||||
"message": f"Tipo meter deve essere tra 0 e {num_types - 1} per {section}: {defn['types']}"}
|
||||
|
||||
ch_idx = channel - 1
|
||||
command = f"get {defn['path']} {ch_idx} {meter_type}"
|
||||
response = self._send_command(command)
|
||||
raw = self._parse_meter_response(response)
|
||||
|
||||
if raw is None:
|
||||
return {"status": "error", "message": f"Risposta non valida: {response}"}
|
||||
|
||||
level_db = self._meter_raw_to_db(raw)
|
||||
type_name = defn["types"][meter_type]
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"section": section,
|
||||
"channel": channel,
|
||||
"type": meter_type,
|
||||
"type_name": type_name,
|
||||
"raw": raw,
|
||||
"level_db": level_db,
|
||||
"message": f"{section} ch{channel} [{type_name}]: {raw}/127 → {level_db} dBFS"
|
||||
}
|
||||
|
||||
def get_all_meters(self, section: str, meter_type: int = 2) -> dict:
|
||||
"""
|
||||
Legge tutti i canali meter di una sezione.
|
||||
|
||||
Args:
|
||||
section: "InCh", "StInCh", "FxRtnCh", "Mix", "Mtrx", "St", "Mono"
|
||||
meter_type: Indice tipo meter (0-based)
|
||||
|
||||
Returns:
|
||||
dict con lista di letture meter per tutti i canali della sezione
|
||||
"""
|
||||
if section not in self._METER_DEFS:
|
||||
return {"status": "error",
|
||||
"message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"}
|
||||
|
||||
defn = self._METER_DEFS[section]
|
||||
num_ch = defn["channels"]
|
||||
num_types = len(defn["types"])
|
||||
|
||||
if not 0 <= meter_type < num_types:
|
||||
return {"status": "error",
|
||||
"message": f"Tipo meter deve essere tra 0 e {num_types - 1} per {section}: {defn['types']}"}
|
||||
|
||||
type_name = defn["types"][meter_type]
|
||||
readings = []
|
||||
|
||||
for ch in range(1, num_ch + 1):
|
||||
ch_idx = ch - 1
|
||||
command = f"get {defn['path']} {ch_idx} {meter_type}"
|
||||
response = self._send_command(command)
|
||||
raw = self._parse_meter_response(response)
|
||||
if raw is not None:
|
||||
level_db = self._meter_raw_to_db(raw)
|
||||
readings.append({
|
||||
"channel": ch,
|
||||
"raw": raw,
|
||||
"level_db": level_db
|
||||
})
|
||||
else:
|
||||
readings.append({
|
||||
"channel": ch,
|
||||
"raw": None,
|
||||
"level_db": None,
|
||||
"error": response
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"section": section,
|
||||
"type": meter_type,
|
||||
"type_name": type_name,
|
||||
"total_channels": num_ch,
|
||||
"readings": readings,
|
||||
"message": f"Meter {section} [{type_name}]: {num_ch} canali letti"
|
||||
}
|
||||
|
||||
def get_meter_snapshot(self, meter_type: int = 2) -> dict:
|
||||
"""
|
||||
Legge un snapshot dei meter di tutte le sezioni principali.
|
||||
|
||||
Args:
|
||||
meter_type: Tipo meter da usare per tutte le sezioni.
|
||||
Nota: FxRtnCh ha solo 2 tipi (0-1), se si passa 2
|
||||
viene automaticamente usato il tipo 1 (PostOn).
|
||||
|
||||
Returns:
|
||||
dict con un sotto-dict per ogni sezione, contenente le letture.
|
||||
"""
|
||||
snapshot = {}
|
||||
for section, defn in self._METER_DEFS.items():
|
||||
# Clamp meter_type al massimo disponibile per la sezione
|
||||
safe_type = min(meter_type, len(defn["types"]) - 1)
|
||||
result = self.get_all_meters(section, safe_type)
|
||||
snapshot[section] = result
|
||||
|
||||
sections_ok = sum(1 for r in snapshot.values() if r["status"] == "success")
|
||||
return {
|
||||
"status": "success",
|
||||
"meter_type_requested": meter_type,
|
||||
"sections": snapshot,
|
||||
"message": f"Snapshot meter: {sections_ok}/{len(self._METER_DEFS)} sezioni lette"
|
||||
}
|
||||
|
||||
def get_meter_types(self, section: str = None) -> dict:
|
||||
"""
|
||||
Restituisce i tipi di meter disponibili per una sezione (o tutte).
|
||||
|
||||
Args:
|
||||
section: Nome sezione opzionale. Se None, restituisce tutte.
|
||||
"""
|
||||
if section:
|
||||
if section not in self._METER_DEFS:
|
||||
return {"status": "error",
|
||||
"message": f"Sezione '{section}' non valida. Valori: {list(self._METER_DEFS.keys())}"}
|
||||
defn = self._METER_DEFS[section]
|
||||
return {
|
||||
"status": "success",
|
||||
"section": section,
|
||||
"channels": defn["channels"],
|
||||
"types": {i: name for i, name in enumerate(defn["types"])}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"status": "success",
|
||||
"sections": {
|
||||
sec: {
|
||||
"channels": d["channels"],
|
||||
"types": {i: name for i, name in enumerate(d["types"])}
|
||||
}
|
||||
for sec, d in self._METER_DEFS.items()
|
||||
}
|
||||
}
|
||||
13
profiles/6648463335.json
Normal file
13
profiles/6648463335.json
Normal 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"
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
dotenv
|
||||
google-genai
|
||||
pyyaml
|
||||
pyyaml
|
||||
readline
|
||||
930
tf5_cly.py
Normal file
930
tf5_cly.py
Normal 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", "sì", "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: 0–127 (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)
|
||||
Reference in New Issue
Block a user