1use rustfft::num_complex::Complex32;
4
5use crate::baseband::{
6 decode_packet_info_baseband, fft, known_pilot_symbols, known_training_symbols, ofdm_bin_plan,
7 packet_symbol_plan, recover_decided_packet_bytes_baseband, tx_one_packet_baseband,
8 PacketSymbolKind,
9};
10use crate::config::{OfdmConfig, PassbandMode};
11use crate::debug::{
12 EncodedBurst, EncodedPacketMeta, PassbandBinDump, PassbandBinDumpRow,
13 PassbandChannelCompareDump, PassbandChannelCompareRow, PassbandConstellationDump,
14 PassbandDiagnostics, PassbandIqChainDump, PassbandPilotTrackDump, PassbandSyncDump,
15};
16use crate::equalizer::{
17 decision_directed_evm, equalize_symbol_with_pilots, equalizer_initial_channel,
18 equalizer_refresh_channel, equalizer_reset_tracking, regularized_equalize, rms_evm,
19 EqualizerTrackingState,
20};
21use crate::packet::{
22 build_packet_bytes, fec_encoded_bits_len, modulation_from_id, split_payload, PacketInfo,
23};
24use crate::sync::{
25 active_baseband_fs, apply_cfo_hz, coarse_cfo_correct, estimate_coarse_cfo_hz,
26 find_repeated_half_sync_offset, refine_sync_offset, repeated_half_sync_metrics,
27 resample_from_offset, resample_from_offset_rate, sample_complex_linear,
28};
29use crate::wake::make_wake_tone;
30
31fn pilot_phase_error(
32 xeq_used: &[Complex32],
33 used_bins: &[usize],
34 pilot_bins: &[usize],
35 pref: &[Complex32],
36) -> Option<f32> {
37 if pilot_bins.is_empty() || pref.is_empty() {
38 return None;
39 }
40 let mut acc = Complex32::new(0.0, 0.0);
41 for (k, pbin) in pilot_bins.iter().enumerate() {
42 if let Some(pos) = used_bins.iter().position(|b| b == pbin) {
43 acc += xeq_used[pos] * pref[k].conj();
44 }
45 }
46 if acc.norm() <= 1.0e-9 {
47 None
48 } else {
49 Some(acc.arg())
50 }
51}
52
53fn resample_complex_linear_rate(x: &[Complex32], fs_in: f32, fs_out: f32) -> Vec<Complex32> {
54 if x.is_empty() || fs_in <= 0.0 || fs_out <= 0.0 {
55 return Vec::new();
56 }
57 if (fs_in - fs_out).abs() <= 1.0e-6 {
58 return x.to_vec();
59 }
60 let out_len = ((x.len() as f32) * fs_out / fs_in).round().max(1.0) as usize;
61 let step = fs_in / fs_out;
62 let mut out = Vec::with_capacity(out_len);
63 for n in 0..out_len {
64 out.push(sample_complex_linear(x, n as f32 * step));
65 }
66 out
67}
68
69pub fn encode_payload(payload: &[u8], cfg: &OfdmConfig) -> EncodedBurst {
77 let chunks = split_payload(payload, cfg.packet_payload_bytes);
78 let num_packets = chunks.len();
79 let mut audio = Vec::<f32>::new();
80 let mut meta = Vec::<EncodedPacketMeta>::with_capacity(num_packets);
81
82 let pre_sil = (0.015 * cfg.fs) as usize;
83 let post_sil = (0.020 * cfg.fs) as usize;
84
85 for (i, chunk) in chunks.iter().enumerate() {
86 let pkt_bytes = build_packet_bytes(chunk, i as u8, num_packets as u8, cfg);
87 let xbb = tx_one_packet_baseband(&pkt_bytes, cfg);
88 let pkt_audio = tx_one_packet(&pkt_bytes, cfg);
89
90 audio.extend(std::iter::repeat(0.0).take(pre_sil));
91 let packet_start = audio.len();
92 audio.extend_from_slice(&pkt_audio);
93 let packet_len = pkt_audio.len();
94 audio.extend(std::iter::repeat(0.0).take(post_sil));
95
96 meta.push(EncodedPacketMeta {
97 frag_index: i,
98 frag_count: num_packets,
99 packet_start,
100 packet_len,
101 xbb,
102 });
103 }
104
105 normalize_in_place(&mut audio, 0.85);
106 EncodedBurst {
107 audio,
108 packet_meta: meta,
109 }
110}
111
112pub fn decode_encoded_burst_oracle(burst: &EncodedBurst, cfg: &OfdmConfig) -> Option<Vec<u8>> {
120 let mut frags: Vec<Option<Vec<u8>>> = vec![None; burst.packet_meta.len()];
121 for m in &burst.packet_meta {
122 let info = decode_packet_info_baseband(&m.xbb, cfg)?;
123 if modulation_from_id(info.mod_id)? != cfg.modulation {
124 return None;
125 }
126 let idx = info.frag_index as usize;
127 if idx >= frags.len() || info.frag_count as usize != frags.len() {
128 return None;
129 }
130 frags[idx] = Some(info.payload);
131 }
132
133 let mut out = Vec::new();
134 for f in frags {
135 out.extend(f?);
136 }
137 Some(out)
138}
139
140pub fn encode_single_packet_passband(payload: &[u8], cfg: &OfdmConfig) -> Vec<f32> {
148 let pkt_bytes = build_packet_bytes(payload, 0, 1, cfg);
149 tx_one_packet(&pkt_bytes, cfg)
150}
151
152pub fn encode_single_packet_passband_body(payload: &[u8], cfg: &OfdmConfig) -> Vec<f32> {
160 let pkt_bytes = build_packet_bytes(payload, 0, 1, cfg);
161 tx_one_packet_body(&pkt_bytes, cfg)
162}
163
164pub fn decode_single_packet_passband(pkt_audio: &[f32], cfg: &OfdmConfig) -> Option<Vec<u8>> {
172 decode_packet_from_passband(pkt_audio, cfg).map(|p| p.payload)
173}
174
175pub fn decode_single_packet_passband_with_sync(
184 pkt_audio: &[f32],
185 cfg: &OfdmConfig,
186 sync_off: f32,
187) -> Option<Vec<u8>> {
188 decode_single_packet_passband_with_sync_rate(pkt_audio, cfg, sync_off, 1.0)
189}
190
191pub fn decode_single_packet_passband_with_sync_rate(
192 pkt_audio: &[f32],
193 cfg: &OfdmConfig,
194 sync_off: f32,
195 time_scale: f32,
196) -> Option<Vec<u8>> {
197 decode_packet_from_passband_with_sync_rate(pkt_audio, cfg, sync_off, time_scale)
198 .map(|p| p.payload)
199}
200
201pub fn recover_decided_packet_bytes_passband_with_sync(
202 pkt_audio: &[f32],
203 cfg: &OfdmConfig,
204 sync_off: f32,
205) -> Option<Vec<u8>> {
206 recover_decided_packet_bytes_passband_with_sync_rate(pkt_audio, cfg, sync_off, 1.0)
207}
208
209pub fn recover_decided_packet_bytes_passband_with_sync_rate(
210 pkt_audio: &[f32],
211 cfg: &OfdmConfig,
212 sync_off: f32,
213 time_scale: f32,
214) -> Option<Vec<u8>> {
215 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
216 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
217 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
218 return None;
219 }
220
221 let passband = &pkt_audio[wake_len + guard_len..];
222 let pre = 128usize.min(passband.len());
223 let mut chunk = vec![0.0f32; pre];
224 chunk.extend_from_slice(passband);
225 let rbb_full = downconvert_passband(&chunk, cfg);
226 let rbb = &rbb_full[pre..];
227 let rbb_sync = resample_from_offset_rate(rbb, sync_off, time_scale);
228 let rbb_cfo = coarse_cfo_correct(&rbb_sync, cfg);
229 recover_decided_packet_bytes_baseband(&rbb_cfo, cfg)
230}
231
232pub fn diagnose_passband_window(pkt_audio: &[f32], cfg: &OfdmConfig) -> PassbandDiagnostics {
240 diagnose_passband_window_with_sync_opt(pkt_audio, cfg, None)
241}
242
243pub fn diagnose_passband_window_with_sync(
252 pkt_audio: &[f32],
253 cfg: &OfdmConfig,
254 sync_off: f32,
255) -> PassbandDiagnostics {
256 diagnose_passband_window_with_sync_rate(pkt_audio, cfg, sync_off, 1.0)
257}
258
259pub fn diagnose_passband_window_with_sync_rate(
260 pkt_audio: &[f32],
261 cfg: &OfdmConfig,
262 sync_off: f32,
263 time_scale: f32,
264) -> PassbandDiagnostics {
265 diagnose_passband_window_with_sync_opt(pkt_audio, cfg, Some((sync_off, time_scale)))
266}
267
268fn diagnose_passband_window_with_sync_opt(
269 pkt_audio: &[f32],
270 cfg: &OfdmConfig,
271 forced_sync: Option<(f32, f32)>,
272) -> PassbandDiagnostics {
273 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
274 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
275 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
276 return PassbandDiagnostics {
277 enough_samples: false,
278 sync_off: 0,
279 cfo_hz: 0.0,
280 sync_rms: 0.0,
281 sync_peak: 0.0,
282 post_rms: 0.0,
283 post_peak: 0.0,
284 train_rms: 0.0,
285 hest_mag_min: 0.0,
286 hest_mag_mean: 0.0,
287 hest_mag_max: 0.0,
288 train_recon_evm: 0.0,
289 pilot_residual_evm: 0.0,
290 pilot_post_evm: 0.0,
291 post_eq_evm: 0.0,
292 decoded: false,
293 decoded_payload_len: None,
294 };
295 }
296
297 let passband = &pkt_audio[wake_len + guard_len..];
298 let pre = 128usize.min(passband.len());
299 let mut chunk = vec![0.0f32; pre];
300 chunk.extend_from_slice(passband);
301 let rbb_full = downconvert_passband(&chunk, cfg);
302 let rbb = &rbb_full[pre..];
303 let coarse_sync_off = find_repeated_half_sync_offset(rbb, cfg);
304 let (sync_off, time_scale) =
305 forced_sync.unwrap_or_else(|| (refine_sync_offset(rbb, cfg, coarse_sync_off), 1.0));
306 let rbb_sync = resample_from_offset_rate(rbb, sync_off, time_scale);
307 let cfo_hz = estimate_coarse_cfo_hz(&rbb_sync, cfg);
308 let rbb_cfo = coarse_cfo_correct(&rbb_sync, cfg);
309
310 let (used_bins, pilot_bins, data_bins) = ofdm_bin_plan(cfg);
311 let xsync_len = 2 * cfg.sync_half_len;
312 let train_len = cfg.nfft + cfg.ncp;
313 if rbb_cfo.len() < xsync_len + train_len || used_bins.is_empty() {
314 return PassbandDiagnostics {
315 enough_samples: false,
316 sync_off: sync_off.round() as usize,
317 cfo_hz,
318 sync_rms: 0.0,
319 sync_peak: 0.0,
320 post_rms: 0.0,
321 post_peak: 0.0,
322 train_rms: 0.0,
323 hest_mag_min: 0.0,
324 hest_mag_mean: 0.0,
325 hest_mag_max: 0.0,
326 train_recon_evm: 0.0,
327 pilot_residual_evm: 0.0,
328 pilot_post_evm: 0.0,
329 post_eq_evm: 0.0,
330 decoded: false,
331 decoded_payload_len: None,
332 };
333 }
334
335 let train_start = xsync_len;
336 let sync_slice = &rbb_cfo[..xsync_len.min(rbb_cfo.len())];
337 let data_post_start = (xsync_len + train_len).min(rbb_cfo.len());
338 let post_slice = &rbb_cfo[data_post_start..];
339 let sync_rms = if sync_slice.is_empty() {
340 0.0
341 } else {
342 (sync_slice.iter().map(|v| v.norm_sqr()).sum::<f32>() / sync_slice.len() as f32).sqrt()
343 };
344 let sync_peak = sync_slice.iter().map(|v| v.norm()).fold(0.0f32, f32::max);
345 let post_rms = if post_slice.is_empty() {
346 0.0
347 } else {
348 (post_slice.iter().map(|v| v.norm_sqr()).sum::<f32>() / post_slice.len() as f32).sqrt()
349 };
350 let post_peak = post_slice.iter().map(|v| v.norm()).fold(0.0f32, f32::max);
351 let train_no_cp = &rbb_cfo[train_start + cfg.ncp..train_start + cfg.ncp + cfg.nfft];
352 let train_rms =
353 (train_no_cp.iter().map(|v| v.norm_sqr()).sum::<f32>() / (train_no_cp.len() as f32)).sqrt();
354 let ytrain = fft(train_no_cp);
355 let train_known = known_training_symbols(used_bins.len(), cfg.modulation);
356 let mut hest = equalizer_initial_channel(cfg, &ytrain, &used_bins, &train_known);
357 let mut eq_state = EqualizerTrackingState::default();
358 equalizer_reset_tracking(&mut eq_state);
359 let mut mags = hest.iter().map(|h| h.norm()).collect::<Vec<_>>();
360 let mut ytrain_eq = Vec::with_capacity(used_bins.len());
361 for (k, &bin) in used_bins.iter().enumerate() {
362 ytrain_eq.push(regularized_equalize(ytrain[bin], hest[k]));
363 }
364 let train_recon_evm = rms_evm(&ytrain_eq, &train_known);
365 let data_start = xsync_len + train_len;
366 let sym_len = cfg.nfft + cfg.ncp;
367 let max_payload_bytes = cfg.packet_payload_bytes + 16;
368 let max_bits = fec_encoded_bits_len(max_payload_bytes * 8, cfg.fec_mode);
369 let max_data_ofdm =
370 max_bits.div_ceil(data_bins.len().max(1) * cfg.modulation.bits_per_symbol()) + 2;
371 let symbol_plan = packet_symbol_plan(max_data_ofdm, cfg);
372 let mut pilot_eq = Vec::new();
373 let mut pilot_ref = Vec::new();
374 let mut pilot_eq_post = Vec::new();
375 let mut post_eq_data = Vec::new();
376 let mut data_symbol_idx = 0usize;
377 for (sym_idx, kind) in symbol_plan.into_iter().enumerate() {
378 let s0 = data_start + sym_idx * sym_len;
379 let s1 = s0 + sym_len;
380 if s1 > rbb_cfo.len() || data_symbol_idx >= 3 {
381 break;
382 }
383 let y = fft(&rbb_cfo[s0 + cfg.ncp..s0 + cfg.ncp + cfg.nfft]);
384 if kind == PacketSymbolKind::Training {
385 equalizer_refresh_channel(cfg, &mut hest, &y, &used_bins, &train_known);
386 equalizer_reset_tracking(&mut eq_state);
387 mags.extend(hest.iter().map(|h| h.norm()));
388 continue;
389 }
390 let pref = known_pilot_symbols(pilot_bins.len(), data_symbol_idx + 1);
391 let xeq_used = equalize_symbol_with_pilots(
392 cfg,
393 &mut eq_state,
394 &y,
395 &used_bins,
396 &pilot_bins,
397 &pref,
398 &hest,
399 );
400 if !pilot_bins.is_empty() {
401 for (k, pbin) in pilot_bins.iter().enumerate() {
402 if let Some(pos) = used_bins.iter().position(|b| b == pbin) {
403 pilot_eq.push(xeq_used[pos]);
404 pilot_ref.push(pref[k]);
405 pilot_eq_post.push(xeq_used[pos]);
406 }
407 }
408 }
409 for dbin in &data_bins {
410 if let Some(pos) = used_bins.iter().position(|b| b == dbin) {
411 post_eq_data.push(xeq_used[pos]);
412 }
413 }
414 data_symbol_idx += 1;
415 }
416 let hest_mag_min = mags.iter().copied().fold(f32::INFINITY, f32::min);
417 let hest_mag_max = mags.iter().copied().fold(0.0f32, f32::max);
418 let hest_mag_mean = mags.iter().sum::<f32>() / (mags.len() as f32);
419 let pilot_residual_evm = rms_evm(&pilot_eq, &pilot_ref);
420 let pilot_post_evm = rms_evm(&pilot_eq_post, &pilot_ref);
421 let post_eq_evm = decision_directed_evm(&post_eq_data, cfg.modulation);
422 let decoded = decode_packet_info_baseband(&rbb_cfo, cfg);
423
424 PassbandDiagnostics {
425 enough_samples: true,
426 sync_off: sync_off.round() as usize,
427 cfo_hz,
428 sync_rms,
429 sync_peak,
430 post_rms,
431 post_peak,
432 train_rms,
433 hest_mag_min,
434 hest_mag_mean,
435 hest_mag_max,
436 train_recon_evm,
437 pilot_residual_evm,
438 pilot_post_evm,
439 post_eq_evm,
440 decoded: decoded.is_some(),
441 decoded_payload_len: decoded.map(|p| p.payload.len()),
442 }
443}
444
445pub(crate) fn dump_passband_constellation_impl(
453 pkt_audio: &[f32],
454 cfg: &OfdmConfig,
455 sync_off: f32,
456) -> Option<PassbandConstellationDump> {
457 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
458 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
459 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
460 return None;
461 }
462
463 let passband = &pkt_audio[wake_len + guard_len..];
464 let pre = 128usize.min(passband.len());
465 let mut chunk = vec![0.0f32; pre];
466 chunk.extend_from_slice(passband);
467 let rbb_full = downconvert_passband(&chunk, cfg);
468 let rbb = &rbb_full[pre..];
469 let rbb_sync = resample_from_offset(rbb, sync_off);
470 let rbb_cfo = coarse_cfo_correct(&rbb_sync, cfg);
471
472 let (used_bins, pilot_bins, data_bins) = ofdm_bin_plan(cfg);
473 if used_bins.is_empty() || data_bins.is_empty() {
474 return None;
475 }
476 let xsync_len = 2 * cfg.sync_half_len;
477 let train_len = cfg.nfft + cfg.ncp;
478 if rbb_cfo.len() < xsync_len + train_len + cfg.nfft + cfg.ncp {
479 return None;
480 }
481
482 let train_start = xsync_len;
483 let train_no_cp = &rbb_cfo[train_start + cfg.ncp..train_start + cfg.ncp + cfg.nfft];
484 let ytrain = fft(train_no_cp);
485 let train_known = known_training_symbols(used_bins.len(), cfg.modulation);
486 let mut hest = equalizer_initial_channel(cfg, &ytrain, &used_bins, &train_known);
487 let mut eq_state = EqualizerTrackingState::default();
488 equalizer_reset_tracking(&mut eq_state);
489
490 let data_start = xsync_len + train_len;
491 let sym_len = cfg.nfft + cfg.ncp;
492 let mut pre_eq = Vec::new();
493 let mut post_eq = Vec::new();
494 let max_payload_bytes = cfg.packet_payload_bytes + 16;
495 let max_bits = fec_encoded_bits_len(max_payload_bytes * 8, cfg.fec_mode);
496 let max_data_ofdm =
497 max_bits.div_ceil(data_bins.len().max(1) * cfg.modulation.bits_per_symbol()) + 2;
498 let symbol_plan = packet_symbol_plan(max_data_ofdm, cfg);
499 let mut data_symbol_idx = 0usize;
500 for (sym_idx, kind) in symbol_plan.into_iter().enumerate() {
501 let s0 = data_start + sym_idx * sym_len;
502 let s1 = s0 + sym_len;
503 if s1 > rbb_cfo.len() || data_symbol_idx >= 8 {
504 break;
505 }
506 let y = fft(&rbb_cfo[s0 + cfg.ncp..s0 + cfg.ncp + cfg.nfft]);
507 if kind == PacketSymbolKind::Training {
508 equalizer_refresh_channel(cfg, &mut hest, &y, &used_bins, &train_known);
509 equalizer_reset_tracking(&mut eq_state);
510 continue;
511 }
512 let pref = known_pilot_symbols(pilot_bins.len(), data_symbol_idx + 1);
513 let xeq_used = equalize_symbol_with_pilots(
514 cfg,
515 &mut eq_state,
516 &y,
517 &used_bins,
518 &pilot_bins,
519 &pref,
520 &hest,
521 );
522 for dbin in &data_bins {
523 if let Some(pos) = used_bins.iter().position(|b| b == dbin) {
524 pre_eq.push(y[*dbin]);
525 post_eq.push(xeq_used[pos]);
526 }
527 }
528 data_symbol_idx += 1;
529 }
530
531 Some(PassbandConstellationDump { pre_eq, post_eq })
532}
533
534pub(crate) fn dump_passband_pilot_tracking_impl(
535 pkt_audio: &[f32],
536 cfg: &OfdmConfig,
537) -> Option<PassbandPilotTrackDump> {
538 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
539 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
540 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
541 return None;
542 }
543
544 let passband = &pkt_audio[wake_len + guard_len..];
545 let pre = 128usize.min(passband.len());
546 let mut chunk = vec![0.0f32; pre];
547 chunk.extend_from_slice(passband);
548 let rbb_full = downconvert_passband(&chunk, cfg);
549 let rbb = &rbb_full[pre..];
550 let coarse_sync_off = find_repeated_half_sync_offset(rbb, cfg);
551 let sync_off = refine_sync_offset(rbb, cfg, coarse_sync_off);
552 let rbb_sync = resample_from_offset(rbb, sync_off);
553 let rbb_cfo = coarse_cfo_correct(&rbb_sync, cfg);
554
555 let (used_bins, pilot_bins, data_bins) = ofdm_bin_plan(cfg);
556 if used_bins.is_empty() || pilot_bins.is_empty() || data_bins.is_empty() {
557 return Some(PassbandPilotTrackDump {
558 pilot_phase_rad: Vec::new(),
559 pilot_evm_pre: Vec::new(),
560 pilot_evm_post: Vec::new(),
561 hest_mag_mean: Vec::new(),
562 hest_mag_max: Vec::new(),
563 });
564 }
565 let xsync_len = 2 * cfg.sync_half_len;
566 let train_len = cfg.nfft + cfg.ncp;
567 if rbb_cfo.len() < xsync_len + train_len + cfg.nfft + cfg.ncp {
568 return None;
569 }
570
571 let train_start = xsync_len;
572 let train_no_cp = &rbb_cfo[train_start + cfg.ncp..train_start + cfg.ncp + cfg.nfft];
573 let ytrain = fft(train_no_cp);
574 let train_known = known_training_symbols(used_bins.len(), cfg.modulation);
575 let mut hest = equalizer_initial_channel(cfg, &ytrain, &used_bins, &train_known);
576 let mut eq_state = EqualizerTrackingState::default();
577 equalizer_reset_tracking(&mut eq_state);
578
579 let data_start = xsync_len + train_len;
580 let sym_len = cfg.nfft + cfg.ncp;
581 let max_payload_bytes = cfg.packet_payload_bytes + 16;
582 let max_bits = fec_encoded_bits_len(max_payload_bytes * 8, cfg.fec_mode);
583 let max_data_ofdm =
584 max_bits.div_ceil(data_bins.len().max(1) * cfg.modulation.bits_per_symbol()) + 2;
585 let symbol_plan = packet_symbol_plan(max_data_ofdm, cfg);
586 let mut data_symbol_idx = 0usize;
587 let mut pilot_phase_rad = Vec::new();
588 let mut pilot_evm_pre = Vec::new();
589 let mut pilot_evm_post = Vec::new();
590 let mut hest_mag_mean = Vec::new();
591 let mut hest_mag_max = Vec::new();
592 for (sym_idx, kind) in symbol_plan.into_iter().enumerate() {
593 let s0 = data_start + sym_idx * sym_len;
594 let s1 = s0 + sym_len;
595 if s1 > rbb_cfo.len() {
596 break;
597 }
598 let y = fft(&rbb_cfo[s0 + cfg.ncp..s0 + cfg.ncp + cfg.nfft]);
599 if kind == PacketSymbolKind::Training {
600 equalizer_refresh_channel(cfg, &mut hest, &y, &used_bins, &train_known);
601 equalizer_reset_tracking(&mut eq_state);
602 let mags = hest.iter().map(|h| h.norm()).collect::<Vec<_>>();
603 hest_mag_mean.push(mags.iter().sum::<f32>() / (mags.len() as f32));
604 hest_mag_max.push(mags.iter().copied().fold(0.0f32, f32::max));
605 continue;
606 }
607 let pref = known_pilot_symbols(pilot_bins.len(), data_symbol_idx + 1);
608 let xeq_used = equalize_symbol_with_pilots(
609 cfg,
610 &mut eq_state,
611 &y,
612 &used_bins,
613 &pilot_bins,
614 &pref,
615 &hest,
616 );
617 let phase = pilot_phase_error(&xeq_used, &used_bins, &pilot_bins, &pref).unwrap_or(0.0);
618 let mut pilot_eq_pre = Vec::new();
619 let mut pilot_ref = Vec::new();
620 for (k, pbin) in pilot_bins.iter().enumerate() {
621 if let Some(pos) = used_bins.iter().position(|b| b == pbin) {
622 pilot_eq_pre.push(xeq_used[pos]);
623 pilot_ref.push(pref[k]);
624 }
625 }
626 let mut pilot_eq_post = Vec::new();
627 for (k, pbin) in pilot_bins.iter().enumerate() {
628 if let Some(pos) = used_bins.iter().position(|b| b == pbin) {
629 let _ = k;
630 pilot_eq_post.push(xeq_used[pos]);
631 }
632 }
633 pilot_phase_rad.push(phase);
634 pilot_evm_pre.push(rms_evm(&pilot_eq_pre, &pilot_ref));
635 pilot_evm_post.push(rms_evm(&pilot_eq_post, &pilot_ref));
636 data_symbol_idx += 1;
637 }
638
639 Some(PassbandPilotTrackDump {
640 pilot_phase_rad,
641 pilot_evm_pre,
642 pilot_evm_post,
643 hest_mag_mean,
644 hest_mag_max,
645 })
646}
647
648pub(crate) fn dump_passband_bins_impl(
649 pkt_audio: &[f32],
650 cfg: &OfdmConfig,
651) -> Option<PassbandBinDump> {
652 dump_passband_bins_with_sync_opt(pkt_audio, cfg, None)
653}
654
655pub(crate) fn dump_passband_bins_with_sync_impl(
656 pkt_audio: &[f32],
657 cfg: &OfdmConfig,
658 sync_off: f32,
659) -> Option<PassbandBinDump> {
660 dump_passband_bins_with_sync_opt(pkt_audio, cfg, Some(sync_off))
661}
662
663fn dump_passband_bins_with_sync_opt(
664 pkt_audio: &[f32],
665 cfg: &OfdmConfig,
666 forced_sync_off: Option<f32>,
667) -> Option<PassbandBinDump> {
668 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
669 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
670 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
671 return None;
672 }
673
674 let passband = &pkt_audio[wake_len + guard_len..];
675 let pre = 128usize.min(passband.len());
676 let mut chunk = vec![0.0f32; pre];
677 chunk.extend_from_slice(passband);
678 let rbb_full = downconvert_passband(&chunk, cfg);
679 let rbb = &rbb_full[pre..];
680 let coarse_sync_off = find_repeated_half_sync_offset(rbb, cfg);
681 let sync_off = forced_sync_off.unwrap_or_else(|| refine_sync_offset(rbb, cfg, coarse_sync_off));
682 let rbb_sync = resample_from_offset(rbb, sync_off);
683 let rbb_cfo = coarse_cfo_correct(&rbb_sync, cfg);
684
685 let (used_bins, pilot_bins, data_bins) = ofdm_bin_plan(cfg);
686 if used_bins.is_empty() || data_bins.is_empty() {
687 return None;
688 }
689 let xsync_len = 2 * cfg.sync_half_len;
690 let train_len = cfg.nfft + cfg.ncp;
691 if rbb_cfo.len() < xsync_len + train_len + cfg.nfft + cfg.ncp {
692 return None;
693 }
694
695 let train_start = xsync_len;
696 let train_no_cp = &rbb_cfo[train_start + cfg.ncp..train_start + cfg.ncp + cfg.nfft];
697 let ytrain = fft(train_no_cp);
698 let train_known = known_training_symbols(used_bins.len(), cfg.modulation);
699 let mut hest = equalizer_initial_channel(cfg, &ytrain, &used_bins, &train_known);
700 let mut eq_state = EqualizerTrackingState::default();
701 equalizer_reset_tracking(&mut eq_state);
702
703 let data_start = xsync_len + train_len;
704 let sym_len = cfg.nfft + cfg.ncp;
705 let max_payload_bytes = cfg.packet_payload_bytes + 16;
706 let max_bits = fec_encoded_bits_len(max_payload_bytes * 8, cfg.fec_mode);
707 let max_data_ofdm =
708 max_bits.div_ceil(data_bins.len().max(1) * cfg.modulation.bits_per_symbol()) + 2;
709 let symbol_plan = packet_symbol_plan(max_data_ofdm, cfg);
710 let mut data_symbol_idx = 0usize;
711 let mut rows = Vec::new();
712 for (sym_idx, kind) in symbol_plan.into_iter().enumerate() {
713 let s0 = data_start + sym_idx * sym_len;
714 let s1 = s0 + sym_len;
715 if s1 > rbb_cfo.len() || data_symbol_idx >= 8 {
716 break;
717 }
718 let y = fft(&rbb_cfo[s0 + cfg.ncp..s0 + cfg.ncp + cfg.nfft]);
719 if kind == PacketSymbolKind::Training {
720 equalizer_refresh_channel(cfg, &mut hest, &y, &used_bins, &train_known);
721 equalizer_reset_tracking(&mut eq_state);
722 continue;
723 }
724 let pref = known_pilot_symbols(pilot_bins.len(), data_symbol_idx + 1);
725 let xeq_used = equalize_symbol_with_pilots(
726 cfg,
727 &mut eq_state,
728 &y,
729 &used_bins,
730 &pilot_bins,
731 &pref,
732 &hest,
733 );
734 let mut pre_eq_used = Vec::with_capacity(used_bins.len());
735 let mut post_eq_used = Vec::with_capacity(used_bins.len());
736 for (k, &bin) in used_bins.iter().enumerate() {
737 pre_eq_used.push(y[bin]);
738 post_eq_used.push(xeq_used[k]);
739 }
740 for (k, &ubin) in used_bins.iter().enumerate() {
741 let role = if pilot_bins.contains(&ubin) {
742 "pilot"
743 } else {
744 "data"
745 };
746 let reference = if role == "pilot" {
747 pilot_bins
748 .iter()
749 .position(|b| *b == ubin)
750 .map(|pos| pref[pos])
751 } else {
752 None
753 };
754 rows.push(PassbandBinDumpRow {
755 data_symbol_idx: data_symbol_idx + 1,
756 used_bin: ubin,
757 role,
758 pre_eq: pre_eq_used[k],
759 post_eq: post_eq_used[k],
760 reference,
761 });
762 }
763 data_symbol_idx += 1;
764 }
765
766 Some(PassbandBinDump { rows })
767}
768
769pub(crate) fn dump_passband_channel_compare_with_sync_impl(
770 payload: &[u8],
771 pkt_audio: &[f32],
772 cfg: &OfdmConfig,
773 sync_off: f32,
774) -> Option<PassbandChannelCompareDump> {
775 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
776 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
777 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
778 return None;
779 }
780
781 let pkt_bytes = build_packet_bytes(payload, 0, 1, cfg);
782 let xbb = tx_one_packet_baseband(&pkt_bytes, cfg);
783 let passband = &pkt_audio[wake_len + guard_len..];
784 let pre = 128usize.min(passband.len());
785 let mut chunk = vec![0.0f32; pre];
786 chunk.extend_from_slice(passband);
787 let rbb_full = downconvert_passband(&chunk, cfg);
788 let rbb = &rbb_full[pre..];
789 let rbb_sync = resample_from_offset(rbb, sync_off);
790 let rbb_cfo = coarse_cfo_correct(&rbb_sync, cfg);
791
792 let (used_bins, pilot_bins, data_bins) = ofdm_bin_plan(cfg);
793 if used_bins.is_empty() || data_bins.is_empty() {
794 return None;
795 }
796 let xsync_len = 2 * cfg.sync_half_len;
797 let train_len = cfg.nfft + cfg.ncp;
798 if rbb_cfo.len() < xsync_len + train_len + cfg.nfft + cfg.ncp
799 || xbb.len() < xsync_len + train_len
800 {
801 return None;
802 }
803
804 let train_start = xsync_len;
805 let train_no_cp = &rbb_cfo[train_start + cfg.ncp..train_start + cfg.ncp + cfg.nfft];
806 let ytrain = fft(train_no_cp);
807 let train_known = known_training_symbols(used_bins.len(), cfg.modulation);
808 let mut hest = equalizer_initial_channel(cfg, &ytrain, &used_bins, &train_known);
809 let mut eq_state = EqualizerTrackingState::default();
810 equalizer_reset_tracking(&mut eq_state);
811
812 let data_start = xsync_len + train_len;
813 let sym_len = cfg.nfft + cfg.ncp;
814 let max_payload_bytes = cfg.packet_payload_bytes + 16;
815 let max_bits = fec_encoded_bits_len(max_payload_bytes * 8, cfg.fec_mode);
816 let max_data_ofdm =
817 max_bits.div_ceil(data_bins.len().max(1) * cfg.modulation.bits_per_symbol()) + 2;
818 let symbol_plan = packet_symbol_plan(max_data_ofdm, cfg);
819 let mut data_symbol_idx = 0usize;
820 let mut rows = Vec::new();
821 for (sym_idx, kind) in symbol_plan.into_iter().enumerate() {
822 let s0 = data_start + sym_idx * sym_len;
823 let s1 = s0 + sym_len;
824 if s1 > rbb_cfo.len() || s1 > xbb.len() || data_symbol_idx >= 8 {
825 break;
826 }
827 let y = fft(&rbb_cfo[s0 + cfg.ncp..s0 + cfg.ncp + cfg.nfft]);
828 let x = fft(&xbb[s0 + cfg.ncp..s0 + cfg.ncp + cfg.nfft]);
829 if kind == PacketSymbolKind::Training {
830 equalizer_refresh_channel(cfg, &mut hest, &y, &used_bins, &train_known);
831 equalizer_reset_tracking(&mut eq_state);
832 continue;
833 }
834 let pref = known_pilot_symbols(pilot_bins.len(), data_symbol_idx + 1);
835 let xeq_used = equalize_symbol_with_pilots(
836 cfg,
837 &mut eq_state,
838 &y,
839 &used_bins,
840 &pilot_bins,
841 &pref,
842 &hest,
843 );
844 let mut phase_by_bin = vec![0.0f32; used_bins.len()];
845 if !pilot_bins.is_empty() && !pref.is_empty() {
846 let mut pilot_phase_pts = Vec::<(f32, f32)>::new();
847 for (k, pbin) in pilot_bins.iter().enumerate() {
848 if let Some(pos) = used_bins.iter().position(|b| b == pbin) {
849 let z0 = regularized_equalize(y[*pbin], hest[pos]);
850 let ref_sym = pref[k];
851 if ref_sym.norm_sqr() > 1.0e-9 {
852 pilot_phase_pts.push((*pbin as f32, (z0 * ref_sym.conj()).arg()));
853 }
854 }
855 }
856 if pilot_phase_pts.len() >= 2 {
857 for i in 1..pilot_phase_pts.len() {
858 let mut phi = pilot_phase_pts[i].1;
859 let prev = pilot_phase_pts[i - 1].1;
860 while phi - prev > std::f32::consts::PI {
861 phi -= 2.0 * std::f32::consts::PI;
862 }
863 while phi - prev < -std::f32::consts::PI {
864 phi += 2.0 * std::f32::consts::PI;
865 }
866 pilot_phase_pts[i].1 = phi;
867 }
868 let n = pilot_phase_pts.len() as f32;
869 let sx = pilot_phase_pts.iter().map(|(x, _)| *x).sum::<f32>();
870 let sy = pilot_phase_pts.iter().map(|(_, y)| *y).sum::<f32>();
871 let sxx = pilot_phase_pts.iter().map(|(x, _)| x * x).sum::<f32>();
872 let sxy = pilot_phase_pts.iter().map(|(x, y)| x * y).sum::<f32>();
873 let denom = n * sxx - sx * sx;
874 if denom.abs() > 1.0e-9 {
875 let slope = (n * sxy - sx * sy) / denom;
876 let intercept = (sy - slope * sx) / n;
877 for (k, &bin) in used_bins.iter().enumerate() {
878 phase_by_bin[k] = intercept + slope * (bin as f32);
879 }
880 }
881 }
882 }
883 let mut num = Complex32::new(0.0, 0.0);
884 let mut den = 0.0f32;
885 for (k, pbin) in pilot_bins.iter().enumerate() {
886 if let Some(pos) = used_bins.iter().position(|b| b == pbin) {
887 num += xeq_used[pos] * pref[k].conj();
888 den += pref[k].norm_sqr();
889 }
890 }
891 let g = if den > 1.0e-9 {
892 num / den
893 } else {
894 Complex32::new(1.0, 0.0)
895 };
896 for (k, &ubin) in used_bins.iter().enumerate() {
897 let xref = x[ubin];
898 if xref.norm() <= 1.0e-9 {
899 continue;
900 }
901 let role = if pilot_bins.contains(&ubin) {
902 "pilot"
903 } else {
904 "data"
905 };
906 rows.push(PassbandChannelCompareRow {
907 data_symbol_idx: data_symbol_idx + 1,
908 used_bin: ubin,
909 role,
910 actual_h: y[ubin] / xref,
911 estimated_h_train: hest[k],
912 estimated_h_pilot: hest[k] * Complex32::from_polar(1.0, phase_by_bin[k]) * g,
913 });
914 }
915 data_symbol_idx += 1;
916 }
917
918 Some(PassbandChannelCompareDump { rows })
919}
920
921pub(crate) fn dump_passband_sync_metric_impl(
929 pkt_audio: &[f32],
930 cfg: &OfdmConfig,
931) -> Option<PassbandSyncDump> {
932 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
933 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
934 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
935 return None;
936 }
937
938 let passband = &pkt_audio[wake_len + guard_len..];
939 let pre = 128usize.min(passband.len());
940 let mut chunk = vec![0.0f32; pre];
941 chunk.extend_from_slice(passband);
942 let rbb_full = downconvert_passband(&chunk, cfg);
943 let rbb = &rbb_full[pre..];
944 let metrics = repeated_half_sync_metrics(rbb, cfg);
945 let coarse_sync_off = find_repeated_half_sync_offset(rbb, cfg);
946 let refined_sync_off = refine_sync_offset(rbb, cfg, coarse_sync_off);
947 Some(PassbandSyncDump {
948 coarse_sync_off,
949 refined_sync_off: refined_sync_off.round() as usize,
950 metrics,
951 })
952}
953
954pub(crate) fn dump_passband_iq_chain_impl(
955 pkt_audio: &[f32],
956 cfg: &OfdmConfig,
957) -> Option<PassbandIqChainDump> {
958 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
959 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
960 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
961 return None;
962 }
963
964 let passband = &pkt_audio[wake_len + guard_len..];
965 let pre = 128usize.min(passband.len());
966 let mut chunk = vec![0.0f32; pre];
967 chunk.extend_from_slice(passband);
968 let down_audio = iq_downconvert(&chunk, cfg.fs, cfg.fc, passband_lpf_cutoff_hz(cfg));
969 let baseband = match cfg.passband_mode {
970 PassbandMode::Legacy => down_audio.clone(),
971 PassbandMode::Iq => resample_complex_linear_rate(&down_audio, cfg.fs, cfg.fs_baseband),
972 };
973 let pre_baseband = match cfg.passband_mode {
974 PassbandMode::Legacy => pre,
975 PassbandMode::Iq => ((pre as f32) * (cfg.fs_baseband / cfg.fs)).round() as usize,
976 };
977 Some(PassbandIqChainDump {
978 downconverted_audio_rate: down_audio[pre..].to_vec(),
979 baseband_rate: baseband[pre_baseband.min(baseband.len())..].to_vec(),
980 fs_audio: cfg.fs,
981 fs_baseband: active_baseband_fs(cfg),
982 })
983}
984
985fn tx_one_packet(pkt_bytes: &[u8], cfg: &OfdmConfig) -> Vec<f32> {
993 let passband = tx_one_packet_body(pkt_bytes, cfg);
994 let mut wake = make_wake_tone(cfg);
995 normalize_in_place(&mut wake, 0.25);
996 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
997
998 let mut out = Vec::with_capacity(wake.len() + guard_len + passband.len());
999 out.extend_from_slice(&wake);
1000 out.extend(std::iter::repeat(0.0).take(guard_len));
1001 out.extend(passband);
1002 out
1003}
1004
1005fn tx_one_packet_body(pkt_bytes: &[u8], cfg: &OfdmConfig) -> Vec<f32> {
1006 let xbb = tx_one_packet_baseband(pkt_bytes, cfg);
1007 let mut passband = upconvert_passband(&xbb, cfg);
1008 if (cfg.payload_gain - 1.0).abs() > f32::EPSILON {
1009 for s in &mut passband {
1010 *s *= cfg.payload_gain;
1011 }
1012 }
1013 let sync_len = (2 * cfg.sync_half_len).min(passband.len());
1014 if sync_len > 0 {
1015 normalize_in_place(&mut passband[..sync_len], 0.35);
1016 }
1017 if sync_len < passband.len() {
1018 normalize_in_place(&mut passband[sync_len..], 0.96);
1019 }
1020 passband
1021}
1022
1023fn upconvert_passband(xbb: &[Complex32], cfg: &OfdmConfig) -> Vec<f32> {
1024 match cfg.passband_mode {
1025 PassbandMode::Legacy => iq_upconvert(xbb, cfg.fs, cfg.fc),
1026 PassbandMode::Iq => {
1027 let xbb_audio = resample_complex_linear_rate(xbb, cfg.fs_baseband, cfg.fs);
1028 iq_upconvert(&xbb_audio, cfg.fs, cfg.fc)
1029 }
1030 }
1031}
1032
1033fn passband_lpf_cutoff_hz(cfg: &OfdmConfig) -> f32 {
1034 let (used_bins, _, _) = ofdm_bin_plan(cfg);
1035 let max_bin = used_bins.iter().copied().max().unwrap_or(1) as f32;
1036 let bw = ((max_bin + 2.0) * active_baseband_fs(cfg) / cfg.nfft as f32).max(500.0);
1037 bw.min(0.45 * cfg.fs).min(0.45 * active_baseband_fs(cfg))
1038}
1039
1040fn downconvert_passband(pkt_audio: &[f32], cfg: &OfdmConfig) -> Vec<Complex32> {
1041 let mixed = iq_downconvert(pkt_audio, cfg.fs, cfg.fc, passband_lpf_cutoff_hz(cfg));
1042 match cfg.passband_mode {
1043 PassbandMode::Legacy => mixed,
1044 PassbandMode::Iq => resample_complex_linear_rate(&mixed, cfg.fs, cfg.fs_baseband),
1045 }
1046}
1047
1048fn decode_packet_from_passband(pkt_audio: &[f32], cfg: &OfdmConfig) -> Option<PacketInfo> {
1056 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
1057 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
1058 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
1059 return None;
1060 }
1061
1062 let passband = &pkt_audio[wake_len + guard_len..];
1063 let pre = 128usize.min(passband.len());
1064 let mut chunk = vec![0.0f32; pre];
1065 chunk.extend_from_slice(passband);
1066 let rbb_full = downconvert_passband(&chunk, cfg);
1067 let rbb = &rbb_full[pre..];
1068 let coarse_sync_off = find_repeated_half_sync_offset(rbb, cfg);
1069 let sync_off = refine_sync_offset(rbb, cfg, coarse_sync_off);
1070 for off in [sync_off, coarse_sync_off as f32] {
1071 let rbb_sync = resample_from_offset(rbb, off);
1072 if let Some(pkt) = decode_packet_from_synced_baseband(&rbb_sync, cfg) {
1073 return Some(pkt);
1074 }
1075 }
1076 None
1077}
1078
1079fn decode_packet_from_passband_with_sync_rate(
1080 pkt_audio: &[f32],
1081 cfg: &OfdmConfig,
1082 sync_off: f32,
1083 time_scale: f32,
1084) -> Option<PacketInfo> {
1085 let wake_len = (cfg.wake_ms * 1e-3 * cfg.fs) as usize;
1086 let guard_len = (cfg.wake_guard_ms * 1e-3 * cfg.fs) as usize;
1087 if pkt_audio.len() <= wake_len + guard_len + cfg.nfft + cfg.ncp {
1088 return None;
1089 }
1090
1091 let passband = &pkt_audio[wake_len + guard_len..];
1092 let pre = 128usize.min(passband.len());
1093 let mut chunk = vec![0.0f32; pre];
1094 chunk.extend_from_slice(passband);
1095 let rbb_full = downconvert_passband(&chunk, cfg);
1096 let rbb = &rbb_full[pre..];
1097 let rbb_sync = resample_from_offset_rate(rbb, sync_off, time_scale);
1098 decode_packet_from_synced_baseband(&rbb_sync, cfg)
1099}
1100
1101fn decode_packet_from_synced_baseband(
1102 rbb_sync: &[Complex32],
1103 cfg: &OfdmConfig,
1104) -> Option<PacketInfo> {
1105 let coarse_cfo_hz = estimate_coarse_cfo_hz(rbb_sync, cfg);
1106 let mut tried = Vec::new();
1107 tried.push(coarse_cfo_hz);
1108 for hz in [-12.0f32, -8.0, -4.0, 4.0, 8.0, 12.0] {
1109 tried.push(coarse_cfo_hz + hz);
1110 }
1111 for cfo_hz in tried {
1112 let rbb_cfo = apply_cfo_hz(rbb_sync, active_baseband_fs(cfg), cfo_hz);
1113 if let Some(pkt) = decode_packet_info_baseband(&rbb_cfo, cfg) {
1114 return Some(pkt);
1115 }
1116 }
1117 None
1118}
1119
1120fn iq_upconvert(xbb: &[Complex32], fs: f32, fc: f32) -> Vec<f32> {
1136 xbb.iter()
1137 .enumerate()
1138 .map(|(n, x)| {
1139 let ph = 2.0 * std::f32::consts::PI * fc * (n as f32) / fs;
1140 (x * Complex32::from_polar(1.0, ph)).re
1141 })
1142 .collect()
1143}
1144
1145fn iq_downconvert(y: &[f32], fs: f32, fc: f32, cutoff_hz: f32) -> Vec<Complex32> {
1155 let mixed: Vec<Complex32> = y
1156 .iter()
1157 .enumerate()
1158 .map(|(n, &s)| {
1159 let ph = -2.0 * std::f32::consts::PI * fc * (n as f32) / fs;
1160 Complex32::new(s, 0.0) * Complex32::from_polar(1.0, ph)
1161 })
1162 .collect();
1163 let mut out = lowpass_fir(&mixed, fs, cutoff_hz.max(100.0), 97);
1164 for z in &mut out {
1165 *z *= 2.0;
1166 }
1167 out
1168}
1169
1170fn normalize_in_place(x: &mut [f32], target_peak: f32) {
1178 let peak = x
1179 .iter()
1180 .fold(0.0f32, |a, &b| if b.abs() > a { b.abs() } else { a });
1181 if peak > 0.0 {
1182 let g = target_peak / peak;
1183 for v in x {
1184 *v *= g;
1185 }
1186 }
1187}
1188
1189fn lowpass_fir(x: &[Complex32], fs: f32, cutoff_hz: f32, len: usize) -> Vec<Complex32> {
1199 let h = fir_lowpass(len, cutoff_hz, fs);
1200 let mut y = vec![Complex32::new(0.0, 0.0); x.len()];
1201 for n in 0..x.len() {
1202 let mut acc = Complex32::new(0.0, 0.0);
1203 let kmax = (n + 1).min(h.len());
1204 for k in 0..kmax {
1205 acc += x[n - k] * h[k];
1206 }
1207 y[n] = acc;
1208 }
1209 let gd = (len - 1) / 2;
1211 let mut out = Vec::with_capacity(y.len());
1212 out.extend_from_slice(&y[gd..]);
1213 out.extend(std::iter::repeat(Complex32::new(0.0, 0.0)).take(gd));
1214 out
1215}
1216
1217fn fir_lowpass(len: usize, cutoff_hz: f32, fs: f32) -> Vec<f32> {
1226 let m = (len - 1) as f32 / 2.0;
1227 let mut h = vec![0.0f32; len];
1228 for (i, hi) in h.iter_mut().enumerate() {
1229 let n = i as f32 - m;
1230 let sinc = if n.abs() < 1e-6 {
1231 2.0 * cutoff_hz / fs
1232 } else {
1233 let x = 2.0 * std::f32::consts::PI * cutoff_hz * n / fs;
1234 (2.0 * cutoff_hz / fs) * (x.sin() / x)
1235 };
1236 let w = 0.54 - 0.46 * (2.0 * std::f32::consts::PI * i as f32 / (len as f32 - 1.0)).cos();
1237 *hi = sinc * w;
1238 }
1239 let sum: f32 = h.iter().sum();
1240 if sum != 0.0 {
1241 for v in &mut h {
1242 *v /= sum;
1243 }
1244 }
1245 h
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::*;
1251 use crate::config::Modulation;
1252 use crate::equalizer::hard_slice_symbols;
1253
1254 #[test]
1255 fn round_trip_oracle_burst() {
1257 let cfg = OfdmConfig::default();
1258 let payload = (0..80u8).collect::<Vec<_>>();
1259 let tx = encode_payload(&payload, &cfg);
1260 let rx = decode_encoded_burst_oracle(&tx, &cfg).expect("decode failed");
1261 assert_eq!(rx, payload);
1262 }
1263
1264 #[test]
1265 fn round_trip_oracle_burst_qpsk() {
1267 let mut cfg = OfdmConfig::default();
1268 cfg.modulation = Modulation::Qpsk;
1269 let payload = (0..96u8).collect::<Vec<_>>();
1270 let tx = encode_payload(&payload, &cfg);
1271 let rx = decode_encoded_burst_oracle(&tx, &cfg).expect("decode failed");
1272 assert_eq!(rx, payload);
1273 }
1274
1275 #[test]
1276 fn round_trip_oracle_burst_qpsk_no_pilots() {
1278 let mut cfg = OfdmConfig::default();
1279 cfg.modulation = Modulation::Qpsk;
1280 cfg.use_pilots = Some(false);
1281 let payload = (0..80u8).collect::<Vec<_>>();
1282 let tx = encode_payload(&payload, &cfg);
1283 let rx = decode_encoded_burst_oracle(&tx, &cfg).expect("decode failed");
1284 assert_eq!(rx, payload);
1285 }
1286
1287 #[test]
1288 fn decode_single_packet_passband_test() {
1290 let cfg = OfdmConfig::default();
1291 let payload: Vec<u8> = (40..64).collect();
1292 let y = encode_single_packet_passband(&payload, &cfg);
1293 let out =
1294 decode_single_packet_passband_with_sync(&y, &cfg, 0.0).expect("passband decode failed");
1295 assert_eq!(out, payload);
1296 }
1297
1298 #[test]
1299 fn hard_slice_bpsk_and_qpsk() {
1301 let bpsk = vec![Complex32::new(0.3, 0.0), Complex32::new(-0.2, 1.0)];
1302 let bpsk_s = hard_slice_symbols(&bpsk, Modulation::Bpsk);
1303 assert_eq!(bpsk_s[0], Complex32::new(1.0, 0.0));
1304 assert_eq!(bpsk_s[1], Complex32::new(-1.0, 0.0));
1305
1306 let qpsk = vec![
1307 Complex32::new(0.1, 0.2),
1308 Complex32::new(0.1, -0.2),
1309 Complex32::new(-0.1, 0.2),
1310 Complex32::new(-0.1, -0.2),
1311 ];
1312 let qpsk_s = hard_slice_symbols(&qpsk, Modulation::Qpsk);
1313 let k = 1.0 / 2.0f32.sqrt();
1314 assert_eq!(qpsk_s[0], Complex32::new(1.0 * k, 1.0 * k));
1315 assert_eq!(qpsk_s[1], Complex32::new(1.0 * k, -1.0 * k));
1316 assert_eq!(qpsk_s[2], Complex32::new(-1.0 * k, 1.0 * k));
1317 assert_eq!(qpsk_s[3], Complex32::new(-1.0 * k, -1.0 * k));
1318 }
1319}
1320
1321