// SPDX-FileCopyrightText: 2026 Stan Grams // // SPDX-License-Identifier: GPL-2.0-or-later /// FTx protocol variants. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FtxProtocol { Ft4, Ft8, #[cfg(feature = "ft2")] Ft2, } impl FtxProtocol { /// Symbol period in seconds. pub fn symbol_period(self) -> f32 { match self { Self::Ft8 => FT8_SYMBOL_PERIOD, Self::Ft4 => FT4_SYMBOL_PERIOD, #[cfg(feature = "ft2")] Self::Ft2 => FT2_SYMBOL_PERIOD, } } /// Slot time in seconds. pub fn slot_time(self) -> f32 { match self { Self::Ft8 => FT8_SLOT_TIME, Self::Ft4 => FT4_SLOT_TIME, #[cfg(feature = "ft2")] Self::Ft2 => FT2_SLOT_TIME, } } /// Whether this protocol uses FT4-style channel layout (FT4 and FT2). pub fn uses_ft4_layout(self) -> bool { #[cfg(feature = "ft2")] if matches!(self, Self::Ft2) { return true; } matches!(self, Self::Ft4) } /// Number of data symbols. pub fn nd(self) -> usize { if self.uses_ft4_layout() { FT4_ND } else { FT8_ND } } /// Total channel symbols. pub fn nn(self) -> usize { if self.uses_ft4_layout() { FT4_NN } else { FT8_NN } } /// Length of each sync group. pub fn sync_length(self) -> usize { if self.uses_ft4_layout() { FT4_LENGTH_SYNC } else { FT8_LENGTH_SYNC } } /// Number of sync groups. pub fn num_sync(self) -> usize { if self.uses_ft4_layout() { FT4_NUM_SYNC } else { FT8_NUM_SYNC } } /// Offset between sync groups. pub fn sync_offset(self) -> usize { if self.uses_ft4_layout() { FT4_SYNC_OFFSET } else { FT8_SYNC_OFFSET } } /// Number of FSK tones. pub fn num_tones(self) -> usize { if self.uses_ft4_layout() { 4 } else { 8 } } } // FT8 timing pub const FT8_SYMBOL_PERIOD: f32 = 0.160; pub const FT8_SLOT_TIME: f32 = 15.0; // FT4 timing pub const FT4_SYMBOL_PERIOD: f32 = 0.048; pub const FT4_SLOT_TIME: f32 = 7.5; // FT2 timing #[cfg(feature = "ft2")] pub const FT2_SYMBOL_PERIOD: f32 = 0.024; #[cfg(feature = "ft2")] pub const FT2_SLOT_TIME: f32 = 3.75; // FT8 symbol counts pub const FT8_ND: usize = 58; pub const FT8_NN: usize = 79; pub const FT8_LENGTH_SYNC: usize = 7; pub const FT8_NUM_SYNC: usize = 3; pub const FT8_SYNC_OFFSET: usize = 36; // FT4 symbol counts pub const FT4_ND: usize = 87; pub const FT4_NR: usize = 2; pub const FT4_NN: usize = 105; pub const FT4_LENGTH_SYNC: usize = 4; pub const FT4_NUM_SYNC: usize = 4; pub const FT4_SYNC_OFFSET: usize = 33; // FT2 reuses FT4 layout #[cfg(feature = "ft2")] pub const FT2_ND: usize = FT4_ND; #[cfg(feature = "ft2")] pub const FT2_NR: usize = FT4_NR; #[cfg(feature = "ft2")] pub const FT2_NN: usize = FT4_NN; #[cfg(feature = "ft2")] pub const FT2_LENGTH_SYNC: usize = FT4_LENGTH_SYNC; #[cfg(feature = "ft2")] pub const FT2_NUM_SYNC: usize = FT4_NUM_SYNC; #[cfg(feature = "ft2")] pub const FT2_SYNC_OFFSET: usize = FT4_SYNC_OFFSET; // LDPC parameters pub const FTX_LDPC_N: usize = 174; pub const FTX_LDPC_K: usize = 91; pub const FTX_LDPC_M: usize = 83; pub const FTX_LDPC_N_BYTES: usize = FTX_LDPC_N.div_ceil(8); pub const FTX_LDPC_K_BYTES: usize = FTX_LDPC_K.div_ceil(8); // CRC parameters pub const FT8_CRC_POLYNOMIAL: u16 = 0x2757; pub const FT8_CRC_WIDTH: u32 = 14; // Message parameters pub const FTX_PAYLOAD_LENGTH_BYTES: usize = 10; pub const FTX_MAX_MESSAGE_LENGTH: usize = 35; #[cfg(test)] mod tests { use super::*; #[test] fn protocol_timing() { assert!((FtxProtocol::Ft8.symbol_period() - 0.160).abs() < 1e-6); assert!((FtxProtocol::Ft4.symbol_period() - 0.048).abs() < 1e-6); #[cfg(feature = "ft2")] assert!((FtxProtocol::Ft2.symbol_period() - 0.024).abs() < 1e-6); } #[test] fn ft4_layout() { assert!(FtxProtocol::Ft4.uses_ft4_layout()); #[cfg(feature = "ft2")] assert!(FtxProtocol::Ft2.uses_ft4_layout()); assert!(!FtxProtocol::Ft8.uses_ft4_layout()); } #[test] fn symbol_counts() { assert_eq!(FtxProtocol::Ft8.nn(), 79); assert_eq!(FtxProtocol::Ft4.nn(), 105); #[cfg(feature = "ft2")] assert_eq!(FtxProtocol::Ft2.nn(), 105); } }