mirror of
https://github.com/radio95-rnt/RadioPlayer.git
synced 2026-07-30 15:59:16 +02:00
claude web ui >> claude code (blik payment?)
This commit is contained in:
+250
-178
@@ -6,7 +6,6 @@
|
|||||||
<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;
|
||||||
color: #e0e6f0;
|
color: #e0e6f0;
|
||||||
@@ -17,135 +16,81 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 30px 16px;
|
padding: 30px 16px;
|
||||||
}
|
}
|
||||||
|
header { text-align: center; margin-bottom: 22px; }
|
||||||
header {
|
header h1 { font-size: 1.5rem; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; color: #7eb8f7; }
|
||||||
text-align: center;
|
header p { font-size: 0.75rem; color: #4a5a7a; margin-top: 4px; letter-spacing: 1px; text-transform: uppercase; }
|
||||||
margin-bottom: 32px;
|
#status-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 22px; font-size: 0.8rem; color: #4a5a7a; }
|
||||||
}
|
#status-dot { width: 9px; height: 9px; border-radius: 50%; background: #444; transition: background 0.3s; }
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #7eb8f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
header p {
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: #4a5a7a;
|
|
||||||
margin-top: 4px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #4a5a7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-dot {
|
|
||||||
width: 9px;
|
|
||||||
height: 9px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #444;
|
|
||||||
transition: background 0.3s;
|
|
||||||
}
|
|
||||||
#status-dot.connecting { background: #f0a500; box-shadow: 0 0 6px #f0a500; }
|
#status-dot.connecting { background: #f0a500; box-shadow: 0 0 6px #f0a500; }
|
||||||
#status-dot.connected { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; }
|
#status-dot.connected { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; }
|
||||||
#status-dot.error { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; }
|
#status-dot.error { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; }
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
gap: 16px;
|
gap: 14px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 900px;
|
max-width: 960px;
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
background: #111827;
|
background: #111827;
|
||||||
border: 1px solid #1e2d45;
|
border: 1px solid #1e2d45;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 20px 22px;
|
padding: 16px 18px;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: border-color 0.3s;
|
|
||||||
}
|
}
|
||||||
.card:hover { border-color: #2e4a70; }
|
|
||||||
|
|
||||||
.card::before {
|
.card::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: 0; right: 0;
|
top: 0; left: 0; right: 0;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: var(--accent, #7eb8f7);
|
background: var(--accent, #7eb8f7);
|
||||||
opacity: 0.7;
|
|
||||||
border-radius: 12px 12px 0 0;
|
border-radius: 12px 12px 0 0;
|
||||||
}
|
}
|
||||||
|
.card-label { font-size: 0.68rem; text-transform: uppercase; letter-spacing: 1.5px; color: #4a6080; margin-bottom: 8px; }
|
||||||
|
.card-value { font-size: 1.8rem; font-weight: 700; color: var(--accent, #7eb8f7); line-height: 1; font-variant-numeric: tabular-nums; }
|
||||||
|
.card-unit { font-size: 0.8rem; color: #3a5070; margin-left: 3px; font-weight: 400; }
|
||||||
|
.bar-wrap { margin-top: 10px; background: #0a0e1a; border-radius: 4px; height: 5px; overflow: hidden; }
|
||||||
|
.bar-fill { height: 100%; border-radius: 4px; background: var(--accent, #7eb8f7); width: 0%; transition: width 0.4s ease; }
|
||||||
|
.card-desc { font-size: 0.68rem; color: #2e4060; margin-top: 6px; }
|
||||||
|
|
||||||
.card-label {
|
/* Oscilloscope */
|
||||||
font-size: 0.7rem;
|
.scope-wrap {
|
||||||
text-transform: uppercase;
|
width: 100%;
|
||||||
letter-spacing: 1.5px;
|
max-width: 960px;
|
||||||
color: #4a6080;
|
background: #0b1120;
|
||||||
margin-bottom: 10px;
|
border: 1px solid #1e2d45;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.scope-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.scope-title { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1.5px; color: #4a6080; }
|
||||||
|
.scope-legend { display: flex; gap: 18px; }
|
||||||
|
.scope-legend span { font-size: 0.7rem; display: flex; align-items: center; gap: 5px; }
|
||||||
|
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||||
|
canvas#scope {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #060b14;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
}
|
||||||
|
.scope-controls { display: flex; gap: 16px; margin-top: 10px; align-items: center; }
|
||||||
|
.scope-controls label { font-size: 0.72rem; color: #4a6080; }
|
||||||
|
.scope-controls select, .scope-controls input {
|
||||||
|
background: #111827; color: #7eb8f7; border: 1px solid #1e2d45;
|
||||||
|
border-radius: 6px; padding: 3px 8px; font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-value {
|
#last-update { font-size: 0.7rem; color: #2e4060; }
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--accent, #7eb8f7);
|
|
||||||
line-height: 1;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
transition: color 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-unit {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #3a5070;
|
|
||||||
margin-left: 4px;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-wrap {
|
|
||||||
margin-top: 14px;
|
|
||||||
background: #0a0e1a;
|
|
||||||
border-radius: 4px;
|
|
||||||
height: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar-fill {
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 4px;
|
|
||||||
background: var(--accent, #7eb8f7);
|
|
||||||
width: 0%;
|
|
||||||
transition: width 0.5s ease, background 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-desc {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
color: #2e4060;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#last-update {
|
|
||||||
margin-top: 28px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
color: #2e4060;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.5; }
|
|
||||||
}
|
|
||||||
.pulse { animation: pulse 1s ease-in-out; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -167,28 +112,24 @@
|
|||||||
<div class="bar-wrap"><div class="bar-fill" id="bar_mpx_power"></div></div>
|
<div class="bar-wrap"><div class="bar-fill" id="bar_mpx_power"></div></div>
|
||||||
<div class="card-desc">Multiplex signal power · dBr</div>
|
<div class="card-desc">Multiplex signal power · dBr</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="--accent:#34d399">
|
<div class="card" style="--accent:#34d399">
|
||||||
<div class="card-label">BS412 Gain</div>
|
<div class="card-label">BS412 Gain</div>
|
||||||
<div class="card-value" id="bs412_gain">—</div>
|
<div class="card-value" id="bs412_gain">—</div>
|
||||||
<div class="bar-wrap"><div class="bar-fill" id="bar_bs412_gain"></div></div>
|
<div class="bar-wrap"><div class="bar-fill" id="bar_bs412_gain"></div></div>
|
||||||
<div class="card-desc">BS412 loudness gain · linear</div>
|
<div class="card-desc">BS412 loudness gain · linear</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="--accent:#fb923c">
|
<div class="card" style="--accent:#fb923c">
|
||||||
<div class="card-label">AGC Gain</div>
|
<div class="card-label">AGC Gain</div>
|
||||||
<div class="card-value" id="agc_gain">—</div>
|
<div class="card-value" id="agc_gain">—</div>
|
||||||
<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 class="card" style="--accent:#f472b6">
|
<div class="card" style="--accent:#f472b6">
|
||||||
<div class="card-label">Input Level</div>
|
<div class="card-label">Input Level</div>
|
||||||
<div class="card-value" id="input_level">—</div>
|
<div class="card-value" id="input_level">—</div>
|
||||||
<div class="bar-wrap"><div class="bar-fill" id="bar_input_level"></div></div>
|
<div class="bar-wrap"><div class="bar-fill" id="bar_input_level"></div></div>
|
||||||
<div class="card-desc">Incoming signal level · linear raw</div>
|
<div class="card-desc">Incoming signal level · linear raw</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" style="--accent:#38bdf8">
|
<div class="card" style="--accent:#38bdf8">
|
||||||
<div class="card-label">Audio Level</div>
|
<div class="card-label">Audio Level</div>
|
||||||
<div class="card-value" id="audio_level">—</div>
|
<div class="card-value" id="audio_level">—</div>
|
||||||
@@ -197,94 +138,225 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Oscilloscope -->
|
||||||
|
<div class="scope-wrap">
|
||||||
|
<div class="scope-header">
|
||||||
|
<div class="scope-title">Oscilloscope — Input & Audio Level</div>
|
||||||
|
<div class="scope-legend">
|
||||||
|
<span><span class="dot" style="background:#f472b6"></span>Input Level</span>
|
||||||
|
<span><span class="dot" style="background:#38bdf8"></span>Audio Level</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="scope"></canvas>
|
||||||
|
<div class="scope-controls">
|
||||||
|
<label>History:
|
||||||
|
<select id="histSel">
|
||||||
|
<option value="100">100 pts</option>
|
||||||
|
<option value="200" selected>200 pts</option>
|
||||||
|
<option value="400">400 pts</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>Y scale:
|
||||||
|
<select id="yscaleSel">
|
||||||
|
<option value="1" selected>0 – 1</option>
|
||||||
|
<option value="2">0 – 2</option>
|
||||||
|
<option value="auto">Auto</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label style="margin-left:auto; display:flex; align-items:center; gap:6px;">
|
||||||
|
<input type="checkbox" id="gridCb" checked> Grid
|
||||||
|
</label>
|
||||||
|
</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 = {
|
||||||
|
mpx_power: { unit:'dB', fmt: v=>v.toFixed(2), bar: v=>Math.max(0,Math.min(100,((v+40)/40)*100)), warn: v=>v>-1 },
|
||||||
|
bs412_gain: { unit:'', fmt: v=>v.toFixed(4), bar: v=>Math.max(0,Math.min(100,v*50)), warn: v=>v>1.9 },
|
||||||
|
agc_gain: { unit:'', fmt: v=>v.toFixed(4), bar: v=>Math.max(0,Math.min(100,v*50)), warn: v=>v>1.9 },
|
||||||
|
input_level: { unit:'', fmt: v=>v.toFixed(5), bar: v=>Math.max(0,Math.min(100,v*100)), warn: v=>v>0.95 },
|
||||||
|
audio_level: { unit:'', fmt: v=>v.toFixed(5), bar: v=>Math.max(0,Math.min(100,v*100)), warn: v=>v>0.95 },
|
||||||
|
};
|
||||||
|
|
||||||
// Per-field display config: { unit, format, toBar, warnHigh }
|
// --- Oscilloscope state ---
|
||||||
const FIELD_CFG = {
|
const MAX_PTS = 400;
|
||||||
mpx_power: { unit: 'dB', fmt: v => v.toFixed(2), bar: v => Math.max(0, Math.min(100, ((v + 40) / 40) * 100)), warn: v => v > -1 },
|
let histInput = new Array(MAX_PTS).fill(null);
|
||||||
bs412_gain: { unit: '', fmt: v => v.toFixed(4), bar: v => Math.max(0, Math.min(100, v * 50)), warn: v => v > 1.9, label: 'BS412 Gain', sub: 'linear gain' },
|
let histAudio = new Array(MAX_PTS).fill(null);
|
||||||
agc_gain: { unit: '', fmt: v => v.toFixed(4), bar: v => Math.max(0, Math.min(100, v * 50)), warn: v => v > 1.9, label: 'AGC Gain', sub: 'linear gain' },
|
|
||||||
input_level:{ unit: '', fmt: v => v.toFixed(5), bar: v => Math.max(0, Math.min(100, v * 100)), warn: v => v > 0.95 },
|
|
||||||
audio_level:{ unit: '', fmt: v => v.toFixed(5), bar: v => Math.max(0, Math.min(100, v * 100)), warn: v => v > 0.95 },
|
|
||||||
};
|
|
||||||
|
|
||||||
function decode(b64) {
|
const canvas = document.getElementById('scope');
|
||||||
const bin = atob(b64);
|
const ctx = canvas.getContext('2d');
|
||||||
const buf = new ArrayBuffer(bin.length);
|
const histSel = document.getElementById('histSel');
|
||||||
const u8 = new Uint8Array(buf);
|
const yscaleSel= document.getElementById('yscaleSel');
|
||||||
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
const gridCb = document.getElementById('gridCb');
|
||||||
const view = new DataView(buf);
|
|
||||||
// 5 floats, little-endian, 4 bytes each
|
function resizeCanvas() {
|
||||||
return FIELDS.map((_, i) => view.getFloat32(i * 4, true));
|
const r = window.devicePixelRatio || 1;
|
||||||
|
const w = canvas.parentElement.clientWidth - 36;
|
||||||
|
canvas.style.width = w + 'px';
|
||||||
|
canvas.style.height = '200px';
|
||||||
|
canvas.width = w * r;
|
||||||
|
canvas.height = 200 * r;
|
||||||
|
ctx.scale(r, r);
|
||||||
|
}
|
||||||
|
resizeCanvas();
|
||||||
|
window.addEventListener('resize', () => { ctx.setTransform(1,0,0,1,0,0); resizeCanvas(); drawScope(); });
|
||||||
|
|
||||||
|
function drawScope() {
|
||||||
|
const W = canvas.width / (window.devicePixelRatio || 1);
|
||||||
|
const H = canvas.height / (window.devicePixelRatio || 1);
|
||||||
|
const nPts = parseInt(histSel.value);
|
||||||
|
const yscale = yscaleSel.value;
|
||||||
|
const showGrid = gridCb.checked;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, W, H);
|
||||||
|
|
||||||
|
// background
|
||||||
|
ctx.fillStyle = '#060b14';
|
||||||
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
|
// determine y range
|
||||||
|
let yMin = 0, yMax = 1;
|
||||||
|
if (yscale === '2') { yMax = 2; }
|
||||||
|
else if (yscale === 'auto') {
|
||||||
|
const vals = [...histInput, ...histAudio].filter(v => v !== null);
|
||||||
|
if (vals.length) {
|
||||||
|
yMin = Math.min(...vals);
|
||||||
|
yMax = Math.max(...vals);
|
||||||
|
const pad = (yMax - yMin) * 0.1 || 0.05;
|
||||||
|
yMin -= pad; yMax += pad;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI(values) {
|
const toY = v => H - ((v - yMin) / (yMax - yMin)) * H;
|
||||||
values.forEach((v, i) => {
|
|
||||||
const key = FIELDS[i];
|
// grid
|
||||||
const cfg = FIELD_CFG[key];
|
if (showGrid) {
|
||||||
const el = document.getElementById(key);
|
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
|
||||||
const bar = document.getElementById('bar_' + key);
|
ctx.lineWidth = 1;
|
||||||
el.innerHTML = `${cfg.fmt(v)}${cfg.unit ? `<span class="card-unit">${cfg.unit}</span>` : ''}`;
|
const hLines = 5;
|
||||||
bar.style.width = cfg.bar(v) + '%';
|
for (let i = 0; i <= hLines; i++) {
|
||||||
bar.style.background = cfg.warn(v) ? '#ef4444' : '';
|
const y = (i / hLines) * H;
|
||||||
});
|
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(W, y); ctx.stroke();
|
||||||
const now = new Date().toLocaleTimeString();
|
const val = yMax - (i / hLines) * (yMax - yMin);
|
||||||
document.getElementById('last-update').textContent = `Last update: ${now}`;
|
ctx.fillStyle = 'rgba(255,255,255,0.25)';
|
||||||
|
ctx.font = '10px monospace';
|
||||||
|
ctx.fillText(val.toFixed(2), 4, y - 3);
|
||||||
|
}
|
||||||
|
const vLines = 8;
|
||||||
|
for (let i = 1; i < vLines; i++) {
|
||||||
|
const x = (i / vLines) * W;
|
||||||
|
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, H); ctx.stroke();
|
||||||
|
}
|
||||||
|
// zero line
|
||||||
|
if (yMin < 0 && yMax > 0) {
|
||||||
|
const y0 = toY(0);
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
|
||||||
|
ctx.setLineDash([4,4]);
|
||||||
|
ctx.beginPath(); ctx.moveTo(0, y0); ctx.lineTo(W, y0); ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(state, text) {
|
// draw series
|
||||||
const dot = document.getElementById('status-dot');
|
function drawLine(hist, color) {
|
||||||
const span = document.getElementById('status-text');
|
const slice = hist.slice(-nPts);
|
||||||
dot.className = 'status-dot ' + state;
|
const step = W / (nPts - 1);
|
||||||
dot.id = 'status-dot';
|
ctx.strokeStyle = color;
|
||||||
dot.classList.add(state);
|
ctx.lineWidth = 1.5;
|
||||||
span.textContent = text;
|
ctx.beginPath();
|
||||||
|
let started = false;
|
||||||
|
for (let i = 0; i < slice.length; i++) {
|
||||||
|
if (slice[i] === null) { started = false; continue; }
|
||||||
|
const x = i * step;
|
||||||
|
const y = toY(slice[i]);
|
||||||
|
if (!started) { ctx.moveTo(x, y); started = true; }
|
||||||
|
else ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
// glow
|
||||||
|
ctx.strokeStyle = color + '44';
|
||||||
|
ctx.lineWidth = 4;
|
||||||
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
let ws, pollTimer;
|
drawLine(histInput, '#f472b6');
|
||||||
|
drawLine(histAudio, '#38bdf8');
|
||||||
|
}
|
||||||
|
|
||||||
function connect() {
|
function decode(b64) {
|
||||||
setStatus('connecting', 'Connecting…');
|
const bin = atob(b64);
|
||||||
|
const buf = new ArrayBuffer(bin.length);
|
||||||
|
const u8 = new Uint8Array(buf);
|
||||||
|
for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
|
||||||
|
const view = new DataView(buf);
|
||||||
|
return FIELDS.map((_, i) => view.getFloat32(i * 4, true));
|
||||||
|
}
|
||||||
|
|
||||||
ws = new WebSocket("/ws");
|
function updateUI(values) {
|
||||||
|
values.forEach((v, i) => {
|
||||||
|
const key = FIELDS[i];
|
||||||
|
const cfg = FIELD_CFG[key];
|
||||||
|
const el = document.getElementById(key);
|
||||||
|
const bar = document.getElementById('bar_' + key);
|
||||||
|
el.innerHTML = `${cfg.fmt(v)}${cfg.unit ? `<span class="card-unit">${cfg.unit}</span>` : ''}`;
|
||||||
|
bar.style.width = cfg.bar(v) + '%';
|
||||||
|
bar.style.background = cfg.warn(v) ? '#ef4444' : '';
|
||||||
|
});
|
||||||
|
|
||||||
ws.addEventListener('open', () => {
|
// push to oscilloscope history
|
||||||
setStatus('connected', 'Connected');
|
const inputVal = values[3];
|
||||||
poll();
|
const audioVal = values[4];
|
||||||
pollTimer = setInterval(poll, 1000);
|
histInput.push(inputVal); if (histInput.length > MAX_PTS) histInput.shift();
|
||||||
});
|
histAudio.push(audioVal); if (histAudio.length > MAX_PTS) histAudio.shift();
|
||||||
|
drawScope();
|
||||||
|
|
||||||
ws.addEventListener('message', e => {
|
document.getElementById('last-update').textContent = `Last update: ${new Date().toLocaleTimeString()}`;
|
||||||
try {
|
}
|
||||||
const msg = JSON.parse(e.data);
|
|
||||||
if (msg.event === 'fm95' && msg.data) {
|
|
||||||
updateUI(decode(msg.data));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Parse error', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener('close', () => {
|
histSel.addEventListener('change', drawScope);
|
||||||
clearInterval(pollTimer);
|
yscaleSel.addEventListener('change', drawScope);
|
||||||
setStatus('error', 'Disconnected — retrying in 3 s…');
|
gridCb.addEventListener('change', drawScope);
|
||||||
setTimeout(connect, 3000);
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener('error', () => {
|
// --- WebSocket ---
|
||||||
setStatus('error', 'WebSocket error');
|
function setStatus(state, text) {
|
||||||
});
|
const dot = document.getElementById('status-dot');
|
||||||
}
|
dot.className = '';
|
||||||
|
dot.classList.add(state);
|
||||||
|
document.getElementById('status-text').textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
function poll() {
|
let ws, pollTimer;
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(POLL_MSG);
|
function connect() {
|
||||||
}
|
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
ws = new WebSocket(`${proto}://${location.host}/ws`);
|
||||||
|
setStatus('connecting', 'Connecting…');
|
||||||
|
|
||||||
connect();
|
ws.addEventListener('open', () => {
|
||||||
|
setStatus('connected', 'Connected');
|
||||||
|
poll();
|
||||||
|
pollTimer = setInterval(poll, 1000);
|
||||||
|
});
|
||||||
|
ws.addEventListener('message', e => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
if (msg.event === 'fm95' && msg.data) updateUI(decode(msg.data));
|
||||||
|
} catch(err) { console.warn('Parse error', err); }
|
||||||
|
});
|
||||||
|
ws.addEventListener('close', () => {
|
||||||
|
clearInterval(pollTimer);
|
||||||
|
setStatus('error', 'Disconnected — retrying in 3s…');
|
||||||
|
setTimeout(connect, 3000);
|
||||||
|
});
|
||||||
|
ws.addEventListener('error', () => setStatus('error', 'WebSocket error'));
|
||||||
|
}
|
||||||
|
function poll() { if (ws && ws.readyState === WebSocket.OPEN) ws.send(POLL_MSG); }
|
||||||
|
|
||||||
|
connect();
|
||||||
|
drawScope();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user