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