Commit Graph

397 Commits

Author SHA1 Message Date
sjg 316d624c95 [feat](trx-ftx): gate FT2 support behind ft2 feature flag, disabled by default
FT2 decoder implementation, protocol constants, server decoder tasks,
background decode, and registry entry are now conditional on the ft2
feature. Lightweight types (enum variants, commands, state fields) remain
unconditional to avoid cascading cfg noise in macros and serde.

Enable with: cargo build -p trx-server --features ft2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-04-01 21:49:17 +02:00
Claude 1fe0b75e20 [fix](trx-rs): fix LRPT pass detection status never updating during active decoding
The #sat-status element was stuck on "Waiting for satellite pass" because:

1. The client audio handler (audio_client.rs) did not include AUDIO_MSG_LRPT_IMAGE
   in its message type match, so LRPT image messages from the server were silently
   dropped and never reached the frontend.

2. No progress was reported during active LRPT decoding — the only status update
   happened when a complete image was finalized, which could take the entire pass.

3. The sat-status text was never updated when the decoder was enabled/disabled,
   leaving it permanently at the HTML default text.

Changes:
- Add DecodedMessage::LrptProgress variant for live MCU progress reporting
- Send LRPT progress updates from the decoder task when new MCUs are decoded
- Add AUDIO_MSG_LRPT_IMAGE and AUDIO_MSG_LRPT_PROGRESS to client audio handler
- Update sat-status text when decoder state changes (enabled/disabled)
- Handle lrpt_progress messages in the frontend to show "Receiving — N MCU rows"

https://claude.ai/code/session_017knbD7dr6hJGAWR6pModL7
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-31 16:21:12 +02:00
Claude f2469fee12 [fix](trx-wxsat): fix LRPT decoder always-enabled bug and implement image decoding
The LRPT decoder task was missing mode checks, processing audio in any
rig mode once toggled on. Now it only activates in FM mode, matching
the decoder registry descriptor. Also corrects active_modes from
DIG/USB to FM.

Replaces the MCU stub (which treated compressed JPEG data as raw
pixels) with proper Huffman + inverse-DCT decompression, CCSDS packet
reassembly from MPDUs, and CCSDS derandomization in the CADU framer.

https://claude.ai/code/session_0135LuveBndEiZHkU2jsKPB9
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-31 14:58:15 +02:00
sjg f2048c583c [feat](trx-rs): add client-side Opus audio recorder
Record Opus audio streams to OGG files on the client. Includes manual start/stop via HTTP API, scheduler-driven auto-recording per schedule entry, and a header REC button in the web UI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-30 23:37:09 +02:00
sjg a45e113bde [docs](trx-server): fix meter tick comment to reflect actual rate
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-30 21:41:32 +02:00
sjg 3c7cad5b85 [feat](trx-server): add fast S-meter tick for SDR backends
Signal strength now refreshes every 100ms for SDR backends using the cached DSP value, keeping the S-meter responsive at half the spectrum redraw rate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-30 21:25:18 +02:00
sjg e6dbfd1edb [feat](trx-protocol): add centralised decoder registry
Add DECODER_REGISTRY in trx-protocol::decoders as the single source of
truth for all decoder metadata (activation mode, supported rig modes,
background-decode capability). Replace duplicated resolver functions in
background_decode.rs and sse.rs with shared resolve_bookmark_decoders().
Add GET /decoders endpoint to expose the registry to the frontend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-30 20:47:12 +02:00
Claude 5c43bac42b [feat](trx-rs): remap decoder modes — remove DIG/PKT from SDR, wire decoders to standard modes
For SDR backends, DIG and PKT are removed from supported_modes and
replaced by USB and FM respectively. CAT backends (FT-817, FT-450D)
retain DIG/PKT as before.

Decoder mode allowances updated:
- APRS: FM | PKT (was PKT only)
- HF-APRS: USB | DIG (was DIG only)
- AIS: AIS | FM | PKT (was AIS only)
- VDES: VDES | FM (was VDES only)
- FT8/FT4/FT2/WSPR: USB | DIG (unchanged)
- CW: CW | CWR (unchanged)
- LRPT: FM (unchanged, mode-independent)

Frontend status text, bookmark decoder toggles, background-decode
fallbacks, and scheduler wiring updated to match.

