use core::fmt;
use crate::geo::{GridPrecision, MaidenheadGrid};
use crate::types::{SampleRate, sine_at};
#[cfg(feature = "std")]
mod rx;
#[cfg(feature = "std")]
pub use rx::{WsprDecode, WsprDecoder, WsprDecoderConfig, WsprRxError};
fn sine_at_f32(phase: u32) -> f32 {
use crate::types::{SINE_I16, TABLE_BITS, TABLE_MASK};
let idx = (phase >> (32 - TABLE_BITS)) as usize & TABLE_MASK;
let frac_bits = phase & ((1 << (32 - TABLE_BITS)) - 1);
let frac = frac_bits as f32 / (1u32 << (32 - TABLE_BITS)) as f32;
let a = SINE_I16.get(idx).copied().unwrap_or(0) as f32;
let b = SINE_I16.get((idx + 1) & TABLE_MASK).copied().unwrap_or(0) as f32;
(a + (b - a) * frac) / 32_767.0
}
pub const SYMBOL_COUNT: usize = 162;
pub const DATA_BITS: usize = 50;
pub const PACKED_LEN: usize = 11;
pub const POLY_A: u32 = 0xF2D0_5351;
pub const POLY_B: u32 = 0xE461_3C47;
pub const TONE_SPACING_NUM: u32 = 12_000;
pub const TONE_SPACING_DEN: u32 = 8_192;
#[rustfmt::skip]
pub const SYNC_VECTOR: [u8; SYMBOL_COUNT] = [
1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WsprError {
CallsignCompound,
CallsignLength {
len: usize,
},
CallsignChar {
ch: char,
index: usize,
},
CallsignShape,
GridLength {
len: usize,
},
PowerOutOfRange {
got: u8,
},
PowerNotStandard {
got: u8,
},
UnpackInvalid,
SampleRateInexact {
got: u32,
},
ToneOutOfRange {
base_hz: u32,
sample_rate: u32,
},
}
impl fmt::Display for WsprError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::CallsignCompound => write!(
f,
"callsign contains '/': compound calls do not fit a type-1 WSPR message"
),
Self::CallsignLength { len } => write!(
f,
"callsign length {len} is invalid: must be 1..=6 characters"
),
Self::CallsignChar { ch, index } => write!(
f,
"callsign character {ch:?} is invalid at aligned position {index}"
),
Self::CallsignShape => write!(
f,
"callsign cannot be aligned to the type-1 shape (third character must be a digit)"
),
Self::GridLength { len } => write!(
f,
"grid locator length {len} is invalid: must be exactly 4 characters"
),
Self::PowerOutOfRange { got } => {
write!(f, "power {got} dBm is out of range: must be within 0..=60")
}
Self::PowerNotStandard { got } => write!(
f,
"power {got} dBm is not a standard WSPR value: must end in 0, 3 or 7"
),
Self::UnpackInvalid => write!(
f,
"packed 50-bit payload does not decode to a valid type-1 message"
),
Self::SampleRateInexact { got } => write!(
f,
"sample rate {got} Hz cannot time WSPR symbols exactly: must be a multiple of 375 Hz"
),
Self::ToneOutOfRange {
base_hz,
sample_rate,
} => write!(
f,
"base frequency {base_hz} Hz is invalid at {sample_rate} Hz: tones must be nonzero and below Nyquist"
),
}
}
}
impl core::error::Error for WsprError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WsprMessage {
callsign: [u8; 6],
grid: MaidenheadGrid,
power_dbm: u8,
}
impl WsprMessage {
pub fn new(callsign: &str, grid: MaidenheadGrid, power_dbm: u8) -> Result<Self, WsprError> {
let callsign = normalize_callsign(callsign)?;
if !matches!(grid.precision(), GridPrecision::Square) {
return Err(WsprError::GridLength {
len: grid.precision().characters(),
});
}
if power_dbm > 60 {
return Err(WsprError::PowerOutOfRange { got: power_dbm });
}
if !matches!(power_dbm % 10, 0 | 3 | 7) {
return Err(WsprError::PowerNotStandard { got: power_dbm });
}
Ok(Self {
callsign,
grid,
power_dbm,
})
}
#[must_use]
pub fn callsign(&self) -> &[u8; 6] {
&self.callsign
}
#[must_use]
pub fn grid(&self) -> MaidenheadGrid {
self.grid
}
#[must_use]
pub fn power_dbm(&self) -> u8 {
self.power_dbm
}
#[must_use]
pub fn pack(&self) -> [u8; PACKED_LEN] {
let n = pack_callsign(&self.callsign);
let m = pack_grid(self.grid) * 128 + u32::from(self.power_dbm) + 64;
let mut bytes = [0u8; PACKED_LEN];
let bits: u64 = (u64::from(n) << 22) | u64::from(m);
let left = bits << (64 - 50); for (i, byte) in bytes.iter_mut().enumerate().take(7) {
*byte = (left >> (56 - 8 * i)) as u8;
}
bytes
}
#[must_use]
pub fn channel_symbols(&self) -> [u8; SYMBOL_COUNT] {
let coded = convolutional_encode(&self.pack());
let data = interleave(&coded);
let mut symbols = [0u8; SYMBOL_COUNT];
for i in 0..SYMBOL_COUNT {
symbols[i] = SYNC_VECTOR[i] + 2 * data[i];
}
symbols
}
pub fn unpack(packed: &[u8; PACKED_LEN]) -> Result<Self, WsprError> {
if packed[7..].iter().any(|&b| b != 0) {
return Err(WsprError::UnpackInvalid);
}
let mut left: u64 = 0;
for (i, &byte) in packed.iter().enumerate().take(7) {
left |= u64::from(byte) << (56 - 8 * i);
}
if left & ((1u64 << 14) - 1) != 0 {
return Err(WsprError::UnpackInvalid);
}
let bits = left >> 14;
let n = (bits >> 22) as u32;
let m = (bits & 0x3F_FFFF) as u32;
fn tail_char(v: u32) -> Result<u8, WsprError> {
match v {
0..=25 => Ok(b'A' + v as u8),
26 => Ok(b' '),
_ => Err(WsprError::UnpackInvalid),
}
}
fn head_char(v: u32) -> Result<u8, WsprError> {
match v {
0..=9 => Ok(b'0' + v as u8),
10..=35 => Ok(b'A' + (v - 10) as u8),
36 => Ok(b' '),
_ => Err(WsprError::UnpackInvalid),
}
}
let mut v = n;
let c6 = tail_char(v % 27)?;
v /= 27;
let c5 = tail_char(v % 27)?;
v /= 27;
let c4 = tail_char(v % 27)?;
v /= 27;
let c3 = b'0' + (v % 10) as u8;
v /= 10;
let c2 = head_char(v % 36)?;
v /= 36;
let c1 = head_char(v)?;
let aligned = [c1, c2, c3, c4, c5, c6];
let start = aligned.iter().position(|&c| c != b' ').unwrap_or(6);
let end = 6 - aligned.iter().rev().position(|&c| c != b' ').unwrap_or(6);
if start >= end {
return Err(WsprError::UnpackInvalid);
}
let call =
core::str::from_utf8(&aligned[start..end]).map_err(|_| WsprError::UnpackInvalid)?;
let power = m % 128;
if !(64..=124).contains(&power) {
return Err(WsprError::UnpackInvalid);
}
let power = (power - 64) as u8;
let g = m / 128;
if g >= 180 * 180 {
return Err(WsprError::UnpackInvalid);
}
let lat = g % 180;
let lon = 179 - g / 180;
let grid = [
b'A' + (lon / 10) as u8,
b'A' + (lat / 10) as u8,
b'0' + (lon % 10) as u8,
b'0' + (lat % 10) as u8,
];
let grid = MaidenheadGrid::from_bytes(&grid).map_err(|_| WsprError::UnpackInvalid)?;
Self::new(call, grid, power).map_err(|_| WsprError::UnpackInvalid)
}
}
fn char_value(c: u8) -> u32 {
match c {
b'0'..=b'9' => u32::from(c - b'0'),
b'A'..=b'Z' => u32::from(c - b'A') + 10,
_ => 36, }
}
fn normalize_callsign(call: &str) -> Result<[u8; 6], WsprError> {
let mut buf = [b' '; 6];
let mut len = 0usize;
for ch in call.chars() {
if ch == '/' {
return Err(WsprError::CallsignCompound);
}
let up = ch.to_ascii_uppercase();
if !(up.is_ascii_uppercase() || up.is_ascii_digit()) {
return Err(WsprError::CallsignChar { ch: up, index: len });
}
if len == 6 {
return Err(WsprError::CallsignLength {
len: call.chars().count(),
});
}
buf[len] = up as u8;
len += 1;
}
if len == 0 {
return Err(WsprError::CallsignLength { len: 0 });
}
if !buf[2].is_ascii_digit() {
if len >= 6 || !buf[1].is_ascii_digit() {
return Err(WsprError::CallsignShape);
}
buf.copy_within(0..5, 1);
buf[0] = b' ';
}
for (index, &c) in buf.iter().enumerate().skip(3) {
if !(c.is_ascii_uppercase() || c == b' ') {
return Err(WsprError::CallsignChar {
ch: c as char,
index,
});
}
}
Ok(buf)
}
fn pack_callsign(call: &[u8; 6]) -> u32 {
let mut n = char_value(call[0]);
n = n * 36 + char_value(call[1]);
n = n * 10 + char_value(call[2]);
for &c in &call[3..] {
n = n * 27 + (char_value(c) - 10);
}
n
}
fn pack_grid(grid: MaidenheadGrid) -> u32 {
let mut wire = [b'A', b'A', b'0', b'0'];
for (dst, &src) in wire.iter_mut().zip(grid.as_bytes()) {
*dst = src;
}
let lon_field = u32::from(wire[0] - b'A');
let lat_field = u32::from(wire[1] - b'A');
let lon_square = u32::from(wire[2] - b'0');
let lat_square = u32::from(wire[3] - b'0');
(179 - 10 * lon_field - lon_square) * 180 + 10 * lat_field + lat_square
}
#[must_use]
pub fn convolutional_encode(packed: &[u8; PACKED_LEN]) -> [u8; SYMBOL_COUNT] {
let mut out = [0u8; SYMBOL_COUNT];
let mut reg: u32 = 0;
for k in 0..SYMBOL_COUNT / 2 {
let bit = u32::from((packed[k / 8] >> (7 - k % 8)) & 1);
reg = (reg << 1) | bit;
out[2 * k] = ((reg & POLY_A).count_ones() & 1) as u8;
out[2 * k + 1] = ((reg & POLY_B).count_ones() & 1) as u8;
}
out
}
#[must_use]
pub fn interleave(coded: &[u8; SYMBOL_COUNT]) -> [u8; SYMBOL_COUNT] {
let mut out = [0u8; SYMBOL_COUNT];
let mut k = 0usize;
for i in 0..=255u8 {
let j = usize::from(i.reverse_bits());
if j < SYMBOL_COUNT {
out[j] = coded[k];
k += 1;
if k == SYMBOL_COUNT {
break;
}
}
}
out
}
pub fn deinterleave<T: Copy>(channel: &[T; SYMBOL_COUNT], out: &mut [T; SYMBOL_COUNT]) {
let mut k = 0usize;
for i in 0..=255u8 {
let j = usize::from(i.reverse_bits());
if j < SYMBOL_COUNT {
out[k] = channel[j];
k += 1;
if k == SYMBOL_COUNT {
break;
}
}
}
}
pub const FANO_NODE_CAP: u32 = 400_000;
pub const FANO_DELTA: i32 = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FanoError {
CapExceeded {
cap: u32,
},
}
impl fmt::Display for FanoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::CapExceeded { cap } => write!(
f,
"Fano search exhausted its node-visit cap ({cap}): input too noisy to decode"
),
}
}
}
impl core::error::Error for FanoError {}
pub fn fano_decode(
metrics: &[[i32; 2]; SYMBOL_COUNT],
delta: i32,
node_cap: u32,
) -> Result<[u8; PACKED_LEN], FanoError> {
const DEPTH: usize = SYMBOL_COUNT / 2; let branch = |reg: u32, k: usize, u: u32| -> i32 {
let r = (reg << 1) | u;
let c0 = ((r & POLY_A).count_ones() & 1) as usize;
let c1 = ((r & POLY_B).count_ones() & 1) as usize;
metrics[2 * k][c0] + metrics[2 * k + 1][c1]
};
let mut bits = [0u8; DEPTH]; let mut tried = [0u8; DEPTH]; let mut reg = [0u32; DEPTH + 1]; let mut cum = [0i32; DEPTH + 1]; let mut k = 0usize;
let mut t: i32 = 0;
let mut visits: u32 = 0;
loop {
visits += 1;
if visits > node_cap {
return Err(FanoError::CapExceeded { cap: node_cap });
}
let candidate = if k >= DATA_BITS {
(tried[k] == 0).then(|| (0u32, branch(reg[k], k, 0)))
} else {
let m0 = branch(reg[k], k, 0);
let m1 = branch(reg[k], k, 1);
let (best, worst) = if m1 > m0 {
((1u32, m1), (0u32, m0))
} else {
((0u32, m0), (1u32, m1))
};
match tried[k] {
0 => Some(best),
1 => Some(worst),
_ => None,
}
};
if let Some((u, m)) = candidate
&& cum[k] + m >= t
{
let new_m = cum[k] + m;
bits[k] = u as u8;
reg[k + 1] = (reg[k] << 1) | u;
cum[k + 1] = new_m;
if cum[k] < t + delta {
while new_m >= t + delta {
t += delta;
}
}
k += 1;
if k == DEPTH {
let mut packed = [0u8; PACKED_LEN];
for (i, &bit) in bits.iter().enumerate().take(DATA_BITS) {
packed[i / 8] |= bit << (7 - i % 8);
}
return Ok(packed);
}
tried[k] = 0;
continue;
}
loop {
visits += 1;
if visits > node_cap {
return Err(FanoError::CapExceeded { cap: node_cap });
}
let prev = if k == 0 { i32::MIN } else { cum[k - 1] };
if prev >= t {
k -= 1;
if tried[k] == 0 && k < DATA_BITS {
tried[k] = 1;
break;
}
} else {
t -= delta;
tried[k] = 0;
break;
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WsprConfig {
base_hz: u32,
sample_rate: SampleRate,
}
impl WsprConfig {
pub const fn new(base_hz: u32, sample_rate: SampleRate) -> Result<Self, WsprError> {
let sr = sample_rate.hz();
if !sr.is_multiple_of(375) {
return Err(WsprError::SampleRateInexact { got: sr });
}
if base_hz == 0 || (base_hz as u64) * 8_192 + 36_000 >= (sr as u64) * 4_096 {
return Err(WsprError::ToneOutOfRange {
base_hz,
sample_rate: sr,
});
}
Ok(Self {
base_hz,
sample_rate,
})
}
#[must_use]
pub const fn base_hz(self) -> u32 {
self.base_hz
}
#[must_use]
pub const fn sample_rate(self) -> SampleRate {
self.sample_rate
}
#[must_use]
pub const fn samples_per_symbol(self) -> u32 {
self.sample_rate.hz() / 375 * 256
}
}
#[derive(Debug, Clone)]
pub struct WsprModulator {
phase: u32,
inc: [u32; 4],
symbols: [u8; SYMBOL_COUNT],
symbol_idx: usize,
emitted_in_symbol: u32,
samples_per_symbol: u32,
}
impl WsprModulator {
#[must_use]
pub fn new(config: WsprConfig, symbols: [u8; SYMBOL_COUNT]) -> Self {
let sr = u128::from(config.sample_rate().hz());
let mut inc = [0u32; 4];
for (k, slot) in inc.iter_mut().enumerate() {
let num = (u128::from(config.base_hz()) * 8_192 + (k as u128) * 12_000) << 32;
let den = 8_192 * sr;
*slot = ((num + den / 2) / den) as u32;
}
Self {
phase: 0,
inc,
symbols,
symbol_idx: 0,
emitted_in_symbol: 0,
samples_per_symbol: config.samples_per_symbol(),
}
}
#[must_use]
pub fn for_message(config: WsprConfig, message: &WsprMessage) -> Self {
Self::new(config, message.channel_symbols())
}
#[must_use]
pub fn total_samples(&self) -> u64 {
SYMBOL_COUNT as u64 * u64::from(self.samples_per_symbol)
}
fn advance(&mut self) {
let inc = self.inc[usize::from(self.symbols[self.symbol_idx] & 3)];
self.phase = self.phase.wrapping_add(inc);
self.emitted_in_symbol += 1;
if self.emitted_in_symbol == self.samples_per_symbol {
self.emitted_in_symbol = 0;
self.symbol_idx += 1;
}
}
pub fn next_i16(&mut self) -> Option<i16> {
if self.symbol_idx >= SYMBOL_COUNT {
return None;
}
let sample = sine_at(self.phase);
self.advance();
Some(sample)
}
pub fn next_f32(&mut self) -> Option<f32> {
if self.symbol_idx >= SYMBOL_COUNT {
return None;
}
let sample = sine_at_f32(self.phase);
self.advance();
Some(sample)
}
pub fn fill_i16(&mut self, buf: &mut [i16]) -> usize {
let mut written = 0;
for slot in buf.iter_mut() {
match self.next_i16() {
Some(s) => {
*slot = s;
written += 1;
}
None => break,
}
}
written
}
pub fn fill_f32(&mut self, buf: &mut [f32]) -> usize {
let mut written = 0;
for slot in buf.iter_mut() {
match self.next_f32() {
Some(s) => {
*slot = s;
written += 1;
}
None => break,
}
}
written
}
}
impl Iterator for WsprModulator {
type Item = i16;
fn next(&mut self) -> Option<i16> {
self.next_i16()
}
}