Allow the main frontend card to grow past the previous 1280px cap
on large screens while keeping side gutters for bookmark stacks.
Only widen the desktop layout at 1440px+ and cap it at 1600px so the
main content stays readable with room for side bookmarks.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The deinterleave function had its indices swapped: it wrote
out[j] = symbols[p] instead of out[p] = symbols[j]. This fed
completely scrambled data to the Fano decoder, making convergence
impossible. Matched against the reference implementation in
raptor/lib/wsprd/wsprd_utils.c which does tmp[p] = sym[j].
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The OSD-lite decoder was the source of FT2 false positives. It tries
685 CRC-14 checks across 5 passes (1 + 16 + 120 per pass), giving a
~4% chance of accepting random noise as a valid decode.
The reference implementation (decode174_91) verifies OSD results
against the received signal; the trx-rs OSD-lite only checked CRC.
Add ft2_count_hard_errors_vs_llr() which counts how many of the 174
coded bits in an OSD candidate disagree with the received hard
decisions. A legitimate correction disagrees in very few positions;
a false CRC match on noise disagrees in ~40-50 parity positions.
Reject OSD results with more than 36 hard errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The ft8_wrapper.c references ft4_encode and ft8_encode from encode.c,
but encode.c was not included in build.rs, causing linker errors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The WSPR decoder was producing almost no valid decodes despite audible
signals. Three root causes:
- No sync vector: the 162-bit WSPR sync pattern was not used, so signal
detection relied on raw peak power which is unreliable.
- Coarse frequency search: 4 Hz steps with 1.465 Hz tone spacing could
miss signals entirely. Now uses 2 Hz coarse + 0.25 Hz fine refinement.
- Fixed timing: assumed signal starts exactly 1s into the slot. Now
searches +/-2s in 0.5s steps to handle real-world timing jitter.
Also evaluates up to 8 frequency/timing candidates per slot and reports
the actual measured timing offset in dt_s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The previous SNR formula (cand->score * 0.5 - 29.0) used the adjacent
tone bin as a noise reference. On a crowded FT8 band that bin is often
occupied by another station, inflating the apparent noise floor by
10-15 dB and capping reported SNR at around -10 dB even for strong
signals.
Replace with ftx_post_decode_snr(): re-encode the decoded message to
obtain the exact per-symbol tone sequence, compare each signal bin
against the minimum of the remaining (noise-only) bins, average over
all valid symbols, and apply the WSJT-X 2500 Hz bandwidth correction
dynamically per protocol. This produces accurate SNR estimates for both
FT8 and FT4 regardless of band occupancy.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Primary bookmark was cramped at one grid column; Extra channels was
unnecessarily spanning the full row. Swap their grid-column spans so
the bookmark select gets full width and Extra channels sits in a
normal column alongside Label and Interleave.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Row gap 0.5→0.75rem and label-to-input gap 0.2→0.35rem, scoped
to #sch-entry-form so the bookmark form is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace the inline always-visible add-row with a modal overlay
(same fixed + blurred-backdrop pattern as the bookmark add/edit
form). The "+ Add Entry" button opens the modal; each row now has
Edit and Remove buttons. Edit pre-fills the modal and updates the
entry in-place on save. CSS reuses the existing bookmark modal
selectors rather than duplicating rules.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Opus is the sole audio format. Removes all FLAC references, the
format config key, the claxon/flac dependency row, and the
FLAC-encoder open question.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add NEON (aarch64) vectorized paths mirroring the existing AVX2 paths:
- demod/math_arm.rs: replace the no-op placeholder with a full NEON FM
discriminator that processes 4 samples per iteration using a 7th-order
minimax atan polynomial and branchless atan2 with argument reduction,
matching the accuracy of the AVX2 path (max error ~2.4e-7 rad).
32-bit ARM retains the scalar fallback.
- dsp/filter.rs: add mul_freq_domain_neon() that deinterleaves 4 complex
pairs via vuzpq/vzipq, performs complex multiply with vmulq/vaddq/vsubq,
then reinterleaves. On aarch64 this path is always taken (NEON is
mandatory); scalar fallback remains for other targets.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a new "Strongest decoded signal" section below the existing
"Longest decode paths" section on the map tab, showing the top 5
stations with the highest SNR across the current map history window.
Reuses existing map-qso-card styling with SNR displayed in place of
distance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace the always-None stub with a full decode pipeline:
- Bit-reversal deinterleave of 4-FSK symbols (data_bit = symbol >> 1)
- Fano sequential decoder for K=32, rate-1/2 convolutional code
(polynomials 0xF2D05351 / 0xE4613C47, 100k-cycle budget)
- Payload unpack: 28-bit callsign (mixed-radix N1), 15-bit Maidenhead
grid (M1 formula), 7-bit power code (dBm + 64)
- Validity checks on callsign, grid, and power range
- Round-trip unit test for K1JT FN20 37
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
clamp_bandwidth_for_mode now floors AMC bandwidth at 9 kHz so that both
the sum (L+R) and difference (L−R) sidebands are always captured,
regardless of what the user or a set_filter call requests.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
RigMode::AMC was implemented but omitted from the supported_modes vec,
so the HTTP frontend never received it in capabilities and the mode
selector did not show AMC-QUAM.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
- remote_client tests: add missing server_connected field and import
AtomicBool in the test module
- pskreporter: replace map_or(true, …) with is_none_or and
repeat(x).take(n) with repeat_n(x, n)
- dsp.rs, scheduler.rs: suppress intentional too_many_arguments with
#[allow(clippy::too_many_arguments)]
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a RigMode::AMC arm to encode_mode returning an Err so that
attempting to set C-QUAM on the FT-450D returns a descriptive error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add RigMode::AMC to the None return arm of encode_mode so that
attempting to set C-QUAM on the FT-817 returns a descriptive error
rather than a compile-time non-exhaustive match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add "AMC-QUAM" to the MODE_BW_DEFAULTS table in app.js with the same
bandwidth settings as AM (9 kHz default, 500 Hz min, 20 kHz max, 500 Hz
step). Add an <option value="AMC-QUAM"> to the bookmark datalist in
index.html.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add RigMode::AMC arm to default_audio_bandwidth_for_mode, returning
9_000 Hz (same as AM).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add CquamDemod (demod/amcquam.rs) with first-order IIR carrier phase
tracker (τ = 50 ms) that rotates baseband IQ to align I with the sum
audio and Q with the difference audio, then DC-blocks each channel to
yield L/R stereo PCM.
Wire AmCQuam into the Demodulator enum, add ChannelDsp::cquam_decoder
field initialized for RigMode::AMC, and insert the C-QUAM audio path
between the WFM and fallback branches in process_block. Update all
mode-dispatch tables (agc_for_mode, iq_agc_for_mode, dc_for_mode,
default_bandwidth_for_mode, lib.rs, vchan_impl.rs) with AMC arms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add parse_mode and mode_to_string support for RigMode::AMC, accepting
"AMC-QUAM", "AMC_QUAM", and "AMC" as input strings and serializing to
"AMC-QUAM". Add test cases for round-trip correctness.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
setRigFrequency applied applyLocalTunedFrequency before the network
round-trip, leaving the spectrum view shifted to the new frequency even
when the POST was rejected (e.g. 403 when the user loses control access).
Save the previous frequency and restore it in the catch block so the UI
stays aligned with the actual server-side state.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Show a full-screen blurred overlay (reusing decode-history-overlay styles)
when the SSE connection to trx-client drops or when trx-server is reported
unreachable via server_connected=false. The overlay distinguishes the two
failure modes with separate titles and sub-text. It is dismissed on
es.onopen (trx-client back) or when a message with server_connected!=false
arrives (trx-server back), and cleared on explicit disconnect/logout.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
decodeSource.onerror terminated the history worker but never called
flushLiveBuffer(), leaving historySettled=false and the "Loading decode
history…" overlay stuck on screen when trx-client breaks. Call
flushLiveBuffer() in the error path so the overlay is dismissed and the
powerHint connection-lost message is visible to the user.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
querySelectorAll("span") was picking up the inner .spectrum-bookmark-name
spans added in the previous commit, causing chips[i] to map to the wrong
element during left-position assignment. Switch to :scope > span to
select only direct-child chip spans.
Also revert axis bar from height:auto (which breaks the CSS transition and
collapses the bar) to height:38px with overflow:visible to accommodate
two-line labels while keeping the open/close animation intact.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Suppress stroke-dashoffset animation and drop-shadow filter on all
contact/radio paths when decodeContactPaths.size > 20 by toggling
.map-paths-static on #aprs-map, avoiding per-frame GPU compositing
with large decode histories.
Wrap non-sideStack bookmark chip labels in
<span class="spectrum-bookmark-name"> and allow word-break on axis
chips so long names split across two lines instead of being clipped.
Axis bar switches to min-height so it grows to fit taller chips.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
When the SSE stream drops (onerror or 15s heartbeat timeout) show
"trx-client connection lost, retrying…". When the stream is live but
server_connected is false, show "trx-server connection lost".
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add server_connected bool to FrontendMeta and inject it into every
/status and /events payload so the browser can distinguish between
trx-client and trx-server connection loss.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Store server_connected in RemoteClientConfig and set it true when
handle_connection begins, false when it returns. Wire the Arc clone
from FrontendRuntimeContext into the config at startup.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Track whether the remote client currently has an active TCP connection
to trx-server via a shared AtomicBool. Frontends can read this to
surface a distinct "trx-server connection lost" message.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Side-panel chips used white-space: normal + word-break: break-word,
causing names longer than ~16 characters to wrap onto a second line.
Switch to nowrap + overflow: hidden + text-overflow: ellipsis so long
names are clipped cleanly in a single line.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Drop FirTapsQuery, the set_fir_taps handler, and its service
registration. Tap count is no longer user-configurable.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Drop fir_taps field from SdrChannelConfig and its default. Remove the
SetFirTaps dispatch arm from rig_task. The DSP layer now auto-calculates
tap count from audio_bandwidth_hz.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace the static fir_taps parameter with auto_taps(cutoff_norm) which
computes ceil(3.32 / cutoff_norm).clamp(63, 16383). This ensures the
filter transition band equals one passband width regardless of SDR sample
rate, giving correct image rejection when the user sets audio_bandwidth_hz.
At 912 ksps with 3 kHz audio bandwidth this yields ~2018 taps instead
of the previous hardcoded 64, eliminating the 114 kHz stopband gap that
caused adjacent-band signals to alias into the audio output.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Drop SetFirTaps ClientCommand variant and its mapping to/from
RigCommand. Remove fir_taps from RigFilterState codec tests.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Drop the SetFirTaps RigCommand variant, remove the set_fir_taps default
trait method from Rig, and remove the fir_taps field from RigFilterState.
Tap count is now derived automatically from audio bandwidth in the DSP
layer rather than being a user-facing control.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add read_named_gain to IqSource (default: None) and implement it in
RealIqSource via Device::gain_element. Read the "LNA" element before
boxing the source so the initial sdr_lna_gain_db reflects the actual
hardware state, making the UI control visible and correct on first
connect. Devices without an LNA element (e.g. RTL-SDR with "TUNER")
return None and the control stays hidden.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add POST /set_sdr_lna_gain endpoint. Add an LNA Gain input+Set button
to the SDR settings row, styled identically to RF Gain. The control is
hidden until the server reports a sdr_lna_gain_db value (i.e. devices
that expose an LNA gain element). AGC disables it alongside RF Gain.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Handle RigCommand::SetSdrLnaGain by calling set_sdr_lna_gain on the
rig and refreshing filter state, matching the pattern used by
SetSdrGain and SetSdrAgc.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add set_named_gain to the IqSource trait and implement it in
RealIqSource via soapysdr Device::set_gain_element. Wire a
lna_gain_cmd channel through SdrPipeline so the IQ read loop applies
LNA gain changes on the next iteration. Add set_sdr_lna_gain to
SoapySdrRig and expose the current value via filter_state.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Map ClientCommand::SetSdrLnaGain { gain_db } to and from
RigCommand::SetSdrLnaGain in both directions.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add RigCommand::SetSdrLnaGain(f64), the corresponding default RigCat
trait method set_sdr_lna_gain, and an sdr_lna_gain_db: Option<f64>
field to RigFilterState for backends that expose a named LNA gain
element.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace vol-label with wfm-control on the Hardware AGC checkbox so its
label typography and alignment match the RF Gain control. Change the
outer container to align-items: flex-end so controls bottom-align.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The bidirectional check required both A→B and B→A directed messages to
draw a contact line, which was too strict — the receiver may not hear
both sides of a QSO. Now a decode path is drawn whenever a directed
message is decoded and the target's locator is known from any message
in the 24 h history window.
Also rename "Longest QSOs" → "Longest decode paths" and update
related UI labels to better reflect what is actually shown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
A QSO requires both parties to hear each other. Previously the map drew
contact paths whenever A sent a directed message to B and B's locator
was known from any message. Now paths only appear when both A→B and B→A
directed messages are present in the decoded history.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add hardware AGC on/off control for SoapySDR backend, wired through the
full stack from RigCommand to the web UI:
- RigCommand::SetSdrAgc(bool) + ClientCommand::SetSdrAgc in protocol
- set_sdr_agc() on RigCat trait (not-supported default)
- SoapySdrRig: agc_enabled field, set_sdr_agc() via pipeline agc_cmd,
sdr_agc_enabled in filter_state(); removes the "not yet implemented"
warning — gain_mode="auto" now properly enables hardware AGC via
SoapySDR set_gain_mode()
- IqSource::set_gain_mode() trait method; RealIqSource implements it
- SdrPipeline: agc_cmd channel, read loop applies it each iteration
- POST /set_sdr_agc endpoint in trx-frontend-http
- New "SDR settings" full-row in index.html with Hardware AGC checkbox
and RF Gain (moved out of WFM controls); row hidden when
show_sdr_gain_control is false
- app.js: AGC checkbox handler, disables RF gain input while AGC is on,
syncs checkbox state from filter.sdr_agc_enabled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three issues caused audible distortion on AM reception:
1. DC blocker shared r=0.9999 (τ≈1.25 s at 8 kHz) across all modes.
For AM the envelope detector outputs A_c+m(t) — always positive —
so the blocker needs to track the carrier bias quickly. AM now uses
r=0.999 (τ≈125 ms), 10× faster, while keeping the highpass cutoff
below 2 Hz so speech is unaffected.
2. Audio AGC time constants were inverted relative to good AM AGC design:
attack=200 ms (should be fast to prevent overload) and
release=3500 ms (unreasonably sluggish). Changed to attack=5 ms /
release=200 ms, target=0.5, max_gain=36 dB.
3. No IQ AGC before envelope detection meant carrier amplitude variation
went directly into the audio chain, forcing the slow audio AGC to
handle both RF level and audio level simultaneously. Added an AM IQ
AGC (attack=0.5 ms, release=50 ms, target=0.7, max=30 dB) that
normalizes carrier power before demod_am, so the DC blocker always
sees the same steady-state bias regardless of signal strength.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Apply #ft8-messages container style (border, rounded corners, monospace
font, max-height with scroll) to #ft4-messages, #ft2-messages, and
#wspr-messages which were missing it.
Add #ft4-decode-toggle-btn and #ft2-decode-toggle-btn to the narrow-
screen white-space:nowrap media query rule alongside FT8/WSPR.
Cap DOM rows rendered per history view to 200 (FT8_MAX_DOM_ROWS,
FT4_MAX_DOM_ROWS, FT2_MAX_DOM_ROWS). Full history is retained in
memory; only the DOM representation is bounded. This prevents tab
switching from becoming sluggish after a long decode session where
thousands of rows accumulate in the DOM.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three bugs caused USB/LSB to sound like AM:
1. The IQ low-pass filter was symmetric (passband ±BW/2), so both
sidebands were passed equally — taking .re then produced DSB-SC
rather than SSB audio.
2. cutoff_hz was computed as bandwidth_hz/2, halving the usable audio
bandwidth (1500 Hz for a 3 kHz USB channel).
3. demod_lsb claimed spectrum inversion was "handled upstream by
negating channel_if_hz", but that negation was never applied; USB
and LSB were functionally identical.
Fix: add a shift_norm parameter to build_fir_kernel / BlockFirFilterPair
that complex-modulates the time-domain FIR coefficients by
e^{j·2π·shift_norm·n}, shifting the passband in the frequency domain.
A new ssb_shift_norm() helper returns +cutoff_norm for USB/CW/DIG
([0, BW] Hz passband) and -cutoff_norm for LSB/CWR ([-BW, 0] Hz
passband); all other modes get 0.0 (symmetric LPF unchanged).
After the one-sided filter, taking .re correctly reconstructs the
selected sideband. No IF negation is needed for LSB.
Also fix two unit tests missing the force_mono_pcm argument introduced
after they were last updated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The OSD-3 (triples) path over 5 LLR passes was doing ~11,600 CRC checks
per candidate. With a 14-bit CRC this gives ~0.7 expected false positives
per candidate — far too high.
Remove OSD-3 entirely. Cap max_candidates at 16 for OSD-1/OSD-2, giving
136 CRC checks per pass (680 total). Gate OSD-lite behind a check that
LDPC reached within 6 parity errors of converging, so it only fires when
the LLRs are already trustworthy. Combined false-positive rate drops to
~0.04 per near-miss candidate.
Also remove the now-unused ft2_osd_decode and ft2_codeword_distance
functions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase BP/SP iteration count from 30 to 50 to match WSJT-X reference
and give belief propagation more opportunities to converge near-threshold
candidates.
Replace the parity-based OSD-1/OSD-2 fallback (which required LDPC to
have nearly converged) with ft2_osd_lite_decode applied to all five LLR
combination passes. The CRC-based decoder works directly from raw LLRs
without depending on LDPC convergence, searching the 24 least-reliable
systematic bits for up to three bit errors via OSD-3.
Also increase max_candidates in ft2_osd_lite_decode from 12 to 24 for
broader coverage of likely error positions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Diagnostic logging showed the FT2 BP/SP decoders consistently reach
1-8 residual parity errors rather than zero — the LLRs are correct
in direction but LDPC belief propagation stalls just short of
convergence.
Add ft2_osd_decode() implementing Ordered Statistics Decoding orders
1 and 2: after the five-pass BP/SP loop fails, sort the 174 codeword
bits by |LLR| ascending and trial-flip single bits (OSD-1, always)
or all pairs of the 50 least-reliable bits (OSD-2, when the remaining
error count is <= 4). Each trial costs one O(83) parity check;
worst-case overhead is ~1300 checks per candidate, negligible next to
the 5 x 30-iteration BP/SP passes already performed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add err=N/N/N/N/N to the FT2 window diagnostic log line, showing the
minimum number of unsatisfied parity equations across all candidates
for each of the five LLR passes. This makes it possible to distinguish
between a signal-quality-limited failure (small error count) and a
systematic decoder bug (large error count), which is the key unknown
in diagnosing the current FT2 LDPC non-convergence.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Normalize FT2 log-likelihoods before LDPC and fall back to\nthe standard waterfall candidate decoder when the raw FT2\npath produces no decodes.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Use a wider FT4 time-offset search range in candidate acquisition\nso decode remains robust with current pipeline latency.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Use millisecond-based slot indexing for FT4 decode windows in\nforeground and background decoders to avoid premature 7s resets.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Use an FT2-specific raw sample window and candidate acquisition path.
Keep the build clean by removing the stale monitor warning.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Use FT2-specific analysis settings and a wider receive span.
Switch server-side FT2 decoding to a rolling async window.
Widen FT2 candidate timing search in the vendored decoder.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Implement a distinct FT2 protocol path in the decoder stack and align\nits timing with the confirmed FT2 framing used by Decodium.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The function handles FT8, FT4, and WSPR locators so the ft8 prefix
was misleading. Updated all call sites in app.js, ft8.js, ft4.js,
and wspr.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
- Add ft4 to DEFAULT_MAP_SOURCE_FILTER (visible by default)
- Add ft4 hue to locatorThemeHues (peakHue + 30° offset from FT8)
- Propagate ft4 through locatorSourceLabel, locatorFilterColor,
locatorHueForEntry, isLocatorOverlay, applyMapFilter,
ensureDecodeLocatorMarker, pruneLocatorEntry, rebuildDecodeContactPaths,
clearMapMarkersByType, markerSearchText, navigateToMapLocator, and
the source items chip row
- ft8MapAddLocator now maps type="ft4" to markerType="ft4" so FT4
locators get their own distinct marker colour and filter chip
- ft4.js: pass "ft4" (not "ft8") to ft8MapAddLocator
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
bmReadDecoders, bmWriteDecoders, and bmApply were all missing FT4,
so the decoder checkbox was never saved and tuning a bookmark never
toggled the FT4 decoder state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Decouple the longest-QSO summary from the contact path render toggle.
Keep the summary filtered by current map visibility while the toggle only
controls whether path overlays are drawn.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Serve grouped decode history payloads and restore each decoder through
explicit history restore hooks instead of replaying a mixed message stream.
This reduces replay overhead further by removing type regrouping and keeping
history restoration on decoder-specific bulk paths.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Serve a dedicated decode-history worker and move compressed history fetch
and CBOR parsing into that worker.
The main thread now drains ready-made decode batches within a frame budget,
which further reduces UI disruption during large history restores.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Override the global button chrome on longest-QSO cards so they keep
the intended card layout instead of inheriting fixed control sizing.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replay decode history in decoder-specific batches instead of feeding every
message through the single-message path.
This reduces per-message array churn and UI scheduling during large history
loads while keeping the existing live decode behavior unchanged.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Serve decode history as gzipped CBOR and decode it in the frontend.
Defer map materialization until replay completes to avoid replay-time stutter,
and include the pending longest-QSO style adjustment.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Let users click longest-QSO cards to isolate a single contact path on the map and click again to restore all visible contact paths. Also remove the extra inner panel styling from decode map tooltips so the popup renders as a single container.
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep map history data cached when the history window is reduced so older APRS, AIS, VDES, FT8, and WSPR items can be shown again when the user expands the window, and add a global decode-history replay overlay with progress updates across the UI. Also update the longest QSO summary to render bidirectional contacts with <-> labels.
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a map summary section below the map that lists the five longest directed FT8 and WSPR contacts in the current view, including distance, band, age, and locator details.
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Verification: git diff --check -- src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a map filter-panel history picker with 15 minute through 24 hour retention options and prune dynamic APRS, AIS, VDES, FT8, and WSPR overlays to the selected age window.
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Write compressed audio-history replay directly into the local frontend history buffers so large APRS and AIS replays survive trx-client restart instead of overrunning the live decode broadcast channel.
Verification: cargo test -p trx-client --no-run
Verification: cargo test -p trx-frontend-http --no-run
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Automatically return control to the scheduler after using the Previous or Next entry controls so manual stepping does not leave the session latched in takeover mode.
Verification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/scheduler.js
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Apply scheduler-backed virtual channels as real manual selections so they take control, retune the rig, and restore bookmark decoder state including APRS/PKT. Also remove the inner border from the map decode locator tooltip.\n\nVerification: cargo test -p trx-frontend-http vchan\nVerification: node --check src/trx-client/trx-frontend/trx-frontend-http/assets/web/plugins/vchan.js\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep scheduler-managed virtual channels reconciled while\nclients remain connected, instead of only materializing\nthem during the initial connect path.\n\nVerification: cargo test -p trx-frontend-http vchan\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Materialize scheduler-managed virtual channels before the\ninitial channels SSE event when the scheduler currently\ncontrols the rig.\n\nVerification: cargo test -p trx-frontend-http vchan\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Use the currently tuned virtual channel for the website title\ninstead of always showing channel 0 metadata.\n\nVerification: node --check assets/web/app.js\nVerification: node --check assets/web/plugins/vchan.js\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Track the last applied scheduler entry so previous/next\ncycles correctly across active entries and resets the\ncountdown after manual entry changes.\n\nVerification: cargo test -p trx-frontend-http scheduler\nVerification: node --check assets/web/plugins/scheduler.js\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add previous/next scheduler entry controls for overlapping\ntime-span slots and fix interleave timing calculations so\nthe active slot and countdown align with the overlap window.\n\nVerification: cargo test -p trx-frontend-http scheduler\nVerification: node --check assets/web/plugins/scheduler.js\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Reduce main-thread stalls while decode history replays.\n\nCoalesce decoder list redraws and map maintenance so spectrum\nand controls stay responsive during history import.\n\nVerification: node --check on modified frontend JS files.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Treat the SPA entry routes as public so direct requests to /map,\n/decoders, /settings, and /about return the app shell and let\nthe frontend show the login screen instead of a 403.\n\nMove the map filter overlay to the bottom-right corner and color\ndecode contact paths by their decoded band so they match the band\nlegend and locator overlays.\n\nVerified with cargo test -p trx-frontend-http.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Append the APRS-IS position beacon comment as\n`trx-rs v<version> by SP2SJG` so IGate beacons identify\nthe running software version.\n\nUpdate the APRS beacon formatter tests to assert the exact\npayload including the generated version string.\n\nCo-Authored-By: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Skip decoded candidates where ftx_message_decode() returns a non-OK
status instead of forwarding a synthetic error string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Radio paths and decode contact paths now use the same color as the
marker they belong to, respecting the active filter mode:
- Band mode: color follows the band (golden-angle HSL hue)
- Mode/source mode: color follows the source type (FT8/WSPR/bookmark)
APRS, AIS, and VDES paths use their fixed source colors unchanged.
Decode contact paths sync color when the filter mode is switched.
CSS stroke/stroke-opacity removed from path classes so Leaflet's
color option takes effect; dasharray and flow animation are retained.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The score from ft8_lib is an averaged uint8 difference between Costas
sync tones and their neighbours (each unit = 0.5 dB). The previous
score * 0.5 gave the signal-above-adjacent-noise in dB relative to a
single 3.125 Hz waterfall bin, yielding values of +5 to +50 dB —
all wrong.
Subtract 10*log10(2500/3.125) ≈ 29 dB to normalise to the 2500 Hz
reference bandwidth used by WSJT-X and expected by PSKReporter:
snr = score * 0.5 - 29.0
This maps score 10 (minimum decodable) → -24 dB and score 60 → +1 dB,
matching typical WSJT-X SNR report ranges.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the two-character beacon_symbol string with separate
beacon_symbol_table (char) and beacon_symbol_code (char) fields to
avoid TOML backslash escaping issues with the alternate symbol table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add periodic IGate position beacon support to the APRS-IS uplink.
New AprsFiConfig fields: beacon (bool), beacon_interval_secs (default
1200), beacon_symbol (default "/-"), latitude/longitude overrides.
A beacon is sent immediately on connect then every beacon_interval_secs.
Coordinates fall back from [aprsfi] to [general].latitude/longitude.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Allow specifying the IGate callsign directly in [aprsfi] instead of
relying on [general].callsign. The aprsfi-specific callsign takes
precedence; [general].callsign is used as fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move aprsfi and pskreporter modules from trx-server into a new
standalone trx-reporting library crate. Config types (AprsFiConfig,
PskReporterConfig) move to trx-reporting and are re-exported from
trx-server::config for backwards compatibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Append mandatory q-construct (,qAR,<callsign>) to all forwarded
TNC2 packets via updated format_tnc2(pkt, igate_call)
- Add TCPIP/TCPXX loop-prevention check before forwarding
- Drain server-sent data in select! loop to prevent TCP backpressure
- Enable TCP_NODELAY for low-latency packet forwarding
- Guard against history replays: skip packets older than 2 minutes
- Use "trx-rs" in login string and keepalive comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add [workspace.package] version = "0.1.0" to the root Cargo.toml and
switch all 21 member crates to version.workspace = true so the entire
workspace is versioned from a single place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Two bugs introduced by 60697bb:
1. dsp.rs passed `channel_idx == 0` as force_mono_pcm, which forced the
primary pipeline channel to output mono samples. The Opus encoder was
configured for stereo, so it received half the expected frame data,
causing distortion for all connected audio clients.
Fixed by passing `false` — hidden virtual channels already set
force_mono_pcm=true via set_force_mono_pcm() in vchan_impl.rs.
2. main.rs short-circuited channel conversion when no audio clients were
connected, sending raw frames to pcm_tx (decoders). When clients then
connected, decoders switched to receiving stereo-interleaved frames,
making decoder input format dependent on client presence.
Fixed by always performing the channel conversion before sending to
pcm_tx; the no-client skip now only bypasses Opus encode + rx_audio_tx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Skip APRS packets whose ts_ms is older than 120 seconds. Live RF-decoded
packets arrive within milliseconds; history replay items can be up to 24
hours old and must not be re-uploaded to APRS-IS as live traffic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rewrite the PSKReporter uplink to match the protocol spec exactly:
- Fix template FlowSetIDs: receiver uses 0x0003 (Options Template Set),
sender uses 0x0002 (Template Set); previously both used 0x9992/0x9993
- Add missing enterprise numbers (0x0000768F = 30351) to all enterprise
field specifiers in both template blocks
- Fix sender template field IDs: use correct attributes (senderCallsign
30351.1, frequency 30351.5, sNR 30351.6, iMD 30351.7, mode 30351.10,
informationSource 30351.11, senderLocator 30351.3, flowStartSeconds 150)
- Fix sender data field order to match the template declaration
- Add iMD byte (0) required by the 8-field template
- Add 4-byte null padding on receiver and sender data records
- Batch spots into one UDP packet per 5-minute window (spec requirement)
- Deduplicate by callsign within each window (keep most-recent spot)
- Send template descriptors only in first 3 packets then once per hour
- Increment sequence number by report count, not packet count
- Guard against history replays: drop any spot older than the flush
window (live FT8/WSPR is seconds old; history can be 24 h old)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rebuild the visible-source chips when live APRS, AIS, or VDES markers are first added so the map filter list updates without a page refresh.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Use an unbounded virtual-channel command queue so background decode and scheduler transitions do not silently drop subscribe or remove commands.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove hidden background decode channels when the owning audio client disconnects to avoid stale DSP and decoder buildup.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Run FT8 and WSPR decode steps in blocking sections so the server listener stays responsive under decode load.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the misleading scheduler task countdown with the actual time-span interleave switch timing in the main controls row.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Raise remote spectrum polling from 100 ms to 50 ms while keeping the relaxed timeout and subscriber gating.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Relax the remote spectrum timeout, poll at the backend update cadence, and stop polling when no spectrum subscribers are connected.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add separate map path toggles, move scheduler handoff into the channels row, and show a live countdown to the next scheduler cycle.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove settings rig pickers, restore the last scheduler cycle on release, fix FT8 locator role parsing, and add toggleable decode contact paths on the map.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rewrite the README, remove AI-generated planning docs, and regenerate the combined example config.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move Scheduler under a new Settings tab in the HTTP frontend.
Add the virtual-channel audio implementation plan document.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Use the active channel frequency for spectrum bandwidth edge hit-testing.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Each RDS PS overlay item (position: absolute within the shared #rds-ps-overlay
container) now receives a z-index derived from its channel frequency: items are
sorted by freq_hz ascending so higher-frequency layers sit on top of
lower-frequency ones by default.
Hovering any layer temporarily assigns it the maximum z-index (entry count + 10)
to bring it to the front; mouseleave restores the frequency-derived default
stored in data-default-z.
Also reverts the incorrectly applied vchan picker layer changes from the
previous commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Render virtual channels as absolutely-positioned layer strips inside a
shared relative container (#vchan-freq-layers). Layers are sorted by
frequency ascending so higher-frequency channels receive a higher z-index
and sit on top by default. Hovering any layer temporarily assigns it the
maximum z-index to bring it to the front; leaving restores the original
stacking order. Each layer is offset by 11 px vertically so all channels
remain visible as a staggered card stack.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Update test fixtures to include hf_aprs_decode_enabled and use the current spectrum watch sender type in remote client tests.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add client-side command plumbing, HTTP endpoint handling, and frontend interception so bandwidth changes are applied per active virtual channel and survive reconnects.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Handle AUDIO_MSG_VCHAN_BW in the audio server path and apply per-channel filter bandwidth through the SoapySDR virtual channel manager.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add virtual-channel bandwidth control to the shared core API and audio protocol constants for client/server coordination.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- trx-server/rig_handle: remove dead vchan_manager field (was set but
never read after the virtual-channel refactor)
- trx-server/listener: remove now-missing vchan_manager initializer
- trx-server/main: remove vchan_manager_for_handle intermediates that
only fed the dropped field
- trx-server/audio: suppress too_many_arguments on run_audio_listener
- trx-frontend-http/server: suppress too_many_arguments on build_server
- trx-core/vchan: update module doc comment to not reference the
removed RigHandle::vchan_manager field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
MARINE was a composite mode that ran both AIS and VDES decoders
simultaneously. It is now fully replaced by allocating two virtual
channels — one tuned to the AIS frequencies and one to VDES — each
decoded independently.
- trx-core/state: remove RigMode::MARINE variant
- trx-protocol/codec: remove MARINE parse/serialize
- trx-backend-ft817: remove MARINE from unsupported-mode guard
- trx-backend-ft450d: remove MARINE from FM CAT code mapping
- trx-backend-soapysdr: remove MARINE from bandwidth table, supported
modes list, AIS channel activity check, parse_rig_mode, vchan_impl
bandwidth table, demod selection, dsp/channel bandwidth / sample-rate
/ IQ-tap guards
- trx-server/audio: remove MARINE from AIS and VDES decoder activation
- trx-server/rig_task: remove MARINE from audio-streaming mode list
- trx-server/main: remove MARINE from bandwidth table, mode parser,
VDES channel subscription match
- app.js: remove isMarineMode(), MARINE entry in MODE_BW_SPECS, MARINE
bandwidth specs block in visibleBandwidthSpecs(), MARINE from
decoder status mode lists, MARINE BW-edge drag guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Allow users to allocate multiple virtual channels independently of
browser tab count. Channels survive SDR center-frequency retuning as
long as they stay within the capture bandwidth; channels that fall
outside the SDR span are automatically destroyed.
Changes:
- trx-core: add AUDIO_MSG_VCHAN_DESTROYED (0x12) wire constant;
add default subscribe_destroyed() to VirtualChannelManager trait
- trx-backend-soapysdr: update_center_hz() detects OOB channels,
removes them, fires destroyed_tx broadcast; add destroyed_sender()
and subscribe_destroyed() override
- trx-server/audio: recv_destroyed() helper avoids select! busy-loop
for non-SDR backends; send AUDIO_MSG_VCHAN_DESTROYED to client when
a channel is evicted server-side
- trx-client/audio_client: persist active_subs across TCP reconnects,
re-subscribe on reconnect; handle AUDIO_MSG_VCHAN_DESTROYED by
pruning vchan_audio map and forwarding UUID via vchan_destroyed_tx
- trx-frontend/lib: add vchan_destroyed broadcast field to
FrontendRuntimeContext
- trx-client/main: wire vchan_destroyed_tx into audio client and
frontend runtime context
- trx-frontend-http/vchan: remove per-session one-channel limit in
allocate(); replace auto-evict in release_session_on_rig() with
subscriber-count-only update; add remove_by_uuid() for server-
triggered OOB destruction (skips redundant VChanAudioCmd::Remove)
- trx-frontend-http/server: spawn background task that forwards
vchan_destroyed broadcast to ClientChannelManager.remove_by_uuid()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three bugs prevented vchan audio from working reliably:
1. vchan.js: `vchanReconnectAudio` returned before updating
`_audioChannelOverride` when audio was inactive. Switching to
a virtual channel with audio off then starting audio manually
would connect to the primary channel instead. Move the override
update before the rxActive guard so it always reflects the
active channel.
2. audio.rs: `audio_ws` returned 404 immediately if the channel
was not yet in `vchan_audio`. The entry is populated when
`AUDIO_MSG_VCHAN_ALLOCATED` arrives from the audio TCP client,
which can lag the HTTP allocation by up to ~100 ms. Replace the
instant 404 with a 2-second polling loop (50 ms intervals) so
the WebSocket upgrade waits for the channel to be ready.
3. vchan.rs: `release_session_on_rig` evicted zero-subscriber
channels silently — no `VChanAudioCmd::Remove` was sent.
Collect evicted channel IDs before retain() and send Remove
commands so the server-side DSP pipeline and Opus encoder are
torn down properly on session disconnect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Add vchanSyncModeDisplay() in vchan.js; called from vchanSyncAccentUI()
and vchanSubscribe() so the mode picker always reflects the active
virtual channel's mode on switch and on channel-list refresh
- Guard the rig-state mode picker update in render() so it is skipped
when vchanIsOnVirtual() is true, preventing primary-channel mode from
overwriting the virtual channel selection
Note: per-channel audio and decoder output require server-side protocol
changes (separate Opus streams per virtual channel) and are not yet
implemented.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When subscribed to a non-primary virtual channel the bandwidth overlay
was still anchored to lastFreqHz (channel 0). Resolve the effective
center from the active vchan when vchanIsOnVirtual() is true.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Draw sky-blue dashed/solid lines on spectrum overlay for each vchan
- Active virtual channel gets a solid line; inactive ones are dashed
- Validate freq against SDR capture window in vchanSetChannelFreq and
show a showHint error when tuning out of bandwidth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Virtual channel display:
- vchan.js: wrap refreshFreqDisplay() so the main freq field always shows
the active virtual channel's frequency instead of channel 0's; expose
vchanSyncAccentUI() to add vchan-ch-active CSS class (colored border) to
#freq and #spectrum-bw-input when on a non-primary channel
- style.css: --vchan-color (#38bdf8 sky-blue), .vchan-ch-active box-shadow,
vchan-picker active button left-border accent
Scheduler multi-channel slots:
- scheduler.rs: add center_hz (Option<u64>) and bookmark_ids (Vec<String>)
to ScheduleEntry; SchedulerStatus gains last_center_hz and
last_bookmark_ids; background task sends SetCenterFreq before SetFreq
when center_hz is set and records extra bookmark_ids in status
- scheduler.js: center-freq input and extra-channel bookmark picker (tag
list with + / × buttons) in the add-entry form; extra channels shown in
the entries table
- index.html: center freq field + extra bookmark picker widgets; table
gains Center freq and Extra channels columns
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When on a non-primary (virtual) channel, redirect freq and mode changes
to the channel metadata API instead of the server:
- vchan.js: add vchanIsOnVirtual(), vchanSetChannelFreq/Mode(); expose
window.vchanInterceptMode() hook; wrap window.setRigFrequency so all
callers (jog, freq input, bookmarks, spectrum click) are automatically
redirected without modification
- app.js: check vchanInterceptMode() in applyModeFromPicker() before
posting /set_mode
- bookmarks.js: check vchanInterceptMode() for mode in bmApply();
setRigFrequency() redirect is automatic via the vchan.js wrapper;
bandwidth and decoder toggles still apply regardless of channel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- VirtualSquelchConfig is only used in tests; gate import with #[cfg(test)]
- fixed_slot_count is reserved for future use; mark #[allow(dead_code)]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add VirtualChannelManager trait in trx-core::vchan with types VChannelInfo,
VChanError, and SharedVChanManager alias. Re-export from trx-backend::vchan.
Implement SdrVirtualChannelManager in trx-backend-soapysdr:
- Wraps Arc<SdrPipeline> + shared AtomicI64 center_hz
- add_channel / remove_channel / set_channel_freq / set_channel_mode
- Slot-stability: on remove, shifts pipeline_slot for surviving channels
- update_center_hz: recomputes IF offsets for all virtual channels on retune
- update_primary_meta: keeps channel-0 freq/mode in sync for API consumers
Wire into SoapySdrRig (holds Arc<SdrVirtualChannelManager>, exposes
channel_manager()), SdrPipeline (shared_center_hz AtomicI64), and RigHandle
(vchan_manager: Option<SharedVChanManager>). main.rs extracts the manager
before boxing the SDR rig and stores it in the handle.
Add max_virtual_channels to SdrConfig (default 4, TOML-configurable).
Add 5 unit tests: add, remove, permanent guard, cap, out-of-bandwidth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the fixed channel_dsps Vec (thread-owned) with an
Arc<RwLock<Vec<...>>> shared between the IQ read thread and the async
side, enabling live add/remove of virtual DSP channels.
Cache audio construction params in SdrPipeline so add_virtual_channel()
can build ChannelDsp instances without being re-passed them. Add:
- SdrPipeline::add_virtual_channel() / remove_virtual_channel()
- SoapySdrRig::virtual_channel_add/remove/set_freq/set_mode()
- SoapySdrRig::center_hz() / half_span_hz() accessors
The IQ read loop holds a brief read lock (~2 ms per block) while
processing all channels; write lock for add/remove waits at most one
block. All 27 existing tests continue to pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
start == end previously matched nothing (empty range). Now treated as a
24-hour window, making it easy to define a catch-all bookmark without
manually entering 00:00–23:59.
UI shows "All day / —" in the entries table and tooltip hints on both time
inputs explain the convention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Each ScheduleEntry can now carry its own interleave_min, overriding the
config-level default for that slot in the cycle. The cycle length is the
sum of all active entries' effective durations (weighted), so entries with
longer individual interleave times occupy proportionally more time.
UI: "Interleave (min, optional)" input in the add-entry form; value shown
in the entries table (displays "—" when using the config default).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove custom padding, border-radius, color, cursor, and hover rules from
.sch-save-btn, .sch-reset-btn, and .sch-remove-btn — the global button rule
already handles all of that consistently across every theme.
.sch-save-btn retains only the accent-green background/border-color to mark
it as the primary action; the global hover/active/disabled transitions still
apply.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The TimeSpan bookmark <select> was populated in wireSchedulerEvents() which
runs before the apiGetBookmarks() fetch completes, leaving it empty.
Moved population to populateTsBookmarkSelect() called from loadScheduler()'s
.then() callback so bookmarkList is already filled.
Also pre-fill grayline lat/lon from serverLat/serverLon when the field has
no saved value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When multiple time-span entries are active simultaneously, the scheduler
now cycles through them by slot: slot = floor(utc_min / interleave_min) % count.
The interleave_min field is optional (null disables, first match wins).
UI: "Interleave time (min)" number input in the TimeSpan section with a
hint explaining the behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
initScheduler() runs before the first SSE event, so lastRigIds is empty.
Now applyRigList() calls reloadSchedulerRigSelect() whenever the rig list
updates, and renderSchedulerRigSelect() loads the config for the first rig
if currentRigId was previously unset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Implements a scheduler that retunes the rig automatically when no SSE
clients are connected. Two modes are supported:
- Grayline: tunes to per-period bookmarks (dawn/day/dusk/night) based on
an inline NOAA solar algorithm given station lat/lon.
- Time Span: tunes to bookmarks within user-defined UTC windows; midnight-
spanning intervals supported.
Backend:
- SchedulerStore (PickleDB, sch:{rig_id} keys) in scheduler.rs
- spawn_scheduler_task polls every 30 s, checks context.sse_clients == 0,
sends SetFreq + SetMode via RigRequest with rig_id_override
- HTTP API: GET/PUT/DELETE /scheduler/{rig_id}, GET …/status
- sse_clients Arc<AtomicUsize> added to FrontendRuntimeContext and shared
with the SSE counter in build_server (single source of truth)
- /scheduler/ added to Read auth routes (write requires Control)
Frontend:
- Scheduler tab (clock icon, 6th position) with Grayline/TimeSpan UI
- scheduler.js plugin: loads config + bookmarks, live status polling
every 15 s, write controls hidden for Rx-role users
- CSS .sch-* component styles added to style.css
- SCHEDULER.md design document at repo root
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
AIS vessels transmit every 2-30 s; without deduplication the 24-hour ring
buffer can hold tens of thousands of entries, making the /decode/history
response huge and causing O(n^2) DOM thrashing on the client side.
- Add AIS_HISTORY_MAX = 10 000 to cap the ring buffer memory footprint.
- snapshot_ais_history() now returns the latest message per MMSI (one entry
per vessel), sorted ascending by ts_ms so the client replays in order.
This matches APRS history behaviour: APRS stations transmit infrequently so
their history is naturally compact; AIS history is now equally compact.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Two root causes:
1. /decode/history was classified as Control in the auth router (not listed
in Read routes), so it returned 401/403 when auth is enabled and the
user had no session or rx-only role. Add it to the Read route list.
2. connectDecode() was called from window.load unconditionally, before the
auth flow completed. On first load with auth enabled the session cookie
doesn't exist yet, so the history fetch fails silently. Move the call to
be alongside connect() in initializeApp(), login, and guest handlers so
it always runs with valid auth context.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
drainDecodeHistory() chunks work via setTimeout but flushLiveBuffer() was
called synchronously right after starting the drain, so live messages could
interleave with in-progress history chunks. Pass flushLiveBuffer as an
onDone callback so live messages are only dispatched once all history chunks
have been processed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Use --accent-green (the primary/lead accent color) for AIS vessel markers and
tracks instead of a hardcoded or red-based color, so they match the active
buttons and other prominent UI elements for every color scheme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace --accent-red with a new --ais-accent CSS variable (default #00aacc
cyan-blue) so AIS vessel markers and track lines are visually distinct from
other UI elements regardless of theme. Light theme uses a slightly darker
#0088aa for readability on the map.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
setAisState only updated heading/course/speed, silently dropping color and
outline fields. Extend it to also accept and apply those fields so theme
color refreshes take effect without recreating the marker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Read --accent-red CSS variable at draw time so markers, track lines, and
TrackSymbol icons automatically match the active color scheme. Add
refreshAisMarkerColors() called on theme toggle and style picker changes
to repaint existing markers without a page reload. Also buffer live SSE
decode messages until the /decode/history fetch settles to eliminate the
history-appears-after-reload race condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The global button transition and the :active scale(0.97) transform were
interfering with the translateY(-50%) centering, making the buttons jump
on press. Added transition:none and reduced :active to translateY(-50%)
only (no scale).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Raise the viewport cap from 60% to 75% of window height and relax the
aspect-ratio divisor from 1.9 to 1.55, giving the map more vertical
space without requiring fullscreen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Mobile Safari (iOS) blocks requestFullscreen() on non-video elements,
so the Fullscreen button silently did nothing.
Add a CSS-based fake fullscreen path:
- mapEnterFakeFullscreen() adds .map-fake-fullscreen to #map-stage
(position:fixed; inset:0; z-index:9000; height:100dvh) and
map-fake-fullscreen-active to <body> (overflow:hidden).
- toggleMapFullscreen() tries native fullscreen first; catches the
thrown NotAllowedError (or any other error) and falls back to the
CSS path. Also handles the case where requestFullscreen is absent.
- mapIsFullscreen() checks for the CSS class in addition to the
native fullscreen element references.
- mapExitFakeFullscreen() removes both classes on exit.
- Escape key exits CSS fake fullscreen (native handles its own Escape).
- sizeAprsMapToViewport() uses window.innerHeight for the fake path
since clientHeight may not reflect fixed layout synchronously.
- sizeAprsMapToViewport() is called via requestAnimationFrame after
toggling so layout is settled before the Leaflet invalidateSize().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Two bugs fixed:
1. Wrong vertical position of shift buttons and bookmark side panels.
top: calc((--spectrum-plot-height - --overview-plot-height) / 2)
evaluates to 0 when both vars are equal (default 160 px), so
translateY(-50%) placed the buttons at the top edge of .spectrum-wrap
instead of mid-canvas. Changed to calc(--spectrum-plot-height / 2).
2. Rapid clicks on the arrows did not accumulate: each call read
lastSpectrumData.center_hz which is only updated when the server
sends a new spectrum frame. Added spectrumCenterPendingHz to track
the optimistic center immediately on click; reset when the server
confirms a frame near the expected position.
Also hide .spectrum-bookmark-side on ≤640px (no horizontal room on
narrow phones); previously visible but clipped off-screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Fix 5-tab bottom nav (grid was repeat(4) with 5 tabs; About overflowed)
- Add SVG icons to each tab; show icon+label on mobile bottom nav
- Swipe left/right to switch tabs (excludes jog wheel, spectrum canvas,
map, scrollable containers and form inputs to avoid conflicts)
- Extract navigateToTab() helper used by both click and swipe handlers
- Collapse header subtitles at ≤640px to reclaim vertical space
- Bookmark table → 2-column card layout at ≤640px with ::before labels
- Audio volume labels switch to horizontal row layout at ≤520px;
squelch slider now also spans full width
- Controls tray uses overflow-x: auto (not visible) at ≤760px so
content wider than viewport scrolls rather than overflowing layout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move history replay out of the /decode SSE stream into a new
GET /decode/history JSON endpoint. The JS client now opens /decode
immediately for live packets (no gating) and fetches history in
parallel via fetch(), draining it in the background with the existing
chunked drainDecodeHistory() helper.
This ensures real-time decode messages are never blocked by a large
history payload, and removes the historyReceived gate entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
System font stack: replace bare 'sans-serif' with system-ui / -apple-system
/ BlinkMacSystemFont / Segoe UI chain — sharper rendering on all platforms
at zero extra load cost.
Button hover/active: add transition (100ms) + color-mix hover brightening
+ active depression (translateY 1px) to all buttons. Previously buttons had
zero visual feedback on interaction.
Scrollbar styling: thin (6px) custom scrollbars via ::-webkit-scrollbar and
scrollbar-width/color for Firefox. Thumb uses border-color tinted with the
accent on hover — matches each theme automatically via CSS variables.
Phosphor theme: classic green-phosphor CRT aesthetic — near-black background,
#39ff14 neon-green accent, glow text-shadow on the freq display, matching
spectrum/waterfall canvas palette. Both dark and light variants included.
Registered in the style picker select, setStyle() valid list, and
CANVAS_PALETTE.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Previously the server emitted N individual SSE events (one per decoded
message) followed by a history_done sentinel. With a 1.3 MB history
this caused thousands of EventSource onmessage callbacks each blocking
the JS main thread, interrupting audio playback and spectrum rendering
for 50+ seconds.
Server: serialize the entire history Vec as a single named "history"
event containing a JSON array, then chain directly into the live
decode stream. One serde_json::to_string call instead of N.
JS: listen for the "history" event, parse the array once, pass it to
the existing drainDecodeHistory() chunked dispatcher (30 msgs per
setTimeout slice to stay off the main thread), then gate onmessage
dispatching on historyReceived. Removes the historyBuffer accumulator
and the history_done event entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Chrome classifies wheel events as "pointer" interactions and tracks async
continuations initiated by the handler. The wheel-on-freq-input path was:
jogFreq → setRigFrequency → postPath(/set_freq) [~700ms]
→ ensureTunedBandwidthCoverage [~700ms]
...two sequential network round-trips totalling ~1400ms of INP.
Three changes:
1. yieldToMain(): add a scheduler.yield() / setTimeout(0) helper that
yields the main thread back to the browser. Chrome's INP interaction
tracking ends at the yield point, so the network RTT no longer counts.
2. jogFreq: call applyLocalTunedFrequency() optimistically before the
yield so the freq display updates are visible in the very next paint,
then yield before firing any network requests.
3. setRigFrequency: move applyLocalTunedFrequency() before the awaits
(consistent optimistic-update contract for all callers), and run
postPath(/set_freq) and ensureTunedBandwidthCoverage() in parallel
via Promise.all — they are independent server operations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace Arc<Mutex<SharedSpectrum>> with Arc<watch::Sender<SharedSpectrum>>
throughout the stack:
- SharedSpectrum: remove revision counter, derive Clone, make fields pub,
rename replace() → set(). The watch channel handles dedup natively.
- FrontendRuntimeContext.spectrum: Mutex → watch::Sender; SSE clients
call .subscribe() to get a push-based receiver at zero polling cost.
- RemoteClientConfig: derive Clone, switch spectrum field to match.
Spectrum polling moves to a dedicated TCP connection (run_spectrum_connection
+ handle_spectrum_connection spawned as a separate tokio task). This
eliminates head-of-line blocking: spectrum timeouts no longer stall state
polls or user commands on the main connection. Each side reconnects
independently; the spectrum task marks the frame None while reconnecting.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Subscribe each SSE client to the watch::Sender<SharedSpectrum> rather
than running an IntervalStream that locks a Mutex every 40 ms. Clients
are now woken push-style exactly when new spectrum data arrives; the
revision counter is no longer needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
State deduplication via PartialEq + send_if_modified:
Derive PartialEq on the full RigState / RigSnapshot type tree
(Freq, Band, RigInfo, RigCapabilities, RigStatus, RigTxStatus,
RigRxStatus, RigControl, RigVfo, RigVfoEntry, RigFilterState,
RdsData, SpectrumData, RigState, RigSnapshot). Use
state_tx.send_if_modified() in refresh_remote_snapshot() so
WatchStream only wakes SSE /events subscribers when state
actually changed; with a stable rig this eliminates ~1.3
spurious JSON serialisations per second per connected client.
Cache-remote-rigs skip on unchanged list:
cache_remote_rigs() was rebuilding the Vec and cloning every
field on every 750 ms poll. Add a structural check (rig_id,
display_name, initialized, audio_port) and return early when
nothing has changed — the common steady-state case.
RDS JSON pre-serialised at ingestion:
SharedSpectrum.replace() now serialises the optional RDS object
once and stores it alongside the Arc<SpectrumData> frame.
Each /spectrum SSE client's 40 ms tick reads the cached string
instead of calling serde_json::to_string() per-client per-tick.
Add serde_json to trx-frontend Cargo.toml to support this.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three hot-path optimizations in the client polling loop and SSE
spectrum stream:
- Set TCP_NODELAY on the client→server connection so each framed
JSON command is sent immediately instead of being held up to 40 ms
by Nagle's algorithm.
- Wrap SpectrumData in Arc<> inside SharedSpectrum. snapshot() was
cloning the full bin vector (~8 KB for 2048 f32 bins) for every SSE
/spectrum client on every 40 ms tick. With N clients that is N×8 KB
per tick; now replace() pays one Arc::new() and each client gets an
O(1) pointer clone.
- Eliminate the format!("{}\n", payload) intermediate String in the
three send_command / send_command_no_state_update / send_get_rigs
call sites. Push '\n' in-place on the serialised payload String
instead of allocating a second buffer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three causes of >30 s SSE stalls:
1. SPECTRUM_IO_TIMEOUT was 300 ms — transient server jitter triggered
false-positive spectrum failures and immediate TCP disconnects.
Raised to 1 s to tolerate brief load spikes.
2. reconnect_delay was never reset after a successful TCP connect, so
after a few spectrum-induced disconnects the backoff reached 10 s.
Each new disconnect then cost 10 s of stale SSE state, and several
cycles accumulated to >30 s. Reset to 1 s on every successful
TCP connect so recovery stays fast.
3. SSE pings were emitted as comments (": ping"), which EventSource
never exposes to onmessage. lastEventAt was therefore never updated
by pings, causing the JS heartbeat to force-reconnect every ~20 s
even on healthy, stable connections. Changed to a named "ping"
event and added es.addEventListener("ping", …) to update lastEventAt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
inject_frontend_meta was calling serde_json::from_str + to_string on
every state update, adding two full JSON round-trips per SSE event.
Replace with string-level injection: strip the closing }, serialize only
the extra meta fields once, and re-close the object. The state JSON is
now serialized exactly once per update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the JSON f32 array (~7.5 KB/frame) with a named SSE event "b"
carrying base64-encoded i8 bins (~1.4 KB/frame, ~5x reduction):
event: b
data: {center_hz},{sample_rate},{base64_i8_bins}
1 dB per step covers the -128…+127 dBFS display range, sufficient for
visualization. RDS is stripped from the spectrum frame and emitted as a
separate named "event: rds" only when the payload changes. The JS
decoder uses atob() + sign-extension to reconstruct the float bin array.
A minimal inline base64 encoder is added server-side (no new crate).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add flate2 dependency and a new AUDIO_MSG_HISTORY_COMPRESSED (0x0a)
wire type. The server gzip-compresses the full history blob before
sending; JSON history compresses ~10-20x so both transfer size and
client wait time drop significantly. The client decompresses and
dispatches sub-messages from the embedded framed stream. MAX_PAYLOAD_SIZE
is kept at 1 MB for normal messages; a separate 16 MB limit is applied
only to the compressed history type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Add "Enable HF APRS" toggle button to the HF APRS tab (same style as
FT8/WSPR); button is disabled during TX like other decoder toggles
- app.js: sync button text/colour from SSE state updates
- hf-aprs.js: connect button click to /toggle_hf_aprs_decode
- bookmarks.js: add "HF APRS" checkbox to Add/Edit Bookmark decoder
section; bmReadDecoders/bmWriteDecoders handle "hf-aprs" key; bmApply
toggles the decoder to match bookmark preference on recall
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Assigns message type 0x09 to HF APRS decoded frames on the binary
audio TCP channel and wires it up in all three layers:
- trx-core: AUDIO_MSG_HF_APRS_DECODE = 0x09
- trx-server: emit 0x09 in the live dispatch match and include
hf_aprs history in the connection-open replay blob
- trx-client: recognise 0x09 and forward to the decode broadcast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Adds a second APRS demodulator path tuned for the HF APRS standard
(300 baud Bell 103-style AFSK, mark=1600 Hz / space=1800 Hz), active
on RigMode::DIG. Shares AX.25 framing, APRS parsing, APRS-IS uplink,
and frontend display with the existing VHF stack.
- trx-aprs: parameterise Demodulator::new(); add AprsDecoder::new_hf()
- trx-core: HfAprs variant in DecodedMessage; hf_aprs_decode_enabled /
hf_aprs_decode_reset_seq in RigState/RigSnapshot; SetHfAprsDecodeEnabled
and ResetHfAprsDecoder commands; handlers.rs fallback arm updated
- trx-protocol: client command variants + bidirectional mapping; test
fixture updated
- trx-server: run_hf_aprs_decoder() task (activates on DIG mode);
hf_aprs history in DecoderHistories; rig_task command dispatch;
aprsfi uplink forwards HfAprs via OR-pattern
- trx-frontend: hf_aprs_history in FrontendRuntimeContext
- trx-frontend-http: prune/record/snapshot/clear helpers; SSE history
replay; toggle_hf_aprs_decode + clear_hf_aprs_decode endpoints;
/hf-aprs.js endpoint; HF APRS tab in web UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Safari stalls noticeably on gl.bufferData (which reallocates the GPU
buffer) when called multiple times per frame. Replace with a pre-
allocated scratch Float32Array and gl.bufferSubData, which only uploads
new data without reallocating. The GPU buffer is grown with bufferData
only when the scratch outgrows it (amortised doubling). Also eliminate
the per-draw-call `new Float32Array(vertices)` allocation in favour of
scratch.set() + subarray(), removing per-frame GC pressure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace per-message write + flush-every-64 with a single in-memory blob
that is sent via one write_all + one flush. Add estimated_total_count()
to DecoderHistories for pre-allocation. Eliminates N/64 partial flushes
and repeated small writes that dominated replay latency for large APRS
histories.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
During history restore with thousands of APRS packets, the console.log
in addAprsPacket was called for every entry, slowing the replay down
significantly. Remove it entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
ResetCwDecoder already bumped cw_decode_reset_seq but omitted the
history flush that APRS, FT8, and WSPR all perform. Wire
clear_cw_history() into the handler to match the pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
No reset event for CW is wired in rig_task.rs, so the method was
dead code and triggered a compiler warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Subscribe to the first sdr.channels[] entry configured as VDES or
MARINE instead of always using channel 0. The ChannelDsp IQ tap only
emits samples when its own mode is VDES/MARINE, so the two must agree.
Fix vdes_sr to mirror channel.rs pipeline_rates(): use
audio_sample_rate.max(96_000) as the target rather than the hardcoded
96_000. The two diverge when audio_sample_rate > 96_000, causing the
VdesDecoder to use the wrong symbol-to-sample ratio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the free-running phase counter in slice_pi4_qpsk_symbols with
linear interpolation to sample the IQ stream at exact symbol epochs.
Add estimate_differential_cfo() that uses the 4th-power method to
cancel pi/4-QPSK modulation phase, yielding a per-burst CFO estimate
that is removed before differential decoding.
At the ~1.25 samples/symbol IQ rate produced by the current decimation
pipeline, a closed-loop Gardner or Mueller-Müller TED requires at least
2 SPS and cannot be applied; the open-loop linear interpolation is the
best achievable without restructuring the IQ tap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
History persistence lives in trx-server, not trx-client.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move persistent history from trx-client to trx-server where decode
events originate. History for AIS, VDES, APRS, CW, FT8, and WSPR is
loaded from ~/.local/cache/trx-rs/history.db at startup and flushed
to disk every 60 seconds. CW events are now also stored in
DecoderHistories and replayed to connecting clients, consistent with
all other decoder types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add pickledb-backed persistent history store for all decoder types
(AIS, VDES, APRS, CW, FT8, WSPR). History is loaded from
~/.local/cache/trx-rs/history.db at startup and flushed to disk
every 60 seconds. On load, entry timestamps are reconstructed from
stored Unix ms values so 24h pruning continues to work correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Two problems prevented reliable recovery after a persistent overflow:
1. Restart storm: once read_error_streak >= 3, every subsequent read
failure triggered a deactivate/activate cycle, potentially preventing
the hardware from stabilising. Fix: after a successful restart, reset
read_error_streak to 1 so the stream gets 2 more failures before the
next restart attempt.
2. Stuck-deactivated stream: if activate() failed after overflow, the
stream was left deactivated. Subsequent reads returned non-overflow
errors which handle_read_error ignored (Ok(false)), so the stream was
never reactivated. Fix: add a high-streak fallback (>= 10 consecutive
errors of any kind) that also attempts a full deactivate/activate
restart, covering the stuck-deactivated case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Two bugs triggered by a SoapySDR IQ overflow:
1. Spectrum dies permanently (trx-client): when GetSpectrum times out
(300ms SPECTRUM_IO_TIMEOUT), the error was silently swallowed and
the spectrum buffer cleared. The in-flight response remained in the
TCP receive buffer, desynchronising all subsequent reads so every
poll kept failing. Fix: propagate the error so handle_connection
returns and the outer loop reconnects, restoring TCP sync.
2. CTRL+C hangs trx-server: after IQ overflow, the sdr-iq-read thread
can get stuck in a blocking SoapySDR/USB call (deactivate/activate
with no timeout). Tokio received SIGINT and aborted async tasks, but
the process could not exit while the native thread was blocked in
uninterruptible I/O. Fix: call std::process::exit(0) after the
graceful shutdown sequence so the OS forcibly terminates all threads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Server emits an SSE sentinel event (history_done) after replaying
stored history. Client buffers all incoming messages until the sentinel
arrives, then drains the buffer in 30-event chunks via setTimeout so
the browser can handle input between batches. Live events after the
sentinel are dispatched immediately as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Prepend the in-progress line to the bar render so characters appear
immediately rather than waiting for a newline or 5s gap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Show a live decode bar on the overview strip when in CW/CWR mode,
matching the APRS and AIS bar pattern. Accumulates decoded characters
into lines (split on newline events or >5s gaps), keeps a 15-minute
rolling history, and shows up to 8 recent lines with timestamp and
WPM/tone metadata. Clears on resetCwHistoryView.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add cwAutoLocalOverride flag in cw.js to block server-state snapshots
from overriding the checkbox while a user-initiated POST is in-flight.
Expose applyCwAutoUiFromServer for app.js render() to call instead of
applyCwAutoUi, preventing a racing SSE event carrying the old cw_auto
value from immediately undoing the user's toggle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Reduce analysis window from 50ms to 10ms so the decoder can detect
dits at 25+ WPM (at 25 WPM a dit is 48ms, shorter than the old window)
- Fix dot/dash classification threshold from 2.0× to 1.5× unit_ms;
ITU Morse dah = 3× dit, so the midpoint boundary is 1.5×
- Replace O(n) on_durations.remove(0) with drain() to trim the window
- Remove pointless emit_text() wrapper; callers now call emit_event()
directly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add per-channel processing gating and disable hidden AIS channel DSP unless mode is AIS or MARINE, reducing continuous IQ read-loop CPU load in normal operation.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add click-to-tune behavior on overview waterfall matching spectrum interactions, restore bookmark marker lines in the overlay using category colors, and keep current-tuned frequency marker visually distinct.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Skip IQ fanout buffer cloning when no subscribers exist and apply backoff on repeated zero-length reads to avoid hot-loop CPU spikes.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Ensure overview waterfall incremental updates continue on HiDPI and anchor bookmark chips to the top of the full spectrum view (waterfall + waveform).\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Improve WebGL runtime performance by caching/downsampling overview waterfall texture updates and batching marker/dashed-line draws; keep bookmark chips anchored at the top of the waterfall area.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace Canvas2D rendering in spectrum, overview, signal overlay, and CW tone picker with a shared WebGL renderer and wire the new asset into frontend HTTP routes.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a right-side slider to control the waterfall/waveform split and\npersist the selected ratio locally.\n\nRework spectrum height layout so manual resize adjusts total plot height\nwhile split controls the overview/spectrum ratio.\n\nKeep center-frequency arrows and side bookmark stacks vertically centered\nwithin the full spectrum view container.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Make the spectrum resize grip easier to use and style it closer to\nexisting controls.\n\nKeep auto-max behavior by default while allowing manual drag to\nresize beyond viewport fill, enforcing only the minimum height.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Make spectrum plot use maximum available viewport height by default.
Add draggable vertical resize grip with a reasonable minimum height
and double-click reset back to auto-max.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When mode is set to CW/CWR, force cw_decode_enabled=true in state
application to avoid stale disabled decode state.
Also route initial mode setup through apply_mode.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Enforce CW decode processing only when mode is CW/CWR and decoder
is enabled, even within the receive loop, and remove CW path RMS
zero-gating to preserve weak tone decoding.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Set cw_decode_enabled default to true so CW decoding can start
without an unavailable UI toggle.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep manual CW tone setting range at 100-10000 Hz, but limit auto-tone
scanning to 300-1200 Hz to avoid locking onto high-frequency noise.
Retain Nyquist-safe upper clamping.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Expand CW decoder tone scan/control range to 100-10000 Hz and cap the
upper bound by sample-rate Nyquist to keep detection stable.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Expand server-side CW tone command clamping to 100-10000 Hz to match
frontend controls and picker behavior.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix CW picker redraw when the decoder sub-tab becomes visible to avoid
white/blank canvas rendering.
Widen CW tone picker/input range to 100-10000 Hz and raise CW/CWR
bandwidth max to 9 kHz.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Apply an RMS gate to near-silent decoder input frames and zero them
before APRS/CW/FT8/WSPR processing to avoid false decodes from
unsquelched/no-signal noise.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove CW tone-window clipping to current spectrum edge coverage so
the picker always renders the audio range and does not blank out.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Render the CW tone picker as an audio-frequency spectrum trace with grid,
line and filled area instead of a normalized gradient wash.
Keep exact click-to-tone selection and marker behavior.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Use an audio-window tone picker for CW with exact click-to-tone mapping.
Make + Add Bookmark inherit the shared button style.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Gate the CW decoder task on cw_decode_enabled in both startup and state changes.
This restores the UI toggle and prevents decode activity when CW decode is off.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Restore empty events on tone edges so the frontend signal indicator updates again.
Add synthetic-tone tests covering transitions and basic decoding.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Align the CW tone picker to the actual tone window and stop live CW decode events from overriding manual tone selection.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Send boolean values for the CW auto toggle query and improve the CW tone picker waterfall contrast.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Mirror live spectrum, tuned frequency, and bandwidth state onto the window object so the CW tone picker can render from current spectrum data.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Make FT-817 mode setting fail for unsupported modes instead of silently mapping SDR-only modes onto FM.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove SDR-only decoder modes from the FT-817 supported mode list so they are not shown in that rig's mode picker.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a mini waterfall-based CW tone selector in the plugin tab and make CW auto mode apply only to WPM.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Raise the AM audio AGC target and headroom again so the restored envelope demod path plays at a stronger level.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Retune the AM audio AGC target, timing, and headroom so the restored envelope demod path plays back at a stronger level.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Revert the recent AM coherent detector experiment and restore the original envelope detector path.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove half-wave clipping from the AM coherent detector output and slow the carrier-reference tracking to reduce audible distortion.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the raw AM envelope detector with a limiter-derived coherent detector and update backend tests for the current channel DSP constructor.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep geo sanity checks, but treat most marginal VDES decodes as low confidence instead of rejecting them outright.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Score parsed VDES payloads and fall back to unsynced output for obviously weak or invalid decodes, including invalid geo boxes.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep the current VDES thresholds but restore a maximum burst duration so continuously open detections still produce frames.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Restore the more permissive intermediate VDES burst thresholds while keeping the current detector reset and close behavior.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Publish decoded VDES positions into the map and revert the VDES burst detector to its original gating thresholds.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Raise the VDES burst thresholds and minimum burst length to cut false positives after the recent sensitivity increase.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Recognize VDES decode frames in the audio client and keep sweet-spot scans from centering directly on the tuned frequency.
Co-authored-by: Stan Grams <sjg@haxx.space>
Signed-off-by: Stan Grams <sjg@haxx.space>
Finish the pending MARINE frontend and decoder activation wiring, and lower the VDES detector power floors so weak signals are eligible for burst detection in the same power domain used by the IQ path.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Keep the SoapySDR VDES and MARINE IQ path at a much higher channel sample rate instead of collapsing toward the normal audio rate, so the decoder receives usable complex baseband for the 76.8 ksps VDES signal.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Lower the VDES burst detector thresholds further, shorten the minimum burst and end timing, and add a max-burst timeout so weak or continuous signals are more likely to finalize into diagnostic frames instead of staying invisible.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Reduce the VDES burst detector trigger and sustain thresholds so weaker received bursts enter and finalize more readily instead of being ignored by the IQ-side detector.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add an FT8 live overlay bar, align APRS top controls with the other decoder tabs, advertise MARINE in the SoapySDR mode list, and make the VDES decoder emit raw unsynced diagnostic frames instead of dropping weak bursts outright.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Keep weak VDES bursts visible by emitting unsynced diagnostic frames instead of dropping them, remove receiver badges from FT8 and WSPR history rows, and carry the current MARINE composite-mode scaffolding through the shared mode enums and backend mappings.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a first VDES payload parser on top of the decoded bitstream so the server surfaces message labels, source and destination IDs, session IDs, ASM IDs, ack fields, geographic hints, and payload previews. Update the VDES frontend pane to render those parsed fields in the history and live bar.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Improve the VDES decoder with sync/rotation metadata and a first hard-decision rate-1/2 Viterbi stage after deinterleaving, then surface the extra lock state in the VDES frontend. Also fix the strict clippy findings in AIS, frontend bookmarks, and the server audio stack signature.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a dedicated VDES plugin tab and live bar, stop reusing the AIS vessel UI, and serve a separate VDES frontend script. Rework the SDR backend so VDES receives a single 100 kHz IQ tap, then replace the fake AIS-clone decoder path with an early M.2092-1 oriented complex-baseband scaffold using burst detection, coarse pi/4-QPSK slicing, and TER-MCS-1.100 frame heuristics.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a new trx-vdes decoder path alongside AIS, wire VDES through the server/frontend decode pipeline, and fix the web map so AIS vessel symbols load correctly and the TRX receiver marker appears when location data arrives.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Bring back the transition-locked AIS sampler with adaptive symbol timing and shaped discriminator filtering while keeping the shorter-frame acceptance path.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Render AIS vessels with heading-aware ship symbols, keep selected tracks on click, and size the map to fit the viewport cleanly without overextending the page.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Show AIS vessel tracks only for the selected marker, keep the APRS and AIS history panes viewport-sized with internal scrolling, and tighten the APRS history controls with shorter bookmark-scale buttons.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the APRS plugin log with a richer history view that adds summaries, filtering, pause/resume, duplicate collapsing, structured rows, row actions, and expandable details.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Revert the AIS decoder to the simpler sampling path while keeping the valid frame-length fix, and correct frontend frequency-range validation so SDR uses all reported bands and shows an explicit popup when tuning is unsupported.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Improve the AIS decoder timing recovery, add AIS vessel linking and map trails, and make the AIS/APRS decoder panels behave like mode-bound views with full-height history panes.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Refine the AIS plugin tab with summary cards, clearer vessel rows, and better live-bar deduping, and let long side bookmark names wrap cleanly inside their chips.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add dual-channel AIS decode support across the SoapySDR backend, server decode pipeline, and frontend plugins, including the new AIS tab, live bar, and map filtering.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Move bookmark frequency markers from drawSpectrum() to drawSignalOverlay()
so dashed lines span both the waterfall and waveform canvases
- Assign distinct palette colors to named categories; uncategorised uses
--accent-yellow resolved from the live theme at runtime
- Compute WCAG-compliant foreground color (dark/light) per category so label
text is always legible against the solid background
- Add text search input to the Bookmarks toolbar; filters by name, category,
and comment client-side without re-fetching from the server
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Named categories are sorted alphabetically and assigned colours from an
8-colour palette (blue, green, orange, red, purple, teal, pink, indigo).
Uncategorised bookmarks fall back to --accent-yellow (the leading UI
colour). Both the canvas dashed lines and the axis span labels (icon,
text, border, background) reflect the category colour via CSS custom
properties --bm-cat-color / --bm-cat-bg / --bm-cat-border.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Switch #spectrum-bookmark-axis from position:relative (creates gap) to
position:absolute at top:var(--spectrum-plot-height) with
transform:translateY(-50%), so it floats centred on the boundary
without affecting layout. z-index:6 keeps it above the BW/freq
selector signal overlay (z-index 4).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Relocate #spectrum-bookmark-axis from inside .spectrum-wrap to the
flex gap between .overview-strip (waterfall) and #spectrum-panel
(waveform). Give it z-index:5 so labels sit above the signal-overlay-
canvas (BW/freq selector, z-index:4). Drop the now-unneeded
#spectrum-freq-axis.bm-axis-open border-radius hack and the
corresponding JS class toggle.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace hardcoded amber values with var(--accent-yellow) throughout the
bookmark axis labels, so they automatically adapt to all UI themes.
Centre spans vertically with top:50%/translate(-50%,-50%) for equal
padding above and below. Canvas dashed line uses pal.waveformPeak
(already theme-aware) at 0.65 opacity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Canvas: remove clashing ribbon polygon; draw only a dashed amber
vertical line at each bookmark frequency
- Axis labels: replace clip-path ribbon with inline SVG bookmark icon
(amber rectangle with V-notch) + name text; add more padding
- DOM rebuild: only rebuild axis spans when the set of visible bookmark
IDs changes; always update left positions for smooth pan/zoom
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
let-declared bmList is not a window property, so window.bmList in
app.js always returned undefined. Change to var so it lands on window;
read it via typeof guard in app.js to stay safe if bookmarks.js is absent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Draw bookmark frequency markers on the spectrum canvas: amber vertical
line + ribbon shape (rectangle with V-notch) at each bookmark in view.
Below the freq axis, show a #spectrum-bookmark-axis row of clickable
amber ribbon labels (clip-path bookmark shape); clicking tunes the rig.
Labels auto-appear / collapse as bookmarks scroll in and out of view.
Server: reject POST/PUT with 409 Conflict when another bookmark already
exists at the requested freq_hz (BookmarkStore::freq_taken helper).
Client: bmFetch() triggers a spectrum redraw so markers appear
immediately on load without requiring a tab visit first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the free-text decoder input with FT8/WSPR checkboxes so users
cannot enter arbitrary decoder strings. Hide the "+ Add Bookmark" button
by default and show it only when the authenticated role is `control`
(or auth is disabled), matching the server-side write-endpoint guard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a "Bookmarks" tab between Main and Plugins in the tab bar.
HTML: tab panel with toolbar (category filter + Add Bookmark button),
an inline add/edit form (hidden by default, prefills freq/mode/BW from
the current rig state), and a sortable table showing all columns with
Tune / Edit / Del action buttons.
CSS: responsive bm-* classes following existing card/button theming,
works in both dark and light modes and all palette variants.
bookmarks.js: fetches bookmarks on tab activation, renders table with
event delegation, handles create/update/delete via REST, and applies a
bookmark by calling set_freq → set_mode → set_bandwidth, plus toggles
FT8/WSPR decoders when the stored mode is DIG.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
GET /bookmarks — list all (optional ?category= filter); rx role
POST /bookmarks — create; control role enforced in handler
PUT /bookmarks/{id} — update; control role enforced in handler
DELETE /bookmarks/{id} — remove; control role enforced in handler
Auth middleware classifies /bookmarks and /bookmarks/* as Read so rx
users can reach GET; write handlers call require_control() to reject
lower-privileged sessions with 403.
Also serves bookmarks.js via GET /bookmarks.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add pickledb and dirs dependencies. Introduce BookmarkStore wrapping
PickleDb behind Arc<RwLock<>> with list/get/insert/upsert/remove ops.
Keys stored as "bm:{id}" in ~/.config/trx-rs/bookmarks.db; falls back
to ./bookmarks.db when config dir is unavailable.
Wire BookmarkStore into build_server() as actix-web app_data so all
request handlers can share the store.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Limit the APRS live bar to the last 15 minutes and show that
window in the overlay header.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add an optional website_name config field and prefer it over
callsign for the linked web header title label.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
updateRdsPsOverlay was called on every spectrum frame (25 Hz)
regardless of mode, doing 15+ DOM element lookups and text updates
even in USB/AM/CW/etc. The server-side RDS DSP already only runs
in WFM; align the client:
- Spectrum SSE handler: only increment rdsFrameCount and call
updateRdsPsOverlay when lastModeName === "WFM"
- Mode change: call resetRdsDisplay() when switching to or from WFM
so the overlay and RDS panel are cleared promptly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add an optional website URL to config and use it for the web header
title when present, falling back to the version title otherwise.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Attach replay timestamps to APRS history and filter the APRS live bar
so it only shows the last hour of valid entries.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Deep purple-black backgrounds, neon magenta (#ff10e0) as primary
accent and neon green (#39ff14) as secondary. Waterfall sweeps
hue 300→120 (magenta→green). Light variant uses muted (#cc00a8 /
#1f8800) counterparts on a lavender-tinted white background.
- style.css: dark + light CSS variable blocks for neon-disco
- app.js: CANVAS_PALETTE entry; "neon-disco" added to valid styles
- index.html: Neon Disco option in the style picker
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove hardcoded #3388ff from the TRX circleMarker; apply
.trx-receiver-marker class and use stroke/fill: var(--accent-green)
so the dot follows the active colour scheme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace plain-text receiver marker popup with a styled info card
matching the APRS popup layout. Shows callsign, trx-server version
and build date, owner callsign (when different), QTH coordinates,
and all configured rigs with manufacturer/model; active rig is
badged.
Rig data (manufacturer, model, display_name, active state) is stored
in serverRigs/serverActiveRigId on each /rigs refresh. Popup content
is rebuilt live on popupopen so it always reflects current state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace plain-text receiver marker popup with a styled info card
matching the APRS popup layout. Shows:
- Station callsign (serverCallsign / ownerCallsign)
- trx-server version and build date
- Owner callsign (when different from station callsign)
- QTH coordinates
- All configured rigs with manufacturer/model; active rig badged
Rig data (manufacturer, model, display_name, active state) is
stored in serverRigs/serverActiveRigId on each /rigs refresh.
Popup content is rebuilt live on popupopen so it always reflects
the current state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove APRS client-side persistence, reset decode views before replay,
and clear decode panes only after the server clears its history.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove hardcoded #3388ff colour from L.polyline options; use
stroke: var(--accent-green) and stroke-opacity in the CSS class
so the path follows the active colour scheme automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Draw a blue dashed polyline from the receiver to the clicked APRS
station on popup open; remove it on popup close. CSS stroke-dashoffset
animation creates a traveling-dash effect suggesting signal propagation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Previously aprsMapAddStation forced Leaflet map init on a hidden element
during history restore. When live packets later arrived they stamped a
fresh _tsMs, resetting the displayed age to "0s ago".
Decouple data storage from Leaflet rendering:
- aprsMapAddStation now stores station data in stationMarkers immediately
(with the original _tsMs from localStorage) without touching the map
- _aprsAddMarkerToMap creates Leaflet markers only when the map is ready
- initAprsMap materialises all buffered APRS entries on first open
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Rebuild popup content on popupopen event so age and distance
are always computed fresh at the moment of opening; store
_aprsCall on each marker for O(1) lookup
- Extend map to fill viewport down to the footer instead of 60%
- Override Leaflet popup background/color to use CSS theme vars,
fixing invisible text in dark theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Each station popup now shows:
- Callsign/SSID header
- Age (s/min/h ago, from _tsMs stamped on receive)
- Distance from receiver (Haversine, km or m)
- Packet type and via path
- Full info/comment string
Adds haversineKm(), formatTimeAgo(), buildAprsPopupHtml() helpers in
app.js and .aprs-popup-* CSS. Passes full packet object as 7th arg
to aprsMapAddStation from aprs.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove custom #aprs-clear-btn CSS overrides and SVG icon; reduce to a
plain <button>Clear</button> so it inherits the same global button
style as the CW clear button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add an HTTP frontend config option for the initial APRS map zoom,
expose it through frontend metadata, and apply it in the web UI.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Tighten the spacing inside the APRS title while adding more room
between the title and the clear action.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Scale the APRS overlay inline clear text down for better balance
with the enlarged APRS title.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase the APRS overlay header base text size so the title reads
larger alongside the clear action.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase the APRS overlay inline clear text size for stronger
readability.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase the APRS overlay header text and make the inline clear text
match the row's base text size.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Render the APRS overlay clear action as inline clickable text inside
a span wrapper instead of a button-like control.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Cut the APRS overlay header sizing further so the row reads much
smaller and no longer dominates the overlay.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Offset the APRS overlay slightly from the waterfall edges so it reads
as a hovering strip and leaves the waterfall visible at the sides.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Reduce the APRS overlay clear control so it no longer sets the
header row height.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Wrap the APRS overlay clear action in a styled span and place it
next to the APRS title for a tighter header layout.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Reduce the APRS overlay header height by trimming spacing and scaling
down the clear control chrome.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace 999px pill on #rds-ps-overlay with 6px to match the rest of
the UI button rounding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace 999px pill radii with 6px (overlay), 4px (clear btn, pin),
3px (clear icon) — matching the standard button radius used throughout
the rest of the UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Restyle the APRS clear button and tighten the APRS overlay bar to use
more compact, polished chrome while keeping the existing behavior.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Reduce line-height to 1.05, use 0.1rem vertical padding + hairline
border-bottom to separate entries instead of whitespace.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Smaller font (0.65em), horizontal padding only, line-height-based
height, tighter border-radius — produces a flat wide rectangle tag
rather than a square button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Make the APRS/Clear header row position:sticky so it stays visible
when the frame list is scrolled. Adds a subtle backdrop-blur background
and a divider border so frames slide cleanly beneath it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove inter-frame gap and reduce line-height 1.45→1.2.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When a frame carries a position, show a small 📍 button between the
timestamp and callsign on the same line. Title tooltip shows the
coordinates; clicking navigates to the Map tab. Removes the two-line
per-entry layout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Reduce frame line-height 1.45→1.3 and pos sub-line to 1.1 to close
the gap between the info and coordinate rows within each entry
- Shrink clear button (smaller padding, 0.78em font)
- Remove 5-entry cap; full history visible via scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Coordinates were clipped by white-space:nowrap on the frame row.
Split each frame into a .aprs-bar-frame-main line (nowrap+ellipsis)
and a separate .aprs-bar-frame-pos line for the coordinate button,
so coordinates are always visible regardless of info length.
Restore 5-entry cap (slice from history).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Small pill-shaped button in the header row, right-aligned. Clicking
it delegates to the existing aprs-clear-btn, clearing history, the
packet panel, and the bar in one shot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add window.navigateToAprsMap(lat, lon) which activates the Map tab
and pans to the given position at zoom 13. APRS bar frames that carry
a position render a clickable coordinate button that calls this
function. Button is styled inline with the frame text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Guard both the live broadcast (decode_tx.send) and the history store
(record_aprs_packet) so CRC-failed packets are never forwarded to
connected clients or replayed on reconnect. The decode logger still
receives all frames for debugging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Drop APRS_BAR_MAX cap and the separate aprsBarFrames ring buffer;
drive the bar directly from aprsPacketHistory (CRC-ok frames only).
Remove the 60-char info truncation. CSS opacity fading still kicks in
after frame 5; older frames are reachable by scrolling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Stretch overlay edge-to-edge (left/right: 0), drop side borders and
border-radius for a full-width band look. Also enable pointer-events,
user-select, and overflow-y: auto so frames are scrollable and
copy-able.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Hide bar whenever mode != PKT; app.js calls window.updateAprsBar()
on every server-pushed mode change so the bar disappears immediately
- CRC-failed frames are excluded from the bar (both live and history
restore); the [CRC] rendering path is removed
- Offset bar 1.2 rem from left edge for visual breathing room
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Update the web audio control labels in the markup and initialize the
same labels from JavaScript for consistency.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Show the last 5 received APRS frames as a compact overlay in the
bottom-left corner of the waterfall strip, styled similarly to the
RDS PS overlay (backdrop blur, pill border). Frames fade out by
recency via CSS sibling-selector opacity steps. Bar auto-hides when
empty and is cleared by the APRS clear button. Restored from
localStorage on page load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Refine the mobile layout for the web frontend, add a sticky bottom tab bar,
and style the GitHub footer link as a badge.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Adds OVERVIEW.md with a comprehensive description of the project
architecture, crate layout, key types, data flow diagrams, DSP
pipeline, plugin system, and configuration reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move the spectrum FFT snapshot logic into a dedicated dsp module so dsp.rs stays focused on pipeline orchestration.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Split the SoapySDR backend demod and dsp code into focused modules while keeping behavior stable, and include the resulting formatting updates.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the checkbox with an On/Off select dropdown to match the
styling of the other WFM controls (Deemp, Audio) in the controls row.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Wire the StereoDenoise processor through the full stack:
- Add SetWfmDenoise command variant and RigCat trait method (trx-core)
- Add wfm_denoise field to RigFilterState with default true
- Add protocol command and bidirectional mapping (trx-protocol)
- Add rig_task command handler (trx-server)
- Implement set_wfm_denoise in SoapySdrRig backend
- Add /set_wfm_denoise API endpoint (trx-frontend-http)
- Add denoise checkbox in WFM controls with SSE sync and
localStorage persistence
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Clean up the workspace so cargo clippy passes across all targets and features.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add frequency-selective attenuation of the L-R difference signal to
reduce stereo hiss on weak FM broadcasts. Uses the quadrature component
(diff_q) as a noise reference per US7292694B2 (Wildhagen/Sony).
The algorithm splits sum, diff_i, and diff_q into 6 overlapping subbands,
estimates per-band SNR from smoothed |diff_q|² noise power, and applies
an energy-weighted broadband gain to the original diff signal. This
preserves clean stereo content (<4 dB loss) while attenuating noise-only
diff channels (>6 dB reduction).
Enabled by default; toggled via set_denoise_enabled() / set_wfm_denoise().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase the stereo matrix gain to 1.2 and trim the WFM output gain slightly to rebalance the decoded audio path.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Collapse the shared signal marker overlay when no spectrum data is available so the non-SDR signal graph renders cleanly.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Output gain clamp catches any peaks from full-deviation signals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
More stereo headroom for broadcast audio. With WFM_OUTPUT_GAIN at 0.35
the effective output is 0.28 peak, well within clipping margin.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace both IQ AGC and audio AGC in the WFM path with a fixed output
gain of 0.35. AGC pumping on broadcast audio degrades stereo separation
and introduces audible artifacts. The IQ hard limiter already normalizes
input magnitude, and the WFM decoder's internal deemphasis + matrix gain
provides consistent output levels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rename the duplicate callback-local variable so app.js loads cleanly and dependent plugin scripts can access shared helpers.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep the non-SDR signal graph visible and drive the audio level bar from decoded sample levels instead of packet size.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add the trx-rs GitHub link in the footer and make favicon handling more explicit for Safari.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Hide the combined waterfall and spectrum block when filter controls are unavailable so non-SDR rigs do not show those visuals.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Render the BW and tuned-frequency markers on a shared overlay and keep spectrum axis labels bold and inside their box.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Set opus encoder complexity from default (9-10) to 5 for both cpal and
SDR audio paths, significantly reducing encoding CPU usage with minimal
quality impact at these bitrates. Raise default bitrate from 192 kbps to
256 kbps for higher fidelity stereo audio.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Wrap the waterfall and spectrum in a shared main-tab block so lower sections and other tabs keep consistent spacing.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep the waterfall and spectrum markers visually continuous and style the theme toggle to match the active theme.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase the RDS overlay padding and width so the waterfall badge has more breathing room.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep the SDR frequency input accented without extra VFOs and restore the bottom spacing below the waterfall.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Resize the step scale control so it reads as a balanced two-option toggle instead of a leftover segmented control.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Align the WFM options into a concise control strip with consistent sizes and spacing in the main window.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rename the tune-step scale labels so the divisor toggle reads clearly in the frequency controls.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Lift the bandwidth label slightly and render it only while the bandwidth edges are being dragged.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove the gap under the waterfall and extend tuning markers plus wheel, click, and bandwidth drag interactions to the overview canvas.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add 10 dB of spectrum headroom and keep the overview waterfall the same height as the spectrum plot.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace per-sample sin_cos(pilot_phase) with a quadrature NCO that
advances via complex rotation (4 muls + 2 adds vs transcendental).
Renormalize every 1024 samples to prevent magnitude drift.
Decimate stereo detection logic (pilot coherence, lock, drive,
hysteresis) to run every 16 composite samples instead of every sample.
Accumulate pilot_mag and pilot_abs over the window and process averaged
values, scaling the IIR coefficients accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep the top bar above the waterfall and remove the rounded logo box styling.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move the logo/header cluster into the top bar on the left side.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Derive sin/cos of PLL phase error directly from I/Q arms (q/mag, i/mag)
instead of calling atan2 + sin_cos. Use double-angle identity to compute
38 kHz carrier (sin2θ = 2·sinθ·cosθ, cos2θ = 2·cos²θ-1) from the
rotated pilot sin/cos, eliminating the second sin_cos call entirely.
Drop Butterworth from 6th to 4th order (resampler Blackman-Harris now
handles stopband). Use power-of-2 bitmask for ring buffer indexing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Halves the coefficient bank from 128×32 to 64×32 (16 KB → 8 KB) for
better L1 cache utilization while maintaining sufficient fractional
sample resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace shift_append (O(N) rotate_left per sample) with a circular buffer
index for O(1) writes. The polyphase resampler now reads from the ring
buffer directly, eliminating 3 × 32-element memmoves per composite sample.
Remove unused dot_product functions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Pre-compute all FM discriminator outputs using demod_fm_with_prev which
processes 8 samples at a time via AVX2 atan2, then iterate the scalar
results through the rest of the stereo pipeline. Eliminates per-sample
f32::atan2 calls from the inner loop.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
With Blackman-Harris window and proper cutoff (~0.24), 32 taps still
provides 60+ dB stopband rejection. Halves the per-sample MAC count
from 192 to 96 across the three resampler channels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase polyphase resampler phases from 32 to 128 for finer fractional
sample positioning. Replace Hamming window with Blackman-Harris for ~92 dB
stopband rejection. Add pilot notch on composite signal before diff demod
to prevent 19 kHz × 38 kHz intermod products in the stereo difference
path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Increase polyphase resampler taps from 16 to 64 for sharper anti-alias
stopband rejection. Upgrade sum/diff lowpass filters from 4th-order to
6th-order Butterworth (three biquad stages) for ~36 dB/octave rolloff,
improving stereo separation by better rejecting the 38 kHz subcarrier
residuals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The fixed WFM_RESAMP_CUTOFF of 0.94 passed frequencies up to 94 kHz at
200 kHz composite rate, while the output Nyquist is only ~24 kHz. The
38 kHz demod products in the stereo diff path were only ~31 dB attenuated
by the Butterworth and aliased back into 10-20 kHz audio, causing treble
corruption in stereo mode. Now the cutoff is computed as
audio_rate / composite_rate, properly anti-aliasing the polyphase
resampler output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
32 taps caused audio silence on real signals. Revert to 16 taps
which works correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the low-accuracy 0.273 linear atan approximation with a
7th-order minimax polynomial (max error ~2.4e-7 rad vs ~0.004 rad).
Use branchless |y|>|x| argument reduction instead of y/x division
with quadrant fixup, avoiding division-by-zero and NaN branches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace fast_atan2 polynomial approximation with f32::atan2 in the WFM
stereo decoder's FM discriminator and pilot PLL. The approximation
introduced harmonic distortion (~0.22 deg error) that manifested as
treble artifacts on strong/overdeviated broadcast signals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Normalize IQ samples exceeding unit magnitude before the FM
discriminator. The discriminator only uses phase, so clamping
amplitude prevents overdeviated signals from producing clipped
composite baseband without losing frequency information.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Doubles the polyphase FIR length for the composite-to-audio rate
converter, improving stopband rejection from ~25 dB to ~50 dB.
This reduces treble distortion from imaging artifacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Set STEREO_DIFF_BW_HZ = AUDIO_BW_HZ so both filter paths have
identical group delay (improves multitone separation by ~10 dB).
- Zero out STEREO_SEPARATION_PHASE_TRIM (unnecessary with matched filters).
- Replace gradual blend ramp with binary blend: full stereo at pilot
lock, mono when unlocked. The hysteresis thresholds already handle
noisy signals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Test L-only and R-only signals with tones at 400, 2000, 8000 and
14000 Hz to catch frequency-dependent group delay and phase trim
issues that the single 1 kHz test misses.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Restore AUDIO_BW_HZ to 15.8 kHz for cleaner mono path, widen
STEREO_DIFF_BW_HZ to 18 kHz for better high-frequency stereo detail,
and raise STEREO_MATRIX_GAIN from 0.30 to 0.50 (mathematically correct
unity gain for the L=(S+D)/2 stereo matrix).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Raise both AUDIO_BW_HZ and STEREO_DIFF_BW_HZ to 18 kHz so the L+R and
L-R filter paths have identical group delay across the full audio band.
The previous mismatch (15.8 vs 14.5 kHz) caused frequency-dependent
phase errors in the stereo matrix that degraded real-world separation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Raise the stereo blend floor from 0.55 to 0.75 at pilot lock and lower
the full-blend ceiling from stereo_detect_level 0.92 to 0.70. This
gives real-world signals with moderate pilot strength much better L/R
separation (~17 dB immediately at lock vs ~5 dB before) and reaches
full unity blend sooner.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix pre-existing compilation failures in four test call sites that were
missing the wfm_denoise: bool argument added to ChannelDsp::new() and
SdrPipeline::start().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Raise audio LPF cutoff from 15 kHz to 17 kHz to pass full FM stereo
audio bandwidth without excessive HF rolloff
- Replace 2-point linear interpolation resampler with 4-point Hermite
cubic spline for a much flatter passband up to 17 kHz
- Add FM discriminator gain normalization (fm_gain = fs / 150000) so
±75 kHz deviation maps to ±1.0 regardless of composite sample rate,
stabilizing stereo carrier amplitude reconstruction
- Double pilot PLL proportional (0.0015→0.003) and integral
(0.00002→0.00005) gains for faster lock and better tracking
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The 19 kHz pilot notch was applied only to the L+R sum path, introducing
~22° of phase shift at 15 kHz relative to the L-R diff path. This phase
mismatch caused interchannel crosstalk (≈ −14 dB separation at 15 kHz).
Fix: remove the notch from the sum processing chain so both sum and diff
pass through identical 4th-order Butterworth LPFs, giving phase-coherent
demodulation across the full audio band. The notch is relocated to the
mono output branch where phase alignment with the diff channel is not
required. Pilot rejection on the stereo L/R outputs is still adequate
(~28 dB) from the combined LPF + deemphasis response at 19 kHz.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a server-side toggle for the multiband stereo denoiser so it can be
enabled or disabled at runtime without restarting the server.
Backend (trx-backend-soapysdr):
- Add `denoise_enabled: bool` to `WfmStereoDecoder`; gate multiband
blend behind it (falls back to uniform single-band blend when off)
- Add `set_denoise_enabled()` method on `WfmStereoDecoder`
- Propagate `wfm_denoise: bool` through `ChannelDsp`, `SdrPipeline`,
and `SoapySdrRig`; add `set_wfm_denoise()` at each layer
- Include `wfm_denoise` in `filter_state()` so it flows into snapshots
Protocol / core (trx-core, trx-protocol, trx-server):
- Add `SetWfmDenoise(bool)` to `RigCommand` and `ClientCommand`
- Add default `set_wfm_denoise()` trait method to `RigCat`
- Handle `SetWfmDenoise` in `rig_task.rs` and update `RigFilterState`
- Add `wfm_denoise: bool` (default `true`) to `RigFilterState`
Frontend (trx-frontend-http):
- Add `POST /toggle_wfm_denoise` endpoint
- Add "Denoise On/Off" button next to the stereo/mono audio picker
- Sync button state from SSE filter snapshot (`update.filter.wfm_denoise`)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Split the L-R diff channel into three frequency bands at audio rate and
apply SNR-weighted blending per band driven by pilot magnitude:
- 0–2 kHz: blend¹ (most stereo — low frequencies have best SNR)
- 2–8 kHz: blend² (moderate noise reduction)
- 8–15 kHz: blend⁴ (aggressive noise reduction — hiss-prone range)
Move blend from composite rate to audio rate so the crossover filters
(2nd-order Butterworth at 2 kHz and 8 kHz) operate at 48 kHz and the
pilot blend is linearly interpolated per audio sample for smooth
transitions. Unblended diff is now stored in prev_diff; prev_blend
tracks the blend value for the same interpolation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three bugs made the AM path sound wrong:
1. AGC attack too fast (5 ms). The slowest audio a broadcast AM station
can transmit is ~50 Hz (20 ms period). A 5 ms attack lets the AGC track
individual audio cycles, which causes severe pumping and amplitude
distortion. Change to 500 ms attack / 5 s release so the AGC only
responds to slow carrier-amplitude fading, not the audio modulation itself.
2. Bandwidth too narrow. The IQ filter cutoff is audio_bandwidth_hz / 2,
so the previous 6 000 Hz setting gave only 3 kHz audio bandwidth.
AM broadcast sidebands extend to ±4.5–5 kHz; raise the default to
12 000 (cutoff 6 kHz) to cover the full audio band.
3. DC blocker rate inconsistent. For AM the demodulated magnitude is
always ≥ 0 and the DC component equals the carrier amplitude; only true
DC needs removing. Unify all non-WFM modes to r = 0.9999 (corner
≈ 0.76 Hz @ 48 kHz), which strips carrier DC without touching any
audible bass content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
CW signals in SDR are centred at an audio offset (e.g. 700 Hz) by the
upstream FIR filter, so demodulating as USB (taking the real part) produces
the correct side-tone. The previous magnitude/envelope approach produced a
DC pulse per key press with no audible tone.
Re-enable the DC blocker for CW/CWR (r = 0.9999): the output is now audio
that can carry a DC offset from BFO frequency error, identical to USB.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SoftAgc — a fast-attack/slow-release envelope AGC with a max-gain cap
— to all demodulated audio paths so that switching between modes (WFM, AM,
SSB, CW, FM) no longer produces large volume jumps. AGC is applied after
every demodulator, including WFM, with a shared target level of 0.5.
Add per-mode DC blocking (DcBlocker) for USB/LSB/AM/FM/DIG paths to remove
carrier frequency-offset DC from the FM discriminator and LO bleedthrough in
SSB. CW is excluded because high-passing a non-negative envelope creates
negative-going artifacts on each key release; WFM already has internal DC
blockers on each output channel.
AGC time constants are tuned per mode:
CW/CWR – 1 ms attack / 50 ms release (follows individual dots/dashes)
AM – 5 ms attack / 200 ms release (tracks fading carriers)
all else– 5 ms attack / 500 ms release (suits voice and data)
Simplify demod_am and demod_cw: remove per-block peak normalisation and DC
removal that caused block-boundary level discontinuities ("pumping"). Both
now return raw magnitudes and rely on the downstream DC blocker and AGC for
normalisation.
DIG is already wired as Passthrough (identical to USB); no change needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace one-pole sum/diff filters with a proper 4th-order Butterworth
cascade (Q = 0.5412 / 1.3066) at 15 kHz. This reduces pilot tone
leakage from −4 dB to −12 dB at 19 kHz and suppresses the 38 kHz DSB
carrier from −9 dB to −32 dB, significantly improving stereo crosstalk.
Add a biquad notch at 19 kHz on the L+R channel to eliminate the residual
pilot tone that would otherwise be audible after downsampling to 48 kHz.
Replace nearest-neighbor (sample-hold) resampling with linear interpolation
inside WfmStereoDecoder. The output sample is now placed at the exact
fractional position between the two adjacent composite samples using the
phase accumulator state, removing timing jitter and harmonic distortion on
sustained tones.
Add DC blockers (pole at 0.9999, corner ≈ 0.75 Hz at 48 kHz) to all audio
outputs to remove carrier frequency-offset DC from the FM discriminator
without any audible bass roll-off.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Drop the now-unused rigctl_port local after removing\nthe shared rigctl listener path.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove the shared rigctl listener path so rigctl only\nruns as per-rig listeners configured through\nfrontends.rigctl.rig_ports.\n\nTighten client config validation to require at least one\nper-rig rigctl port when the rigctl frontend is enabled.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Make the primary SoapySDR DSP channel follow the tuned\nfrequency so RDS decoding stays aligned with the active\nfrequency rather than the hardware center.\n\nMove the default WFM deemphasis setting to server SDR\nconfig and default it to 50 us, then pass that value into\nthe SoapySDR backend.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add server-side debug log when RDS data is decoded (PI, PS, PTY).
Extend the RDS panel with active mode, frame counter, and a raw JSON
dump of the last spectrum frame (bins excluded) to help diagnose why
RDS remains absent from the SSE stream.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Draw small peak markers on strong visible spectrum maxima\nso snap-tune targets are easier to spot.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Apply the same explicit height and padding rules to the\nAuto BW button as the Set button in the spectrum\ncontrols, including the mobile layout.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
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>
Include the snapped peak signal level in the spectrum\nhover tooltip alongside the peak frequency.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The FIR LPF cutoff was audio_bandwidth_hz/2; with wfm_bandwidth_hz=75000
this gave 37.5 kHz, stripping the 57 kHz RDS subcarrier before FM
demodulation. Clamp the IQ filter bandwidth to at least 130 kHz (cutoff
≥ 65 kHz) for WFM so the RDS subcarrier always reaches the decoder,
regardless of the configured audio bandwidth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add an Auto BW control that estimates a suitable\nreceive bandwidth from the live spectrum around the\ncurrently tuned peak and applies it to the server.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Update the local tuned-frequency state immediately after\nsuccessful set_freq requests so the marker and display stay\nin sync with click-to-tune, manual entry, and jog tuning.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Show snapped peak frequencies in the spectrum hover tooltip\nand move the bandwidth label to the bottom of the tuned\nfrequency marker.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Improve spectrum click-to-tune by snapping the selected\nfrequency to a nearby dominant local peak, making signals\neasier to select.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
In multi-rig mode, each RigInstanceConfig.audio.listen defaulted to
127.0.0.1 independently of the global [audio] listen setting, causing
per-rig audio ports to bind to localhost only and refuse connections
from remote clients.
Fix by passing cli.listen.or(Some(cfg.audio.listen)) as the listen
override, so the global address is always the fallback when --listen
is not supplied on the CLI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep the top bar visible for unauthenticated users, but\nhide the rig selector until a session is established.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keep the top bar visible while logged out, limit access to\nthe Main tab, and leave theme controls available while the\nrig selector stays disabled.\n\nRename the internal style ids from nord/arctic and\nmonokai/lime, including a compatibility remap for saved\nsettings.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Rename the Nord and Monokai theme labels in the web UI\nto Arctic and Brownie, and update the matching CSS\nsection comments.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Only trx-rs.toml with [trx-server] / [trx-client] section headers is
now supported. Simplify ConfigFile trait to a single required method
section_key(); remove config_filename(), combined_key(), and
default_search_paths(). load_from_file() now errors when the expected
section is absent rather than falling back to flat parsing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Update the README system libraries section to list\nlibsoapysdr as a build-time requirement for SoapySDR\nSDR backends.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add rig_ports map to RigctlFrontendConfig. When non-empty, one rigctl
TCP listener is spawned per entry instead of the single shared listener,
each routing commands to its assigned rig via rig_id_override on RigRequest.
Add rig_id_override: Option<String> to RigRequest so the remote client
can route individual requests to a specific rig without changing the
globally selected rig. build_envelope prefers the override when set.
Example config:
[frontends.rigctl]
enabled = true
listen = "127.0.0.1"
port = 4532
rig_ports.ft817 = 4532
rig_ports.airspyhf = 4533
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Document the combined trx-rs.toml search order and clarify\nthe HTTP auth example for combined vs legacy config files.\n\nCo-authored-by: Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove the home-directory dotfile paths that predate the XDG layout.
Search order is now: CWD → ~/.config/trx-rs/ → /etc/trx-rs/, checked
for both the combined trx-rs.toml and the per-binary flat file at each
tier.
Update doc comments and tests accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove trx-server.toml.example and trx-client.toml.example in favour of
a single trx-rs.toml.example using [trx-server] and [trx-client] section
headers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Superseded by example_combined_toml() which now covers all use cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Both trx-server and trx-client now look for a combined trx-rs.toml
with [trx-server] and [trx-client] section headers respectively,
falling back to per-binary config files as before.
Search order per tier: combined trx-rs.toml → flat per-binary file,
checked in CWD, ~/.config/trx-rs/, and /etc/trx-rs/.
--print-config now outputs the config under the appropriate section
header so the combined file can be generated directly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
JS: fix stereo AudioData channel copy — frame.copyTo with planeIndex:0
only fills the left plane; reading it as interleaved data caused every
other sample to be zero, making WFM stereo play at half speed. Now
calls copyTo per channel with the correct planeIndex.
Rust WFM: replace integer output_decim/output_counter in WfmStereoDecoder
with a fractional phase accumulator (output_phase_inc = audio_rate /
composite_rate). Integer division caused the effective output rate to
drift from audio_sample_rate when the SDR rate is not an exact multiple
(e.g. 2 MHz SDR → 250 kHz composite → ~50 kHz output instead of 48 kHz,
making audio play 4% slow).
Rust non-WFM: add resample_phase/resample_phase_inc to ChannelDsp and
use a fractional-phase resampler in process_block for non-WFM paths,
ensuring exactly audio_sample_rate samples/sec regardless of SDR rate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix updateJogStepSupport to snap jogUnit (not jogStep) to nearest
supported unit, then recompute jogStep = jogUnit * jogMult so the
multiplier is preserved across rig connect/reconnect.
Rename "Mult" label to "Unit Multiplier" for clarity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix waterfall overview freezing at steady state by tracking a monotonic
push counter instead of row array length — the array size stays constant
once the waterfall is full, so the previous row-count comparison never
triggered the incremental draw path.
Fix WFM RDS not decoding when switching to WFM from a narrowband mode:
set_mode now resets audio_bandwidth_hz to the mode-appropriate default
(180 kHz for WFM) before rebuilding the FIR, preventing the 57 kHz RDS
subcarrier from being filtered out.
Add 1×/10×/100× multiplier button group next to the jog unit selector.
jogUnit × jogMult gives the effective jog step; both are persisted to
localStorage independently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Lower SPECTRUM_POLL_INTERVAL and SSE tick from 100 ms to 200 ms to halve
the number of spectrum frames pushed to the browser.
Introduce an OffscreenCanvas cache for the overview waterfall: at steady
state only the new row is painted and the existing image is scrolled up,
reducing per-frame work from O(rows × cols) to O(cols).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When a WFM/SDR spectrum stream carries RDS data, show the Program
Service name (8-char station name) centered on the visible waveform
area in DSEG14 monospace — the same font as the frequency display.
- Add #rds-ps-overlay div inside .overview-strip (pointer-events: none,
z-index: 2, absolutely positioned at the center of the visible canvas)
- updateRdsPsOverlay(rds) shows/hides and updates text on every spectrum
frame; trims trailing spaces common in RDS PS strings
- Overlay cleared on spectrum disconnect / null frame
- text-shadow uses CSS color-mix against --bg for legibility across all
style/theme combinations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When in dark mode the button has a light appearance; when in light mode
it has a dark appearance. This makes the button a preview of what
clicking will switch to, rather than mirroring the current theme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Same root cause as the overviewDrawPending TDZ: setStyle() references
lastSpectrumData at module init time but it was declared far below.
Hoist let lastSpectrumData = null to the top variable block and remove
the now-duplicate declaration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
setStyle() was called at module init time (to restore the saved style
from localStorage) before the let overviewDrawPending declaration, which
caused a ReferenceError: Cannot access 'overviewDrawPending' before
initialization.
Fix: hoist let overviewDrawPending = false to the top of the variable
declarations block, before any top-level code that may call
scheduleOverviewDraw(). Remove the now-duplicate declaration from its
original location.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add a style picker dropdown to the tab bar (right of rig picker) with
four styles — Original, Nord, Monokai, Contrast — each with full
light/dark variants.
CSS: define data-style attribute overrides for all CSS custom properties
(bg, card-bg, borders, text, accents, jog, audio level, filter, spectrum
background) for each of the three new styles × two themes (6 new blocks).
JS: introduce CANVAS_PALETTE lookup table covering spectrum/waveform/
waterfall colors for all style×theme combinations. Add currentStyle(),
canvasPalette(), setStyle() helpers. Persist selection to localStorage.
Replace all isLight ternaries in drawing code with palette lookups:
- drawOverviewWaterfall, drawOverviewSignalHistory, waterfallColor
signatures changed from isLight flag to pal object
- drawSpectrum uses canvasPalette() for grid lines, labels, fill, line
- spectrumBgColor() now delegates to canvasPalette().bg
Theme toggle also triggers a spectrum redraw so canvas colors update
immediately when switching light/dark.
Also fix light-theme spectrum rendering broken since canvas drawing used
hardcoded dark-only colors (white grid lines invisible on light bg).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Add viewport meta tag so mobile browsers use device width instead of
the default 980px desktop viewport
- Add 520px breakpoint: remove controls-tray forced min-width (was
52–58rem, causing horizontal scroll on phones), stack controls-row
into a single column with jog wheel first (order: -1)
- Scale frequency DSEG14 display with clamp() on narrow screens
- Make volume sliders responsive width with larger 20px thumb
- Raise spectrum Set/Auto button and input heights to 2.2rem (overrides
the min-height: 0 that blocked the existing 760px touch-size rule)
- Add overflow-x: auto + flex-shrink: 0 to sub-tab-bar so all six
plugin tabs are reachable on small screens without clipping
- Swap spectrum hint text based on input device: mouse/keyboard hint
hidden on touch devices, touch-specific hint shown instead
- Offset S0/S9/S9+ signal history labels 6px below their grid lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
The SDR audio bridge was forwarding PCM to server-side decoders but
never encoding it to Opus for rx_audio_tx, so TCP audio clients
(browser RX button) received nothing. Add Opus encoder initialised
from the rig's audio config and encode each PCM frame alongside the
pcm_tx broadcast.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the BW slider + FIR taps filter panel with a visual bandwidth
bookmark drawn directly on the spectrum canvas:
- Semi-transparent amber gradient strip spanning dialFreq ± BW/2
- Rounded-top bookmark tab at the top of the strip showing the current BW
- Draggable left/right edge handles (cursor: ew-resize) that adjust bandwidth
live and send set_bandwidth on mouse-up; range clamped per-mode defaults
- Y-axis now labeled with dB values (floor to ceiling) drawn on canvas
- Configurable floor level via number input below spectrum (default -100 dB)
- Auto button fits floor/range to current noise floor and peak level
- Remove FIR taps selector (internal DSP implementation detail)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add MODE_BW_DEFAULTS table mapping each mode to [default, min, max, step]
in Hz. When mode changes (via picker or SSE), applyBwDefaultForMode
updates the filter bandwidth slider range and sends set_bandwidth to the
server, so the DSP filter is rebuilt automatically for the new mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Compute hardware center as dial - center_offset_hz (was ignoring offset)
- Pass configured bandwidth_hz to device instead of hardcoded 1.5 MHz
- Add retune_cmd channel so set_freq repoints SDR hardware in real time
- Auto-add default channel with mode-appropriate bandwidth when [[sdr.channels]]
is empty, preventing silent audio with minimal config
- Add ChannelDsp::set_filter to rebuild FIR LPFs at runtime; wire
set_bandwidth and set_fir_taps to call it so UI filter changes take effect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Spectrum panel is now placed above the freq/mode/jog row, spanning the
full card width. Key improvements:
- Scroll wheel zooms in/out at the cursor position (up to 64x); double-
click resets to full bandwidth view.
- Mouse drag pans the visible window; click-to-tune is suppressed when a
drag has occurred.
- Touch pinch-to-zoom and single-finger drag-to-pan supported.
- Hover tooltip shows the frequency under the cursor, formatted to the
currently selected unit (MHz/kHz/Hz, matching the jog-step selection).
- Frequency axis labels update to reflect the zoomed visible range.
- Canvas height increased to 160 px; axis bar styled with card bg.
- A small hint line below the panel explains the controls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Poll GetSpectrum every 200 ms in remote_client via a dedicated timer that
bypasses the main state-watch channel (no SSE noise). The resulting
SpectrumData is stored in FrontendRuntimeContext::spectrum and served by
a new GET /spectrum endpoint (JSON or 204 when unavailable).
HTTP frontend shows a spectrum panel (canvas + frequency axis) only when
the rig reports filter_controls=true (i.e. SoapySDR). The canvas renders:
- dark background with dBFS grid lines
- green FFT spectrum line with semi-transparent fill
- red dashed vertical marker at the currently tuned frequency
- frequency axis labels (MHz/kHz) below the canvas
Clicking the canvas tunes the rig to the clicked frequency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SpectrumData struct (bins, center_hz, sample_rate) to RigState and
RigSnapshot. Add GetSpectrum RigCommand and ClientCommand plumbed through
the protocol layer. SoapySDR DSP pipeline now computes a 1024-bin FFT
(Hann window, FFT-shifted, dBFS) every 4 IQ blocks (~10 Hz update rate)
and exposes it via RigCat::get_spectrum(). The rig_task handles
GetSpectrum without persisting spectrum data in ongoing state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Three root causes prevented APRS decoding at 144.800 MHz with PKT/FM mode:
1. `RealIqSource::read_into` returned zeros — the SoapySDR streaming API
was never wired up. `RxStream<Complex<f32>>` is `Send` and
`StreamSample` is implemented for `num_complex::Complex<f32>` in the
soapysdr 0.3 crate, so the stream can read directly into the IQ buffer.
Now creates and activates an `RxStream` in `new()` and calls
`stream.read` in `read_into`.
2. PKT mode used `Passthrough` (take `.re`) demodulation. VHF/UHF packet
radio (APRS, AX.25) is FM-encoded AFSK — it must be FM-demodulated
before the APRS decoder sees the audio tones. Changed PKT to `Fm`.
3. `iq_read_loop` always slept `block_duration_ms` after each read. Real
hardware already blocks inside `read_into`; the extra sleep doubled
latency. Added `IqSource::is_blocking()` (default `false`; `true` for
`RealIqSource`) and skip the throttle sleep for blocking sources.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Pass `watch::Receiver<bool>` into `run_capture` and `run_playback` so
both threads check for shutdown at the top of their outer recovery loop
and inner monitoring loop. Without this, restarting the process (e.g.
via systemd after an ALSA error) left the old threads stalled forever
while new ones were created alongside them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the per-sample ring-buffer FIR convolution with block-level
overlap-save convolution using rustfft. For a block of M samples and
N taps the old approach costs O(N·M); the new one costs O(M log M),
with rustfft using SIMD (AVX2/SSE4) internally.
Key changes:
- Add rustfft = "6" dependency
- Add BlockFirFilter: overlap-save filter with pre-computed H(f) and
a single forward+inverse FFT pair per block (no per-sample multiply)
- ChannelDsp.process_block() now:
1. Batch-mixes entire block to baseband in one vectorisable loop
2. Applies BlockFirFilter to I and Q (one FFT pair each)
3. Decimates and demodulates as before
- Keep the old FirFilter for unit tests (sample-by-sample interface)
- Add BlockFirFilter unit tests (DC passthrough, length preservation)
- IQ_BLOCK_SIZE promoted to pub const for use in filter sizing
For the default config (4096-sample blocks, 64 taps, decim=40):
Old: ~262144 multiply-adds per FIR × 2 components = ~524k per block
New: ~2 × (3 × 8192 × log2(8192)) ops, all SIMD-vectorised by rustfft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Fix compiler warning about discarded const qualifier in strchr() call.
The result of strchr(const char*, ...) should be assigned to const char*,
not char*.
This resolves:
warning: initialization discards 'const' qualifier from pointer target type
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add .cargo/config.toml to enable native CPU features (AVX2, SSE4.2, etc.)
for maximum performance during compilation. This allows rustc and
dependencies to use SIMD instructions and other CPU optimizations.
This should reduce CPU usage of the DSP backend by allowing:
- Vectorized floating-point operations
- Better compiler optimizations for complex number math
- SIMD acceleration in dependencies like num-complex
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Hide the header rig picker when showing the authentication gate,
since no rigs are accessible until the user authenticates.
Show the picker again when auth gate is hidden (after login).
This improves UX by not showing an empty picker to unauthenticated users.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
When rigs are configured without explicit IDs in TOML, they now
receive auto-generated IDs based on model name and index (e.g.,
"ft817_0", "soapysdr_1"). Previously, the default empty ID prevented
auto-generation from triggering.
Changes:
- Set RigInstanceConfig default id to empty string (was "default")
- This allows resolved_rigs() auto-generation to trigger for all rigs
- Legacy single-rig path still gets explicit "default" ID
- Auto-generated IDs now appear in HTTP API /rigs endpoint
- Rig picker now displays auto-IDs correctly
The fix ensures that rigs without explicit ids get proper identifiers
that appear in the protocol and frontend selectors.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Enhance error handling and messaging for SoapySDR device initialization:
- Add fallback logic to try empty args if device-specific args fail
- Provide detailed multi-line error messages with troubleshooting guidance
- Include suggestions to check SoapySDR plugins and run SoapySDRUtil --probe
- Clarify device lifetime management in struct
- Document why actual streaming not yet implemented (soapysdr 0.3 limitations)
- Note that sdr 0.3 requires FFI or upgrade for streaming support
This helps users diagnose device initialization failures and understand
the current architectural limitations.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Remove direct dependency on trx-backend-soapysdr from server Cargo.toml.
Re-export SoapySdrRig from trx-backend instead so server accesses it
through the proper facade. This keeps sub-backends internal to trx-backend
and maintains proper module organization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop optional feature gating - SoapySDR hardware support is now required.
Make soapysdr a required dependency in Cargo.toml instead of optional.
Update server to always enable soapysdr backend and its dependencies.
Simplify initialization to always use RealIqSource instead of conditional
fallback to MockIqSource.
This assumes SoapySDR library is installed on the system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Allow rigs to have empty IDs in config; auto-generate from backend model
name with numeric suffix (e.g., 'ft817_0', 'ft817_1', 'soapysdr_0').
This makes config more concise when using multiple instances of the same
model without explicit ID assignment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add RealIqSource that connects to actual SoapySDR devices when soapysdr-sys
feature is enabled. Implements proper device initialization, frequency/bandwidth
configuration, gain control, and RX stream management.
When soapysdr-sys feature is disabled (default), falls back to MockIqSource
for testing. Update feature flags in Cargo.toml dependencies to support both
real hardware and mock operation.
- Device initialization with proper error handling
- RX frequency, bandwidth, and gain configuration
- IQ sample streaming via broadcast channel
- Proper resource cleanup via Drop trait
- Throttled MockIqSource to prevent 100% CPU
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add sleep proportional to block duration in iq_read_loop to simulate
real hardware timing. MockIqSource immediately returns samples without
any delay, causing busy-looping. Throttling prevents excessive CPU usage
when using the mock source.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use header rig selector as the single point for rig switching. Remove
redundant about page rig selector controls and associated JavaScript
event listeners.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add display_name field to RemoteRigEntry and propagate from GetRigs
response. Update http-json frontend to include display_name in rig
entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Include optional display_name field in GetRigs response to allow clients
to show friendly rig names instead of just identifiers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add optional `name` field to RigInstanceConfig to allow long-form rig names
(e.g., "HF Transceiver"). The name defaults to rig id if not configured.
Add display_name() method to get display name with fallback to id.
Propagate display_name through RigHandle to listener for GetRigs response.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix useless vec! allocation in demod tests, improve loop iterator usage,
and reformat code for better readability across dummy and soapysdr
backends.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reformat assertions, multi-line function calls, and error handling for
better readability. Remove unused soapysdr feature import in main.rs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Enable SoapySDR backend by default to make multi-device setups work out
of the box without requiring explicit feature flags.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reformat filter state methods and error handling for better consistency.
Improve readability of complex return type expressions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Improve assertion and struct literal formatting across test cases
for better code clarity and consistency.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents the /frontend-design skill (project-scoped, in
.claude/commands/frontend-design.md) and explains how to add new
skills. Lists global skills available across all projects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Prevent Claude Code session files, worktrees, and settings from
being accidentally committed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add /set_bandwidth and /set_fir_taps HTTP endpoints to api.rs.
Add applyCapabilities(caps) function to app.js that shows/hides:
- PTT button and TX meters: capabilities.tx
- TX limit row: capabilities.tx_limit
- VFO row: capabilities.vfo_switch
- Signal meter row: capabilities.signal_meter
- Filters panel: capabilities.filter_controls
Called from render() whenever capabilities are present; runs on both
initial /status response and every SSE event.
Add a Filters panel to index.html with bandwidth slider (1..500 kHz)
and FIR taps select (16/32/64/128/256); hidden by default, revealed by
applyCapabilities when filter_controls is set. Each control dispatches
to the corresponding HTTP endpoint on change.
Sync filter state from update.filter in render() to keep slider/select
in sync with server-side DSP state.
Fix missing struct fields in test helpers across remote_client.rs,
trx-frontend-http-json/server.rs, trx-frontend-rigctl/server.rs, and
trx-core controller tests (handlers.rs, machine.rs).
Update aidocs/UI-CAPS.md: all tasks UC-01..UC-09 marked [x].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Handle SetBandwidth(hz) and SetFirTaps(taps) in process_command:
call the RigCat methods, update filter state in-place, broadcast
the updated state, and return the new snapshot.
Fix missing capability fields in listener.rs test helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Set tx/tx_limit/vfo_switch/filter_controls/signal_meter on all backends:
- FT-817, FT-450D, dummy: tx=true, tx_limit=true, vfo_switch=true,
filter_controls=false, signal_meter=true
- SoapySDR: tx=false, tx_limit=false, vfo_switch=false,
filter_controls=true, signal_meter=true
SoapySDR backend now stores bandwidth_hz and fir_taps fields; overrides
set_bandwidth, set_fir_taps, and filter_state on RigCat to expose live
DSP state in the snapshot.
Add UC-08 unit tests on dummy backend asserting tx capabilities present
and filter_controls absent, and that filter_state returns None.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SetBandwidth { bandwidth_hz: u32 } and SetFirTaps { taps: u32 } to
ClientCommand with bidirectional mapping to RigCommand variants.
Add UC-09 protocol serialization tests confirming that RigSnapshot
serializes the filter field when Some and omits it when None.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add five new boolean fields to RigCapabilities: tx, tx_limit,
vfo_switch, filter_controls, signal_meter. These drive which controls
the HTTP frontend shows or hides per rig type.
Add RigFilterState struct (bandwidth_hz, fir_taps, cw_center_hz) and
filter: Option<RigFilterState> to both RigState (skip-serialized) and
RigSnapshot (skip_serializing_if = None).
Add SetBandwidth and SetFirTaps to RigCommand; add default not-supported
implementations of set_bandwidth, set_fir_taps, and filter_state to
the RigCat trait.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Keeps README.md, CLAUDE.md, and CONTRIBUTING.md at root as standard
project files. Moves AI-generated design/specification documents
(AGENTS, AUTH, CONFIGURATION, ENHANCEMENT, MULTI, OVERVIEW, SDR,
UI-CAPS) into autogendoc/ to distinguish them from hand-maintained docs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Specifies UC-01 through UC-09: extending RigCapabilities with tx/tx_limit/
vfo_switch/filter_controls/signal_meter flags, RigFilterState struct,
SetBandwidth/SetFirTaps protocol commands, new HTTP endpoints, and
frontend visibility gating via applyCapabilities() in app.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Documents architecture, TOML format, protocol wire format, validation
rules, and task completion status (MR-01 through MR-09).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add GetRigs command, rig_id routing fields, and RigEntry type:
- ClientCommand::GetRigs (intercepted in listener before rig_task)
- rig_id: Option<String> on ClientEnvelope (absent = first rig)
- rig_id: Option<String> and rigs: Option<Vec<RigEntry>> on ClientResponse
- RigEntry { rig_id, state } for GetRigs aggregated response
- Sentinel unreachable!() arm for GetRigs in client_command_to_rig()
- MR-09 codec tests: rig_id parsing, GetRigs round-trip, serde omit-when-None
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
- Call validate_sdr() at startup and abort on errors (SDR.md §11)
- Build SoapySdrRig with full [[sdr.channels]] config when access_type is
"sdr"; subscribe to its primary-channel PCM sender before handing the
pre-built rig to the rig task via RigTaskConfig.prebuilt_rig
- Skip cpal capture when an SDR audio source is available; bridge the
SdrPipeline PCM broadcast into pcm_tx so all decoder tasks are unchanged
- Add trx-backend-soapysdr optional dep and soapysdr feature to trx-server
- Add prebuilt_rig field to RigTaskConfig so rig_task skips the registry
factory when a pre-built rig is supplied by main
Marks SDR-08 complete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Adds 12 unit tests for ServerConfig::validate_sdr() covering: minimal
valid config, non-SDR skip, empty/missing args, zero sample_rate,
channel IF out-of-range (positive, negative, exactly at Nyquist), dual
stream_opus, tx_enabled with SDR backend, duplicate decoder, and
multiple simultaneous errors. Marks SDR-11 complete in SDR.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Replace the stub SoapySdrRig with a full implementation: wire up SdrPipeline
from dsp.rs, implement AudioSource::subscribe_pcm on the primary channel,
add gain control (manual/auto with fallback warning), and track primary
channel freq/mode so set_freq/set_mode update the live DSP pipeline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add 9 unit tests covering all demodulators in demod.rs: USB/LSB/Passthrough
real-part extraction, AM DC removal and varying-envelope, FM tone frequency
and silence, CW peak normalisation, mode mapping, and empty-input safety.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add dsp.rs with IqSource trait abstraction, MockIqSource, windowed-sinc
FIR low-pass filter, ChannelDsp (mixer/decimate/demod/frame-accumulator),
and SdrPipeline which spawns a dedicated IQ read thread. No soapysdr crate
dependency; the real device will be wired in SDR-07.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add [sdr], [sdr.gain], and [[sdr.channels]] sections to CONFIGURATION.md,
extend [rig.access] with type = "sdr" / args, add CLI override note, and
append a commented SoapySDR example block to trx-server.toml.example.
Mark SDR-09 as complete in SDR.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Introduces the trx-backend-soapysdr crate with a compilable SoapySdrRig
struct that satisfies the Rig + RigCat trait bounds. RX methods
(get_status, set_freq, set_mode, get_signal_strength) are implemented;
TX-only methods return RigError::not_supported. as_audio_source returns
None for now (overridden in SDR-07). Wires the crate into the workspace
and trx-backend (feature "soapysdr"), and fixes the non-exhaustive match
on RigAccess::Sdr in trx-server main.rs and rig_task.rs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add SdrConfig, SdrGainConfig, and SdrChannelConfig structs to config.rs,
extend AccessConfig with an args field for the "sdr" access type, wire
SdrConfig into ServerConfig, and implement validate_sdr() with all
startup validation rules from SDR.md §11. Marks SDR-03 complete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add AudioSource trait to trx-core rig module providing subscribe_pcm()
for demodulated PCM audio. Add opt-in as_audio_source() default method
to RigCat returning None; SDR backends will override to return Some(self).
Re-export AudioSource from the crate root. Marks SDR-01 complete.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Add `RigAccess::Sdr { args: String }` to the backend access enum, register
a feature-gated `soapysdr` factory stub in `register_builtin_backends_on`,
handle the new variant in all existing `match` arms, and add the `soapysdr`
feature flag (no dep yet; implementation lands in SDR-04). Mark SDR-02 done.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
Move DecoderLoggers and DecodeLogsConfig out of trx-server into a
dedicated src/decoders/trx-decode-log crate, giving file logging the
same standalone crate treatment as the four decoder crates.
- src/decoders/trx-decode-log/ (new — DecodeLogsConfig + DecoderLoggers)
- trx-server/config.rs: re-exports DecodeLogsConfig from trx-decode-log
so ServerConfig field references and all tests compile unchanged
- trx-server: drop decode_logs module, use trx_decode_log directly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move the Goertzel-based CW decoder out of trx-server::decode::cw into
a dedicated src/decoders/trx-cw crate, matching the layout of trx-aprs,
trx-ft8, and trx-wspr. The decode module is now empty and removed.
- src/decoders/trx-cw/ (new — Goertzel + Morse decoder)
- trx-server: drop decode module entirely, use trx_cw::CwDecoder
- CwEvent stays in trx-core (mirrors AprsPacket / Ft8Message / WsprMessage)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move trx-ft8 and trx-wspr into src/decoders/ alongside a new trx-aprs
crate that extracts the Bell 202/AX.25 decoder from trx-server, giving
all three modems a consistent crate-per-decoder layout.
- src/decoders/trx-ft8/ (moved from src/trx-ft8/)
- src/decoders/trx-wspr/ (moved from src/trx-wspr/)
- src/decoders/trx-aprs/ (new — Bell 202 AFSK + AX.25/APRS decoder)
- trx-ft8/build.rs: fix external/ft8_lib relative path after move
- trx-server: drop decode::aprs module, use trx_aprs::AprsDecoder
- AprsPacket stays in trx-core (mirrors Ft8Message / WsprMessage)
- Workspace Cargo.toml updated with new member paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forwards CRC-valid RF APRS packets to APRS-IS via plain TCP using the
TNC2 line format, making them visible on aprs.fi and other APRS-IS
consumers. Mirrors the pskreporter module in structure.
- New aprsfi.rs: IGate task with reconnect loop (exponential backoff
1s→60s), login/logresp, 60s keepalive, 60s stats, passcode
auto-computation from callsign (standard APRS hash algorithm)
- config.rs: AprsFiConfig struct with enabled/host/port/passcode fields
and validation; default host rotate.aprs.net:14580
- main.rs: mod aprsfi; spawn task inside audio block when aprsfi.enabled
- trx-server.toml.example, CONFIGURATION.md: document [aprsfi] section
- Remove APRSFI_IMPLEMENTATION.rs planning artifact
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After ALSA POLLERR the existing cpal Device handle can be stale, causing
repeated stream build failures with no path to recovery. Re-acquire host
and device inside the outer recreation loop so each attempt gets a fresh
handle. Device-not-found is now a warn+retry rather than a fatal error,
allowing recovery from transient USB audio device disappearances.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
CPAL error callbacks can fire millions of times per second on ALSA
EPIPE (errno -32). Previously each invocation did a string allocation
and mutex lock, saturating CPU and eventually crashing the server.
- Use atomic swap in both input and output error callbacks so only the
first error fires the expensive log+notify path; all subsequent hits
cost a single atomic op and return immediately.
- Replace stream.play()? with explicit error handling in run_playback
so a play failure triggers stream recreation instead of permanently
terminating the playback thread.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <stanislawgrams@gmail.com>
Change default remote connection port from 4532 to 4530 and audio
server_port from 4533 to 4531.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <stanislawgrams@gmail.com>
Change JSON TCP listener default from 4532 to 4530 and audio
streaming default from 4533 to 4531.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <stanislawgrams@gmail.com>
Simplify the opening paragraph for better readability and remove the
(work in progress) notice since the project has substantial, documented
features and a stable structure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <sjg@haxx.space>
Add CORS, referrer policy, and content-type security headers.
Configure logger to track real client IP in reverse-proxy setups
via Forwarded / X-Forwarded-For / X-Real-IP headers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <sjg@haxx.space>
Fix authenticated refresh by restoring tab visibility during startup, and retune dark mode toward deep blue with amber-red accents.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add compile-time build dates for trx-server and trx-frontend-http, propagate server build metadata through rig state/snapshot, and render both versions + build dates in the HTTP footer.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Enable sig-clear-btn for RX users since clearing signal measurements
is a local UI operation that doesn't affect rig state.
Only disable decode history clear buttons:
- aprs-clear-btn
- ft8-clear-btn
- wspr-clear-btn
- cw-clear-btn
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace separate theme and auth buttons with a visually connected
button group. Buttons are grouped with no gap between them and
rounded corners only on the outer edges, creating a cohesive control.
Features:
- First button: rounded left edges
- Last button: rounded right edges
- Middle buttons: sharp edges (if more added)
- Negative margin to overlap borders smoothly
- Hover effect for feedback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
After logging in, automatically show the Main tab-panel and mark
the Main tab button as active. Previously, users had to click the
Main tab to see content after login.
hideAuthGate() now:
- Shows the Main tab-panel
- Hides all other tab-panels
- Marks the Main tab button as active
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
When logout is clicked from the About tab, hide all tab panels to
prevent tab content from showing behind the login window.
Also ensures auth gate is displayed cleanly without any stray content
visible behind it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Disable all decode history clear buttons for rx-authenticated users:
- APRS clear
- FT8 clear
- WSPR clear
- CW clear
- Signal clear
These are control operations that should be restricted to full access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
After logout, check if guest mode is available and show the
"Continue as Guest" button if no rx_passphrase is configured.
Previously, authLogout() always passed false to showAuthGate(),
hiding the guest button.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
When rx_passphrase is not set, RX users have an implicit role without
a session. They should get 403 on control endpoints, not 401.
Previously, unrestricted RX users (with no session) trying control
endpoints would get 401 Unauthorized, triggering login redirect.
Now they get 403 Forbidden with "Insufficient permissions" hint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Fix auth middleware to return correct HTTP status codes:
- 401 Unauthorized: No session (not authenticated)
- 403 Forbidden: Has session but insufficient role
Previously, all auth errors returned 401, which caused the frontend
to redirect rx users to login when they tried control endpoints.
Now rx users scrolling jog wheel/frequency will get a "Insufficient
permissions" hint instead of being redirected to login.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Remove duplicate logout button from About tab. Use only the header
Login/Logout button for unified authentication control.
The About tab now shows the authentication badge (when logged in)
without the redundant logout button.
Single login view:
- Auth gate with Login form + Continue as Guest button (when no rx pass)
- Header Login/Logout button for quick access
- Auth badge in About tab showing current role
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Only redirect to login on 401 (unauthenticated). For 403 errors
(authenticated but insufficient role), let the caller handle the error.
This prevents rx-authenticated users from being redirected to login
when they attempt to scroll the jog wheel or frequency input, which
tries to call /set_freq (a control-only endpoint).
RX users will now see "Insufficient permissions" hint instead of
being sent to login screen.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Fix logout flow to properly show auth gate and clear form. Also
disable additional controls for rx role:
- Jog wheel (faded out)
- Jog up/down buttons
- VFO selector buttons
For rx-authenticated users, all frequency/mode adjustment controls
are now properly disabled to prevent accidental changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace location.reload() with disconnect() + auth gate to prevent
browser hang when logging out. The full page reload was causing
issues with resource loading and event source reconnection timers.
Changes:
- Add disconnect() function to cleanly close EventSource connections
and clear all timers (esHeartbeat, reconnectTimer)
- authLogout() now disconnects locally and shows auth gate instead
of reloading the page
- Faster logout experience without full page reload
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a Login/Logout button in the header next to the theme toggle,
styled consistently with the theme button. Button behavior:
- When logged in: Shows "Logout" with confirmation
- When not logged in: Shows "Login" to open auth gate
- Visible when on main app (not in auth gate)
- Same theme-toggle-btn styling
Provides quick access to authentication controls without needing to
navigate to the About tab.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Do not auto-connect with guest role. Always show the auth gate when
there's no valid session, allowing the user to choose between:
- Login: Enter passphrase for control access
- Continue as Guest: Proceed with read-only access (if available)
This allows users to enter their control passphrase instead of being
forced into guest mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
When RX access is unrestricted (no rx_passphrase required), show
a "Continue as Guest" button on the auth gate to allow immediate
access without requiring login. The button is only shown when guest
mode is available.
Guest mode:
- No authentication required
- Grants rx role immediately
- Can monitor radio but cannot transmit
Login path still available for full control access.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
When a user is authenticated as 'rx' role (read-only), disable all
TX/PTT control buttons and frequency/mode inputs to prevent accidental
attempts to transmit. This provides clear visual feedback that these
controls are not available.
Controls disabled for rx role:
- PTT button
- Power button
- Lock button
- TX Audio button
- Frequency input
- Mode select
- TX Limit input/button
- Jog up/down buttons
- Jog step buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
When HTTP auth is enabled but rx_passphrase is not configured, allow
unauthenticated users to access read-only endpoints (status, events,
decode, audio) without authentication. This enables monitoring-only
access while protecting TX control with a passphrase.
Changes:
- AuthMiddleware: Skip auth check for read routes when rx_passphrase is None
- session_status: Grant rx role to unauthenticated users when no rx passphrase required
Use case: Set only control_passphrase to protect TX/PTT while allowing
anyone on the network to monitor the radio.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The HTTP server was hardcoding auth config with enabled=false,
ignoring the actual configuration from trx-client.toml. This prevented
authentication enforcement even when enabled with passphrases.
Solution: Store auth config values in FrontendRuntimeContext during
initialization in main.rs, then extract and use them in server.rs
build_server() instead of hardcoding.
Fixes auth bypass where unauthenticated users could access the web UI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Update example_toml() to include example values for rx_passphrase and
control_passphrase in the printed config output, so users can see what
these configuration fields should look like.
Now --print-config shows:
[frontends.http.auth]
enabled = false
rx_passphrase = "rx-passphrase-example"
control_passphrase = "control-passphrase-example"
tx_access_control_enabled = true
session_ttl_min = 480
cookie_secure = false
cookie_same_site = "Lax"
This helps users understand all available auth parameters.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add ~/.trx-client.toml to the config file search paths, making it easy
for users to place their config in the home directory.
Updated search order:
1. Path specified via --config CLI argument
2. ./trx-client.toml (current directory)
3. ~/.trx-client.toml (home directory) [NEW]
4. ~/.config/trx-rs/client.toml (XDG config)
5. /etc/trx-rs/client.toml (system-wide)
This provides a more standard Unix convention for user config files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
When HTTP authentication is disabled (the default), the /auth/session
endpoint now returns { authenticated: true, role: "control" } instead
of 404. This allows the frontend to proceed without showing a login
gate, providing the expected out-of-the-box experience.
With this change:
- Default behavior: no login required, full control access
- Auth enabled: login gate shown, roles enforced per config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Remove the redundant logo image from the auth gate. The header already
displays the logo, so this duplicate was unnecessary.
The login screen now shows only:
- "Access Required" heading
- "Enter passphrase to continue" subtitle
- Passphrase input
- Login button
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add box-sizing: border-box to both the passphrase input and login button
to ensure padding is included in width calculations. This makes them
exactly the same width visually.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Hide the Main/Plugins/About tab bar initially, only showing it after
the user successfully authenticates. This prevents navigation options
from being visible when access has not been granted.
Changes:
- Add display:none and id to tab-bar div in index.html
- Update showAuthGate() to hide tab-bar
- Update hideAuthGate() to show tab-bar
Now the UI flow is:
1. Header only (auth gate visible)
2. After login: Header + tabs + content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
- Derive Default for SameSite enum in auth.rs using #[default] attribute
- Derive Default for CookieSameSite enum in config.rs
- Replace and_then(|x| Some(y)) with map(|x| y) in extract_session_id()
All clippy warnings resolved. Tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Update ClientConfig::example_toml() to explicitly include all HTTP auth
config fields with their default values, so the --print-config output
displays the complete auth configuration section.
Also add #[allow(dead_code)] to session_ttl() method to suppress warning.
The example config now shows:
[frontends.http.auth]
enabled = false
tx_access_control_enabled = true
session_ttl_min = 480
cookie_secure = false
cookie_same_site = "Lax"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Phase 4: Frontend login gate and role-based UI
- Add auth-gate HTML overlay with passphrase form
- Implement checkAuthStatus, authLogin, authLogout functions
- Auth startup sequence checks /auth/session before connecting
- Apply role-based restrictions: hide PTT/TX controls for rx role
- Handle 401/403 errors in postPath, return to login screen
- Add logout button in About tab with auth role display
- Passphrase form shows generic error messages (no info leakage)
Phase 5: Documentation
- Update trx-client.toml.example with [frontends.http.auth] section
- All config fields with inline documentation and examples
- security notes about cookie settings
- Update README.md with HTTP Frontend Authentication section
- Role model explanation (rx vs control)
- Configuration example
- Security considerations for local, LAN, and remote deployments
- Architecture overview
UI Features:
- Login gate blocks main UI until authenticated
- Role badge shows authenticated status in About tab
- Error messages clear after 5 seconds
- Logout confirmation prevents accidental logouts
- Smooth transition from auth gate to main UI
All code compiles successfully. HTTP frontend build verified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add optional passphrase-based authentication with two roles (rx/control),
session management, auth middleware, and protected routes.
Phase 1: Config model with HttpAuthConfig struct, CookieSameSite enum,
validation logic for enabled auth requiring at least one passphrase.
Phase 2: Auth module with:
- AuthRole enum (Rx, Control)
- SessionRecord and SessionStore for in-memory session management
- AuthConfig at runtime
- /auth/login, /auth/logout, /auth/session endpoints
- Constant-time passphrase comparison for timing attack mitigation
Phase 3: Integration with:
- AuthMiddleware for route protection with public/read/control classification
- Server-side AuthState setup with cleanup task for expired sessions
- Auth endpoints registered in api.rs configure()
Sessions use 128-bit random IDs (hex-encoded), HttpOnly cookies, configurable
SameSite attribute. Auth is disabled by default to preserve current behavior.
All unit and integration tests passing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Refine map plotting and filter UX in HTTP frontend plugins.\n\n- support plotting multiple locator squares from FT8/WSPR messages\n- show locator lists in popup content as newline-separated entries\n- add WSPR map layer filter toggle and marker typing\n- style filter controls for strong dark/light mode contrast\n- keep themed behavior aligned with map and control updates\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add dark/light mode selector below logo and wire theme-aware visuals.\n\n- add compact theme toggle control under header logo\n- support persistent dark/light theme switching\n- use emoji labels for theme toggle actions\n- make jog and audio level visuals theme-aware\n- add dark-mode map tile layer and live layer switching on theme changes\n- keep responsive behavior for header graph and controls\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Implement UI refinements for the HTTP frontend main and plugin views.\n\n- add dimmed header signal graph with live rendering and scale\n- make graph responsive, colorized by signal strength, and keep last 10s only\n- add APRS and WSPR text filtering, matching FT8 behavior\n- refine responsive layout for controls/map/header behavior\n- tune jog wheel/button sizing and mode selector height alignment\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Adjust responsive behavior and interaction details in the HTTP frontend.\n\n- switch signal measurement from sample-based to time-based averaging\n- move Transmit/Power below Mode+Tune on small viewports\n- add practical mobile breakpoints and width handling\n- resize and tune logo/top spacing/layout placement\n- make map height viewport-aware with adjustable minimum\n- improve FT8/WSPR control wrapping on small screens\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Refine main control interactions and presentation in the HTTP frontend.\n\n- remove frequency and mode Set buttons\n- apply mode changes immediately on picker change\n- place Mode/Tune/Transmit-Power controls in one horizontal row\n- align control labels vertically across that row\n- move and enlarge MHz/kHz/Hz selector beside frequency input\n- keep Enter-to-set frequency behavior\n- switch signal measurement to elapsed-time averaging\n- enlarge header logo 2x\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add rigctl frontend visibility in HTTP status/about UI and refine frequency controls layout.\n\n- track rigctl listen endpoint and active rigctl client count in frontend runtime context\n- inject rigctl metadata into HTTP /events payload\n- show rigctl endpoint and rigctl client count in About tab\n- remove frequency Set button from UI\n- move MHz/kHz/Hz selector beside frequency input and enlarge it\n- center jog wheel row and keep Enter-to-set frequency behavior\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Improve rigctl interoperability with hamlib/WSJT-X and stabilize FT-817 PTT handling.\n\n- support extended '+' command replies\n- accept decimal and MHz-style frequency inputs\n- retry set_freq rounded to 10 Hz on CAT alignment errors\n- add compatibility handling for get_level probes\n- broaden PTT command parsing and aliases\n- derive PTT capability dynamically from snapshot data\n- improve dump_state/dump_caps compatibility behavior\n- move temporary rigctl diagnostics to debug level\n- make FT-817 set_ptt more reliable with unlock/clear and double-send\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Return setting=value lines with a done terminator for dumpcaps commands so Hamlib netrigctl_open can parse capabilities.
Add a unit test that verifies dumpcaps output formatting.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add min_freq_step_hz to RigCapabilities, set backend values, and make HTTP frontend parse suffix-less frequency input using the selected unit while snapping set/jog frequencies to rig step granularity.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Make the frequency field render and parse values in the currently selected unit (MHz/kHz/Hz), including immediate refresh when switching jog step.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Require a receiver locator source when PSK Reporter is enabled,
show inactive reason in status text, and add periodic uplink runtime
counters (received/sent/skipped/errors).
This makes missing-spot issues visible instead of silently dropping
all decode events.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Implement ALSA/CPAL stream auto-recovery by recreating input/output
streams after backend callback failures with bounded retry delay.
Also improve HTTP frontend resilience by polling /status on reconnect
and after SSE errors to refresh snapshot state after broken pipes.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Pass pskreporter_status through RigTaskConfig and apply it to rig_task
state initialization so snapshot updates keep the About-tab value.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Fix server config discovery to include ~/.trx-server.toml and update
example/print-config output to explicitly include general latitude and
longitude fields.
Also update config docs and add a test for the legacy search path.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add pskreporter_status to shared rig snapshots and display it in the
HTTP frontend About tab.
Also include audio stream error log throttling to avoid repetitive ALSA
error flooding in backend logs.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add CONFIGURATION.md documenting all server/client config options,
defaults, and validation constraints, and link it from README.md.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a PSK Reporter uploader task that subscribes to decoded FT8/WSPR
messages and sends spots over UDP when [pskreporter].enabled is true.
Include new [pskreporter] server config options and example config docs,
and hardcode software identification as 'trx-server v<version> by SP2SJG'.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Wire the WSPR period label to a live 120-second slot countdown so
it no longer stays at the placeholder value.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Reorder Plugins subtabs to align with the Overview plugin listing
(APRS, CW, FT8, WSPR), with Map moved to the end.
Also add a live FT8 period countdown indicator in the FT8 panel.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Expose a WSPR subtab in the Plugins view with its own controls and
message list, wire a dedicated wspr.js asset endpoint, and route WSPR
decode events to the new panel.
This makes WSPR visible in the HTTP frontend instead of reusing the
FT8 panel for WSPR messages.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Drop all external wsprd wrapper/build plumbing and keep trx-wspr on
an internal Rust-only decoder path.
This removes wsprd process dependencies and leaves a native decoder
scaffold with the same public API for incremental algorithm work.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Extract external wsprd process invocation into a dedicated wrapper
module and keep WSPR decode orchestration in a separate decoder module.
This mirrors the ft8 crate layering and makes wsprd integration easier
to test and evolve.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a new trx-wspr crate that wraps wsprd slot decoding and parsed
results, wire it into the server audio pipeline, and emit WSPR decode
events to clients.
Also add frontend event routing for WSPR decode messages and temporary
rendering in the FT8 table until a dedicated WSPR panel is introduced.
Co-authored-by: Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Reanalyze current architecture status and rewrite ENHANCEMENT.md to reflect remaining high-impact issues after completed phases.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add semantic validate() checks for server/client config models and fail fast on invalid ranges, field combinations, and auth token values before runtime startup.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add coordinated shutdown signaling and task supervision for long-running server and client tasks to avoid detached runtimes on Ctrl+C.
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace legacy global FrontendRegistry with bootstrap context adapter that
maintains backward compatibility while delegating to explicit context.
Changes:
- Create BOOTSTRAP_CONTEXT: OnceLock<Arc<Mutex<FrontendRegistrationContext>>>
- register_frontend(): delegates to bootstrap context
- is_frontend_registered(): reads from bootstrap context
- registered_frontends(): reads from bootstrap context
- spawn_frontend(): reads from bootstrap context
Result: Plugins continue calling global functions, but all operations
now route through the bootstrap context. Frontends receive context
parameter explicitly, enabling multiple concurrent instances.
Complete de-globalization achieved with full backward compatibility.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace legacy global BackendRegistry with bootstrap context adapter that
maintains backward compatibility while delegating to explicit context.
Changes:
- Create BOOTSTRAP_CONTEXT: OnceLock<Arc<Mutex<RegistrationContext>>>
- register_backend(): delegates to bootstrap context
- is_backend_registered(): reads from bootstrap context
- registered_backends(): reads from bootstrap context
- build_rig(): reads from bootstrap context
Result: Plugins continue calling global functions, but all operations
now route through the bootstrap context instead of a separate global
registry. This completes the de-globalization while maintaining full
backward compatibility with existing plugins.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Create FrontendRuntimeContext as Arc during async_init and pass it to all
spawn_frontend calls, enabling explicit context-based initialization.
Changes:
- Create frontend_runtime_ctx as Arc<FrontendRuntimeContext>
- Pass context to all spawn_frontend invocations in the frontend loop
- Update comment to reflect Phase 3C completion
This completes the threading of context through the client bootstrap,
moving away from global mutable state for audio channels, decode channels,
and authentication tokens.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Update all three built-in frontends to accept Arc<FrontendRuntimeContext>
parameter in their spawn_frontend implementations:
- trx-frontend-http: passes context to serve function
- trx-frontend-http-json: passes context to serve function
- trx-frontend-rigctl: accepts context (minimal impact, no globals used)
Frontends are now ready to use context for audio channels, decode
channels, and auth tokens instead of accessing globals directly.
This completes the trait signature change for all frontends.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Update FrontendSpawner trait and related functions to accept and pass
Arc<FrontendRuntimeContext> parameter instead of relying on global
accessors for audio channels, decode channels, and auth tokens.
Changes:
- FrontendSpawner::spawn_frontend now accepts context parameter
- FrontendSpawnFn type signature includes context parameter
- FrontendRegistrationContext::spawn_frontend passes context to spawner
- Global spawn_frontend function accepts and passes context
This enables frontends to receive runtime data explicitly without
accessing globals, improving testability and supporting multiple
concurrent frontends with different contexts.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Demonstrate context-based frontend initialization in async_init.
Creates FrontendRegistrationContext and FrontendRuntimeContext at bootstrap
to establish the pattern for explicit frontend management instead of globals.
Full threading of context through spawn_frontend would require changing the
frontend trait signature and updating all frontend implementations - planned
for Phase 3C.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add register_builtin_backends_on(context: &mut RegistrationContext) function
to allow explicit backend registration on a context instead of always using globals.
This enables proper initialization sequencing where backends are registered
on a specific context that can be passed through bootstrap.
The global register_builtin_backends() still works for plugin compatibility,
delegating to the new context-based approach.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Create explicit context types for frontend registration and runtime:
FrontendRegistrationContext:
- register_frontend(name, spawner) - register a frontend
- is_frontend_registered(name) - check if registered
- registered_frontends() -> Vec<String> - list all frontends
- spawn_frontend(name, ...) -> DynResult - spawn a frontend
FrontendRuntimeContext (NEW):
- audio_rx: broadcast channel for audio RX
- audio_tx: mpsc channel for audio TX
- audio_info: watch channel for audio stream metadata
- decode_rx: broadcast channel for decoded messages
- aprs_history: Arc<Mutex<VecDeque>> for APRS decode history
- cw_history: Arc<Mutex<VecDeque>> for CW decode history
- ft8_history: Arc<Mutex<VecDeque>> for FT8 decode history
- auth_tokens: HashSet for authentication
Replaces global mutable state with explicit context that can be
threaded through bootstrap. Maintains global API for compatibility.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Create explicit RegistrationContext type for backend factory registration
instead of relying solely on global mutable state.
New RegistrationContext:
- register_backend(name, factory) - register a backend
- is_backend_registered(name) - check if registered
- registered_backends() -> Vec<String> - list all backends
- build_rig(name, access) -> DynResult - instantiate a rig
Maintains global API for plugin compatibility, delegates to context.
Paves way for threading context through bootstrap in Phase 3B.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace client's local implementations with unified trx-app utilities.
Changes:
- Use trx_app::normalize_name() instead of local fn
- Depend on trx-app crate
This eliminates the client's copy of the normalize_name logic and ensures
both server and client use the same implementation.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace server's local implementations with unified trx-app utilities.
Changes:
- Use trx_app::normalize_name() instead of local fn
- Depend on trx-app crate
This eliminates the server's copy of the normalize_name logic and ensures
both server and client use the same implementation.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace duplicated RigState initialization with new constructor methods.
Changes:
- Use RigState::new_uninitialized() in main.rs
- Use RigState::from_snapshot() in remote_client.rs
- Remove standalone state_from_snapshot() function
These constructors eliminate 155 lines of duplicated struct literals
and provide clear semantics for different initialization contexts.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace duplicated RigState initialization with new constructor methods.
This fixes a critical bug where rig_task was hardcoding 144.3 MHz/USB
instead of respecting config.initial_freq_hz and config.initial_mode.
Changes:
- Use RigState::new_with_metadata() in main.rs
- Use RigState::new_with_metadata() in rig_task.rs (FIX: now respects config)
- Remove 45-line build_initial_state() helper function
The rig_task bug fix ensures the configured initial frequency and mode
are applied when the rig task starts, instead of always using defaults.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add new constructors and Default trait implementations to consolidate
RigState initialization patterns. This eliminates 195 lines of duplicated
struct literals across server and client.
New methods:
- RigState::new_uninitialized() - for client-side initialization
- RigState::new_with_metadata() - for server-side with config values
- RigState::from_snapshot() - convert RigSnapshot to full state
- Default for RigStatus - 2m calling frequency (144.3 MHz) in USB mode
- Default for RigControl - disabled with no active repeater settings
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Update workspace Cargo.toml to include new trx-protocol crate
and update Cargo.lock with new dependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Use centralized trx-protocol crate for:
- parse_mode and envelope parsing
- command mapping (ClientCommand -> RigCommand)
- token validation via RegistryTokenValidator wrapper
RegistryTokenValidator maintains compatibility with global auth
registry pattern while leveraging shared protocol logic from
trx-protocol. Removes duplicate auth and codec functions.
No behavior changes, all tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Use centralized trx-protocol::rig_command_to_client for command
conversion, eliminating 62 lines of duplicate code in mode handling
and command mapping logic.
Updates remote_client to delegate to trx-protocol for bidirectional
command conversion. No behavior changes, all tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Use centralized trx-protocol crate for:
- parse_mode and mode string parsing
- parse_envelope with fallback behavior
- command mapping (ClientCommand -> RigCommand)
- token validation with SimpleTokenValidator
Removes 116 lines of duplicate code. Wraps validator in Arc for
safe sharing across async tasks. No behavior changes, all tests pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add compiler flags to suppress C warnings from vendored ft8_lib:
- -Wno-unused-const-variable for db_power_sum array
- -Wno-unused-function for ft8_decode_multi_symbols
These are harmless warnings from external code we don't control.
Suppressing at build system level keeps external code unchanged.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Remove AppKit frontend mentions from documentation:
- Update AGENTS.md project structure
- Remove AppKit from capabilities table in OVERVIEW.md
- Remove AppKit from frontends table in OVERVIEW.md
- Remove AppKit from Frontends section in README.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Remove macOS AppKit frontend (trx-frontend-appkit) and related code:
- Delete appkit crate directory
- Remove appkit dependency and feature from Cargo.toml
- Remove appkit imports, main thread handling, and config from main.rs
- Remove AppKit config struct from config.rs
- Remove appkit section from example config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add POST endpoints for toggle_aprs_decode, toggle_cw_decode,
clear_aprs_decode, and clear_cw_decode. Add toggle buttons in APRS
and CW tabs. Render decoder enabled state from SSE updates. Clear
button now also resets server-side decoder state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add SetAprsDecodeEnabled, SetCwDecodeEnabled, ResetAprsDecoder, and
ResetCwDecoder to the JSON TCP frontend command mapping.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Initialize decoder state fields and map new RigCommand/ClientCommand
variants through the remote client TCP bridge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Process decoder commands as early returns in rig_task (no CAT needed).
Check aprs_decode_enabled/cw_decode_enabled flags in decoder tasks
alongside mode. Track reset_seq to trigger decoder.reset() on clear.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add aprs_decode_enabled, cw_decode_enabled, aprs_decode_reset_seq, and
cw_decode_reset_seq fields to RigState and RigSnapshot. Add corresponding
RigCommand and ClientCommand variants for toggling and resetting decoders.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace HEAD probe with EventSource readyState check to properly
detect 404 vs connection drop. HEAD requests to SSE endpoints may
not behave reliably across all setups.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Log whether audio/decode is enabled at startup and warn when
/decode is requested but the decode channel was not set. Helps
diagnose broken decode pipelines.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The APRS info field is raw AX.25 bytes, not valid UTF-8.
from_utf8_lossy inserts multi-byte replacement characters, causing
panics when the position parser sliced by byte index. Switch
parse_aprs_position, parse_aprs_compressed, parse_aprs_lat, and
parse_aprs_lon to operate on &[u8] instead of &str.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The decode EventSource silently retried on 404 leaving the status
stuck on "Waiting for server decode". Probe /decode first to
distinguish 404 (audio disabled) from real SSE errors and update
the APRS/CW status text accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The capture thread only checked Opus broadcast subscribers to decide
whether to start cpal input. PCM tap subscribers (APRS/CW decoders)
were not considered, so decoding never started without a browser
audio client. Include pcm_tx receiver count in the has_receivers
check.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Server-side decoding makes client-side decoders redundant. Remove
~1000 lines of browser-side Bell 202 AFSK, AX.25/APRS parsing, and
Goertzel CW decoding. The frontend now relies solely on the /decode
SSE endpoint for decoded data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Clears the packet list, map markers are unaffected. Also wipes
the persisted packet history from localStorage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add /decode SSE endpoint streaming decoded messages from the server.
Add decode channel OnceLock with set/subscribe pattern.
In the browser, connect to /decode EventSource and dispatch to
onServerAprs/onServerCw handlers. APRS and CW plugins now receive
server-decoded data automatically while keeping browser-side decoding
as a fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Handle APRS/CW decode message types (0x03/0x04) in audio_client,
deserialize and forward via broadcast channel. Create decode channel
and pass to audio client and HTTP frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Port Bell 202 AFSK demodulator (correlation detector, PLL clock
recovery, NRZI+HDLC, AX.25/APRS parser) and Goertzel CW decoder
(auto tone scan, auto WPM via k-means, Morse lookup) from browser JS
to Rust.
Add PCM tap to audio capture thread, spawn APRS/CW decoder tasks
gated by rig mode (PKT for APRS, CW/CWR for CW). Forward decoded
messages over the audio TCP wire using new message types 0x03/0x04.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add localStorage persistence (trx_ prefix) for UI settings:
- Jog step, RX/TX volume (app.js)
- CW WPM, tone, threshold, auto-detect flags (cw.js)
- APRS decoded packets and running state (aprs.js)
APRS decoder auto-restarts on page refresh if it was active,
and all decoded packets plus map markers are restored from storage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Render non-ASCII characters in decoded APRS info text as yellow
[0xNN] hex tags. Printable ASCII is HTML-escaped to prevent XSS.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Show the proper APRS symbol sprite icon on map markers instead of
generic green circles. Falls back to a green circle marker when no
symbol info is available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a Map sub-tab under Plugins that displays an interactive
OpenStreetMap via Leaflet.js showing:
- Receiver location (blue marker) from server config lat/lon
- APRS station positions (green markers) updated in real-time
The map lazy-initializes on first tab switch, handles tile rendering
on tab visibility changes, and deduplicates station markers by
callsign. Also includes the fallback snapshot lat/lon fields in the
API layer.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Map server_latitude and server_longitude from RigSnapshot to RigState
in remote_client and set None defaults in standalone client init.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add latitude and longitude Option<f64> fields to [general] config
section and propagate through ResolvedConfig, RigTaskConfig, and
build_initial_state into the RigState/RigSnapshot pipeline.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Propagate receiver location (WGS84 decimal degrees) through the state
pipeline so frontends can display the server position on a map.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
- Add APRS symbol icons using hessu/aprs-symbols sprite sheets
- Parse uncompressed and compressed position formats for lat/lon
- Render clickable OpenStreetMap links for position packets
- Replace delay-and-multiply discriminator with mark/space correlation
detector for more robust AFSK decoding
- Reduce PLL gain from 0.7 to 0.4 for stable clock recovery
- Move plugin JS files to plugins/ subdirectory
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add Auto checkboxes (checked by default) next to WPM and Tone inputs.
Auto Tone uses a multi-bin Goertzel scan across 300-1200 Hz with
stability tracking. Auto WPM collects on-durations in a rolling buffer
and uses k-means-style clustering to separate dits from dahs.
Unchecking Auto allows manual control.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Extract the APRS decoder from app.js into its own aprs.js file and add
a new CW (Morse code) decoder plugin in cw.js. The CW decoder uses a
Goertzel tone detector with configurable WPM, tone frequency, and
signal threshold. The Plugins tab now has three sub-tabs: Overview,
APRS, and CW.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace bandpass filter envelope detector with delay-and-multiply
frequency discriminator. Add biquad bandpass pre-filter centered at
1700 Hz to remove out-of-band noise. Replace IIR low-pass with
moving-average LPF over half a bit period, which places a null at
2x mark frequency for clean discriminator artifact removal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add fast path in the TCP listener to serve GetSnapshot requests
directly from the state watch channel, so clients get a response
even while the rig task is initializing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace the coherent correlation detector with a non-coherent
bandpass filter + envelope detection approach for significantly
better frequency discrimination between mark (1200Hz) and space
(2200Hz) tones. Uses two cascaded 2nd-order IIR biquad filters
per tone with IIR-smoothed envelope detection.
Replace hard clock reset with PLL-style gradual correction for
smoother bit timing recovery.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add silence detection that resets the demodulator state when RMS
drops below threshold, so burst-mode APRS packets after squelch
activation start with a clean correlator and clock recovery.
Display CRC-failing frames at reduced opacity with a [CRC] tag to
aid debugging. Enhanced CRC-fail console logging now includes
attempted address decode, bit count, and hex dump.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Assign green to VFO A, yellow to VFO B, and deterministic hues
to additional VFOs. The active VFO color is also applied to the
main frequency display. Rework the header title to show
"trx-rs <version> @ <callsign>'s <rig>" instead of "<rig> status".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Fix three bugs in the Bell 202 AFSK demodulator preventing frame
decoding:
- Separate NRZI state from clock recovery (shared lastBit variable
caused NRZI to always output 1)
- Buffer 1-bits in ones counter instead of pushing to frameBits
immediately, preventing flag/stuff bits from contaminating frame
data and corrupting byte alignment
- Detect flags via ones-count on decoded bits instead of shift
register on raw bits
Also add framed packet log container styling, remove redundant
description text, and add debug counters logging pipeline health
to the browser console.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace server-side /frontends endpoint with client-side plugin system.
The Plugins tab now has Overview and APRS sub-tabs. The APRS plugin
decodes packets from RX audio using Bell 202 AFSK demodulation (1200
baud), AX.25 frame decoding with NRZI/HDLC, and CRC-16-CCITT
validation. Decoded packets are displayed in a scrolling log.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add GET /frontends API endpoint returning registered frontend names as
JSON. Add Plugins tab to the web UI that fetches and displays the list.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Close #tab-main properly so the footer and About tab are not nested
inside it — footer is now visible on all tabs.
Add server address, rig connection method, supported modes, and VFO
count to the About tab.
Move logo from translucent centered overlay to solid image in the
top-right of the header.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a tab bar to the HTTP frontend card. All existing controls stay in
the Main tab. A new About tab shows server version, callsign, rig info,
client version, and connected client count, updated live via SSE.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Return 204 for non-WebSocket GET requests to /audio instead of
letting actix-ws reject with 400. This allows the browser's audio
availability probe to work correctly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
The audio listener was ignoring the CLI --listen override and always
binding to the config default (127.0.0.1), making it unreachable
from remote clients.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Start the output stream paused and only play when TX packets
arrive. Pause again when the packet queue drains to prevent
continuous ALSA buffer underruns (EPIPE errno -32) on Linux.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Use subsecond nanosecond jitter to return a varying signal strength
(2-8) from the dummy backend instead of a static value of 5.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace the rolling canvas signal graph with a practical signal
measurement feature. The operator can start/stop measurement to
collect signal samples, then view averaged and peak S-unit results.
Also fix signal display to correctly convert dBm wire format to
S-units using standard S-meter scale (S1=-121dBm, S9=-73dBm,
6dB per S-unit).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
- Make server callsign a clickable link to qrzcq.com
- Center frequency input text and use DSEG14 Classic font
- Include callsign in page title (e.g. N0CALL - trx-frontend-http v0.1.0)
- Block jog wheel when rig lock is active
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Load DSEG14 Classic 14-segment LCD font from CDN and apply it to
the frequency input at 2x size for a realistic radio display look.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
- Add RX/TX volume sliders with GainNode audio chain integration,
scroll wheel support, and percentage display
- Display server callsign and version from SSE event data
- Merge TX Limit hint into its label line
- Add copyright footer with link to haxx.space
- Uniform section spacing via flex gap on #content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Carry server_callsign and server_version through from received
snapshots into the local RigState. Default client callsign is
N0CALL when not configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Set server_callsign and server_version on the rig state so they
are included in snapshots sent to clients. Default callsign is
N0CALL when not configured.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add optional server_callsign and server_version fields to both
RigState and RigSnapshot so that server identity information can
flow through the protocol to clients and frontends.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Reorganize layout: frequency row with jog wheel on top, mode and
transmit/power side by side below. Replace VFO text box with segmented
picker. Add rolling signal history canvas. Track connected SSE clients
and display count in status hint. Unify button heights and add Enter
key support for TX limit input.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
time::interval() fires its first tick immediately. Recreating it on
every loop iteration made the select! always resolve instantly,
turning the main polling loop into a busy-loop (~13% CPU idle).
Replace with a Box::pin(sleep()) that is only reset after it
completes or when the poll duration changes (rx/tx transition).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Instead of running the cpal input stream continuously, start it
paused and only activate when broadcast subscribers are present.
When the last client disconnects, pause the stream and sleep at
100ms intervals polling for new receivers.
This eliminates idle CPU usage from continuous CoreAudio callbacks,
channel allocations, and sample processing when nobody is listening.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Enable audio streaming by default in AudioClientConfig so the
client connects to the server audio port without requiring
explicit configuration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Enable audio streaming by default in AudioConfig.
Fix panic in audio playback thread caused by calling
tokio::runtime::Handle::current() from a plain std::thread.
Use rx.blocking_recv() instead, which is the correct API for
consuming a tokio mpsc receiver from a synchronous context.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Move window._opusDecoder, window._txEncoder, window._nextPlayTime
into closure-scoped variables to avoid polluting the global namespace.
Add showHint() helper to debounce status hint text, preventing
multiple button handlers from fighting over powerHint.textContent.
Throttle audio level indicator updates to max 10/sec instead of
updating on every Opus packet (~50/sec).
Hide audio controls row if the server has no audio configured
(checks /audio endpoint on load).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a 120-second TX safety timeout that auto-releases PTT if no mic
data flows (browser crash, disconnect). Timer resets on each audio
callback. Shows countdown in status when < 10s remaining.
Add beforeunload handler that releases PTT via navigator.sendBeacon
when the browser tab is closed during TX.
Detect WebCodecs support on page load and show "Audio requires
Chrome/Edge" on non-Chromium browsers instead of silently failing.
Remove dead playBuffer/playNode variables that were declared but
never used. Fix TX AudioData to always use mono (channel 0) with
numberOfChannels: 1 matching the f32-planar format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace all hardcoded colors with CSS custom properties on :root for
easier theming. Add focus-visible outlines for keyboard accessibility.
Add mobile breakpoints for screens <= 480px.
Fix PTT button styling to use theme-aware colors (var(--accent-red))
instead of hardcoded light-theme colors (#ffefef, #f3f3f3).
Remove unused .value, .controls, and .section-title CSS classes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Resize favicon from 1024x1024 to 64x64 and logo from 1024x1024 to
256x256, reducing total image size from ~3 MB to ~70 KB.
Fix SSE heartbeat race condition where the 10s ping interval competed
with the 8s stale threshold, causing spurious reconnects. Now pings
every 5s server-side, with a 15s stale threshold and 5s check interval
client-side.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Split the monolithic status.rs string template into three files under
assets/web/ (index.html, style.css, app.js) loaded via include_str!.
Add /style.css and /app.js endpoints with correct content types.
This makes the frontend editable with proper syntax highlighting and
linting support in editors.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add Audio streaming section and Dependencies table covering system
libraries (libopus, cmake, pkgconf) and new Rust crates (cpal, opus,
bytes, actix-ws).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add /audio WebSocket endpoint that streams RX Opus frames to the
browser and accepts TX frames back. Browser UI includes RX/TX Audio
toggle buttons with WebCodecs Opus decode/encode and a level indicator.
TX audio automatically engages PTT on start and releases on stop or
WebSocket disconnect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Connect to the server audio TCP port, relay RX Opus frames into a
broadcast channel and TX frames from an mpsc channel. Pass audio
channels to the HTTP frontend via set_audio_channels. Reconnects
with exponential backoff on disconnect.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add AudioConfig to server configuration with support for RX capture
and TX playback via cpal and Opus encoding. Run a dedicated TCP
listener (default port 4533) that sends StreamInfo on connect, streams
RX Opus frames to clients, and receives TX frames back.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace #[tokio::main] with a manual fn main() that builds the tokio
runtime explicitly. All async initialization moves into async_init().
When the appkit frontend is requested, the runtime context is entered
on the main thread and run_appkit_main_thread() is called directly,
giving AppKit thread 0 as required by MainThreadMarker. Ctrl+C is
handled via a spawned task that calls process::exit.
When appkit is not requested, behaviour is unchanged: block on Ctrl+C.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Extract the AppKit event loop from FrontendSpawner::spawn_frontend into
a new public run_appkit_main_thread() function that blocks on the
calling thread. This allows the process main thread (thread 0) to drive
the UI, which is required for MainThreadMarker::new() to succeed.
The FrontendSpawner impl now only spawns the async state watcher task.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Allow connecting with just an IP address (e.g. --url 127.0.0.1)
instead of requiring host:port format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a JSON-over-TCP listener so trx-client can connect to trx-server.
Speaks the ClientEnvelope/ClientResponse protocol from trx-core::client.
- New listener.rs module with per-client connection handling
- ListenConfig/AuthConfig in config.rs (default: 127.0.0.1:4532)
- CLI args --listen and --port for override
- Optional token-based authentication
- Updated example config with [listen] section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Register a dummy rig backend that holds state in memory and responds
to all CAT commands immediately. Useful for development and testing
without hardware.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Add a new trx-frontend-appkit crate using objc2 + AppKit as a
replacement for the removed Qt/QML frontend. The frontend provides
the same feature set: frequency/mode/band display, PTT/power/VFO/lock
controls, signal/TX metering, and frequency/mode/TX-limit input.
Architecture splits platform-agnostic model (model.rs) from AppKit
UI (ui.rs) to facilitate future UIKit porting. State flows from the
async tokio watcher via std::sync::mpsc to the AppKit main thread;
button actions flow back through a channel to stay on the UI thread.
Feature-gated behind `appkit-frontend` cargo feature.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Remove the Linux-only Qt/QML frontend (trx-frontend-qt) crate and all
references to it from the workspace, trx-client binary, configuration,
and documentation. This prepares for replacement with a native macOS
AppKit frontend.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Update dependency paths to new trx-backend and trx-frontend
locations. Replace trx-bin references with trx-server/trx-client.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Replace trx-rs.toml.example with separate trx-server.toml.example
and trx-client.toml.example. Update OVERVIEW.md and README.md
references from trx-bin to trx-server/trx-client.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
Move the frontend and backend crate trees to live physically under their
respective binary crate directories, grouping related code together
without merging crate boundaries. Also flatten sub-crate nesting by
moving them out of src/ subdirectories into direct children.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete trx-bin (all-in-one) and trx-bin-common (shared lib). Each binary
now has its own config, plugins, and helper modules inlined.
- trx-server: backend-only daemon with ServerConfig (general, rig, behavior)
no frontend dependencies
- trx-client: remote client with ClientConfig (general, remote, frontends)
includes all frontend support (http, rigctl, http-json, qt)
- Dedicated config files: trx-server.toml / trx-client.toml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@@ -99,11 +86,11 @@ The rig controller (`src/trx-core/src/rig/controller/`) is the central state man
### Decoders
Signal decoders run as background tasks in `trx-server`, consuming decoded audio. `trx-ftx`provides the FT8/FT4/FT2 decoder in pure Rust. Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via `trx-decode-log`.
Signal decoders run as background tasks in `trx-server`, consuming decoded audio. `trx-ft8`wraps a C library (`external/ft8_lib`). Decoded frames can be forwarded to PSKReporter and APRS-IS (IGate) uplinks, or logged via `trx-decode-log`.
## Diagrams
### Plugin system
Always use [Mermaid](https://mermaid.js.org/) for diagrams in Markdown files. Never use ASCII art, box-drawing characters, or plain-text diagrams. GitHub renders Mermaid natively in ```mermaid fenced code blocks.
Both `trx-server` and `trx-client` can load shared-library plugins exporting a `trx_register` symbol. Search paths: `./plugins`, `~/.config/trx-rs/plugins`, `TRX_PLUGIN_DIRS` env var.
## Commit Format
@@ -112,27 +99,3 @@ Always use [Mermaid](https://mermaid.js.org/) for diagrams in Markdown files. Ne
```
Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Use `(trx-rs)` for repo-wide changes. Sign commits with `git commit -s`. Write isolated commits per crate.
## Codebase Review Observations
Full architecture documentation: `docs/Architecture.md`
Improvement plan: `docs/Improvement-Areas.md`
*Last reviewed: 2026-03-29*
### Strengths
- **Explicit state machine**: `RigMachineState` FSM (7 states) prevents invalid states with a deterministic transition table and exhaustive matching. Well-tested with lifecycle, error recovery, and invalid transition tests. `ReadyStateData`/`TransmittingStateData` use `pub(crate)` fields with controlled accessors.
- **Multi-rig architecture**: Per-rig task isolation with `HashMap<rig_id, RigHandle>` routing, per-rig state/spectrum/audio/decoder-history channels, dual-connection model (main + spectrum) in the client, and backward-compatible single-rig mode.
- **Async concurrency model**: Proper use of tokio channels -- `watch` for state snapshots, `broadcast` for PCM/decode fan-out, `mpsc` for commands. No mutex contention on hot paths. Spectrum deduplication collapses concurrent GetSpectrum requests.
- **Comprehensive SDR support**: Full DSP pipeline with multi-mode demodulation (SSB, AM, SAM, FM, WFM, AIS, VDES), virtual channel management, squelch, noise blanker, spectrum FFT, RDS decoding. AVX2-optimized FM discriminator with scalar fallbacks.
- **Pure Rust decoders**: FT8/FT4/FT2, APRS, CW, WSPR, AIS, VDES, RDS -- all implemented without C FFI dependencies. Consistent decoder pattern: stateful struct → `process_block()` → `decode_if_ready()`.
- **Good test coverage** in protocol layer: codec, mapping, auth all have thorough unit tests with round-trip verification. 45+ mapping tests cover all command variants.
- **Feature-gated backends**: ft817, ft450d, soapysdr compiled conditionally to minimize binary size. Factory pattern with name normalization for registration.
- **Defensive error handling**: Lock poisoning recovery, stream error deduplication with 60s summaries, input truncation in logs (128 chars), per-IP rate limiting on auth endpoints.
- **Well-documented DSP guidelines**: `docs/Optimization-Guidelines.md` captures lessons on NCO design, polyphase resampling, AVX2 batching, and stereo FM decoding.
### Areas for Improvement
All P0–P3 items resolved or dropped. See `docs/Improvement-Areas.md` for details.
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting — in part or in whole — any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
The server and client are separate binaries. They communicate over **JSON-over-TCP** (control) and **Opus-encoded TCP** (audio). Both binaries can load shared-library plugins at startup.
@@ -91,6 +99,7 @@ The server and client are separate binaries. They communicate over **JSON-over-T
**Known limitation:** IQ sample streaming (`real_iq_source.rs:149–157`) is not yet implemented — the IQ source currently returns zero buffers. The soapysdr 0.3 crate lacks streaming APIs; direct `soapysdr-sys` FFI or a crate upgrade would be required.
---
## Client (trx-client)
@@ -647,8 +658,32 @@ Built on **Actix-web**, serves a browser-based control panel.
| GET | `/audio` | WebSocket audio stream |
| GET | `/favicon.png` | Static asset |
`/status` response includes a `FrontendMeta` block:
| `server.rs` | Actix app builder, middleware, CORS |
| `api.rs` | REST handler functions |
| `audio.rs` | WebSocket ↔ PCM audio bridge |
| `auth.rs` | Token or basic-auth middleware |
| `status.rs` | State formatting for JSON responses |
### Rigctl Frontend (`trx-frontend-rigctl/`)
Hamlib-compatible plaintext TCP interface on port 4532. Allows WSJT-X, JS8Call, and other Hamlib-aware applications to control the rig without modification.
@@ -669,14 +704,14 @@ All decoders run as background Tokio tasks inside `trx-server`. They subscribe t
|-------|---------|-------|
| `trx-aprs` | APRS (AX.25) | Forwards to APRS-IS if enabled |
| `trx-cw` | CW / Morse | Auto WPM detection |
| `trx-ftx` | FTx | Pure Rust FT8/FT4/FT2 decoder; posts to PSKReporter |
| `trx-ft8` | FT8 | Wraps `external/ft8_lib` C library via FFI; posts to PSKReporter |
| `trx-wspr` | WSPR beacons | Posts to PSKReporter |
| `trx-rds` | FM RDS | Station name, radiotext, time |
Control commands (e.g., `SetAprsDecodeEnabled(bool)`, `ResetCwDecoder`) are routed through `rig_task.rs` to the active decoder tasks.
Decoded events are multiplexed onto the audio stream wire protocol (`0x03`–`0x06` frame types) and also buffered in `DecoderHistories` for replay to newly connected clients.
Decoded events are multiplexed onto the audio stream wire protocol (`0x03–0x06` frame types) and also buffered in `DecoderHistories` for replay to newly connected clients.
---
@@ -711,7 +746,7 @@ Decoders / Audio server
WFM demodulator supports:
- Stereo pilot detection and L+R/L−R matrix decoding
- Configurable de-emphasis time constant (50 us EU / 75 us US)
- Configurable de-emphasis time constant (50 µs EU / 75 µs US)
The rig task is the heart of the server. Key implementation details:
- **Command batching**: Accumulates pending requests before processing sequentially in FIFO order.
- **Spectrum deduplication**: Concurrent `GetSpectrum` requests are collapsed — one DSP computation broadcasts to all waiting responders.
- **Adaptive polling**: Poll interval adjusts based on TX state (100ms during TX, 500ms idle).
- **Grace period**: 800ms pause on polling after power-on/off operations to let hardware settle.
- **VFO priming**: Optional initialization sequence that toggles VFO A/B to populate the state cache.
- **Per-rig decoder histories**: Each rig maintains independent `Arc<DecoderHistories>` for all 11 decoder types.
- **Configurable timeouts**: `command_exec_timeout` (default 10s) and `poll_refresh_timeout` (default 8s) are configurable via `RigTaskConfig` and the TOML `[timeouts]` section.
- **Crash recovery**: Rig tasks are monitored; on crash, an `Error` state is broadcast to clients via the watch channel so they see the failure instead of silent timeout.
### Audio Pipeline (`audio.rs` — 3,977 lines)
The audio module handles decoder history storage and stream management:
- **`DecoderHistories`**: Per-rig mutable store for 11 decoder history queues (AIS, VDES, APRS, HF_APRS, CW, FT8, FT4, FT2, WSPR, WXSAT, LRPT).
- **Time-based retention**: 24h TTL on all history with periodic pruning.
- **Capacity bounds**: Per-decoder max of 10,000 entries (`MAX_HISTORY_ENTRIES`) prevents unbounded memory growth on busy channels.
- **Atomic total count**: `AtomicUsize` with CAS loop avoids acquiring 11 mutex locks in `snapshot_all()`.
- **Lock poisoning recovery with logging**: Uses `lock_or_recover()` helper that logs a warning when recovering from a poisoned mutex.
- **`StreamErrorLogger`**: Suppresses duplicate stream errors with 60s periodic summaries and error classification (alsa_poll_failure, input/output_stream_error).
- **Device enumeration helpers**: `find_input_device()` and `find_output_device()` extract the repeated device lookup logic from `run_capture()`/`run_playback()`.
- **CRC filtering**: APRS records filtered by `crc_ok` before storage.
### Remote Client Dual-Connection Model
`remote_client.rs` maintains two independent TCP connections to the server:
2. **Spectrum connection** (dedicated): Polls `GetSpectrum` at 50ms intervals (20 fps) independently to avoid blocking the main connection during command processing.
Constants: `CONNECT_TIMEOUT: 5s`, `IO_TIMEOUT: 15s`, `SPECTRUM_IO_TIMEOUT: 3s`. Exponential backoff with jitter on reconnect.
### FrontendRuntimeContext Sub-Structs
The `FrontendRuntimeContext` struct in `trx-frontend/src/lib.rs` is decomposed into coherent sub-structs:
This document describes the design and implementation plan for the recorder feature in trx-rs. The recorder captures the demodulated audio stream alongside associated metadata (FFT data, decoded signals, rig state) into a structured session on disk, with full playback and seeking support from within the application.
## Requirements
| ID | Description |
|----|-------------|
| REQ-REC-001 | When the user starts recording, the system shall record the currently demodulated audio stream. |
| REQ-REC-002 | When recording audio, the system shall store the recording in OPUS format. |
| REQ-REC-003 | While recording audio, the system shall automatically detect whether the recording should be stored in mono or stereo and select the appropriate format. |
| REQ-REC-004 | While recording is active, the system shall simultaneously record FFT data and all currently visible decoded elements, including APRS and FT8. |
| REQ-REC-005 | While recording metadata, the system shall store FFT data and decoded signal data in a structured data file format. |
| REQ-PLAY-001 | Where recorded sessions exist, the system shall allow playback of recordings from within the same application. |
| REQ-PLAY-002 | During playback, the system shall allow the user to seek to any position in the recording. |
| REQ-SYNC-001 | The system shall maintain time synchronization between the audio recording and the associated data file with at least one-second resolution. |
| REQ-REC-006 | While recording is active, the system shall allow the current cursor position to be stored. |
---
## Architecture
### New Crate: `trx-recorder`
A new crate `src/trx-server/trx-recorder/` handles all record and playback logic. It is a library crate consumed by `trx-server`.
```
src/trx-server/
trx-recorder/
src/
lib.rs # Public API: RecorderHandle, start_recorder_task()
- **Format**: Opus, using the `opus` crate (already a workspace dependency via `trx-backend-soapysdr`). Seek index (`index.bin`) provides byte → time mapping.
- **Channel count**: determined at session open from `AudioConfig.channels`. If `channels == 1` → mono; if `channels == 2` → stereo. Written into the file header and recorded in the session's first data event.
- **Sample rate**: preserved from `AudioConfig.sample_rate` (default 48 000 Hz).
### Data File (REQ-REC-004, REQ-REC-005)
`data.jsonl` — one JSON object per line, each with a required `offset_ms` field giving the millisecond offset from session start (satisfies REQ-SYNC-001 at ≥1 s resolution):
- **http-json frontend**: same commands as JSON messages
---
## Playback Engine (REQ-PLAY-001, REQ-PLAY-002)
`PlaybackEngine` opens a session directory and:
1. Reads `audio.opus` and decodes PCM frames in real time.
2. Publishes decoded PCM frames onto a `broadcast::Sender<Vec<f32>>` — the **same channel type** as the live `pcm_tx`, so existing decoder tasks and audio-streaming clients receive playback data transparently.
3. Replays `data.jsonl` events on their original `offset_ms` timestamps, injecting them into the `DecodedMessage` broadcast so the HTTP frontend displays historic decodes during playback.
4. For seek: binary-searches `index.bin` to find the audio byte offset, then replays data events from the same point.
The playback state machine has two modes, switched by a new `RigState.playback` field:
```rust
pubenumPlaybackState{
Live,
Playing{session: String,offset_ms: u64},
Paused{session: String,offset_ms: u64},
}
```
While `PlaybackState` is not `Live`, the server suppresses live hardware polling and PCM capture to avoid mixing live and playback audio.
---
## Time Synchronisation (REQ-SYNC-001)
All timestamps use a single `session_epoch: std::time::Instant` captured at `StartRecording`. Every PCM frame, every data event, and every seek-index entry is stamped as `(Instant::now() - session_epoch).as_millis() as u64`. This gives sub-millisecond internal precision; the requirement of ≥1 s resolution is met by orders of magnitude.
Wall-clock UTC is embedded only in `session_start` (`wall_clock_utc`) and in the session directory name, providing absolute time anchoring without depending on system clock monotonicity for sync.
1. Implement `PlaybackEngine`; Opus decode + PCM broadcast.
2. Add `PlaybackState` to `RigState`; suppress live capture during playback.
3. Implement seek via `index.bin` binary search.
4. Replay `data.jsonl` events; feed into `DecodedMessage` broadcast.
5. Expose start/stop/seek endpoints in `trx-frontend-http`.
---
## Dependencies to Add
| Crate | Use | Already present? |
|-------|-----|-----------------|
| `opus` | Opus encode/decode | Yes (via trx-backend-soapysdr) |
| `serde_json` | data.jsonl serialisation | Yes |
| `tokio::fs` | async file I/O | Yes |
---
## Open Questions
1.**Playback isolation**: Should playback be exclusive (block all CAT commands) or concurrent? Initial design blocks CAT polling; revisit if users need to change frequency during playback.
2.**Session listing API**: The HTTP frontend needs an endpoint to enumerate sessions (`GET /api/recorder/sessions`). Schema TBD in Phase 4.
3.**Storage limits**: `max_session_duration_s` auto-splits sessions; a `max_total_size_gb` housekeeping option may be needed but is out of scope for initial phases.
-`bitrate_bps` (`u32`, default: `24000`, must be `> 0`)
Notes:
- When `[audio].enabled = true`, at least one of `rx_enabled` or `tx_enabled` must be true.
### `[pskreporter]`
-`enabled` (`bool`, default: `false`)
-`host` (`string`, default: `"report.pskreporter.info"`, must not be empty when enabled)
-`port` (`u16`, default: `4739`, must be `> 0` when enabled)
-`receiver_locator` (`string`, optional)
Notes:
- If `receiver_locator` is omitted, server tries deriving it from `[general].latitude`/`longitude`.
- PSK Reporter software ID is hardcoded to: `trx-server v<version> by SP2SJG`.
### `[aprsfi]`
-`enabled` (`bool`, default: `false`)
-`host` (`string`, default: `"rotate.aprs.net"`, must not be empty when enabled)
-`port` (`u16`, default: `14580`, must be `> 0` when enabled)
-`passcode` (`i32`, default: `-1`)
Notes:
- When `passcode = -1` (the default), the passcode is auto-computed from `[general].callsign` using the standard APRS-IS hash algorithm.
-`[general].callsign` must be non-empty when `[aprsfi].enabled = true`; otherwise the IGate is silently disabled at startup.
- Only APRS packets with valid CRC are forwarded; packets from other decoders (FT8, WSPR, CW) are ignored.
- The IGate reconnects automatically with exponential backoff (1 s → 2 s → … → 60 s) on TCP errors.
- Requires `[audio].enabled = true` (APRS packets are decoded from audio).
### `[sdr]`
-`sample_rate` (`u32`, default: `1920000`, must be `> 0`): IQ capture rate in Hz. Must be supported by the device.
-`bandwidth` (`u32`, default: `1500000`): Hardware IF filter bandwidth in Hz.
-`center_offset_hz` (`i64`, default: `100000`): The SDR tunes this many Hz below the dial frequency to keep the signal off the DC spur. Negative values tune above.
### `[sdr.gain]`
-`mode` (`string`, default: `"auto"`): `"auto"` enables hardware AGC (falls back to `"manual"` with a warning if the device does not support it); `"manual"` uses the fixed `value`.
-`value` (`f64`, default: `30.0`): Gain in dB. Used only when `mode = "manual"`.
### `[sdr.squelch]`
-`enabled` (`bool`, default: `false`): Enables virtual software squelch for demodulated audio except WFM on the primary SDR channel.
-`threshold_db` (`f32`, default: `-65.0`, valid: `-140..=0`): Open threshold in dBFS.
-`hysteresis_db` (`f32`, default: `3.0`, valid: `0..=40`): Close hysteresis in dB.
-`tail_ms` (`u32`, default: `180`, valid: `0..=10000`): Tail hold time after signal drops below threshold.
### `[[sdr.channels]]`
Defines one virtual receiver channel within the wideband IQ stream. At least one channel is required when using the `soapysdr` backend. The **first** channel in the list is the *primary* channel: `set_freq` and `set_mode` from rig control apply to it, and `get_status` reads from it.
-`id` (`string`, default: `""`): Human-readable label used in logs.
-`offset_hz` (`i64`, default: `0`): Frequency offset from the dial frequency in Hz. Primary channel should be `0`.
-`mode` (`string`, default: `"auto"`): Demodulation mode. `"auto"` follows the RigCat `set_mode` command; or a fixed mode string: `LSB`, `USB`, `CW`, `CWR`, `AM`, `WFM`, `FM`, `DIG`, `PKT`.
-`audio_bandwidth_hz` (`u32`, default: `3000`): One-sided bandwidth of the post-demodulation audio BPF in Hz.
-`fir_taps` (`usize`, default: `64`): FIR filter tap count. Higher values give sharper roll-off at the cost of latency.
-`cw_center_hz` (`u32`, default: `700`): CW tone centre frequency in the audio domain (Hz).
-`wfm_bandwidth_hz` (`u32`, default: `75000`): Pre-demodulation filter bandwidth for WFM only (Hz).
-`decoders` (`string[]`, default: `[]`): Decoder IDs that receive this channel's PCM audio. Valid values: `"ft8"`, `"wspr"`, `"aprs"`, `"cw"`. Each decoder ID may appear in at most one channel.
-`stream_opus` (`bool`, default: `false`): Encode this channel's audio as Opus and stream to clients over the TCP audio port. At most one channel may set this to `true`.
Notes:
- Requires `libSoapySDR` installed (`brew install soapysdr` on macOS; `libsoapysdr-dev` on Debian/Ubuntu).
- The SDR backend is RX-only. `[audio] tx_enabled` must be `false`.
- Channel IF constraint: `|center_offset_hz + offset_hz| < sample_rate / 2` for every channel; violated channels cause a startup error.
-`[audio] sample_rate` must match the output audio rate of the SDR pipeline (48000 Hz recommended).
- Use `trx-server --print-config` to see all defaults. SDR fields appear only if the binary was built with `--features soapysdr`.
### `[decode_logs]`
-`enabled` (`bool`, default: `false`)
-`dir` (`string`, default: `"$XDG_DATA_HOME/trx-rs/decoders"`; fallback: `"logs/decoders"`, must not be empty when enabled)
-`aprs_file` (`string`, default: `"TRXRS-APRS-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
-`cw_file` (`string`, default: `"TRXRS-CW-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
-`ft8_file` (`string`, default: `"TRXRS-FT8-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
-`wspr_file` (`string`, default: `"TRXRS-WSPR-%YYYY%-%MM%-%DD%.log"`, must not be empty when enabled)
Notes:
- Decoder logs are server-side and split by decoder name (APRS/CW/FT8/WSPR).
- Files are appended in JSON Lines format (one JSON object per line).
- Supported filename date tokens: `%YYYY%`, `%MM%`, `%DD%` (UTC date).
## `trx-client` Options
### `[general]`
-`callsign` (`string`, default: `"N0CALL"`)
-`log_level` (`string`, optional): one of `trace|debug|info|warn|error`
### `[remote]`
-`url` (`string`, optional in file but required at runtime unless provided by CLI `--url`)
-`poll_interval_ms` (`u64`, default: `750`, must be `> 0`)
### `[remote.auth]`
-`token` (`string`, optional)
Notes:
- If provided, token must not be empty/whitespace.
### `[frontends.http]`
-`enabled` (`bool`, default: `true`)
-`listen` (`ip`, default: `127.0.0.1`)
-`port` (`u16`, default: `8080`, must be `> 0` when enabled)
### `[frontends.rigctl]`
-`enabled` (`bool`, default: `false`)
-`listen` (`ip`, default: `127.0.0.1`)
-`port` (`u16`, default: `4532`, must be `> 0` when enabled)
### `[frontends.http_json]`
-`enabled` (`bool`, default: `true`)
-`listen` (`ip`, default: `127.0.0.1`)
-`port` (`u16`, default: `0`)
-`auth.tokens` (`string[]`, default: `[]`)
Notes:
-`port = 0` means ephemeral bind (allowed).
- Empty token strings are invalid.
### `[frontends.audio]`
-`enabled` (`bool`, default: `true`)
-`server_port` (`u16`, default: `4531`, must be `> 0` when enabled)
-`bridge.enabled` (`bool`, default: `false`): enables local `cpal` audio bridge
-`bridge.rx_output_device` (`string`, optional): exact local playback device name
-`bridge.tx_input_device` (`string`, optional): exact local capture device name
-`bridge.rx_gain` (`float`, default: `1.0`, must be finite and `>= 0`)
-`bridge.tx_gain` (`float`, default: `1.0`, must be finite and `>= 0`)
Notes:
- The bridge is intended for local WSJT-X integration via virtual audio devices.
- Linux: typically use ALSA loopback (`snd-aloop`).
- macOS: install a virtual CoreAudio device (e.g. BlackHole), then set device names above.
## CLI Override Summary
### `trx-server`
-`--config`, `--print-config`
-`--rig`, `--access`, positional `RIG_ADDR`
-`--callsign`
-`--listen`, `--port` (JSON listener)
- SDR backend: all SDR options are file-only (`[sdr]` and `[[sdr.channels]]`).
Plugin loading is now explicit (good), but still assumes exact symbol names and raw FFI contracts with no ABI/version handshake. A plugin built against an older/newer ABI can fail at runtime in hard-to-diagnose ways.
### Fix steps
1. Add an ABI version symbol/handshake (`trx_plugin_abi_version`) and reject incompatible plugins with clear errors.
2. Split plugin capability metadata (backend/frontend/both) from registration symbols to avoid noisy failed-load logs.
3. Provide a tiny shared plugin-API crate for stable entrypoint signatures.
## 2) Runtime supervision is still ad-hoc (sleep + abort)
### Files
-`src/trx-server/src/main.rs`
-`src/trx-client/src/main.rs`
### Why this matters
Shutdown is coordinated, but supervision still uses a fixed delay plus manual `abort()` over `Vec<JoinHandle<_>>`. This can mask task failures, race shutdown ordering, and make lifecycle behavior harder to reason about.
### Fix steps
1. Move to `JoinSet` (or a small supervisor type) for task ownership and result handling.
2. Replace fixed sleep with bounded graceful-join timeout logic.
3. Surface task failure reasons consistently in one place.
## 3) JSON/TCP transport logic is duplicated across modules
`read_limited_line`, timeout handling, and response write patterns are repeated in multiple places. This increases drift risk and makes protocol hardening changes expensive.
### Fix steps
1. Extract shared JSON-over-TCP helpers into `trx-protocol` (or a small transport crate/module).
2. Keep one source of truth for max line size, timeout behavior, and framing errors.
3. Cover shared transport with focused tests once instead of per-module copies.
## 4) Boundary tests are present but mostly ignored in constrained envs
Important network-path tests exist, but are marked `#[ignore]` in this environment due bind restrictions. Without a clear CI strategy, regressions can still slip through.
### Fix steps
1. Add CI jobs/environment where bind-based tests run by default.
2. Split pure transport logic from socket bind/accept so more behavior can be tested without real sockets.
3. Keep ignored tests minimal and document how/when they run.
## 5) Decode/history shared state still relies on global mutexes
History/state paths still use shared mutex-backed globals/contexts with `expect` on lock poisoning in hot paths. This is workable but fragile for long-running async services.
### Fix steps
1. Replace panic-on-poison lock usage with resilient handling.
2. Consider bounded channel or lock-free append/read model for decode history.
3. Define explicit ownership/lifetime for history data instead of implicit shared mutation.
This document specifies the requirements for running N simultaneous rig backends in one `trx-server` process and the protocol/config changes required to support them.
---
## Progress
> **For AI agents:** This section is the single source of truth for implementation status.
> Each task has a unique ID (e.g. `MR-01`), a status badge, a description, the files it touches, and any blocking dependencies.
>
> Status legend: `[ ]` not started · `[~]` in progress · `[x]` done · `[!]` blocked
### Foundational (parallel)
| ID | Status | Task | Files | Needs |
|----|--------|------|-------|-------|
| MR-01 | `[x]` | Add `rig_id: Option<String>` to `ClientEnvelope`; add `rig_id: Option<String>` to `ClientResponse`; add `ClientCommand::GetRigs`; add `GetRigsResponseBody` + `RigEntry`; add sentinel arm in `mapping.rs` | `src/trx-protocol/src/types.rs`, `mapping.rs`, `lib.rs` | — |
**trx-rs** is a modular transceiver (radio) control stack written in Rust. It provides a backend service for controlling amateur radio transceivers via CAT (Computer-Aided Transceiver) protocols, with multiple frontend interfaces for access and monitoring.
This document specifies the requirements for a SoapySDR-based RX-only backend (`trx-backend-soapysdr`) and the associated IQ-to-audio pipeline changes in `trx-server`.
---
## Progress
> **For AI agents:** This section is the single source of truth for implementation status.
> Each task has a unique ID (e.g. `SDR-01`), a status badge, a description, the files it touches, and any blocking dependencies.
> Pick any task whose status is `[ ]` and whose `Needs` list is fully `[x]`. Update status to `[~]` while working, `[x]` when merged. Record notes under the task if you hit non-obvious issues.
>
> Status legend: `[ ]` not started · `[~]` in progress · `[x]` done · `[!]` blocked
### Foundational (must land first)
| ID | Status | Task | Touches |
|----|--------|------|---------|
| SDR-01 | `[x]` | Add `AudioSource` trait to `trx-core`; add `as_audio_source()` default on `RigCat` | `src/trx-core/src/rig/mod.rs` |
| SDR-07 | `[x]` | Implement `SoapySdrRig` in `lib.rs`: `RigCat` (RX methods + `not_supported` stubs for TX), `AudioSource`, gain control (manual/auto with fallback), primary channel freq/mode tracking | `…/src/lib.rs` | SDR-03, SDR-06 |
### Server integration
| ID | Status | Task | Touches | Needs |
|----|--------|------|---------|-------|
| SDR-08 | `[x]` | `main.rs`: after building rig, if `as_audio_source()` is `Some` skip cpal, subscribe each decoder and the Opus encoder to the appropriate channel PCM senders; validate `stream_opus` count ≤ 1 | `src/trx-server/src/main.rs` | SDR-03, SDR-07 |
| SDR-09 | `[x]` | Add `trx-backend-soapysdr` to workspace `Cargo.toml`; update `CONFIGURATION.md` with new `[sdr]` / `[[sdr.channels]]` options | `Cargo.toml`, `CONFIGURATION.md` | SDR-04 |
### Validation & tests
| ID | Status | Task | Touches | Needs |
|----|--------|------|---------|-------|
| SDR-10 | `[x]` | Unit tests for `demod.rs`: known-input tone through each demodulator, check output frequency correct | `…/src/demod.rs` | SDR-05 |
| SDR-11 | `[x]` | Unit tests for config validation: channel IF out-of-range, dual `stream_opus`, TX enabled with SDR backend, AGC fallback warning | `src/trx-server/src/config.rs` | SDR-03 |
---
## Goals
- Receive-only backend that uses any SoapySDR-compatible device (RTL-SDR, Airspy, HackRF, SDRplay, etc.) as the rig
- Full IQ pipeline: raw IQ samples → demodulated PCM → existing decoders (FT8, WSPR, APRS, CW) with zero decoder-side changes
- Wideband capture: one SDR IQ stream feeds multiple simultaneous virtual receivers, each independently tuned and demodulated
- Configurable per-channel filters and demodulation modes
- Demodulated audio streamed to clients as Opus over the existing TCP audio channel
---
## Non-Goals
- Transmit (TX/PTT) of any kind
- Replacing or deprecating the existing cpal-based audio path (it stays for transceiver backends)
---
## 1. Device Abstraction
### 1.1 `RigAccess` extension
A new access type `sdr` is added alongside `serial` and `tcp`:
```toml
[rig.access]
type="sdr"
args="driver=rtlsdr"# SoapySDR device args string
```
The `args` value is passed verbatim to `SoapySDR::Device::new(args)`. It follows SoapySDR's key=value comma-separated convention (e.g., `driver=airspy`, `driver=rtlsdr,serial=00000001`).
### 1.2 `AudioSource` trait
A new trait is added to `trx-core` (`src/trx-core/src/rig/mod.rs`):
```rust
pubtraitAudioSource: Send+Sync{
/// Subscribe to demodulated PCM audio from the primary channel.
`SoapySdrRig` overrides `as_audio_source()` to return `Some(self)`. When the server detects this, it skips spawning the cpal capture thread entirely.
### 1.3 TX-only `RigCat` methods
The following methods return `RigError::not_supported(...)` on the SDR backend:
-`set_ptt()`
-`power_on()` / `power_off()`
-`get_tx_power()`
-`get_tx_limit()` / `set_tx_limit()`
-`toggle_vfo()` (not applicable; channels are defined statically in config)
-`lock()` / `unlock()`
The following methods are fully supported:
-`get_status()` → returns primary channel's current `(freq, mode, None)`
-`set_freq()` → re-tunes the SDR center frequency (keeping `center_offset_hz` invariant) and updates all channel mixer offsets
-`set_mode()` → changes the primary channel's demodulator
-`get_signal_strength()` → returns instantaneous RSSI for the primary channel (dBFS mapped to 0–255 S-unit range)
---
## 2. IQ Pipeline Architecture
### 2.1 Center frequency offset
SDR hardware has a DC offset spur at exactly 0 Hz in the IQ spectrum. To keep the primary channel off DC, the SDR is tuned to a frequency offset from the desired dial frequency:
```
sdr_center_freq = dial_freq - center_offset_hz
```
With `center_offset_hz = 200000` and dial freq 14.074 MHz, the SDR tunes to 13.874 MHz. The 14.074 MHz signal appears at +200 kHz in the IQ spectrum and is mixed down to baseband in software.
`center_offset_hz` is a global SDR parameter (not per-channel). A reasonable default is `100000` (100 kHz).
### 2.2 Wideband channel model
One SoapySDR RX stream produces IQ samples at `sdr.sample_rate` (e.g. 1.92 MHz). This stream is shared among all configured channels. Each channel defines an independent virtual receiver:
This is the frequency at which the channel's signal appears in the captured IQ bandwidth, and is what the channel's mixer shifts to baseband.
**Constraint:**`|channel_if_hz|` must be less than `sdr_sample_rate / 2` for every channel. The server validates this at startup and rejects invalid configs.
### 2.3 Per-channel DSP chain
Each channel runs the following stages independently on the shared IQ stream:
```
IQ input (complex f32, sdr_sample_rate)
1. Mixer: multiply by exp(-j·2π·channel_if_hz·n/sdr_sample_rate)
→ complex f32 centred at 0 Hz
2. FIR LPF: cutoff = audio_bandwidth_hz / 2, order configurable
3. Decimator: sdr_sample_rate / audio_sample_rate (must be integer; resampler used otherwise)
4. Demodulator (mode-dependent, see §3)
5. Output: real f32 at audio_sample_rate
6. Frame accumulator: chunks of frame_duration_ms
7. broadcast::Sender<Vec<f32>> → decoders + optional Opus encoder
```
Channels run concurrently in separate tasks, all reading from the same raw IQ broadcast channel.
### 2.4 IQ broadcast channel
The SoapySDR read loop runs in a dedicated OS thread (matching the existing cpal thread model). It reads IQ sample blocks from the device and publishes them on:
| `FM` | Quadrature: `arg(s[n] · conj(s[n-1]))`, i.e. instantaneous frequency |
| `WFM` | Same as FM, wider pre-demod filter (`wfm_bandwidth_hz`) |
| `CW` | Narrow BPF centred at `cw_center_hz` (audio domain), then envelope |
| `DIG`/`PKT` | Same as USB (pass audio through for downstream digital decoders) |
| `CWR` | Same as CW (reversed sideband, uses same audio envelope) |
For SSB modes (USB/LSB), after mixing to baseband the channel's `audio_bandwidth_hz` defines the one-sided cutoff of the post-demod LPF.
---
## 4. Gain Control
Gain is configured globally under `[sdr.gain]`.
```toml
[sdr.gain]
mode="auto"# "auto" (AGC via SoapySDR) or "manual"
value=30.0# dB; ignored when mode = "auto"
```
- **`auto`**: calls `device.set_gain_mode(SOAPY_SDR_RX, 0, true)` to enable hardware AGC if the device supports it. If the device does not support hardware AGC, falls back to `manual` with a warning.
- **`manual`**: calls `device.set_gain(SOAPY_SDR_RX, 0, value)` with the specified total gain in dB.
Advanced per-element gain is out of scope for this phase (no `lna`/`vga`/`if` sub-keys initially).
### 4.1 Virtual Squelch
Software squelch is configured globally under `[sdr.squelch]` and currently applies to the primary channel's demodulated audio path except WFM.
```toml
[sdr.squelch]
enabled=false
threshold_db=-65.0# dBFS open threshold
hysteresis_db=3.0# dB close hysteresis
tail_ms=180# hold time after signal drops
```
---
## 5. Filter Configuration
Filters are configured per-channel. The following are settable:
```toml
[[sdr.channels]]
audio_bandwidth_hz=3000# One-sided bandwidth of post-demod BPF (Hz)
# For FM: deviation hint for deemphasis
fir_taps=64# FIR filter tap count (default 64); higher = sharper roll-off
cw_center_hz=700# CW tone centre in audio domain (default 700 Hz)
wfm_bandwidth_hz=75000# Pre-demod bandwidth for WFM only (default 75 kHz)
```
`fir_taps` controls the same FIR used in stage 2 of the DSP chain (§2.3). It applies uniformly to both the pre-demod decimation filter and the post-demod audio BPF in this phase.
---
## 6. Channel Configuration and Decoder Binding
Channels are declared as a TOML array. The first channel in the list is the **primary channel** and is the one exposed via `RigCat` (`set_freq`/`set_mode` affect it; `get_status` reads from it).
```toml
[[sdr.channels]]
id="primary"# Identifier, used in logs
offset_hz=0# Offset from dial frequency (Hz)
mode="auto"# "auto" = follows RigCat set_mode; or fixed RigMode string
audio_bandwidth_hz=3000
fir_taps=64
decoders=["ft8","cw"]# Which decoders receive this channel's PCM
stream_opus=true# Encode and stream via TCP audio channel
[[sdr.channels]]
id="wspr-14"
offset_hz=21600# 14.0956 MHz when dial = 14.074 MHz
mode="USB"# Fixed mode, ignores RigCat set_mode
audio_bandwidth_hz=3000
decoders=["wspr"]
stream_opus=false
[[sdr.channels]]
id="aprs"
offset_hz=-673600# e.g. 144.390 MHz when dial = 145.0635 MHz
mode="FM"
audio_bandwidth_hz=8000
decoders=["aprs"]
stream_opus=false
```
**`mode = "auto"`** means the channel's demodulator tracks whatever `set_mode()` last set on the backend. Only the primary channel should use `"auto"` in typical use.
**`decoders`** maps to the decoder task IDs: `"ft8"`, `"wspr"`, `"aprs"`, `"cw"`. Each named decoder subscribes to the PCM broadcast channel of the listed channel(s). A decoder can only be bound to one channel (first binding wins if duplicated).
---
## 7. Opus Streaming
Channels with `stream_opus = true` have their demodulated PCM Opus-encoded and streamed over the server's existing TCP audio port (default 4531).
For this phase, only **one channel** may have `stream_opus = true` (validation error otherwise). This channel's Opus stream replaces what cpal would have produced — the TCP audio protocol and client-side handling are unchanged.
The Opus encoder uses the `[audio]` config for `frame_duration_ms`, `bitrate_bps`, and `sample_rate`. The SDR pipeline must output PCM at the same `sample_rate` as `[audio]`; a mismatch is a startup validation error.
---
## 8. Full Configuration Example
```toml
[rig]
model="soapysdr"
initial_freq_hz=14074000
initial_mode="USB"
[rig.access]
type="sdr"
args="driver=rtlsdr"
[sdr]
sample_rate=1920000# IQ capture rate (Hz) — must be supported by device
bandwidth=1500000# Hardware IF filter (Hz)
center_offset_hz=200000# SDR tunes this many Hz below dial frequency
[sdr.gain]
mode="auto"
value=30.0# Effective only when mode = "manual"
[sdr.squelch]
enabled=false
threshold_db=-65.0
hysteresis_db=3.0
tail_ms=180
[[sdr.channels]]
id="primary"
offset_hz=0
mode="auto"
audio_bandwidth_hz=3000
fir_taps=64
decoders=["ft8","cw"]
stream_opus=true
[[sdr.channels]]
id="wspr"
offset_hz=21600
mode="USB"
audio_bandwidth_hz=3000
decoders=["wspr"]
stream_opus=false
[audio]
enabled=true
listen="127.0.0.1"
port=4531
rx_enabled=true
tx_enabled=false# No TX on SDR backend
sample_rate=48000
channels=1
frame_duration_ms=20
bitrate_bps=24000
```
---
## 9. Code Changes Map
| File | Change |
|------|--------|
| `Cargo.toml` (workspace) | Add `src/trx-server/trx-backend/trx-backend-soapysdr` member |
| `src/trx-server/src/config.rs` | Add `SdrConfig`, `SdrGainConfig`, `SdrChannelConfig`; parse `type = "sdr"` in `AccessConfig`; add `sdr: SdrConfig` to `ServerConfig` |
| `src/trx-server/src/main.rs` | After building rig: if `as_audio_source()` is `Some`, skip cpal, use `AudioSource::subscribe_pcm()` for each decoder and for the Opus encoder; validate at most one `stream_opus = true` channel |
| `src/trx-server/src/audio.rs` | Expose `spawn_audio_capture` and `run_*_decoder` without assuming cpal as the sole source; no functional change needed — decoders already take `broadcast::Receiver<Vec<f32>>` |
| `src/trx-server/trx-backend/trx-backend-soapysdr/Cargo.toml` | New crate |
| `src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs` | `SoapySdrRig`: implements `RigCat` + `AudioSource`; spawns IQ read thread and channel DSP tasks |
Custom slash commands (skills) available in this repository.
Invoke with `/skill-name [args]` inside Claude Code.
---
## `/frontend-design` — HTTP frontend work
**When to use:** Any time you need to add or modify UI in the HTTP web frontend — new control rows, panels, visual polish, capability-gated elements, or JS behaviour wired to REST endpoints.
**What it loads:** Design system context (palette, layout primitives, patterns), key file paths, and coding conventions so Claude writes code that matches the existing UI without needing to re-read the style guide each time.
**File:**`.claude/commands/frontend-design.md`
**Example invocations**
```
/frontend-design Add a CW keyer speed row (wpm slider) that POSTs to /set_cw_wpm, shown only when capabilities.tx is true.
/frontend-design Polish the filters panel — align the bandwidth label with the FIR taps label and add a unit suffix to the slider readout.
/frontend-design Add a waterfall canvas below the signal meter that renders frequency vs. time from a new SSE stream.
```
---
## Adding new skills
Place a Markdown file in `.claude/commands/<skill-name>.md`.
Use `$ARGUMENTS` as the placeholder for user-supplied text.
Skills in `.claude/commands/` are project-scoped and not committed if `.claude/` is in `.gitignore`.
To make a skill part of the repo (shared with all contributors), add it to `aidocs/` as documentation and track the command file in version control by removing `.claude/` from `.gitignore` or adding a specific exception.
---
## Global skills (available in all projects)
| Skill | When to use |
|-------|------------|
| `frontend-design` | Also installed globally; project version takes precedence here |
| `keybindings-help` | Customise Claude Code keyboard shortcuts |
This document specifies how `trx-client`'s HTTP frontend adapts its controls to the capabilities of the connected rig backend. Devices such as SDR receivers expose filter controls but not TX controls; traditional transceivers are the reverse.
---
## Progress
> **For AI agents:** This section is the single source of truth for implementation status.
> Each task has a unique ID (e.g. `UC-01`), a status badge, a description, the files it touches, and any blocking dependencies.
>
> Status legend: `[ ]` not started · `[~]` in progress · `[x]` done · `[!]` blocked
| UC-02 | `[x]` | Update capability declarations in all backends to set new flags | `src/trx-server/trx-backend/trx-backend-ft817/src/lib.rs`, `trx-backend-ft450d/src/lib.rs`, `trx-backend-soapysdr/src/lib.rs` | UC-01 |
| UC-03 | `[x]` | Add `RigFilterState` struct; add `filter: Option<RigFilterState>` to `RigSnapshot`; populate from SDR rig state | `src/trx-core/src/rig/state.rs`, `src/trx-server/trx-backend/trx-backend-soapysdr/src/lib.rs` | — |
| UC-04 | `[x]` | Add `SetBandwidth`, `SetFirTaps` to `ClientCommand`; add mapping arms; update `rig_task.rs` to dispatch them | `src/trx-protocol/src/types.rs`, `mapping.rs`, `src/trx-server/src/rig_task.rs` | UC-03 |
| UC-06 | `[x]` | Read `state.info.capabilities` on each SSE event; toggle visibility of TX controls, meter rows, VFO button, lock button | `assets/web/app.js` | UC-01, UC-02 |
| UC-07 | `[x]` | Add "Filters" control panel (bandwidth, FIR taps, CW tone Hz); show only when `capabilities.filter_controls` | `assets/web/index.html`, `assets/web/app.js` | UC-05, UC-06 |
| UC-09 | `[x]` | Protocol round-trip test: `RigSnapshot` serialises `filter` field when `Some`, omits it when `None` | `src/trx-protocol/src/codec.rs` or `types.rs` | UC-03 |
---
## Goals
- All UI control groups are **shown/hidden purely from `RigCapabilities`** flags received in the initial `GET /status` and each SSE `status` event — no hard-coding per model name
Visibility is applied in a single `applyCapabilities(caps)` function called from the SSE `status` handler, using `element.classList.toggle('hidden', !condition)`.
### Filter panel layout (UC-07)
```
┌─ Filters ──────────────────────────────────┐
│ Bandwidth [──────●──────] 3000 Hz │
│ FIR taps [32 ▾] (32 / 64 / 128 / 256) │
│ CW tone [──●───────────] 700 Hz │
└────────────────────────────────────────────┘
```
Each control dispatches to its REST endpoint on `change`/`input` (debounced 200 ms). The panel is hidden by default (`class="hidden"`) and revealed when `capabilities.filter_controls` is set.
---
## Implementation Notes
-`applyCapabilities()` must run **before** the first paint (call it synchronously on the initial `/status` response, not only on SSE events) to avoid layout flash of unsupported controls.
-`hidden` CSS class should set `display: none` and `aria-hidden: true`.
- The existing `set_cw_tone` endpoint and CW decoder panel remain in the CW decoder tab — they are decoder settings, not filter settings. The Filters panel bandwidth/taps apply to the DSP chain; CW tone moves to both places or is de-duplicated in a follow-up.
- If a future backend supports TX but not `tx_limit`, only the TX Limit row is hidden; PTT remains.
The recorder captures the demodulated audio stream alongside associated metadata (FFT data, decoded signals, rig state) into a structured session on disk, with full playback and seeking support from within the application.
### Requirements
| ID | Description |
|----|-------------|
| REQ-REC-001 | When the user starts recording, the system shall record the currently demodulated audio stream. |
| REQ-REC-002 | When recording audio, the system shall store the recording in OPUS format. |
| REQ-REC-003 | While recording audio, the system shall automatically detect whether the recording should be stored in mono or stereo and select the appropriate format. |
| REQ-REC-004 | While recording is active, the system shall simultaneously record FFT data and all currently visible decoded elements, including APRS and FT8. |
| REQ-REC-005 | While recording metadata, the system shall store FFT data and decoded signal data in a structured data file format. |
| REQ-PLAY-001 | Where recorded sessions exist, the system shall allow playback of recordings from within the same application. |
| REQ-PLAY-002 | During playback, the system shall allow the user to seek to any position in the recording. |
| REQ-SYNC-001 | The system shall maintain time synchronization between the audio recording and the associated data file with at least one-second resolution. |
| REQ-REC-006 | While recording is active, the system shall allow the current cursor position to be stored. |
---
### Architecture
#### New Crate: `trx-recorder`
A new crate `src/trx-server/trx-recorder/` handles all record and playback logic. It is a library crate consumed by `trx-server`.
```
src/trx-server/
trx-recorder/
src/
lib.rs # Public API: RecorderHandle, start_recorder_task()
- **Format**: Opus, using the `opus` crate (already a workspace dependency via `trx-backend-soapysdr`). Seek index (`index.bin`) provides byte → time mapping.
- **Channel count**: determined at session open from `AudioConfig.channels`. If `channels == 1` → mono; if `channels == 2` → stereo. Written into the file header and recorded in the session's first data event.
- **Sample rate**: preserved from `AudioConfig.sample_rate` (default 48 000 Hz).
#### Data File (REQ-REC-004, REQ-REC-005)
`data.jsonl` — one JSON object per line, each with a required `offset_ms` field giving the millisecond offset from session start (satisfies REQ-SYNC-001 at ≥1 s resolution):
- **http-json frontend**: same commands as JSON messages
---
### Playback Engine (REQ-PLAY-001, REQ-PLAY-002)
`PlaybackEngine` opens a session directory and:
1. Reads `audio.opus` and decodes PCM frames in real time.
2. Publishes decoded PCM frames onto a `broadcast::Sender<Vec<f32>>` — the **same channel type** as the live `pcm_tx`, so existing decoder tasks and audio-streaming clients receive playback data transparently.
3. Replays `data.jsonl` events on their original `offset_ms` timestamps, injecting them into the `DecodedMessage` broadcast so the HTTP frontend displays historic decodes during playback.
4. For seek: binary-searches `index.bin` to find the audio byte offset, then replays data events from the same point.
The playback state machine has two modes, switched by a new `RigState.playback` field:
```rust
pubenumPlaybackState{
Live,
Playing{session: String,offset_ms: u64},
Paused{session: String,offset_ms: u64},
}
```
While `PlaybackState` is not `Live`, the server suppresses live hardware polling and PCM capture to avoid mixing live and playback audio.
---
### Time Synchronisation (REQ-SYNC-001)
All timestamps use a single `session_epoch: std::time::Instant` captured at `StartRecording`. Every PCM frame, every data event, and every seek-index entry is stamped as `(Instant::now() - session_epoch).as_millis() as u64`. This gives sub-millisecond internal precision; the requirement of ≥1 s resolution is met by orders of magnitude.
Wall-clock UTC is embedded only in `session_start` (`wall_clock_utc`) and in the session directory name, providing absolute time anchoring without depending on system clock monotonicity for sync.
1. Implement `PlaybackEngine`; Opus decode + PCM broadcast.
2. Add `PlaybackState` to `RigState`; suppress live capture during playback.
3. Implement seek via `index.bin` binary search.
4. Replay `data.jsonl` events; feed into `DecodedMessage` broadcast.
5. Expose start/stop/seek endpoints in `trx-frontend-http`.
---
### Dependencies to Add
| Crate | Use | Already present? |
|-------|-----|-----------------|
| `opus` | Opus encode/decode | Yes (via trx-backend-soapysdr) |
| `serde_json` | data.jsonl serialisation | Yes |
| `tokio::fs` | async file I/O | Yes |
---
### Open Questions
1.**Playback isolation**: Should playback be exclusive (block all CAT commands) or concurrent? Initial design blocks CAT polling; revisit if users need to change frequency during playback.
2.**Session listing API**: The HTTP frontend needs an endpoint to enumerate sessions (`GET /api/recorder/sessions`). Schema TBD in Phase 4.
3.**Storage limits**: `max_session_duration_s` auto-splits sessions; a `max_total_size_gb` housekeeping option may be needed but is out of scope for initial phases.
---
## Configurator Helper
An interactive CLI tool that guides users through creating configuration files
for trx-rs. Instead of editing TOML by hand, the user answers prompts and the
Two halves of one feature ([#54](https://git.haxx.space/sjg/trx-rs/issues/54)): a station
logbook in a panel of its own, and an operator layout that puts a transceiver's controls
around it.
Nothing in the application records a QSO today. The map's "QSO summary" cards describe
contacts *between other stations*, reconstructed from decoded traffic; bookmarks are
frequencies, not contacts. Neither is the operator's own log.
### Requirements
| ID | Description |
|----|-------------|
| REQ-LOG-001 | The system shall record QSOs the operator makes, each holding at minimum callsign, date, time, band, frequency, mode and both signal reports. |
| REQ-LOG-002 | When starting a log entry, the system shall pre-fill exactly six fields: frequency, mode, rig name, time, callsign and locator. |
| REQ-LOG-003 | The system shall leave every other field of a log entry empty for the operator to fill. |
| REQ-LOG-004 | The system shall allow a logged QSO to be edited and deleted. |
| REQ-LOG-005 | The system shall survive a crash without losing a QSO that was recorded before it. |
| REQ-LOG-006 | The system shall list, search and filter the log by callsign, band, mode and date range. |
| REQ-LOG-007 | Where a decoded station is on screen, the system shall offer to start a log entry from it, pre-filled, without logging it unattended. |
| REQ-LOG-008 | The system shall show whether a callsign has been worked before, and on which bands. |
| REQ-FMT-001 | The system shall export the log as an ADIF 3.1.x `.adi` file. |
| REQ-FMT-002 | The system shall import ADIF `.adi` files produced by other logging software, preserving fields it does not itself use. |
| REQ-FMT-003 | When importing, the system shall identify QSOs already held and shall not duplicate them. |
| REQ-FMT-004 | The system shall export a filtered selection of the log as a Cabrillo 3.0 file for contest submission. |
| REQ-LOG-009 | The system shall stamp QSO times from the server's clock in UTC, and shall tell the operator when the browser's clock disagrees with it by more than one second. |
| REQ-LAY-001 | The system shall present the logbook in a panel of its own, reachable whatever layout is selected. |
| REQ-LAY-002 | The system shall offer a "Ham radio" operator layout presenting the transceiver controls and that panel together. |
| REQ-LAY-003 | Where the selected rig cannot transmit, the system shall not offer the ham layout. |
### A decode is not a QSO
The decoders are receive-only: FT8, CW, APRS and the rest report what was *heard*. A heard
callsign is the beginning of a log entry, not a contact, and the logbook must never write one
by itself — REQ-LOG-007 says offer and pre-fill, never auto-log. Digital QSOs made in
WSJT-X or similar arrive the way every other logger takes them: through ADIF import.
### What is pre-filled, and what is not
Six fields, and no more (REQ-LOG-002, REQ-LOG-003):
| Field | From | ADIF |
|-------|------|------|
| Frequency | the selected rig's dial | `FREQ`, with `BAND` derived from it |
| Mode | the selected rig | `MODE`, and `SUBMODE` where the mode implies one |
| Rig name | the rig's display name | `MY_RIG` |
| Time | the server's clock, UTC, when the entry opens | `QSO_DATE`, `TIME_ON` |
| Callsign | the decode row or map station the entry was started from, else empty | `CALL` |
| Locator | that station's grid, where the decode carried one, else empty | `GRIDSQUARE` |
Signal reports, power, name, QTH and the rest stay empty. A report in particular is the
operator's to give: an FT8 SNR is not what was sent, and pre-filling one would put a number in
the log that nobody exchanged.
The station's own callsign and locator are not pre-filled per entry either — they are station
identity, taken from configuration when the QSO is written (`STATION_CALLSIGN`, `OPERATOR`,
`MY_GRIDSQUARE`), and shown once at the top of the panel rather than typed into every row.
| IA-1 | **"Settings" is a catch-all bucket.** Scheduler and Background Decode are operational features, not user preferences. Bandplan and History are true settings/maintenance. Mixing them under one tab creates cognitive overhead. | Medium |
| IA-2 | **Scheduler sub-tab is overloaded.** It packs three conceptually distinct features (Grayline, Time Span, Satellite) into one scrollable panel via conditional `display:none` sections. Users must scroll past irrelevant sections. | Medium |
| IA-3 | **History clearing is buried.** Users wanting to clear FT8 decode history must navigate to Settings → History — an unintuitive path. This action is more naturally accessible from the Digital Modes tab itself. | Low |
| IA-4 | **No search or categorization.** With 4 sub-tabs today, it's manageable, but the flat sub-tab bar won't scale if more settings (e.g., audio, display theme, reporting/PSKReporter, notifications) are added. | Low |
### 2.2 Interaction Design
| # | Issue | Severity |
|---|---|---|
| IX-1 | **Save button visibility is inconsistent.** Save/Reset buttons use `style="display:none"` and are shown dynamically, but there is no dirty-state indicator. Users can change fields without realizing they haven't saved. | High |
| IX-2 | **No confirmation on destructive actions.** The 10 history-clear buttons and "Reset to Disabled" (scheduler) fire immediately on click. No confirmation dialog protects against accidental data loss. | High |
| IX-3 | **Entry table details collapsed by default.** The Time Span entry table is inside a `<details>` element — users must expand it to see, edit, or delete entries. This adds an unnecessary click when entries already exist. | Medium |
| IX-4 | **Satellite form uses a modal overlay; Time Span form is inline.** Inconsistent form presentation within the same sub-tab. Both should use the same pattern. | Medium |
| IX-5 | **Toast notification positioning.** The `.sch-toast` uses `position: fixed; bottom: 1.5rem` which can overlap with the main tab bar or mobile navigation. It also disappears without user control. | Low |
| IX-6 | **Bookmark filter in Background Decode has no "select all / deselect all" shortcut.** With many bookmarks, toggling them one by one is tedious. | Medium |
### 2.3 Visual & Layout
| # | Issue | Severity |
|---|---|---|
| VL-1 | **Scheduler has no visual state summary.** The "No activity yet." card doesn't show whether the scheduler is enabled or what mode it's in at a glance. Users must inspect the mode dropdown. | Medium |
| VL-2 | **History clear buttons are uniform.** All 10 buttons look identical (`sch-write sch-reset-btn`). No indication of which decoders have data to clear. Buttons for empty histories are noise. | Low |
| VL-3 | **Mobile responsiveness is partial.** The `@media (max-width: 600px)` rules handle `.sch-row` and `.bgd-*` layout, but the Time Span table (`.sch-ts-table` with 8 columns) overflows on narrow screens. | Medium |
| VL-4 | **Sub-tab bar can overflow.** It uses `overflow-x: auto` but gives no visual scroll indicator. On small screens, the "History" tab can be hidden off-screen with no affordance. | Low |
### 2.4 Accessibility
| # | Issue | Severity |
|---|---|---|
| A-1 | **Missing `aria-label` on several controls.** The scheduler mode select has one, but the grayline lat/lon inputs, interleave fields, and satellite fields lack accessible names beyond their visible label text (which is acceptable for `<label>` wrapping `<input>`, but form titles like "Add Entry" aren't linked to the form via `aria-labelledby`). | Low |
| A-2 | **No keyboard navigation for the 24h timeline SVG.** Timeline segments are clickable (`cursor: pointer`) but not focusable or keyboard-operable. | Medium |
| A-3 | **Color-only state indication in Background Decode status.** States like "active" (green), "waiting" (yellow), "error" (red) rely solely on color. Not sufficient for color-blind users. | Medium |
| A-4 | **Toast notifications aren't announced to screen readers.** The `.sch-toast` div lacks `role="alert"` or `aria-live` attributes. | Low |
---
## 3. Improvement Plan
### Phase 1 — Quick Wins (Low effort, high impact)
```mermaid
gantt
title Phase 1 — Quick Wins
dateFormat X
axisFormat %s
section Interaction
IX-2 Add confirmation dialogs :a1, 0, 2
IX-6 Select all / deselect all :a2, 0, 1
IX-1 Dirty-state indicator on Save :a3, 0, 2
section Accessibility
A-4 Add aria-live to toasts :a4, 0, 1
A-3 Add text labels to state dots :a5, 0, 1
```
**IX-2: Add confirmation dialogs for destructive actions**
- Wrap history-clear and "Reset to Disabled" clicks in a `confirm()` dialog (or a lightweight inline confirmation pattern).
- Estimated: ~30 lines of JS.
**IX-6: Add select all / deselect all for Background Decode bookmarks**
- Add two small buttons above the bookmark checklist: "Select All" / "Deselect All".
- Alternatively, a single toggle that reads the current state.
**IX-1: Dirty-state indicator**
- Track whether any field has changed since last load/save.
- Show a visual cue (e.g., dot on the Save button, or change button color) when there are unsaved changes.
- Optionally warn on tab navigation away from dirty settings.
**A-4: Toast accessibility**
- Add `role="alert"` and `aria-live="polite"` to `.sch-toast` elements.
**A-3: State badge text labels**
- The `.bgd-status-state` already shows uppercase text — ensure the SVG dot badges (`.bgd-state-dot`) are supplemented with visible text, not just color.
| **Maintenance** | History clearing, with per-decoder item counts |
Additionally, add contextual "Clear history" links directly in the Digital Modes tab (next to each decoder's output panel), so users don't need to navigate to Settings at all for this common action.
**IX-3: Auto-expand entry table when entries exist**
- If `scheduler-ts-tbody` has rows, set the `<details>` element's `open` attribute on render.
**IX-4: Unify form presentation**
- Convert the satellite modal (`#sch-sat-form-wrap` with `position: fixed`) to an inline form matching the Time Span entry form pattern, or vice versa. Inline is preferred for consistency and mobile friendliness.
**VL-1: Scheduler status summary card**
- Enhance the "Now Playing" card to always show: current mode, active entry (if any), next scheduled event, and satellite pass countdown (if enabled).
- Use a compact two-line format when idle: "Mode: Grayline | Next: Dawn transition in 2h 14m".
**VL-3: Responsive table for Time Span entries**
- Replace the 8-column table with a card-based layout on narrow screens (`@media (max-width: 600px)`), or use horizontal scroll with a scroll shadow indicator.
**A-2: Keyboard-accessible timeline**
- Add `tabindex="0"` and `role="button"` to timeline segments.
| **Read** | `/status`, `/events`, `/audio`, `/decode`, `/spectrum`, `/bookmarks` | Rx or Control role |
| **Control** | `/set_freq`, `/set_mode`, `/set_ptt`, `/toggle_power`, all other POST | Control role only |
### 7.2 Session Management
- Sessions are 128-bit random hex tokens stored in HttpOnly cookies
- Configurable TTL (default from TOML config)
- Expired sessions auto-pruned on access
- Constant-time passphrase comparison to mitigate timing attacks
### 7.3 TX Access Control
An additional `tx_access_control_enabled` flag can restrict transmit-related actions even
for Control-role users, providing an extra safety layer.
---
## 8. Virtual Channels (SDR)
Virtual channels allow SDR users to monitor multiple frequencies simultaneously:
- Channels appear in a picker row below the VFO controls
- CRUD API: `POST /channels/{remote}` to create, `DELETE` to remove, `PUT` to update freq/mode/BW
- Subscribe/unsubscribe audio per channel
- Background decode channels (hidden, no audio stream back)
- Channels auto-destroyed when out-of-bandwidth after center-frequency retune
- Channel-list changes broadcast to SSE clients via `event: channels`
---
## 9. Design Principles (Inferred)
1.**Server-rendered SPA**: All HTML/CSS/JS embedded in the binary -- zero external build tooling, no CDN dependency for core functionality (CDN used only for fonts and Leaflet maps).
2.**Progressive disclosure**: Advanced controls (WFM, SAM, SDR settings, spectrum controls) are hidden by default and revealed based on the active mode and backend type.
3.**Keyboard-first, touch-aware**: Spectrum supports full keyboard navigation alongside mouse and touch gestures. Mobile breakpoints enlarge hit targets and adapt layout.
4.**Real-time by default**: SSE + WebSocket provide sub-second state updates without polling from the browser. 5-second ping heartbeat detects stale connections.
5.**Per-tab isolation**: Each browser tab gets its own SSE session UUID and can independently select a rig, preventing cross-tab interference.
6.**Configuration over code**: UI behaviour knobs (gain visibility, map zoom, history retention, spectrum margins) are exposed as TOML config rather than requiring code changes.
7.**Graceful degradation**: The UI handles server disconnection gracefully with visible banners, and only the affected rig shows as disconnected in multi-rig setups.
8.**Defensive security defaults**: Auth disabled by default for ease of setup, but when enabled, provides role-based access, rate limiting, constant-time comparison, and HttpOnly cookies.
`--listen` on the server overrides the bind address of both the control
listener and every rig's audio listener.
---
## Multiple Rigs in the Web UI
A client connected to several rigs decodes all of them at once, whichever one
is on screen. The rig picker in the header decides which rig the UI is about,
and each page answers that differently:
| Page | Shows |
|------|-------|
| Radio | The selected rig: its spectrum, its audio, and the mini decode views over the waterfall. |
| Digital Modes | The selected rig: every decoder panel, its counts and its status line. |
| Map | The whole station — every rig's positions, with the map's own rig filter to narrow it. HF APRS is a source of its own there, filtered apart from VHF APRS. |
| Statistics | The whole station, including the per-rig comparison. |
Switching rigs repaints the radio and digital modes pages for the rig now
selected. Nothing is lost by switching: the traffic other rigs heard is still
held, and switching back brings it up again. The one exception is the CW pane,
which is a single running stream of copied text rather than a list of frames,
so it starts empty on the rig you switch to.
Each browser tab keeps its own selection, so two tabs can watch two rigs.
---
## Tune Links
Every page of the web UI carries what the radio is doing in its address, so the
URL in the address bar is always a link someone else can open:
| `enabled` | bool | false | — | Turn the noise blanker on or off. |
| `threshold` | float | 10.0 | 1 – 100 | Multiplier applied to the running RMS. A sample whose magnitude exceeds this multiple is replaced. Lower values blank more aggressively; higher values only catch strong impulses. |
The noise blanker is off by default.
### Choosing a threshold
The threshold controls how aggressively the blanker suppresses impulses.
A value of **N** means: blank any sample whose magnitude exceeds **N times**
the running average signal level.
| Threshold | Behavior | Use case |
|-----------|----------|----------|
| 3 – 5 | Very aggressive — blanks frequently | Dense impulse noise (motors, power lines, LED drivers nearby) |
| 8 – 12 | Moderate — catches clear spikes without touching normal signals | Typical HF conditions with occasional ignition or switching noise |
| 15 – 25 | Conservative — only blanks strong impulses well above the noise floor | Light interference, or when you want minimal artifacts on weak signals |
| 30 – 100 | Very light — rarely triggers | Faint, infrequent clicks; mostly a safety net |
**Start at 10** (the default) and adjust while listening:
- If impulse noise is still audible, lower the threshold.
- If weak signals sound choppy or distorted, raise it — the blanker may be
mistaking signal peaks for noise.
- On bands with steady atmospheric noise (e.g. 160 m / 80 m), a threshold of
**5 – 8** usually works well.
- On quieter VHF/UHF bands where the noise floor is low, values of **15 – 25**
avoid false triggers from strong signals.
### Web UI
When the server reports noise-blanker support, two controls appear in the
**SDR Settings** row of the web interface:
- **Noise Blanker** checkbox — enables or disables the blanker in real time.
- **NB Threshold** number input (1–100) with a **Set** button — adjusts the
detection threshold. Press Enter or click Set to apply.
Both controls stay hidden until the server sends filter state containing NB
fields, so they only appear when connected to an SDR backend.
### HTTP API
```
POST /set_sdr_noise_blanker?enabled=true&threshold=10
```
| Parameter | Type | Required | Description |
|-------------|--------|----------|-------------|
| `enabled` | bool | yes | `true` or `false` |
| `threshold` | float | yes | Value between 1 and 100 |
### How it works
The blanker runs on every IQ block (4096 samples) *before* the mixer stage in
the DSP pipeline:
1. For each sample, compute magnitude² (`re² + im²`).
2. Compare against `threshold² × mean_sq` (the exponentially-smoothed running
mean of magnitude²).
3. If the sample exceeds the threshold, replace it with the previous clean
sample.
4. Otherwise, update the running mean with smoothing factor α = 1/128 and store
the sample as the last clean value.
Because the blanker operates on raw IQ before frequency translation, it removes
impulse noise across the entire captured bandwidth regardless of the tuned
SPDX-FileCopyrightText: 2026 Stan Grams <sjg@haxx.space>
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Frontend architecture
The HTTP frontend is a strict TypeScript application built with esbuild. Rust
embeds deterministic JavaScript output from `assets/web/generated`; Cargo does
not run Node.js or contact the network.
## Runtime graph
`frontend/src/bootstrap.ts` is the only first-party script referenced by the
HTML document. Its imports establish startup order for WebGL support, shared UI
services, decoder dispatch, the local Leaflet AIS adapter, and the application
coordinator. Leaflet and the Opus decoder remain isolated vendored scripts.
Feature entries under `frontend/src/plugins` are ESM bundles. The typed plugin
loader imports them by feature group and keeps expensive map, scheduling, and
decoder behavior lazy. Shared code is emitted as content-hashed chunks. The
decode-history worker is an independent entry compiled against Web Worker
globals.
Dependencies point from application and feature code toward `core` and `api`.
`api/generated.ts` contains Rust wire formats; `api/client.ts` and focused
parsers validate untrusted HTTP, SSE, WebSocket, and worker data before it is
used as typed application state.
## Browser host contract
Separate lazy bundles cannot share module instances with the stable application
entry, so they use three intentional host namespaces:
| Global | Purpose | Mutation policy |
| --- | --- | --- |
| `window.trx` | Application state, core services, and feature registrations | The root is frozen; lazy features may register only their documented `modules.*` service. |
| `window.trxPluginRuntime` | Typed decoder registration and message dispatch | Runtime object is installed once; plugins register lifecycle handlers through its API. |
| `window.trxUi` | Notifications, confirmations, tab accessibility, and control presentation | Installed once by `ui-core.ts`; consumers call methods but do not replace them. |
`frontend/src/plugins/host.ts` declares the typed view of `window.trx.state`
and `window.trx.core` that feature bundles consume. Feature entries import it
instead of re-deriving the contract, so a service that moves out of the
application entry is added in one place. Application state that a feature needs
belongs in `trx.state`, and shared behavior belongs in `trx.core`; a feature
that has to intercept application behavior registers a `modules.*` method the
application calls, as the virtual-channel entry does for tuning, mode,
bandwidth, and frequency display.
The WebGL adapter exposes `createTrxWebGlRenderer`, `trxParseCssColor`,
`trxHslToRgba`, and `trxClearCssColorCache` for the application bundle. Leaflet
adds `L.TrxAisTrackSymbol` and `L.trxAisTrackSymbol` to the vendored Leaflet
namespace.
The following transitional properties are explicitly part of the lazy-feature
host contract and are declared in `app.ts`: `lastSpectrumData`, `lastFreqHz`,
`style.css` is a single 5,318-line file covering every tab, theme, responsive
breakpoint, map overlay, decoder UI, scheduler, recorder, and settings panel.
Browsers must parse the entire stylesheet before first paint even though most
users only interact with 1-2 tabs at a time.
**Recommendations:**
- Split into logical partitions: `base.css` (variables, reset, layout), `tabs/*.css` (per-tab styles), `themes/*.css`. The server can concatenate and compress at build time.
- At minimum, move the theme colour blocks (lines 3770-5318, ~1,550 lines / 29% of the file) into a separate `themes.css` loaded asynchronously after initial paint, since the default theme is already in `:root`.
- Consider using `@layer` (CSS Cascade Layers) to manage specificity between base, component, and theme styles, eliminating the need for `!important` (currently 21 occurrences).
### 1.2 `backdrop-filter` overuse (P1)
There are 26 `backdrop-filter` declarations (13 pairs with `-webkit-` prefix).
`backdrop-filter: blur()` is one of the most expensive CSS properties -- it
forces the browser to composite, rasterize, and blur everything behind the
element on every frame.
Affected areas: tab bar, controls tray, frequency overlay, modals, connection
banner, bottom nav, neon-disco theme overlay.
**Recommendations:**
- Remove `backdrop-filter` from elements that are always opaque or rarely overlap dynamic content (e.g. bottom tab bar over static background).
- For the spectrum/waterfall overlay controls, use a solid semi-transparent `background` instead of blur -- the visual difference is negligible on a dark spectrogram.
- Where blur is desired (modals), use `will-change: backdrop-filter` and keep blur radius low (4-6px instead of 12-18px). Larger radii are proportionally more expensive.
- Gate expensive blur behind a `@media (prefers-reduced-motion: no-preference)` query or a `[data-effects="full"]` attribute so low-end devices can opt out.
### 1.3 `color-mix()` usage (P2)
184 occurrences of `color-mix(in srgb, ...)` throughout the stylesheet. While
`color-mix` is well-supported in modern browsers, each call is resolved at
computed-value time. Repeated identical mixes (e.g. button hover states
repeated across themes) add unnecessary style recalculation cost.
**Recommendations:**
- Pre-compute frequently used mixes as CSS custom properties in the theme blocks (e.g. `--btn-hover-bg`, `--btn-active-bg`).
- This reduces computed-value work and also makes the palette more explicit and maintainable.
### 1.4 Theme system duplication (P2)
Each of the 10 colour themes repeats ~28 variable declarations for both dark
and light mode (560 variable declarations total). The theme blocks span lines
3770-5318 (29% of the entire stylesheet).
**Recommendations:**
- Move themes to a separate file loaded after first paint (the default `:root` theme is always available).
- Consider generating theme CSS from a data source (JSON/TOML) at build time to reduce manual duplication.
- Use `color-scheme` and `light-dark()` (CSS Color Level 5) to collapse the dark/light pairs where values differ only in lightness.
### 1.5 Transitions on non-essential properties (P3)
25 `transition` declarations, several targeting `background`, `border-color`,
and `box-shadow` simultaneously. Multi-property transitions on buttons and
inputs cause style recalculation on hover/focus for every such element.
**Recommendations:**
- Prefer transitioning only `opacity` and `transform` (GPU-composited).
- For colour changes, use `transition: background-color 100ms` rather than the shorthand `background` which also transitions `background-image` and other sub-properties.
- Add `will-change: transform` only to elements that are actively animating (currently only 2 occurrences, which is good).
### 1.6 Missing `contain` declarations (P2)
Tab content panels, decode history tables, map containers, and spectrum
canvases do not use CSS `contain` or `content-visibility`. When a large decode
history table updates, the browser recalculates layout for the entire page.
**Recommendations:**
- Add `contain: content` to inactive tab panels (`[data-tab]:not(.active)`).
- Add `content-visibility: auto` with `contain-intrinsic-size` to off-screen panels (decode history, map, statistics). This lets the browser skip rendering for hidden content entirely.
- Add `contain: strict` to the spectrum/waterfall canvas containers since their size is fixed and they don't affect sibling layout.
---
## 2. JavaScript observations
### 2.1 Monolithic `app.js` (P1)
The main application script is 11,928 lines (428 KB uncompressed). It is loaded
synchronously in the HTML `<head>` (via embedded asset), blocking first paint
until fully parsed and executed. The 15 plugin scripts add another 7,360 lines.
**Recommendations:**
- Mark the script tag `defer` or move it to end of `<body>` so HTML parsing completes before script execution.
- Split `app.js` into logical modules: `core.js` (SSE, auth, render loop), `spectrum.js`, `map.js`, `decoder.js`, `recorder.js`, `settings.js`. Load non-critical modules lazily when the user navigates to the corresponding tab.
- Use ES modules (`type="module"`) for clean dependency management and tree-shaking potential.
### 2.2 DOM query overhead (P2)
The codebase contains ~359 `querySelector`/`getElementById` calls, many of
which execute on every SSE event (inside `render()`). DOM lookups are not free,
especially `querySelector` with compound selectors.
**Recommendations:**
- Cache DOM references at initialization time (many already are, but the render path still re-queries elements like `document.getElementById("tab-main")`).
- Move repeated lookups (e.g. line 3575 `document.getElementById("tab-main")` inside `es.onmessage`) to module-level constants.
### 2.3 `innerHTML` usage (P2)
33 `innerHTML` assignments in `app.js` and 72 across plugin scripts. Each
`innerHTML` write forces the browser to:
1. Serialize the old DOM subtree for GC
2. Parse the HTML string
3. Build and insert a new DOM subtree
This is both a performance concern (layout thrashing) and a security concern
(XSS if any user-controlled data is interpolated without escaping).
**Recommendations:**
- Replace `innerHTML` with DOM APIs (`createElement`/`appendChild`) or `DocumentFragment` for bulk updates (only 4 `createDocumentFragment` uses currently).
- For large lists (decode history, bookmarks, recorder file lists), use a virtualised list pattern that only renders visible rows.
- Where `innerHTML` is used to clear a container, prefer `replaceChildren()` (clears children without HTML parsing).
### 2.4 SSE render path efficiency (P2)
Every SSE state event triggers `render(update)` which is a ~300-line function
touching dozens of DOM elements. The function does not diff -- it
unconditionally sets properties even when values have not changed.
The string-equality guard (`if (evt.data === lastRendered) return`) is a good
optimisation for identical payloads, but when any field changes (e.g. S-meter
value), the entire render function runs.
**Recommendations:**
- Implement field-level diffing: compare individual fields against previous values and only update DOM elements whose backing data changed.
- Group updates by tab: if the user is on the "Map" tab, skip render work for "Main" tab elements (meters, frequency display, controls).
- Use `scheduleUiFrameJob()` (already exists at line 3685) more aggressively to batch DOM writes into animation frames.
### 2.5 Spectrum/waterfall rendering (P2)
The WebGL renderer (`webgl-renderer.js`) is well-implemented with proper
shader programs and batched draws. However:
- The CSS colour parsing (`parseCssColor`) uses a DOM probe element (appended to
body) and `getComputedStyle` as a fallback, which triggers layout.
- The colour cache is a simple `Map` with no eviction policy.
**Recommendations:**
- Parse theme colours once when the theme changes, not on every frame.
- Invalidate the `cssColorCache` on theme switch events.
### 2.6 Plugin script loading (P3)
All 15 plugin scripts are loaded eagerly in `index.html` regardless of which
decoders are active. Plugins like `ais.js`, `vdes.js`, `sat.js`,
`sat-scheduler.js`, and `hf-aprs.js` are only relevant for specific use cases.
**Recommendations:**
- Load plugin scripts on demand when the corresponding decoder or feature is activated.
- Use dynamic `import()` if migrated to ES modules, or lazy `<script>` injection.
### 2.7 Web Worker utilisation (P3)
Only one Web Worker exists (`decode-history-worker.js`, 176 lines) for CBOR
decode-history parsing. All other heavy work (SSE parsing, DOM updates, spectrum
rendering, map marker management) runs on the main thread.
**Recommendations:**
- Move SSE JSON parsing to a shared worker so the main thread only receives pre-parsed objects.
- Offload spectrum FFT data processing / colour mapping to a worker, posting the resulting `ImageData` to the main thread for canvas rendering.
---
## 3. HTML observations
### 3.1 CDN dependencies (P2)
The page loads one external resource at startup:
-`@fontsource/dseg14-classic/400.css` from `cdn.jsdelivr.net`
~~`leaflet@1.9.4` was previously loaded from `unpkg.com` but is now bundled
as a vendored asset (`/vendor/leaflet.{js,css}` + marker/layer images),
eliminating the CDN dependency.~~
The font uses `rel="preload" as="style"` with an `onload` trick to make it
non-blocking, which is good. However:
- If CDN is unreachable (offline/firewalled deployments common in ham radio),
the font never loads and the frequency display falls back to the system font.
**Recommendations:**
- Self-host the DSEG14 font as an embedded asset (it is small, ~30 KB woff2). This eliminates the CDN dependency entirely and ensures the frequency display always renders correctly.
### 3.2 Inline SVG icons (P3)
Tab bar icons are inline SVGs in the HTML (lines 35-63). Each icon is ~150-250
bytes of markup. This is acceptable for a small number of icons and avoids
extra HTTP requests, but the tab bar HTML is dense and hard to maintain.
**Recommendation:**
- Consider an SVG sprite sheet or moving icons to a small icon font to improve readability without extra requests.
### 3.3 HTML size (P2)
`index.html` is 1,564 lines (96 KB uncompressed). All tab content panels are
present in the initial HTML regardless of which tab is active.
**Recommendations:**
- Use `<template>` elements for tab panels that are not initially visible. Clone and insert them when the tab is first activated. This reduces initial DOM node count and speeds up first paint.
- The server already does template substitution (`{ver}` placeholders). Extend this to strip unused tab content for deployments that don't use certain features.
---
## 4. Responsive design observations
### 4.1 Breakpoints (P3)
Six responsive breakpoints are defined:
-`>1100px`: side bookmark panels
-`<1099px`: hide side bookmarks
-`<900px`: full-width card
-`<760px`: mobile layout (touch targets, stacked controls)
-`<640px`: bottom tab bar, mobile nav
-`<520px`: compact mobile
-`(hover: none) and (pointer: coarse)`: touch-specific
This is a well-structured responsive system. Minor improvements:
- Use `min-width` mobile-first instead of `max-width` desktop-first to reduce CSS specificity conflicts.
- Consider `container queries` for components like the controls tray and decode history table, so they respond to their container size rather than the viewport.
### 4.2 Touch target sizing (P3)
Mobile buttons get `min-height: 2.8rem` at `<760px`. The
`(hover: none) and (pointer: coarse)` media query adds additional touch
accommodations. This meets the 44px minimum recommended by WCAG.
---
## 5. Accessibility observations
### 5.1 `aria-live` regions (P1)
The connection-lost banner and power hint text update dynamically but were
flagged in the Settings-Menu-UX-Analysis as missing `aria-live` on toast
notifications. Ensuring all dynamic status text has `aria-live="polite"` or
`aria-live="assertive"` (for errors) is critical for screen reader users.
### 5.2 Keyboard navigation (P2)
The tab bar uses `<button>` elements (good, natively focusable). However, the
spectrum canvas, jog wheel, and map are mouse/touch-only without keyboard
equivalents. The Settings-Menu-UX-Analysis noted the timeline SVG is not
keyboard-operable.
### 5.3 Colour contrast (P2)
`--text-muted` values (`#91a3bd` on `#0f172a` for dark, `#4a5568` on `#ffffff`
for light) should be verified against WCAG AA (4.5:1 for normal text). The
dark theme muted text calculates to approximately 4.8:1 (passes), but some
theme variants (e.g. Neon Disco) may not meet contrast requirements.
---
## 6. Server-side delivery observations
### 6.1 Asset compression (already good)
Static assets are pre-compressed with `gzip` at `Compression::best()` level
and served with ETag headers. Conditional `304 Not Modified` responses avoid
re-transferring unchanged assets.
### 6.2 Missing `Cache-Control` headers (P2)
While ETags are present, the analysis did not find explicit `Cache-Control`
headers on static assets. Adding `Cache-Control: public, max-age=31536000,
immutable` for versioned assets (with cache-busting query strings) would
eliminate conditional requests entirely for repeat visits.
### 6.3 Consider Brotli compression (P3)
Brotli (`br`) typically achieves 15-25% better compression than gzip for text
assets. For a 428 KB `app.js`, this could save ~60-100 KB of transfer. Actix
supports Brotli via the `Compress` middleware.
---
## 7. Priority summary
```mermaid
quadrantChart
title Impact vs Effort
x-axis Low Effort --> High Effort
y-axis Low Impact --> High Impact
quadrant-1 Do next
quadrant-2 Plan carefully
quadrant-3 Low priority
quadrant-4 Quick wins
"backdrop-filter reduction": [0.25, 0.80]
"Cache-Control headers": [0.15, 0.55]
"CSS contain/content-visibility": [0.30, 0.70]
"Cache DOM refs in render": [0.20, 0.50]
"Theme CSS split": [0.35, 0.45]
"Self-host DSEG14 font": [0.20, 0.40]
"Field-level render diffing": [0.60, 0.75]
"Split app.js into modules": [0.80, 0.70]
"Lazy plugin loading": [0.50, 0.40]
"innerHTML to DOM APIs": [0.65, 0.55]
"Brotli compression": [0.30, 0.25]
"Template-based tab panels": [0.70, 0.60]
```
### Quick wins (low effort, high impact)
1.~~Reduce `backdrop-filter` usage (13 blur instances)~~**DONE** -- replaced with solid backgrounds, blur preserved for modals only, `prefers-reduced-motion` gate added
2.~~Add `contain: content` / `content-visibility: auto` to inactive tabs~~**DONE** -- containment added for inactive tabs, spectrum/waterfall containers, map, statistics
3.~~Add `Cache-Control` headers to static assets~~**DONE** -- upgraded to `public, max-age=31536000, immutable`
4.~~Cache remaining DOM references in the render path~~**DONE** -- `tabMainEl` and other hot-path refs cached at module level
### Next phase (moderate effort)
5.~~Split theme CSS into a separate lazy-loaded file~~**DONE** -- theme blocks extracted to `/themes.css`, lazy-loaded via `<link rel="preload">`
6.~~Self-host DSEG14 font~~**DONE** -- `@font-face` with `font-display: swap` added to `style.css`, CDN preconnect/preload removed from HTML
7.~~Pre-compute `color-mix` results as CSS variables~~**DONE** -- common mixes pre-computed as `--btn-hover-bg`, `--btn-active-bg`, etc.
8.~~Field-level diffing in the SSE render function~~**DONE** -- `prevRenderData` tracks freq/mode/ptt/meter, active-tab-aware skip logic added
9.~~Replace `innerHTML` with DOM APIs in hot paths~~**DONE** -- 15+ `innerHTML = ""` replaced with `replaceChildren()`
### Longer-term
10.~~Split `app.js` into modules with lazy loading~~**DONE** -- `map-core.js` (3,480 lines, map/stats/geo) and `screenshot.js` (260 lines) extracted as IIFE modules communicating via `window.trx` namespace; lazy-loaded on tab activation and on-demand respectively; `app.js` reduced from 11,967 to 8,420 lines (30% reduction)
11.~~Lazy-load plugin scripts and Leaflet on demand~~**DONE** -- plugin scripts loaded on tab activation, core plugins loaded immediately
12.~~Use `<template>` elements for deferred tab content~~**DONE** -- map, statistics, about tabs wrapped in `<template>`, cloned on first activation
13.~~Migrate to Brotli compression~~**DONE** -- Brotli added alongside gzip, preferred when `Accept-Encoding: br` present
14. Move SSE parsing and spectrum processing to Web Workers -- **DEFERRED** (requires SharedWorker + MessagePort plumbing, tracked separately)
C implementation of a lightweight FT8/FT4 decoder and encoder, mostly intended for experimental use on microcontrollers.
The intent of this library is to allow FT8/FT4 encoding and decoding in standalone environments (i.e. without a PC or RPi), e.g. automated beacons or SDR transceivers. It's also my learning process, optimization problem and source of fun.
The encoding process is relatively light on resources, and an Arduino should be perfectly capable of running this code.
The decoder is designed with memory and computing efficiency in mind, in order to be usable with a fast enough microcontroller. It is shown to be working on STM32F7 boards fast enough for real work, but the embedded application itself is beyond this repository. This repository provides an example decoder which can decode a 15-second WAV file on a desktop machine or SBC. The decoder needs to access the whole 15-second window in spectral magnitude representation (the window can be also shorter, and messages can have varying starting time within the window). The example FT8 decoder can work with slightly less than 200 KB of RAM.
# Current state
Currently the basic message set for establishing QSOs, as well as telemetry and free-text message modes are supported:
* CQ {call} {grid}, e.g. CQ CA0LL GG77
* CQ {xy} {call} {grid}, e.g. CQ JA CA0LL GG77
* {call} {call} {report}, e.g. CA0LL OT7ER R-07
* {call} {call} 73/RRR/RR73, e.g. OT7ER CA0LL 73
* Free-text messages (up to 13 characters from a limited alphabet) (decoding only, untested)
* Telemetry data (71 bits as 18 hex symbols)
Encoding and decoding works for both FT8 and FT4. For encoding and decoding, there is a console application provided for each, which serves mostly as test code, and could be a starting point for your potential application on an MCU. The console apps should run perfectly well on a RPi or a PC/Mac. I don't provide a concrete example for a particular MCU hardware here, since it would be very specific.
The code is not yet really a library, rather a collection of routines and example code.
# Future ideas
Incremental decoding (processing during the 15 second window) is something that I would like to explore, but haven't started.
These features are low on my priority list:
* Contest modes
* Compound callsigns with country prefixes and special callsigns
# What to do with it
You can generate 15-second WAV files with your own messages as a proof of concept or for testing purposes. They can either be played back or opened directly from WSJT-X. To do that, run ```make```. Then run ```gen_ft8``` (run it without parameters to check what parameters are supported). Currently messages are modulated at 1000-1050 Hz.
You can decode 15-second (or shorter) WAV files with ```decode_ft8```. This is only an example application and does not support live processing/recording. For that you could use third party code (PortAudio, for example).
# References and credits
Thanks goes out to:
* my contributors who have provided me with various improvements which have often been beyond my skill set.
* Robert Morris, AB1HL, whose Python code (https://github.com/rtmrtmrtmrtm/weakmon) inspired this and helped to test various parts of the code.
* Mark Borgerding for his FFT implementation (https://github.com/mborgerding/kissfft). I have included a portion of his code.
* WSJT-X authors, who developed a very interesting and novel communications protocol
The details of FT4 and FT8 procotols and decoding/encoding are described here: https://physics.princeton.edu/pulsar/k1jt/FT4_FT8_QEX.pdf
The public part of FT4/FT8 implementation is included in this repository under ft4_ft8_public.
Of course in moments of frustration I have looked up the original WSJT-X code, which is mostly written in Fortran (http://physics.princeton.edu/pulsar/K1JT/wsjtx.html). However, this library contains my own original DSP routines and a different implementation of the decoder which is suitable for resource-constrained embedded environments.
/// Convert integer index to ASCII character according to one of character tables
charcharn(intc,ft8_char_table_etable);
/// Look up the index of an ASCII character in one of character tables
intnchar(charc,ft8_char_table_etable);
#ifdef __cplusplus
}
#endif
#endif // _INCLUDE_TEXT_H_
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.