https://claude.ai/code/session_01DCAaMH8RF5FNB2gRtVu4pY
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-30 18:55:04 +02:00
sjg 083caf412f [style](trx-rs): apply rustfmt formatting
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-29 22:59:43 +02:00
Claude c041ac83f3 [refactor](trx-rs): resolve all improvement areas (P1–P3)
P1 — High:
- Merge duplicate APRS/HF-APRS decoder tasks into parameterised inner fn
- Merge duplicate FT8/FT4 decoder tasks into shared ftx inner fn
- Add multi-rig state isolation and command routing tests (listener.rs)
- Add background decode evaluate_bookmark unit tests

P2 — Medium:
- Fix decode-log silent flush errors and rotation failure fallback
- Split api.rs (2,831 LOC) into 7 logical modules (decoder, rig, vchan,
  sse, bookmarks, assets, mod)
- Extract background decode decision cascade into pure evaluate_bookmark()
  function with ChannelAction enum
- Relax actix-web pin from =4.4.1 to 4.4
- Replace VDES magic numbers with named constants

P3 — Low:
- Add doc comments to AisDecoder, VdesDecoder, RdsDecoder
- Add debug_assert on turbo decoder interleaver/deinterleaver lengths
- Add tracing info_span! to all 10 decoder block_in_place calls
- Optimize hot-path string cloning in remote_client spectrum loop

https://claude.ai/code/session_01Y3G65hrfsRRjwyBF2qbBmc
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-29 19:29:17 +02:00
sjg ef9d97d4b5 [style](trx-rs): apply rustfmt formatting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-29 14:28:53 +02:00
Claude a69c5143e6 [refactor](trx-rs): resolve all improvement areas (P0-P3)
Addresses every item in docs/Improvement-Areas.md:

P0 - Plugin signing: new src/trx-app/src/plugins.rs with SHA-256 checksum
     manifest, filename allowlisting, API version compatibility checks,
     and cross-platform file permission validation.

P1 - Session store mutex poisoning: all .unwrap() calls on RwLock/Mutex in
     auth.rs replaced with .unwrap_or_else(|e| e.into_inner()) + warning logs.
   - TCP listener rate limiting: added ConnectionTracker with per-IP connection
     cap (10 concurrent connections per IP).
   - RigState refactoring: decoder fields grouped into DecoderConfig and
     DecoderResetSeqs sub-structs with #[serde(flatten)] for wire compat.
   - spawn_blocking timeout: satellite pass computation wrapped in 30s timeout.

P2 - Command handler macro: rig_command! macro generates 7 unit-struct command
     implementations, reducing ~200 lines of boilerplate.
   - Protocol versioning: added protocol_version field to ClientEnvelope and
     ClientResponse; improved unknown command error handling in parse_envelope.
   - Unsafe string: replaced from_utf8_unchecked with safe from_utf8().expect().
   - Dead code: removed 2 unnecessary annotations, documented remaining 4.

P3 - Tests: added 4 unit tests for history_store.rs (round-trip, expiry, etc).
   - FT-817 VFO: improved inference for ambiguous same-frequency case.
   - Configurator: implemented serial port detection via tokio_serial.
   - Plugin versioning: integrated into plugin manifest (api_version field).
   - Naming: documented as intentional semantic distinctions, not inconsistencies.

https://claude.ai/code/session_01Gj1vEkP6GKVcVaMqzFW885
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-29 14:10:41 +02:00
sjg d3f958fc37 [fix](trx-server): uppercase DEFAULT_COMMAND_EXEC_TIMEOUT constant
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-29 11:40:44 +02:00
sjg 944fd4a0fc [refactor](trx-rs): fix clippy too_many_arguments warnings
Bundle parameters into structs to reduce argument counts:
- geo.rs: find_passes_for_sat takes &TleEntry instead of individual fields
- listener.rs: handle_client takes ClientContext struct

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-29 11:38:32 +02:00
Claude 01d0b9efdd [fix](trx-server): fix compilation errors in audio, rig_task, and main
- Add lifetime parameter to lock_or_recover and fix missing .lock() call
- Replace undefined COMMAND_EXEC_TIMEOUT constant with local command_exec_timeout variable
- Add explicit type annotations to closure parameters in history snapshot methods
- Remove unused HostTrait import
- Fix non-existent machine_state/error_message fields on RigState in crash recovery

https://claude.ai/code/session_01HAkST2gLsYDXPom3282ABY
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-29 09:52:14 +02:00
Claude 6c08ff4776 [fix](trx-server): wire TimeoutsConfig into example config and fix test signatures
- Include TimeoutsConfig in --print-config example output
- Update run_listener() test call sites with new parameters

