1use core::fmt;
33
34const NBASE: u32 = 37 * 36 * 10 * 27 * 27 * 27;
37
38const NGBASE: u32 = 180 * 180;
41
42#[derive(Clone, Debug, Eq, PartialEq)]
49pub enum Jt72Message {
50 Standard {
52 call1: String,
53 call2: String,
54 grid_or_report: String,
58 },
59 Unsupported { nc1: u32, nc2: u32, ng: u32 },
63}
64
65impl fmt::Display for Jt72Message {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 match self {
68 Jt72Message::Standard {
69 call1,
70 call2,
71 grid_or_report,
72 } => write!(f, "{} {} {}", call1, call2, grid_or_report),
73 Jt72Message::Unsupported { nc1, nc2, ng } => {
74 write!(f, "<unsupported nc1={nc1} nc2={nc2} ng={ng}>")
75 }
76 }
77 }
78}
79
80const CALL_ALPHA: &[u8; 37] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
86
87fn nchar(c: u8) -> Option<u32> {
90 match c {
91 b'0'..=b'9' => Some((c - b'0') as u32),
92 b'A'..=b'Z' => Some((c - b'A' + 10) as u32),
93 b'a'..=b'z' => Some((c - b'a' + 10) as u32),
94 b' ' => Some(36),
95 _ => None,
96 }
97}
98
99pub fn pack_call(call: &str) -> Option<u32> {
112 let bytes = call.as_bytes();
113 match call {
115 "CQ" => return Some(NBASE + 1),
116 "QRZ" => return Some(NBASE + 2),
117 "DE" => return Some(267_796_945),
118 _ => {}
119 }
120 if bytes.is_empty() || bytes.len() > 6 {
121 return None;
122 }
123
124 let mut tmp = [b' '; 6];
126 if bytes.len() >= 3 && bytes[2].is_ascii_digit() {
127 for (i, &b) in bytes.iter().enumerate() {
129 tmp[i] = b;
130 }
131 } else if bytes.len() >= 2 && bytes[1].is_ascii_digit() {
132 if bytes.len() > 5 {
135 return None;
136 }
137 for (i, &b) in bytes.iter().enumerate() {
138 tmp[i + 1] = b;
139 }
140 } else {
141 return None;
142 }
143
144 for t in tmp.iter_mut() {
146 if t.is_ascii_lowercase() {
147 *t -= b'a' - b'A';
148 }
149 }
150
151 let n = [
153 nchar(tmp[0])?,
154 nchar(tmp[1])?,
155 nchar(tmp[2])?,
156 nchar(tmp[3])?,
157 nchar(tmp[4])?,
158 nchar(tmp[5])?,
159 ];
160 if n[1] == 36 {
163 return None;
164 }
165 if n[2] >= 10 {
167 return None;
168 }
169 for k in 3..6 {
171 if n[k] < 10 {
172 return None;
173 }
174 }
175
176 let mut ncall = n[0];
177 ncall = 36 * ncall + n[1];
178 ncall = 10 * ncall + n[2];
179 ncall = 27 * ncall + n[3] - 10;
180 ncall = 27 * ncall + n[4] - 10;
181 ncall = 27 * ncall + n[5] - 10;
182 Some(ncall)
183}
184
185pub fn unpack_call(ncall: u32) -> Option<String> {
189 match ncall {
191 v if v == NBASE + 1 => return Some("CQ".into()),
192 v if v == NBASE + 2 => return Some("QRZ".into()),
193 267_796_945 => return Some("DE".into()),
194 _ => {}
195 }
196 if ncall >= NBASE {
197 return None;
198 }
199 let mut n = ncall;
200 let mut chars = [b' '; 6];
201 let c6 = (n % 27) + 10;
202 chars[5] = CALL_ALPHA[c6 as usize];
203 n /= 27;
204 let c5 = (n % 27) + 10;
205 chars[4] = CALL_ALPHA[c5 as usize];
206 n /= 27;
207 let c4 = (n % 27) + 10;
208 chars[3] = CALL_ALPHA[c4 as usize];
209 n /= 27;
210 let c3 = n % 10;
211 chars[2] = CALL_ALPHA[c3 as usize];
212 n /= 10;
213 let c2 = n % 36;
214 chars[1] = CALL_ALPHA[c2 as usize];
215 n /= 36;
216 let c1 = n; chars[0] = CALL_ALPHA[c1 as usize];
218
219 let s = core::str::from_utf8(&chars).ok()?;
220 Some(s.trim().to_string())
221}
222
223fn pack_grid4_plain(grid: &str) -> Option<u32> {
232 let b = grid.as_bytes();
233 if b.len() != 4 {
234 return None;
235 }
236 let fl = match b[0] {
237 c @ b'A'..=b'R' => (c - b'A') as i32,
238 _ => return None,
239 };
240 let fla = match b[1] {
241 c @ b'A'..=b'R' => (c - b'A') as i32,
242 _ => return None,
243 };
244 let sl = match b[2] {
245 c @ b'0'..=b'9' => (c - b'0') as i32,
246 _ => return None,
247 };
248 let sla = match b[3] {
249 c @ b'0'..=b'9' => (c - b'0') as i32,
250 _ => return None,
251 };
252 let dlong_int = -180 + fl * 20 + sl * 2 + 1;
254 let lat_int = fla * 10 + sla;
255 let ng = ((dlong_int + 180) / 2) * 180 + lat_int;
256 Some(ng as u32)
257}
258
259pub fn pack_grid_or_report(s: &str) -> Option<u32> {
263 match s.trim_end() {
264 "" => Some(NGBASE + 1),
265 "RO" => Some(NGBASE + 62),
266 "RRR" => Some(NGBASE + 63),
267 "73" => Some(NGBASE + 64),
268 other => {
269 if let Some(rest) = other.strip_prefix('-')
270 && let Ok(n) = rest.parse::<i32>()
271 && (1..=30).contains(&n)
272 {
273 return Some(NGBASE + 1 + n as u32);
274 }
275 if let Some(rest) = other.strip_prefix("R-")
276 && let Ok(n) = rest.parse::<i32>()
277 && (1..=30).contains(&n)
278 {
279 return Some(NGBASE + 31 + n as u32);
280 }
281 pack_grid4_plain(other)
282 }
283 }
284}
285
286pub fn unpack_grid(ng: u32) -> String {
289 if ng == NGBASE + 1 {
290 return String::new();
291 }
292 match ng {
293 v if v == NGBASE + 62 => return "RO".into(),
294 v if v == NGBASE + 63 => return "RRR".into(),
295 v if v == NGBASE + 64 => return "73".into(),
296 _ => {}
297 }
298 if ng > NGBASE && ng <= NGBASE + 30 + 1 {
299 let n = ng - NGBASE - 1;
300 return format!("-{:02}", n);
301 }
302 if ng > NGBASE + 31 && ng <= NGBASE + 61 {
303 let n = ng - NGBASE - 31;
304 return format!("R-{:02}", n);
305 }
306 if ng < NGBASE {
307 let long = (ng / 180) as i32;
309 let lat = (ng % 180) as i32;
310 let fl = long / 10;
313 let sl = long % 10; let fla = lat / 10;
315 let sla = lat % 10;
316 let mut g = [0u8; 4];
317 g[0] = b'A' + fl as u8;
318 g[1] = b'A' + fla as u8;
319 g[2] = b'0' + sl as u8;
320 g[3] = b'0' + sla as u8;
321 return core::str::from_utf8(&g).unwrap_or("????").to_string();
322 }
323 "?".into()
324}
325
326pub fn pack_words(nc1: u32, nc2: u32, ng: u32) -> [u8; 12] {
334 let mut d = [0u8; 12];
335 d[0] = ((nc1 >> 22) & 0x3f) as u8;
336 d[1] = ((nc1 >> 16) & 0x3f) as u8;
337 d[2] = ((nc1 >> 10) & 0x3f) as u8;
338 d[3] = ((nc1 >> 4) & 0x3f) as u8;
339 d[4] = (((nc1 & 0xf) << 2) | ((nc2 >> 26) & 0x3)) as u8;
340 d[5] = ((nc2 >> 20) & 0x3f) as u8;
341 d[6] = ((nc2 >> 14) & 0x3f) as u8;
342 d[7] = ((nc2 >> 8) & 0x3f) as u8;
343 d[8] = ((nc2 >> 2) & 0x3f) as u8;
344 d[9] = (((nc2 & 0x3) << 4) | ((ng >> 12) & 0xf)) as u8;
345 d[10] = ((ng >> 6) & 0x3f) as u8;
346 d[11] = (ng & 0x3f) as u8;
347 d
348}
349
350pub fn unpack_words(d: &[u8; 12]) -> (u32, u32, u32) {
353 let nc1 = ((d[0] as u32) << 22)
354 | ((d[1] as u32) << 16)
355 | ((d[2] as u32) << 10)
356 | ((d[3] as u32) << 4)
357 | (((d[4] as u32) >> 2) & 0xf);
358 let nc2 = (((d[4] as u32) & 0x3) << 26)
359 | ((d[5] as u32) << 20)
360 | ((d[6] as u32) << 14)
361 | ((d[7] as u32) << 8)
362 | ((d[8] as u32) << 2)
363 | (((d[9] as u32) >> 4) & 0x3);
364 let ng = (((d[9] as u32) & 0xf) << 12) | ((d[10] as u32) << 6) | (d[11] as u32);
365 (nc1, nc2, ng)
366}
367
368pub fn pack_standard(call1: &str, call2: &str, grid_or_report: &str) -> Option<[u8; 12]> {
371 let nc1 = pack_call(call1)?;
372 let nc2 = pack_call(call2)?;
373 let ng = pack_grid_or_report(grid_or_report)?;
374 Some(pack_words(nc1, nc2, ng))
375}
376
377pub fn unpack(d: &[u8; 12]) -> Jt72Message {
379 let (nc1, nc2, ng) = unpack_words(d);
380 let c1 = unpack_call(nc1);
381 let c2 = unpack_call(nc2);
382 if ng >= 32768 {
386 return Jt72Message::Unsupported { nc1, nc2, ng };
387 }
388 match (c1, c2) {
389 (Some(call1), Some(call2)) => Jt72Message::Standard {
390 call1,
391 call2,
392 grid_or_report: unpack_grid(ng),
393 },
394 _ => Jt72Message::Unsupported { nc1, nc2, ng },
395 }
396}
397
398use crate::core::{DecodeContext, MessageCodec, MessageFields};
403
404#[derive(Copy, Clone, Debug, Default)]
406pub struct Jt72Message_;
407
408pub type Jt72Codec = Jt72Message_;
412
413impl MessageCodec for Jt72Message_ {
414 type Unpacked = Jt72Message;
415 const PAYLOAD_BITS: u32 = 72;
416 const CRC_BITS: u32 = 0;
417
418 fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
419 let c1 = fields.call1.as_deref()?;
420 let c2 = fields.call2.as_deref()?;
421 let rep = fields
422 .grid
423 .as_deref()
424 .or(fields.free_text.as_deref())
425 .unwrap_or("");
426 let words = pack_standard(c1, c2, rep)?;
427 let mut bits = Vec::with_capacity(72);
431 for &w in &words {
432 for b in (0..6).rev() {
433 bits.push((w >> b) & 1);
434 }
435 }
436 Some(bits)
437 }
438
439 fn unpack(&self, payload: &[u8], _ctx: &DecodeContext) -> Option<Self::Unpacked> {
440 if payload.len() != 72 {
441 return None;
442 }
443 let mut words = [0u8; 12];
444 for (i, slot) in words.iter_mut().enumerate() {
445 let mut w = 0u8;
446 for b in 0..6 {
447 w = (w << 1) | (payload[6 * i + b] & 1);
448 }
449 *slot = w;
450 }
451 Some(unpack(&words))
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn call_roundtrip_standard() {
461 for call in ["K1ABC", "K9AN", "JA1ABC", "VK3KCN", "G4BWP", "W7AV"] {
462 let n = pack_call(call).unwrap_or_else(|| panic!("pack {call}"));
463 let back = unpack_call(n).unwrap_or_else(|| panic!("unpack {call}"));
464 assert_eq!(back, call, "roundtrip: {call}");
465 }
466 }
467
468 #[test]
469 fn call_special_tokens() {
470 assert_eq!(pack_call("CQ"), Some(NBASE + 1));
471 assert_eq!(pack_call("QRZ"), Some(NBASE + 2));
472 assert_eq!(unpack_call(NBASE + 1).as_deref(), Some("CQ"));
473 assert_eq!(unpack_call(NBASE + 2).as_deref(), Some("QRZ"));
474 }
475
476 #[test]
477 fn grid_roundtrip() {
478 for grid in ["FN42", "PM95", "JN58", "AA00", "RR99"] {
479 let ng = pack_grid_or_report(grid).unwrap_or_else(|| panic!("pack {grid}"));
480 let back = unpack_grid(ng);
481 assert_eq!(back, grid, "roundtrip {grid}");
482 }
483 }
484
485 #[test]
486 fn grid_reports_and_tokens() {
487 for s in ["RO", "RRR", "73", "-15", "R-05"] {
488 let ng = pack_grid_or_report(s).unwrap_or_else(|| panic!("pack {s}"));
489 assert_eq!(unpack_grid(ng), s);
490 }
491 }
492
493 #[test]
494 fn standard_message_roundtrip() {
495 let words = pack_standard("K1ABC", "JA1ABC", "FN42").expect("pack");
496 let m = unpack(&words);
497 assert_eq!(
498 m,
499 Jt72Message::Standard {
500 call1: "K1ABC".into(),
501 call2: "JA1ABC".into(),
502 grid_or_report: "FN42".into(),
503 }
504 );
505 }
506
507 #[test]
508 fn codec_trait_roundtrip() {
509 let codec = Jt72Message_;
510 let fields = MessageFields {
511 call1: Some("K1ABC".into()),
512 call2: Some("JA1ABC".into()),
513 grid: Some("PM95".into()),
514 ..MessageFields::default()
515 };
516 let payload = codec.pack(&fields).expect("pack");
517 assert_eq!(payload.len(), 72);
518 let ctx = DecodeContext::default();
519 let m = codec.unpack(&payload, &ctx).expect("unpack");
520 assert!(matches!(m, Jt72Message::Standard { .. }));
521 }
522
523 #[test]
524 fn pack_words_bit_layout() {
525 let nc1 = 0x0F00_00F0u32; let nc2 = 0x0A00_000Au32;
528 let ng = 0x0F0Fu32;
529 let words = pack_words(nc1 & 0x0fff_ffff, nc2 & 0x0fff_ffff, ng & 0xffff);
530 let (n1b, n2b, ngb) = unpack_words(&words);
531 assert_eq!(n1b, nc1 & 0x0fff_ffff);
532 assert_eq!(n2b, nc2 & 0x0fff_ffff);
533 assert_eq!(ngb, ng & 0xffff);
534 }
535
536 #[test]
537 fn cq_standard_message() {
538 let words = pack_standard("CQ", "K1ABC", "FN42").expect("pack CQ");
539 let m = unpack(&words);
540 match m {
541 Jt72Message::Standard {
542 call1,
543 call2,
544 grid_or_report,
545 } => {
546 assert_eq!(call1, "CQ");
547 assert_eq!(call2, "K1ABC");
548 assert_eq!(grid_or_report, "FN42");
549 }
550 other => panic!("expected Standard, got {:?}", other),
551 }
552 }
553}