1use crate::core::{
29 FecCodec, FecOpts, FecResult, FrameLayout, ModulationParams, Protocol, ProtocolId, SyncMode,
30};
31use crate::fec::qra::Q65Codec;
32use crate::fec::qra15_65_64::QRA15_65_64_IRR_E23;
33use crate::msg::Q65Message;
34
35use super::sync_pattern::Q65_SYNC_BLOCKS;
36
37const IDENTITY_65: [u8; 65] = {
38 let mut m = [0u8; 65];
39 let mut i = 0usize;
40 while i < 65 {
41 m[i] = i as u8;
42 i += 1;
43 }
44 m
45};
46
47macro_rules! q65_submode {
53 (
54 $(#[$attr:meta])*
55 $name:ident,
56 nsps = $nsps:literal,
57 spacing_mult = $mult:literal,
58 tr_period_s = $period:literal,
59 ) => {
60 $(#[$attr])*
61 #[derive(Copy, Clone, Debug, Default)]
62 pub struct $name;
63
64 impl ModulationParams for $name {
65 const NTONES: u32 = 65;
66 const BITS_PER_SYMBOL: u32 = 6;
67 const NSPS: u32 = $nsps;
68 const SYMBOL_DT: f32 = ($nsps as f32) / 12_000.0;
69 const TONE_SPACING_HZ: f32 = (12_000.0 / ($nsps as f32)) * ($mult as f32);
71 const GRAY_MAP: &'static [u8] = &IDENTITY_65;
72 const GFSK_BT: f32 = 0.0;
74 const GFSK_HMOD: f32 = 1.0;
75 const NFFT_PER_SYMBOL_FACTOR: u32 = 2;
76 const NSTEP_PER_SYMBOL: u32 = 2;
77 const NDOWN: u32 = 3;
81 }
82
83 impl FrameLayout for $name {
84 const N_DATA: u32 = 63;
85 const N_SYNC: u32 = 22;
86 const N_SYMBOLS: u32 = 85;
87 const N_RAMP: u32 = 0;
88 const SYNC_MODE: SyncMode = SyncMode::Block(&Q65_SYNC_BLOCKS);
89 const T_SLOT_S: f32 = $period as f32;
90 const TX_START_OFFSET_S: f32 = 1.0;
92 }
93
94 impl Protocol for $name {
95 type Fec = Q65Fec;
96 type Msg = Q65Message;
97 const ID: ProtocolId = ProtocolId::Q65;
98 }
99 };
100}
101
102q65_submode! {
103 Q65a30,
107 nsps = 3600,
108 spacing_mult = 1,
109 tr_period_s = 30,
110}
111
112q65_submode! {
113 Q65a60,
117 nsps = 7200,
118 spacing_mult = 1,
119 tr_period_s = 60,
120}
121
122q65_submode! {
123 Q65b60,
127 nsps = 7200,
128 spacing_mult = 2,
129 tr_period_s = 60,
130}
131
132q65_submode! {
133 Q65c60,
137 nsps = 7200,
138 spacing_mult = 4,
139 tr_period_s = 60,
140}
141
142q65_submode! {
143 Q65d60,
147 nsps = 7200,
148 spacing_mult = 8,
149 tr_period_s = 60,
150}
151
152q65_submode! {
153 Q65e60,
157 nsps = 7200,
158 spacing_mult = 16,
159 tr_period_s = 60,
160}
161
162#[derive(Copy, Clone, Debug, Default)]
174pub struct Q65Fec;
175
176impl FecCodec for Q65Fec {
177 const N: usize = 63 * 6;
179 const K: usize = 13 * 6;
182
183 fn encode(&self, info: &[u8], codeword: &mut [u8]) {
184 assert_eq!(info.len(), Self::K, "encode: info.len() != K");
185 assert_eq!(codeword.len(), Self::N, "encode: codeword.len() != N");
186
187 let mut info_syms = [0_i32; 13];
189 for (i, slot) in info_syms.iter_mut().enumerate() {
190 let mut s = 0_i32;
191 for b in 0..6 {
192 s = (s << 1) | (info[6 * i + b] & 1) as i32;
193 }
194 *slot = s;
195 }
196
197 let mut codec = Q65Codec::new(&QRA15_65_64_IRR_E23);
198 let mut channel = [0_i32; 63];
199 codec.encode(&info_syms, &mut channel);
200
201 for (i, &sym) in channel.iter().enumerate() {
203 for b in 0..6 {
204 codeword[6 * i + b] = ((sym >> (5 - b)) & 1) as u8;
205 }
206 }
207 }
208
209 fn decode_soft(&self, _llr: &[f32], _opts: &FecOpts) -> Option<FecResult> {
210 None
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn modulation_constants_match_spec() {
223 assert_eq!(<Q65a30 as ModulationParams>::NTONES, 65);
224 assert_eq!(<Q65a30 as ModulationParams>::BITS_PER_SYMBOL, 6);
225 assert_eq!(<Q65a30 as ModulationParams>::NSPS, 3600);
226 assert!(
227 (<Q65a30 as ModulationParams>::SYMBOL_DT - 0.3).abs() < 1e-6,
228 "SYMBOL_DT must be 0.3 s for the 30-s T/R period"
229 );
230 let spacing = <Q65a30 as ModulationParams>::TONE_SPACING_HZ;
231 assert!(
232 (spacing - 12_000.0 / 3600.0).abs() < 1e-3,
233 "Q65-30A tone spacing must be 12000/3600 ≈ 3.333 Hz, got {spacing}"
234 );
235 }
236
237 #[test]
238 fn eme_submode_constants_match_q65params_f90() {
239 let baud_60 = 12_000.0 / 7200.0; for (name, spacing, mult) in [
244 ("Q65-60A", <Q65a60 as ModulationParams>::TONE_SPACING_HZ, 1),
245 ("Q65-60B", <Q65b60 as ModulationParams>::TONE_SPACING_HZ, 2),
246 ("Q65-60C", <Q65c60 as ModulationParams>::TONE_SPACING_HZ, 4),
247 ("Q65-60D", <Q65d60 as ModulationParams>::TONE_SPACING_HZ, 8),
248 ("Q65-60E", <Q65e60 as ModulationParams>::TONE_SPACING_HZ, 16),
249 ] {
250 let expected = baud_60 * mult as f32;
251 assert!(
252 (spacing - expected).abs() < 1e-3,
253 "{name} spacing {spacing} != expected {expected}"
254 );
255 }
256 assert_eq!(<Q65a60 as ModulationParams>::NSPS, 7200);
258 assert_eq!(<Q65b60 as ModulationParams>::NSPS, 7200);
259 assert_eq!(<Q65c60 as ModulationParams>::NSPS, 7200);
260 assert_eq!(<Q65d60 as ModulationParams>::NSPS, 7200);
261 assert_eq!(<Q65e60 as ModulationParams>::NSPS, 7200);
262 assert_eq!(<Q65a60 as FrameLayout>::T_SLOT_S, 60.0);
264 assert_eq!(<Q65e60 as FrameLayout>::T_SLOT_S, 60.0);
265 }
266
267 #[test]
268 fn all_q65_submodes_share_frame_layout() {
269 for (name, n_data, n_sync, n_symbols) in [
273 (
274 "Q65a30",
275 <Q65a30 as FrameLayout>::N_DATA,
276 <Q65a30 as FrameLayout>::N_SYNC,
277 <Q65a30 as FrameLayout>::N_SYMBOLS,
278 ),
279 (
280 "Q65a60",
281 <Q65a60 as FrameLayout>::N_DATA,
282 <Q65a60 as FrameLayout>::N_SYNC,
283 <Q65a60 as FrameLayout>::N_SYMBOLS,
284 ),
285 (
286 "Q65b60",
287 <Q65b60 as FrameLayout>::N_DATA,
288 <Q65b60 as FrameLayout>::N_SYNC,
289 <Q65b60 as FrameLayout>::N_SYMBOLS,
290 ),
291 (
292 "Q65c60",
293 <Q65c60 as FrameLayout>::N_DATA,
294 <Q65c60 as FrameLayout>::N_SYNC,
295 <Q65c60 as FrameLayout>::N_SYMBOLS,
296 ),
297 (
298 "Q65d60",
299 <Q65d60 as FrameLayout>::N_DATA,
300 <Q65d60 as FrameLayout>::N_SYNC,
301 <Q65d60 as FrameLayout>::N_SYMBOLS,
302 ),
303 (
304 "Q65e60",
305 <Q65e60 as FrameLayout>::N_DATA,
306 <Q65e60 as FrameLayout>::N_SYNC,
307 <Q65e60 as FrameLayout>::N_SYMBOLS,
308 ),
309 ] {
310 assert_eq!(n_data, 63, "{name} N_DATA");
311 assert_eq!(n_sync, 22, "{name} N_SYNC");
312 assert_eq!(n_symbols, 85, "{name} N_SYMBOLS");
313 }
314 }
315
316 #[test]
317 fn frame_layout_constants_match_spec() {
318 assert_eq!(<Q65a30 as FrameLayout>::N_DATA, 63);
319 assert_eq!(<Q65a30 as FrameLayout>::N_SYNC, 22);
320 assert_eq!(<Q65a30 as FrameLayout>::N_SYMBOLS, 85);
321 assert_eq!(<Q65a30 as FrameLayout>::N_RAMP, 0);
322 assert_eq!(<Q65a30 as FrameLayout>::T_SLOT_S, 30.0);
323 match <Q65a30 as FrameLayout>::SYNC_MODE {
324 SyncMode::Block(blocks) => {
325 assert_eq!(blocks.len(), 22, "Q65 has 22 distributed sync symbols");
326 for b in blocks {
327 assert_eq!(b.pattern, &[0u8], "every Q65 sync symbol is tone 0");
328 }
329 }
330 SyncMode::Interleaved { .. } => {
331 panic!("Q65 must use Block sync, not Interleaved")
332 }
333 }
334 }
335
336 #[test]
337 fn protocol_id_is_q65() {
338 assert_eq!(<Q65a30 as Protocol>::ID, ProtocolId::Q65);
339 }
340
341 #[test]
342 fn q65fec_encode_matches_q65codec_direct() {
343 let fec = Q65Fec;
346 let info: Vec<u8> = (0..Q65Fec::K)
348 .map(|i| ((i.wrapping_mul(13) ^ 0x55) & 1) as u8)
349 .collect();
350 let mut codeword = vec![0u8; Q65Fec::N];
351 fec.encode(&info, &mut codeword);
352
353 let mut info_syms = [0_i32; 13];
356 for (i, slot) in info_syms.iter_mut().enumerate() {
357 let mut s = 0_i32;
358 for b in 0..6 {
359 s = (s << 1) | (info[6 * i + b] & 1) as i32;
360 }
361 *slot = s;
362 }
363 let mut codec = Q65Codec::new(&QRA15_65_64_IRR_E23);
364 let mut expected_channel = [0_i32; 63];
365 codec.encode(&info_syms, &mut expected_channel);
366
367 let mut expected_bits = vec![0u8; Q65Fec::N];
370 for (i, &sym) in expected_channel.iter().enumerate() {
371 for b in 0..6 {
372 expected_bits[6 * i + b] = ((sym >> (5 - b)) & 1) as u8;
373 }
374 }
375 assert_eq!(codeword, expected_bits);
376 }
377
378 #[test]
379 fn decode_soft_is_a_stub() {
380 let fec = Q65Fec;
381 let llr = vec![0.0_f32; Q65Fec::N];
382 assert!(fec.decode_soft(&llr, &FecOpts::default()).is_none());
383 }
384}