Skip to main content

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}