[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>
This commit is contained in:
@@ -47,6 +47,22 @@ struct RawFrame {
|
||||
crc_ok: bool,
|
||||
}
|
||||
|
||||
/// AIS (Automatic Identification System) GMSK/HDLC decoder.
|
||||
///
|
||||
/// Operates on narrowband FM-demodulated audio at any sample rate (internally
|
||||
/// resampled to the 9,600 baud AIS symbol rate). The decoder performs sign
|
||||
/// slicing, NRZI decoding, HDLC flag detection with bit de-stuffing, CRC-16
|
||||
/// validation, and parsing of common AIS message types (1–3, 5, 18, 19).
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut decoder = AisDecoder::new(48_000);
|
||||
/// let messages = decoder.process_samples(&pcm_samples, "A");
|
||||
/// ```
|
||||
///
|
||||
/// Call [`reset()`](Self::reset) when switching frequency or restarting
|
||||
/// reception to clear internal symbol-tracking state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AisDecoder {
|
||||
sample_rate: f32,
|
||||
|
||||
@@ -143,8 +143,11 @@ impl DecoderFileLogger {
|
||||
state.writer = next_writer;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("decode log reopen failed for {}: {}", self.label, e);
|
||||
return;
|
||||
warn!(
|
||||
"decode log rotation failed for {}, keeping current writer: {}",
|
||||
self.label, e
|
||||
);
|
||||
// Keep the old writer rather than silently dropping writes.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,7 +160,9 @@ impl DecoderFileLogger {
|
||||
warn!("decode log write failed for {}", self.label);
|
||||
return;
|
||||
}
|
||||
let _ = state.writer.flush();
|
||||
if let Err(e) = state.writer.flush() {
|
||||
warn!("decode log flush failed for {}: {}", self.label, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -787,6 +787,29 @@ fn af_code_to_hz(code: u8) -> Option<u32> {
|
||||
// RdsDecoder — main public entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// RDS (Radio Data System) decoder for WFM broadcast signals.
|
||||
///
|
||||
/// Operates on baseband WFM audio at the configured sample rate. The decoder
|
||||
/// performs 57 kHz subcarrier recovery (via Costas loop or pilot-derived
|
||||
/// reference), RRC matched filtering, biphase (Manchester) clock recovery
|
||||
/// with multi-candidate tracking, CRC-10 syndrome checking with OSD(2)
|
||||
/// error correction, and full Group A/B parsing (PI, PS, RT, AF, CT, PTY).
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut decoder = RdsDecoder::new(228_000);
|
||||
/// // Optionally lock to pilot-derived 57 kHz reference:
|
||||
/// // decoder.set_pilot_ref(cos57, sin57);
|
||||
/// for &sample in &baseband_samples {
|
||||
/// if let Some(rds) = decoder.process_sample(sample, 1.0) {
|
||||
/// println!("PI={:04X} PS={}", rds.pi_code, rds.ps_name);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Call [`clear_pilot_ref()`](Self::clear_pilot_ref) to revert to free-running
|
||||
/// Costas loop carrier recovery when the pilot tone is lost.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RdsDecoder {
|
||||
sample_rate_hz: u32,
|
||||
|
||||
@@ -44,6 +44,17 @@ const BURST_TRIGGER_FLOOR: f32 = 1.0e-10;
|
||||
const BURST_SUSTAIN_NOISE_MULT: f32 = 1.15;
|
||||
const BURST_SUSTAIN_FLOOR: f32 = 1.0e-11;
|
||||
|
||||
/// Plausibility score below which a burst is treated as unsynced noise.
|
||||
/// The scoring scale is an integer sum of weighted heuristics (link-ID
|
||||
/// validity, tail-zero count, payload structure). –35 rejects bursts
|
||||
/// that fail nearly every plausibility check.
|
||||
const PLAUSIBILITY_UNSYNCED_THRESHOLD: i32 = -35;
|
||||
|
||||
/// Plausibility score below which the FEC label is annotated with
|
||||
/// "Low confidence". 15 corresponds to marginal decodes where enough
|
||||
/// heuristics pass to attempt CRC but the result is uncertain.
|
||||
const PLAUSIBILITY_LOW_CONFIDENCE_THRESHOLD: i32 = 15;
|
||||
|
||||
/// Warmup period: number of samples to observe before burst detection starts.
|
||||
/// This allows the noise-floor EMA (α = 0.005, τ ≈ 200 samples) to converge
|
||||
/// to the actual SDR noise level. Without warmup the initial floor of 1e-12
|
||||
@@ -52,6 +63,22 @@ const BURST_SUSTAIN_FLOOR: f32 = 1.0e-11;
|
||||
/// reaches quiet_limit and the burst never terminates.
|
||||
const NOISE_FLOOR_WARMUP_SECS: f32 = 0.05; // 50 ms ≈ 10 EMA time-constants
|
||||
|
||||
/// VDES (VHF Data Exchange System) TER-MCS-1 decoder for 100 kHz channels.
|
||||
///
|
||||
/// Consumes complex baseband IQ samples and performs burst detection,
|
||||
/// π/4-QPSK demodulation, block deinterleaving, Turbo FEC decoding
|
||||
/// (dual 8-state RSC with BCJR/MAP), CRC-16 validation, and ITU-R
|
||||
/// M.2092-1 link-layer frame parsing.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut decoder = VdesDecoder::new(192_000);
|
||||
/// let messages = decoder.process_samples(&iq_samples);
|
||||
/// ```
|
||||
///
|
||||
/// Call [`reset()`](Self::reset) when switching frequency to clear burst
|
||||
/// detection state and noise-floor estimates.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VdesDecoder {
|
||||
sample_rate: f32,
|
||||
@@ -259,7 +286,7 @@ impl VdesDecoder {
|
||||
.filter(|bit| *bit == 0)
|
||||
.count();
|
||||
let plausibility = vdes_plausibility_score(&parsed, link_id, tail_zero_bits);
|
||||
if plausibility < -35 {
|
||||
if plausibility < PLAUSIBILITY_UNSYNCED_THRESHOLD {
|
||||
return Some(build_unsynced_message(
|
||||
channel,
|
||||
&framed,
|
||||
@@ -276,7 +303,7 @@ impl VdesDecoder {
|
||||
format!(
|
||||
"Turbo FEC (8-iter BCJR), reliability {:.2}{}",
|
||||
turbo_reliability,
|
||||
if plausibility < 15 {
|
||||
if plausibility < PLAUSIBILITY_LOW_CONFIDENCE_THRESHOLD {
|
||||
" · Low confidence"
|
||||
} else {
|
||||
""
|
||||
@@ -287,7 +314,7 @@ impl VdesDecoder {
|
||||
"Hard-decision 1/2 Viterbi, tail {} / {} zero bits{}",
|
||||
tail_zero_bits,
|
||||
TER_MCS1_100_FEC_TAIL_BITS,
|
||||
if plausibility < 15 {
|
||||
if plausibility < PLAUSIBILITY_LOW_CONFIDENCE_THRESHOLD {
|
||||
" · Low confidence"
|
||||
} else {
|
||||
""
|
||||
|
||||
@@ -206,7 +206,17 @@ pub fn turbo_decode_soft(received_llrs: &[Llr], info_len: usize) -> (Vec<u8>, f3
|
||||
}
|
||||
|
||||
let interleaver = qpp_interleaver(info_len);
|
||||
debug_assert_eq!(
|
||||
interleaver.len(),
|
||||
info_len,
|
||||
"interleaver length must equal info_len"
|
||||
);
|
||||
let deinterleaver = invert_permutation(&interleaver);
|
||||
debug_assert_eq!(
|
||||
deinterleaver.len(),
|
||||
info_len,
|
||||
"deinterleaver length must equal info_len"
|
||||
);
|
||||
|
||||
let (sys_llr, par1_llr, par2_llr) = depuncture_rate_half(received_llrs, info_len);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user