[feat](trx-rs): add CCI/ACI bars to WFM panel with RDS mitigation

Estimate Co-Channel Interference (CCI) from pilot tone quadrature
leakage and coherence degradation.  Estimate Adjacent Channel
Interference (ACI) from CMA equalizer tap deviation from identity.
Both metrics (0-100 scale) are surfaced through RigFilterState and
displayed as colour-coded bars in the WFM control panel.

The RDS decoder quality parameter is now adaptively penalised when
CCI/ACI levels are elevated, reducing block-error rate under
interference conditions.

https://claude.ai/code/session_016EKzep42RCvE4GxvvRaCwu
Signed-off-by: Claude <noreply@anthropic.com>
This commit is contained in:
Claude
2026-03-27 08:17:34 +00:00
committed by Stan Grams
parent 6b33550116
commit 4da4d8ec66
8 changed files with 180 additions and 9 deletions
@@ -1701,6 +1701,7 @@ function resetDecoderStateOnRigSwitch() {
vchanRdsById = new Map();
resetRdsDisplay();
resetWfmStereoIndicator();
resetIntfBars();
// Spectrum — clear stale data from previous rig's SDR
lastSpectrumData = null;
@@ -1728,6 +1729,23 @@ function resetWfmStereoIndicator() {
wfmStFlagEl.classList.add("wfm-st-flag-mono");
}
function updateIntfBar(fillEl, valEl, level) {
if (!fillEl || !valEl) return;
const v = Math.round(Math.min(Math.max(level, 0), 100));
valEl.textContent = String(v);
fillEl.style.width = v + "%";
fillEl.classList.toggle("wfm-intf-warn", v >= 35 && v < 65);
fillEl.classList.toggle("wfm-intf-high", v >= 65);
if (v < 35) {
fillEl.classList.remove("wfm-intf-warn", "wfm-intf-high");
}
}
function resetIntfBars() {
updateIntfBar(wfmCciFillEl, wfmCciValEl, 0);
updateIntfBar(wfmAciFillEl, wfmAciValEl, 0);
}
// ── Fast CSS-based frequency/BW marker positioning ──────────────────────────
// These lightweight DOM elements reposition via `transform: translateX()`
// which is GPU-composited — zero layout/paint cost. The full WebGL overlay
@@ -1800,6 +1818,7 @@ function applyLocalTunedFrequency(hz, forceDisplay = false) {
primaryRds = null;
resetRdsDisplay();
resetWfmStereoIndicator();
resetIntfBars();
}
lastFreqHz = hz;
window.lastFreqHz = lastFreqHz;
@@ -2955,6 +2974,8 @@ function render(update) {
wfmStFlagEl.classList.toggle("wfm-st-flag-stereo", detected);
wfmStFlagEl.classList.toggle("wfm-st-flag-mono", !detected);
}
if (typeof update.filter.wfm_cci === "number") updateIntfBar(wfmCciFillEl, wfmCciValEl, update.filter.wfm_cci);
if (typeof update.filter.wfm_aci === "number") updateIntfBar(wfmAciFillEl, wfmAciValEl, update.filter.wfm_aci);
if (samStereoWidthEl && typeof update.filter.sam_stereo_width === "number") {
samStereoWidthEl.value = String(Math.round(update.filter.sam_stereo_width * 100));
}
@@ -7494,6 +7515,10 @@ const sdrLnaGainEl = document.getElementById("sdr-lna-gain-db");
const sdrLnaGainSetBtn = document.getElementById("sdr-lna-gain-set");
const sdrAgcEl = document.getElementById("sdr-agc-enabled");
const wfmStFlagEl = document.getElementById("wfm-st-flag");
const wfmCciFillEl = document.getElementById("wfm-cci-fill");
const wfmCciValEl = document.getElementById("wfm-cci-val");
const wfmAciFillEl = document.getElementById("wfm-aci-fill");
const wfmAciValEl = document.getElementById("wfm-aci-val");
const samControlsCol = document.getElementById("sam-controls-col");
const samStereoWidthEl = document.getElementById("sam-stereo-width");
const samCarrierSyncEl = document.getElementById("sam-carrier-sync");
@@ -250,6 +250,14 @@
<span class="wfm-control-label">Pilot</span>
<span id="wfm-st-flag" class="wfm-st-flag wfm-st-flag-mono">MO</span>
</label>
<label class="wfm-control wfm-intf-bar-wrap" aria-label="Co-Channel Interference">
<span class="wfm-control-label">CCI</span>
<span class="wfm-intf-bar"><span id="wfm-cci-fill" class="wfm-intf-fill"></span><span id="wfm-cci-val" class="wfm-intf-val">0</span></span>
</label>
<label class="wfm-control wfm-intf-bar-wrap" aria-label="Adjacent Channel Interference">
<span class="wfm-control-label">ACI</span>
<span class="wfm-intf-bar"><span id="wfm-aci-fill" class="wfm-intf-fill"></span><span id="wfm-aci-val" class="wfm-intf-val">0</span></span>
</label>
</div>
<div class="label"><span>WFM</span></div>
</div>
@@ -265,6 +265,47 @@ input.status-input, select.status-input { width: 100%; padding: 0.45rem 0.5rem;
border-color: var(--border-light);
background: color-mix(in srgb, var(--input-bg) 92%, var(--panel-2));
}
.wfm-intf-bar-wrap {
min-width: 3.6rem;
}
.wfm-intf-bar {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-width: 3.6rem;
min-height: 2.1rem;
padding: 0;
box-sizing: border-box;
border: 1px solid var(--border-light);
border-radius: 6px;
overflow: hidden;
background: var(--input-bg);
}
.wfm-intf-fill {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0%;
border-radius: 5px 0 0 5px;
background: color-mix(in srgb, #4fc3f7 45%, transparent);
transition: width 0.25s ease, background 0.35s ease;
}
.wfm-intf-fill.wfm-intf-warn {
background: color-mix(in srgb, #ffa726 55%, transparent);
}
.wfm-intf-fill.wfm-intf-high {
background: color-mix(in srgb, #ef5350 55%, transparent);
}
.wfm-intf-val {
position: relative;
z-index: 1;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--text-primary);
}
.controls-col-center::after {
content: "";
display: block;