This commit is contained in:
2026-05-10 12:24:36 +02:00
parent 703b08b1b7
commit 897b156a53
2 changed files with 306 additions and 326 deletions
+4 -5
View File
@@ -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
View File
@@ -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 &amp; Audio Level</div> <div class="scope-title">Oscilloscope — Input &amp; 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>