Merge pull request #1 from picchiosat/websocket-upgrade
Aggiunti WebSockets al frontend
This commit is contained in:
@@ -13,24 +13,25 @@
|
|||||||
|
|
||||||
### 🏗️ System Architecture
|
### 🏗️ System Architecture
|
||||||
The ecosystem consists of three main parts:
|
The ecosystem consists of three main parts:
|
||||||
1. **The Central Dashboard (`app.py`):** A Flask web server that handles the UI (with modern Glassmorphism design), user permissions (SQLite), and logic.
|
1. **The Central Dashboard (`app.py`):** A Flask web server that handles the UI (with modern Glassmorphism design), user permissions (SQLite), and **WebSocket connections for zero-latency updates**.
|
||||||
2. **The Remote Agent (`system_monitor.py`):** A lightweight script running on each repeater (Raspberry Pi/Linux) that monitors hardware and executes commands.
|
2. **The Remote Agent (`system_monitor.py`):** A lightweight script running on each repeater (Raspberry Pi/Linux) that monitors hardware and executes commands.
|
||||||
3. **The MQTT Broker:** The communication backbone. All data and commands flow through MQTT for near-instant responsiveness.
|
3. **The MQTT Broker:** The communication backbone. All data and commands flow through MQTT for near-instant responsiveness.
|
||||||
|
|
||||||
### ✨ Features
|
### ✨ Features
|
||||||
|
* **Zero-Latency Real-Time UI:** Powered by WebSockets (Socket.IO), the dashboard updates instantly upon radio traffic or telemetry changes, completely eliminating heavy HTTP polling overhead.
|
||||||
* **Centralized Telemetry:** Real-time CPU, RAM, Temperature, and Disk usage for all nodes.
|
* **Centralized Telemetry:** Real-time CPU, RAM, Temperature, and Disk usage for all nodes.
|
||||||
* **Service Management:** Start, Stop, or Restart system daemons (MMDVMHost, DMRGateway, etc.) remotely.
|
* **Service Management:** Start, Stop, or Restart system daemons (MMDVMHost, DMRGateway, etc.) remotely.
|
||||||
* **Smart Auto-Healing:** The agent automatically detects crashed services and attempts to revive them before raising critical alerts (includes Telegram notifications).
|
* **Smart Auto-Healing:** The agent automatically detects crashed services and attempts to revive them before raising critical alerts (includes Telegram notifications).
|
||||||
* **Hardware Reset (GPIO):** Physically reboot the MMDVM radio HAT via GPIO pins directly from the dashboard, automatically restarting the host daemon for a fail-proof serial connection recovery.
|
* **Hardware Reset (GPIO):** Physically reboot the MMDVM radio HAT via GPIO pins directly from the dashboard, automatically restarting the host daemon for a fail-proof serial connection recovery.
|
||||||
* **Remote Configuration:** Built-in web editor for `.ini` files—no SSH required.
|
* **Remote Configuration:** Built-in web editor for `.ini` files—no SSH required.
|
||||||
* **Live Heard Log:** Unified view of radio traffic across the entire network.
|
* **Live Heard Log:** Unified, real-time view of radio traffic across the entire network.
|
||||||
* **Global Operations:** Switch profiles (e.g., Profile A/B) or force network-wide configuration updates instantly on all repeaters with one click.
|
* **Global Operations:** Switch profiles (e.g., Profile A/B) or force network-wide configuration updates instantly on all repeaters with one click.
|
||||||
* **PWA Ready (Mobile App):** Fully responsive design that can be installed directly on Android/iOS as a Progressive Web App for a native, full-screen mobile experience.
|
* **PWA Ready (Mobile App):** Fully responsive design that can be installed directly on Android/iOS as a Progressive Web App for a native, full-screen mobile experience.
|
||||||
|
|
||||||
### 🚀 Installation & Setup
|
### 🚀 Installation & Setup
|
||||||
|
|
||||||
#### 1. Server Setup (Central Hub)
|
#### 1. Server Setup (Central Hub)
|
||||||
* Install dependencies: `pip install flask paho-mqtt psutil werkzeug`
|
* Install dependencies: `pip install flask flask-socketio paho-mqtt psutil werkzeug`
|
||||||
* Configure `config.json` (use `config.example.json` as template) with your MQTT credentials.
|
* Configure `config.json` (use `config.example.json` as template) with your MQTT credentials.
|
||||||
* Define your repeaters in `clients.json`.
|
* Define your repeaters in `clients.json`.
|
||||||
* Run: `python3 app.py`
|
* Run: `python3 app.py`
|
||||||
@@ -53,24 +54,25 @@ The `system_monitor.py` must be installed on every node you wish to monitor.
|
|||||||
|
|
||||||
### 🏗️ Architettura del Sistema
|
### 🏗️ Architettura del Sistema
|
||||||
L'ecosistema si compone di tre parti principali:
|
L'ecosistema si compone di tre parti principali:
|
||||||
1. **Dashboard Centrale (`app.py`):** Un server web Flask che gestisce l'interfaccia (con design moderno in stile Glassmorphism), i permessi utenti (SQLite) e la logica di controllo.
|
1. **Dashboard Centrale (`app.py`):** Un server web Flask che gestisce l'interfaccia (con design moderno in stile Glassmorphism), i permessi utenti (SQLite) e **connessioni WebSocket per aggiornamenti a latenza zero**.
|
||||||
2. **Agente Remoto (`system_monitor.py`):** Uno script leggero in esecuzione su ogni ripetitore (Raspberry Pi/Linux) che raccoglie i dati hardware ed esegue i comandi.
|
2. **Agente Remoto (`system_monitor.py`):** Uno script leggero in esecuzione su ogni ripetitore (Raspberry Pi/Linux) che raccoglie i dati hardware ed esegue i comandi.
|
||||||
3. **Broker MQTT:** Il centro nevralgico della comunicazione. Tutti i dati e i comandi viaggiano su MQTT per una reattività istantanea.
|
3. **Broker MQTT:** Il centro nevralgico della comunicazione. Tutti i dati e i comandi viaggiano su MQTT per una reattività istantanea.
|
||||||
|
|
||||||
### ✨ Funzionalità
|
### ✨ Funzionalità
|
||||||
|
* **Interfaccia Real-Time a Latenza Zero:** Grazie all'integrazione di WebSockets (Socket.IO), la dashboard scatta all'istante al passaggio di traffico radio o ai cambi di telemetria, eliminando totalmente il carico del polling HTTP continuo.
|
||||||
* **Telemetria Centralizzata:** Stato in tempo reale di CPU, RAM, Temperatura e Disco di tutti i nodi.
|
* **Telemetria Centralizzata:** Stato in tempo reale di CPU, RAM, Temperatura e Disco di tutti i nodi.
|
||||||
* **Gestione Servizi:** Avvio, arresto o riavvio dei demoni di sistema (MMDVMHost, DMRGateway, ecc.) da remoto.
|
* **Gestione Servizi:** Avvio, arresto o riavvio dei demoni di sistema (MMDVMHost, DMRGateway, ecc.) da remoto.
|
||||||
* **Auto-Healing Intelligente:** L'agente rileva automaticamente i servizi andati in blocco e tenta di rianimarli prima di inviare allarmi critici (include notifiche Telegram).
|
* **Auto-Healing Intelligente:** L'agente rileva automaticamente i servizi andati in blocco e tenta di rianimarli prima di inviare allarmi critici (include notifiche Telegram).
|
||||||
* **Reset Hardware (GPIO):** Invia un impulso di reset fisico alla scheda radio MMDVM tramite i pin GPIO direttamente dalla dashboard, riavviando automaticamente il demone host per ripristinare la comunicazione seriale.
|
* **Reset Hardware (GPIO):** Invia un impulso di reset fisico alla scheda radio MMDVM tramite i pin GPIO direttamente dalla dashboard, riavviando automaticamente il demone host per ripristinare la comunicazione seriale.
|
||||||
* **Configurazione Remota:** Editor web integrato per i file `.ini`: modifica i parametri senza accedere in SSH.
|
* **Configurazione Remota:** Editor web integrato per i file `.ini`: modifica i parametri senza accedere in SSH.
|
||||||
* **Log Ascolti Live:** Vista unificata del traffico radio di tutta la rete.
|
* **Log Ascolti Live:** Vista unificata e in tempo reale del traffico radio di tutta la rete.
|
||||||
* **Operazioni Globali:** Commuta tra diversi assetti (es. Profilo A/B) o forza l'aggiornamento dei dati contemporaneamente su tutta la rete con un solo clic.
|
* **Operazioni Globali:** Commuta tra diversi assetti (es. Profilo A/B) o forza l'aggiornamento dei dati contemporaneamente su tutta la rete con un solo clic.
|
||||||
* **PWA Ready (App Mobile):** Design completamente responsivo, installabile su smartphone Android e iOS come Progressive Web App per un'esperienza fluida e nativa a schermo intero.
|
* **PWA Ready (App Mobile):** Design completamente responsivo, installabile su smartphone Android e iOS come Progressive Web App per un'esperienza fluida e nativa a schermo intero.
|
||||||
|
|
||||||
### 🚀 Installazione e Configurazione
|
### 🚀 Installazione e Configurazione
|
||||||
|
|
||||||
#### 1. Setup del Server (Hub Centrale)
|
#### 1. Setup del Server (Hub Centrale)
|
||||||
* Installa le dipendenze: `pip install flask paho-mqtt psutil werkzeug`
|
* Installa le dipendenze: `pip install flask flask-socketio paho-mqtt psutil werkzeug`
|
||||||
* Configura `config.json` (usa `config.example.json` come base) con le credenziali MQTT.
|
* Configura `config.json` (usa `config.example.json` come base) con le credenziali MQTT.
|
||||||
* Definisci i tuoi ripetitori nel file `clients.json`.
|
* Definisci i tuoi ripetitori nel file `clients.json`.
|
||||||
* Avvia: `python3 app.py`
|
* Avvia: `python3 app.py`
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from flask_socketio import SocketIO, emit
|
||||||
|
|
||||||
# --- CONFIGURAZIONE LOGGING ---
|
# --- CONFIGURAZIONE LOGGING ---
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -99,6 +100,7 @@ def get_call(id, proto="DMR"):
|
|||||||
|
|
||||||
def save_cache(data):
|
def save_cache(data):
|
||||||
with open(CACHE_FILE, 'w') as f: json.dump(data, f)
|
with open(CACHE_FILE, 'w') as f: json.dump(data, f)
|
||||||
|
socketio.emit('dati_aggiornati')
|
||||||
|
|
||||||
def save_to_sqlite(client_id, data, protocol="DMR"):
|
def save_to_sqlite(client_id, data, protocol="DMR"):
|
||||||
conn = sqlite3.connect(DB_PATH)
|
conn = sqlite3.connect(DB_PATH)
|
||||||
@@ -107,8 +109,10 @@ def save_to_sqlite(client_id, data, protocol="DMR"):
|
|||||||
(client_id, protocol, str(data.get('source_id', '---')), str(data.get('destination_id', '---')), data.get('slot', 0), round(data.get('duration', 0), 1), round(data.get('ber', 0), 2)))
|
(client_id, protocol, str(data.get('source_id', '---')), str(data.get('destination_id', '---')), data.get('slot', 0), round(data.get('duration', 0), 1), round(data.get('ber', 0), 2)))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
socketio.emit('dati_aggiornati')
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
|
||||||
app.secret_key = 'ari_fvg_secret_ultra_secure'
|
app.secret_key = 'ari_fvg_secret_ultra_secure'
|
||||||
client_states = {}
|
client_states = {}
|
||||||
device_configs = {}
|
device_configs = {}
|
||||||
@@ -169,6 +173,7 @@ def on_message(client, userdata, msg):
|
|||||||
|
|
||||||
elif parts[0] == 'servizi':
|
elif parts[0] == 'servizi':
|
||||||
client_states[cid] = payload
|
client_states[cid] = payload
|
||||||
|
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
|
||||||
if payload.upper() not in ['OFF', 'OFFLINE', '']:
|
if payload.upper() not in ['OFF', 'OFFLINE', '']:
|
||||||
tel = client_telemetry.get(cid, {})
|
tel = client_telemetry.get(cid, {})
|
||||||
if isinstance(tel, dict) and '🔄' in str(tel.get('ts1', '')):
|
if isinstance(tel, dict) and '🔄' in str(tel.get('ts1', '')):
|
||||||
@@ -187,9 +192,9 @@ def on_message(client, userdata, msg):
|
|||||||
"files": data.get("files", data.get("config_files", [])),
|
"files": data.get("files", data.get("config_files", [])),
|
||||||
"profiles": data.get("profiles", {"A": "PROFILO A", "B": "PROFILO B"})
|
"profiles": data.get("profiles", {"A": "PROFILO A", "B": "PROFILO B"})
|
||||||
}
|
}
|
||||||
|
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
|
||||||
except Exception as e: logger.error(f"Errore parsing health: {e}")
|
except Exception as e: logger.error(f"Errore parsing health: {e}")
|
||||||
|
|
||||||
# NUOVO BLOCCO: Intercettazione configurazione DMRGateway
|
|
||||||
elif len(parts) >= 4 and parts[0] == 'data' and parts[2].lower() == 'dmrgateway' and (parts[3].upper().startswith('NETWORK') or parts[3].upper().startswith('DMR NETWORK')):
|
elif len(parts) >= 4 and parts[0] == 'data' and parts[2].lower() == 'dmrgateway' and (parts[3].upper().startswith('NETWORK') or parts[3].upper().startswith('DMR NETWORK')):
|
||||||
try:
|
try:
|
||||||
cid = parts[1].lower()
|
cid = parts[1].lower()
|
||||||
@@ -211,6 +216,7 @@ def on_message(client, userdata, msg):
|
|||||||
|
|
||||||
if is_ts1: network_mapping[cid]["ts1"] = net_name
|
if is_ts1: network_mapping[cid]["ts1"] = net_name
|
||||||
if is_ts2: network_mapping[cid]["ts2"] = net_name
|
if is_ts2: network_mapping[cid]["ts2"] = net_name
|
||||||
|
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Errore parsing DMRGateway per {cid}: {e}")
|
logger.error(f"Errore parsing DMRGateway per {cid}: {e}")
|
||||||
@@ -259,6 +265,7 @@ def on_message(client, userdata, msg):
|
|||||||
active_calls[cid][sk] = {'src': src, 'dst': dst}
|
active_calls[cid][sk] = {'src': src, 'dst': dst}
|
||||||
client_telemetry[cid]["alt"] = ""
|
client_telemetry[cid]["alt"] = ""
|
||||||
client_telemetry[cid][sk] = f"🎙️ {src} ➔ TG {dst}"
|
client_telemetry[cid][sk] = f"🎙️ {src} ➔ TG {dst}"
|
||||||
|
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
|
||||||
elif act in ['end', 'lost']:
|
elif act in ['end', 'lost']:
|
||||||
info = active_calls[cid].get(sk, {'src': '---', 'dst': '---'})
|
info = active_calls[cid].get(sk, {'src': '---', 'dst': '---'})
|
||||||
d['source_id'], d['destination_id'] = info['src'], info['dst']
|
d['source_id'], d['destination_id'] = info['src'], info['dst']
|
||||||
@@ -288,6 +295,7 @@ def on_message(client, userdata, msg):
|
|||||||
|
|
||||||
active_calls[cid][k] = {'src': src, 'dst': target}
|
active_calls[cid][k] = {'src': src, 'dst': target}
|
||||||
client_telemetry[cid].update({"ts1":"","ts2":"","alt": f"{ico} {name}: {src} ➔ {target}"})
|
client_telemetry[cid].update({"ts1":"","ts2":"","alt": f"{ico} {name}: {src} ➔ {target}"})
|
||||||
|
socketio.emit('dati_aggiornati') # <--- WEBSOCKET
|
||||||
|
|
||||||
elif act in ['end', 'lost']:
|
elif act in ['end', 'lost']:
|
||||||
info = active_calls[cid].get(k, {'src': '---', 'dst': '---'})
|
info = active_calls[cid].get(k, {'src': '---', 'dst': '---'})
|
||||||
@@ -619,4 +627,4 @@ def serve_icon():
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
threading.Thread(target=auto_update_ids, daemon=True).start()
|
threading.Thread(target=auto_update_ids, daemon=True).start()
|
||||||
app.run(host='0.0.0.0', port=5000)
|
socketio.run(app, host='0.0.0.0', port=9000)
|
||||||
|
|||||||
+21
-2
@@ -363,6 +363,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// --- 1. TRANSLATION SYSTEM (i18n) ---
|
// --- 1. TRANSLATION SYSTEM (i18n) ---
|
||||||
const i18n = {
|
const i18n = {
|
||||||
@@ -655,7 +657,7 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
refreshStates(); refreshLogs();
|
refreshStates(); refreshLogs();
|
||||||
setInterval(() => { refreshStates(); refreshLogs(); }, 3000);
|
// setInterval rimosso! Da oggi si va in tempo reale!
|
||||||
} catch (e) { console.error(e); }
|
} catch (e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1074,7 +1076,24 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
initUI();
|
initUI();
|
||||||
|
|
||||||
|
// --- MOTORE WEBSOCKET REAL-TIME ---
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
socket.on('connect', () => {
|
||||||
|
console.log("🟢 Connesso al server via WebSocket in tempo reale!");
|
||||||
|
// Facciamo un aggiornamento di sicurezza appena il tunnel si apre
|
||||||
|
refreshStates();
|
||||||
|
refreshLogs();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on('dati_aggiornati', function() {
|
||||||
|
console.log("⚡ Rilevato nuovo traffico! Scatto istantaneo dell'interfaccia...");
|
||||||
|
// Il server ha appena urlato che ci sono novità: ricarichiamo le card e i log!
|
||||||
|
refreshStates();
|
||||||
|
refreshLogs();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
|
|||||||
Reference in New Issue
Block a user