Skip to main content

mfsk_core/msg/
ap.rs

1//! A Priori (AP) hint for WSJT 77-bit message payloads.
2//!
3//! Known parts of the message (callsigns, grid, report) are converted to
4//! their packed bit representation and marked as "locked" so a downstream
5//! FEC decoder can clamp those LLRs to a high-confidence value. AP hints
6//! typically drop the decode threshold by a few dB when the caller knows
7//! the expected message format (e.g. "CQ from a specific DX call", or
8//! "RRR/RR73/73 as part of a QSO exchange").
9//!
10//! The 77-bit bit layout is shared across FT8, FT4, FT2 and FST4 — all WSJT
11//! Type-1 messages use the same `call1 / call2 / grid-or-report / i3` field
12//! positions — so `ApHint` lives in the protocol-agnostic message layer.
13
14use alloc::string::{String, ToString};
15use alloc::vec;
16use alloc::vec::Vec;
17
18use super::wsjt77::{pack_grid4, pack28};
19use crate::core::MessageCodec;
20
21/// Marker trait for `MessageCodec`s whose information-bit layout matches the
22/// 77-bit Wsjt77 family field positions (call1 at 0..28, call2 at 29..57,
23/// grid at 58..73, message-type i3 at 74..76). Used to gate the
24/// callsign/grid-based [`ApHint`] AP path to compatible protocols.
25///
26/// Implementing this trait is an assertion that the codec's bit layout is
27/// byte-for-byte equivalent to [`crate::msg::Wsjt77Message`] for the first 77
28/// bits — the AP module reads / writes those positions directly. Codecs with
29/// different layouts (e.g. byte-oriented packet codecs whose first bits
30/// encode a length field rather than a callsign hash) must NOT implement
31/// this trait; they need their own AP design.
32///
33/// Sealed: only the `mfsk-core` crate may implement.
34pub trait WsjtApCompatible: MessageCodec + sealed::Sealed {}
35
36mod sealed {
37    pub trait Sealed {}
38}
39
40impl sealed::Sealed for super::Wsjt77Message {}
41impl WsjtApCompatible for super::Wsjt77Message {}
42
43#[cfg(feature = "q65")]
44impl sealed::Sealed for super::Q65Message {}
45#[cfg(feature = "q65")]
46impl WsjtApCompatible for super::Q65Message {}
47
48/// A Priori information to bias decoding.
49#[derive(Debug, Clone, Default)]
50pub struct ApHint {
51    /// Known first callsign (e.g. "CQ", "JA1ABC"). Locks message bits 0–28.
52    pub call1: Option<String>,
53    /// Known second callsign (e.g. "3Y0Z"). Locks message bits 29–57.
54    pub call2: Option<String>,
55    /// Known grid locator (e.g. "PM95"). Locks bits 58–73.
56    pub grid: Option<String>,
57    /// Known response token: "RRR", "RR73", or "73". Locks bits 58–73.
58    pub report: Option<String>,
59}
60
61impl ApHint {
62    pub fn new() -> Self {
63        Self::default()
64    }
65    pub fn with_call1(mut self, call: &str) -> Self {
66        self.call1 = Some(call.to_string());
67        self
68    }
69    pub fn with_call2(mut self, call: &str) -> Self {
70        self.call2 = Some(call.to_string());
71        self
72    }
73    pub fn with_grid(mut self, grid: &str) -> Self {
74        self.grid = Some(grid.to_string());
75        self
76    }
77    pub fn with_report(mut self, rpt: &str) -> Self {
78        self.report = Some(rpt.to_string());
79        self
80    }
81
82    /// True if any AP field is populated.
83    pub fn has_info(&self) -> bool {
84        self.call1.is_some() || self.call2.is_some()
85    }
86
87    /// Build the `(mask, bit_values)` bit vectors of length `n_codeword` for
88    /// a downstream FEC codec. Bits 0–76 (the message payload) are populated
89    /// from the hint fields; bits 77..N are left unmasked.
90    ///
91    /// `mask[i] == 1` means bit `i` is AP-locked; `values[i]` is the target
92    /// bit value (0 or 1). The FEC codec clamps its LLR at these positions
93    /// to `±apmag` accordingly.
94    pub fn build_bits(&self, n_codeword: usize) -> (Vec<u8>, Vec<u8>) {
95        let mut mask = vec![0u8; n_codeword];
96        let mut values = vec![0u8; n_codeword];
97
98        // Write 28-bit packed call + 1-bit flag (=0) starting at `start`.
99        let mut set_call_bits = |call: &str, start: usize| {
100            if let Some(n28) = pack28(call) {
101                for i in 0..28 {
102                    let bit = ((n28 >> (27 - i)) & 1) as u8;
103                    mask[start + i] = 1;
104                    values[start + i] = bit;
105                }
106                // Flag bit (ipa/ipb) = 0 for standard calls.
107                mask[start + 28] = 1;
108                values[start + 28] = 0;
109            }
110        };
111
112        if let Some(ref c1) = self.call1 {
113            set_call_bits(c1, 0);
114        }
115        if let Some(ref c2) = self.call2 {
116            set_call_bits(c2, 29);
117        }
118
119        // Bits 58–73: grid or response field (15-bit value + 1-bit ir flag).
120        if let Some(ref grid) = self.grid
121            && let Some(igrid) = pack_grid4(grid)
122        {
123            mask[58] = 1;
124            values[58] = 0; // ir=0
125            for i in 0..15 {
126                let bit = ((igrid >> (14 - i)) & 1) as u8;
127                mask[59 + i] = 1;
128                values[59 + i] = bit;
129            }
130        }
131        if let Some(ref rpt) = self.report {
132            let igrid_val: Option<u32> = match rpt.as_str() {
133                "RRR" => Some(32_400 + 2),
134                "RR73" => Some(32_400 + 3),
135                "73" => Some(32_400 + 4),
136                _ => None,
137            };
138            if let Some(igrid) = igrid_val {
139                mask[58] = 1;
140                values[58] = 0;
141                for i in 0..15 {
142                    let bit = ((igrid >> (14 - i)) & 1) as u8;
143                    mask[59 + i] = 1;
144                    values[59 + i] = bit;
145                }
146            }
147        }
148
149        // Lock message type i3 = 001 (Type 1 standard) when any call known.
150        if self.has_info() {
151            mask[74] = 1;
152            values[74] = 0;
153            mask[75] = 1;
154            values[75] = 0;
155            mask[76] = 1;
156            values[76] = 1;
157        }
158
159        (mask, values)
160    }
161
162    /// Number of AP-locked message bits (informational; callers use it to
163    /// scale per-pass confidence thresholds).
164    pub fn locked_bits(&self, n_codeword: usize) -> usize {
165        let (mask, _) = self.build_bits(n_codeword);
166        mask.iter().filter(|&&m| m != 0).count()
167    }
168}