1use alloc::format;
26use alloc::string::{String, ToString};
27use alloc::vec::Vec;
28use core::fmt;
29
30const POWERS: &[i32] = &[
31 0, 3, 7, 10, 13, 17, 20, 23, 27, 30, 33, 37, 40, 43, 47, 50, 53, 57, 60,
32];
33
34#[derive(Clone, Debug, Eq, PartialEq)]
36pub enum WsprMessage {
37 Type1 {
39 callsign: String,
40 grid: String,
41 power_dbm: i32,
42 },
43 Type2 {
45 callsign: String,
48 power_dbm: i32,
49 },
50 Type3 {
53 callsign_hash: u32,
55 grid6: String,
57 power_dbm: i32,
58 },
59}
60
61impl fmt::Display for WsprMessage {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 WsprMessage::Type1 {
65 callsign,
66 grid,
67 power_dbm,
68 } => write!(f, "{} {} {}", callsign, grid, power_dbm),
69 WsprMessage::Type2 {
70 callsign,
71 power_dbm,
72 } => write!(f, "{} {}", callsign, power_dbm),
73 WsprMessage::Type3 {
74 callsign_hash,
75 grid6,
76 power_dbm,
77 } => write!(f, "<#{:05x}> {} {}", callsign_hash, grid6, power_dbm),
78 }
79 }
80}
81
82const CHAR37: &[u8; 37] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
89
90fn callsign_char_code(ch: u8) -> Option<u8> {
91 match ch {
92 b'0'..=b'9' => Some(ch - b'0'),
93 b'A'..=b'Z' => Some(ch - b'A' + 10),
94 b' ' => Some(36),
95 _ => None,
96 }
97}
98
99fn locator_char_code(ch: u8) -> Option<u8> {
100 match ch {
101 b'0'..=b'9' => Some(ch - b'0'),
102 b'A'..=b'R' => Some(ch - b'A'),
103 b' ' => Some(36),
104 _ => None,
105 }
106}
107
108pub fn pack50(n1: u32, n2: u32) -> [u8; 7] {
120 [
121 ((n1 >> 20) & 0xff) as u8,
122 ((n1 >> 12) & 0xff) as u8,
123 ((n1 >> 4) & 0xff) as u8,
124 (((n1 & 0x0f) << 4) | ((n2 >> 18) & 0x0f)) as u8,
125 ((n2 >> 10) & 0xff) as u8,
126 ((n2 >> 2) & 0xff) as u8,
127 (((n2 & 0x03) << 6) & 0xff) as u8,
128 ]
129}
130
131pub fn unpack50(data: &[u8; 7]) -> (u32, u32) {
134 let mut n1: u32 = (data[0] as u32) << 20;
135 n1 |= (data[1] as u32) << 12;
136 n1 |= (data[2] as u32) << 4;
137 n1 |= ((data[3] >> 4) & 0x0f) as u32;
138
139 let mut n2: u32 = ((data[3] & 0x0f) as u32) << 18;
140 n2 |= (data[4] as u32) << 10;
141 n2 |= (data[5] as u32) << 2;
142 n2 |= ((data[6] >> 6) & 0x03) as u32;
143
144 (n1, n2)
145}
146
147pub fn pack_call(callsign: &str) -> Option<u32> {
155 let bytes = callsign.as_bytes();
156 if bytes.len() > 6 || bytes.is_empty() {
157 return None;
158 }
159 let mut call6 = [b' '; 6];
160 if bytes.len() >= 3 && bytes[2].is_ascii_digit() {
163 for (i, &b) in bytes.iter().enumerate() {
164 call6[i] = b;
165 }
166 } else if bytes.len() >= 2 && bytes[1].is_ascii_digit() {
167 for (i, &b) in bytes.iter().enumerate() {
168 call6[i + 1] = b;
169 }
170 } else {
171 return None;
172 }
173
174 let codes: [u8; 6] = {
175 let mut c = [0u8; 6];
176 for i in 0..6 {
177 c[i] = callsign_char_code(call6[i])?;
178 }
179 c
180 };
181
182 let mut n: u32 = codes[0] as u32;
185 n = n * 36 + codes[1] as u32;
186 n = n * 10 + codes[2] as u32;
187 n = n * 27 + (codes[3].wrapping_sub(10)) as u32;
188 n = n * 27 + (codes[4].wrapping_sub(10)) as u32;
189 n = n * 27 + (codes[5].wrapping_sub(10)) as u32;
190 Some(n)
191}
192
193pub fn unpack_call(ncall: u32) -> Option<String> {
196 if ncall >= 262_177_560 {
197 return None;
198 }
199 let mut n = ncall;
200 let mut tmp = [b' '; 6];
201 let i = (n % 27 + 10) as usize;
203 tmp[5] = CHAR37[i];
204 n /= 27;
205 let i = (n % 27 + 10) as usize;
206 tmp[4] = CHAR37[i];
207 n /= 27;
208 let i = (n % 27 + 10) as usize;
209 tmp[3] = CHAR37[i];
210 n /= 27;
211 let i = (n % 10) as usize;
212 tmp[2] = CHAR37[i];
213 n /= 10;
214 let i = (n % 36) as usize;
215 tmp[1] = CHAR37[i];
216 n /= 36;
217 tmp[0] = CHAR37[n as usize];
218
219 let s = core::str::from_utf8(&tmp).ok()?;
220 Some(s.trim().to_string())
221}
222
223pub fn pack_grid4_power(grid: &str, power_dbm: i32) -> Option<u32> {
225 let bytes = grid.as_bytes();
226 if bytes.len() != 4 {
227 return None;
228 }
229 let g0 = locator_char_code(bytes[0])? as u32;
230 let g1 = locator_char_code(bytes[1])? as u32;
231 let g2 = locator_char_code(bytes[2])? as u32;
232 let g3 = locator_char_code(bytes[3])? as u32;
233 let m = (179 - 10 * g0 - g2) * 180 + 10 * g1 + g3;
234 Some(m * 128 + (power_dbm as u32) + 64)
235}
236
237pub fn unpack_grid(ngrid_full: u32) -> Option<(String, i32)> {
241 let ntype = (ngrid_full & 127) as i32 - 64;
242 let ngrid = ngrid_full >> 7;
243 if ngrid >= 32_400 {
244 return None;
245 }
246 let dlat = (ngrid % 180) as i32 - 90;
247 let mut dlong = (ngrid / 180) as i32 * 2 - 180 + 2;
248 if dlong < -180 {
249 dlong += 360;
250 }
251 if dlong > 180 {
252 dlong += 360;
253 }
254 let nlong = (60.0 * (180.0 - dlong as f32) / 5.0) as i32;
255 let ln1 = nlong / 240;
256 let ln2 = (nlong - 240 * ln1) / 24;
257
258 let nlat = (60.0 * (dlat + 90) as f32 / 2.5) as i32;
259 let la1 = nlat / 240;
260 let la2 = (nlat - 240 * la1) / 24;
261
262 let mut grid = [b'0'; 4];
263 grid[0] = CHAR37[(10 + ln1) as usize];
264 grid[2] = CHAR37[ln2 as usize];
265 grid[1] = CHAR37[(10 + la1) as usize];
266 grid[3] = CHAR37[la2 as usize];
267 Some((core::str::from_utf8(&grid).ok()?.to_string(), ntype))
268}
269
270pub fn pack_type1(callsign: &str, grid: &str, power_dbm: i32) -> Option<[u8; 50]> {
278 if !POWERS.contains(&power_dbm) {
279 return None;
280 }
281 let n1 = pack_call(callsign)?;
282 let n2 = pack_grid4_power(grid, power_dbm)?;
283 let bytes = pack50(n1, n2);
284 let mut bits = [0u8; 50];
285 for i in 0..50 {
286 let byte = bytes[i / 8];
287 bits[i] = (byte >> (7 - (i % 8))) & 1;
288 }
289 Some(bits)
290}
291
292fn apply_prefix(nprefix: u32, base_call: &str) -> Option<String> {
300 const A37: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
301 if nprefix < 60_000 {
302 let mut n = nprefix;
304 let mut pfx = [b' '; 3];
305 for i in (0..3).rev() {
306 let nc = (n % 37) as usize;
307 pfx[i] = A37[nc];
308 n /= 37;
309 }
310 let start = pfx.iter().position(|&b| b != b' ')?;
312 let pfx_str = core::str::from_utf8(&pfx[start..]).ok()?;
313 Some(format!("{}/{}", pfx_str, base_call))
314 } else {
315 let nc = nprefix - 60_000;
316 if nc <= 9 {
317 Some(format!("{}/{}", base_call, (b'0' + nc as u8) as char))
318 } else if nc <= 35 {
319 Some(format!(
320 "{}/{}",
321 base_call,
322 (b'A' + (nc - 10) as u8) as char
323 ))
324 } else if nc <= 125 {
325 let d1 = (nc - 26) / 10;
326 let d2 = (nc - 26) % 10;
327 Some(format!(
328 "{}/{}{}",
329 base_call,
330 (b'0' + d1 as u8) as char,
331 (b'0' + d2 as u8) as char
332 ))
333 } else {
334 None
335 }
336 }
337}
338
339pub fn unpack(bits: &[u8; 50]) -> Option<WsprMessage> {
342 let mut data = [0u8; 7];
344 for i in 0..50 {
345 if bits[i] & 1 != 0 {
346 data[i / 8] |= 1 << (7 - (i % 8));
347 }
348 }
349 let (n1, n2) = unpack50(&data);
350
351 let (maybe_grid, ntype) = unpack_grid(n2).unzip();
352
353 if let Some(t) = ntype
359 && t < 0
360 {
361 let power_dbm = -(t + 1);
362 let pseudo_call = unpack_call(n1).unwrap_or_default();
364 let mut grid6 = String::new();
365 if pseudo_call.len() == 6 {
366 let bytes = pseudo_call.as_bytes();
367 grid6.push(bytes[5] as char); grid6.push_str(core::str::from_utf8(&bytes[..5]).ok()?);
369 }
370 let hash = n2 >> 7;
374 return Some(WsprMessage::Type3 {
375 callsign_hash: hash,
376 grid6,
377 power_dbm,
378 });
379 }
380
381 let ntype_val = ntype?;
382 let grid = maybe_grid?;
383
384 if (0..=62).contains(&ntype_val) {
386 let nu = ntype_val % 10;
387 if nu == 0 || nu == 3 || nu == 7 {
388 let callsign = unpack_call(n1)?;
389 return Some(WsprMessage::Type1 {
390 callsign,
391 grid,
392 power_dbm: ntype_val,
393 });
394 }
395 let nadd = if nu > 7 {
400 nu - 7
401 } else if nu > 3 {
402 nu - 3
403 } else {
404 nu
405 };
406 let n3 = (n2 >> 7) + 32_768 * (nadd as u32 - 1);
407 let base_call = unpack_call(n1)?;
408 let full_call = apply_prefix(n3, &base_call)?;
409 let power_dbm = ntype_val - nadd;
410 let pu = power_dbm.rem_euclid(10);
412 if pu != 0 && pu != 3 && pu != 7 {
413 return None;
414 }
415 return Some(WsprMessage::Type2 {
416 callsign: full_call,
417 power_dbm,
418 });
419 }
420
421 None
422}
423
424use crate::core::{DecodeContext, MessageCodec, MessageFields};
429
430#[derive(Copy, Clone, Debug, Default)]
431pub struct Wspr50Message;
432
433impl MessageCodec for Wspr50Message {
434 type Unpacked = WsprMessage;
435 const PAYLOAD_BITS: u32 = 50;
436 const CRC_BITS: u32 = 0;
437
438 fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
439 let call = fields.call1.as_deref()?;
440 let grid = fields.grid.as_deref()?;
441 let power = fields.report?; let bits = pack_type1(call, grid, power)?;
443 Some(bits.to_vec())
444 }
445
446 fn unpack(&self, payload: &[u8], _ctx: &DecodeContext) -> Option<Self::Unpacked> {
447 if payload.len() != 50 {
448 return None;
449 }
450 let mut buf = [0u8; 50];
451 buf.copy_from_slice(payload);
452 unpack(&buf)
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn type1_roundtrip_callsign() {
462 let bits = pack_type1("K1ABC", "FN42", 37).expect("pack");
463 let m = unpack(&bits).expect("unpack");
464 assert_eq!(
465 m,
466 WsprMessage::Type1 {
467 callsign: "K1ABC".into(),
468 grid: "FN42".into(),
469 power_dbm: 37,
470 }
471 );
472 }
473
474 #[test]
475 fn type1_roundtrip_with_digit_in_second_slot() {
476 let bits = pack_type1("K9AN", "EN50", 33).expect("pack");
479 let m = unpack(&bits).expect("unpack");
480 match m {
481 WsprMessage::Type1 {
482 callsign,
483 grid,
484 power_dbm,
485 } => {
486 assert_eq!(callsign, "K9AN");
487 assert_eq!(grid, "EN50");
488 assert_eq!(power_dbm, 33);
489 }
490 other => panic!("expected Type 1, got {:?}", other),
491 }
492 }
493
494 #[test]
495 fn invalid_power_rejected() {
496 assert!(pack_type1("K1ABC", "FN42", 42).is_none());
497 }
498
499 #[test]
500 fn invalid_grid_rejected() {
501 assert!(pack_type1("K1ABC", "SS01", 37).is_none());
503 }
504
505 #[test]
506 fn unpack_rejects_reserved_call_range() {
507 let bits = {
510 let mut b = [0u8; 50];
511 let n1 = 0x0fff_ffffu32;
513 let n2 = pack_grid4_power("FN42", 37).unwrap();
514 let bytes = pack50(n1, n2);
515 for i in 0..50 {
516 b[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
517 }
518 b
519 };
520 if let Some(WsprMessage::Type1 { .. }) = unpack(&bits) {
522 panic!("shouldn't be Type 1");
523 }
524 }
525
526 #[test]
527 fn type2_single_char_suffix() {
528 let n1 = pack_call("K1ABC").expect("pack call");
541 let m_local = 60_000 - 32_768 + 7; let ntype = 37 + 1 + 1; let n2 = 128 * m_local + (ntype + 64);
544 let bytes = pack50(n1, n2);
545 let mut bits = [0u8; 50];
546 for i in 0..50 {
547 bits[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
548 }
549 let m = unpack(&bits).expect("unpack");
550 assert_eq!(
551 m,
552 WsprMessage::Type2 {
553 callsign: "K1ABC/7".into(),
554 power_dbm: 37,
555 }
556 );
557 }
558
559 #[test]
560 fn type2_prefix_pj4() {
561 let n1 = pack_call("K1ABC").expect("pack call");
576 let m_local = {
577 let mut m: u32 = 0;
578 for &ch in b"PJ4" {
579 let nc = match ch {
580 b'0'..=b'9' => ch - b'0',
581 b'A'..=b'Z' => ch - b'A' + 10,
582 _ => 36,
583 };
584 m = 37 * m + nc as u32;
585 }
586 assert!(m > 32_768, "PJ4 should land above 32768");
587 m - 32_768
588 };
589 let ntype = 37 + 1 + 1;
590 let n2 = 128 * m_local + (ntype + 64);
591 let bytes = pack50(n1, n2);
592 let mut bits = [0u8; 50];
593 for i in 0..50 {
594 bits[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
595 }
596 let m = unpack(&bits).expect("unpack");
597 assert_eq!(
598 m,
599 WsprMessage::Type2 {
600 callsign: "PJ4/K1ABC".into(),
601 power_dbm: 37,
602 }
603 );
604 }
605
606 #[test]
607 fn type3_hashed_call_grid6() {
608 let hash = 12_345u32;
615 let grid6 = "FN42LX";
616 let power = 27i32;
617 let rotated = {
618 let b = grid6.as_bytes();
619 format!(
620 "{}{}",
621 core::str::from_utf8(&b[1..6]).unwrap(),
622 b[0] as char
623 )
624 };
625 assert_eq!(rotated, "N42LXF");
626 let n1 = pack_call(&rotated).expect("pack call(grid6)");
630 let ntype: i32 = -(power + 1); let n2 = hash * 128 + (ntype + 64) as u32;
633 let bytes = pack50(n1, n2);
634 let mut bits = [0u8; 50];
635 for i in 0..50 {
636 bits[i] = (bytes[i / 8] >> (7 - (i % 8))) & 1;
637 }
638 let m = unpack(&bits).expect("unpack");
639 assert_eq!(
640 m,
641 WsprMessage::Type3 {
642 callsign_hash: hash,
643 grid6: grid6.into(),
644 power_dbm: power,
645 }
646 );
647 }
648
649 #[test]
650 fn pack50_unpack50_all_bits() {
651 let n1 = 0x0deadb3u32;
652 let n2 = 0x001abcdu32 & 0x003f_ffff;
653 let bytes = pack50(n1, n2);
654 let (rn1, rn2) = unpack50(&bytes);
655 assert_eq!(rn1, n1);
656 assert_eq!(rn2, n2);
657 }
658}