use core::fmt;
const NBASE: u32 = 37 * 36 * 10 * 27 * 27 * 27;
const NGBASE: u32 = 180 * 180;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Jt72Message {
Standard {
call1: String,
call2: String,
grid_or_report: String,
},
Unsupported { nc1: u32, nc2: u32, ng: u32 },
}
impl fmt::Display for Jt72Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Jt72Message::Standard {
call1,
call2,
grid_or_report,
} => write!(f, "{} {} {}", call1, call2, grid_or_report),
Jt72Message::Unsupported { nc1, nc2, ng } => {
write!(f, "<unsupported nc1={nc1} nc2={nc2} ng={ng}>")
}
}
}
}
const CALL_ALPHA: &[u8; 37] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
fn nchar(c: u8) -> Option<u32> {
match c {
b'0'..=b'9' => Some((c - b'0') as u32),
b'A'..=b'Z' => Some((c - b'A' + 10) as u32),
b'a'..=b'z' => Some((c - b'a' + 10) as u32),
b' ' => Some(36),
_ => None,
}
}
pub fn pack_call(call: &str) -> Option<u32> {
let bytes = call.as_bytes();
match call {
"CQ" => return Some(NBASE + 1),
"QRZ" => return Some(NBASE + 2),
"DE" => return Some(267_796_945),
_ => {}
}
if bytes.is_empty() || bytes.len() > 6 {
return None;
}
let mut tmp = [b' '; 6];
if bytes.len() >= 3 && bytes[2].is_ascii_digit() {
for (i, &b) in bytes.iter().enumerate() {
tmp[i] = b;
}
} else if bytes.len() >= 2 && bytes[1].is_ascii_digit() {
if bytes.len() > 5 {
return None;
}
for (i, &b) in bytes.iter().enumerate() {
tmp[i + 1] = b;
}
} else {
return None;
}
for t in tmp.iter_mut() {
if t.is_ascii_lowercase() {
*t -= b'a' - b'A';
}
}
let n = [
nchar(tmp[0])?,
nchar(tmp[1])?,
nchar(tmp[2])?,
nchar(tmp[3])?,
nchar(tmp[4])?,
nchar(tmp[5])?,
];
if n[1] == 36 {
return None;
}
if n[2] >= 10 {
return None;
}
for k in 3..6 {
if n[k] < 10 {
return None;
}
}
let mut ncall = n[0];
ncall = 36 * ncall + n[1];
ncall = 10 * ncall + n[2];
ncall = 27 * ncall + n[3] - 10;
ncall = 27 * ncall + n[4] - 10;
ncall = 27 * ncall + n[5] - 10;
Some(ncall)
}
pub fn unpack_call(ncall: u32) -> Option<String> {
match ncall {
v if v == NBASE + 1 => return Some("CQ".into()),
v if v == NBASE + 2 => return Some("QRZ".into()),
267_796_945 => return Some("DE".into()),
_ => {}
}
if ncall >= NBASE {
return None;
}
let mut n = ncall;
let mut chars = [b' '; 6];
let c6 = (n % 27) + 10;
chars[5] = CALL_ALPHA[c6 as usize];
n /= 27;
let c5 = (n % 27) + 10;
chars[4] = CALL_ALPHA[c5 as usize];
n /= 27;
let c4 = (n % 27) + 10;
chars[3] = CALL_ALPHA[c4 as usize];
n /= 27;
let c3 = n % 10;
chars[2] = CALL_ALPHA[c3 as usize];
n /= 10;
let c2 = n % 36;
chars[1] = CALL_ALPHA[c2 as usize];
n /= 36;
let c1 = n; chars[0] = CALL_ALPHA[c1 as usize];
let s = core::str::from_utf8(&chars).ok()?;
Some(s.trim().to_string())
}
fn pack_grid4_plain(grid: &str) -> Option<u32> {
let b = grid.as_bytes();
if b.len() != 4 {
return None;
}
let fl = match b[0] {
c @ b'A'..=b'R' => (c - b'A') as i32,
_ => return None,
};
let fla = match b[1] {
c @ b'A'..=b'R' => (c - b'A') as i32,
_ => return None,
};
let sl = match b[2] {
c @ b'0'..=b'9' => (c - b'0') as i32,
_ => return None,
};
let sla = match b[3] {
c @ b'0'..=b'9' => (c - b'0') as i32,
_ => return None,
};
let dlong_int = -180 + fl * 20 + sl * 2 + 1;
let lat_int = fla * 10 + sla;
let ng = ((dlong_int + 180) / 2) * 180 + lat_int;
Some(ng as u32)
}
pub fn pack_grid_or_report(s: &str) -> Option<u32> {
match s.trim_end() {
"" => Some(NGBASE + 1),
"RO" => Some(NGBASE + 62),
"RRR" => Some(NGBASE + 63),
"73" => Some(NGBASE + 64),
other => {
if let Some(rest) = other.strip_prefix('-')
&& let Ok(n) = rest.parse::<i32>()
&& (1..=30).contains(&n)
{
return Some(NGBASE + 1 + n as u32);
}
if let Some(rest) = other.strip_prefix("R-")
&& let Ok(n) = rest.parse::<i32>()
&& (1..=30).contains(&n)
{
return Some(NGBASE + 31 + n as u32);
}
pack_grid4_plain(other)
}
}
}
pub fn unpack_grid(ng: u32) -> String {
if ng == NGBASE + 1 {
return String::new();
}
match ng {
v if v == NGBASE + 62 => return "RO".into(),
v if v == NGBASE + 63 => return "RRR".into(),
v if v == NGBASE + 64 => return "73".into(),
_ => {}
}
if ng > NGBASE && ng <= NGBASE + 30 + 1 {
let n = ng - NGBASE - 1;
return format!("-{:02}", n);
}
if ng > NGBASE + 31 && ng <= NGBASE + 61 {
let n = ng - NGBASE - 31;
return format!("R-{:02}", n);
}
if ng < NGBASE {
let long = (ng / 180) as i32;
let lat = (ng % 180) as i32;
let fl = long / 10;
let sl = long % 10; let fla = lat / 10;
let sla = lat % 10;
let mut g = [0u8; 4];
g[0] = b'A' + fl as u8;
g[1] = b'A' + fla as u8;
g[2] = b'0' + sl as u8;
g[3] = b'0' + sla as u8;
return core::str::from_utf8(&g).unwrap_or("????").to_string();
}
"?".into()
}
pub fn pack_words(nc1: u32, nc2: u32, ng: u32) -> [u8; 12] {
let mut d = [0u8; 12];
d[0] = ((nc1 >> 22) & 0x3f) as u8;
d[1] = ((nc1 >> 16) & 0x3f) as u8;
d[2] = ((nc1 >> 10) & 0x3f) as u8;
d[3] = ((nc1 >> 4) & 0x3f) as u8;
d[4] = (((nc1 & 0xf) << 2) | ((nc2 >> 26) & 0x3)) as u8;
d[5] = ((nc2 >> 20) & 0x3f) as u8;
d[6] = ((nc2 >> 14) & 0x3f) as u8;
d[7] = ((nc2 >> 8) & 0x3f) as u8;
d[8] = ((nc2 >> 2) & 0x3f) as u8;
d[9] = (((nc2 & 0x3) << 4) | ((ng >> 12) & 0xf)) as u8;
d[10] = ((ng >> 6) & 0x3f) as u8;
d[11] = (ng & 0x3f) as u8;
d
}
pub fn unpack_words(d: &[u8; 12]) -> (u32, u32, u32) {
let nc1 = ((d[0] as u32) << 22)
| ((d[1] as u32) << 16)
| ((d[2] as u32) << 10)
| ((d[3] as u32) << 4)
| (((d[4] as u32) >> 2) & 0xf);
let nc2 = (((d[4] as u32) & 0x3) << 26)
| ((d[5] as u32) << 20)
| ((d[6] as u32) << 14)
| ((d[7] as u32) << 8)
| ((d[8] as u32) << 2)
| (((d[9] as u32) >> 4) & 0x3);
let ng = (((d[9] as u32) & 0xf) << 12) | ((d[10] as u32) << 6) | (d[11] as u32);
(nc1, nc2, ng)
}
pub fn pack_standard(call1: &str, call2: &str, grid_or_report: &str) -> Option<[u8; 12]> {
let nc1 = pack_call(call1)?;
let nc2 = pack_call(call2)?;
let ng = pack_grid_or_report(grid_or_report)?;
Some(pack_words(nc1, nc2, ng))
}
pub fn unpack(d: &[u8; 12]) -> Jt72Message {
let (nc1, nc2, ng) = unpack_words(d);
let c1 = unpack_call(nc1);
let c2 = unpack_call(nc2);
if ng >= 32768 {
return Jt72Message::Unsupported { nc1, nc2, ng };
}
match (c1, c2) {
(Some(call1), Some(call2)) => Jt72Message::Standard {
call1,
call2,
grid_or_report: unpack_grid(ng),
},
_ => Jt72Message::Unsupported { nc1, nc2, ng },
}
}
use crate::core::{DecodeContext, MessageCodec, MessageFields};
#[derive(Copy, Clone, Debug, Default)]
pub struct Jt72Message_;
pub type Jt72Codec = Jt72Message_;
impl MessageCodec for Jt72Message_ {
type Unpacked = Jt72Message;
const PAYLOAD_BITS: u32 = 72;
const CRC_BITS: u32 = 0;
fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
let c1 = fields.call1.as_deref()?;
let c2 = fields.call2.as_deref()?;
let rep = fields
.grid
.as_deref()
.or(fields.free_text.as_deref())
.unwrap_or("");
let words = pack_standard(c1, c2, rep)?;
let mut bits = Vec::with_capacity(72);
for &w in &words {
for b in (0..6).rev() {
bits.push((w >> b) & 1);
}
}
Some(bits)
}
fn unpack(&self, payload: &[u8], _ctx: &DecodeContext) -> Option<Self::Unpacked> {
if payload.len() != 72 {
return None;
}
let mut words = [0u8; 12];
for (i, slot) in words.iter_mut().enumerate() {
let mut w = 0u8;
for b in 0..6 {
w = (w << 1) | (payload[6 * i + b] & 1);
}
*slot = w;
}
Some(unpack(&words))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_roundtrip_standard() {
for call in ["K1ABC", "K9AN", "JA1ABC", "VK3KCN", "G4BWP", "W7AV"] {
let n = pack_call(call).unwrap_or_else(|| panic!("pack {call}"));
let back = unpack_call(n).unwrap_or_else(|| panic!("unpack {call}"));
assert_eq!(back, call, "roundtrip: {call}");
}
}
#[test]
fn call_special_tokens() {
assert_eq!(pack_call("CQ"), Some(NBASE + 1));
assert_eq!(pack_call("QRZ"), Some(NBASE + 2));
assert_eq!(unpack_call(NBASE + 1).as_deref(), Some("CQ"));
assert_eq!(unpack_call(NBASE + 2).as_deref(), Some("QRZ"));
}
#[test]
fn grid_roundtrip() {
for grid in ["FN42", "PM95", "JN58", "AA00", "RR99"] {
let ng = pack_grid_or_report(grid).unwrap_or_else(|| panic!("pack {grid}"));
let back = unpack_grid(ng);
assert_eq!(back, grid, "roundtrip {grid}");
}
}
#[test]
fn grid_reports_and_tokens() {
for s in ["RO", "RRR", "73", "-15", "R-05"] {
let ng = pack_grid_or_report(s).unwrap_or_else(|| panic!("pack {s}"));
assert_eq!(unpack_grid(ng), s);
}
}
#[test]
fn standard_message_roundtrip() {
let words = pack_standard("K1ABC", "JA1ABC", "FN42").expect("pack");
let m = unpack(&words);
assert_eq!(
m,
Jt72Message::Standard {
call1: "K1ABC".into(),
call2: "JA1ABC".into(),
grid_or_report: "FN42".into(),
}
);
}
#[test]
fn codec_trait_roundtrip() {
let codec = Jt72Message_;
let fields = MessageFields {
call1: Some("K1ABC".into()),
call2: Some("JA1ABC".into()),
grid: Some("PM95".into()),
..MessageFields::default()
};
let payload = codec.pack(&fields).expect("pack");
assert_eq!(payload.len(), 72);
let ctx = DecodeContext::default();
let m = codec.unpack(&payload, &ctx).expect("unpack");
assert!(matches!(m, Jt72Message::Standard { .. }));
}
#[test]
fn pack_words_bit_layout() {
let nc1 = 0x0F00_00F0u32; let nc2 = 0x0A00_000Au32;
let ng = 0x0F0Fu32;
let words = pack_words(nc1 & 0x0fff_ffff, nc2 & 0x0fff_ffff, ng & 0xffff);
let (n1b, n2b, ngb) = unpack_words(&words);
assert_eq!(n1b, nc1 & 0x0fff_ffff);
assert_eq!(n2b, nc2 & 0x0fff_ffff);
assert_eq!(ngb, ng & 0xffff);
}
#[test]
fn cq_standard_message() {
let words = pack_standard("CQ", "K1ABC", "FN42").expect("pack CQ");
let m = unpack(&words);
match m {
Jt72Message::Standard {
call1,
call2,
grid_or_report,
} => {
assert_eq!(call1, "CQ");
assert_eq!(call2, "K1ABC");
assert_eq!(grid_or_report, "FN42");
}
other => panic!("expected Standard, got {:?}", other),
}
}
}