mfsk_core/uvpacket/protocol.rs
1// SPDX-License-Identifier: GPL-3.0-or-later
2//! Protocol markers + trait wiring for the four uvpacket modes.
3//!
4//! Phase 2 modulation pivot (see `docs/0.3.1_PLAN.md`): the modem
5//! is **single-carrier coherent QPSK at 1200 baud + RRC pulse + 31-
6//! bit m-sequence preamble + periodic pilots**. The first 0.3.1
7//! attempt with non-coherent 4-FSK at h=0.5 broke down on tone
8//! orthogonality; QPSK's I/Q axes are orthogonal by construction.
9//!
10//! All four modes share the same modem (1200 baud QPSK, 1500 Hz
11//! audio centre, RRC α=0.5), the same FEC mother code
12//! (`Ldpc240_101`), the same preamble + pilot scheme, and the same
13//! per-LDPC-block frame layout at the [`Protocol`] trait level.
14//! They differ only in the puncturing applied to the FEC parity
15//! bits, which lives outside the trait constants in
16//! [`crate::uvpacket::puncture`].
17//!
18//! ## Scope boundary: decorative trait constants
19//!
20//! The `mfsk-core` `Protocol` trait surface was designed to express
21//! the WSJT-X family of M-ary tone-FSK modes. uvpacket lives at
22//! the boundary of that abstraction: it reuses the FEC layer but
23//! its modulation (single-carrier coherent QPSK + RRC) and demod
24//! (matched filter + pilot-aided phase track) bypass the generic
25//! mfsk-core TX / RX pipeline entirely. The natural consequence is
26//! that several `ModulationParams` constants — `NTONES = 4`,
27//! `TONE_SPACING_HZ`, `GFSK_BT`, `GFSK_HMOD` — are **decorative**
28//! for this module: they exist solely to satisfy the trait signature
29//! and the `protocol_invariants` test. They are **not** consulted
30//! by [`crate::uvpacket::tx::encode`] or
31//! [`crate::uvpacket::rx::decode_known_layout`].
32//!
33//! See [`crate::uvpacket`]'s module docs for the full scope-note
34//! table and the rationale for keeping uvpacket in-tree as an
35//! "applied example of FEC reuse" rather than a peer WSJT-family
36//! mode.
37//!
38//! | ZST | rate | net bps (at 4-GFSK 2400 ch bps) | use |
39//! |----------------|-----:|--------------------------------:|-----|
40//! | [`UvRobust`] | 0.42 | 1008 | mountain / weak signal / deep fading |
41//! | [`UvStandard`] | 0.50 | 1200 | typical NFM with fading |
42//! | [`UvFast`] | 0.66 | 1600 | good-signal default |
43//! | [`UvExpress`] | 0.75 | 1800 | strong-signal headline-fast mode (OSD-2 essentially mandatory) |
44//!
45//! Higher-rate modes use kSR-greedy puncture-set selection (see
46//! [`crate::uvpacket::puncture`]) — the empirical AWGN sweep showed
47//! ~1–3 dB Eb/N0 gain over uniform-spread at the deeper puncture
48//! rates, which makes `UvExpress` (76 % parity puncturing) viable.
49//!
50//! Note: at the [`Protocol`] level, all four ZSTs claim the same
51//! `N_DATA = 120` (= unpunctured codeword 240 ch bits / 2 bits/sym).
52//! The actual on-air block length post-puncture is shorter for
53//! Standard / Fast / Express and is handled by the bespoke TX/RX
54//! paths in [`crate::uvpacket::tx`] / [`crate::uvpacket::rx`]. The
55//! Protocol-level constants describe the *unpunctured* codeword so
56//! the standard mfsk-core invariants (FEC fits in N_DATA × bits/sym)
57//! hold.
58
59use crate::core::{FrameLayout, ModulationParams, Protocol, ProtocolId, SyncMode};
60use crate::fec::Ldpc240_101;
61
62use super::message::UvPacketRawMessage;
63use super::puncture::Mode;
64use super::sync_pattern::UVPACKET_SYNC_BLOCKS;
65
66/// Identity Gray map for 4-FSK (FT4 uses the same).
67const GRAY_4: [u8; 4] = [0, 1, 3, 2];
68
69/// Audio-domain centre frequency at synth time (Hz). Tones land at
70/// 800 / 1400 / 2000 / 2600 Hz, comfortably inside the typical NFM
71/// HT audio passband while clearing the 300–500 Hz HPF found on
72/// cheaper handhelds.
73pub const AUDIO_CENTRE_HZ: f32 = 1700.0;
74
75/// Define a uvpacket sub-mode ZST with all four trait impls.
76///
77/// All sub-modes share modulation, frame layout, FEC, message codec,
78/// and sync. The only per-mode datum is the inherent `MODE` constant
79/// pointing at the puncturing variant.
80macro_rules! uvpacket_submode {
81 (
82 $(#[$attr:meta])*
83 $name:ident,
84 mode = $mode:expr,
85 ) => {
86 $(#[$attr])*
87 #[derive(Copy, Clone, Debug, Default)]
88 pub struct $name;
89
90 impl $name {
91 /// Puncturing posture for this sub-mode. Used by the
92 /// bespoke TX / RX paths to pick the right puncture
93 /// table.
94 pub const MODE: Mode = $mode;
95 }
96
97 impl ModulationParams for $name {
98 const NTONES: u32 = 4;
99 const BITS_PER_SYMBOL: u32 = 2;
100 /// 1200 baud at 12 kHz sample rate → 10 samples / symbol.
101 const NSPS: u32 = 10;
102 const SYMBOL_DT: f32 = 1.0 / 1200.0;
103 /// h = 0.5 → tone spacing = baud × h = 600 Hz.
104 const TONE_SPACING_HZ: f32 = 600.0;
105 const GRAY_MAP: &'static [u8] = &GRAY_4;
106 const GFSK_BT: f32 = 0.5;
107 const GFSK_HMOD: f32 = 0.5;
108 const NFFT_PER_SYMBOL_FACTOR: u32 = 4;
109 const NSTEP_PER_SYMBOL: u32 = 2;
110 /// 12000 / 4 = 3000 Hz baseband window — clears the
111 /// 800–2600 Hz tone span with margin.
112 const NDOWN: u32 = 4;
113 }
114
115 impl FrameLayout for $name {
116 /// 240 codeword bits / 2 bits-per-symbol = 120 data symbols
117 /// per LDPC block. (Unpunctured. Higher-rate modes
118 /// transmit fewer ch bits per block but the trait-level
119 /// constant describes the mother codeword.)
120 const N_DATA: u32 = 120;
121 /// One Costas-4 at the head of each LDPC block.
122 const N_SYNC: u32 = 4;
123 const N_SYMBOLS: u32 = 124;
124 const N_RAMP: u32 = 0;
125 const SYNC_MODE: SyncMode = SyncMode::Block(&UVPACKET_SYNC_BLOCKS);
126 /// uvpacket frames are not slot-aligned — value is
127 /// informational only. Use the duration of one
128 /// LDPC-block-sized "protocol unit" so callers that
129 /// expect a non-zero T_SLOT_S see something reasonable.
130 const T_SLOT_S: f32 = 124.0 / 1200.0;
131 const TX_START_OFFSET_S: f32 = 0.0;
132 }
133
134 impl Protocol for $name {
135 type Fec = Ldpc240_101;
136 type Msg = UvPacketRawMessage;
137 const ID: ProtocolId = ProtocolId::UvPacket;
138 }
139 };
140}
141
142uvpacket_submode! {
143 /// **Robust** — rate 0.42 (unpunctured `Ldpc240_101`).
144 /// 1008 net bps. For mountain / weak-signal / deep-fading
145 /// channels where AFSK 1200 cannot deliver. AFSK has no
146 /// equivalent mode — this is the design's headline value-prop.
147 UvRobust, mode = Mode::Robust,
148}
149
150uvpacket_submode! {
151 /// **Standard** — punctured to rate 1/2. 1200 net bps.
152 /// Throughput parity with AFSK 1200 plus FEC for typical NFM
153 /// channels.
154 UvStandard, mode = Mode::Standard,
155}
156
157uvpacket_submode! {
158 /// **Fast** — punctured to rate 2/3. 1600 net bps (+33 % vs
159 /// AFSK 1200). Good-signal default. 63 % parity puncturing;
160 /// kSR-greedy puncture selection delivers ~1 dB Eb/N0 gain
161 /// over uniform-spread at the BP threshold.
162 UvFast, mode = Mode::Fast,
163}
164
165uvpacket_submode! {
166 /// **Express** — punctured to rate 3/4. 1800 net bps (+50 % vs
167 /// AFSK 1200). Strong-signal headline-fast mode. 76 % parity
168 /// puncturing — OSD-2 is essentially mandatory at the BP
169 /// threshold (~+3 dB Eb/N0 with OSD-2; BP-only needs ~+5 dB).
170 /// Viable only thanks to kSR-greedy puncture selection
171 /// (uniform-spread fails at this rate).
172 UvExpress, mode = Mode::Express,
173}