https://claude.ai/code/session_01P9G7QCWfiYbPVJ7cgiXznf
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-29 08:54:59 +02:00
Claude 16426548de [refactor](trx-rs): resolve all P1/P2 improvement areas
P1 (High Priority):
- Fix LIFO command batching in rig_task.rs (batch.pop→batch.remove(0))
- Add ±25% jitter to ExponentialBackoff to prevent thundering herd
- Add 10,000-entry capacity bounds to decoder history queues
- Add rig task crash detection with Error state broadcast
- Decompose FrontendRuntimeContext 50-field god-struct into 9 sub-structs
  (AudioContext, DecodeHistoryContext, HttpAuthConfig, HttpUiConfig,
   RigRoutingContext, OwnerInfo, VChanContext, SpectrumContext, PerRigAudioContext)
- Migrate std::sync::RwLock to tokio::sync::RwLock in background_decode.rs
- Extract find_input_device/find_output_device helpers from audio pipeline

P2 (Medium Priority):
- Introduce SoapySdrConfig builder struct (replaces 20+ positional params)
- Add define_command_mappings! macro for ClientCommand↔RigCommand mapping
- Replace silent lock poison recovery with lock_or_recover() warning logger
- Make timeouts configurable via RigTaskConfig/ListenerConfig and TOML
- Extract shared config types to trx-app/src/shared_config.rs

Documentation updated in CLAUDE.md, Architecture.md, Improvement-Areas.md.

https://claude.ai/code/session_01P9G7QCWfiYbPVJ7cgiXznf
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-29 08:54:59 +02:00
sjg 0a60684e28 [feat](trx-rs): remove NOAA APT decoder
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-28 23:32:10 +01:00
Claude afaf19d2b4 [fix](trx-server): fix satellite pass computation degrading spectrum performance
Two issues introduced with wxsat/satellite support caused indirect
performance degradation on the spectrum rendering path:

1. spawn_tle_refresh_task() was called inside spawn_rig_audio_stack(),
   which runs per-rig. With N rigs this spawned N redundant TLE refresh
   tasks, each making 3 concurrent HTTP requests to CelesTrak and
   competing for write locks on the global TLE store. Moved to a single
   global call after the per-rig loop.

2. compute_upcoming_passes() (SGP4 propagation for 200+ satellites over
   24h = ~300K propagation steps) ran on every GetSatPasses request with
   no caching. Multiple client connections could trigger concurrent
   CPU-heavy computations, causing cache pollution and tokio runtime
   contention that indirectly slowed spectrum frame processing. Added a
   60-second server-side cache shared across all client connections.

https://claude.ai/code/session_017g7VNMb6CChaiWrfzVBhbR
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 18:25:40 +01:00
sjg 47a85d9832 [fix](trx-rs): add NOAA-15/18/19 TLEs and move sat pass refresh off main connection
CelesTrak GROUP=weather does not include legacy NOAA POES satellites.
Added GROUP=noaa fetch so NOAA-15/18/19 appear in predictions. Moved
GetSatPasses to a dedicated TCP connection (client) and spawn_blocking
(server) so pass computation never blocks state polling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-28 17:39:02 +01:00
sjg 91f50ebb3f [fix](trx-rs): increase JSON line limit to 256KB for large sat pass responses
GetSatPasses responses with 100+ satellites easily exceed the previous
16KB limit, causing the remote client to disconnect.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-28 17:24:47 +01:00
sjg 2f7adf05c8 [feat](trx-rs): add GetSatPasses protocol command for server-side TLE management
TLE refresh now happens only on trx-server (once at startup, then every
24h). Client fetches satellite predictions from server via new
GetSatPasses fast-path command and caches them locally, refreshing
every 5 minutes. Removes spawn_tle_refresh_task from trx-client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-28 15:38:39 +01:00
sjg aab344b729 [feat](trx-rs): add NOAA/Meteor predictions; rename wxsat → sat
- Add NOAA-15/18/19 and Meteor-M N2-3/N2-4 to predictions list
- Rename PREDICTION_SATS (was HAM_SATS) to include weather + ham sats
- Rename all wxsat identifiers to sat throughout JS/HTML/CSS/Rust:
  wxsat.js → sat.js, WXSAT_JS → SAT_JS, /wxsat.js route → /sat.js,
  all #wxsat-* element IDs, .wxsat-* CSS classes, window.addWxsat* →
  window.addSat*, window.onServerWxsatImage → window.onServerSatImage,
  etc. (backend protocol strings unchanged)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-28 13:53:26 +01:00
