[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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user