aggiunti meter
This commit is contained in:
+241
-184
@@ -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")
|
||||
Reference in New Issue
Block a user