Claude 27117a8de5 [feat](trx-core): add periodic TLE refresh from CelesTrak
Fetch fresh weather satellite TLEs from CelesTrak on startup and then
once every 24 hours. The dynamic TLE store is checked first in
tle_for_satellite(), falling back to the existing hardcoded TLEs when
the fetch has not yet completed or fails.

- Add global TLE_STORE (RwLock<HashMap<norad_id, (line1, line2)>>)
- Add parse_tle_response() to parse 3-line TLE format
- Add refresh_tles_from_celestrak() async fetch + store update
- Add spawn_tle_refresh_task() for startup + daily refresh loop
- Refactor tle_for_satellite() into norad_id lookup + store check
- Spawn refresh task in trx-server alongside wxsat decoder tasks
- Add reqwest (rustls-tls) dependency to trx-core

https://claude.ai/code/session_01RB19i93dnemDYLcfrhyhqc
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 12:41:31 +01:00
Claude 929f1d3fab [fix](trx-server): remove unused pass_start_ms variable in wxsat decoder
The variable was assigned in six places but never read, producing
compiler warnings. The other `pass_start_ms` in `run_wxsat_image_task`
is unaffected and still used.

https://claude.ai/code/session_01SWe5x4CWy1q6BXtX4Gt3Qd
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 12:35:29 +01:00
Claude 560b6ec912 [feat](trx-rs): add weather satellite map overlay integration
Add SGP4-based geo-referencing for NOAA APT and Meteor LRPT decoded
satellite images, enabling them to be displayed as semi-transparent
overlays on the Leaflet map module with ground track polylines.

Changes:
- Add sgp4 crate dependency to trx-core for orbital propagation
- New trx-core/src/geo.rs module with TLE-based pass geo-referencing,
  ECI-to-geodetic conversion, and station-location fallback estimation
- Extend WxsatImage and LrptImage structs with geo_bounds and
  ground_track optional fields (backward compatible via serde defaults)
- Compute geo-bounds in finalize_wxsat_pass and finalize_lrpt_pass
  using satellite identity, pass timestamps, and station coordinates
- Add 'wxsat' source filter to the map module (off by default)
- Add L.imageOverlay rendering with popup and ground track polyline
- Add "Show on Map" buttons in wxsat plugin live/history views

https://claude.ai/code/session_01DUCfb9CjGoViwBrznpfWyt
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 12:24:36 +01:00
Claude 384e1597f6 [style](trx-wxsat): apply cargo fmt formatting
https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 10:36:34 +01:00
Claude 4d40c29e49 [refactor](trx-wxsat): unify image encoding to shared PNG module
Extract common image_enc module at crate root with encode_grayscale_png
and encode_rgb_png helpers. Both NOAA APT and Meteor-M LRPT now use PNG
as the output format through the shared encoder. Drop jpeg image feature
dependency.

https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 10:36:34 +01:00
Claude 1a3b815ed8 [feat](trx-wxsat): add Meteor-M LRPT decoder and Weather Satellites frontend panel
Restructure trx-wxsat into noaa/ (APT) and lrpt/ (Meteor-M LRPT) submodules
with shared crate base. Add QPSK demodulator, CCSDS CADU framer, MCU channel
assembler for LRPT. Wire LRPT through full stack (core types, protocol, server
decoder task, client). Add Weather Satellites sub-tab in Digital Modes with
toggle buttons for NOAA APT and Meteor LRPT, descriptions, and image history.

https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 10:36:34 +01:00
Claude d26ef6ca81 [feat](trx-wxsat): rename trx-noaa to trx-wxsat with full NOAA APT decode
Rename the crate from trx-noaa to trx-wxsat (weather satellite) across
the entire workspace. Add full NOAA satellite decode support:

- Telemetry frame parsing: extract 16-wedge calibration data from the
  128-line telemetry frames embedded in APT lines
- Radiometric calibration: piecewise-linear LUT built from wedges 1-8
  to correct pixel values against known reference levels
- Channel identification: detect AVHRR sensor channels (VIS, NIR, MIR,
  TIR) from wedge 9 values per APT sub-channel
- Satellite identification: heuristic NOAA-15/18/19 detection from
  channel A/B sensor pairings
