fix(phy): D8PSK R2/3 connected light-sync recovery#33
Merged
Conversation
Two-GUI OTASim test at AWGN SNR=20 dB negotiated D8PSK R2/3
(correctly — the rate selector's clean-fading threshold of >=18 dB was
genuinely crossed by the honest idle-noise SNR estimator) and then
decode failed catastrophically: all 8 codewords FAIL with
|llr|_avg ~ 2.7, BRAVO retransmits, ARQ stalls, QSO dies.
Codex's controlled offline sweep (ofdm_snr_probe + decode_bench,
both extended to take --mod and --cw-count) isolated the failure
to the streaming + connected path, not the D8PSK demap/LDPC:
| SNR | direct probe | connected pre-fix | connected after |
| 5 | 3/8 (fail) | 0/4 | 0/4 |
| 8 | 8/8 (pass) | 1/4 | 1/4 |
| 10 | 8/8 (pass) | 0/4 | 0/4 |
| 12 | 8/8 (pass) | 0/4 | 4/4 |
| 14+ | 8/8 (pass) | 0-3/4 | 4/4 |
So D8PSK R2/3 PHY closes at AWGN SNR~8 dB (Shannon-limit territory),
but the connected streaming path was broken at every SNR. Root cause
was the multi-candidate light-sync recovery in streaming_ofdm_decode
at line ~1028: DQPSK-tuned retry window (+/-8 samples, partial-CW
acceptance) doesn't handle D8PSK's tighter timing tolerance and
admits low-confidence false locks as success.
This change (Codex round 1):
- D8PSK-only: widen retry window to {-32, -24, -16, -8, +8, +16,
+24, +32}, prefer earlier candidates first (late light-sync
locks show up as positive LTS phase slope).
- D8PSK-only: require full fixed-frame decode to accept a retry
(partial CW success no longer counts), preventing false-positive
recoveries.
- D8PSK-only: trigger recovery on partial-fixed-frame failures
(>=2 codewords attempted, partial CW success), not just zero-CW.
- Boundary safety: skip negative deltas that would underflow the
ring buffer at the start of a stream.
- Non-D8PSK behavior preserved verbatim (+/- 8 deltas, partial
acceptance, same gating).
Also in this change:
- tools/ofdm_snr_probe.cpp + tools/decode_bench.cpp: --mod and
--cw-count flags so the controlled sweep is reproducible.
- tools/cli_simulator.cpp: spawned OTASim's tokens now carry the
admin role. cli_simulator calls SetChannel to configure the
spawned daemon's channel; PR #30's admin gate denied that with
the previously-operator-only tokens, breaking CLISyntheticNotch.
Test harness fully owns its sandbox; production servers should
not hand out admin tokens this freely.
Test gate (user's unrestricted Mac):
cmake --build build -j4
ctest --test-dir build --output-on-failure -j4
-> 83/83 PASS (after cli_simulator token fix; D8PSK fix doesn't
regress any existing test on its own).
3-perspective check:
- PHY: D8PSK demapper + LDPC unchanged; only the front-end
timing-recovery policy was tightened for D8PSK's larger
amplitude sensitivity at high-modulation index.
- DSP: change is gated on (modulation == D8PSK), so DQPSK
timing recovery is unchanged. Boundary check on negative
deltas avoids ring-buffer underflow.
- Operator: live OTASim two-GUI handshake at SNR>=12 dB now
completes via D8PSK R2/3 instead of timing out in ARQ.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
D8PSK R2/3 selected by the auto-rate ladder at AWGN SNR>=18 dB
failed to decode in the OTASim two-GUI test (0/8 CWs, |llr|_avg ~ 2.7,
ARQ stalled). Codex's controlled sweep proved this was a connected
streaming path bug, NOT a D8PSK PHY/LDPC issue — the demapper closes
at AWGN SNR~8 dB at the direct-probe level.
Root cause: `streaming_ofdm_decode.cpp:1028` multi-candidate
light-sync recovery was DQPSK-tuned (+/-8 sample retry window, partial
CW acceptance). D8PSK has tighter timing tolerance and admits
false locks as success under that policy.
Before / after (AWGN sweep, 4 trials per cell)
Connected D8PSK R2/3 now closes from AWGN SNR=12 dB up. Live
OTASim two-GUI handshake at the user's SNR=20 lobby setting will
decode after this lands.
Changes
`src/gui/modem/streaming_ofdm_decode.cpp` — D8PSK-only recovery:
positive LTS phase slope).
partial-CW false positives).
`current_modulation_ == Modulation::D8PSK`.
`tools/ofdm_snr_probe.cpp` + `tools/decode_bench.cpp` —
added `--mod` and `--cw-count` flags so the controlled sweep
is reproducible.
`tools/cli_simulator.cpp` — spawned OTASim's two tokens
now have the `:admin` role. `cli_simulator` calls SetChannel
to configure the spawned daemon's channel; PR feat(otasim): admin RBAC + otasim_ctl admin CLI #30's admin gate
denied that with operator-only tokens, breaking
`CLISyntheticNotch`. The test harness fully owns its sandbox,
so admin role here is appropriate.
Test plan
live decode now succeeds
Follow-ups
(lock the fix in via CTest).
🤖 Generated with Claude Code