Commit Graph

331 Commits

Author SHA1 Message Date
sjg 189d27bac8 [feat](trx-rs): add configurable noise blanker for SoapySDR backend
IQ-domain impulse noise blanker using exponential-smoothing RMS tracker. Samples exceeding threshold × running RMS are replaced with the last clean sample. Configurable via [sdr.noise_blanker] in TOML config and runtime via POST /set_sdr_noise_blanker API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-22 13:54:17 +01:00
sjg bc596dd9a1 [feat](trx-server): make VFO priming optional via behavior.vfo_prime config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-21 21:45:59 +01:00
sjg 7898c3d61a [fix](trx-backend-soapysdr): enumerate available devices on match failure for diagnostics
When Device::new(args) fails, enumerate all available SoapySDR devices and
include them in the error message. Also hint that args are case-sensitive.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-21 15:32:46 +01:00
sjg d74f77537a [fix](trx-backend-soapysdr): remove silent fallback to first device when args are specified
When specific SoapySDR device args are provided (e.g. with a serial number),
fail hard instead of silently falling back to Device::new("") which opens
the first available device. This caused multi-device setups to bind both
rig instances to the same physical device.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-21 15:23:06 +01:00
sjg 43eb9a2292 [fix](trx-server): scope decoder resets by mode
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-18 23:31:55 +01:00
sjg ab30270a63 [chore](trx-rs): update SPDX copyright headers
Normalize tracked SPDX headers to the 2026 Stan Grams identity.

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-18 22:39:06 +01:00
sjg 5dabe32106 [refactor](trx-rs): remove trx-ft8 C FFI crate, use trx-ftx directly
Delete trx-ft8 (C wrapper around ft8_lib + ft2_ldpc) and update
trx-server to depend on trx-ftx (pure Rust) directly. Removes
~2,900 lines of C code and all unsafe FFI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-18 22:22:20 +01:00
sjg 7cf829ef52 [feat](trx-rs): display APRS-IS connection status on About page
Thread aprs_is_status through RigState, RigSnapshot, and the protocol
layer following the same pattern as pskreporter_status. Show the
connection target and callsign when enabled, or "Disabled" otherwise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 23:20:43 +01:00
sjg 9019acee0e [feat](trx-backend-soapysdr): enable hardware AGC by default if available
Query the device for AGC support via has_gain_mode and enable it
automatically at startup. Devices without hardware AGC fall back to
manual gain as before.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 22:57:20 +01:00
sjg b533d704a1 [style](trx-rs): reformat codebase
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 22:36:11 +01:00
sjg a823e66816 [fix](trx-server): drop in-flight decodes on main retune
Invalidate main-decoder windows whenever the rig's main frequency
changes, including direct SetFreq, scheduler-driven retunes, and
external CAT retunes observed during polling.

The decoder loops now resubscribe their PCM receivers on reset and drop
results that finish after the reset sequence advances, preventing false
decodes from stale pre-retune audio.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-17 22:28:31 +01:00
sjg bff3be38be [feat](trx-backend-soapysdr): implement ARM NEON optimizations
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>
2026-03-17 01:22:56 +01:00
sjg 7a144375f2 [fix](trx-backend-soapysdr): enforce 9 kHz minimum bandwidth for AMC mode
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>
2026-03-17 00:19:35 +01:00
sjg e337870d53 [fix](trx-backend-soapysdr): add AMC to supported_modes capabilities list
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>
2026-03-17 00:15:47 +01:00
sjg 016d4ddfd7 [fix](trx-rs): resolve all clippy warnings
- 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>
2026-03-17 00:15:03 +01:00
sjg 6eb2d5341f [fix](trx-backend-ft450d): mark AMC unsupported in FT-450D encoder
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>
2026-03-17 00:10:48 +01:00
sjg bdc581637c [fix](trx-backend-ft817): mark AMC unsupported in FT-817 encoder
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>
2026-03-17 00:10:45 +01:00
sjg 223726368d [feat](trx-server): handle AMC mode in rig_task and server config
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>
2026-03-17 00:10:34 +01:00
sjg 3564a48ced [feat](trx-backend-soapysdr): implement C-QUAM stereo demodulator
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>
2026-03-17 00:10:31 +01:00
sjg c2d6530b83 [refactor](trx-server): remove fir_taps from SDR config and rig_task
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>
2026-03-16 23:17:32 +01:00
sjg 37a5600d99 [refactor](trx-backend-soapysdr): auto-calculate FIR taps from bandwidth
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>
2026-03-16 23:17:23 +01:00
sjg 5aa3d61ce0 [feat](trx-backend-soapysdr): seed LNA gain from hardware at init
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>
2026-03-15 19:29:46 +01:00
sjg 2ebd085aec [feat](trx-server): dispatch SetSdrLnaGain in rig_task
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>
2026-03-15 19:25:09 +01:00
sjg 2a298add7d [feat](trx-backend-soapysdr): implement LNA gain element control
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>
2026-03-15 19:25:04 +01:00
sjg 3d99cac03b [feat](trx-backend-soapysdr): add hardware AGC toggle and SDR settings UI row
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>
2026-03-15 09:44:46 +01:00
sjg 262e78e72b [fix](trx-backend-soapysdr): fix AM demodulation DC blocker, AGC, and add IQ AGC
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>
2026-03-15 09:28:59 +01:00
sjg ab3bf9120e [fix](trx-backend-soapysdr): fix SSB demodulation with asymmetric complex BPF
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>
2026-03-15 08:35:33 +01:00
sjg 7c08e12870 [fix](trx-rs): fix FT4 7.5s slot rollover timing
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>
2026-03-14 21:50:26 +01:00
sjg d3abce9ab0 [fix](trx-rs): align FT2 async window geometry 2026-03-14 20:44:25 +01:00
sjg 2570fe739e [fix](trx-rs): improve FT2 decoder acquisition
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>
2026-03-14 20:20:18 +01:00
sjg ad6aa6aab4 [feat](trx-rs): add FT2 decoder protocol support
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>
2026-03-14 19:49:32 +01:00
sjg d547c45a9c [feat](trx-rs): add FT2 decoder support (wired to FT4)
Mirrors the FT4 implementation across the full stack. The trx-ft8
crate wires Ft8Decoder::new_ft2() to FTX_PROTOCOL_FT4 as a
placeholder pending a dedicated FT2 implementation.

