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