diff --git a/src/decoders/trx-ftx/Cargo.toml b/src/decoders/trx-ftx/Cargo.toml index 8bcb92e..ae02b57 100644 --- a/src/decoders/trx-ftx/Cargo.toml +++ b/src/decoders/trx-ftx/Cargo.toml @@ -7,6 +7,10 @@ name = "trx-ftx" version.workspace = true edition = "2021" +[features] +default = [] +ft2 = [] + [dependencies] rustfft = "6" realfft = "3" diff --git a/src/decoders/trx-ftx/src/common/decode.rs b/src/decoders/trx-ftx/src/common/decode.rs index 560a3ad..d05843e 100644 --- a/src/decoders/trx-ftx/src/common/decode.rs +++ b/src/decoders/trx-ftx/src/common/decode.rs @@ -6,6 +6,7 @@ //! //! Ports `decode.c` from ft8_lib. +#[cfg(feature = "ft2")] use num_complex::Complex32; use super::constants::*; @@ -37,6 +38,7 @@ pub struct FtxMessage { pub hash: u16, } +#[cfg(feature = "ft2")] pub(crate) fn wf_elem_to_complex(elem: WfElem) -> Complex32 { Complex32::new(elem.re, elem.im) } @@ -104,12 +106,20 @@ pub fn ftx_find_candidates( max_candidates: usize, min_score: i32, ) -> Vec { + #[cfg(feature = "ft2")] let is_ft2 = wf.protocol == FtxProtocol::Ft2; + #[cfg(not(feature = "ft2"))] + let is_ft2 = false; let num_tones = if wf.protocol.uses_ft4_layout() { 4 } else { 8 }; let (time_offset_min, time_offset_max) = if is_ft2 { - let max = (wf.num_blocks as i32 - FT2_NN as i32 + 2).max(-1); - (-2i16, max as i16) + #[cfg(feature = "ft2")] + { + let max = (wf.num_blocks as i32 - FT2_NN as i32 + 2).max(-1); + (-2i16, max as i16) + } + #[cfg(not(feature = "ft2"))] + unreachable!() } else if wf.protocol == FtxProtocol::Ft4 { let max = (wf.num_blocks as i32 - FT4_NN as i32 + 34).max(-33); (-34i16, max as i16) @@ -135,7 +145,10 @@ pub fn ftx_find_candidates( }; let score = if is_ft2 { - crate::ft2::ft2_sync_score(wf, &cand) + #[cfg(feature = "ft2")] + { crate::ft2::ft2_sync_score(wf, &cand) } + #[cfg(not(feature = "ft2"))] + unreachable!() } else if wf.protocol.uses_ft4_layout() { crate::ft4::ft4_sync_score(wf, &cand) } else { @@ -267,6 +280,7 @@ pub fn ftx_decode_candidate( ) -> Option { let mut log174 = [0.0f32; FTX_LDPC_N]; + #[cfg(feature = "ft2")] if wf.protocol == FtxProtocol::Ft2 { crate::ft2::ft2_extract_likelihood(wf, cand, &mut log174); } else if wf.protocol.uses_ft4_layout() { @@ -274,6 +288,12 @@ pub fn ftx_decode_candidate( } else { crate::ft8::ft8_extract_likelihood(wf, cand, &mut log174); } + #[cfg(not(feature = "ft2"))] + if wf.protocol.uses_ft4_layout() { + crate::ft4::ft4_extract_likelihood(wf, cand, &mut log174); + } else { + crate::ft8::ft8_extract_likelihood(wf, cand, &mut log174); + } ftx_normalize_logl(&mut log174); diff --git a/src/decoders/trx-ftx/src/common/monitor.rs b/src/decoders/trx-ftx/src/common/monitor.rs index e324567..5489fb0 100644 --- a/src/decoders/trx-ftx/src/common/monitor.rs +++ b/src/decoders/trx-ftx/src/common/monitor.rs @@ -267,6 +267,7 @@ mod tests { assert_eq!(mon.block_size, 576); // 12000 * 0.048 } + #[cfg(feature = "ft2")] #[test] fn monitor_block_size_ft2() { let cfg = MonitorConfig { diff --git a/src/decoders/trx-ftx/src/common/protocol.rs b/src/decoders/trx-ftx/src/common/protocol.rs index 9353325..4dc56e1 100644 --- a/src/decoders/trx-ftx/src/common/protocol.rs +++ b/src/decoders/trx-ftx/src/common/protocol.rs @@ -7,6 +7,7 @@ pub enum FtxProtocol { Ft4, Ft8, + #[cfg(feature = "ft2")] Ft2, } @@ -16,6 +17,7 @@ impl FtxProtocol { match self { Self::Ft8 => FT8_SYMBOL_PERIOD, Self::Ft4 => FT4_SYMBOL_PERIOD, + #[cfg(feature = "ft2")] Self::Ft2 => FT2_SYMBOL_PERIOD, } } @@ -25,13 +27,18 @@ impl FtxProtocol { match self { Self::Ft8 => FT8_SLOT_TIME, Self::Ft4 => FT4_SLOT_TIME, + #[cfg(feature = "ft2")] Self::Ft2 => FT2_SLOT_TIME, } } /// Whether this protocol uses FT4-style channel layout (FT4 and FT2). pub fn uses_ft4_layout(self) -> bool { - matches!(self, Self::Ft4 | Self::Ft2) + #[cfg(feature = "ft2")] + if matches!(self, Self::Ft2) { + return true; + } + matches!(self, Self::Ft4) } /// Number of data symbols. @@ -98,7 +105,9 @@ pub const FT4_SYMBOL_PERIOD: f32 = 0.048; pub const FT4_SLOT_TIME: f32 = 7.5; // FT2 timing +#[cfg(feature = "ft2")] pub const FT2_SYMBOL_PERIOD: f32 = 0.024; +#[cfg(feature = "ft2")] pub const FT2_SLOT_TIME: f32 = 3.75; // FT8 symbol counts @@ -117,11 +126,17 @@ pub const FT4_NUM_SYNC: usize = 4; pub const FT4_SYNC_OFFSET: usize = 33; // FT2 reuses FT4 layout +#[cfg(feature = "ft2")] pub const FT2_ND: usize = FT4_ND; +#[cfg(feature = "ft2")] pub const FT2_NR: usize = FT4_NR; +#[cfg(feature = "ft2")] pub const FT2_NN: usize = FT4_NN; +#[cfg(feature = "ft2")] pub const FT2_LENGTH_SYNC: usize = FT4_LENGTH_SYNC; +#[cfg(feature = "ft2")] pub const FT2_NUM_SYNC: usize = FT4_NUM_SYNC; +#[cfg(feature = "ft2")] pub const FT2_SYNC_OFFSET: usize = FT4_SYNC_OFFSET; // LDPC parameters @@ -147,12 +162,14 @@ mod tests { fn protocol_timing() { assert!((FtxProtocol::Ft8.symbol_period() - 0.160).abs() < 1e-6); assert!((FtxProtocol::Ft4.symbol_period() - 0.048).abs() < 1e-6); + #[cfg(feature = "ft2")] assert!((FtxProtocol::Ft2.symbol_period() - 0.024).abs() < 1e-6); } #[test] fn ft4_layout() { assert!(FtxProtocol::Ft4.uses_ft4_layout()); + #[cfg(feature = "ft2")] assert!(FtxProtocol::Ft2.uses_ft4_layout()); assert!(!FtxProtocol::Ft8.uses_ft4_layout()); } @@ -161,6 +178,7 @@ mod tests { fn symbol_counts() { assert_eq!(FtxProtocol::Ft8.nn(), 79); assert_eq!(FtxProtocol::Ft4.nn(), 105); + #[cfg(feature = "ft2")] assert_eq!(FtxProtocol::Ft2.nn(), 105); } } diff --git a/src/decoders/trx-ftx/src/decoder.rs b/src/decoders/trx-ftx/src/decoder.rs index f6cab5e..0df5b35 100644 --- a/src/decoders/trx-ftx/src/decoder.rs +++ b/src/decoders/trx-ftx/src/decoder.rs @@ -19,9 +19,13 @@ const DEFAULT_F_MAX_HZ: f32 = 3000.0; const DEFAULT_TIME_OSR: i32 = 2; const DEFAULT_FREQ_OSR: i32 = 2; +#[cfg(feature = "ft2")] const FT2_F_MIN_HZ: f32 = 200.0; +#[cfg(feature = "ft2")] const FT2_F_MAX_HZ: f32 = 5000.0; +#[cfg(feature = "ft2")] const FT2_TIME_OSR: i32 = 8; +#[cfg(feature = "ft2")] const FT2_FREQ_OSR: i32 = 4; const MAX_LDPC_ITERATIONS: usize = 20; @@ -37,7 +41,7 @@ pub struct Ft8DecodeResult { pub freq_hz: f32, } -/// FTx decoder instance supporting FT8, FT4, and FT2 protocols. +/// FTx decoder instance supporting FT8, FT4, and (optionally) FT2 protocols. pub struct Ft8Decoder { protocol: FtxProtocol, sample_rate: u32, @@ -46,6 +50,7 @@ pub struct Ft8Decoder { monitor: Monitor, callsign_hash: CallsignHashTable, // FT2-specific pipeline + #[cfg(feature = "ft2")] ft2_pipeline: Option, } @@ -61,12 +66,14 @@ impl Ft8Decoder { } /// Create a new FT2 decoder. + #[cfg(feature = "ft2")] pub fn new_ft2(sample_rate: u32) -> Result { Self::new_with_protocol(sample_rate, FtxProtocol::Ft2) } fn new_with_protocol(sample_rate: u32, protocol: FtxProtocol) -> Result { let (f_min, f_max, time_osr, freq_osr) = match protocol { + #[cfg(feature = "ft2")] FtxProtocol::Ft2 => (FT2_F_MIN_HZ, FT2_F_MAX_HZ, FT2_TIME_OSR, FT2_FREQ_OSR), _ => ( DEFAULT_F_MIN_HZ, @@ -92,9 +99,16 @@ impl Ft8Decoder { return Err(format!("invalid {:?} block size", protocol)); } - let window_samples = match protocol { - FtxProtocol::Ft2 => crate::ft2::FT2_NMAX, - _ => { + let window_samples = { + #[cfg(feature = "ft2")] + if protocol == FtxProtocol::Ft2 { + crate::ft2::FT2_NMAX + } else { + let slot_time = protocol.slot_time(); + (sample_rate as f32 * slot_time) as usize + } + #[cfg(not(feature = "ft2"))] + { let slot_time = protocol.slot_time(); (sample_rate as f32 * slot_time) as usize } @@ -104,6 +118,7 @@ impl Ft8Decoder { return Err(format!("invalid {:?} analysis window", protocol)); } + #[cfg(feature = "ft2")] let ft2_pipeline = if protocol == FtxProtocol::Ft2 { Some(crate::ft2::Ft2Pipeline::new(sample_rate as i32)) } else { @@ -117,6 +132,7 @@ impl Ft8Decoder { window_samples, monitor, callsign_hash: CallsignHashTable::new(), + #[cfg(feature = "ft2")] ft2_pipeline, }) } @@ -140,6 +156,7 @@ impl Ft8Decoder { pub fn reset(&mut self) { self.monitor.reset(); self.callsign_hash.cleanup(10); + #[cfg(feature = "ft2")] if let Some(ref mut pipe) = self.ft2_pipeline { pipe.reset(); } @@ -151,6 +168,7 @@ impl Ft8Decoder { return; } + #[cfg(feature = "ft2")] if self.protocol == FtxProtocol::Ft2 { // FT2: accumulate raw audio and also feed the monitor if let Some(ref mut pipe) = self.ft2_pipeline { @@ -164,6 +182,7 @@ impl Ft8Decoder { /// Check if enough data has been collected and run the decode. /// Returns decoded messages, or empty if not ready yet. pub fn decode_if_ready(&mut self, max_results: usize) -> Vec { + #[cfg(feature = "ft2")] if self.protocol == FtxProtocol::Ft2 { return self.decode_ft2(max_results); } @@ -232,6 +251,7 @@ impl Ft8Decoder { } /// FT2-specific decode pipeline. + #[cfg(feature = "ft2")] fn decode_ft2(&mut self, max_results: usize) -> Vec { let ft2_results = { let pipe = match self.ft2_pipeline.as_mut() { @@ -295,6 +315,7 @@ mod tests { assert_eq!(dec.block_size(), 576); // 12000 * 0.048 } + #[cfg(feature = "ft2")] #[test] fn ft2_uses_distinct_block_size() { let ft4 = Ft8Decoder::new_ft4(12_000).expect("ft4 decoder"); diff --git a/src/decoders/trx-ftx/src/lib.rs b/src/decoders/trx-ftx/src/lib.rs index 0e2b488..c5f90fa 100644 --- a/src/decoders/trx-ftx/src/lib.rs +++ b/src/decoders/trx-ftx/src/lib.rs @@ -4,6 +4,7 @@ pub mod common; mod decoder; +#[cfg(feature = "ft2")] pub mod ft2; pub mod ft4; pub mod ft8; diff --git a/src/trx-protocol/Cargo.toml b/src/trx-protocol/Cargo.toml index 5ddec5d..c59b355 100644 --- a/src/trx-protocol/Cargo.toml +++ b/src/trx-protocol/Cargo.toml @@ -7,6 +7,10 @@ name = "trx-protocol" version.workspace = true edition = "2021" +[features] +default = [] +ft2 = [] + [dependencies] serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/src/trx-protocol/src/decoders.rs b/src/trx-protocol/src/decoders.rs index 4d93b8f..7d14d2f 100644 --- a/src/trx-protocol/src/decoders.rs +++ b/src/trx-protocol/src/decoders.rs @@ -97,6 +97,7 @@ pub const DECODER_REGISTRY: &[DecoderDescriptor] = &[ background_decode: true, bookmark_selectable: true, }, + #[cfg(feature = "ft2")] DecoderDescriptor { id: "ft2", label: "FT2", diff --git a/src/trx-server/Cargo.toml b/src/trx-server/Cargo.toml index 25c61d7..cb99214 100644 --- a/src/trx-server/Cargo.toml +++ b/src/trx-server/Cargo.toml @@ -10,6 +10,7 @@ build = "build.rs" [features] default = ["soapysdr"] +ft2 = ["trx-ftx/ft2", "trx-protocol/ft2"] soapysdr = ["trx-backend/soapysdr"] [dependencies] diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index 823470f..8a86876 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -4,7 +4,9 @@ //! Audio capture, playback, and TCP streaming for trx-server. -use std::collections::{HashMap, HashSet, VecDeque}; +#[cfg(feature = "ft2")] +use std::collections::HashMap; +use std::collections::{HashSet, VecDeque}; use std::net::SocketAddr; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; @@ -63,8 +65,11 @@ const MAX_HISTORY_ENTRIES: usize = 10_000; /// Silence timeout before auto-finalising an LRPT pass (30 s without new MCUs). const LRPT_PASS_SILENCE_TIMEOUT: Duration = Duration::from_secs(30); const FT8_SAMPLE_RATE: u32 = 12_000; +#[cfg(feature = "ft2")] const FT2_ASYNC_BUFFER_SAMPLES: usize = 45_000; +#[cfg(feature = "ft2")] const FT2_ASYNC_TRIGGER_SAMPLES: usize = 9_000; +#[cfg(feature = "ft2")] const FT2_DEDUPE_RETENTION: Duration = Duration::from_secs(8); const DECODE_AUDIO_GATE_RMS: f32 = 2.5e-4; const AUDIO_STREAM_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(60); @@ -77,6 +82,7 @@ fn current_timestamp_ms() -> i64 { } } +#[cfg(feature = "ft2")] fn retain_ft2_window(buf: &mut Vec) { if buf.len() > FT2_ASYNC_BUFFER_SAMPLES { let excess = buf.len() - FT2_ASYNC_BUFFER_SAMPLES; @@ -84,10 +90,12 @@ fn retain_ft2_window(buf: &mut Vec) { } } +#[cfg(feature = "ft2")] fn prune_recent_ft2_decodes(recent: &mut HashMap, now: Instant) { recent.retain(|_, seen_at| now.duration_since(*seen_at) <= FT2_DEDUPE_RETENTION); } +#[cfg(feature = "ft2")] fn should_emit_ft2_decode(recent: &mut HashMap, text: &str, freq_hz: f32) -> bool { let now = Instant::now(); prune_recent_ft2_decodes(recent, now); @@ -99,6 +107,7 @@ fn should_emit_ft2_decode(recent: &mut HashMap, text: &str, fre true } +#[cfg(feature = "ft2")] fn decode_ft2_window( decoder: &mut Ft8Decoder, samples: &[f32], @@ -561,6 +570,7 @@ impl DecoderHistories { // --- FT2 --- + #[cfg_attr(not(feature = "ft2"), allow(dead_code))] fn prune_ft2(history: &mut VecDeque<(Instant, Ft8Message)>) { let cutoff = Instant::now() - FT8_HISTORY_RETENTION; while let Some((ts, _)) = history.front() { @@ -572,6 +582,7 @@ impl DecoderHistories { } } + #[cfg_attr(not(feature = "ft2"), allow(dead_code))] pub fn record_ft2_message(&self, msg: Ft8Message) { let mut h = lock_or_recover(&self.ft2, "ft2_history"); let before = h.len(); @@ -581,6 +592,7 @@ impl DecoderHistories { self.adjust_total_count(before, h.len()); } + #[cfg_attr(not(feature = "ft2"), allow(dead_code))] pub fn snapshot_ft2_history(&self) -> Vec { let mut h = lock_or_recover(&self.ft2, "ft2_history"); let before = h.len(); @@ -2047,6 +2059,7 @@ async fn run_ftx_decoder_inner( } /// Run the FT2 decoder task. Mirrors FT4 but uses FT2 protocol timing. +#[cfg(feature = "ft2")] pub async fn run_ft2_decoder( sample_rate: u32, channels: u16, @@ -2902,6 +2915,7 @@ async fn run_background_ft4_decoder( } } +#[cfg(feature = "ft2")] async fn run_background_ft2_decoder( sample_rate: u32, channels: u16, @@ -3204,6 +3218,7 @@ async fn handle_audio_client( DecodedMessage::Ft4, AUDIO_MSG_FT4_DECODE ); + #[cfg(feature = "ft2")] push_history!( histories.snapshot_ft2_history(), DecodedMessage::Ft2, @@ -3479,6 +3494,7 @@ async fn handle_audio_client( ) .await; }), + #[cfg(feature = "ft2")] "ft2" => tokio::spawn(async move { run_background_ft2_decoder( sr, diff --git a/src/trx-server/src/main.rs b/src/trx-server/src/main.rs index 052f347..6dcf4f1 100644 --- a/src/trx-server/src/main.rs +++ b/src/trx-server/src/main.rs @@ -766,19 +766,22 @@ fn spawn_rig_audio_stack( })); // Spawn FT2 decoder task - let ft2_pcm_rx = pcm_tx.subscribe(); - let ft2_state_rx = state_rx.clone(); - let ft2_decode_tx = decode_tx.clone(); - let ft2_sr = rig_cfg.audio.sample_rate; - let ft2_ch = rig_cfg.audio.channels; - let ft2_shutdown_rx = shutdown_rx.clone(); - let ft2_histories = histories.clone(); - handles.push(tokio::spawn(async move { - tokio::select! { - _ = audio::run_ft2_decoder(ft2_sr, ft2_ch as u16, ft2_pcm_rx, ft2_state_rx, ft2_decode_tx, ft2_histories) => {} - _ = wait_for_shutdown(ft2_shutdown_rx) => {} - } - })); + #[cfg(feature = "ft2")] + { + let ft2_pcm_rx = pcm_tx.subscribe(); + let ft2_state_rx = state_rx.clone(); + let ft2_decode_tx = decode_tx.clone(); + let ft2_sr = rig_cfg.audio.sample_rate; + let ft2_ch = rig_cfg.audio.channels; + let ft2_shutdown_rx = shutdown_rx.clone(); + let ft2_histories = histories.clone(); + handles.push(tokio::spawn(async move { + tokio::select! { + _ = audio::run_ft2_decoder(ft2_sr, ft2_ch as u16, ft2_pcm_rx, ft2_state_rx, ft2_decode_tx, ft2_histories) => {} + _ = wait_for_shutdown(ft2_shutdown_rx) => {} + } + })); + } // Spawn WSPR decoder task let wspr_pcm_rx = pcm_tx.subscribe();