Skip to content

Commit a57140d

Browse files
secupclaude
andcommitted
feat(sync): two-crater discipline for the sync re-anchor (ULTRA_CRATER_REANCHOR_HOLD, default-off)
The RECEIVER forced a full chirp+LTS re-anchor (resetFrameArrivalTrackingLocked + expect_full_ofdm_anchor_=true) on EVERY 0-CW group crater — treating a deep fade (an AMPLITUDE event; warm TIMING survives, the samples are just attenuated) as a lost sync. Cost: ~7 full re-anchors per 50KB transfer, each delaying the ACK (the RX re-acquires instead of signaling the failure) and forcing a ~1.2s chirp on the next group — the operator-observed "long ACK -> sender RTO -> resend" loop, watched repeatedly. The RATE controller already declines to react to a single crater (F122 two-crater rule: one crater is a fade the ARQ absorbs, not evidence). The SYNC re-anchor never got that discipline. This gives it the same one: new crater_reanchor_streak_ (reset on any decoded frame); behind ULTRA_CRATER_REANCHOR_HOLD (default-off => byte-identical), the full re-anchor is HELD on crater #1 (RX tries warm light-sync on the next group) and fires only on crater #2 consecutive. Safe: expect_full_ofdm_anchor_ is RECEIVER-LOCAL (drives the RX's own search mode, NOT a sender signal — the WAITING-REBASE voice is a separate ARQ-timeout path). A held crater just makes the RX attempt warm next group: if warm works, a full re-anchor + ~1.2s chirp is saved and the ACK isn't delayed; if the CFO genuinely walked, the next group craters and #2 re-anchors + re-centers exactly as before. Worst case = ONE extra warm-attempt group (bounded); crater #2 onward unchanged. Verification: ctest -j4 full suite green (86/86, knob off = byte-identical). Rig A/B (on vs off, MPG@20) is the throughput proof; default-on pending it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019btwFxDt7D19SZSqPYvZnk
1 parent 94faa1f commit a57140d

4 files changed

Lines changed: 68 additions & 1 deletion

File tree

docs/CHANGELOG.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,40 @@ This log tracks all bug fixes and behavioral changes to prevent re-doing work du
1010

1111
---
1212

13+
## 2026-07-21 — feat(sync): two-crater discipline for the sync re-anchor (ULTRA_CRATER_REANCHOR_HOLD, default-off) — stop nuking warm sync on a single fade
14+
15+
**What was broken.** On every 0-CW group crater the RECEIVER
16+
(`streaming_ofdm_decode.cpp` decodeCurrentFrame) forced a full chirp+LTS re-anchor —
17+
`resetFrameArrivalTrackingLocked()` + `expect_full_ofdm_anchor_ = true`. A deep fade is
18+
an AMPLITUDE event (warm TIMING survives it — the samples are there, just attenuated),
19+
so re-acquiring timing+CFO from a fresh chirp is over-reaction: it delays the ACK (the RX
20+
is re-acquiring instead of signaling the failure), and the next group must prepend a
21+
~1.2 s chirp. Measured on the rig: ~7 full re-anchors per 50 KB transfer, and the
22+
operator watched the resulting "long ACK → sender RTO → resend" loop repeatedly. The RATE
23+
controller already declines to react to a single crater (F122 two-crater rule: one crater
24+
is a fade the ARQ absorbs, not evidence) — but the SYNC re-anchor never got that discipline
25+
and fired on the FIRST crater every time.
26+
27+
**What changed.** New `crater_reanchor_streak_` counter (StreamingDecoder), reset on any
28+
decoded frame. Behind `ULTRA_CRATER_REANCHOR_HOLD` (default-off ⇒ byte-identical): the
29+
full re-anchor is HELD on crater #1 (warm sync kept — the RX tries warm light-sync on the
30+
next group instead of a full blind chirp search) and fires only on crater #2 consecutive.
31+
Same restraint the rate controller already uses.
32+
33+
**Why safe.** `expect_full_ofdm_anchor_` is RECEIVER-LOCAL (drives the RX's own search
34+
mode; NOT wired to force a sender full-anchor — the WAITING-REBASE voice is a separate
35+
ARQ-timeout path). So a held crater just makes the RX attempt warm on the next group:
36+
if warm works, a full re-anchor + ~1.2 s chirp is saved and the ACK isn't delayed; if the
37+
CFO genuinely walked, the next group craters and #2 re-anchors + re-centers exactly as
38+
before. Worst case = ONE extra warm-attempt group (bounded); crater #2 onward is unchanged.
39+
40+
**Verification.** `ctest -j4` full suite green (86/86, knob off = byte-identical). Rig A/B
41+
(ULTRA_CRATER_REANCHOR_HOLD on vs off, MPG@20) is the throughput proof — expected to cut
42+
the per-run full-re-anchor count and the ACK-delay→RTO→resend loop the operator flagged.
43+
Default-on pending the rig A/B.
44+
45+
---
46+
1347
## 2026-07-21 — fix(arq): cross-frame burst interleave DEFAULT-OFF for all modulations (was >=16QAM-ON) — rig-measured harmful on fading, +37% with per-frame SACK
1448

