diff --git a/index.html b/index.html index a052132..d933fee 100644 --- a/index.html +++ b/index.html @@ -3,61 +3,100 @@ - DMR Web Receiver + DMR Web Receiver Pro -
-

ARI FVG Web-Receiver

- - -
IN ASCOLTO
+

ARI FVG Web-Receiver

+ +
+ +
+
Selettore Talkgroup
+ +
STANDBY
+
- -
- - +
+ + +
+ +
+ + + +
+ +
+ + +
+ + +
Sistema in attesa...
- -
- - - + +
+
Monitor Rete Globale
+
+
Nessun traffico rilevato...
+
- - -
- - -
- - - - -
Clicca il pulsante per attivare l'audio.
diff --git a/server.py b/server.py index 76ba4c6..e03df7d 100644 --- a/server.py +++ b/server.py @@ -120,6 +120,9 @@ class OpenBridgeRxClient(asyncio.DatagramProtocol): src_id = int.from_bytes(data[5:8], 'big') tg_arrivo = str(int.from_bytes(data[8:11], 'big')) + # Traccia il traffico a prescindere da chi lo ascolta + self.server_app.aggiorna_traffico_globale(src_id, tg_arrivo) + # Ricava l'elenco dei TG attualmente richiesti da almeno un utente Web tg_attivi = {client_data["tg"] for client_data in self.server_app.clients.values()} @@ -156,6 +159,27 @@ class WebDmrServer: # Dizionario stati indipendenti per TG: "222" -> {"rx_in_corso": False, ...} self.tg_states = {} + + # Dizionario globale traffico + self.traffico_globale = {} + + def aggiorna_traffico_globale(self, src_id, tg): + """Aggiorna il monitor globale e avvisa i client se c'è un nuovo TX""" + ora = time.time() + nuovo = tg not in self.traffico_globale or self.traffico_globale[tg]["src_id"] != src_id + self.traffico_globale[tg] = {"src_id": src_id, "time": ora} + + if nuovo: + print(f"[Traffico Globale] Rilevato TX su TG {tg} da ID {src_id}") # <--- AGGIUNGI QUESTA RIGA + asyncio.create_task(self.invia_stato_traffico()) + + async def invia_stato_traffico(self): + """Invia la lista dei TG attivi a tutti i WebSocket connessi""" + attivi = [{"tg": k, "src": cerca_nominativo(v["src_id"])} for k, v in self.traffico_globale.items()] + msg_str = json.dumps({"action": "global_traffic", "data": attivi}) + for ws in list(self.clients.keys()): + try: await ws.send_str(msg_str) + except Exception: pass def ricevi_frame_dmr(self, ambe_frames, src_id, tg): if tg not in self.tg_states: @@ -181,10 +205,12 @@ class WebDmrServer: asyncio.create_task(self.processa_audio_dmr(ambe_frames, tg)) async def watchdog_rx(self): - """Monitora il silenzio e ripristina lo stato per i singoli TG""" + """Monitora il silenzio e ripristina lo stato per i singoli TG e per il traffico globale""" while True: await asyncio.sleep(0.4) ora = time.time() + + # Watchdog TG sintonizzati for tg, stato_tg in list(self.tg_states.items()): if stato_tg["rx_in_corso"] and (ora - stato_tg["ultimo_frame_time"] > 1.2): stato_tg["rx_in_corso"] = False @@ -195,6 +221,15 @@ class WebDmrServer: "text": "IN ASCOLTO", "active": False }) + + # Watchdog traffico globale + cambiamento = False + for tg in list(self.traffico_globale.keys()): + if ora - self.traffico_globale[tg]["time"] > 1.5: + del self.traffico_globale[tg] + cambiamento = True + if cambiamento: + await self.invia_stato_traffico() async def processa_audio_dmr(self, ambe_27_bytes, tg): try: @@ -264,6 +299,9 @@ async def websocket_handler(request): print(f"[WebSocket] Nuovo client connesso (Totali: {len(server_app.clients)}/{MAX_UTENTI_WEB})") try: + # Invia subito lo stato del traffico globale al nuovo connesso + await server_app.invia_stato_traffico() + async for msg in ws: if msg.type == web.WSMsgType.TEXT: dati = json.loads(msg.data) diff --git a/static/app.js b/static/app.js index 040a652..3265422 100644 --- a/static/app.js +++ b/static/app.js @@ -1,6 +1,10 @@ +console.log("App.js caricato - VU Meter & Click-to-Tune attivi!"); + let ws = null; let audioCtx = null; -let gainNode = null; // Il controller del volume +let gainNode = null; +let analyser = null; +let dataArray = null; let nextStartTime = 0; const connectBtn = document.getElementById('connectBtn'); @@ -8,135 +12,160 @@ const tgInput = document.getElementById('tgInput'); const setTgBtn = document.getElementById('setTgBtn'); const displayScreen = document.getElementById('displayScreen'); const statusText = document.getElementById('statusText'); - -// --- CONTROLLI VOLUME --- const volumeSlider = document.getElementById('volumeSlider'); const volumeValue = document.getElementById('volumeValue'); +const liveTrafficList = document.getElementById('liveTrafficList'); +const vuMeterBar = document.getElementById('vuMeterBar'); -volumeSlider.addEventListener('input', (e) => { - const val = parseFloat(e.target.value); - volumeValue.innerText = Math.round(val * 100) + '%'; - - // Modifica il volume in tempo reale - if (gainNode) { - gainNode.gain.value = val; - } -}); +if(statusText) statusText.innerText = "✅ Sistema pronto. Clicca Avvia Ascolto."; -// --- RIPRODUZIONE AUDIO PCM --- -function playPCM320(arrayBuffer) { - if (!audioCtx || !gainNode) return; - - const int16Array = new Int16Array(arrayBuffer); - const float32Array = new Float32Array(int16Array.length); - for (let i = 0; i < int16Array.length; i++) { - float32Array[i] = int16Array[i] / 32768.0; - } - - const buffer = audioCtx.createBuffer(1, float32Array.length, 8000); - buffer.getChannelData(0).set(float32Array); - - const source = audioCtx.createBufferSource(); - source.buffer = buffer; - - // Colleghiamo l'audio al controller del volume invece che direttamente all'uscita - source.connect(gainNode); - - const currentTime = audioCtx.currentTime; - if (nextStartTime < currentTime) { - nextStartTime = currentTime + 0.02; - } - source.start(nextStartTime); - nextStartTime += buffer.duration; +if(volumeSlider) { + volumeSlider.addEventListener('input', (e) => { + const val = parseFloat(e.target.value); + if(volumeValue) volumeValue.innerText = Math.round(val * 100) + '%'; + if (gainNode) gainNode.gain.value = val; + }); } -// --- CONNESSIONE WEBSOCKET --- -function startAudio() { - if (!audioCtx) { - audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - - // Creazione del nodo Volume - gainNode = audioCtx.createGain(); - gainNode.connect(audioCtx.destination); - gainNode.gain.value = parseFloat(volumeSlider.value); - } - if (audioCtx.state === 'suspended') { - audioCtx.resume(); - } +window.setQuickTg = function(valore) { + if (tgInput) tgInput.value = valore; + inviaCambioTG(valore); +}; - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - ws = new WebSocket(`${protocol}//${window.location.host}/ws`); - ws.binaryType = 'arraybuffer'; - - ws.onopen = () => { - connectBtn.style.display = 'none'; - inviaCambioTG(tgInput.value); - }; - - ws.onmessage = (event) => { - if (event.data instanceof ArrayBuffer) { - playPCM320(event.data); - } else if (typeof event.data === 'string') { - const data = JSON.parse(event.data); - - // --- GESTIONE ERRORE SERVER PIENO --- - if (data.action === "error") { - displayScreen.innerText = "SERVER PIENO"; - displayScreen.style.background = "#e74c3c"; // Rosso[cite: 5] - statusText.innerText = "❌ " + data.text; - statusText.style.color = "#e74c3c"; - return; - } - - if (data.action === "display") { - displayScreen.innerText = data.text; - statusText.style.color = "#bdc3c7"; // Ripristina il colore di default del testo[cite: 5] - if (data.active) { - displayScreen.style.background = "#e67e22"; // Arancione durante RX[cite: 5] - } else { - displayScreen.style.background = "#1abc9c"; // Verde in Standby[cite: 5] - } - } - } - }; - - ws.onclose = () => { - // Se il display non è in stato di errore "SERVER PIENO", mostra la riconnessione standard[cite: 5] - if (displayScreen.innerText !== "SERVER PIENO") { - statusText.innerText = "Connessione persa. Riconnessione..."; - statusText.style.color = "#bdc3c7"; - connectBtn.style.display = 'block'; - connectBtn.innerText = "Riconnetti Ricevitore"; - } - }; -} - -// --- FUNZIONE PER CAMBIARE TG --- function inviaCambioTG(nuovoTG) { if (!nuovoTG || nuovoTG <= 0) return; - if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ action: "cambia_tg", tg: nuovoTG })); - displayScreen.innerText = "SINTONIA..."; - statusText.innerText = "Sintonizzato su TG " + nuovoTG; + if(displayScreen) displayScreen.innerText = "SINTONIA..."; + if(statusText) statusText.innerText = "Sintonizzato su TG " + nuovoTG; } else { - statusText.innerText = "Pronto a sintonizzare TG " + nuovoTG; + if(statusText) statusText.innerText = "Pronto a sintonizzare TG " + nuovoTG + " (Avvia prima)"; } } -// Scelta Rapida dai tasti prefissati -function setQuickTg(valore) { - tgInput.value = valore; - inviaCambioTG(valore); +// --- VU METER LOOP --- +function renderVUMeter() { + requestAnimationFrame(renderVUMeter); + if (!analyser || !vuMeterBar) return; + + analyser.getByteTimeDomainData(dataArray); + let sum = 0; + for(let i = 0; i < dataArray.length; i++) { + let v = (dataArray[i] - 128) / 128.0; + sum += v * v; + } + let rms = Math.sqrt(sum / dataArray.length); + let percent = Math.min(100, rms * 500); // Moltiplicatore di sensibilità grafica + + vuMeterBar.style.width = percent + '%'; + if (percent > 85) vuMeterBar.style.background = '#e74c3c'; // Rosso saturo + else if (percent > 50) vuMeterBar.style.background = '#f1c40f'; // Giallo + else vuMeterBar.style.background = '#2ecc71'; // Verde standard } -// --- EVENTI --- -connectBtn.addEventListener('click', startAudio); -setTgBtn.addEventListener('click', () => inviaCambioTG(tgInput.value)); +function playPCM320(arrayBuffer) { + if (!audioCtx || !analyser) return; + try { + const int16Array = new Int16Array(arrayBuffer); + const float32Array = new Float32Array(int16Array.length); + for (let i = 0; i < int16Array.length; i++) { + float32Array[i] = int16Array[i] / 32768.0; + } -// Invio anche premendo "Invio" sulla tastiera dentro la casella di testo -tgInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - inviaCambioTG(tgInput.value); + const buffer = audioCtx.createBuffer(1, float32Array.length, 8000); + buffer.getChannelData(0).set(float32Array); + + const source = audioCtx.createBufferSource(); + source.buffer = buffer; + + // Collega al VU meter, che a sua volta è collegato al volume + source.connect(analyser); + + const currentTime = audioCtx.currentTime; + if (nextStartTime < currentTime) nextStartTime = currentTime + 0.02; + source.start(nextStartTime); + nextStartTime += buffer.duration; + } catch(err) { + console.error("Errore audio:", err); } -}); +} + +function startAudio() { + try { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + + analyser = audioCtx.createAnalyser(); + analyser.fftSize = 256; + dataArray = new Uint8Array(analyser.frequencyBinCount); + + gainNode = audioCtx.createGain(); + + // Catena: Source -> Analyser(VU) -> Gain(Volume) -> Casse + analyser.connect(gainNode); + gainNode.connect(audioCtx.destination); + + if(volumeSlider) gainNode.gain.value = parseFloat(volumeSlider.value); + + renderVUMeter(); // Avvia l'animazione grafica + } + if (audioCtx.state === 'suspended') audioCtx.resume(); + if(statusText) statusText.innerText = "🔄 Connessione in corso..."; + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + ws = new WebSocket(`${protocol}//${window.location.host}/ws`); + ws.binaryType = 'arraybuffer'; + + ws.onopen = () => { + if(connectBtn) connectBtn.style.display = 'none'; + if(tgInput) inviaCambioTG(tgInput.value); + }; + + ws.onmessage = (event) => { + if (event.data instanceof ArrayBuffer) { + playPCM320(event.data); + } else if (typeof event.data === 'string') { + const data = JSON.parse(event.data); + + if (data.action === "error") return; + + if (data.action === "display") { + if(displayScreen) { + displayScreen.innerText = data.text; + displayScreen.style.background = data.active ? "#e67e22" : "#1abc9c"; + } + if(statusText) statusText.style.color = "#bdc3c7"; + } + + // CLICK-TO-TUNE IMPLEMENTATO QUI + if (data.action === "global_traffic" && liveTrafficList) { + if (!data.data || data.data.length === 0) { + liveTrafficList.innerHTML = "Nessun traffico in corso..."; + } else { + liveTrafficList.innerHTML = data.data.map(item => + `
+ TG ${item.tg}: ${item.src} +
` + ).join(""); + } + } + } + }; + + ws.onclose = () => { + if(statusText) statusText.innerText = "❌ Connessione persa."; + if(connectBtn) connectBtn.style.display = 'block'; + }; + + } catch (err) { + console.error("Errore in avvio:", err); + } +} + +if(connectBtn) connectBtn.addEventListener('click', startAudio); +if(setTgBtn) setTgBtn.addEventListener('click', () => { if(tgInput) inviaCambioTG(tgInput.value); }); +if(tgInput) { + tgInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') inviaCambioTG(tgInput.value); + }); +}