[feat](trx-rs): add FT2 decoder protocol support

Implement a distinct FT2 protocol path in the decoder stack and align\nits timing with the confirmed FT2 framing used by Decodium.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>

Signed-off-by: Stanislaw Grams <stanislawgrams@gmail.com>
This commit is contained in:
2026-03-14 19:49:05 +01:00
parent d547c45a9c
commit ad6aa6aab4
9 changed files with 116 additions and 46 deletions
+9
View File
@@ -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");
}
+20 -1
View File
@@ -11,6 +11,13 @@
#include <string.h>
#include <stdio.h>
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);
+29 -28
View File
@@ -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<Self, String> {
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, String> {
Self::new_with_protocol(sample_rate, PROTOCOL_FT4, "FT4")
}
pub fn new_ft2(sample_rate: u32) -> Result<Self, String> {
Self::new_with_protocol(sample_rate, PROTOCOL_FT2, "FT2")
}
fn new_with_protocol(sample_rate: u32, protocol: c_int, label: &str) -> Result<Self, String> {
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<Self, String> {
// 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);
}
}
+7 -9
View File
@@ -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();