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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>