#[allow(unused_imports)]
use crate::{FecCodec, FrameLayout, MessageCodec, ModulationParams, Protocol};
use crate::ProtocolId;
pub mod caps {
pub const DECODE_HANDLE: u32 = 1 << 0;
pub const SNIPER: u32 = 1 << 1;
pub const AP_NARROW: u32 = 1 << 2;
pub const AP_WIDEBAND: u32 = 1 << 3;
pub const SIC_ROUNDS: u32 = 1 << 4;
pub const SIC_EARLY: u32 = 1 << 5;
pub const OSD: u32 = 1 << 6;
pub const EQ_MODE: u32 = 1 << 7;
pub const STRICTNESS: u32 = 1 << 8;
pub const BUDGET: u32 = 1 << 9;
pub const KNOWN_FILTER: u32 = 1 << 10;
pub const KNOWN_SUBTRACT: u32 = 1 << 11;
pub const FFT_CACHE: u32 = 1 << 12;
pub const ON_RESULT: u32 = 1 << 13;
pub const ENCODE: u32 = 1 << 14;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum SyncScale {
CostasAbsolute = 0,
BaselineNormalised = 1,
}
#[derive(Clone, Copy, Debug)]
pub struct DecodeDefaults {
pub freq_min_hz: f32,
pub freq_max_hz: f32,
pub sync_min: f32,
pub max_cand: u32,
}
#[derive(Clone, Copy, Debug)]
pub struct DecodeProfile {
pub caps: u32,
pub defaults: DecodeDefaults,
pub sync_scale: SyncScale,
pub sniper_max_cand_cap: Option<u32>,
}
#[derive(Clone, Copy, Debug)]
pub struct ProtocolMeta {
pub id: ProtocolId,
pub name: &'static str,
pub ntones: u32,
pub bits_per_symbol: u32,
pub nsps: u32,
pub symbol_dt: f32,
pub tone_spacing_hz: f32,
pub gfsk_bt: f32,
pub gfsk_hmod: f32,
pub n_data: u32,
pub n_sync: u32,
pub n_symbols: u32,
pub t_slot_s: f32,
pub fec_k: usize,
pub fec_n: usize,
pub payload_bits: u32,
pub tx_start_offset_s: f32,
pub slot_samples_12k: u32,
pub decode_fft1_size: u32,
pub profile: DecodeProfile,
}
#[allow(unused_macros)] macro_rules! protocol_meta {
($name:literal, $ty:ty, $profile:expr) => {
ProtocolMeta {
id: <$ty as Protocol>::ID,
name: $name,
ntones: <$ty as ModulationParams>::NTONES,
bits_per_symbol: <$ty as ModulationParams>::BITS_PER_SYMBOL,
nsps: <$ty as ModulationParams>::NSPS,
symbol_dt: <$ty as ModulationParams>::SYMBOL_DT,
tone_spacing_hz: <$ty as ModulationParams>::TONE_SPACING_HZ,
gfsk_bt: <$ty as ModulationParams>::GFSK_BT,
gfsk_hmod: <$ty as ModulationParams>::GFSK_HMOD,
n_data: <$ty as FrameLayout>::N_DATA,
n_sync: <$ty as FrameLayout>::N_SYNC,
n_symbols: <$ty as FrameLayout>::N_SYMBOLS,
t_slot_s: <$ty as FrameLayout>::T_SLOT_S,
fec_k: <<$ty as Protocol>::Fec as FecCodec>::K,
fec_n: <<$ty as Protocol>::Fec as FecCodec>::N,
payload_bits: <<$ty as Protocol>::Msg as MessageCodec>::PAYLOAD_BITS,
tx_start_offset_s: <$ty as FrameLayout>::TX_START_OFFSET_S,
slot_samples_12k: (<$ty as FrameLayout>::T_SLOT_S * 12_000.0) as u32,
decode_fft1_size: <$ty as Protocol>::DECODE_FFT1_SIZE,
profile: $profile,
}
};
}
#[allow(dead_code)] const FT8_PROFILE: DecodeProfile = DecodeProfile {
caps: caps::DECODE_HANDLE
| caps::SNIPER
| caps::AP_NARROW
| caps::AP_WIDEBAND
| caps::SIC_ROUNDS
| caps::SIC_EARLY
| caps::OSD
| caps::EQ_MODE
| caps::STRICTNESS
| caps::BUDGET
| caps::KNOWN_FILTER
| caps::KNOWN_SUBTRACT
| caps::FFT_CACHE
| caps::ON_RESULT
| caps::ENCODE,
defaults: DecodeDefaults {
freq_min_hz: 100.0,
freq_max_hz: 3000.0,
sync_min: 0.8,
max_cand: 60,
},
sync_scale: SyncScale::CostasAbsolute,
sniper_max_cand_cap: None,
};
#[allow(dead_code)]
const FT4_PROFILE: DecodeProfile = DecodeProfile {
caps: caps::DECODE_HANDLE
| caps::AP_WIDEBAND
| caps::SIC_ROUNDS
| caps::OSD
| caps::EQ_MODE
| caps::STRICTNESS
| caps::BUDGET
| caps::KNOWN_FILTER
| caps::FFT_CACHE
| caps::ON_RESULT
| caps::ENCODE,
defaults: DecodeDefaults {
freq_min_hz: 300.0,
freq_max_hz: 2700.0,
sync_min: 1.2,
max_cand: 100,
},
sync_scale: SyncScale::BaselineNormalised,
sniper_max_cand_cap: Some(15),
};
#[allow(dead_code)]
const FST4_PROFILE: DecodeProfile = DecodeProfile {
caps: caps::DECODE_HANDLE
| caps::AP_WIDEBAND
| caps::OSD
| caps::EQ_MODE
| caps::BUDGET
| caps::KNOWN_FILTER
| caps::FFT_CACHE
| caps::ON_RESULT
| caps::ENCODE,
defaults: DecodeDefaults {
freq_min_hz: 100.0,
freq_max_hz: 3000.0,
sync_min: 0.8,
max_cand: 50,
},
sync_scale: SyncScale::CostasAbsolute,
sniper_max_cand_cap: None,
};
#[allow(dead_code)]
const Q65_PROFILE: DecodeProfile = DecodeProfile {
caps: caps::AP_NARROW | caps::ON_RESULT | caps::ENCODE,
defaults: DecodeDefaults {
freq_min_hz: 200.0,
freq_max_hz: 3000.0,
sync_min: 0.05,
max_cand: 32,
},
sync_scale: SyncScale::CostasAbsolute,
sniper_max_cand_cap: None,
};
#[allow(dead_code)]
const WSPR_PROFILE: DecodeProfile = DecodeProfile {
caps: caps::ON_RESULT | caps::ENCODE,
defaults: DecodeDefaults {
freq_min_hz: 1400.0,
freq_max_hz: 1600.0,
sync_min: 0.0,
max_cand: 0,
},
sync_scale: SyncScale::CostasAbsolute,
sniper_max_cand_cap: None,
};
#[allow(dead_code)]
const JT_PROFILE: DecodeProfile = DecodeProfile {
caps: caps::ON_RESULT | caps::ENCODE,
defaults: DecodeDefaults {
freq_min_hz: 0.0,
freq_max_hz: 0.0,
sync_min: 0.0,
max_cand: 0,
},
sync_scale: SyncScale::CostasAbsolute,
sniper_max_cand_cap: None,
};
#[allow(dead_code)]
const UV_PROFILE: DecodeProfile = DecodeProfile {
caps: caps::ENCODE,
defaults: DecodeDefaults {
freq_min_hz: 0.0,
freq_max_hz: 0.0,
sync_min: 0.0,
max_cand: 0,
},
sync_scale: SyncScale::CostasAbsolute,
sniper_max_cand_cap: None,
};
pub static PROTOCOLS: &[ProtocolMeta] = &[
#[cfg(feature = "ft8")]
protocol_meta!("FT8", crate::Ft8, FT8_PROFILE),
#[cfg(feature = "ft4")]
protocol_meta!("FT4", crate::Ft4, FT4_PROFILE),
#[cfg(feature = "fst4")]
protocol_meta!("FST4-60A", crate::Fst4s60, FST4_PROFILE),
#[cfg(feature = "fst4")]
protocol_meta!("FST4-15", crate::fst4::Fst4s15, FST4_PROFILE),
#[cfg(feature = "fst4")]
protocol_meta!("FST4-30", crate::fst4::Fst4s30, FST4_PROFILE),
#[cfg(feature = "fst4")]
protocol_meta!("FST4-120", crate::fst4::Fst4s120, FST4_PROFILE),
#[cfg(feature = "fst4")]
protocol_meta!("FST4-300", crate::fst4::Fst4s300, FST4_PROFILE),
#[cfg(feature = "wspr")]
protocol_meta!("WSPR", crate::Wspr, WSPR_PROFILE),
#[cfg(feature = "jt9")]
protocol_meta!("JT9", crate::Jt9, JT_PROFILE),
#[cfg(feature = "jt65")]
protocol_meta!("JT65", crate::Jt65, JT_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-15A", crate::q65::Q65a15, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-30A", crate::q65::Q65a30, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-60A", crate::q65::Q65a60, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-60B", crate::q65::Q65b60, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-60C", crate::q65::Q65c60, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-60D", crate::q65::Q65d60, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-60E", crate::q65::Q65e60, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-120D", crate::q65::Q65d120, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-120E", crate::q65::Q65e120, Q65_PROFILE),
#[cfg(feature = "q65")]
protocol_meta!("Q65-300A", crate::q65::Q65a300, Q65_PROFILE),
#[cfg(feature = "uvpacket")]
protocol_meta!("UvRobust", crate::UvRobust, UV_PROFILE),
#[cfg(feature = "uvpacket")]
protocol_meta!("UvStandard", crate::UvStandard, UV_PROFILE),
#[cfg(feature = "uvpacket")]
protocol_meta!("UvUltraRobust", crate::UvUltraRobust, UV_PROFILE),
#[cfg(feature = "uvpacket")]
protocol_meta!("UvExpress", crate::UvExpress, UV_PROFILE),
];
pub fn by_id(id: ProtocolId) -> impl Iterator<Item = &'static ProtocolMeta> {
PROTOCOLS.iter().filter(move |p| p.id == id)
}
pub fn by_name(name: &str) -> Option<&'static ProtocolMeta> {
PROTOCOLS.iter().find(|p| p.name == name)
}
pub fn for_protocol_id(id: ProtocolId) -> Option<&'static ProtocolMeta> {
by_id(id).next()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_is_non_empty_in_default_build() {
assert!(!PROTOCOLS.is_empty());
}
#[test]
fn names_are_unique() {
let mut names: Vec<&str> = PROTOCOLS.iter().map(|p| p.name).collect();
names.sort_unstable();
let dedup_len = {
let mut v = names.clone();
v.dedup();
v.len()
};
assert_eq!(
dedup_len,
names.len(),
"duplicate protocol names in registry: {names:?}"
);
}
#[test]
fn by_name_round_trips() {
for p in PROTOCOLS {
let q = by_name(p.name).expect("by_name should find every registered name");
assert!(
std::ptr::eq(p, q),
"by_name returned a different entry for {}",
p.name
);
}
}
#[test]
fn by_name_returns_none_for_unknown() {
assert!(by_name("NotAProtocol-9000").is_none());
}
#[test]
fn by_id_yields_at_least_one_entry_for_each_distinct_id() {
let mut ids: Vec<ProtocolId> = PROTOCOLS.iter().map(|p| p.id).collect();
ids.sort_unstable_by_key(|id| *id as u8);
ids.dedup();
for id in ids {
assert!(
by_id(id).next().is_some(),
"by_id({id:?}) found no entries despite the id appearing in the registry"
);
}
}
#[cfg(feature = "q65")]
#[test]
fn q65_id_yields_all_ten_submodes() {
let q65_entries: Vec<&ProtocolMeta> = by_id(ProtocolId::Q65).collect();
assert_eq!(
q65_entries.len(),
10,
"expected ten Q65 sub-modes in the registry, got {}: {:?}",
q65_entries.len(),
q65_entries.iter().map(|p| p.name).collect::<Vec<_>>()
);
let names: Vec<&str> = q65_entries.iter().map(|p| p.name).collect();
for expected in &[
"Q65-15A", "Q65-30A", "Q65-60A", "Q65-60B", "Q65-60C", "Q65-60D", "Q65-60E",
"Q65-120D", "Q65-120E", "Q65-300A",
] {
assert!(
names.contains(expected),
"Q65 registry missing sub-mode {expected}; have {names:?}"
);
}
}
#[cfg(feature = "fst4")]
#[test]
fn fst4_id_yields_all_five_submodes() {
let fst4_entries: Vec<&ProtocolMeta> = by_id(ProtocolId::Fst4).collect();
assert_eq!(
fst4_entries.len(),
5,
"expected five FST4 sub-modes in the registry, got {}: {:?}",
fst4_entries.len(),
fst4_entries.iter().map(|p| p.name).collect::<Vec<_>>()
);
let names: Vec<&str> = fst4_entries.iter().map(|p| p.name).collect();
for expected in &["FST4-15", "FST4-30", "FST4-60A", "FST4-120", "FST4-300"] {
assert!(
names.contains(expected),
"FST4 registry missing sub-mode {expected}; have {names:?}"
);
}
}
#[cfg(feature = "fst4")]
#[test]
fn for_protocol_id_defaults_to_fst4_60a() {
let meta = for_protocol_id(ProtocolId::Fst4).expect("fst4 feature is on");
assert_eq!(meta.name, "FST4-60A");
}
}