- Histogram equalisation: per-channel contrast enhancement for improved
  image output
- WxsatImage now carries satellite name and channel labels in decoded
  message broadcasts

https://claude.ai/code/session_01JA13DHuzuHUL4nSBBRU83f
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-28 10:36:34 +01:00
sjg 450a5388bf [fix](trx-server): fix NOAA decoder warnings and Send bound
- Remove unused chrono::Local import (use fully-qualified path)
- Drop watch::Ref before .await in state-change branch to satisfy Send
- Remove unused pass_start_ms parameter from finalize_noaa_pass

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-28 07:06:39 +01:00
sjg 4b40d44814 [feat](trx-noaa): add NOAA APT satellite image decoder
New trx-noaa crate: FFT-based Hilbert transform (rustfft) for 2400 Hz
AM demodulation, sync A detection via cross-correlation, line assembly
at 4160 Hz, and JPEG output via the image crate.

- trx-core: NoaaImage type, DecodedMessage::NoaaImage variant,
  noaa_decode_enabled/noaa_decode_reset_seq on RigState/RigSnapshot,
  AUDIO_MSG_NOAA_IMAGE = 0x16
- trx-server: DecoderHistories::noaa, run_noaa_decoder task (activates
  on noaa_decode_enabled, auto-finalises after 30 s silence), saves
  JPEGs to ~/.cache/trx-rs/noaa/<YYYY-MM-DD_HH-MM-SS>.jpg, forwards
  events over TCP audio channel and history replay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-28 07:00:24 +01:00
Claude 0905360ba5 [fix](trx-backend-soapysdr): remove noise/pilot dependency from WFM signal strength
The noise floor subtraction was over-aggressive: the bandwidth ratio
scaling between the 67 kHz baseband probe and the IQ domain amplified
the noise estimate excessively, causing weak stations to be subtracted
to nothing.  The pilot-referenced correction only worked for stereo
stations.

Strip the signal strength path back to what actually works universally:
mean IQ envelope power with asymmetric attack/decay smoothing.  This
always produces a reading for any FM signal — mono, stereo, with or
without RDS.

The baseband noise probe, CNR estimation, and pilot metrics remain in
the WfmStereoDecoder for their existing uses (RDS quality weighting,
CCI/ACI estimation) but no longer feed into the S-meter.

https://claude.ai/code/session_017URSDqSJ8TyZpDhV2vKZUe
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-27 20:50:06 +01:00
Claude 41fa9dc242 [feat](trx-backend-soapysdr): proper WFM signal strength algorithm
Replace the simple IQ power averaging with a proper WFM signal
strength measurement algorithm based on established RF engineering
practice:

1. Asymmetric attack/decay smoothing (τ_attack=2ms, τ_decay=300ms)
   per IARU Region 1 Technical Recommendation R.1 for professional
   S-meter behaviour.  Fast attack catches signal increases
   immediately; slow decay provides stable, readable meter movement.

2. Baseband noise floor estimation via a 67 kHz probe in the
   demodulated FM baseband.  FM demodulation noise follows an f²
   spectral shape, so energy above the useful baseband (audio +
   RDS ≤ 57 kHz) is dominated by channel noise and independent of
   program content.  Subtracting this noise estimate in the linear
   domain reveals the carrier-only power, preventing the meter from
   reading the noise floor on empty/weak channels.

3. Pilot-referenced quality correction.  The 19 kHz stereo pilot
   has a known fixed amplitude at the transmitter (±7.5 kHz
   deviation, 10% of ±75 kHz).  Near the FM threshold (~10 dB CNR)
   where noise dominates the IQ reading, the pilot tone power
   provides an independent quality-weighted correction.  The blend
   factor scales from 0.3 at low CNR down to 0 at high CNR where
   the raw IQ measurement is already accurate.

4. CNR estimation from the ratio of total baseband power to the
   above-band noise probe, enabling adaptive pilot correction and
   providing a signal quality metric for future use.

