use crate::core::{FrameLayout, ModulationParams, Protocol, ProtocolId, SyncBlock, SyncMode};
use crate::fec::Ldpc240_101;
use crate::msg::Wsjt77Message;
pub mod decode;
pub mod encode;
const FST4_RVEC: [u8; 77] = [
0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0,
1, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1,
1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1,
];
const FST4_SYNC_A: [u8; 8] = [0, 1, 3, 2, 1, 0, 2, 3];
const FST4_SYNC_B: [u8; 8] = [2, 3, 1, 0, 3, 2, 0, 1];
const FST4_SYNC_BLOCKS: [SyncBlock; 5] = [
SyncBlock {
start_symbol: 0,
pattern: &FST4_SYNC_A,
},
SyncBlock {
start_symbol: 38,
pattern: &FST4_SYNC_B,
},
SyncBlock {
start_symbol: 76,
pattern: &FST4_SYNC_A,
},
SyncBlock {
start_symbol: 114,
pattern: &FST4_SYNC_B,
},
SyncBlock {
start_symbol: 152,
pattern: &FST4_SYNC_A,
},
];
macro_rules! fst4_submode {
(
$(#[$attr:meta])*
$name:ident,
nsps = $nsps:literal,
ndown = $ndown:literal,
tr_period_s = $period:literal,
tx_start_offset_s = $tx_start:literal,
) => {
$(#[$attr])*
#[derive(Copy, Clone, Debug, Default)]
pub struct $name;
impl ModulationParams for $name {
const NTONES: u32 = 4;
const BITS_PER_SYMBOL: u32 = 2;
const NSPS: u32 = $nsps;
const SYMBOL_DT: f32 = ($nsps as f32) / 12_000.0;
const TONE_SPACING_HZ: f32 = 12_000.0 / ($nsps as f32);
const GRAY_MAP: &'static [u8] = &[0, 1, 3, 2];
const GFSK_BT: f32 = 2.0;
const GFSK_HMOD: f32 = 1.0;
const NFFT_PER_SYMBOL_FACTOR: u32 = 2;
const NSTEP_PER_SYMBOL: u32 = 2;
const NDOWN: u32 = $ndown;
const INFO_SCRAMBLE_RVEC: Option<&'static [u8]> = Some(&FST4_RVEC);
const LLR_NSYM_MAX: u32 = 8;
const LLR_NSYM_MID: Option<u32> = Some(4);
}
impl FrameLayout for $name {
const N_DATA: u32 = 120;
const N_SYNC: u32 = 40; const N_SYMBOLS: u32 = 160;
const N_RAMP: u32 = 0; const SYNC_MODE: SyncMode = SyncMode::Block(&FST4_SYNC_BLOCKS);
const T_SLOT_S: f32 = $period as f32;
const TX_START_OFFSET_S: f32 = $tx_start;
}
impl Protocol for $name {
type Fec = Ldpc240_101;
type Msg = Wsjt77Message;
const ID: ProtocolId = ProtocolId::Fst4;
}
};
}
fst4_submode! {
Fst4s15,
nsps = 720,
ndown = 18,
tr_period_s = 15,
tx_start_offset_s = 0.5,
}
fst4_submode! {
Fst4s30,
nsps = 1_680,
ndown = 42,
tr_period_s = 30,
tx_start_offset_s = 1.0,
}
fst4_submode! {
Fst4s60,
nsps = 3_888,
ndown = 108,
tr_period_s = 60,
tx_start_offset_s = 1.0,
}
fst4_submode! {
Fst4s120,
nsps = 8_200,
ndown = 205,
tr_period_s = 120,
tx_start_offset_s = 1.0,
}
fst4_submode! {
Fst4s300,
nsps = 21_504,
ndown = 512,
tr_period_s = 300,
tx_start_offset_s = 1.0,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::FecCodec;
#[test]
fn fst4s60_trait_surface() {
assert_eq!(<Fst4s60 as ModulationParams>::NTONES, 4);
assert_eq!(<Fst4s60 as ModulationParams>::NSPS, 3_888);
assert!((<Fst4s60 as ModulationParams>::SYMBOL_DT - 0.324).abs() < 1e-6,);
assert_eq!(<Fst4s60 as FrameLayout>::N_SYMBOLS, 160);
assert_eq!(<Fst4s60 as FrameLayout>::N_DATA, 120);
assert_eq!(<Fst4s60 as FrameLayout>::N_SYNC, 40);
let blocks = <Fst4s60 as FrameLayout>::SYNC_MODE.blocks();
assert_eq!(blocks.len(), 5);
assert_eq!(
blocks.iter().map(|b| b.start_symbol).collect::<Vec<_>>(),
vec![0, 38, 76, 114, 152],
);
assert_eq!(blocks[0].pattern.len(), 8);
assert_eq!(<<Fst4s60 as Protocol>::Fec as FecCodec>::N, 240);
assert_eq!(<<Fst4s60 as Protocol>::Fec as FecCodec>::K, 101);
}
#[test]
fn all_submodes_match_wsjtx_fst4_decode_f90() {
fn check<P: ModulationParams + FrameLayout>(
name: &str,
nsps: u32,
ndown: u32,
t_slot_s: f32,
tx_start_offset_s: f32,
) {
assert_eq!(P::NSPS, nsps, "{name} NSPS");
assert_eq!(P::NDOWN, ndown, "{name} NDOWN");
assert_eq!(
P::NSPS % P::NDOWN,
0,
"{name}: NSPS must be an exact multiple of NDOWN"
);
assert!(
(P::SYMBOL_DT - nsps as f32 / 12_000.0).abs() < 1e-6,
"{name} SYMBOL_DT"
);
assert!(
(P::TONE_SPACING_HZ - 12_000.0 / nsps as f32).abs() < 1e-3,
"{name} TONE_SPACING_HZ"
);
assert_eq!(P::T_SLOT_S, t_slot_s, "{name} T_SLOT_S");
assert_eq!(
P::TX_START_OFFSET_S,
tx_start_offset_s,
"{name} TX_START_OFFSET_S"
);
}
check::<Fst4s15>("Fst4s15", 720, 18, 15.0, 0.5);
check::<Fst4s30>("Fst4s30", 1_680, 42, 30.0, 1.0);
check::<Fst4s60>("Fst4s60", 3_888, 108, 60.0, 1.0);
check::<Fst4s120>("Fst4s120", 8_200, 205, 120.0, 1.0);
check::<Fst4s300>("Fst4s300", 21_504, 512, 300.0, 1.0);
}
#[test]
fn all_submodes_share_frame_layout_and_fec() {
fn check<P: ModulationParams + FrameLayout + Protocol>(name: &str) {
assert_eq!(P::N_DATA, 120, "{name} N_DATA");
assert_eq!(P::N_SYNC, 40, "{name} N_SYNC");
assert_eq!(P::N_SYMBOLS, 160, "{name} N_SYMBOLS");
assert_eq!(P::GFSK_BT, 2.0, "{name} GFSK_BT");
assert_eq!(P::GFSK_HMOD, 1.0, "{name} GFSK_HMOD");
assert_eq!(P::GRAY_MAP, &[0, 1, 3, 2], "{name} GRAY_MAP");
assert_eq!(<P::Fec as FecCodec>::N, 240, "{name} Fec::N");
assert_eq!(<P::Fec as FecCodec>::K, 101, "{name} Fec::K");
assert_eq!(P::ID, ProtocolId::Fst4, "{name} ID");
match P::SYNC_MODE {
SyncMode::Block(blocks) => assert_eq!(blocks.len(), 5, "{name} sync blocks"),
SyncMode::Interleaved { .. } => panic!("{name}: FST4 must use Block sync"),
}
}
check::<Fst4s15>("Fst4s15");
check::<Fst4s30>("Fst4s30");
check::<Fst4s60>("Fst4s60");
check::<Fst4s120>("Fst4s120");
check::<Fst4s300>("Fst4s300");
}
#[test]
fn downsample_and_gfsk_configs_match_submode_constants() {
use crate::core::dsp::downsample::DownsampleCfg;
use crate::core::dsp::gfsk::GfskCfg;
fn check_downsample<P: ModulationParams + FrameLayout>(name: &str, cfg: &DownsampleCfg) {
let ndown = P::NDOWN as usize;
assert_eq!(
cfg.fft1_size % ndown,
0,
"{name}: fft1_size {} not divisible by NDOWN {ndown}",
cfg.fft1_size
);
assert_eq!(
cfg.fft1_size / ndown,
cfg.fft2_size,
"{name}: fft1_size/NDOWN != fft2_size"
);
let nmax = (P::T_SLOT_S * 12_000.0).round() as usize;
assert!(
cfg.fft1_size >= nmax,
"{name}: fft1_size {} < nmax {nmax} (slot won't fit)",
cfg.fft1_size
);
assert!(
(cfg.tone_spacing_hz - P::TONE_SPACING_HZ).abs() < 1e-3,
"{name}: cfg.tone_spacing_hz {} != ModulationParams::TONE_SPACING_HZ {}",
cfg.tone_spacing_hz,
P::TONE_SPACING_HZ
);
}
fn check_gfsk<P: ModulationParams>(name: &str, cfg: &GfskCfg) {
assert_eq!(
cfg.samples_per_symbol as u32,
P::NSPS,
"{name}: gfsk samples_per_symbol != NSPS"
);
assert_eq!(cfg.bt, P::GFSK_BT, "{name}: gfsk bt != GFSK_BT");
assert_eq!(cfg.hmod, P::GFSK_HMOD, "{name}: gfsk hmod != GFSK_HMOD");
assert_eq!(
cfg.ramp_samples as u32,
P::NSPS / 8,
"{name}: gfsk ramp_samples != NSPS/8"
);
}
check_downsample::<Fst4s15>("Fst4s15", &decode::FST4_15_DOWNSAMPLE);
check_downsample::<Fst4s30>("Fst4s30", &decode::FST4_30_DOWNSAMPLE);
check_downsample::<Fst4s60>("Fst4s60", &decode::FST4_60A_DOWNSAMPLE);
check_downsample::<Fst4s120>("Fst4s120", &decode::FST4_120_DOWNSAMPLE);
check_downsample::<Fst4s300>("Fst4s300", &decode::FST4_300_DOWNSAMPLE);
check_gfsk::<Fst4s15>("Fst4s15", &encode::FST4_15_GFSK);
check_gfsk::<Fst4s30>("Fst4s30", &encode::FST4_30_GFSK);
check_gfsk::<Fst4s60>("Fst4s60", &encode::FST4_60A_GFSK);
check_gfsk::<Fst4s120>("Fst4s120", &encode::FST4_120_GFSK);
check_gfsk::<Fst4s300>("Fst4s300", &encode::FST4_300_GFSK);
}
}