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