diff --git a/external/ft8_lib/common/monitor.c b/external/ft8_lib/common/monitor.c index 6bd228a..19290c7 100644 --- a/external/ft8_lib/common/monitor.c +++ b/external/ft8_lib/common/monitor.c @@ -54,8 +54,8 @@ static void waterfall_free(ftx_waterfall_t* me) void monitor_init(monitor_t* me, const monitor_config_t* cfg) { - float slot_time = (cfg->protocol == FTX_PROTOCOL_FT4) ? FT4_SLOT_TIME : FT8_SLOT_TIME; - float symbol_period = (cfg->protocol == FTX_PROTOCOL_FT4) ? FT4_SYMBOL_PERIOD : FT8_SYMBOL_PERIOD; + float slot_time = ftx_protocol_slot_time(cfg->protocol); + float symbol_period = ftx_protocol_symbol_period(cfg->protocol); // Compute DSP parameters that depend on the sample rate me->block_size = (int)(cfg->sample_rate * symbol_period); // samples corresponding to one FSK symbol me->subblock_size = me->block_size / cfg->time_osr; diff --git a/external/ft8_lib/ft8/constants.h b/external/ft8_lib/ft8/constants.h index eb50fca..156c809 100644 --- a/external/ft8_lib/ft8/constants.h +++ b/external/ft8_lib/ft8/constants.h @@ -14,6 +14,9 @@ extern "C" #define FT4_SYMBOL_PERIOD (0.048f) ///< FT4 symbol duration, defines tone deviation in Hz and symbol rate #define FT4_SLOT_TIME (7.5f) ///< FT4 slot period +#define FT2_SYMBOL_PERIOD (0.024f) ///< FT2 symbol duration (288 samples @ 12 kHz) +#define FT2_SLOT_TIME (3.75f) ///< FT2 slot period + // Define FT8 symbol counts // FT8 message structure: // S D1 S D2 S @@ -38,6 +41,14 @@ extern "C" #define FT4_NUM_SYNC (4) ///< Number of sync groups #define FT4_SYNC_OFFSET (33) ///< Offset between sync groups +// FT2 reuses the FT4 channel structure with a shorter slot and symbol period. +#define FT2_ND FT4_ND +#define FT2_NR FT4_NR +#define FT2_NN FT4_NN +#define FT2_LENGTH_SYNC FT4_LENGTH_SYNC +#define FT2_NUM_SYNC FT4_NUM_SYNC +#define FT2_SYNC_OFFSET FT4_SYNC_OFFSET + // Define LDPC parameters #define FTX_LDPC_N (174) ///< Number of bits in the encoded message (payload with LDPC checksum bits) #define FTX_LDPC_K (91) ///< Number of payload bits (including CRC) @@ -52,9 +63,29 @@ extern "C" typedef enum { FTX_PROTOCOL_FT4, - FTX_PROTOCOL_FT8 + FTX_PROTOCOL_FT8, + FTX_PROTOCOL_FT2 } ftx_protocol_t; +static inline float ftx_protocol_symbol_period(ftx_protocol_t protocol) +{ + return (protocol == FTX_PROTOCOL_FT8) + ? FT8_SYMBOL_PERIOD + : ((protocol == FTX_PROTOCOL_FT2) ? FT2_SYMBOL_PERIOD : FT4_SYMBOL_PERIOD); +} + +static inline float ftx_protocol_slot_time(ftx_protocol_t protocol) +{ + return (protocol == FTX_PROTOCOL_FT8) + ? FT8_SLOT_TIME + : ((protocol == FTX_PROTOCOL_FT2) ? FT2_SLOT_TIME : FT4_SLOT_TIME); +} + +static inline int ftx_protocol_uses_ft4_layout(ftx_protocol_t protocol) +{ + return (protocol == FTX_PROTOCOL_FT4) || (protocol == FTX_PROTOCOL_FT2); +} + /// Costas 7x7 tone pattern for synchronization extern const uint8_t kFT8_Costas_pattern[7]; extern const uint8_t kFT4_Costas_pattern[4][4]; diff --git a/external/ft8_lib/ft8/decode.c b/external/ft8_lib/ft8/decode.c index efb1718..5254fcd 100644 --- a/external/ft8_lib/ft8/decode.c +++ b/external/ft8_lib/ft8/decode.c @@ -189,8 +189,9 @@ static int ft4_sync_score(const ftx_waterfall_t* wf, const ftx_candidate_t* cand int ftx_find_candidates(const ftx_waterfall_t* wf, int num_candidates, ftx_candidate_t heap[], int min_score) { - int (*sync_fun)(const ftx_waterfall_t*, const ftx_candidate_t*) = (wf->protocol == FTX_PROTOCOL_FT4) ? ft4_sync_score : ft8_sync_score; - int num_tones = (wf->protocol == FTX_PROTOCOL_FT4) ? 4 : 8; + int (*sync_fun)(const ftx_waterfall_t*, const ftx_candidate_t*) = + ftx_protocol_uses_ft4_layout(wf->protocol) ? ft4_sync_score : ft8_sync_score; + int num_tones = ftx_protocol_uses_ft4_layout(wf->protocol) ? 4 : 8; int heap_size = 0; ftx_candidate_t candidate; @@ -327,7 +328,7 @@ static void ftx_normalize_logl(float* log174) bool ftx_decode_candidate(const ftx_waterfall_t* wf, const ftx_candidate_t* cand, int max_iterations, ftx_message_t* message, ftx_decode_status_t* status) { float log174[FTX_LDPC_N]; // message bits encoded as likelihood - if (wf->protocol == FTX_PROTOCOL_FT4) + if (ftx_protocol_uses_ft4_layout(wf->protocol)) { ft4_extract_likelihood(wf, cand, log174); } @@ -366,7 +367,7 @@ bool ftx_decode_candidate(const ftx_waterfall_t* wf, const ftx_candidate_t* cand // Reuse CRC value as a hash for the message (TODO: 14 bits only, should perhaps use full 16 or 32 bits?) message->hash = status->crc_calculated; - if (wf->protocol == FTX_PROTOCOL_FT4) + if (ftx_protocol_uses_ft4_layout(wf->protocol)) { // '[..] for FT4 only, in order to avoid transmitting a long string of zeros when sending CQ messages, // the assembled 77-bit message is bitwise exclusive-OR’ed with [a] pseudorandom sequence before computing the CRC and FEC parity bits' @@ -589,4 +590,4 @@ static void pack_bits(const uint8_t bit_array[], int num_bits, uint8_t packed[]) ++byte_idx; } } -} \ No newline at end of file +} diff --git a/external/ft8_lib/ft8/encode.c b/external/ft8_lib/ft8/encode.c index 33c249c..07df6a7 100644 --- a/external/ft8_lib/ft8/encode.c +++ b/external/ft8_lib/ft8/encode.c @@ -193,3 +193,8 @@ void ft4_encode(const uint8_t* payload, uint8_t* tones) } } } + +void ft2_encode(const uint8_t* payload, uint8_t* tones) +{ + ft4_encode(payload, tones); +} diff --git a/external/ft8_lib/ft8/encode.h b/external/ft8_lib/ft8/encode.h index dd3712f..339bd35 100644 --- a/external/ft8_lib/ft8/encode.h +++ b/external/ft8_lib/ft8/encode.h @@ -34,6 +34,12 @@ void ft8_encode(const uint8_t* payload, uint8_t* tones); /// @param[out] tones - array of FT4_NN (105) bytes to store the generated tones (encoded as 0..3) void ft4_encode(const uint8_t* payload, uint8_t* tones); +/// Generate FT2 tone sequence from payload data. +/// FT2 uses the FT4 framing with a doubled symbol rate. +/// @param[in] payload - 10 byte array consisting of 77 bit payload +/// @param[out] tones - array of FT2_NN (105) bytes to store the generated tones (encoded as 0..3) +void ft2_encode(const uint8_t* payload, uint8_t* tones); + #ifdef __cplusplus } #endif diff --git a/src/decoders/trx-ft8/build.rs b/src/decoders/trx-ft8/build.rs index fb6ffd6..3017158 100644 --- a/src/decoders/trx-ft8/build.rs +++ b/src/decoders/trx-ft8/build.rs @@ -31,12 +31,21 @@ fn main() { println!("cargo:rerun-if-changed=src/ft8_wrapper.c"); println!("cargo:rerun-if-changed={base}/common/monitor.c"); + println!("cargo:rerun-if-changed={base}/common/monitor.h"); println!("cargo:rerun-if-changed={base}/fft/kiss_fft.c"); + println!("cargo:rerun-if-changed={base}/fft/kiss_fft.h"); println!("cargo:rerun-if-changed={base}/fft/kiss_fftr.c"); + println!("cargo:rerun-if-changed={base}/fft/kiss_fftr.h"); println!("cargo:rerun-if-changed={base}/ft8/constants.c"); + println!("cargo:rerun-if-changed={base}/ft8/constants.h"); println!("cargo:rerun-if-changed={base}/ft8/crc.c"); + println!("cargo:rerun-if-changed={base}/ft8/crc.h"); println!("cargo:rerun-if-changed={base}/ft8/decode.c"); + println!("cargo:rerun-if-changed={base}/ft8/decode.h"); println!("cargo:rerun-if-changed={base}/ft8/ldpc.c"); + println!("cargo:rerun-if-changed={base}/ft8/ldpc.h"); println!("cargo:rerun-if-changed={base}/ft8/message.c"); + println!("cargo:rerun-if-changed={base}/ft8/message.h"); println!("cargo:rerun-if-changed={base}/ft8/text.c"); + println!("cargo:rerun-if-changed={base}/ft8/text.h"); } diff --git a/src/decoders/trx-ft8/src/ft8_wrapper.c b/src/decoders/trx-ft8/src/ft8_wrapper.c index 91533a7..24787aa 100644 --- a/src/decoders/trx-ft8/src/ft8_wrapper.c +++ b/src/decoders/trx-ft8/src/ft8_wrapper.c @@ -11,6 +11,13 @@ #include #include +enum +{ + TRX_FTX_PROTOCOL_FT4 = 0, + TRX_FTX_PROTOCOL_FT8 = 1, + TRX_FTX_PROTOCOL_FT2 = 2, +}; + // Callsign hash table (from demo/decode_ft8.c) #define CALLSIGN_HASHTABLE_SIZE 256 @@ -120,7 +127,19 @@ ft8_decoder_t* ft8_decoder_create(int sample_rate, float f_min, float f_max, int dec->cfg.sample_rate = sample_rate; dec->cfg.time_osr = time_osr; dec->cfg.freq_osr = freq_osr; - dec->cfg.protocol = (protocol == 0) ? FTX_PROTOCOL_FT4 : FTX_PROTOCOL_FT8; + switch (protocol) + { + case TRX_FTX_PROTOCOL_FT4: + dec->cfg.protocol = FTX_PROTOCOL_FT4; + break; + case TRX_FTX_PROTOCOL_FT2: + dec->cfg.protocol = FTX_PROTOCOL_FT2; + break; + case TRX_FTX_PROTOCOL_FT8: + default: + dec->cfg.protocol = FTX_PROTOCOL_FT8; + break; + } hashtable_init(); monitor_init(&dec->mon, &dec->cfg); diff --git a/src/decoders/trx-ft8/src/lib.rs b/src/decoders/trx-ft8/src/lib.rs index 9344cd5..c834fcb 100644 --- a/src/decoders/trx-ft8/src/lib.rs +++ b/src/decoders/trx-ft8/src/lib.rs @@ -12,6 +12,9 @@ const TIME_OSR: i32 = 2; const FREQ_OSR: i32 = 2; const FTX_MAX_MESSAGE_LENGTH: usize = 35; +const PROTOCOL_FT4: c_int = 0; +const PROTOCOL_FT8: c_int = 1; +const PROTOCOL_FT2: c_int = 2; #[repr(C)] #[derive(Clone, Copy)] @@ -63,30 +66,18 @@ unsafe impl Send for Ft8Decoder {} impl Ft8Decoder { pub fn new(sample_rate: u32) -> Result { - unsafe { - let ptr = ft8_decoder_create( - sample_rate as c_int, - F_MIN_HZ, - F_MAX_HZ, - TIME_OSR as c_int, - FREQ_OSR as c_int, - 1, // FTX_PROTOCOL_FT8 - ); - let inner = NonNull::new(ptr).ok_or_else(|| "ft8_decoder_create failed".to_string())?; - let block_size = ft8_decoder_block_size(inner.as_ptr()) as usize; - if block_size == 0 { - ft8_decoder_free(inner.as_ptr()); - return Err("invalid FT8 block size".to_string()); - } - Ok(Self { - inner, - block_size, - sample_rate, - }) - } + Self::new_with_protocol(sample_rate, PROTOCOL_FT8, "FT8") } pub fn new_ft4(sample_rate: u32) -> Result { + Self::new_with_protocol(sample_rate, PROTOCOL_FT4, "FT4") + } + + pub fn new_ft2(sample_rate: u32) -> Result { + Self::new_with_protocol(sample_rate, PROTOCOL_FT2, "FT2") + } + + fn new_with_protocol(sample_rate: u32, protocol: c_int, label: &str) -> Result { unsafe { let ptr = ft8_decoder_create( sample_rate as c_int, @@ -94,13 +85,13 @@ impl Ft8Decoder { F_MAX_HZ, TIME_OSR as c_int, FREQ_OSR as c_int, - 0, // FTX_PROTOCOL_FT4 + protocol, ); let inner = NonNull::new(ptr).ok_or_else(|| "ft8_decoder_create failed".to_string())?; let block_size = ft8_decoder_block_size(inner.as_ptr()) as usize; if block_size == 0 { ft8_decoder_free(inner.as_ptr()); - return Err("invalid FT4 block size".to_string()); + return Err(format!("invalid {label} block size")); } Ok(Self { inner, @@ -110,11 +101,6 @@ impl Ft8Decoder { } } - pub fn new_ft2(sample_rate: u32) -> Result { - // Wired to FT4 protocol pending a dedicated FT2 implementation. - Self::new_ft4(sample_rate) - } - pub fn block_size(&self) -> usize { self.block_size } @@ -179,3 +165,18 @@ impl Drop for Ft8Decoder { } } } + +#[cfg(test)] +mod tests { + use super::Ft8Decoder; + + #[test] + fn ft2_uses_distinct_block_size() { + let ft4 = Ft8Decoder::new_ft4(12_000).expect("ft4 decoder"); + let ft2 = Ft8Decoder::new_ft2(12_000).expect("ft2 decoder"); + + assert!(ft2.block_size() < ft4.block_size()); + assert_eq!(ft4.block_size(), 576); + assert_eq!(ft2.block_size(), 288); + } +} diff --git a/src/trx-server/src/audio.rs b/src/trx-server/src/audio.rs index cb7f81d..fde0040 100644 --- a/src/trx-server/src/audio.rs +++ b/src/trx-server/src/audio.rs @@ -1853,7 +1853,7 @@ pub async fn run_ft4_decoder( } } -/// Run the FT2 decoder task. Mirrors FT4 but uses FT2 protocol (7.5s slots for now). +/// Run the FT2 decoder task. Mirrors FT4 but uses FT2 protocol timing. pub async fn run_ft2_decoder( sample_rate: u32, channels: u16, @@ -1902,12 +1902,11 @@ pub async fn run_ft2_decoder( recv = pcm_rx.recv() => { match recv { Ok(frame) => { - let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { - Ok(dur) => dur.as_secs() as i64, + let now_ms = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { + Ok(dur) => dur.as_millis() as i64, Err(_) => 0, }; - // FT2 slot period is 7.5s (same as FT4 for now); use now * 2 / 15 - let slot = now * 2 / 15; + let slot = now_ms / 3_750; if slot != last_slot { last_slot = slot; decoder.reset(); @@ -2449,13 +2448,12 @@ async fn run_background_ft2_decoder( loop { match pcm_rx.recv().await { Ok(frame) => { - let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) + let now_ms = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) { - Ok(dur) => dur.as_secs() as i64, + Ok(dur) => dur.as_millis() as i64, Err(_) => 0, }; - // FT2 slot period is 7.5s (same as FT4 for now); use now * 2 / 15 - let slot = now * 2 / 15; + let slot = now_ms / 3_750; if slot != last_slot { last_slot = slot; decoder.reset();