Changes:
- trx-ft8: Ft8Decoder::new_ft2() delegates to with_protocol(Ft4)
- trx-core: DecodedMessage::Ft2, AUDIO_MSG_FT2_DECODE (0x15),
  ft2_decode_enabled/ft2_decode_reset_seq state, SetFt2DecodeEnabled/
  ResetFt2Decoder commands, protocol mapping
- trx-server: DecoderHistories::ft2, run_ft2_decoder (7.5s slots),
  run_background_ft2_decoder, history push/replay, decoder task spawn
- trx-frontend-http: ft2_history in FrontendRuntimeContext,
  toggle/clear endpoints, /ft2.js route, bookmark/scheduler/background
  decode support, DecodeHistoryPayload ft2 field
- web: ft2.js plugin (3.75s period timer), FT2 subtab in index.html,
  FT2 map source (distinct hue), app.js dispatch, decode-history-worker
  HISTORY_GROUP_KEYS, bookmarks read/write/apply

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 19:34:41 +01:00
sjg 8eae376c56 [feat](trx-rs): add FT4 decoder support
Reuse the existing ft8_lib C library (FTX_PROTOCOL_FT4) and FT8
decoder infrastructure to add FT4 decoding across the full stack.

Changes:
- trx-ft8: add protocol param to ft8_decoder_create; add Ft8Decoder::new_ft4()
- trx-core: DecodedMessage::Ft4 variant, AUDIO_MSG_FT4_DECODE (0x14),
  ft4_decode_enabled/ft4_decode_reset_seq state, SetFt4DecodeEnabled/
  ResetFt4Decoder commands, protocol mapping
- trx-server: DecoderHistories::ft4, run_ft4_decoder (7.5s slots via
  now*2/15), run_background_ft4_decoder, history push/replay, decoder
  task spawn
- trx-frontend-http: ft4_history in FrontendRuntimeContext,
  toggle/clear endpoints, /ft4.js route, bookmark/scheduler/background
  decode support, DecodeHistoryPayload ft4 field
- web: ft4.js plugin (7.5s period timer, reuses FT8 CSS/map infra),
  FT4 subtab in index.html, app.js dispatch (onServerFt4/Batch,
  restoreFt4History), decode-history-worker HISTORY_GROUP_KEYS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
2026-03-14 18:50:08 +01:00
sjg c1e8ce42d2 [fix](trx-frontend-http): start APRS on scheduler PKT channels
Carry bookmark decoder kinds through visible scheduler virtual channels so PKT/APRS scheduler entries start their decode workers instead of acting as audio-only channels.

Verification: cargo test -p trx-frontend-http vchan
Verification: cargo test -p trx-client (fails in existing config::tests::test_default_config)

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 13:54:44 +01:00
sjg 40e99d3ea9 [feat](trx-reporting): add APRS-IS position beaconing
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>
2026-03-14 09:49:29 +01:00
sjg 872e086763 [feat](trx-reporting): add callsign field to AprsFiConfig
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>
2026-03-14 09:35:32 +01:00
sjg fed8948a61 [feat](trx-rs): extract trx-reporting crate for uplink tasks
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>
2026-03-14 09:28:47 +01:00
sjg ecb058a9d2 [fix](trx-server): comply with APRS-IS IGate spec
- 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>
2026-03-14 09:24:03 +01:00
sjg 1dec2c2663 [chore](trx-rs): centralize version in workspace.package
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>
2026-03-14 09:13:06 +01:00
sjg f7d3a935ee [fix](trx-server): use "trx-rs" as PSKReporter software identifier
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-14 09:04:46 +01:00
sjg 587ece9903 [fix](trx-backend-soapysdr): fix audio distortion from incorrect force_mono_pcm
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>
2026-03-14 08:53:08 +01:00
sjg 945912adc1 [fix](trx-server): guard APRS-IS uplink against history replays
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>
2026-03-14 08:45:24 +01:00
sjg 5ed3e29d90 [fix](trx-server): fix PSKReporter IPFIX packet format and batching
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>
2026-03-14 08:33:36 +01:00
sjg 60697bb26a [perf](trx-server): reduce headless scheduler audio cpu
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 14:18:03 +01:00
sjg 7fde0f7730 [chore](trx-server): remove temporary hang tracing
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:50:41 +01:00
sjg 47f57aa10d [fix](trx-server): drop state watch borrows before decode
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:37:42 +01:00
sjg bc153b84ee [debug](trx-server): trace rig task progress
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:32:38 +01:00
sjg 12f1d81af8 [debug](trx-server): trace backend control flow
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 13:19:21 +01:00
sjg 1be02ec8ad [debug](trx-server): trace listener and rig task activity
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 11:57:34 +01:00
sjg e5bf7346c8 [debug](trx-backend-soapysdr): trace SDR retune application
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-13 11:31:27 +01:00