Skip to content

Commit bbdffd6

Browse files
committed
Harden OFDM light sync transition and add simulator regression sweep
1 parent 96e51c1 commit bbdffd6

File tree

8 files changed

+227
-54
lines changed

8 files changed

+227
-54
lines changed

src/gui/modem/modem_mode.cpp

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,22 @@ void ModemEngine::setWaveformMode(protocol::WaveformMode mode) {
5050
// When disconnected, always use MC_DPSK for PING detection (chirp-based sync)
5151
if (streaming_decoder_) {
5252
protocol::WaveformMode decoder_mode = connected_ ? mode : protocol::WaveformMode::MC_DPSK;
53-
streaming_decoder_->setMode(decoder_mode, connected_);
54-
55-
// For OFDM modes, propagate the current config (for custom FFT/carriers like NVIS mode)
56-
if (mode == protocol::WaveformMode::OFDM_COX ||
57-
mode == protocol::WaveformMode::OFDM_CHIRP) {
58-
streaming_decoder_->setOFDMConfig(config_);
59-
LOG_MODEM(INFO, "setWaveformMode: StreamingDecoder OFDM config set (FFT=%d, carriers=%d)",
53+
if (connected_ &&
54+
(mode == protocol::WaveformMode::OFDM_COX ||
55+
mode == protocol::WaveformMode::OFDM_CHIRP)) {
56+
streaming_decoder_->setConnectedOFDMMode(mode, config_, data_modulation_, data_code_rate_);
57+
LOG_MODEM(INFO, "setWaveformMode: StreamingDecoder connected OFDM config set (FFT=%d, carriers=%d)",
6058
config_.fft_size, config_.num_carriers);
59+
} else {
60+
streaming_decoder_->setMode(decoder_mode, connected_);
61+
62+
// For OFDM modes, propagate the current config (for custom FFT/carriers like NVIS mode)
63+
if (mode == protocol::WaveformMode::OFDM_COX ||
64+
mode == protocol::WaveformMode::OFDM_CHIRP) {
65+
streaming_decoder_->setOFDMConfig(config_);
66+
LOG_MODEM(INFO, "setWaveformMode: StreamingDecoder OFDM config set (FFT=%d, carriers=%d)",
67+
config_.fft_size, config_.num_carriers);
68+
}
6169
}
6270
}
6371

@@ -141,8 +149,8 @@ void ModemEngine::setConnected(bool connected) {
141149
// For OFDM modes, propagate the correct config (with proper pilot settings)
142150
if (waveform_mode_ == protocol::WaveformMode::OFDM_COX ||
143151
waveform_mode_ == protocol::WaveformMode::OFDM_CHIRP) {
144-
streaming_decoder_->setOFDMConfig(config_);
145-
streaming_decoder_->setDataMode(data_modulation_, data_code_rate_);
152+
streaming_decoder_->setConnectedOFDMMode(
153+
waveform_mode_, config_, data_modulation_, data_code_rate_);
146154
}
147155

148156
// Propagate known CFO from handshake to StreamingDecoder
@@ -224,10 +232,17 @@ void ModemEngine::setDataMode(Modulation mod, CodeRate rate) {
224232
// Update StreamingDecoder's waveform configuration
225233
// CRITICAL: Set OFDM config BEFORE setDataMode so decoder has correct pilot layout
226234
if (streaming_decoder_) {
227-
if (waveform_mode_ != protocol::WaveformMode::MC_DPSK) {
228-
streaming_decoder_->setOFDMConfig(config_);
235+
if (connected_ &&
236+
(waveform_mode_ == protocol::WaveformMode::OFDM_CHIRP ||
237+
waveform_mode_ == protocol::WaveformMode::OFDM_COX)) {
238+
streaming_decoder_->setConnectedOFDMMode(
239+
waveform_mode_, config_, mod, rate);
240+
} else {
241+
if (waveform_mode_ != protocol::WaveformMode::MC_DPSK) {
242+
streaming_decoder_->setOFDMConfig(config_);
243+
}
244+
streaming_decoder_->setDataMode(mod, rate);
229245
}
230-
streaming_decoder_->setDataMode(mod, rate);
231246
}
232247

233248
// Update StreamingEncoder to match

src/gui/modem/streaming_decoder.cpp

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -509,23 +509,23 @@ void StreamingDecoder::searchForSync() {
509509
current_modulation_ == Modulation::BPSK);
510510
const float fading_hint = last_fading_index_.load();
511511
const float snr_hint = last_snr_.load();
512-
float light_sync_min_confidence = is_coherent ? 0.88f : 0.70f;
513-
float weak_sync_floor = is_coherent ? 0.80f : 0.30f;
512+
float light_sync_min_confidence = is_coherent ? 0.90f : 0.72f;
513+
float weak_sync_floor = is_coherent ? 0.85f : 0.55f;
514514
if (!is_coherent) {
515515
// OTA HF can produce valid LTS peaks in the 0.55-0.65 range during
516516
// fades. Start conservative, but avoid hard-locking to 0.70.
517517
if (fading_hint >= 1.00f || snr_hint < 10.0f) {
518-
light_sync_min_confidence = 0.55f;
518+
light_sync_min_confidence = 0.62f;
519519
} else if (fading_hint >= 0.70f || snr_hint < 14.0f) {
520-
light_sync_min_confidence = 0.58f;
520+
light_sync_min_confidence = 0.65f;
521521
} else if (fading_hint >= 0.50f || snr_hint < 18.0f) {
522-
light_sync_min_confidence = 0.62f;
522+
light_sync_min_confidence = 0.68f;
523523
}
524524

525525
if (connected_ && sync_reject_streak_ >= 8) {
526-
float extra_relax = std::min(0.18f,
526+
float extra_relax = std::min(0.12f,
527527
0.015f * static_cast<float>(sync_reject_streak_ - 7));
528-
light_sync_min_confidence = std::max(0.45f, light_sync_min_confidence - extra_relax);
528+
light_sync_min_confidence = std::max(0.56f, light_sync_min_confidence - extra_relax);
529529
}
530530
}
531531

@@ -540,8 +540,9 @@ void StreamingDecoder::searchForSync() {
540540
if (found && sync_result.correlation < light_sync_min_confidence) {
541541
bool allow_weak_accept = !is_coherent &&
542542
connected_ &&
543-
sync_reject_streak_ >= 4 &&
544-
sync_result.correlation >= weak_sync_floor;
543+
sync_reject_streak_ >= 3 &&
544+
sync_result.correlation >= std::max(weak_sync_floor,
545+
light_sync_min_confidence - 0.08f);
545546
if (allow_weak_accept) {
546547
weak_accept = true;
547548
LOG_MODEM(INFO, "[%s] DATA sync weak-accepted (corr=%.2f < %.2f, streak=%llu)",
@@ -839,8 +840,10 @@ void StreamingDecoder::decodeCurrentFrame() {
839840
if (mode_ == protocol::WaveformMode::MC_DPSK) {
840841
training_skip = 4608; // training + ref samples
841842
} else {
842-
// OFDM: check after preamble (2 LTS = ~1024 samples)
843-
training_skip = 1024;
843+
// OFDM: check after light preamble (training symbols)
844+
// Must be waveform/config dependent (FFT/CP/carriers).
845+
int data_preamble = waveform_->getDataPreambleSamples();
846+
training_skip = (data_preamble > 0) ? static_cast<size_t>(data_preamble) : size_t(1024);
844847
}
845848

846849
// Compute training region RMS (signal + noise)
@@ -1493,6 +1496,55 @@ void StreamingDecoder::setOFDMConfig(const ModemConfig& config) {
14931496
interleaver_ = std::make_unique<ChannelInterleaver>(bps, v2::LDPC_CODEWORD_BITS);
14941497
}
14951498

1499+
void StreamingDecoder::setConnectedOFDMMode(protocol::WaveformMode mode,
1500+
const ModemConfig& config,
1501+
Modulation mod,
1502+
CodeRate rate) {
1503+
std::lock_guard<std::mutex> lock(buffer_mutex_);
1504+
1505+
mode_ = mode;
1506+
connected_ = true;
1507+
code_rate_ = rate;
1508+
current_modulation_ = mod;
1509+
ofdm_carriers_ = config.num_carriers;
1510+
ofdm_data_carriers_ = config.getDataCarriers();
1511+
1512+
if (mode_ == protocol::WaveformMode::OFDM_CHIRP) {
1513+
waveform_ = std::make_unique<OFDMChirpWaveform>(config);
1514+
} else {
1515+
waveform_ = std::make_unique<OFDMNvisWaveform>(config);
1516+
}
1517+
1518+
if (waveform_) {
1519+
waveform_->configure(mod, rate);
1520+
}
1521+
1522+
// Query waveform for effective pilot layout after configure().
1523+
int pilot_spacing = waveform_ ? waveform_->getPilotSpacing() : 0;
1524+
if (pilot_spacing > 0) {
1525+
int pilot_count = (ofdm_carriers_ + pilot_spacing - 1) / pilot_spacing;
1526+
ofdm_data_carriers_ = ofdm_carriers_ - pilot_count;
1527+
} else {
1528+
ofdm_data_carriers_ = ofdm_carriers_;
1529+
}
1530+
1531+
size_t bps = ofdm_data_carriers_ * getBitsPerSymbol(mod);
1532+
interleaver_ = std::make_unique<ChannelInterleaver>(bps, v2::LDPC_CODEWORD_BITS);
1533+
1534+
state_ = DecoderState::SEARCHING;
1535+
pending_total_cw_ = 0;
1536+
sync_reject_streak_ = 0;
1537+
constellation_cache_.clear();
1538+
constellation_cache_time_ = std::chrono::steady_clock::time_point{};
1539+
burst_soft_buffer_.clear();
1540+
correlation_pos_ = write_pos_;
1541+
1542+
LOG_MODEM(INFO, "StreamingDecoder: connected OFDM mode=%s, mod=%s, rate=%s, carriers=%d data=%d bps=%zu",
1543+
protocol::waveformModeToString(mode_),
1544+
modulationToString(mod), codeRateToString(rate),
1545+
ofdm_carriers_, ofdm_data_carriers_, bps);
1546+
}
1547+
14961548
void StreamingDecoder::setDataMode(Modulation mod, CodeRate rate) {
14971549
std::lock_guard<std::mutex> lock(buffer_mutex_);
14981550
code_rate_ = rate;

src/gui/modem/streaming_decoder.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,13 @@ class StreamingDecoder {
143143
// Use this for NVIS mode (1024 FFT, 59 carriers) or custom OFDM settings
144144
void setOFDMConfig(const ModemConfig& config);
145145

146+
// Atomically apply OFDM connected-mode settings to avoid transient
147+
// mode/config mismatches during handshake transitions.
148+
void setConnectedOFDMMode(protocol::WaveformMode mode,
149+
const ModemConfig& config,
150+
Modulation mod,
151+
CodeRate rate);
152+
146153
// Set data mode (modulation and code rate) for the waveform
147154
// Called when connection is established with negotiated settings
148155
void setDataMode(Modulation mod, CodeRate rate);

src/ofdm/demodulator.cpp

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -865,8 +865,6 @@ bool OFDMDemodulator::process(SampleSpan samples) {
865865
LOG_SYNC(INFO, "SYNC: offset=%zu, corr=%.3f, CFO=%.1f Hz, buffer=%zu",
866866
sync_offset, sync_corr, coarse_cfo, impl_->rx_buffer.size());
867867

868-
impl_->last_sync_offset = sync_offset;
869-
870868
// LTS fine timing
871869
size_t sts_start = sync_offset;
872870
size_t refined_lts_start = impl_->refineLTSTiming(sts_start);
@@ -886,18 +884,28 @@ bool OFDMDemodulator::process(SampleSpan samples) {
886884
LOG_SYNC(INFO, "LTS fine timing: coarse_lts=%zu, refined=%zu, delta=%+d samples",
887885
coarse_lts_pos, refined_lts_start, timing_refinement);
888886

887+
// Report true preamble start for diagnostics/tests.
888+
// In practice the Schmidl-Cox coarse peak tends to land one symbol late,
889+
// so refined_lts_start is commonly the SECOND LTS symbol.
890+
// Preamble layout: 4 STS + 2 LTS.
891+
if (refined_lts_start >= 5 * preamble_symbol_len) {
892+
impl_->last_sync_offset = refined_lts_start - 5 * preamble_symbol_len;
893+
} else {
894+
impl_->last_sync_offset = 0;
895+
}
896+
889897
// Check if differential mode for LTS phase extraction
890898
bool is_differential = (impl_->config.modulation == Modulation::DQPSK ||
891899
impl_->config.modulation == Modulation::D8PSK ||
892900
impl_->config.modulation == Modulation::DBPSK);
893901
LOG_SYNC(DEBUG, "LTS phase check: is_differential=%d, use_pilots=%d",
894902
is_differential, impl_->config.use_pilots);
895903

896-
// Consume preamble: from start up through LTS
897-
// Data starts after: refined_lts_start + 2 LTS symbols
898-
size_t consume = refined_lts_start + 2 * preamble_symbol_len + impl_->manual_timing_offset;
899-
LOG_SYNC(DEBUG, "Consume calc: refined_lts=%zu + 2*preamble_sym=%zu + offset=%d = %zu",
900-
refined_lts_start, 2 * preamble_symbol_len, impl_->manual_timing_offset, consume);
904+
// Consume preamble up through last LTS. With second-LTS lock, data starts
905+
// one symbol after refined_lts_start.
906+
size_t consume = refined_lts_start + preamble_symbol_len + impl_->manual_timing_offset;
907+
LOG_SYNC(DEBUG, "Consume calc: refined_lts=%zu + preamble_sym=%zu + offset=%d = %zu",
908+
refined_lts_start, preamble_symbol_len, impl_->manual_timing_offset, consume);
901909
impl_->rx_buffer.erase(impl_->rx_buffer.begin(),
902910
impl_->rx_buffer.begin() + consume);
903911

@@ -945,14 +953,19 @@ bool OFDMDemodulator::process(SampleSpan samples) {
945953
if (impl_->state.load() == Impl::State::SYNCED) {
946954
// Check for new preamble (mid-frame detection)
947955
bool should_check_preamble = impl_->synced_symbol_count.load() > 0 &&
948-
impl_->idle_call_count.load() >= 2;
956+
impl_->idle_call_count.load() >= 2 &&
957+
impl_->soft_bits.empty();
949958

950959
if (should_check_preamble && impl_->rx_buffer.size() >= preamble_total_len) {
951960
size_t search_limit = std::min(impl_->rx_buffer.size() - preamble_total_len,
952961
impl_->symbol_samples * 2);
953962
constexpr size_t STEP = 8;
954963

955964
for (size_t offset = 0; offset <= search_limit; offset += STEP) {
965+
if (!impl_->hasMinimumEnergy(offset, correlation_window)) {
966+
continue;
967+
}
968+
956969
float corr = impl_->measureCorrelation(offset);
957970
if (corr > impl_->sync_threshold) {
958971
size_t sts_start = offset;
@@ -963,7 +976,7 @@ bool OFDMDemodulator::process(SampleSpan samples) {
963976
continue;
964977
}
965978

966-
size_t consume = refined_lts_start + 2 * preamble_symbol_len;
979+
size_t consume = refined_lts_start + preamble_symbol_len;
967980

968981
LOG_SYNC(INFO, "SYNCED preamble: sts=%zu, refined_lts=%zu, consume=%zu",
969982
sts_start, refined_lts_start, consume);
@@ -1462,6 +1475,7 @@ bool OFDMDemodulator::searchForSync(SampleSpan samples, size_t& out_position, fl
14621475
// Search for preamble
14631476
bool found_sync = false;
14641477
size_t sync_offset = 0;
1478+
size_t refined_lts_offset = 0;
14651479
float sync_cfo = 0.0f;
14661480

14671481
size_t search_end = impl_->rx_buffer.size() - preamble_total_len - correlation_window;
@@ -1501,10 +1515,11 @@ bool OFDMDemodulator::searchForSync(SampleSpan samples, size_t& out_position, fl
15011515
if (refined_lts != SIZE_MAX) {
15021516
found_sync = true;
15031517
sync_offset = peak_pos;
1518+
refined_lts_offset = refined_lts;
15041519
sync_cfo = impl_->estimateCoarseCFO(peak_pos);
15051520

1506-
LOG_SYNC(INFO, "searchForSync: found at %zu, corr=%.3f, CFO=%.1f Hz",
1507-
sync_offset, peak_corr, sync_cfo);
1521+
LOG_SYNC(INFO, "searchForSync: found at %zu (LTS=%zu), corr=%.3f, CFO=%.1f Hz",
1522+
sync_offset, refined_lts_offset, peak_corr, sync_cfo);
15081523
break;
15091524
}
15101525
}
@@ -1517,12 +1532,10 @@ bool OFDMDemodulator::searchForSync(SampleSpan samples, size_t& out_position, fl
15171532
if (found_sync) {
15181533
out_cfo_hz = sync_cfo;
15191534

1520-
// Calculate where LTS (training) starts
1521-
// Preamble: 4 STS + 2 LTS = 6 symbols
1522-
// processPresynced expects samples starting at LTS (it needs training for channel est)
1523-
// So we return position of LTS start (after 4 STS symbols)
1524-
size_t sts_symbols = 4;
1525-
out_position = sync_offset + sts_symbols * preamble_symbol_len;
1535+
// processPresynced expects samples starting at FIRST LTS.
1536+
out_position = (refined_lts_offset >= preamble_symbol_len)
1537+
? (refined_lts_offset - preamble_symbol_len)
1538+
: refined_lts_offset;
15261539
}
15271540

15281541
return found_sync;

src/ofdm/ofdm_sync.cpp

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,13 @@ size_t OFDMDemodulator::Impl::refineLTSTiming(size_t coarse_sts_start) {
410410
}
411411
energy_ref *= 0.5f;
412412

413-
size_t expected_lts = 4 * preamble_sym_len;
413+
size_t expected_lts = coarse_lts_start;
414414
float corr_at_expected = 0.0f;
415415

416-
for (int delta = -SEARCH_BACK; delta <= SEARCH_FWD; ++delta) {
417-
size_t offset = coarse_lts_start + delta;
416+
auto compute_lts_corr = [&](size_t offset) -> float {
417+
if (offset + lts_passband_I.size() > rx_buffer.size()) {
418+
return 0.0f;
419+
}
418420

419421
float corr_I = 0.0f;
420422
float corr_Q = 0.0f;
@@ -429,7 +431,12 @@ size_t OFDMDemodulator::Impl::refineLTSTiming(size_t coarse_sts_start) {
429431

430432
float corr_mag = std::sqrt(corr_I * corr_I + corr_Q * corr_Q);
431433
float norm = std::sqrt(energy_rx * energy_ref);
432-
float corr = (norm > 1e-6f) ? corr_mag / norm : 0.0f;
434+
return (norm > 1e-6f) ? corr_mag / norm : 0.0f;
435+
};
436+
437+
for (int delta = -SEARCH_BACK; delta <= SEARCH_FWD; ++delta) {
438+
size_t offset = coarse_lts_start + delta;
439+
float corr = compute_lts_corr(offset);
433440

434441
if (corr > best_corr) {
435442
best_corr = corr;
@@ -445,6 +452,22 @@ size_t OFDMDemodulator::Impl::refineLTSTiming(size_t coarse_sts_start) {
445452
expected_lts, corr_at_expected, best_offset, best_corr,
446453
(int)best_offset - (int)expected_lts);
447454

455+
// LTS appears twice with near-identical correlation. If we lock onto the second
456+
// one, data extraction starts one symbol late and decode reliability drops sharply.
457+
// Prefer the previous-symbol candidate when it's close in correlation.
458+
if (best_offset >= preamble_sym_len) {
459+
size_t prev_lts = best_offset - preamble_sym_len;
460+
if (prev_lts >= coarse_lts_start - SEARCH_BACK) {
461+
float prev_corr = compute_lts_corr(prev_lts);
462+
if (prev_corr >= best_corr * 0.92f) {
463+
LOG_SYNC(DEBUG, "LTS ambiguity: prefer earlier LTS %zu (corr=%.3f) over %zu (corr=%.3f)",
464+
prev_lts, prev_corr, best_offset, best_corr);
465+
best_offset = prev_lts;
466+
best_corr = prev_corr;
467+
}
468+
}
469+
}
470+
448471
int timing_correction = (int)best_offset - (int)coarse_lts_start;
449472
LOG_SYNC(DEBUG, "LTS fine timing: coarse=%zu, refined=%zu, correction=%+d samples, corr=%.3f",
450473
coarse_lts_start, best_offset, timing_correction, best_corr);

src/waveform/ofdm_chirp_waveform.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,10 @@ bool OFDMChirpWaveform::detectDataSync(SampleSpan samples, SyncResult& result,
267267
int best_offset = 0;
268268
Complex best_p(0.0f, 0.0f);
269269

270-
// When buffer starts with signal (burst continuation), use wider search window
271-
// to find the LTS peak anywhere in the buffer. LTS autocorrelation (~0.99) is
272-
// much higher than random data autocorrelation (~0.2-0.4).
273-
int actual_search_window = signal_in_noise ? search_window : static_cast<int>(samples.size());
270+
// Keep search local to expected frame start. Scanning the full buffer makes
271+
// payload autocorrelation peaks compete with LTS and increases false locks.
272+
int max_connected_search = std::max(search_window, symbol_samples * 8);
273+
int actual_search_window = signal_in_noise ? search_window : max_connected_search;
274274
int search_end = std::min(static_cast<int>(signal_start) + actual_search_window,
275275
static_cast<int>(samples.size()) - symbol_samples * 2);
276276

0 commit comments

Comments
 (0)