[fix](trx-rs): GQRX-style S-meter ballistics across DSP and frontend
DSP: 400 ms attack / 1.0 s decay IIR on IQ power (block-rate corrected). JS: asymmetric EMA (α=0.08 attack, α=0.03 decay) with rAF coalescing. CSS: bar transition 150 ms → 300 ms ease-out. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
@@ -7006,9 +7006,18 @@ function stopSpectrumStreaming() {
|
||||
|
||||
// ── /meter (fast signal-strength) streaming ─────────────────────────────────
|
||||
// Dedicated SSE channel pushed at ~30 Hz by trx-server; bypasses /events so
|
||||
// meter frames are never gated by full-RigState diffing. Synchronous DOM
|
||||
// write per frame — no rAF coalescing, per user requirement that it "feel
|
||||
// instant" on the frontend.
|
||||
// meter frames are never gated by full-RigState diffing.
|
||||
//
|
||||
// Client-side asymmetric EMA smoothing (GQRX-style ballistics):
|
||||
// attack τ ≈ 400 ms — rises in ~12 frames at 30 Hz
|
||||
// decay τ ≈ 1.0 s — falls in ~30 frames, readable
|
||||
// DOM updates are coalesced via requestAnimationFrame so the bar
|
||||
// animates at display refresh rate, not SSE rate.
|
||||
const METER_ATTACK_ALPHA = 0.08; // per-frame at ~30 Hz ≈ 400 ms τ
|
||||
const METER_DECAY_ALPHA = 0.03; // per-frame at ~30 Hz ≈ 1.0 s τ
|
||||
let meterSmoothedDbm = null;
|
||||
let meterRafPending = false;
|
||||
|
||||
function scheduleMeterReconnect() {
|
||||
if (meterReconnectTimer !== null) return;
|
||||
meterReconnectTimer = setTimeout(() => {
|
||||
@@ -7019,6 +7028,24 @@ function scheduleMeterReconnect() {
|
||||
|
||||
function applyMeterSample(dbm) {
|
||||
if (typeof dbm !== "number" || !Number.isFinite(dbm)) return;
|
||||
// Asymmetric EMA: fast attack, slow decay.
|
||||
if (meterSmoothedDbm === null) {
|
||||
meterSmoothedDbm = dbm;
|
||||
} else {
|
||||
const alpha = dbm > meterSmoothedDbm ? METER_ATTACK_ALPHA : METER_DECAY_ALPHA;
|
||||
meterSmoothedDbm += alpha * (dbm - meterSmoothedDbm);
|
||||
}
|
||||
// Coalesce DOM writes to display refresh rate.
|
||||
if (!meterRafPending) {
|
||||
meterRafPending = true;
|
||||
requestAnimationFrame(flushMeterDom);
|
||||
}
|
||||
}
|
||||
|
||||
function flushMeterDom() {
|
||||
meterRafPending = false;
|
||||
const dbm = meterSmoothedDbm;
|
||||
if (dbm === null) return;
|
||||
prevRenderData.sigDbm = dbm;
|
||||
const sUnits = dbmToSUnits(dbm);
|
||||
sigLastSUnits = sUnits;
|
||||
@@ -7059,6 +7086,7 @@ function stopMeterStreaming() {
|
||||
clearTimeout(meterReconnectTimer);
|
||||
meterReconnectTimer = null;
|
||||
}
|
||||
meterSmoothedDbm = null; // reset so next rig starts fresh
|
||||
}
|
||||
|
||||
// ── Rendering ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1356,7 +1356,7 @@ small { color: var(--text-muted); }
|
||||
.band-tag { display: inline-block; padding: 2px 6px; border-radius: 6px; background: var(--btn-bg); color: var(--text); font-size: 0.82rem; border: 1px solid var(--border-light); margin-left: 6px; }
|
||||
.signal { display: flex; gap: 0.6rem; align-items: center; }
|
||||
.signal-bar { flex: 1 1 auto; height: 12px; border-radius: 999px; background: var(--btn-bg); border: 1px solid var(--border-light); overflow: hidden; }
|
||||
.signal-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent-green), var(--accent-yellow), var(--accent-red)); transition: width 150ms ease; }
|
||||
.signal-bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent-green), var(--accent-yellow), var(--accent-red)); transition: width 300ms ease-out; }
|
||||
.signal-value { font-size: 0.95rem; color: var(--text-heading); min-width: 48px; text-align: right; }
|
||||
.meter { display: flex; gap: 0.6rem; align-items: center; }
|
||||
.meter-bar { flex: 1 1 auto; height: 12px; border-radius: 999px; background: var(--btn-bg); border: 1px solid var(--border-light); overflow: hidden; }
|
||||
|
||||
Reference in New Issue
Block a user