[feat](trx-frontend-http): add RDS debug panel in Plugins tab

Add an RDS sub-tab to the Plugins panel showing PI code, PS name, PTY
number and name, decoder status, and a raw JSON dump of the latest RDS
data received via SSE. Also list the RDS decoder in the Overview tab.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-02-28 09:16:15 +01:00
parent 8b3951d99d
commit f7367e6270
3 changed files with 65 additions and 7 deletions
@@ -3103,14 +3103,44 @@ function clearSpectrumCanvas() {
} }
function updateRdsPsOverlay(rds) { function updateRdsPsOverlay(rds) {
if (!rdsPsOverlay) return; // Overview strip overlay
const ps = rds?.program_service?.trim(); if (rdsPsOverlay) {
if (ps) { const ps = rds?.program_service?.trim();
rdsPsOverlay.textContent = ps; if (ps) {
rdsPsOverlay.style.display = ""; rdsPsOverlay.textContent = ps;
} else { rdsPsOverlay.style.display = "";
rdsPsOverlay.style.display = "none"; } else {
rdsPsOverlay.style.display = "none";
}
} }
// RDS debug panel
const statusEl = document.getElementById("rds-status");
const piEl = document.getElementById("rds-pi");
const psEl = document.getElementById("rds-ps");
const ptyEl = document.getElementById("rds-pty");
const ptyNameEl = document.getElementById("rds-pty-name");
const rawEl = document.getElementById("rds-raw");
if (!statusEl) return;
if (!rds) {
statusEl.textContent = "No signal";
statusEl.className = "rds-value rds-no-signal";
piEl.textContent = "--";
psEl.textContent = "--";
ptyEl.textContent = "--";
ptyNameEl.textContent = "--";
rawEl.textContent = "--";
return;
}
statusEl.textContent = "Decoding";
statusEl.className = "rds-value rds-decoding";
piEl.textContent = rds.pi != null ? `0x${rds.pi.toString(16).toUpperCase().padStart(4, "0")}` : "--";
psEl.textContent = rds.program_service ?? "--";
ptyEl.textContent = rds.pty != null ? String(rds.pty) : "--";
ptyNameEl.textContent = rds.pty_name ?? "--";
rawEl.textContent = JSON.stringify(rds, null, 2);
} }
function scheduleSpectrumDraw() { function scheduleSpectrumDraw() {
@@ -236,6 +236,7 @@
<button class="sub-tab" data-subtab="cw">CW</button> <button class="sub-tab" data-subtab="cw">CW</button>
<button class="sub-tab" data-subtab="ft8">FT8</button> <button class="sub-tab" data-subtab="ft8">FT8</button>
<button class="sub-tab" data-subtab="wspr">WSPR</button> <button class="sub-tab" data-subtab="wspr">WSPR</button>
<button class="sub-tab" data-subtab="rds">RDS</button>
<button class="sub-tab" data-subtab="map">Map</button> <button class="sub-tab" data-subtab="map">Map</button>
</div> </div>
<div id="subtab-overview" class="sub-tab-panel"> <div id="subtab-overview" class="sub-tab-panel">
@@ -263,6 +264,23 @@
Decodes WSPR messages from RX audio (DIG/USB only, toggle required). Decodes WSPR messages from RX audio (DIG/USB only, toggle required).
</div> </div>
</div> </div>
<div class="plugin-item">
<strong>RDS Decoder</strong>
<div style="color:var(--text-muted); font-size:0.85rem; margin-top:0.2rem;">
Decodes Radio Data System (RDS) metadata from WFM broadcasts (57 kHz subcarrier).
</div>
</div>
</div>
<div id="subtab-rds" class="sub-tab-panel" style="display:none;">
<div class="rds-grid">
<div class="rds-field"><span class="rds-label">Status</span><span id="rds-status" class="rds-value rds-no-signal">No signal</span></div>
<div class="rds-field"><span class="rds-label">PI</span><span id="rds-pi" class="rds-value">--</span></div>
<div class="rds-field"><span class="rds-label">PS</span><span id="rds-ps" class="rds-value rds-ps">--</span></div>
<div class="rds-field"><span class="rds-label">PTY</span><span id="rds-pty" class="rds-value">--</span></div>
<div class="rds-field"><span class="rds-label">PTY Name</span><span id="rds-pty-name" class="rds-value">--</span></div>
</div>
<div class="rds-raw-label">Raw JSON</div>
<pre id="rds-raw" class="rds-raw">--</pre>
</div> </div>
<div id="subtab-map" class="sub-tab-panel" style="display:none;"> <div id="subtab-map" class="sub-tab-panel" style="display:none;">
<div class="map-controls"> <div class="map-controls">
@@ -729,6 +729,16 @@ small { color: var(--text-muted); }
} }
.map-controls input[type="checkbox"] { margin-right: 0.3rem; } .map-controls input[type="checkbox"] { margin-right: 0.3rem; }
.rds-grid { display: grid; grid-template-columns: auto 1fr; gap: 0.4rem 1rem; align-items: baseline; margin-bottom: 1rem; }
.rds-field { display: contents; }
.rds-label { color: var(--text-muted); font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; }
.rds-value { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 0.95rem; color: var(--text); }
.rds-ps { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.08em; color: var(--accent-green); }
.rds-no-signal { color: var(--text-muted); }
.rds-decoding { color: var(--accent-green); }
.rds-raw-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 0.3rem; }
.rds-raw { background: var(--input-bg); border: 1px solid var(--border-light); border-radius: 6px; padding: 0.5rem 0.6rem; font-size: 0.8rem; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; color: var(--text); white-space: pre; overflow-x: auto; margin: 0; }
.cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; } .cw-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; }
.cw-config { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; } .cw-config { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
.cw-config label { display: flex; flex-direction: column; gap: 0.2rem; color: var(--text-muted); font-size: 0.82rem; } .cw-config label { display: flex; flex-direction: column; gap: 0.2rem; color: var(--text-muted); font-size: 0.82rem; }