mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-07-30 15:59:16 +02:00
fix
This commit is contained in:
+4
-5
@@ -186,19 +186,18 @@ async def socket_handler(socket: asyncio.StreamReader, writer: asyncio.StreamWri
|
|||||||
while True:
|
while True:
|
||||||
qdata, ws = await writer_q.get()
|
qdata, ws = await writer_q.get()
|
||||||
if not qdata or not ws: break
|
if not qdata or not ws: break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
writer.write(qdata)
|
writer.write(qdata)
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
data = await socket.read(256)
|
data = await socket.read(256)
|
||||||
if not data:
|
if not data:
|
||||||
await ws.send(json.dumps({"event": "error", "error": "fm95 socket closed"}))
|
await ws.send(json.dumps({"event": "error", "error": "fm95 socket closed"}))
|
||||||
break
|
raise ConnectionResetError("fm95 socket closed") # ← let reconnect handle it
|
||||||
await ws.send(json.dumps({"event": "fm95", "data": base64.b64encode(data).decode()}))
|
await ws.send(json.dumps({"event": "fm95", "data": base64.b64encode(data).decode()}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ws.send(json.dumps({"event": "error", "error": str(e)}))
|
try: await ws.send(json.dumps({"event": "error", "error": str(e)}))
|
||||||
break
|
except Exception: pass
|
||||||
|
raise
|
||||||
|
|
||||||
def websocket_server_process(shared_data: dict, imc_q: multiprocessing.Queue, ws_q: multiprocessing.Queue):
|
def websocket_server_process(shared_data: dict, imc_q: multiprocessing.Queue, ws_q: multiprocessing.Queue):
|
||||||
async def runner():
|
async def runner():
|
||||||
|
|||||||
+52
-71
@@ -1,10 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>FM95 Processor Status</title>
|
<title>FM95 Processor Status</title>
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
body {
|
body {
|
||||||
background: #0a0e1a;
|
background: #0a0e1a;
|
||||||
@@ -91,21 +91,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#last-update { font-size: 0.7rem; color: #2e4060; }
|
#last-update { font-size: 0.7rem; color: #2e4060; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header>
|
||||||
<header>
|
|
||||||
<h1>FM95 Processor</h1>
|
<h1>FM95 Processor</h1>
|
||||||
<p>Live Status Monitor</p>
|
<p>Live Status Monitor</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="status-bar">
|
<div id="status-bar">
|
||||||
<div id="status-dot" class="connecting"></div>
|
<div id="status-dot" class="connecting"></div>
|
||||||
<span id="status-text">Connecting…</span>
|
<span id="status-text">Connecting…</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div class="card" style="--accent:#a78bfa">
|
<div class="card" style="--accent:#a78bfa">
|
||||||
<div class="card-label">MPX Power</div>
|
<div class="card-label">MPX Power</div>
|
||||||
<div class="card-value" id="mpx_power">—<span class="card-unit">dB</span></div>
|
<div class="card-value" id="mpx_power">—<span class="card-unit">dB</span></div>
|
||||||
@@ -124,10 +123,10 @@
|
|||||||
<div class="bar-wrap"><div class="bar-fill" id="bar_agc_gain"></div></div>
|
<div class="bar-wrap"><div class="bar-fill" id="bar_agc_gain"></div></div>
|
||||||
<div class="card-desc">Automatic gain control · linear</div>
|
<div class="card-desc">Automatic gain control · linear</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Oscilloscope -->
|
<!-- Oscilloscope -->
|
||||||
<div class="scope-wrap">
|
<div class="scope-wrap">
|
||||||
<div class="scope-header">
|
<div class="scope-header">
|
||||||
<div class="scope-title">Oscilloscope — Input & Audio Level</div>
|
<div class="scope-title">Oscilloscope — Input & Audio Level</div>
|
||||||
<div class="scope-legend">
|
<div class="scope-legend">
|
||||||
@@ -145,42 +144,34 @@
|
|||||||
<option value="400">400 pts</option>
|
<option value="400">400 pts</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>Y scale:
|
|
||||||
<select id="yscaleSel">
|
|
||||||
<option value="1">-1 – 1</option>
|
|
||||||
<option value="2">-2 – 2</option>
|
|
||||||
<option value="auto" selected>Auto</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label style="margin-left:auto; display:flex; align-items:center; gap:6px;">
|
<label style="margin-left:auto; display:flex; align-items:center; gap:6px;">
|
||||||
<input type="checkbox" id="gridCb" checked> Grid
|
<input type="checkbox" id="gridCb" checked> Grid
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="last-update">Waiting for data…</div>
|
<div id="last-update">Waiting for data…</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const POLL_MSG = JSON.stringify({ action: "fm95", data: "/w==" });
|
const POLL_MSG = JSON.stringify({ action: "fm95", data: "/w==" });
|
||||||
const FIELDS = ['mpx_power','bs412_gain','agc_gain','input_level','audio_level'];
|
const FIELDS = ['mpx_power','bs412_gain','agc_gain','input_level','audio_level'];
|
||||||
const FIELD_CFG = {
|
const FIELD_CFG = {
|
||||||
mpx_power: { unit:'dB', fmt: v=>v.toFixed(3), bar: v=>Math.max(0,Math.min(100,(v/3)*100)), warn: v=>v>3 },
|
mpx_power: { unit:'dB', fmt: v=>v.toFixed(3), bar: v=>Math.max(0,Math.min(100,(v/3)*100)), warn: v=>v>3 },
|
||||||
bs412_gain: { unit:'x', fmt: v=>v.toFixed(4), bar: v=>Math.max(0,Math.min(100,v*10)), warn: v=>v>2 },
|
bs412_gain: { unit:'x', fmt: v=>v.toFixed(4), bar: v=>Math.max(0,Math.min(100,v*10)), warn: v=>v>2 },
|
||||||
agc_gain: { unit:'x', fmt: v=>v.toFixed(4), bar: v=>Math.max(0,Math.min(100,v*10)), warn: v=>v>3 },
|
agc_gain: { unit:'x', fmt: v=>v.toFixed(4), bar: v=>Math.max(0,Math.min(100,v*10)), warn: v=>v>3 },
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_PTS = 400;
|
const MAX_PTS = 400;
|
||||||
let histInput = new Array(MAX_PTS).fill(null);
|
let histInput = new Array(MAX_PTS).fill(null);
|
||||||
let histAudio = new Array(MAX_PTS).fill(null);
|
let histAudio = new Array(MAX_PTS).fill(null);
|
||||||
let histAgc = new Array(MAX_PTS).fill(null);
|
let histAgc = new Array(MAX_PTS).fill(null);
|
||||||
|
|
||||||
const canvas = document.getElementById('scope');
|
const canvas = document.getElementById('scope');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const histSel = document.getElementById('histSel');
|
const histSel = document.getElementById('histSel');
|
||||||
const yscaleSel= document.getElementById('yscaleSel');
|
const gridCb = document.getElementById('gridCb');
|
||||||
const gridCb = document.getElementById('gridCb');
|
|
||||||
|
|
||||||
function resizeCanvas() {
|
function resizeCanvas() {
|
||||||
const r = window.devicePixelRatio || 1;
|
const r = window.devicePixelRatio || 1;
|
||||||
const w = canvas.parentElement.clientWidth - 36;
|
const w = canvas.parentElement.clientWidth - 36;
|
||||||
canvas.style.width = w + 'px';
|
canvas.style.width = w + 'px';
|
||||||
@@ -188,27 +179,20 @@ function resizeCanvas() {
|
|||||||
canvas.width = w * r;
|
canvas.width = w * r;
|
||||||
canvas.height = 200 * r;
|
canvas.height = 200 * r;
|
||||||
ctx.scale(r, r);
|
ctx.scale(r, r);
|
||||||
}
|
} resizeCanvas();
|
||||||
resizeCanvas();
|
window.addEventListener('resize', () => { ctx.setTransform(1,0,0,1,0,0); resizeCanvas(); drawScope(); });
|
||||||
window.addEventListener('resize', () => { ctx.setTransform(1,0,0,1,0,0); resizeCanvas(); drawScope(); });
|
|
||||||
|
|
||||||
function drawScope() {
|
function drawScope() {
|
||||||
const W = canvas.width / (window.devicePixelRatio || 1);
|
const W = canvas.width / (window.devicePixelRatio || 1);
|
||||||
const H = canvas.height / (window.devicePixelRatio || 1);
|
const H = canvas.height / (window.devicePixelRatio || 1);
|
||||||
const nPts = parseInt(histSel.value);
|
const nPts = parseInt(histSel.value);
|
||||||
const yscale = yscaleSel.value;
|
|
||||||
const showGrid = gridCb.checked;
|
const showGrid = gridCb.checked;
|
||||||
|
|
||||||
ctx.clearRect(0, 0, W, H);
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
|
||||||
// background
|
|
||||||
ctx.fillStyle = '#060b14';
|
ctx.fillStyle = '#060b14';
|
||||||
ctx.fillRect(0, 0, W, H);
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
// determine y range
|
|
||||||
let yMin = -1, yMax = 1;
|
|
||||||
if (yscale === '2') { yMin = -2; yMax = 2; }
|
|
||||||
else if (yscale === 'auto') {
|
|
||||||
const vals = [...histInput, ...histAudio, ...histAgc].filter(v => v !== null);
|
const vals = [...histInput, ...histAudio, ...histAgc].filter(v => v !== null);
|
||||||
if (vals.length) {
|
if (vals.length) {
|
||||||
yMin = Math.min(...vals);
|
yMin = Math.min(...vals);
|
||||||
@@ -216,7 +200,6 @@ function drawScope() {
|
|||||||
const pad = (yMax - yMin) * 0.1 || 0.05;
|
const pad = (yMax - yMin) * 0.1 || 0.05;
|
||||||
yMin -= pad; yMax += pad;
|
yMin -= pad; yMax += pad;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const toY = v => H - ((v - yMin) / (yMax - yMin)) * H;
|
const toY = v => H - ((v - yMin) / (yMax - yMin)) * H;
|
||||||
|
|
||||||
@@ -273,18 +256,18 @@ function drawScope() {
|
|||||||
drawLine(histInput, '#f472b6');
|
drawLine(histInput, '#f472b6');
|
||||||
drawLine(histAudio, '#38bdf8');
|
drawLine(histAudio, '#38bdf8');
|
||||||
drawLine(histAgc, '#fb923c');
|
drawLine(histAgc, '#fb923c');
|
||||||
}
|
}
|
||||||
|
|
||||||
function decode(b64) {
|
function decode(b64) {
|
||||||
const bin = atob(b64);
|
const bin = atob(b64);
|
||||||
const buf = new ArrayBuffer(bin.length);
|
const buf = new ArrayBuffer(bin.length);
|
||||||
const u8 = new Uint8Array(buf);
|
const u8 = new Uint8Array(buf);
|
||||||
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
||||||
const view = new DataView(buf);
|
const view = new DataView(buf);
|
||||||
return FIELDS.map((_, i) => view.getFloat32(i * 4, true));
|
return FIELDS.map((_, i) => view.getFloat32(i * 4, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI(values) {
|
function updateUI(values) {
|
||||||
values.forEach((v, i) => {
|
values.forEach((v, i) => {
|
||||||
const key = FIELDS[i];
|
const key = FIELDS[i];
|
||||||
const cfg = FIELD_CFG[key];
|
const cfg = FIELD_CFG[key];
|
||||||
@@ -304,22 +287,20 @@ function updateUI(values) {
|
|||||||
drawScope();
|
drawScope();
|
||||||
|
|
||||||
document.getElementById('last-update').textContent = `Last update: ${new Date().toLocaleTimeString()}`;
|
document.getElementById('last-update').textContent = `Last update: ${new Date().toLocaleTimeString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
histSel.addEventListener('change', drawScope);
|
histSel.addEventListener('change', drawScope);
|
||||||
yscaleSel.addEventListener('change', drawScope);
|
gridCb.addEventListener('change', drawScope);
|
||||||
gridCb.addEventListener('change', drawScope);
|
|
||||||
|
|
||||||
// --- WebSocket ---
|
function setStatus(state, text) {
|
||||||
function setStatus(state, text) {
|
|
||||||
const dot = document.getElementById('status-dot');
|
const dot = document.getElementById('status-dot');
|
||||||
dot.className = '';
|
dot.className = '';
|
||||||
dot.classList.add(state);
|
dot.classList.add(state);
|
||||||
document.getElementById('status-text').textContent = text;
|
document.getElementById('status-text').textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ws, pollTimer;
|
let ws, pollTimer;
|
||||||
function connect() {
|
function connect() {
|
||||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
ws = new WebSocket(`${proto}://${location.host}/ws`);
|
ws = new WebSocket(`${proto}://${location.host}/ws`);
|
||||||
setStatus('connecting', 'Connecting…');
|
setStatus('connecting', 'Connecting…');
|
||||||
@@ -341,11 +322,11 @@ function connect() {
|
|||||||
setTimeout(connect, 3000);
|
setTimeout(connect, 3000);
|
||||||
});
|
});
|
||||||
ws.addEventListener('error', () => setStatus('error', 'WebSocket error'));
|
ws.addEventListener('error', () => setStatus('error', 'WebSocket error'));
|
||||||
}
|
}
|
||||||
function poll() { if (ws && ws.readyState === WebSocket.OPEN) ws.send(POLL_MSG); }
|
function poll() { if (ws && ws.readyState === WebSocket.OPEN) ws.send(POLL_MSG); }
|
||||||
|
|
||||||
connect();
|
connect();
|
||||||
drawScope();
|
drawScope();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user