1549
**What changed.** `connection_policy.hpp::burstCrossFrameInterleaveOn(mod)` now returns

docs/MODEM_INFRASTRUCTURE_MAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ Buckets per the env-knobs→runtime-derivation workstream: **[FEAT]** in-flight
378378
| `ULTRA_ZF_LLR_UNBIAS` | 🟡 EXPERIMENTAL (default OFF) | channel_equalizer_equalize.cpp (end of equalize()) | Stage A ZF-consistent LLR unbias — measured MARGINAL on the failing sim cell (docs/LLR_NOTCH_CALIBRATION_2026_07_06.md §measured); controls exactly invariant (QPSK/AWGN); keep OFF unless rig evidence diverges from the sim cell |
379379
| `ULTRA_NOTCH_NV` | 🟡 EXPERIMENTAL (default OFF) | channel_equalizer_equalize.cpp nv denominator (+ raw-obs writer channel_equalizer_pilot.cpp) | Stage B per-carrier reliability floor from raw LS pilot observations — same experiment, same marginal verdict; keep OFF |
380380
| `ULTRA_RX_PREDICTIVE_CLIMB` | 🟢 ACTIVE (default ON, `=0` opt-out) | connection.cpp updateRxAuthorityCommand | EESM per-carrier virtual rung evaluation — direct multi-rung climbs from measured gamma snapshots (docs/RX_AUTHORITY_PREDICTIVE_2026_07_07.md); fallback = one-rung ladder |
381+
| `ULTRA_CRATER_REANCHOR_HOLD` | 🟡 EXPERIMENTAL (A/B knob, default OFF ⇒ byte-identical; throughput-ceiling lever #3, 2026-07-21, CHANGELOG). TWO-CRATER discipline for the SYNC re-anchor: the RX forces a full chirp+LTS re-anchor (`resetFrameArrivalTrackingLocked` + `expect_full_ofdm_anchor_=true`) on EVERY 0-CW group crater (`streaming_ofdm_decode.cpp` decodeCurrentFrame, ~7×/50KB transfer), treating a deep fade (an AMPLITUDE event — warm TIMING survives) as a lost sync → delayed ACK + ~1.2s chirp on the next group → the operator's "long ACK → RTO → resend" loop. ON: hold warm sync through crater #1 (RX tries warm light-sync next group), re-anchor only on crater #2 — the same restraint the RATE controller's F122 two-crater rule already uses. Safe: `expect_full_ofdm_anchor_` is RECEIVER-LOCAL (own search mode, not a sender signal); worst case = 1 extra warm-attempt group, crater #2 re-anchors + re-centers CFO as before. `crater_reanchor_streak_` resets on any decoded frame. | **OFF** | `streaming_ofdm_decode.cpp` decodeCurrentFrame (`crater_reanchor_streak_` + `ULTRA_CRATER_REANCHOR_HOLD` gate); member `streaming_decoder.hpp` | FEAT (A/B) |
381382
| `ULTRA_RX_EMA_HOLD` | 🟡 EXPERIMENTAL (A/B knob, default OFF ⇒ byte-identical; throughput-ceiling audit lever #1, 2026-07-21, CHANGELOG). Two coupled corrections to the RX-authority crater response, targeting the fast-vs-slow variance (F344 2.61 vs F372 1.16 kbps SAME channel = rate-controller limit cycle, not PHY): (1) **EMA-supported HOLD** — a confirmed crater does not demote/ratchet while the fade-averaged ring `snr_avg` STRICTLY exceeds the current rung's class floor (`rungClassAnchorDb`, new ladder-aware+class-specific anchor); consecutive craters at a supported average are deep-null brushes the ARQ resends through. (2) **Censored failed-group SNR** — a non-delivered group enters the obs ring right-censored at `min(obs, calibrationAnchorDbFor)` instead of the stale-crest `burst_obs_snr_db_`, killing the survivor bias so sustained real failure drives `snr_avg` below the class anchor and DOES demote (strict `>` breaks the censor==anchor latch). No new magic constants (thresholds are the rung's own anchors). Preserves the F122-F160 crater_streak≥2 / one-rung-down / ratchet / dwell machinery — only makes the demote conditional on the measured average. Unset/`=0` → byte-identical. Read per-group (togglable in tests). | **OFF** | `connection.cpp` `emaHoldEnabled()` / `updateRxAuthorityCommand` (censored ring feed + `ema_supports_cur`/`demote_on_crater`); `waveform_selection.hpp` `rungClassAnchorDb`; tests `test_rx_authority.cpp` (`test_ema_hold_absorbs_supported_crater`, `test_ema_hold_still_demotes_sustained_failure`) | FEAT (A/B) |
382383
ABSOLUTE rung selection: receiver maps its fresh per-group observation (broadband SNR EMA +
383384
coherence-adjusted fading, `Connection::setBurstChannelObservation` fed by the modem binding)

src/gui/modem/streaming_decoder.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,13 @@ class StreamingDecoder {
816816
size_t last_decoded_sync_pos_ = SIZE_MAX; // Last successfully decoded sync position (to prevent duplicates)
817817
bool sync_from_warm_timed_window_ = false;
818818
bool sync_from_full_anchor_fallback_ = false;
819+
// Lever #3 (ULTRA_CRATER_REANCHOR_HOLD): consecutive 0-CW crater re-anchor
820+
// triggers since the last decoded frame. The sync layer forces a full
821+
// chirp+LTS re-anchor on the FIRST crater; the RATE controller learned long ago
822+
// (F122 two-crater rule) that a single crater is a fade the ARQ absorbs, not
823+
// evidence — this counter gives the SYNC re-anchor the same discipline: hold warm
824+
// sync through the first crater, re-anchor only on the second consecutive one.
825+
int crater_reanchor_streak_ = 0;
819826
// warm_sync_phase_ + last_frame_* relocated into sync_controller_ (§7.4 shell-move A1,
820827
// 2026-05-31); accessed as sync_controller_.<field> until A2 moves the transition logic in.
821828

src/gui/modem/streaming_ofdm_decode.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2251,6 +2251,7 @@ void StreamingDecoder::decodeCurrentFrame() {
22512251
size_t next_search_abs = frame_sync_abs + consumed;
22522252

22532253
if (result.success && connected_ && is_ofdm) {
2254+
crater_reanchor_streak_ = 0; // lever #3: a decode proves warm sync is fine
22542255
noteFrameArrivalSuccess(frame_sync_abs, next_search_abs);
22552256
std::lock_guard<std::mutex> lock(sync_controller_.ring_.buffer_mutex_);
22562257
if (!is_non_data_frame) {
@@ -2283,13 +2284,37 @@ void StreamingDecoder::decodeCurrentFrame() {
22832284
}();
22842285
const bool ack_listen_self_echo =
22852286
kEchoReanchorGate && tone_burst_monitor_.isArmed();
2286-
if (mode_ == protocol::WaveformMode::OFDM_CHIRP && !ack_listen_self_echo) {
2287+
// Lever #3 TWO-CRATER SYNC DISCIPLINE (ULTRA_CRATER_REANCHOR_HOLD, default-off):
2288+
// a deep fade is an AMPLITUDE event, not a lost sync — warm TIMING survives it
2289+
// (the samples are there, just attenuated). Forcing a full chirp+LTS re-anchor
2290+
// on the FIRST crater treats the fade as a sync loss: it delays the ACK (the RX
2291+
// is re-acquiring instead of signaling) and makes the sender prepend a ~1.2s
2292+
// chirp. The RATE controller already declines to react to a single crater
2293+
// (F122 two-crater rule); this gives the SYNC re-anchor the same restraint —
2294+
// hold warm sync through crater #1, re-anchor only on crater #2. Worst case is
2295+
// ONE extra warm-attempt group before the re-anchor (bounded), and crater #2
2296+
// still re-centers a genuinely-walked CFO exactly as before.
2297+
static const bool kCraterReanchorHold = [] {
2298+
const char* e = std::getenv("ULTRA_CRATER_REANCHOR_HOLD");
2299+
return e && e[0] == '1' && e[1] == '\0';
2300+
}();
2301+
++crater_reanchor_streak_;
2302+
const bool hold_warm_sync =
2303+
kCraterReanchorHold && crater_reanchor_streak_ < 2;
2304+
if (mode_ == protocol::WaveformMode::OFDM_CHIRP && !ack_listen_self_echo &&
2305+
!hold_warm_sync) {
22872306
resetFrameArrivalTrackingLocked();
22882307
sync_controller_.expect_full_ofdm_anchor_ = true;
22892308
sync_controller_.clearRejectStreak();
22902309
LOG_MODEM(WARN,
22912310
"[%s] OFDM decode failed with 0/%d CWs; forcing full chirp+LTS re-anchor",
22922311
log_prefix_.c_str(), result.codewords_failed);
2312+
} else if (hold_warm_sync && mode_ == protocol::WaveformMode::OFDM_CHIRP &&
2313+
!ack_listen_self_echo) {
2314+
LOG_MODEM(INFO,
2315+
"[%s] OFDM decode failed 0/%d CWs (crater #%d) — HOLDING warm sync "
2316+
"(a single fade is not a sync loss; #2 forces re-anchor)",
2317+
log_prefix_.c_str(), result.codewords_failed, crater_reanchor_streak_);
22932318
} else if (ack_listen_self_echo) {
22942319
LOG_MODEM(INFO,
22952320
"[%s] SELF-ECHO 0-CW during ACK-listen (monitor armed) — "

0 commit comments

Comments
 (0)