https://claude.ai/code/session_017URSDqSJ8TyZpDhV2vKZUe
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-27 20:33:09 +01:00
sjg 0efdb5e360 [fix](trx-rs): show signal strength with decimal precision
Change RigRxStatus.sig from i32 to f64 and add get_signal_strength_db
to RigCat trait so SDR backends can bypass the coarse 0..15 quantisation.
Compensate for decimation processing gain so the meter matches the
spectrum peak. Display with one decimal place in all units.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 20:06:36 +01:00
sjg d468a96448 [fix](trx-backend-soapysdr): stabilize WFM signal strength and speed up SDR polling
Smooth envelope power (I²+Q²) instead of filtering I/Q components
separately — eliminates ~6 dB modulation-dependent fluctuation caused
by FM carrier rotation in the IQ plane. Reset signal strength on
frequency change. Reduce SDR poll interval from 500ms to 100ms.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 18:52:12 +01:00
sjg 07cb8818f5 [style](trx-rs): cargo fmt
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 18:52:05 +01:00
Claude 3ca3836702 [fix](trx-backend-soapysdr): measure WFM signal strength in IQ domain, not power domain
The previous carrier power IIR filtered |IQ|² (power), which only smoothed
temporal fluctuations but still integrated noise across the full 180 kHz WFM
channel bandwidth. This caused background noise to read ~-78 dBFS instead of
the expected ~-110 dBFS (~32 dB too high ≈ 10·log₁₀(180kHz/500Hz)).

Move the single-pole IIR lowpass to the IQ domain (filter I and Q separately
at ~500 Hz cutoff), then compute power from the filtered output. This rejects
out-of-band noise before the power measurement, so the meter reads true
carrier level rather than total wideband noise.

https://claude.ai/code/session_01W4WPMB2Lg3hgaY6opsk25f
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-27 18:28:35 +01:00
Claude 0ad3440a2d [fix](trx-backend-soapysdr): use narrow carrier IIR for WFM signal strength
Replace peak |IQ|² measurement with a per-sample single-pole IIR lowpass
on the instantaneous power (~500 Hz cutoff).  FM has constant envelope so
the IIR converges to the true carrier power A², rejecting wideband noise
that previously inflated the peak reading and masked actual signal level.

Other modes keep the existing peak + EMA approach.

https://claude.ai/code/session_01X6tedMVpjX3DEqLFDBR7FK
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-27 17:35:08 +01:00
Claude a387047464 [fix](trx-backend-soapysdr): widen RDS bandpass Q from 5.0 to 3.5 to reduce in-band distortion
The 8th-order (4×biquad) RDS bandpass at Q=5 per stage produced a
composite −3 dB bandwidth of ±2480 Hz, but the steep 8th-order roll-off
tapered the RDS signal edges (±1544 Hz at α=0.30) by −1.2 dB.  This
distorted the RRC matched filter's expected flat spectrum, causing ISI
and degrading soft-decision confidence — directly hurting PS/RT decode
on weak signals.

Q=3.5 widens the composite passband to ±3560 Hz, reducing band-edge
attenuation to −0.59 dB while still providing ≈−4 dB rejection at the
stereo difference signal edge (53 kHz) and steep 8th-order far-out
roll-off.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>

https://claude.ai/code/session_01Sw9esAuic8KHP1t8nZgvH2
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-27 16:28:39 +01:00
sjg 56c19ae15d [fix](trx-backend-soapysdr): measure signal strength from peak filtered IQ before AGC
Replace the DC-component approach (which underreads FM due to carrier deviation) with peak |s|² on the filtered+decimated IQ before AGC is applied. Works correctly for both constant-envelope FM and narrowband modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 15:57:38 +01:00
sjg 6440f67d94 [fix](trx-rs): add EMA smoothing to signal strength and fix freq input clobbering
EMA (α=0.4) smooths the carrier power estimate across DSP blocks. Custom PartialEq on VchanRdsEntry excludes signal_db so rapidly-changing levels do not trigger main state SSE updates that overwrite the frequency input.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 15:24:18 +01:00
sjg 5220e606c1 [fix](trx-backend-soapysdr): measure carrier power from mixed-signal DC for signal strength
Use the DC component of the baseband-mixed IQ (before LPF/decimation) as a narrow-band carrier power estimate. This correlates with the spectrum FFT peak instead of measuring wideband channel power which inflates the reading for WFM.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 15:00:17 +01:00
sjg 7ffd1ccd6a [fix](trx-backend-soapysdr): use peak IQ magnitude for signal strength display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 14:48:03 +01:00
sjg 4fc32f0e90 [fix](trx-rs): wire DSP signal strength to Signal strength field and per-vchan SSE
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 14:24:44 +01:00
Claude e44e616ab8 [fix](trx-backend-soapysdr): fix ACI/CCI always reading 0% in WFM
ACI: the hard limiter in channel.rs normalised IQ samples to unit
magnitude *before* the CMA equalizer, making the signal perfectly
constant-modulus so the CMA never adapted and tap deviation stayed
at zero.  Fix by moving the hard limiter inside process_iq (after
the CMA) and replacing the CMA-based metric with IQ envelope
coefficient of variation, computed on the raw samples.

