mfsk_core/msg/mod.rs
1//! # `msg` — message-layer codecs and callsign hash table
2//!
3//! Message-layer codecs for WSJT-family digital modes.
4//!
5//! | Module | Payload bits | Used by |
6//! |--------------|--------------|---------------------------|
7//! | [`wsjt77`] | 77 | FT8, FT4, FT2, FST4 |
8//! | [`wspr`] | 50 | WSPR |
9//! | [`jt72`] | 72 | JT65, JT9 |
10//!
11//! [`hash_table::CallsignHashTable`] tracks hashed callsigns across decodes;
12//! typically a single instance lives in the decoder's side-channel state and
13//! is shared by every message unpack invocation.
14
15pub mod ap;
16pub mod hash_table;
17pub mod jt72;
18pub mod pipeline_ap;
19#[cfg(feature = "q65")]
20pub mod q65;
21pub mod wsjt77;
22pub mod wspr;
23
24pub use ap::ApHint;
25pub use hash_table::CallsignHashTable;
26pub use jt72::{Jt72Codec, Jt72Message};
27#[cfg(feature = "q65")]
28pub use q65::Q65Message;
29pub use wspr::{Wspr50Message, WsprMessage};
30
31use crate::core::{DecodeContext, MessageCodec, MessageFields};
32
33/// WSJT 77-bit message codec used by FT8, FT4, FT2 and FST4.
34///
35/// Pure wrapper around the free functions in [`wsjt77`], implementing the
36/// generic [`crate::MessageCodec`] trait so pipeline code can
37/// consume messages without knowing which concrete protocol produced them.
38#[derive(Copy, Clone, Debug, Default)]
39pub struct Wsjt77Message;
40
41impl MessageCodec for Wsjt77Message {
42 type Unpacked = String;
43 const PAYLOAD_BITS: u32 = 77;
44 const CRC_BITS: u32 = 14;
45
46 fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
47 // Free text wins if set; otherwise fall back to the standard three-
48 // field call/call/report packing used by the overwhelming majority of
49 // FT8/FT4 QSOs.
50 if let Some(txt) = &fields.free_text {
51 return wsjt77::pack77_free_text(txt).map(|a| a.to_vec());
52 }
53 let call1 = fields.call1.as_deref()?;
54 let call2 = fields.call2.as_deref()?;
55 // Prefer grid; if the caller supplied a numeric report, format it
56 // WSJT-X-style (sign-padded two-digit dB string).
57 let report = if let Some(g) = &fields.grid {
58 g.clone()
59 } else if let Some(r) = fields.report {
60 if r >= 0 {
61 format!("+{:02}", r)
62 } else {
63 format!("{:03}", r)
64 }
65 } else {
66 return None;
67 };
68 wsjt77::pack77(call1, call2, &report).map(|a| a.to_vec())
69 }
70
71 fn unpack(&self, payload: &[u8], ctx: &DecodeContext) -> Option<Self::Unpacked> {
72 if payload.len() != 77 {
73 return None;
74 }
75 let mut buf = [0u8; 77];
76 buf.copy_from_slice(payload);
77
78 // Prefer the hash-aware path when the caller threaded a table through
79 // `DecodeContext`; fall back to the placeholder-emitting variant.
80 if let Some(any) = ctx.callsign_hash_table.as_ref()
81 && let Some(ht) = any.downcast_ref::<CallsignHashTable>()
82 {
83 return wsjt77::unpack77_with_hash(&buf, ht);
84 }
85 wsjt77::unpack77(&buf)
86 }
87}