Fix MQTT, memory persitence e UI

This commit is contained in:
2026-04-22 22:02:25 +02:00
parent 1780a4a737
commit df8ac4ab31
4 changed files with 1312 additions and 85 deletions
+96 -73
View File
@@ -15,7 +15,7 @@ import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
# ========================================== # ==========================================
# 0. CONFIGURAZIONE LOGGING & HARDWARE # 0. LOGGING & HARDWARE CONFIGURATION
# ========================================== # ==========================================
logging.basicConfig( logging.basicConfig(
handlers=[ handlers=[
@@ -33,48 +33,48 @@ try:
GPIO_AVAILABLE = True GPIO_AVAILABLE = True
except ImportError: except ImportError:
GPIO_AVAILABLE = False GPIO_AVAILABLE = False
logger.warning("Libreria RPi.GPIO non trovata. Reset hardware disabilitato.") logger.warning("RPi.GPIO library not found. Hardware reset disabled.")
# ========================================== # ==========================================
# 1. CARICAMENTO CONFIGURAZIONE UNIFICATA # 1. UNIFIED CONFIGURATION LOADING
# ========================================== # ==========================================
CONFIG_PATH = Path("/opt/node_config.json") CONFIG_PATH = Path("/opt/node_config.json")
def load_config(): def load_config():
try: try:
if not CONFIG_PATH.exists(): if not CONFIG_PATH.exists():
logger.error(f"ERRORE: File {CONFIG_PATH} non trovato!") logger.error(f"ERROR: File {CONFIG_PATH} not found!")
sys.exit(1) sys.exit(1)
with open(CONFIG_PATH, 'r') as f: with open(CONFIG_PATH, 'r') as f:
return json.load(f) return json.load(f)
except Exception as e: except Exception as e:
logger.error(f"ERRORE CRITICO JSON: {e}") logger.error(f"CRITICAL JSON ERROR: {e}")
sys.exit(1) sys.exit(1)
cfg = load_config() cfg = load_config()
# Identificativi e Topic # Identifiers and Topics
CLIENT_ID = cfg.get('client_id', 'iv3jdv').lower() CLIENT_ID = cfg.get('client_id', 'iv3jdv').lower()
BASE_TOPIC = cfg.get('mqtt', {}).get('base_topic', f"servizi/{CLIENT_ID}") BASE_TOPIC = cfg.get('mqtt', {}).get('base_topic', f"servizi/{CLIENT_ID}")
TOPIC_CMD = f"{BASE_TOPIC}/cmnd" TOPIC_CMD = f"{BASE_TOPIC}/cmnd"
TOPIC_STAT = f"{BASE_TOPIC}/stat" TOPIC_STAT = f"{BASE_TOPIC}/stat"
# Variabili di Stato Globali # Global Status Variables
boot_recovered = False boot_recovered = False
current_status = "ONLINE - Pronto" current_status = "ONLINE"
auto_healing_counter = {} auto_healing_counter = {}
# ========================================== # ==========================================
# 2. FUNZIONE NOTIFICA TELEGRAM # 2. TELEGRAM NOTIFICATION FUNCTION
# ========================================== # ==========================================
def send_telegram_message(message): def send_telegram_message(message):
t_cfg = cfg.get('telegram', {}) t_cfg = cfg.get('telegram', {})
if not t_cfg.get('enabled', False): return if not t_cfg.get('enabled', False): return
ora_attuale = int(time.strftime("%H")) current_hour = int(time.strftime("%H"))
if ora_attuale >= 23 or ora_attuale < 7: if current_hour >= 23 or current_hour < 7:
logger.info(f"🌙 Notte fonda ({ora_attuale}:00): Notifica Telegram evitata.") logger.info(f"🌙 Late night ({current_hour}:00): Telegram notification skipped.")
return return
token = t_cfg.get('token') token = t_cfg.get('token')
@@ -87,25 +87,35 @@ def send_telegram_message(message):
payload = {"chat_id": chat_id, "text": f"[{CLIENT_ID.upper()}]\n{clean_msg}"} payload = {"chat_id": chat_id, "text": f"[{CLIENT_ID.upper()}]\n{clean_msg}"}
requests.post(url, json=payload, timeout=10) requests.post(url, json=payload, timeout=10)
except Exception as e: except Exception as e:
logger.error(f"Errore invio Telegram: {e}") logger.error(f"Telegram sending error: {e}")
# ========================================== # ==========================================
# 3. LOGICA CAMBIO PROFILO MULTIPLO # 3. MULTIPLE PROFILE SWITCH LOGIC
# ========================================== # ==========================================
def get_actual_config_from_disk(): def get_actual_config_from_disk():
return "ONLINE - Da memoria" try:
path = "/opt/last_profile.txt"
if os.path.exists(path):
with open(path, "r") as f:
label = f.read().strip()
if label:
return f"ONLINE - {label}"
except Exception as e:
logger.error(f"Errore lettura memoria profilo: {e}")
return "ONLINE" # Default se il file non esiste o è vuoto
def switch_config(config_type): def switch_config(config_type):
profile = cfg.get('profiles', {}).get(config_type) profile = cfg.get('profiles', {}).get(config_type)
if not profile: if not profile:
return f"ERRORE: Profilo {config_type} non trovato in JSON" return f"ERROR: Profile {config_type} not found in JSON"
label = profile.get('label', f"Profilo {config_type}") label = profile.get('label', f"Profile {config_type}")
services = profile.get('services', []) services = profile.get('services', [])
if not services: if not services:
return f"ERRORE: Nessun servizio configurato per {config_type}" return f"ERROR: No services configured for {config_type}"
try: try:
for s in services: for s in services:
@@ -113,28 +123,32 @@ def switch_config(config_type):
for s in services: for s in services:
if not os.path.exists(s['source']): if not os.path.exists(s['source']):
return f"ERRORE: Manca il file sorgente {s['source']}" return f"ERROR: Missing source file {s['source']}"
shutil.copy(s['source'], s['target']) shutil.copy(s['source'], s['target'])
for s in services: for s in services:
subprocess.run(["sudo", "systemctl", "start", s['name']], check=False) subprocess.run(["sudo", "systemctl", "start", s['name']], check=False)
send_telegram_message(f"✅ Switch multiplo completato: {label}") # Save the current profile to disk to remember it on reboot
with open("/opt/last_profile.txt", "w") as f:
f.write(label)
send_telegram_message(f"✅ Multiple switch completed: {label}")
return f"ONLINE - {label}" return f"ONLINE - {label}"
except Exception as e: except Exception as e:
return f"ERRORE: {str(e)}" return f"ERROR: {str(e)}"
def force_online_if_needed(client): def force_online_if_needed(client):
global boot_recovered, current_status global boot_recovered, current_status
if not boot_recovered: if not boot_recovered:
logger.info("⚠️ Recupero memoria saltato. Imposto stato da disco...") logger.info("⚠️ Memory recovery skipped. Setting status from disk...")
current_status = get_actual_config_from_disk() current_status = get_actual_config_from_disk()
client.publish(TOPIC_STAT, current_status, retain=True) client.publish(TOPIC_STAT, current_status, retain=True)
boot_recovered = True boot_recovered = True
# ========================================== # ==========================================
# 4. TELEMETRIA E AUTO-HEALING # 4. TELEMETRY AND AUTO-HEALING
# ========================================== # ==========================================
def get_cpu_temperature(): def get_cpu_temperature():
temp = 0.0 temp = 0.0
@@ -157,8 +171,8 @@ def get_system_status():
"processes": {}, "processes": {},
"timestamp": time.strftime("%H:%M:%S"), "timestamp": time.strftime("%H:%M:%S"),
"profiles": { "profiles": {
"A": cfg.get('profiles', {}).get('A', {}).get('label', 'PROFILO A'), "A": cfg.get('profiles', {}).get('A', {}).get('label', 'PROFILE A'),
"B": cfg.get('profiles', {}).get('B', {}).get('label', 'PROFILO B') "B": cfg.get('profiles', {}).get('B', {}).get('label', 'PROFILE B')
} }
} }
proc_path = Path(cfg['paths'].get('process_list', '')) proc_path = Path(cfg['paths'].get('process_list', ''))
@@ -169,7 +183,7 @@ def get_system_status():
for name in target_processes: for name in target_processes:
name = name.strip().lower() name = name.strip().lower()
if name: status["processes"][name] = "online" if name in running_names else "offline" if name: status["processes"][name] = "online" if name in running_names else "offline"
except Exception as e: logger.error(f"Errore controllo processi: {e}") except Exception as e: logger.error(f"Process check error: {e}")
return status return status
def check_auto_healing(client, status): def check_auto_healing(client, status):
@@ -179,33 +193,33 @@ def check_auto_healing(client, status):
attempts = auto_healing_counter.get(proc_name, 0) attempts = auto_healing_counter.get(proc_name, 0)
if attempts < 3: if attempts < 3:
auto_healing_counter[proc_name] = attempts + 1 auto_healing_counter[proc_name] = attempts + 1
msg = f"🛠 Auto-healing: {proc_name} offline. Riavvio {attempts+1}/3..." msg = f"🛠 Auto-healing: {proc_name} offline. Restarting {attempts+1}/3..."
client.publish(f"devices/{CLIENT_ID}/logs", msg) client.publish(f"devices/{CLIENT_ID}/logs", msg)
send_telegram_message(msg) send_telegram_message(msg)
# --- INIZIO MODIFICA: RESET HARDWARE SPECIFICO PER MMDVMHOST --- # --- START MODIFICATION: SPECIFIC HARDWARE RESET FOR MMDVMHOST ---
if proc_name.lower() == "mmdvmhost" and GPIO_AVAILABLE: if proc_name.lower() == "mmdvmhost" and GPIO_AVAILABLE:
logger.info("Esecuzione RESET HAT automatico pre-riavvio MMDVMHost...") logger.info("Executing automatic HAT RESET before restarting MMDVMHost...")
try: try:
RESET_PIN = 21 # Assicurati che il PIN sia quello corretto per i tuoi nodi RESET_PIN = 21 # Ensure the PIN is correct for your nodes
GPIO.setwarnings(False) GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BCM)
GPIO.setup(RESET_PIN, GPIO.OUT) GPIO.setup(RESET_PIN, GPIO.OUT)
# Impulso LOW per resettare # LOW pulse to reset
GPIO.output(RESET_PIN, GPIO.LOW) GPIO.output(RESET_PIN, GPIO.LOW)
time.sleep(0.5) time.sleep(0.5)
GPIO.output(RESET_PIN, GPIO.HIGH) GPIO.output(RESET_PIN, GPIO.HIGH)
GPIO.cleanup(RESET_PIN) GPIO.cleanup(RESET_PIN)
# Diamo tempo al microcontrollore di riavviarsi # Give the microcontroller time to restart
time.sleep(1.5) time.sleep(1.5)
client.publish(f"devices/{CLIENT_ID}/logs", "🔌 Impulso GPIO (Reset MMDVM) inviato!") client.publish(f"devices/{CLIENT_ID}/logs", "🔌 GPIO Pulse (MMDVM Reset) sent!")
except Exception as e: except Exception as e:
logger.error(f"Errore GPIO in auto-healing: {e}") logger.error(f"GPIO error in auto-healing: {e}")
# --- FINE MODIFICA --- # --- END MODIFICATION ---
subprocess.run(["sudo", "systemctl", "restart", proc_name]) subprocess.run(["sudo", "systemctl", "restart", proc_name])
elif attempts == 3: elif attempts == 3:
msg = f"🚨 CRITICO: {proc_name} fallito!" msg = f"🚨 CRITICAL: {proc_name} failed!"
client.publish(f"devices/{CLIENT_ID}/logs", msg) client.publish(f"devices/{CLIENT_ID}/logs", msg)
send_telegram_message(msg) send_telegram_message(msg)
auto_healing_counter[proc_name] = 4 auto_healing_counter[proc_name] = 4
@@ -221,9 +235,9 @@ def publish_all(client):
if file_list_path.exists(): if file_list_path.exists():
try: try:
files = file_list_path.read_text(encoding="utf-8").splitlines() files = file_list_path.read_text(encoding="utf-8").splitlines()
nomi_estrattti = [Path(f.strip()).stem for f in files if f.strip()] extracted_names = [Path(f.strip()).stem for f in files if f.strip()]
status["config_files"] = nomi_estrattti status["config_files"] = extracted_names
status["files"] = nomi_estrattti status["files"] = extracted_names
except: pass except: pass
client.publish(f"devices/{CLIENT_ID}/services", json.dumps(status), qos=1) client.publish(f"devices/{CLIENT_ID}/services", json.dumps(status), qos=1)
@@ -246,7 +260,7 @@ def publish_all_ini_files(client):
with open(file_list_path, 'r') as f: with open(file_list_path, 'r') as f:
files_to_parse = [line.strip() for line in f if line.strip()] files_to_parse = [line.strip() for line in f if line.strip()]
except Exception as e: except Exception as e:
logger.error(f"Errore lettura {file_list_path}: {e}") logger.error(f"Error reading {file_list_path}: {e}")
return return
for file_path in files_to_parse: for file_path in files_to_parse:
@@ -275,7 +289,7 @@ def publish_all_ini_files(client):
topic = f"data/{CLIENT_ID}/{base_name}/{section}" topic = f"data/{CLIENT_ID}/{base_name}/{section}"
client.publish(topic, json.dumps(payload), retain=True) client.publish(topic, json.dumps(payload), retain=True)
except Exception as e: except Exception as e:
logger.error(f"Errore parsing INI per {file_path}: {e}") logger.error(f"Error parsing INI for {file_path}: {e}")
def write_config_from_json(slug, json_payload): def write_config_from_json(slug, json_payload):
file_list_path = Path(cfg['paths'].get('file_list', '')) file_list_path = Path(cfg['paths'].get('file_list', ''))
@@ -285,21 +299,21 @@ def write_config_from_json(slug, json_payload):
for f in files: for f in files:
p = Path(f.strip()) p = Path(f.strip())
if p.stem.lower() == slug.lower(): if p.stem.lower() == slug.lower():
nuovi_dati = json.loads(json_payload) new_data = json.loads(json_payload)
shutil.copy(p, str(p) + ".bak") shutil.copy(p, str(p) + ".bak")
with open(p, 'w', encoding="utf-8") as file: file.write(nuovi_dati.get("raw_text", "")) with open(p, 'w', encoding="utf-8") as file: file.write(new_data.get("raw_text", ""))
os.system(f"sudo systemctl restart {slug}") os.system(f"sudo systemctl restart {slug}")
send_telegram_message(f"📝 Config {slug.upper()} aggiornata via Web.") send_telegram_message(f"📝 Config {slug.upper()} updated via Web.")
logger.info(f"Configurazione {slug} aggiornata con successo.") logger.info(f"Configuration {slug} updated successfully.")
break break
except Exception as e: logger.error(f"Errore scrittura config: {e}") except Exception as e: logger.error(f"Config writing error: {e}")
# ========================================== # ==========================================
# 5. CALLBACK MQTT # 5. MQTT CALLBACKS
# ========================================== # ==========================================
def on_connect(client, userdata, flags, reason_code, properties=None): def on_connect(client, userdata, flags, reason_code, properties=None):
if reason_code == 0: if reason_code == 0:
logger.info(f"✅ Connesso al broker MQTT: {CLIENT_ID.upper()}") logger.info(f"✅ Connected to MQTT broker: {CLIENT_ID.upper()}")
client.subscribe([(TOPIC_CMD, 0), (TOPIC_STAT, 0)]) client.subscribe([(TOPIC_CMD, 0), (TOPIC_STAT, 0)])
client.subscribe([ client.subscribe([
("devices/control/request", 0), ("devices/control/request", 0),
@@ -310,12 +324,11 @@ def on_connect(client, userdata, flags, reason_code, properties=None):
publish_all(client) publish_all(client)
publish_all_ini_files(client) publish_all_ini_files(client)
else: else:
logger.error(f"Errore connessione MQTT. Codice: {reason_code}") logger.error(f"MQTT connection error. Code: {reason_code}")
def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None): def on_disconnect(client, userdata, disconnect_flags, reason_code, properties=None):
logger.warning(f"⚠️ Disconnessione dal broker MQTT! Codice: {reason_code}") logger.warning(f"⚠️ Disconnected from MQTT broker! Code: {reason_code}")
logger.error("Forzo il riavvio del processo per ripristinare la rete in modo pulito...") logger.info("Waiting for network return. Paho-MQTT will attempt automatic reconnection...")
os._exit(1) # Uccide lo script immediatamente (Systemd lo farà risorgere)
def on_message(client, userdata, msg): def on_message(client, userdata, msg):
global boot_recovered, current_status, cfg global boot_recovered, current_status, cfg
@@ -323,7 +336,7 @@ def on_message(client, userdata, msg):
topic = msg.topic topic = msg.topic
if topic == TOPIC_STAT and not boot_recovered: if topic == TOPIC_STAT and not boot_recovered:
if not any(x in payload.upper() for x in ["OFFLINE", "ERRORE", "RIAVVIO"]): if not any(x in payload.upper() for x in ["OFFLINE", "ERROR", "REBOOT"]):
current_status = payload current_status = payload
boot_recovered = True boot_recovered = True
client.publish(TOPIC_STAT, current_status, retain=True) client.publish(TOPIC_STAT, current_status, retain=True)
@@ -336,8 +349,8 @@ def on_message(client, userdata, msg):
boot_recovered = True boot_recovered = True
publish_all(client) publish_all(client)
elif cmd == "REBOOT": elif cmd == "REBOOT":
client.publish(TOPIC_STAT, f"OFFLINE - Riavvio {CLIENT_ID.upper()}...", retain=False) client.publish(TOPIC_STAT, f"OFFLINE - Rebooting {CLIENT_ID.upper()}...", retain=False)
logger.info("Comando REBOOT ricevuto. Riavvio sistema...") logger.info("REBOOT command received. Rebooting system...")
time.sleep(1) time.sleep(1)
subprocess.run(["sudo", "reboot"], check=True) subprocess.run(["sudo", "reboot"], check=True)
elif cmd == 'RESET_HAT': elif cmd == 'RESET_HAT':
@@ -351,28 +364,31 @@ def on_message(client, userdata, msg):
time.sleep(0.5) time.sleep(0.5)
GPIO.output(RESET_PIN, GPIO.HIGH) GPIO.output(RESET_PIN, GPIO.HIGH)
GPIO.cleanup(RESET_PIN) GPIO.cleanup(RESET_PIN)
logger.info(f"Impulso di RESET inviato al GPIO {RESET_PIN}") logger.info(f"RESET pulse sent to GPIO {RESET_PIN}")
time.sleep(1.5) time.sleep(1.5)
logger.info("Riavvio di MMDVMHost in corso...") logger.info("Restarting MMDVMHost...")
subprocess.run(["sudo", "systemctl", "restart", "mmdvmhost"], check=False) subprocess.run(["sudo", "systemctl", "restart", "mmdvmhost"], check=False)
client.publish(f"fleet/{CLIENT_ID}/status", "HAT RESET + MMDVM RESTART OK") client.publish(f"fleet/{CLIENT_ID}/status", "HAT RESET + MMDVM RESTART OK")
client.publish(f"devices/{CLIENT_ID}/logs", "🔌 HAT Reset + MMDVMHost Restarted") client.publish(f"devices/{CLIENT_ID}/logs", "🔌 HAT Reset + MMDVMHost Restarted")
except Exception as e: except Exception as e:
logger.error(f"Errore durante il reset GPIO/MMDVMHost: {e}") logger.error(f"Error during GPIO/MMDVMHost reset: {e}")
client.publish(f"fleet/{CLIENT_ID}/status", f"ERRORE RESET: {e}") client.publish(f"fleet/{CLIENT_ID}/status", f"RESET ERROR: {e}")
elif cmd in ["TG:OFF", "TG:ON"]: elif cmd in ["TG:OFF", "TG:ON"]:
nuovo_stato = (cmd == "TG:ON") new_state = (cmd == "TG:ON")
cfg['telegram']['enabled'] = nuovo_stato cfg['telegram']['enabled'] = new_state
try: try:
with open(CONFIG_PATH, 'w') as f: json.dump(cfg, f, indent=4) with open(CONFIG_PATH, 'w') as f: json.dump(cfg, f, indent=4)
client.publish(f"devices/{CLIENT_ID}/logs", f"{'🔔' if nuovo_stato else '🔇'} Notifiche {'ON' if nuovo_stato else 'OFF'}") client.publish(f"devices/{CLIENT_ID}/logs", f"{'🔔' if new_state else '🔇'} Notifications {'ON' if new_state else 'OFF'}")
if nuovo_stato: send_telegram_message("Notifiche riattivate!") if new_state: send_telegram_message("Notifications reactivated!")
except Exception as e: logger.error(f"Errore salvataggio stato Telegram: {e}") except Exception as e: logger.error(f"Error saving Telegram status: {e}")
elif topic == "devices/control/request" and payload.lower() in ["status", "update"]: elif topic == "devices/control/request" and payload.lower() in ["status", "update"]:
logger.info("📥 Received global update command (REQ CONFIG)")
publish_all(client) publish_all(client)
publish_all_ini_files(client) publish_all_ini_files(client)
# Force the visual update of the card on the dashboard!
client.publish(TOPIC_STAT, current_status, retain=True)
elif topic == f"devices/{CLIENT_ID}/control": elif topic == f"devices/{CLIENT_ID}/control":
if ":" in payload: if ":" in payload:
@@ -381,11 +397,11 @@ def on_message(client, userdata, msg):
try: try:
subprocess.run(["sudo", "systemctl", action.lower(), service.lower()], check=True) subprocess.run(["sudo", "systemctl", action.lower(), service.lower()], check=True)
client.publish(f"devices/{CLIENT_ID}/logs", f"{action.upper()}: {service}") client.publish(f"devices/{CLIENT_ID}/logs", f"{action.upper()}: {service}")
logger.info(f"Comando servizio eseguito: {action.upper()} {service}") logger.info(f"Service command executed: {action.upper()} {service}")
publish_all(client) publish_all(client)
except Exception as e: except Exception as e:
client.publish(f"devices/{CLIENT_ID}/logs", f"❌ ERROR: {str(e)}") client.publish(f"devices/{CLIENT_ID}/logs", f"❌ ERROR: {str(e)}")
logger.error(f"Errore esecuzione comando servizio: {e}") logger.error(f"Error executing service command: {e}")
elif topic.startswith(f"devices/{CLIENT_ID}/config_set/"): elif topic.startswith(f"devices/{CLIENT_ID}/config_set/"):
slug = topic.split("/")[-1] slug = topic.split("/")[-1]
@@ -402,6 +418,8 @@ def auto_publish_task(client):
time.sleep(cfg['settings'].get('update_interval', 30)) time.sleep(cfg['settings'].get('update_interval', 30))
def start_service(): def start_service():
global current_status
current_status = get_actual_config_from_disk()
client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID.upper()) client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID.upper())
client.will_set(TOPIC_STAT, payload=f"OFFLINE - {CLIENT_ID.upper()}", qos=1, retain=False) client.will_set(TOPIC_STAT, payload=f"OFFLINE - {CLIENT_ID.upper()}", qos=1, retain=False)
client.username_pw_set(cfg['mqtt']['user'], cfg['mqtt']['password']) client.username_pw_set(cfg['mqtt']['user'], cfg['mqtt']['password'])
@@ -409,19 +427,24 @@ def start_service():
client.on_disconnect = on_disconnect client.on_disconnect = on_disconnect
client.on_message = on_message client.on_message = on_message
while True: # 1. Start the telemetry "engine" ONLY ONCE
try:
logger.info("Tentativo di connessione al broker MQTT...")
client.connect(cfg['mqtt']['broker'], cfg['mqtt']['port'], 60)
client.loop_start()
threading.Thread(target=auto_publish_task, args=(client,), daemon=True).start() threading.Thread(target=auto_publish_task, args=(client,), daemon=True).start()
# Mantiene il processo vivo while True:
try:
logger.info("Attempting connection to MQTT broker...")
client.connect(cfg['mqtt']['broker'], cfg['mqtt']['port'], 60)
# 2. Start network manager in background (handles reconnections automatically!)
client.loop_start()
# 3. Pause main thread indefinitely
while True: while True:
time.sleep(1) time.sleep(1)
except Exception as e: except Exception as e:
logger.error(f"Impossibile connettersi o connessione persa ({e}). Riprovo in 10 secondi...") # This triggers ONLY if the broker is down when the node boots
logger.error(f"Broker unreachable at boot ({e}). Retrying in 10 seconds...")
time.sleep(10) time.sleep(10)
if __name__ == "__main__": if __name__ == "__main__":
+8
View File
@@ -459,6 +459,14 @@ def update_nodes():
mqtt_backend.publish("devices/control/request", "update") mqtt_backend.publish("devices/control/request", "update")
return jsonify({"success": True}) return jsonify({"success": True})
# Mandiamo il comando "update" direttamente nel topic privato di ciascun nodo
for client in clients_list:
cid = client['id'].lower()
mqtt_backend.publish(f"devices/{cid}/control", "update", qos=1)
logger.info("📢 Inviato comando REQ CONFIG diretto a tutti i nodi della flotta.")
return jsonify({"success": True})
@app.route('/api/users', methods=['GET']) @app.route('/api/users', methods=['GET'])
def get_users(): def get_users():
if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403 if session.get('role') != 'admin': return jsonify({"error": "Unauthorized"}), 403
+37 -14
View File
@@ -78,13 +78,23 @@
th { padding: 12px 15px; font-weight: 600; text-transform: uppercase; font-size: 0.75rem; color: var(--text-muted); } th { padding: 12px 15px; font-weight: 600; text-transform: uppercase; font-size: 0.75rem; color: var(--text-muted); }
td { padding: 10px 15px; border-bottom: 1px solid var(--border-color); color: var(--text-main); } td { padding: 10px 15px; border-bottom: 1px solid var(--border-color); color: var(--text-main); }
/* Animazioni Flat per i transiti */ /* Animazioni Uniformate per i transiti */
@keyframes pulse-border {
0% { border-color: var(--border-color); }
50% { border-color: var(--pulse-color, var(--primary)); }
100% { border-color: var(--border-color); }
}
.tx-active-unified {
animation: pulse-border 1.5s infinite !important;
color: var(--text-main) !important;
border-left: 4px solid var(--pulse-color, var(--primary)) !important;
background: #010409 !important;
}
/* La vecchia classe blink la teniamo solo per gli allarmi rossi dei demoni KO */
@keyframes flat-blink { 0% { border-color: var(--border-color); } 50% { border-color: var(--danger); } 100% { border-color: var(--border-color); } } @keyframes flat-blink { 0% { border-color: var(--border-color); } 50% { border-color: var(--danger); } 100% { border-color: var(--border-color); } }
.blink { animation: flat-blink 1.5s infinite; color: var(--danger) !important; } .blink { animation: flat-blink 1.5s infinite; color: var(--danger) !important; }
@keyframes flat-tx { 0% { border-left-color: var(--border-color); background: #010409; } 50% { border-left-color: var(--primary); background: rgba(47, 129, 247, 0.1); } 100% { border-left-color: var(--border-color); background: #010409; } }
.tx-active { animation: flat-tx 1.5s infinite !important; color: var(--text-main) !important; border-color: var(--border-color) !important; }
/* Finestre Modali (Popup) */ /* Finestre Modali (Popup) */
.modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(1, 4, 9, 0.85); z-index:1000; align-items:center; justify-content:center; } .modal-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(1, 4, 9, 0.85); z-index:1000; align-items:center; justify-content:center; }
.modal-content { background:var(--card-bg); border:1px solid var(--border-color); padding:25px; border-radius:6px; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 30px rgba(0,0,0,0.8); } .modal-content { background:var(--card-bg); border:1px solid var(--border-color); padding:25px; border-radius:6px; max-height: 90vh; overflow-y: auto; box-shadow: 0 10px 30px rgba(0,0,0,0.8); }
@@ -658,18 +668,31 @@
if (altDiv) { if (altDiv) {
altDiv.style.display = "block"; altDiv.innerText = telemetryObj.alt; altDiv.style.display = "block"; altDiv.innerText = telemetryObj.alt;
let altText = telemetryObj.alt.toUpperCase(); let altText = telemetryObj.alt.toUpperCase();
if (altText.includes("NXDN")) activeModeColor = "#10b981"; else if (altText.includes("YSF")) activeModeColor = "#8b5cf6"; else if (altText.includes("D-STAR")) activeModeColor = "#06b6d4"; else if (altText.includes("P25")) activeModeColor = "#f59e0b";
// Assegna il colore in base al modo
if (altText.includes("NXDN")) activeModeColor = "#10b981";
else if (altText.includes("YSF")) activeModeColor = "#8b5cf6";
else if (altText.includes("D-STAR")) activeModeColor = "#06b6d4";
else if (altText.includes("P25")) activeModeColor = "#f59e0b";
isTx = altText.includes("🟢") || altText.includes("🟣") || altText.includes("🔵") || altText.includes("🟠"); isTx = altText.includes("🟢") || altText.includes("🟣") || altText.includes("🔵") || altText.includes("🟠");
altDiv.style.setProperty('color', activeModeColor, 'important');
altDiv.style.setProperty('border-left', `4px solid ${activeModeColor}`, 'important'); // Passiamo il colore al CSS per l'animazione
if (isTx) { altDiv.classList.add('blink'); } else { altDiv.classList.remove('blink'); } altDiv.style.setProperty('--pulse-color', activeModeColor);
if (isTx) {
altDiv.classList.add('tx-active-unified');
} else {
altDiv.classList.remove('tx-active-unified');
altDiv.style.setProperty('color', 'var(--text-muted)', 'important');
altDiv.style.setProperty('border-left', `4px solid var(--border-color)`, 'important');
}
} }
} else { } else {
if (altDiv) altDiv.style.display = "none"; if (altDiv) altDiv.style.display = "none";
if (tsContainer) tsContainer.style.display = "flex"; if (tsContainer) tsContainer.style.display = "flex";
let netObj = data.networks && data.networks[c.id.toLowerCase()] ? data.networks[c.id.toLowerCase()] : {ts1: "", ts2: ""}; let netObj = data.networks && data.networks[c.id.toLowerCase()] ? data.networks[c.id.toLowerCase()] : {ts1: "", ts2: ""};
activeModeColor = "var(--primary)"; activeModeColor = "var(--primary)"; // Default Blu per il DMR
if (ts1Div && ts2Div) { if (ts1Div && ts2Div) {
[ts1Div, ts2Div].forEach((div, idx) => { [ts1Div, ts2Div].forEach((div, idx) => {
@@ -679,15 +702,15 @@
const fullLabel = netName ? `${baseLabel} [${netName}]` : baseLabel; const fullLabel = netName ? `${baseLabel} [${netName}]` : baseLabel;
div.innerText = `${fullLabel}: ${val}`; div.innerText = `${fullLabel}: ${val}`;
div.style.setProperty('--pulse-color', activeModeColor);
if (val.includes("🎙️")) { if (val.includes("🎙️")) {
isTx = true; isTx = true;
div.classList.add('tx-active'); div.classList.add('tx-active-unified');
} else { } else {
div.classList.remove('tx-active'); div.classList.remove('tx-active-unified');
div.style.setProperty('color', 'var(--text-main)', 'important'); div.style.setProperty('color', 'var(--text-muted)', 'important');
div.style.setProperty('border-left-color', 'var(--primary)', 'important'); div.style.setProperty('border-left', '4px solid var(--border-color)', 'important');
div.style.setProperty('background', 'rgba(59, 130, 246, 0.1)', 'important');
} }
}); });
} }
File diff suppressed because it is too large Load Diff