@@ -1241,6 +1241,118 @@ void StreamingDecoder::decodeCurrentFrame() {
12411241 }
12421242 }
12431243
1244+ // Multi-candidate light-sync recovery (connected OFDM):
1245+ // If decode fails at the detected sync point, retry nearby timing candidates.
1246+ // detectDataSync() scans with coarse steps, and fading can shift the best
1247+ // decode point by a few samples even when correlation looks valid.
1248+ if (!result.success && result.codewords_ok == 0 && is_ofdm && connected_) {
1249+ const int retry_deltas[] = {8 , -8 , 16 , -16 , 24 , -24 , 32 , -32 };
1250+ bool recovered = false ;
1251+ int recovered_delta = 0 ;
1252+ uint64_t recovery_attempts = 0 ;
1253+
1254+ auto ringPosToAbsolute = [this ](size_t ring_pos) -> size_t {
1255+ if (total_fed_ < MAX_BUFFER_SAMPLES) {
1256+ return ring_pos;
1257+ }
1258+ const size_t oldest_abs = total_fed_ - MAX_BUFFER_SAMPLES;
1259+ const size_t oldest_pos = write_pos_;
1260+ const size_t offset = (ring_pos >= oldest_pos)
1261+ ? (ring_pos - oldest_pos)
1262+ : (MAX_BUFFER_SAMPLES - oldest_pos + ring_pos);
1263+ return oldest_abs + offset;
1264+ };
1265+
1266+ for (int delta : retry_deltas) {
1267+ recovery_attempts++;
1268+ size_t retry_sync = (sync_position_ + MAX_BUFFER_SAMPLES + delta) % MAX_BUFFER_SAMPLES;
1269+
1270+ std::vector<float > retry_buffer;
1271+ size_t retry_len = frame_len;
1272+ {
1273+ std::lock_guard<std::mutex> lock (buffer_mutex_);
1274+ size_t available;
1275+ if (write_pos_ >= retry_sync) {
1276+ available = write_pos_ - retry_sync;
1277+ } else {
1278+ available = MAX_BUFFER_SAMPLES - retry_sync + write_pos_;
1279+ }
1280+ retry_len = std::min (retry_len, available);
1281+ if (retry_len == 0 ) {
1282+ continue ;
1283+ }
1284+ retry_buffer.resize (retry_len);
1285+ for (size_t i = 0 ; i < retry_len; ++i) {
1286+ retry_buffer[i] = buffer_[(retry_sync + i) % MAX_BUFFER_SAMPLES];
1287+ }
1288+ }
1289+
1290+ waveform_->reset ();
1291+ waveform_->setAbsoluteTrainingPosition (ringPosToAbsolute (retry_sync));
1292+ waveform_->setFrequencyOffset (sync_cfo_);
1293+ bool retry_ok = waveform_->process (SampleSpan (retry_buffer.data (), retry_buffer.size ()));
1294+ if (!retry_ok) {
1295+ continue ;
1296+ }
1297+ captureConstellationSnapshot ();
1298+ auto retry_bits = waveform_->getSoftBits ();
1299+ if (retry_bits.empty ()) {
1300+ continue ;
1301+ }
1302+
1303+ auto retry_result = decodeFrame (retry_bits, sync_snr_, sync_cfo_);
1304+ if (!(retry_result.success || retry_result.codewords_ok > 0 )) {
1305+ continue ;
1306+ }
1307+
1308+ LOG_MODEM (INFO, " [%s] Multi-candidate sync recovery: delta=%+d samples succeeded" ,
1309+ log_prefix_.c_str (), delta);
1310+
1311+ // Keep CFO tracking consistent with the accepted retry candidate.
1312+ float corrected_cfo = waveform_->estimatedCFO ();
1313+ float current_cfo = last_cfo_.load ();
1314+ constexpr float MAX_PILOT_CFO_DRIFT_HZ = 2 .0f ;
1315+ float drift = corrected_cfo - current_cfo;
1316+ if (std::abs (drift) > MAX_PILOT_CFO_DRIFT_HZ) {
1317+ corrected_cfo = current_cfo + std::copysign (MAX_PILOT_CFO_DRIFT_HZ, drift);
1318+ }
1319+ last_cfo_.store (corrected_cfo);
1320+ sync_cfo_ = corrected_cfo;
1321+ last_fading_index_.store (waveform_->getFadingIndex ());
1322+
1323+ sync_position_ = retry_sync;
1324+ frame_len = retry_len;
1325+ result = std::move (retry_result);
1326+ recovered_delta = delta;
1327+ recovered = true ;
1328+ break ;
1329+ }
1330+
1331+ if (recovery_attempts > 0 ) {
1332+ std::lock_guard<std::mutex> slock (stats_mutex_);
1333+ stats_.sync_recovery_attempts += recovery_attempts;
1334+ if (recovered) {
1335+ stats_.sync_recovery_successes ++;
1336+ switch (recovered_delta) {
1337+ case 8 : stats_.sync_recovery_delta_p8 ++; break ;
1338+ case -8 : stats_.sync_recovery_delta_m8 ++; break ;
1339+ case 16 : stats_.sync_recovery_delta_p16 ++; break ;
1340+ case -16 : stats_.sync_recovery_delta_m16 ++; break ;
1341+ case 24 : stats_.sync_recovery_delta_p24 ++; break ;
1342+ case -24 : stats_.sync_recovery_delta_m24 ++; break ;
1343+ case 32 : stats_.sync_recovery_delta_p32 ++; break ;
1344+ case -32 : stats_.sync_recovery_delta_m32 ++; break ;
1345+ default : break ;
1346+ }
1347+ }
1348+ }
1349+
1350+ if (!recovered) {
1351+ LOG_MODEM (DEBUG, " [%s] Multi-candidate sync recovery: no nearby offset decoded" ,
1352+ log_prefix_.c_str ());
1353+ }
1354+ }
1355+
12441356 auto decode_end = std::chrono::steady_clock::now ();
12451357 float ms = std::chrono::duration<float , std::milli>(decode_end - decode_start).count ();
12461358
0 commit comments