use core::fmt;
const POWERS: &[i32] = &[
0, 3, 7, 10, 13, 17, 20, 23, 27, 30, 33, 37, 40, 43, 47, 50, 53, 57, 60,
];
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum WsprMessage {
Type1 {
callsign: String,
grid: String,
power_dbm: i32,
},
Type2 {
callsign: String,
power_dbm: i32,
},
Type3 {
callsign_hash: u32,
grid6: String,
power_dbm: i32,
},
}
impl fmt::Display for WsprMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
WsprMessage::Type1 {
callsign,
grid,
power_dbm,
} => write!(f, "{} {} {}", callsign, grid, power_dbm),
WsprMessage::Type2 {
callsign,
power_dbm,
} => write!(f, "{} {}", callsign, power_dbm),
WsprMessage::Type3 {
callsign_hash,
grid6,
power_dbm,
} => write!(f, "<#{:05x}> {} {}", callsign_hash, grid6, power_dbm),
}
}
}
const CHAR37: &[u8; 37] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
fn callsign_char_code(ch: u8) -> Option<u8> {
match ch {
b'0'..=b'9' => Some(ch - b'0'),
b'A'..=b'Z' => Some(ch - b'A' + 10),
b' ' => Some(36),
_ => None,
}
}
fn locator_char_code(ch: u8) -> Option<u8> {
match ch {
b'0'..=b'9' => Some(ch - b'0'),
b'A'..=b'R' => Some(ch - b'A'),
b' ' => Some(36),
_ => None,
}
}
pub fn pack50(n1: u32, n2: u32) -> [u8; 7] {
[
((n1 >> 20) & 0xff) as u8,
((n1 >> 12) & 0xff) as u8,
((n1 >> 4) & 0xff) as u8,
(((n1 & 0x0f) << 4) | ((n2 >> 18) & 0x0f)) as u8,
((n2 >> 10) & 0xff) as u8,
((n2 >> 2) & 0xff) as u8,
(((n2 & 0x03) << 6) & 0xff) as u8,
]
}
pub fn unpack50(data: &[u8; 7]) -> (u32, u32) {
let mut n1: u32 = (data[0] as u32) << 20;
n1 |= (data[1] as u32) << 12;
n1 |= (data[2] as u32) << 4;
n1 |= ((data[3] >> 4) & 0x0f) as u32;
let mut n2: u32 = ((data[3] & 0x0f) as u32) << 18;
n2 |= (data[4] as u32) << 10;
n2 |= (data[5] as u32) << 2;
n2 |= ((data[6] >> 6) & 0x03) as u32;
(n1, n2)
}
pub fn pack_call(callsign: &str) -> Option<u32> {
let bytes = callsign.as_bytes();
if bytes.len() > 6 || bytes.is_empty() {
return None;
}
let mut call6 = [b' '; 6];
if bytes.len() >= 3 && bytes[2].is_ascii_digit() {
for (i, &b) in bytes.iter().enumerate() {
call6[i] = b;
}
} else if bytes.len() >= 2 && bytes[1].is_ascii_digit() {
for (i, &b) in bytes.iter().enumerate() {
call6[i + 1] = b;
}
} else {
return None;
}
let codes: [u8; 6] = {
let mut c = [0u8; 6];
for i in 0..6 {
c[i] = callsign_char_code(call6[i])?;
}
c
};
let mut n: u32 = codes[0] as u32;
n = n * 36 + codes[1] as u32;
n = n * 10 + codes[2] as u32;
n = n * 27 + (codes[3].wrapping_sub(10)) as u32;
n = n * 27 + (codes[4].wrapping_sub(10)) as u32;
n = n * 27 + (codes[5].wrapping_sub(10)) as u32;
Some(n)
}
pub fn unpack_call(ncall: u32) -> Option<String> {
if ncall >= 262_177_560 {
return None;
}
let mut n = ncall;
let mut tmp = [b' '; 6];
let i = (n % 27 + 10) as usize;
tmp[5] = CHAR37[i];
n /= 27;
let i = (n % 27 + 10) as usize;
tmp[4] = CHAR37[i];
n /= 27;
let i = (n % 27 + 10) as usize;
tmp[3] = CHAR37[i];
n /= 27;
let i = (n % 10) as usize;
tmp[2] = CHAR37[i];
n /= 10;
let i = (n % 36) as usize;
tmp[1] = CHAR37[i];
n /= 36;
tmp[0] = CHAR37[n as usize];
let s = core::str::from_utf8(&tmp).ok()?;
Some(s.trim().to_string())
}
pub fn pack_grid4_power(grid: &str, power_dbm: i32) -> Option<u32> {
let bytes = grid.as_bytes();
if bytes.len() != 4 {
return None;
}
let g0 = locator_char_code(bytes[0])? as u32;
let g1 = locator_char_code(bytes[1])? as u32;
let g2 = locator_char_code(bytes[2])? as u32;
let g3 = locator_char_code(bytes[3])? as u32;
let m = (179 - 10 * g0 - g2) * 180 + 10 * g1 + g3;
Some(m * 128 + (power_dbm as u32) + 64)
}
pub fn unpack_grid(ngrid_full: u32) -> Option<(String, i32)> {
let ntype = (ngrid_full & 127) as i32 - 64;
let ngrid = ngrid_full >> 7;
if ngrid >= 32_400 {
return None;
}
let dlat = (ngrid % 180) as i32 - 90;
let mut dlong = (ngrid / 180) as i32 * 2 - 180 + 2;
if dlong < -180 {
dlong += 360;
}
if dlong > 180 {
dlong += 360;
}
let nlong = (60.0 * (180.0 - dlong as f32) / 5.0) as i32;
let ln1 = nlong / 240;
let ln2 = (nlong - 240 * ln1) / 24;
let nlat = (60.0 * (dlat + 90) as f32 / 2.5) as i32;
let la1 = nlat / 240;
let la2 = (nlat - 240 * la1) / 24;
let mut grid = [b'0'; 4];
grid[0] = CHAR37[(10 + ln1) as usize];
grid[2] = CHAR37[ln2 as usize];
grid[1] = CHAR37[(10 + la1) as usize];
grid[3] = CHAR37[la2 as usize];
Some((core::str::from_utf8(&grid).ok()?.to_string(), ntype))
}
pub fn pack_type1(callsign: &str, grid: &str, power_dbm: i32) -> Option<[u8; 50]> {
if !POWERS.contains(&power_dbm) {
return None;
}
let n1 = pack_call(callsign)?;
let n2 = pack_grid4_power(grid, power_dbm)?;
let bytes = pack50(n1, n2);
let mut bits = [0u8; 50];
for i in 0..50 {
let byte = bytes[i / 8];
bits[i] = (byte >> (7 - (i % 8))) & 1;
}
Some(bits)
}
fn apply_prefix(nprefix: u32, base_call: &str) -> Option<String> {
const A37: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
if nprefix < 60_000 {
let mut n = nprefix;
let mut pfx = [b' '; 3];
for i in (0..3).rev() {
let nc = (n % 37) as usize;
pfx[i] = A37[nc];
n /= 37;
}
let start = pfx.iter().position(|&b| b != b' ')?;
let pfx_str = core::str::from_utf8(&pfx[start..]).ok()?;
Some(format!("{}/{}", pfx_str, base_call))
} else {
let nc = nprefix - 60_000;
if nc <= 9 {
Some(format!("{}/{}", base_call, (b'0' + nc as u8) as char))
} else if nc <= 35 {
Some(format!(
"{}/{}",
base_call,
(b'A' + (nc - 10) as u8) as char
))
} else if nc <= 125 {
let d1 = (nc - 26) / 10;
let d2 = (nc - 26) % 10;
Some(format!(
"{}/{}{}",
base_call,
(b'0' + d1 as u8) as char,
(b'0' + d2 as u8) as char
))
} else {
None
}
}
}
pub fn unpack(bits: &[u8; 50]) -> Option<WsprMessage> {
let mut data = [0u8; 7];
for i in 0..50 {
if bits[i] & 1 != 0 {
data[i / 8] |= 1 << (7 - (i % 8));
}
}
let (n1, n2) = unpack50(&data);
let (maybe_grid, ntype) = unpack_grid(n2).unzip();
if let Some(t) = ntype
&& t < 0
{
let power_dbm = -(t + 1);
let pseudo_call = unpack_call(n1).unwrap_or_default();
let mut grid6 = String::new();
if pseudo_call.len() == 6 {
let bytes = pseudo_call.as_bytes();
grid6.push(bytes[5] as char); grid6.push_str(core::str::from_utf8(&bytes[..5]).ok()?);
}
let hash = n2 >> 7;
return Some(WsprMessage::Type3 {
callsign_hash: hash,
grid6,
power_dbm,
});
}
let ntype_val = ntype?;
let grid = maybe_grid?;
if (0..=62).contains(&ntype_val) {
let nu = ntype_val % 10;
if nu == 0 || nu == 3 || nu == 7 {
let callsign = unpack_call(n1)?;
return Some(WsprMessage::Type1 {
callsign,
grid,
power_dbm: ntype_val,
});
}
let nadd = if nu > 7 {
nu - 7
} else if nu > 3 {
nu - 3
} else {
nu
};
let n3 = (n2 >> 7) + 32_768 * (nadd as u32 - 1);
let base_call = unpack_call(n1)?;
let full_call = apply_prefix(n3, &base_call)?;
let power_dbm = ntype_val - nadd;
let pu = power_dbm.rem_euclid(10);
if pu != 0 && pu != 3 && pu != 7 {
return None;
}
return Some(WsprMessage::Type2 {
callsign: full_call,
power_dbm,
});
}
None
}
use crate::core::{DecodeContext, MessageCodec, MessageFields};
#[derive(Copy, Clone, Debug, Default)]
pub struct Wspr50Message;
impl MessageCodec for Wspr50Message {
type Unpacked = WsprMessage;
const PAYLOAD_BITS: u32 = 50;
const CRC_BITS: u32 = 0;
fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
let call = fields.call1.as_deref()?;
let grid = fields.grid.as_deref()?;
let power = fields.report?; let bits = pack_type1(call, grid, power)?;
Some(bits.to_vec())
}
fn unpack(&self, payload: &[u8], _ctx: &DecodeContext) -> Option<Self::Unpacked> {
if payload.len() != 50 {
return None;
}
let mut buf = [0u8; 50];
buf.copy_from_slice(payload);
unpack(&buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type1_roundtrip_callsign() {
let bits = pack_type1("K1ABC", "FN42", 37).expect("pack");
let m = unpack(&bits).expect("unpack");
assert_eq!(
m,
WsprMessage::Type1 {
callsign: "K1ABC".into(),
grid: "FN42".into(),
power_dbm: 37,
}
);
}
#[test]
fn type1_roundtrip_with_digit_in_second_slot() {
let bits = pack_type1("K9AN", "EN50", 33).expect("pack");
let m = unpack(&bits).expect("unpack");
match m {
WsprMessage::Type1 {
callsign,
grid,
power_dbm,
} => {
assert_eq!(callsign, "K9AN");
assert_eq!(grid, "EN50");
assert_eq!(power_dbm, 33);
}
other => panic!("expected Type 1, got {:?}", other),
}
}
#[test]
fn invalid_power_rejected() {
assert!(pack_type1("K1ABC", "FN42", 42).is_none());
}
#[test]
fn invalid_grid_rejected() {
assert!(pack_type1("K1ABC", "SS01", 37).is_none());
}
#[test]
fn unpack_rejects_reserved_call_range() {
let bits = {
let mut b = [0u8; 50];
let n1 = 0x0fff_ffffu32;
let n2 = pack_grid4_power("FN42", 37).unwrap();
let bytes = pack50(n1, n2);
for i in 0..50 {
b[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
}
b
};
if let Some(WsprMessage::Type1 { .. }) = unpack(&bits) {
panic!("shouldn't be Type 1");
}
}
#[test]
fn type2_single_char_suffix() {
let n1 = pack_call("K1ABC").expect("pack call");
let m_local = 60_000 - 32_768 + 7; let ntype = 37 + 1 + 1; let n2 = 128 * m_local + (ntype + 64);
let bytes = pack50(n1, n2);
let mut bits = [0u8; 50];
for i in 0..50 {
bits[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
}
let m = unpack(&bits).expect("unpack");
assert_eq!(
m,
WsprMessage::Type2 {
callsign: "K1ABC/7".into(),
power_dbm: 37,
}
);
}
#[test]
fn type2_prefix_pj4() {
let n1 = pack_call("K1ABC").expect("pack call");
let m_local = {
let mut m: u32 = 0;
for &ch in b"PJ4" {
let nc = match ch {
b'0'..=b'9' => ch - b'0',
b'A'..=b'Z' => ch - b'A' + 10,
_ => 36,
};
m = 37 * m + nc as u32;
}
assert!(m > 32_768, "PJ4 should land above 32768");
m - 32_768
};
let ntype = 37 + 1 + 1;
let n2 = 128 * m_local + (ntype + 64);
let bytes = pack50(n1, n2);
let mut bits = [0u8; 50];
for i in 0..50 {
bits[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
}
let m = unpack(&bits).expect("unpack");
assert_eq!(
m,
WsprMessage::Type2 {
callsign: "PJ4/K1ABC".into(),
power_dbm: 37,
}
);
}
#[test]
fn type3_hashed_call_grid6() {
let hash = 12_345u32;
let grid6 = "FN42LX";
let power = 27i32;
let rotated = {
let b = grid6.as_bytes();
format!(
"{}{}",
core::str::from_utf8(&b[1..6]).unwrap(),
b[0] as char
)
};
assert_eq!(rotated, "N42LXF");
let n1 = pack_call(&rotated).expect("pack call(grid6)");
let ntype: i32 = -(power + 1); let n2 = hash * 128 + (ntype + 64) as u32;
let bytes = pack50(n1, n2);
let mut bits = [0u8; 50];
for i in 0..50 {
bits[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
}
let m = unpack(&bits).expect("unpack");
assert_eq!(
m,
WsprMessage::Type3 {
callsign_hash: hash,
grid6: grid6.into(),
power_dbm: power,
}
);
}
#[test]
fn pack50_unpack50_all_bits() {
let n1 = 0x0deadb3u32;
let n2 = 0x001abcdu32 & 0x003f_ffff;
let bytes = pack50(n1, n2);
let (rn1, rn2) = unpack50(&bytes);
assert_eq!(rn1, n1);
assert_eq!(rn2, n2);
}
}