SSE status updates called applyLocalTunedFrequency with forceDisplay=true,
clearing the freqDirty flag on every update and overwriting user input mid-
typing. Remove forceDisplay from SSE path so the dirty flag is respected.
Skip applyLocalTunedFrequency entirely when frequency hasn't changed to
avoid redundant spectrum redraws and overlay repositioning on every SSE
frame. Only trigger scheduleSpectrumDraw when frequency actually changes.
Add blur and Escape handlers on frequency inputs to cleanly exit editing
mode when the user abandons input.
https://claude.ai/code/session_01H2VMATj29FPgR64t9YMdSR
Signed-off-by: Claude <noreply@anthropic.com>
The closed-loop Gardner Timing Error Detector was causing decoder
freezes under real-world conditions. Remove all TED state and logic,
reverting to the simpler open-loop fixed clock_inc approach. The
8-candidate parallel architecture already provides adequate timing
coverage via phase offsets without needing closed-loop tracking.
All other improvements (adaptive Costas bandwidth, syndrome-based OSD,
OSD(3/4), PI LLR accumulation) are retained.
https://claude.ai/code/session_01FsK5hZWGpAaaCpmWupN5AD
Signed-off-by: Claude <noreply@anthropic.com>
The 8th-order (4×biquad) RDS bandpass at Q=5 per stage produced a
composite −3 dB bandwidth of ±2480 Hz, but the steep 8th-order roll-off
tapered the RDS signal edges (±1544 Hz at α=0.30) by −1.2 dB. This
distorted the RRC matched filter's expected flat spectrum, causing ISI
and degrading soft-decision confidence — directly hurting PS/RT decode
on weak signals.
Q=3.5 widens the composite passband to ±3560 Hz, reducing band-edge
attenuation to −0.59 dB while still providing ≈−4 dB rejection at the
stereo difference signal edge (53 kHz) and steep 8th-order far-out
roll-off.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
https://claude.ai/code/session_01Sw9esAuic8KHP1t8nZgvH2
Signed-off-by: Claude <noreply@anthropic.com>
The span-5 reduction passed synthetic tests because both the TX and RX
filters used the same truncated pulse shape (perfect matched filtering).
On real signals, the transmitter uses a full RRC pulse, and our truncated
RX filter couldn't match it — the weaker stopband rejection (~25% less
than pre-TED at α=0.30) allowed adjacent-channel interference through,
degrading soft confidence values and block decode rate, which caused
poor PS accumulation.
Span 10 at α=0.30 gives 50% better stopband rejection than the pre-TED
α=0.50/span=4 configuration, at the cost of 2048 vs 1024 FFT size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
https://claude.ai/code/session_01Sw9esAuic8KHP1t8nZgvH2
Signed-off-by: Claude <noreply@anthropic.com>
Three root causes for the post-TED decode quality regression:
1. OSD(4) at cost ceiling 0.60 produced excessive false positives at
marginal SNR. Tightened to OSD(2)/0.45 baseline, OSD(3)/0.50 only
after 2+ successful groups.
2. Gardner TED activated after just 1 group (score >= 1), but a single
false OSD match could trigger timing adjustments that injected jitter
into soft values. Raised lock gate to score >= 3 so the TED only
engages after the candidate has proven itself on a real signal.
3. RRC filter span of 10 chips doubled FFT size to 2048 with negligible
sensitivity gain over span 5 at α=0.30 (sidelobes beyond ±2.5 chips
contribute <5% energy). Reduced to span 5 → FFT 1024, matching
pre-TED efficiency.
Additional optimizations (no quality impact):
- Syndrome-based OSD: replaces per-trial CRC recomputation with a single
XOR per trial (CRC linearity), and sorts bit positions by ascending
soft confidence so inner loops break early instead of continuing.
- Pre-allocated FFT scratch buffer: eliminates ~234 heap allocations/sec
in the overlap-save convolution.
- PI_ACC_THRESHOLD reduced from 8 to 5 for faster acquisition while
retaining reliable majority voting.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
https://claude.ai/code/session_01Sw9esAuic8KHP1t8nZgvH2
Signed-off-by: Claude <noreply@anthropic.com>
Add a new "Sig Strength" display field in the freq row that shows
the measured signal strength. Clicking the field cycles through
three units: dBFS (default), dBf, and dBm. The selected unit is
persisted in localStorage.
https://claude.ai/code/session_01EvRV8UgsVtbrcH4t2hmFBF
Signed-off-by: Claude <noreply@anthropic.com>
The Gardner TED (Tech 11) caused PI instability and worse weak-signal
pickup due to three issues:
1. Loop gains too aggressive: noise×noise error products at low SNR
injected sub-chip jitter that degraded OSD soft confidence and PI
LLR accumulation. Reduced Kp from 4e-4→1.5e-4, Ki from 8e-8→2e-8
(loop BW 0.11→0.053 Hz).
2. TED active during acquisition: before any group is decoded, the
error signal is unreliable. Now lock-gated (score >= 1) so the
TED only engages after the first successful group decode, when
timing is already close. During acquisition, the 8-candidate
architecture with fixed clocks provides adequate timing coverage.
3. Slow power estimate convergence: ted_power_est took ~420 ms to
settle (0.999 alpha), causing the TED to over-steer during startup.
Now uses 0.995 alpha (~84 ms convergence).
Additionally, when TED is gated off, the integrator decays toward zero
so stale corrections from a previous strong-signal period don't persist.
https://claude.ai/code/session_01KcVUcQQXrFyFA9NEjLhr9J
Signed-off-by: Claude <noreply@anthropic.com>
ACI: the hard limiter in channel.rs normalised IQ samples to unit
magnitude *before* the CMA equalizer, making the signal perfectly
constant-modulus so the CMA never adapted and tap deviation stayed
at zero. Fix by moving the hard limiter inside process_iq (after
the CMA) and replacing the CMA-based metric with IQ envelope
coefficient of variation, computed on the raw samples.
CCI: the pilot coherence has a theoretical maximum of π/4 ≈ 0.785
(not 1.0), so coherence_penalty was always ~0.215 even for a clean
signal. The Q/I ratio also depended on the arbitrary NCO-pilot
phase offset rather than actual interference. Fix by normalising
coherence by its theoretical max and dropping the phase-dependent
Q/I ratio. Gate CCI on pilot detection so mono signals read 0%.
https://claude.ai/code/session_01PUXWNMRGfrWYH56k2DLmen
Signed-off-by: Claude <noreply@anthropic.com>
After completing a group (Block D), the decoder dropped lock and
reverted to search mode which only uses hard CRC. On weak signals,
Block A frequently has bit errors that OSD could correct but hard
decode cannot, causing the decoder to freeze after 2-3 successful
groups. Stay locked with ExpectBlock::A so the next Block A benefits
from OSD soft decoding.
https://claude.ai/code/session_015Ds9dxpeyFimYHySBuzbFw
Signed-off-by: Claude <noreply@anthropic.com>
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>
If the incumbent candidate has not produced a state update in 2 seconds,
clear its score advantage so any candidate can take over. This prevents
the decoder from "freezing" on stale data when the incumbent's timing or
carrier tracking degrades — particularly important for dynamic PS where
the station rotates program service text.
Signed-off-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_0136sPdLUpYgvskrzbi2Epkv
Signed-off-by: Claude <noreply@anthropic.com>
Fix Gardner TED loop structure bug (type-3 → type-2 PLL) and tune
gains for ζ=0.707 damping. Add adaptive Costas loop bandwidth that
narrows from ~22 Hz to ~5.5 Hz once carrier is locked, reducing phase
noise at low SNR. Narrow RRC matched filter (α=0.30, span=10 chips)
for ~0.6 dB noise BW gain. Add OSD(4) for locked-mode blocks after
first successful group, and increase PI accumulation threshold to 8.
TED bug details: the original code used `clock_inc += correction`
which added the full integrator value at every chip, creating an
extra integration (type-3 loop) that is unconditionally unstable.
Fixed to `clock_inc = nominal + correction` (standard type-2 PLL).
Gains retuned: Kp=4e-4, Ki=8e-8 for ζ≈0.707 and loop BW≈0.11 Hz.
Signed-off-by: Claude <noreply@anthropic.com>
https://claude.ai/code/session_0136sPdLUpYgvskrzbi2Epkv
Signed-off-by: Claude <noreply@anthropic.com>
The Auto button now toggles between Off and Auto states. Default is Off.
First click sets squelch to noise floor + 6 dB; second click resets to
Open (0%). Button shows active state with green highlight when engaged.
https://claude.ai/code/session_01TDQyrZiPKfWGATVWPsLmHT
Signed-off-by: Claude <noreply@anthropic.com>
Add an "Auto" button next to the SQL slider that sets the squelch
threshold to the current noise floor (estimated from spectrum bins)
plus a 6 dB margin. Uses the existing estimateNoiseFloorDb() heuristic.
https://claude.ai/code/session_01TDQyrZiPKfWGATVWPsLmHT
Signed-off-by: Claude <noreply@anthropic.com>
Add OSD_MAX_FLIP_COST (0.45) to reject OSD corrections where the flipped
bits had high confidence — a strong false-decode indicator. Genuine errors
at 9-10 dB SNR have cost ≲0.3; noise matches cost 0.6-1.2.
Add PI consistency gate in process_group: reject groups whose Block A PI
differs from the candidate's established PI, preventing noise from
polluting accumulated PS/RT/PTYN text fields.
Raise PI_ACC_THRESHOLD from 2 to 3 for stronger PI voting.
Extend noise rejection test from 0.5s to 2s. Add 9 dB SNR sensitivity
test (all 16 tests pass).
https://claude.ai/code/session_01GYax4BQ9ZV9ZZfMjmmzgbh
Signed-off-by: Claude <noreply@anthropic.com>
The WSPR decoder was producing many false positive decodes due to
several overly permissive thresholds that allowed noise to reach the
Fano sequential decoder, which could then converge on random data:
- Raise normalized sync score threshold from 0.10 to 0.20 to reject
noise candidates before attempting expensive Fano decoding
- Add minimum SNR gate (-20 dB) to skip candidates where the signal
is indistinguishable from noise
- Return and check the Fano decoder's cumulative path metric, rejecting
low-confidence decodes (metric < 20) that are likely noise artifacts
- Raise RMS threshold from 0.0005 to 0.005 to reject near-silent audio
- Add near-frequency deduplication to prevent the same signal decoded
at slightly different (freq, dt) offsets from appearing multiple times
- Add noise-only regression test to verify no false positives on random
input
https://claude.ai/code/session_01HTBoEsD1hp99TiYMSaHMVG
Signed-off-by: Claude <noreply@anthropic.com>
The `chips_to_rds_signal` test helper was generating rectangular chip
pulses, but the receiver expects RRC-shaped transmit pulses so that
RRC(tx) × RRC(rx) = raised cosine with zero ISI. The rectangular
pulses caused ISI that drifted the symbol clock sampling point,
consistently skipping PS segment 2 in the end-to-end test.
Replace rectangular pulses with an impulse train convolved with the
same RRC taps used by the receiver. All 15 tests now pass including
`end_to_end_clean_signal_decodes_ps`.
https://claude.ai/code/session_01N2UcGaLDzYiM3gNrZ6kFBj
Signed-off-by: Claude <noreply@anthropic.com>
The map module was tagging all decode markers (APRS, AIS, VDES,
FT8/FT4/FT2/WSPR locators) with the global rig picker's active rig
instead of the actual source rig. This made the map's own rig filter
dropdown ineffective in multi-rig setups.
- Add rig_id field to all decode message structs (AisMessage,
VdesMessage, AprsPacket, CwEvent, Ft8Message, WsprMessage)
- Set rig_id on messages in audio_client before broadcasting, using
the actual rig connection identifier
- Update history collector to prefer message rig_id over the global
active rig fallback
- Pass rig_id through plugin normalize functions (AIS, APRS, VDES,
HF-APRS) so it reaches the map add functions
- Update all map marker functions (aprsMapAddStation, aisMapAddVessel,
vdesMapAddPoint, mapAddLocator) to use the message's rig_id with
fallback to the global picker for backward compatibility
https://claude.ai/code/session_015gC7axHk2jmp7HbFPdbivN
Signed-off-by: Claude <noreply@anthropic.com>
Drop the plugin loading infrastructure (libloading-based dynamic .so/.dylib/.dll
loading) from both trx-server and trx-client. The feature was unused and posed an
unnecessary security risk by executing arbitrary native code from disk.
Removed:
- src/trx-app/src/plugins.rs (plugin discovery, validation, FFI registration)
- examples/trx-plugin-example/ (cdylib example plugin)
- libloading dependency from trx-app
- load_backend_plugins / load_frontend_plugins calls from server and client
- Plugin documentation from README.md and CLAUDE.md
https://claude.ai/code/session_01DTEUpz3XPUeWmz74NeaFgb
Signed-off-by: Claude <noreply@anthropic.com>
Update all SDR command handlers in rig_task to access SDR methods via
ctx.rig.as_sdr() instead of calling them directly on RigCat. Query-only
SDR operations (filter_state, get_spectrum, get_vchan_rds) use
as_sdr_ref(). Non-SDR rigs now get proper not_supported errors.
https://claude.ai/code/session_01XzurkeuUmamBuhQwxVy7T4
Signed-off-by: Claude <noreply@anthropic.com>
Extract 13 SDR-specific methods (set_center_freq, set_bandwidth,
set_sdr_gain/lna/agc/squelch/nb, set_wfm_*, filter_state, get_spectrum,
get_vchan_rds) into a new RigSdr trait. RigCat retains core CAT
operations and gains as_sdr()/as_sdr_ref() for optional SDR access.
Non-SDR backends no longer see SDR methods in their trait impl.
https://claude.ai/code/session_01XzurkeuUmamBuhQwxVy7T4
Signed-off-by: Claude <noreply@anthropic.com>
Replace all .unwrap() on RwLock/Mutex acquisitions with
.unwrap_or_else(|e| e.into_inner()) to gracefully recover from poisoned
locks instead of panicking. Add lock ordering documentation to the
module header to prevent deadlocks.
https://claude.ai/code/session_01XzurkeuUmamBuhQwxVy7T4
Signed-off-by: Claude <noreply@anthropic.com>
Add an AtomicUsize total_count field to DecoderHistories, maintained by
record/prune/clear methods, so estimated_total_count() avoids 9 separate
mutex acquisitions. Also replace audio ring buffer .unwrap() calls with
.unwrap_or_else(|e| e.into_inner()) to recover from poisoned locks.
https://claude.ai/code/session_01XzurkeuUmamBuhQwxVy7T4
Signed-off-by: Claude <noreply@anthropic.com>
Use a StateWithMeta wrapper struct with #[serde(flatten)] for merging
rig state with frontend meta, replacing the manual string manipulation.
Also add Serialize derive and skip_serializing_if to FrontendMeta.
https://claude.ai/code/session_01XzurkeuUmamBuhQwxVy7T4
Signed-off-by: Claude <noreply@anthropic.com>
Introduce DecodeHistory<T> alias for the repeated
Arc<Mutex<VecDeque<(Instant, Option<String>, T)>>> pattern (9 fields).
Also switch VChanAudioCmd channel senders from UnboundedSender to Sender
to prevent unbounded memory growth under backpressure.
https://claude.ai/code/session_01XzurkeuUmamBuhQwxVy7T4
Signed-off-by: Claude <noreply@anthropic.com>