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;
18#[cfg(feature = "packet-bytes")]
19pub mod packet_bytes;
20pub mod pipeline_ap;
21#[cfg(feature = "q65")]
22pub mod q65;
23pub mod wsjt77;
24pub mod wspr;
25
26pub use ap::ApHint;
27pub use hash_table::CallsignHashTable;
28pub use jt72::{Jt72Codec, Jt72Message};
29#[cfg(feature = "packet-bytes")]
30pub use packet_bytes::PacketBytesMessage;
31#[cfg(feature = "q65")]
32pub use q65::Q65Message;
33pub use wspr::{Wspr50Message, WsprMessage};
34
35use crate::core::{DecodeContext, MessageCodec, MessageFields};
36
37/// WSJT 77-bit message codec used by FT8, FT4, FT2 and FST4.
38///
39/// Pure wrapper around the free functions in [`wsjt77`], implementing the
40/// generic [`crate::MessageCodec`] trait so pipeline code can
41/// consume messages without knowing which concrete protocol produced them.
42#[derive(Copy, Clone, Debug, Default)]
43pub struct Wsjt77Message;
44
45impl MessageCodec for Wsjt77Message {
46    type Unpacked = String;
47    const PAYLOAD_BITS: u32 = 77;
48    const CRC_BITS: u32 = 14;
49
50    fn pack(&self, fields: &MessageFields) -> Option<Vec<u8>> {
51        // Free text wins if set; otherwise fall back to the standard three-
52        // field call/call/report packing used by the overwhelming majority of
53        // FT8/FT4 QSOs.
54        if let Some(txt) = &fields.free_text {
55            return wsjt77::pack77_free_text(txt).map(|a| a.to_vec());
56        }
57        let call1 = fields.call1.as_deref()?;
58        let call2 = fields.call2.as_deref()?;
59        // Prefer grid; if the caller supplied a numeric report, format it
60        // WSJT-X-style (sign-padded two-digit dB string).
61        let report = if let Some(g) = &fields.grid {
62            g.clone()
63        } else if let Some(r) = fields.report {
64            if r >= 0 {
65                format!("+{:02}", r)
66            } else {
67                format!("{:03}", r)
68            }
69        } else {
70            return None;
71        };
72        wsjt77::pack77(call1, call2, &report).map(|a| a.to_vec())
73    }
74
75    fn unpack(&self, payload: &[u8], ctx: &DecodeContext) -> Option<Self::Unpacked> {
76        if payload.len() != 77 {
77            return None;
78        }
79        let mut buf = [0u8; 77];
80        buf.copy_from_slice(payload);
81
82        // Prefer the hash-aware path when the caller threaded a table through
83        // `DecodeContext`; fall back to the placeholder-emitting variant.
84        if let Some(any) = ctx.callsign_hash_table.as_ref()
85            && let Some(ht) = any.downcast_ref::<CallsignHashTable>()
86        {
87            return wsjt77::unpack77_with_hash(&buf, ht);
88        }
89        wsjt77::unpack77(&buf)
90    }
91
92    /// Wsjt77 reserves the trailing K-77 info bits for a CRC. Two
93    /// flavours coexist in the WSJT-X family: FT8 / FT4 / FT2 use
94    /// LDPC(174, 91) with a 14-bit CRC at bits 77..91, while FST4
95    /// uses LDPC(240, 101) with a 24-bit CRC at bits 77..101.
96    /// Both share the same Wsjt77 77-bit message field; only the
97    /// CRC width differs by FEC pairing. We length-dispatch on the
98    /// `info` slice the FEC layer passes through here:
99    ///
100    /// - 91 → [`crate::fec::ldpc::check_crc14`]
101    /// - 101 → [`crate::fec::ldpc240_101::check_crc24`]
102    /// - other → reject (no Wsjt77-compatible CRC for that K)
103    fn verify_info(info: &[u8]) -> bool {
104        match info.len() {
105            91 => crate::fec::ldpc::check_crc14(info),
106            101 => crate::fec::ldpc240_101::check_crc24(info),
107            _ => false,
108        }
109    }
110}