CCI: the pilot coherence has a theoretical maximum of π/4 ≈ 0.785
(not 1.0), so coherence_penalty was always ~0.215 even for a clean
signal.  The Q/I ratio also depended on the arbitrary NCO-pilot
phase offset rather than actual interference.  Fix by normalising
coherence by its theoretical max and dropping the phase-dependent
Q/I ratio.  Gate CCI on pilot detection so mono signals read 0%.

https://claude.ai/code/session_01PUXWNMRGfrWYH56k2DLmen
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-27 11:38:03 +01:00
Claude 4da4d8ec66 [feat](trx-rs): add CCI/ACI bars to WFM panel with RDS mitigation
Estimate Co-Channel Interference (CCI) from pilot tone quadrature
leakage and coherence degradation.  Estimate Adjacent Channel
Interference (ACI) from CMA equalizer tap deviation from identity.
Both metrics (0-100 scale) are surfaced through RigFilterState and
displayed as colour-coded bars in the WFM control panel.

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

https://claude.ai/code/session_016EKzep42RCvE4GxvvRaCwu
Signed-off-by: Claude <noreply@anthropic.com>
2026-03-27 09:44:28 +01:00
sjg 57e88b3590 [fix](trx-rds): tune RDS parameters for maximum sensitivity
- RRC_ALPHA 0.75→0.50: narrower noise BW, ~0.6 dB SNR gain
- COSTAS_KI 3.5e-7: maintain ζ≈0.68 (1e-6 caused loop instability)
- Soft confidence: use biphase_i.abs() instead of full vector magnitude
  so OSD confidence is aligned with bit-decision sign; suppresses
  false groups under noise with residual Costas phase error
- OSD(2) in locked mode: corrects ≤2-bit errors after block sync
- Search mode: hard decode only for Block A; OSD(1) in search yielded
  ~13% false Block A rate per bit, letting wrong clock candidates
  accumulate false groups as fast as the correct candidate
- Incumbent candidate tracking (best_candidate_idx): the winning
  candidate updates best_state at equal score; challengers need strictly
  higher score; best_score tracks incumbent even on no-state-change
  groups so challengers can't leapfrog on a single false group
- blocks_to_chips: add NRZI (NRZ-Mark) pre-encoding so the differential
  biphase decoder recovers actual data bits rather than XOR-of-pairs
- Add blocks_to_chips_round_trips_all_groups test: verifies all 16 blocks
  across 4 PS segments round-trip correctly without BPSK modulation

[fix](trx-backend-soapysdr): lower pilot lock threshold for weak-signal RDS

- PILOT_LOCK_THRESHOLD 0.25→0.20, add PILOT_LOCK_ONSET=0.30 constant
- Pilot reference engages at coherence ≥0.36 (was ≥0.45)

WIP: end_to_end_clean_signal_decodes_ps still failing (13/15 pass).
Decoder skips segment 2 due to ISI from rectangular test chips through
RRC receive filter. chips_to_rds_signal needs RRC pulse shaping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 01:46:01 +01:00
sjg 8b310c184b [fix](trx-backend-soapysdr): lower pilot lock threshold for weak-signal RDS
Lower PILOT_LOCK_THRESHOLD 0.5 -> 0.25 so the accurate 57 kHz pilot-derived
carrier reference is handed to the RDS decoder even with a weaker pilot tone.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-27 00:01:10 +01:00
sjg 32e090f927 [fix](trx-backend-soapysdr): preserve RDS subcarrier with narrow WFM bandwidth
The IQ prefilter cutoff was audio_bandwidth_hz/2, so any setting below
~120 kHz would cut off the 57 kHz RDS subcarrier before FM demod.

- Clamp IQ prefilter cutoff to >= 60 kHz for WFM in both new() and
  rebuild_filters() — audio quality is unaffected since WfmStereoDecoder
  applies its own 18 kHz lowpass internally
- Ensure pipeline target rate >= 120 kHz for WFM so the decimated IQ
  sample rate can represent the 60 kHz cutoff

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
2026-03-26 23:31:54 +01:00