aggiunti meter

This commit is contained in:
Nick
2026-04-20 20:17:46 +02:00
parent 84c1f0da35
commit c2900bbb2e
10 changed files with 377 additions and 314 deletions
+241 -184
View File
@@ -24,6 +24,7 @@ class TF5MixerController:
self._ensure_cache_dir()
self._cache_lock = threading.Lock()
self._refresh_lock = threading.Lock() # <--- AGGIUNGI QUESTA RIGA
self._cache = self._load_cache()
self._command_lock = threading.Lock()
@@ -47,6 +48,12 @@ class TF5MixerController:
self._reader_thread.daemon = True
self._reader_thread.start()
# --- AGGIUNGI QUESTE 3 RIGHE ---
self._keepalive_thread = threading.Thread(target=self._meter_keepalive_loop)
self._keepalive_thread.daemon = True
self._keepalive_thread.start()
# -------------------------------
if not self._is_connected.wait(timeout=5):
print("⚠️ Impossibile connettersi al mixer all'avvio.")
else:
@@ -63,19 +70,13 @@ class TF5MixerController:
self._is_connected.set()
print(f"🔗 Connesso a {self.host}:{self.port}")
with self._cache_lock:
is_cache_empty = not self._cache.get("channels")
if is_cache_empty:
print("Cache iniziale vuota. Avvio refresh completo...")
self.refresh_cache()
# Piccola pausa per assicurarsi che il buffer sia pulito
time.sleep(0.2)
# Sottoscrivi DOPO il refresh (cache occupa il socket, meter devono aspettare)
self.subscribe_meters(interval_ms=100)
self._socket_reader() # blocca qui finché connesso
# Caricamento stato iniziale dal mixer (una tantum)
threading.Thread(target=self.refresh_cache, daemon=True).start()
self._socket_reader()
except socket.error as e:
print(f"🔥 Errore di connessione: {e}. Riprovo tra 5 secondi...")
@@ -108,12 +109,14 @@ class TF5MixerController:
if line.startswith("NOTIFY"):
self._handle_notify(line)
elif line.startswith("OK") or line.startswith("ERROR"):
# Ignoriamo i messaggi di routine e keepalive per non intasare la coda
if "mtrstart" in line or "mtrstop" in line or "Current/Mono/Fader" in line:
continue
try:
self._response_queue.put_nowait(line)
except queue.Full:
print(f"⚠️ Coda risposte piena, scartato: {line}")
else:
print(f"🤔 Messaggio non gestito: {line}")
except socket.timeout:
continue
@@ -136,13 +139,20 @@ class TF5MixerController:
Analizza un messaggio NOTIFY e aggiorna la cache in modo robusto.
Gestisce InCh, StInCh, FxRtnCh, DCA, Mix, Mtrx, St, Mono, MuteMaster.
"""
# --- NUOVA GESTIONE METER TF5 ---
if message.startswith("NOTIFY mtr "):
self._handle_tf5_meter_notify(message)
return
# --------------------------------
print(f"RECV NOTIFY: {message}")
parts = message.split()
if len(parts) >= 2 and parts[1] == "mtrinfo":
self._handle_meter_notify(message)
return
# Rimuovi o commenta queste 3 righe vecchie:
# if len(parts) >= 2 and parts[1] == "mtrinfo":
# self._handle_meter_notify(message)
# return
if len(parts) < 2:
return
@@ -495,7 +505,6 @@ class TF5MixerController:
if self._reader_thread and self._reader_thread.is_alive():
self._reader_thread.join(timeout=2)
print("✅ Controller fermato.")
self._save_cache()
def __enter__(self):
return self
@@ -518,28 +527,11 @@ class TF5MixerController:
CACHE_DIR.mkdir(parents=True, exist_ok=True)
def _load_cache(self) -> dict:
with self._cache_lock:
if CACHE_FILE.exists():
try:
with open(CACHE_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
for key in ("channels", "mixes", "steinch", "fxrtn", "dcas", "matrices", "stereo", "mono", "mute_masters"):
if key not in data:
data[key] = {} if key != "mono" else {}
return data
except Exception as e:
print(f"⚠️ Errore nel caricamento della cache: {e}")
return {"channels": {}, "mixes": {}, "steinch": {}, "fxrtn": {}, "dcas": {},
"matrices": {}, "stereo": {}, "mono": {}, "mute_masters": {}, "timestamp": 0}
return {"channels": {}, "mixes": {}, "steinch": {}, "fxrtn": {}, "dcas": {},
"matrices": {}, "stereo": {}, "mono": {}, "mute_masters": {}, "timestamp": 0}
def _save_cache(self):
with self._cache_lock:
try:
cache_to_save = self._prepare_cache_for_json(self._cache)
with open(CACHE_FILE, 'w', encoding='utf-8') as f:
json.dump(cache_to_save, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"⚠️ Errore nel salvataggio della cache: {e}")
pass # Non salviamo nulla su disco
def _prepare_cache_for_json(self, obj):
if isinstance(obj, dict): return {k: self._prepare_cache_for_json(v) for k, v in obj.items()}
@@ -550,10 +542,7 @@ class TF5MixerController:
else: return obj
def _is_cache_valid(self) -> bool:
with self._cache_lock:
if not self._cache.get("channels"): return False
cache_age = time.time() - self._cache.get("timestamp", 0)
return cache_age < CACHE_DURATION
return self._is_connected.is_set()
def _normalize_level_from_cache(self, level_value):
if level_value is None: return float('-inf')
@@ -582,7 +571,7 @@ class TF5MixerController:
"""Converte stringa valore interno in dB float."""
try:
v = int(raw)
return v / 100.0 if v > -32768 else float('-inf')
return v / 100.0 if v > -32768 else -120.0 # <--- SOSTITUITO float('-inf') CON -120.0
except:
return None
@@ -591,115 +580,134 @@ class TF5MixerController:
# =========================================================================
def refresh_cache(self) -> dict:
print("🔄 Aggiornamento cache completo in corso...")
channels_data, mixes_data, steinch_data = {}, {}, {}
fxrtn_data, dcas_data, matrices_data = {}, {}, {}
stereo_data, mute_masters_data = {}, {}
mono_data = {}
# --- NUOVO BLOCCO LOCK ---
# Se c'è già un refresh in corso, aspetta che finisca e poi esci.
if not self._refresh_lock.acquire(blocking=False):
print("⏳ Un refresh è già in corso. Attendo che finisca...")
self._refresh_lock.acquire() # Aspetta il rilascio
self._refresh_lock.release()
return {"status": "success", "message": "Cache già aggiornata da un altro processo."}
# Input Channels
for ch in range(1, TF5_INPUT_CHANNELS + 1):
ch_idx = ch - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0")))
pan_raw = self._parse_value(self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0"))
try: pan_value = int(pan_raw)
except: pan_value = None
channels_data[str(ch)] = {"channel": ch, "name": name, "on": is_on, "level_db": level_db, "pan": pan_value}
time.sleep(0.01)
try:
print("🔄 Aggiornamento cache completo in corso...")
channels_data, mixes_data, steinch_data = {}, {}, {}
fxrtn_data, dcas_data, matrices_data = {}, {}, {}
stereo_data, mute_masters_data = {}, {}
mono_data = {}
# Stereo Input Channels
for ch in range(1, TF5_ST_INPUT_CHANNELS + 1):
ch_idx = ch - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/StInCh/Label/Name {ch_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/StInCh/Fader/On {ch_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/StInCh/Fader/Level {ch_idx} 0")))
pan_raw = self._parse_value(self._send_command(f"get MIXER:Current/StInCh/ToSt/Pan {ch_idx} 0"))
try: pan_value = int(pan_raw)
except: pan_value = None
steinch_data[str(ch)] = {"channel": ch, "name": name, "on": is_on, "level_db": level_db, "pan": pan_value}
time.sleep(0.01)
# Input Channels
for ch in range(1, TF5_INPUT_CHANNELS + 1):
ch_idx = ch - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/InCh/Fader/On {ch_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/InCh/Fader/Level {ch_idx} 0")))
pan_raw = self._parse_value(self._send_command(f"get MIXER:Current/InCh/ToSt/Pan {ch_idx} 0"))
try: pan_value = int(pan_raw)
except: pan_value = None
# --- NUOVO ---
ha_gain_raw = self._parse_value(self._send_command(f"get MIXER:Current/InCh/Ha/Gain {ch_idx} 0"))
try: ha_gain = int(ha_gain_raw)
except: ha_gain = None
# -------------
channels_data[str(ch)] = {
"channel": ch, "name": name, "on": is_on,
"level_db": level_db, "pan": pan_value,
"ha_gain": ha_gain # <-- nuovo campo
}
# FX Return Channels
for ch in range(1, TF5_FX_RETURN_CHANNELS + 1):
ch_idx = ch - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/FxRtnCh/Label/Name {ch_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/FxRtnCh/Fader/On {ch_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/FxRtnCh/Fader/Level {ch_idx} 0")))
fxrtn_data[str(ch)] = {"channel": ch, "name": name, "on": is_on, "level_db": level_db}
time.sleep(0.01)
# Stereo Input Channels
for ch in range(1, TF5_ST_INPUT_CHANNELS + 1):
ch_idx = ch - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/StInCh/Label/Name {ch_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/StInCh/Fader/On {ch_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/StInCh/Fader/Level {ch_idx} 0")))
pan_raw = self._parse_value(self._send_command(f"get MIXER:Current/StInCh/ToSt/Pan {ch_idx} 0"))
try: pan_value = int(pan_raw)
except: pan_value = None
steinch_data[str(ch)] = {"channel": ch, "name": name, "on": is_on, "level_db": level_db, "pan": pan_value}
# time.sleep(0.01)
# DCA Groups
for dca in range(1, TF5_DCA_GROUPS + 1):
dca_idx = dca - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/DCA/Label/Name {dca_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/DCA/Fader/On {dca_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/DCA/Fader/Level {dca_idx} 0")))
dcas_data[str(dca)] = {"dca": dca, "name": name, "on": is_on, "level_db": level_db}
time.sleep(0.01)
# FX Return Channels
for ch in range(1, TF5_FX_RETURN_CHANNELS + 1):
ch_idx = ch - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/FxRtnCh/Label/Name {ch_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/FxRtnCh/Fader/On {ch_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/FxRtnCh/Fader/Level {ch_idx} 0")))
fxrtn_data[str(ch)] = {"channel": ch, "name": name, "on": is_on, "level_db": level_db}
# time.sleep(0.01)
# Mix Buses
for mix in range(1, TF5_MIX_BUSSES + 1):
mix_idx = mix - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0")))
mixes_data[str(mix)] = {"mix": mix, "name": name, "on": is_on, "level_db": level_db}
time.sleep(0.01)
# DCA Groups
for dca in range(1, TF5_DCA_GROUPS + 1):
dca_idx = dca - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/DCA/Label/Name {dca_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/DCA/Fader/On {dca_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/DCA/Fader/Level {dca_idx} 0")))
dcas_data[str(dca)] = {"dca": dca, "name": name, "on": is_on, "level_db": level_db}
# time.sleep(0.01)
# Matrix Buses
for mtrx in range(1, TF5_MATRIX_BUSSES + 1):
mtrx_idx = mtrx - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/Mtrx/Label/Name {mtrx_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/Mtrx/Fader/On {mtrx_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/Mtrx/Fader/Level {mtrx_idx} 0")))
matrices_data[str(mtrx)] = {"matrix": mtrx, "name": name, "on": is_on, "level_db": level_db}
time.sleep(0.01)
# Mix Buses
for mix in range(1, TF5_MIX_BUSSES + 1):
mix_idx = mix - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/Mix/Label/Name {mix_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/Mix/Fader/On {mix_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/Mix/Fader/Level {mix_idx} 0")))
mixes_data[str(mix)] = {"mix": mix, "name": name, "on": is_on, "level_db": level_db}
# time.sleep(0.01)
# Stereo Bus (max 2: L/R)
for st in range(1, TF5_STEREO_BUSSES + 1):
st_idx = st - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/St/Label/Name {st_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/St/Fader/On {st_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/St/Fader/Level {st_idx} 0")))
stereo_data[str(st)] = {"bus": st, "name": name, "on": is_on, "level_db": level_db}
time.sleep(0.01)
# Matrix Buses
for mtrx in range(1, TF5_MATRIX_BUSSES + 1):
mtrx_idx = mtrx - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/Mtrx/Label/Name {mtrx_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/Mtrx/Fader/On {mtrx_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/Mtrx/Fader/Level {mtrx_idx} 0")))
matrices_data[str(mtrx)] = {"matrix": mtrx, "name": name, "on": is_on, "level_db": level_db}
# time.sleep(0.01)
# Mono Bus
name = self._parse_name(self._send_command("get MIXER:Current/Mono/Label/Name 0 0"))
is_on = self._parse_value(self._send_command("get MIXER:Current/Mono/Fader/On 0 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command("get MIXER:Current/Mono/Fader/Level 0 0")))
mono_data = {"name": name, "on": is_on, "level_db": level_db}
# Stereo Bus (max 2: L/R)
for st in range(1, TF5_STEREO_BUSSES + 1):
st_idx = st - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/St/Label/Name {st_idx} 0"))
is_on = self._parse_value(self._send_command(f"get MIXER:Current/St/Fader/On {st_idx} 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command(f"get MIXER:Current/St/Fader/Level {st_idx} 0")))
stereo_data[str(st)] = {"bus": st, "name": name, "on": is_on, "level_db": level_db}
# time.sleep(0.01)
# Mute Masters
for group in range(1, TF5_MUTE_GROUPS + 1):
group_idx = group - 1
is_on = self._parse_value(self._send_command(f"get MIXER:Current/MuteMaster/On {group_idx} 0")) == "1"
name_raw = self._parse_name(self._send_command(f"get MIXER:Current/MuteMaster/Label/Name {group_idx} 0"))
mute_masters_data[str(group)] = {"group": group, "name": name_raw, "on": is_on}
time.sleep(0.01)
# Mono Bus
name = self._parse_name(self._send_command("get MIXER:Current/Mono/Label/Name 0 0"))
is_on = self._parse_value(self._send_command("get MIXER:Current/Mono/Fader/On 0 0")) == "1"
level_db = self._internal_to_level(self._parse_value(self._send_command("get MIXER:Current/Mono/Fader/Level 0 0")))
mono_data = {"name": name, "on": is_on, "level_db": level_db}
with self._cache_lock:
self._cache = {
"channels": channels_data,
"mixes": mixes_data,
"steinch": steinch_data,
"fxrtn": fxrtn_data,
"dcas": dcas_data,
"matrices": matrices_data,
"stereo": stereo_data,
"mono": mono_data,
"mute_masters": mute_masters_data,
"timestamp": time.time()
}
self._save_cache()
# Mute Masters
for group in range(1, TF5_MUTE_GROUPS + 1):
group_idx = group - 1
is_on = self._parse_value(self._send_command(f"get MIXER:Current/MuteMaster/On {group_idx} 0")) == "1"
name_raw = self._parse_name(self._send_command(f"get MIXER:Current/MuteMaster/Label/Name {group_idx} 0"))
mute_masters_data[str(group)] = {"group": group, "name": name_raw, "on": is_on}
# time.sleep(0.01)
msg = (f"Cache aggiornata: {len(channels_data)} InCh, {len(steinch_data)} StInCh, "
f"{len(fxrtn_data)} FxRtn, {len(dcas_data)} DCA, {len(mixes_data)} Mix, "
f"{len(matrices_data)} Mtrx, {len(stereo_data)} St, Mono, {len(mute_masters_data)} MuteMaster")
print(f"{msg}")
return {"status": "success", "message": msg}
with self._cache_lock:
self._cache = {
"channels": channels_data,
"mixes": mixes_data,
"steinch": steinch_data,
"fxrtn": fxrtn_data,
"dcas": dcas_data,
"matrices": matrices_data,
"stereo": stereo_data,
"mono": mono_data,
"mute_masters": mute_masters_data,
"timestamp": time.time()
}
self._save_cache()
msg = (f"Cache aggiornata: {len(channels_data)} InCh, {len(steinch_data)} StInCh, "
f"{len(fxrtn_data)} FxRtn, {len(dcas_data)} DCA, {len(mixes_data)} Mix, "
f"{len(matrices_data)} Mtrx, {len(stereo_data)} St, Mono, {len(mute_masters_data)} MuteMaster")
print(f"{msg}")
return {"status": "success", "message": msg}
finally:
self._refresh_lock.release()
# =========================================================================
# INPUT CHANNELS (InCh)
@@ -840,7 +848,8 @@ class TF5MixerController:
"channel": cached_data["channel"], "name": cached_data["name"],
"on": cached_data["on"],
"level_db": self._sanitize_value(self._normalize_level_from_cache(cached_data.get("level_db"))),
"pan": cached_data.get("pan")}
"pan": cached_data.get("pan"),
"ha_gain": cached_data.get("ha_gain")}
ch_idx = channel - 1
name = self._parse_name(self._send_command(f"get MIXER:Current/InCh/Label/Name {ch_idx} 0"))
@@ -854,9 +863,13 @@ class TF5MixerController:
self._cache.setdefault("channels", {})[str(channel)] = {
"channel": channel, "name": name, "on": is_on, "level_db": level_db, "pan": pan_value}
self._cache['timestamp'] = time.time()
ha_gain_raw = self._parse_value(self._send_command(f"get MIXER:Current/InCh/Ha/Gain {ch_idx} 0"))
try: ha_gain = int(ha_gain_raw)
except: ha_gain = None
return {"status": "success", "source": "mixer", "channel": channel, "name": name, "on": is_on,
"level_db": self._sanitize_value(level_db), "pan": pan_value}
"level_db": self._sanitize_value(level_db), "pan": pan_value, "ha_gain": ha_gain}
def get_channel_to_mix_info(self, channel: int, mix_number: int) -> dict:
if not 1 <= channel <= TF5_INPUT_CHANNELS:
@@ -872,7 +885,7 @@ class TF5MixerController:
"message": f"Canale {channel} → Mix {mix_number}: {sanitized:+.1f} dB ({'ON' if is_on else 'OFF'})"}
def get_all_channels_summary(self) -> dict:
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
with self._cache_lock:
items_copy = list(self._cache.get("channels", {}).values())
cache_age = time.time() - self._cache.get("timestamp", 0)
@@ -884,7 +897,8 @@ class TF5MixerController:
"channel": ch_num,
"name": info.get("name") or f"CH {ch_num}",
"on": info.get("on", False),
"level_db": self._sanitize_value(self._normalize_level_from_cache(info.get("level_db")))
"level_db": self._sanitize_value(self._normalize_level_from_cache(info.get("level_db"))),
"ha_gain": info.get("ha_gain")
})
results.sort(key=lambda x: x["channel"])
return {"status": "success", "total_channels": len(results), "channels": results, "cache_age_seconds": int(cache_age)}
@@ -902,7 +916,7 @@ class TF5MixerController:
"message": f"Riattivati {success_count}/{len(channels)} canali: {channels}", "details": results}
def search_channels_by_name(self, search_term: str) -> dict:
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
search_lower = search_term.lower()
with self._cache_lock:
channels_copy = list(self._cache.get("channels", {}).values())
@@ -1053,7 +1067,7 @@ class TF5MixerController:
"message": f"StInCh {channel} → Mono send {'acceso' if state else 'spento'}", "response": response}
def get_all_steinch_summary(self) -> dict:
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
with self._cache_lock:
items_copy = list(self._cache.get("steinch", {}).values())
cache_age = time.time() - self._cache.get("timestamp", 0)
@@ -1183,7 +1197,7 @@ class TF5MixerController:
"message": f"FxRtnCh {channel} → St pan impostato a {pan_value}", "response": response}
def get_all_fxrtn_summary(self) -> dict:
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
with self._cache_lock:
items_copy = list(self._cache.get("fxrtn", {}).values())
cache_age = time.time() - self._cache.get("timestamp", 0)
@@ -1250,7 +1264,7 @@ class TF5MixerController:
def get_all_dca_summary(self) -> dict:
"""Restituisce il riepilogo di tutti i gruppi DCA dalla cache."""
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
with self._cache_lock:
dcas_copy = list(self._cache.get("dcas", {}).values())
cache_age = time.time() - self._cache.get("timestamp", 0)
@@ -1354,7 +1368,7 @@ class TF5MixerController:
"message": f"Mix {mix_number} → Matrix {matrix_number} send {'acceso' if state else 'spento'}", "response": response}
def get_all_mixes_summary(self) -> dict:
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
with self._cache_lock:
items_copy = list(self._cache.get("mixes", {}).values())
cache_age = time.time() - self._cache.get("timestamp", 0)
@@ -1372,7 +1386,7 @@ class TF5MixerController:
return {"status": "success", "total_mixes": len(results), "mixes": results, "cache_age_seconds": int(cache_age)}
def search_mixes_by_name(self, search_term: str) -> dict:
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
search_lower = search_term.lower()
with self._cache_lock:
mixes_copy = list(self._cache.get("mixes", {}).values())
@@ -1387,7 +1401,7 @@ class TF5MixerController:
def get_full_mix_details(self, mix_number: int) -> dict:
if not 1 <= mix_number <= TF5_MIX_BUSSES:
return {"status": "error", "message": f"Il mix deve essere tra 1 e {TF5_MIX_BUSSES}"}
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
mix_info = self.get_mix_info(mix_number)
if mix_info["status"] != "success": return mix_info
sends = []
@@ -1455,7 +1469,7 @@ class TF5MixerController:
def get_all_mtrx_summary(self) -> dict:
"""Restituisce il riepilogo di tutti i bus Matrix dalla cache."""
if not self._is_cache_valid(): self.refresh_cache()
# if not self._is_cache_valid(): self.refresh_cache()
with self._cache_lock:
matrices_copy = list(self._cache.get("matrices", {}).values())
cache_age = time.time() - self._cache.get("timestamp", 0)
@@ -1697,61 +1711,88 @@ class TF5MixerController:
"message": f"Scena salvata nel banco {bank.upper()}{scene_number}.", "response": response}
# =========================================================================
# METER READING (via prminfo subscription)
# METER READING (via mtrstart subscription protocollo TF)
# =========================================================================
# Mappiamo le stringhe attese dal frontend con i percorsi SCP del TF5
_METER_SUBSCRIPTIONS = {
2000: {"key": "InCh", "channels": 40, "types": 3},
2001: {"key": "StInCh", "channels": 4, "types": 3},
2002: {"key": "FxRtnCh", "channels": 4, "types": 2},
2100: {"key": "Mix", "channels": 20, "types": 3},
2101: {"key": "Mtrx", "channels": 4, "types": 3},
2102: {"key": "St", "channels": 2, "types": 3},
2103: {"key": "Mono", "channels": 1, "types": 3},
"channel": "MIXER:Current/InCh/PostOn",
"steinch": "MIXER:Current/StInCh/PostOn",
"fxrtn": "MIXER:Current/FxRtnCh/PostOn",
"mix": "MIXER:Current/Mix/PostOn",
"mtrx": "MIXER:Current/Mtrx/PostOn",
"stereo": "MIXER:Current/St/PostOn",
}
# Dizionario meter: { "InCh": [[ch1t0, ch1t1, ch1t2], [ch2t0,...], ...], ... }
# Oppure più semplice: { "InCh": {type_idx: [val_ch1, val_ch2, ...]}, ... }
_meter_data: dict = {}
_meter_lock: threading.Lock = None # verrà inizializzato in __init__
_meter_lock: threading.Lock = None
def _init_meter_state(self):
"""Inizializza la struttura dati per i meter."""
self._meter_lock = threading.Lock()
self._meter_data = {}
for info in self._METER_SUBSCRIPTIONS.values():
key = info["key"]
self._meter_data[key] = {
t: [0] * info["channels"] for t in range(info["types"])
}
self._meter_data = {key: [] for key in self._METER_SUBSCRIPTIONS.keys()}
def subscribe_meters(self, interval_ms: int = 100):
# Il comando corretto per i meter Yamaha TF è "mtrinfo"
for meter_id in self._METER_SUBSCRIPTIONS:
self._send_raw(f"mtrinfo {meter_id} {interval_ms}")
"""Invia il comando mtrstart per attivare l'invio dei dati dei meter."""
for path in self._METER_SUBSCRIPTIONS.values():
self._send_raw(f"mtrstart {path} {interval_ms}")
time.sleep(0.05)
def unsubscribe_meters(self):
for meter_id in self._METER_SUBSCRIPTIONS:
self._send_raw(f"mtrinfo {meter_id} 0")
"""Ferma l'invio dei dati dei meter."""
for path in self._METER_SUBSCRIPTIONS.values():
self._send_raw(f"mtrstop {path}")
def _handle_tf5_meter_notify(self, message: str):
"""
Gestisce: NOTIFY mtr MIXER:Current/Mix/PostOn level 00 00 00 00...
"""
try:
# Dividiamo la parte intestazione dalla parte dati (i valori hex)
parts = message.split(" level ")
if len(parts) != 2:
return
header_parts = parts[0].split()
path = header_parts[2] # Es: MIXER:Current/Mix/PostOn
hex_values_str = parts[1].strip()
# Troviamo a quale chiave frontend corrisponde questo path
target_key = None
for key, sub_path in self._METER_SUBSCRIPTIONS.items():
if sub_path == path:
target_key = key
break
if not target_key:
return
# Convertiamo l'array di stringhe hex ('00', '4A', ecc) in interi (0-127)
int_values = [int(v, 16) for v in hex_values_str.split()]
with self._meter_lock:
self._meter_data[target_key] = int_values
except Exception as e:
# Ignora errori di parsing su pacchetti malformati per non crashare
pass
def get_meter_snapshot(self) -> dict:
"""Restituisce l'ultimo snapshot meter ricevuto."""
"""Restituisce l'ultimo snapshot meter formattato per il frontend Vue."""
with self._meter_lock:
# Costruisce il formato atteso dal frontend
snapshot = {}
for meter_id, info in self._METER_SUBSCRIPTIONS.items():
key = info["key"]
frontend_key = key.lower()
# Il frontend si aspetta { readings: [{channel, raw, level_db}] }
type_idx = info["types"] - 1 # Usiamo l'ultimo tipo (PostOn)
for frontend_key in self._METER_SUBSCRIPTIONS.keys():
raw_values = self._meter_data.get(frontend_key, [])
readings = []
for ch_idx, raw in enumerate(self._meter_data[key][type_idx]):
# Creiamo l'array [{channel: 1, raw: 45}, {channel: 2, raw: 0}...]
for ch_idx, raw in enumerate(raw_values):
readings.append({
"channel": ch_idx + 1,
"raw": raw,
"level_db": self._meter_raw_to_db(raw)
})
snapshot[frontend_key] = {
"readings": readings,
"type_name": "PostOn"
@@ -1759,6 +1800,22 @@ class TF5MixerController:
return snapshot
def _meter_raw_to_db(self, raw: int) -> float:
"""Converte il valore raw (0-127) in dB (approssimativo)"""
if raw <= 0:
return float('-inf')
return round((raw / 127.0) * 90.0 - 72.0, 1)
return -120.0 # <--- SOSTITUITO float('-inf') CON -120.0
return round((raw / 127.0) * 90.0 - 72.0, 1)
def _meter_keepalive_loop(self):
"""
Invia periodicamente il comando di iscrizione ai meter per evitare
che il mixer vada in timeout e smetta di trasmetterli.
"""
while self._is_running.is_set():
# Aspetta 10 secondi
time.sleep(10)
# Se siamo connessi, rinnova la richiesta
if self._is_connected.is_set():
self.subscribe_meters(interval_ms=100)
# Inviamo anche un comando vuoto per tenere vivo il socket in generale
self._send_raw("get MIXER:Current/Mono/Fader/Level 0 0")