mfsk-core 0.7.4

Pure-Rust WSJT-family decoders + synthesisers (FT8 FT4 FST4 WSPR JT9 JT65 Q65) behind a zero-cost Protocol trait. Host (rustfft) or no_std embedded (ESP32-S3, RP2350, Cortex-M) via a pluggable FFT backend; fixed-point hot path for FPU-less MCUs. Ships with embedded-poc/m5stack-s3-app, a working M5StickS3 FT8 controller (LCD UI, BLE CI-V to IC-705, acoustic mic, QSO FSM) decoding real on-air signals in ~1.2 s post-SlotEnd on Xtensa LX7.
Documentation
//! FT8 protocol parameters (derived from WSJT-X ft8_params.f90)

/// LDPC (174,91): information bits = 77 msg + 14 CRC
pub const KK: usize = 91;
/// Data symbols
pub const ND: usize = 58;
/// Sync symbols (3 Costas 7×7 arrays)
pub const NS: usize = 21;
/// Total channel symbols
pub const NN: usize = NS + ND; // 79
/// Samples per symbol at 12000 S/s
pub const NSPS: usize = 1920;
/// Samples in a full 15-second waveform
pub const NZ: usize = NSPS * NN; // 151,680
/// Samples in the receive buffer (15 s × 12000 S/s)
pub const NMAX: usize = 15 * 12000; // 180,000
/// FFT size for symbol spectra (2 × NSPS)
pub const NFFT1: usize = 2 * NSPS; // 3840
pub const NH1: usize = NFFT1 / 2; // 1920
/// Rough time-sync step size. WSJT-X reference uses 1/4-symbol
/// (= NSPS/4 = 480) which gives `coarse_sync` 2× the time-axis
/// resolution but doubles spec memory + stage-1 FFT count vs the
/// `nstep-half` build (NSPS/2 = 960). Embedded targets enable
/// `nstep-half` to fit the LX6/LX7 budget; host stays on NSPS/4.
#[cfg(not(feature = "nstep-half"))]
pub const NSTEP: usize = NSPS / 4; // 480
#[cfg(feature = "nstep-half")]
pub const NSTEP: usize = NSPS / 2; // 960
/// Number of symbol spectra at the active step size.
pub const NHSYM: usize = NMAX / NSTEP - 3;

/// Downsample factor: 12000 Hz → 200 Hz
pub const NDOWN: usize = 60;
/// Downsampled sample rate (Hz)
pub const DS_RATE: usize = 12000 / NDOWN; // 200
/// Samples per symbol after downsampling
pub const DS_SPB: usize = NSPS / NDOWN; // 32
/// Downsampled buffer length
pub const NP2: usize = 3200;

/// LDPC codeword length
pub const LDPC_N: usize = 174;
/// LDPC information bits (message + CRC14)
pub const LDPC_K: usize = 91;
/// LDPC parity bits
pub const LDPC_M: usize = LDPC_N - LDPC_K; // 83

/// Message payload bits (before CRC)
pub const MSG_BITS: usize = 77;
/// CRC width
pub const CRC_BITS: usize = 14;

/// Costas array tone pattern
pub const COSTAS: [usize; 7] = [3, 1, 4, 0, 6, 5, 2];

/// Symbol offsets of the three Costas arrays within a frame
/// Positions: 0-6, 36-42, 72-78
pub const COSTAS_POS: [usize; 3] = [0, 36, 72];

/// Number of tones in FT8 (8-FSK)
pub const NTONES: usize = 8;

/// Symbol duration (seconds)
pub const SYMBOL_DT: f32 = NSPS as f32 / 12000.0; // 0.16 s

/// Time step at 1/4-symbol resolution (seconds)
pub const TSTEP: f32 = SYMBOL_DT / 4.0; // 0.04 s

/// Frequency resolution at NFFT1 (Hz/bin)
pub const DF: f32 = 12000.0 / NFFT1 as f32; // 3.125 Hz

/// Gray code map for 8-FSK (tone index → 3-bit Gray code)
pub const GRAYMAP: [usize; 8] = [0, 1, 3, 2, 5, 6, 4, 7];

/// LLR scale factor (empirically tuned in WSJT-X ft8b.f90)
pub const LLR_SCALE: f32 = 2.83;

/// Default maximum BP decoder iterations (= WSJT-X
/// `bpdecode174_91.f90` `maxiterations`). Runtime-tunable via
/// `crate::ft8::decode_block::DecodeTuning` / `_tuned` entry points
/// — embedded targets (LX6 / LX7) lower this to trade weak-signal
/// recall for post-SlotEnd time budget.
pub const DEFAULT_BP_MAX_ITER: u32 = 30;

/// Backwards-compat alias used by the legacy `decode.rs` path.
pub const BP_MAX_ITER: u32 = DEFAULT_BP_MAX_ITER;