1use alloc::format;
33use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35use core::fmt;
36
37const NBASE: u32 = 37 * 36 * 10 * 27 * 27 * 27;
40
41const NGBASE: u32 = 180 * 180;
44
45#[derive(Clone, Debug, Eq, PartialEq)]
52pub enum Jt72Message {
53 Standard {
55 call1: String,
56 call2: String,
57 grid_or_report: String,
61 },
62 Unsupported { nc1: u32, nc2: u32, ng: u32 },
66}
67
68impl fmt::Display for Jt72Message {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 match self {
71 Jt72Message::Standard {
72 call1,
73 call2,
74 grid_or_report,
75 } => write!(f, "{} {} {}", call1, call2, grid_or_report),
76 Jt72Message::Unsupported { nc1, nc2, ng } => {
77 write!(f, "<unsupported nc1={nc1} nc2={nc2} ng={ng}>")
78 }
79 }
80 }
81}
82
83const CALL_ALPHA: &[u8; 37] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
89
90fn nchar(c: u8) -> Option<u32> {
93 match c {
94 b'0'..=b'9' => Some((c - b'0') as u32),
95 b'A'..=b'Z' => Some((c - b'A' + 10) as u32),
96 b'a'..=b'z' => Some((c - b'a' + 10) as u32),
97 b' ' => Some(36),
98 _ => None,
99 }
100}
101
102pub fn pack_call(call: &str) -> Option<u32> {
115 let bytes = call.as_bytes();
116 match call {
118 "CQ" => return Some(NBASE + 1),
119 "QRZ" => return Some(NBASE + 2),
120 "DE" => return Some(267_796_945),
121 _ => {}
122 }
123 if bytes.is_empty() || bytes.len() > 6 {
124 return None;
125 }
126
127 let mut tmp = [b' '; 6];
129 if bytes.len() >= 3 && bytes[2].is_ascii_digit() {
130 for (i, &b) in bytes.iter().enumerate() {
132 tmp[i] = b;
133 }
134 } else if bytes.len() >= 2 && bytes[1].is_ascii_digit() {
135 if bytes.len() > 5 {
138 return None;
139 }
140 for (i, &b) in bytes.iter().enumerate() {
141 tmp[i + 1] = b;
142 }
143 } else {
144 return None;
145 }
146
147 for t in tmp.iter_mut() {
149 if t.is_ascii_lowercase() {
150 *t -= b'a' - b'A';
151 }
152 }
153
154 let n = [
156 nchar(tmp[0])?,
157 nchar(tmp[1])?,
158 nchar(tmp[2])?,
159 nchar(tmp[3])?,
160 nchar(tmp[4])?,
161 nchar(tmp[5])?,
162 ];
163 if n[1] == 36 {
166 return None;
167 }
168 if n[2] >= 10 {
170 return None;
171 }
172 for k in 3..6 {
174 if n[k] < 10 {
175 return None;
176 }
177 }
178
179 let mut ncall = n[0];
180 ncall = 36 * ncall + n[1];
181 ncall = 10 * ncall + n[2];
182 ncall = 27 * ncall + n[3] - 10;
183 ncall = 27 * ncall + n[4] - 10;
184 ncall = 27 * ncall + n[5] - 10;
185 Some(ncall)
186}
187
188pub fn unpack_call(ncall: u32) -> Option<String> {
192 match ncall {
194 v if v == NBASE + 1 => return Some("CQ".into()),
195 v if v == NBASE + 2 => return Some("QRZ".into()),
196 267_796_945 => return Some("DE".into()),
197 _ => {}
198 }
199 if ncall >= NBASE {
200 return None;
201 }
202 let mut n = ncall;
203 let mut chars = [b' '; 6];
204 let c6 = (n % 27) + 10;
205 chars[5] = CALL_ALPHA[c6 as usize];
206 n /= 27;
207 let c5 = (n % 27) + 10;
208 chars[4] = CALL_ALPHA[c5 as usize];
209 n /= 27;
210 let c4 = (n % 27) + 10;
211 chars[3] = CALL_ALPHA[c4 as usize];
212 n /= 27;
213 let c3 = n % 10;
214 chars[2] = CALL_ALPHA[c3 as usize];
215 n /= 10;
216 let c2 = n % 36;
217 chars[1] = CALL_ALPHA[c2 as usize];
218 n /= 36;
219 let c1 = n; chars[0] = CALL_ALPHA[c1 as usize];
221
222 let s = core::str::from_utf8(&chars).ok()?;
223 Some(s.trim().to_string())
224}
225
226fn pack_grid4_plain(grid: &str) -> Option<u32> {
241 let b = grid.as_bytes();
242 if b.len() != 4 {
243 return None;
244 }
245 let fl = match b[0] {
246 c @ b'A'..=b'R' => (c - b'A') as i32,
247 _ => return None,
248 };
249 let fla = match b[1] {
250 c @ b'A'..=b'R' => (c - b'A') as i32,
251 _ => return None,
252 };
253 let sl = match b[2] {
254 c @ b'0'..=b'9' => (c - b'0') as i32,
255 _ => return None,
256 };
257 let sla = match b[3] {
258 c @ b'0'..=b'9' => (c - b'0') as i32,
259 _ => return None,
260 };
261 let ng_long_part = 179 - 10 * fl - sl;
262 let lat_int = 10 * fla + sla;
263 let ng = ng_long_part * 180 + lat_int;
264 Some(ng as u32)
265}
266
267pub fn pack_grid_or_report(s: &str) -> Option<u32> {
271 match s.trim_end() {
272 "" => Some(NGBASE + 1),
273 "RO" => Some(NGBASE + 62),
274 "RRR" => Some(NGBASE + 63),
275 "73" => Some(NGBASE + 64),
276 other => {
277 if let Some(rest) = other.strip_prefix('-')
278 && let Ok(n) = rest.parse::<i32>()
279 && (1..=30).contains(&n)
280 {
281 return Some(NGBASE + 1 + n as u32);
282 }
283 if let Some(rest) = other.strip_prefix("R-")
284 && let Ok(n) = rest.parse::<i32>()
285 && (1..=30).contains(&n)
286 {
287 return Some(NGBASE + 31 + n as u32);
288 }
289 pack_grid4_plain(other)
290 }
291 }
292}
293
294pub fn unpack_grid(ng: u32) -> String {
297 if ng == NGBASE + 1 {
298 return String::new();
299 }
300 match ng {
301 v if v == NGBASE + 62 => return "RO".into(),
302 v if v == NGBASE + 63 => return "RRR".into(),
303 v if v == NGBASE + 64 => return "73".into(),
304 _ => {}
305 }
306 if ng > NGBASE && ng <= NGBASE + 30 + 1 {
307 let n = ng - NGBASE - 1;
308 return format!("-{:02}", n);
309 }
310 if ng > NGBASE + 31 && ng <= NGBASE + 61 {
311 let n = ng - NGBASE - 31;
312 return format!("R-{:02}", n);
313 }
314 if ng < NGBASE {
315 let long_part = (ng / 180) as i32;
319 let lat = (ng % 180) as i32;
320 let fl_sl = 179 - long_part;
321 let fl = fl_sl / 10;
322 let sl = fl_sl % 10;
323 let fla = lat / 10;
324 let sla = lat % 10;
325 let mut g = [0u8; 4];
326 g[0] = b'A' + fl as u8;
327 g[1] = b'A' + fla as u8;
328 g[2] = b'0' + sl as u8;
329 g[3] = b'0' + sla as u8;
330 return core::str::from_utf8(&g).unwrap_or("????").to_string();
331 }
332 "?".into()
333}
334
335pub fn pack_words(nc1: u32, nc2: u32, ng: u32) -> [u8; 12] {
343 let mut d = [0u8; 12];
344 d[0] = ((nc1 >> 22) & 0x3f) as u8;
345 d[1] = ((nc1 >> 16) & 0x3f) as u8;
346 d[2] = ((nc1 >> 10) & 0x3f) as u8;
347 d[3] = ((nc1 >> 4) & 0x3f) as u8;
348 d[4] = (((nc1 & 0xf) << 2) | ((nc2 >> 26) & 0x3)) as u8;
349 d[5] = ((nc2 >> 20) & 0x3f) as u8;
350 d[6] = ((nc2 >> 14) & 0x3f) as u8;
351 d[7] = ((nc2 >> 8) & 0x3f) as u8;
352 d[8] = ((nc2 >> 2) & 0x3f) as u8;
353 d[9] = (((nc2 & 0x3) << 4) | ((ng >> 12) & 0xf)) as u8;
354 d[10] = ((ng >> 6) & 0x3f) as u8;
355 d[11] = (ng & 0x3f) as u8;
356 d
357}
358
359pub fn unpack_words(d: &[u8; 12]) -> (u32, u32, u32) {
362 let nc1 = ((d[0] as u32) << 22)
363 | ((d[1] as u32) << 16)
364 | ((d[2] as u32) << 10)
365 | ((d[3] as u32) << 4)
366 | (((d[4] as u32) >> 2) & 0xf);
367 let nc2 = (((d[4] as u32) & 0x3) << 26)
368 | ((d[5] as u32) << 20)
369 | ((d[6] as u32) << 14)
370 | ((d[7] as u32) << 8)
371 | ((d[8] as u32) << 2)
372 | (((d[9] as u32) >> 4) & 0x3);
373 let ng = (((d[9] as u32) & 0xf) << 12) | ((d[10] as u32) << 6) | (d[11] as u32);
374 (nc1, nc2, ng)
375}
376
377pub fn pack_standard(call1: &str, call2: &str, grid_or_report: &str) -> Option<[u8; 12]> {
380 let nc1 = pack_call(call1)?;
381 let nc2 = pack_call(call2)?;
382 let ng = pack_grid_or_report(grid_or_report)?;
383 Some(pack_words(nc1, nc2, ng))
384}
385
386pub fn unpack(d: &[u8; 12]) -> Jt72Message {
388 let (nc1, nc2, ng) = unpack_words(d);
389 let c1 = unpack_call(nc1);
390 let c2 = unpack_call(nc2);
391 if ng >= 32768 {
395 return Jt72Message::Unsupported { nc1, nc2, ng };
396 }
397 match (c1, c2) {
398 (Some(call1), Some(call2)) => Jt72Message::Standard {
399 call1,
400 call2,
401 grid_or_report: unpack_grid(ng),
402 },
403 _ => Jt72Message::Unsupported { nc1, nc2, ng },
404 }
405}
406
407use crate::core::{DecodeContext, MessageCodec, MessageFields};
412
413#[derive(Copy, Clone, Debug, Default)]
415pub struct Jt72Message_;
416
417pub type Jt72Codec = Jt72Message_;
421
422impl MessageCodec for Jt72Message_ {
423 type Unpacked = Jt72Message;
424 const PAYLOAD_BITS: u32 = 72;
425 const CRC_BITS: u32 = 0;
426
427 fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
428 let c1 = fields.call1.as_deref()?;
429 let c2 = fields.call2.as_deref()?;
430 let rep = fields
431 .grid
432 .as_deref()
433 .or(fields.free_text.as_deref())
434 .unwrap_or("");
435 let words = pack_standard(c1, c2, rep)?;
436 let mut bits = Vec::with_capacity(72);
440 for &w in &words {
441 for b in (0..6).rev() {
442 bits.push((w >> b) & 1);
443 }
444 }
445 Some(bits)
446 }
447
448 fn unpack(&self, payload: &[u8], _ctx: &DecodeContext) -> Option<Self::Unpacked> {
449 if payload.len() != 72 {
450 return None;
451 }
452 let mut words = [0u8; 12];
453 for (i, slot) in words.iter_mut().enumerate() {
454 let mut w = 0u8;
455 for b in 0..6 {
456 w = (w << 1) | (payload[6 * i + b] & 1);
457 }
458 *slot = w;
459 }
460 Some(unpack(&words))
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn call_roundtrip_standard() {
470 for call in ["K1ABC", "K9AN", "JA1ABC", "VK3KCN", "G4BWP", "W7AV"] {
471 let n = pack_call(call).unwrap_or_else(|| panic!("pack {call}"));
472 let back = unpack_call(n).unwrap_or_else(|| panic!("unpack {call}"));
473 assert_eq!(back, call, "roundtrip: {call}");
474 }
475 }
476
477 #[test]
478 fn call_special_tokens() {
479 assert_eq!(pack_call("CQ"), Some(NBASE + 1));
480 assert_eq!(pack_call("QRZ"), Some(NBASE + 2));
481 assert_eq!(unpack_call(NBASE + 1).as_deref(), Some("CQ"));
482 assert_eq!(unpack_call(NBASE + 2).as_deref(), Some("QRZ"));
483 }
484
485 #[test]
486 fn grid_roundtrip() {
487 for grid in ["FN42", "PM95", "JN58", "AA00", "RR99"] {
488 let ng = pack_grid_or_report(grid).unwrap_or_else(|| panic!("pack {grid}"));
489 let back = unpack_grid(ng);
490 assert_eq!(back, grid, "roundtrip {grid}");
491 }
492 }
493
494 #[test]
498 fn grid_wsjtx_canonical_ng() {
499 for (grid, expected_ng) in [
500 ("EM41", 24421u32),
501 ("FN42", 22632),
502 ("PM95", 3725),
503 ("JN58", 15258),
504 ("AA00", 32220),
505 ("RR99", 179),
506 ] {
507 let ng = pack_grid_or_report(grid).unwrap_or_else(|| panic!("pack {grid}"));
508 assert_eq!(ng, expected_ng, "WSJT-X canonical ng for {grid}");
509 }
510 }
511
512 #[test]
513 fn grid_reports_and_tokens() {
514 for s in ["RO", "RRR", "73", "-15", "R-05"] {
515 let ng = pack_grid_or_report(s).unwrap_or_else(|| panic!("pack {s}"));
516 assert_eq!(unpack_grid(ng), s);
517 }
518 }
519
520 #[test]
521 fn standard_message_roundtrip() {
522 let words = pack_standard("K1ABC", "JA1ABC", "FN42").expect("pack");
523 let m = unpack(&words);
524 assert_eq!(
525 m,
526 Jt72Message::Standard {
527 call1: "K1ABC".into(),
528 call2: "JA1ABC".into(),
529 grid_or_report: "FN42".into(),
530 }
531 );
532 }
533
534 #[test]
535 fn codec_trait_roundtrip() {
536 let codec = Jt72Message_;
537 let fields = MessageFields {
538 call1: Some("K1ABC".into()),
539 call2: Some("JA1ABC".into()),
540 grid: Some("PM95".into()),
541 ..MessageFields::default()
542 };
543 let payload = codec.pack(&fields).expect("pack");
544 assert_eq!(payload.len(), 72);
545 let ctx = DecodeContext::default();
546 let m = codec.unpack(&payload, &ctx).expect("unpack");
547 assert!(matches!(m, Jt72Message::Standard { .. }));
548 }
549
550 #[test]
551 fn pack_words_bit_layout() {
552 let nc1 = 0x0F00_00F0u32; let nc2 = 0x0A00_000Au32;
555 let ng = 0x0F0Fu32;
556 let words = pack_words(nc1 & 0x0fff_ffff, nc2 & 0x0fff_ffff, ng & 0xffff);
557 let (n1b, n2b, ngb) = unpack_words(&words);
558 assert_eq!(n1b, nc1 & 0x0fff_ffff);
559 assert_eq!(n2b, nc2 & 0x0fff_ffff);
560 assert_eq!(ngb, ng & 0xffff);
561 }
562
563 #[test]
564 fn cq_standard_message() {
565 let words = pack_standard("CQ", "K1ABC", "FN42").expect("pack CQ");
566 let m = unpack(&words);
567 match m {
568 Jt72Message::Standard {
569 call1,
570 call2,
571 grid_or_report,
572 } => {
573 assert_eq!(call1, "CQ");
574 assert_eq!(call2, "K1ABC");
575 assert_eq!(grid_or_report, "FN42");
576 }
577 other => panic!("expected Standard, got {:?}", other),
578 }
579 }
580}