Feed deep/late-pass decodes to the QSO auto-sequencer#406
Conversation
…dence Field report (POTA activation 2026-07-03): QSOs stalled repeating the previous message while the partner's reply was visible on screen, and some stations calling K1AF were never answered. debug.log shows the cause: MainViewModel only handed fast-pass decodes to parseMessageToFunction, so any reply found only by a deep, subtraction or late/cross-slot pass (routine since the decode-gap work, PRs #367-#370) never reached the sequencer. Both stalled QSOs in the report show newOrder=-1 for two cycles while the partner's R-report was deep-pass-only (one cycle even logged replyToMe=false while the R-report was displayed in the QSO panel). Fix: deep passes now drive the sequencer via an evidence-only parse variant - they can advance the QSO, trigger the RR73->73 reply, complete on a received 73, and enqueue callers, but never make absence-of-evidence decisions (no-reply counting, give-up, "partner went silent" completions), which stay fast-pass-only so a deep pass finding nothing new is not double-counted as a second no-reply. Deep results that land while TX is playing are stashed in PendingSequencerDecodes (age-capped) and replayed at the first delivery after key-up, so evidence arriving mid-transmission still advances the next cycle instead of being dropped. Results landing before TX keys (~0.9s window) upgrade the very next transmission. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #406 +/- ##
=========================================
Coverage 20.80% 20.80%
Complexity 133 133
=========================================
Files 148 148
Lines 19297 19297
Branches 2876 2876
=========================================
Hits 4015 4015
Misses 15115 15115
Partials 167 167
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR updates the FT8 QSO auto-sequencer so that deep/subtraction/late-pass decodes can advance the QSO state machine as positive evidence, fixing stalls where replies were visible in the UI but ignored by sequencing. It also adds a mid-TX “stash and replay after key-up” mechanism and extracts QSO-completion logic for better testability.
Changes:
- Route deep/late-pass decodes into an evidence-only sequencer parse path, and replay mid-TX deep results after key-up.
- Add
PendingSequencerDecodes(age-capped) to stash deep decodes while transmitting. - Extract
shouldCompleteQso(...)and add targeted unit/Robolectric coverage for the new sequencing behavior.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java | Feeds deep/late-pass decodes to the sequencer (evidence-only), stashes mid-TX deep decodes, and replays them after TX ends. |
| ft8af/app/src/main/java/com/k1af/ft8af/PendingSequencerDecodes.java | New synchronized stash/drain helper with MAX_AGE eviction for mid-TX deep-pass decodes. |
| ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java | Adds evidence-only parse variant, extracts shouldCompleteQso, and exposes package-visible hooks for testing. |
| ft8af/app/src/test/java/com/k1af/ft8af/PendingSequencerDecodesTest.java | Unit coverage for stash/drain ordering and age-based eviction. |
| ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java | Adds unit tests for shouldCompleteQso behavior across evidence-only vs fast-pass paths. |
| ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/EvidenceOnlyParseTest.java | Robolectric coverage ensuring deep-pass evidence advances sequencing without triggering absence-of-evidence behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Copilot review on PR #406: Ft8Message.utcTime is in the NTP/GPS-corrected time base (UtcTimer.getSystemTime() = delay + currentTimeMillis), while the stash/drain calls passed the raw system clock. On a phone with a badly-set clock (the reason the app has clock sync at all) the mixed bases would skew age eviction by the correction offset - keeping stale evidence or evicting fresh evidence. Pass the corrected time and document the expected base on PendingSequencerDecodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot review findings, both valid: - writeElapsedMs was measured with System.currentTimeMillis(); a wall clock step (NTP set, user change) could fake a fast failure and wrongly allow the restart-from-zero UsbRequest fallback after audio already went to air. Use SystemClock.elapsedRealtime(). - The tx_audio_dropped toast hard-coded "USB audio device dropped" as the cause, but writeAudio can fail for other reasons (libusb setup errors, UsbRequest path failures). Genericized to "USB audio error" in all 16 locales; the precise cause stays in debug.log via describeLibusbWriteError. Also move the shouldWarnTxDropped tests into their own TxDropWarningTest file so this PR no longer appends to the same FT8TransmitSignalTest region as PR #406 (eliminates the guaranteed merge conflict between the two open PRs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Problem
Field report from the 2026-07-03 POTA activation (K1AF): QSOs stalled repeating the previous message while the partner's reply was visible on screen, and some stations calling K1AF were never answered.
The pulled
debug.logpins it down: the auto-sequencer only ever saw the fast decode pass.MainViewModel.afterDecodegatedparseMessageToFunctionon!isDeep, so every deep, subtraction-loop, and late/cross-slot pass (routine since the decode-gap work, #367–#370) reached the UI and QSO panel but never the sequencer.Evidence from the log (local = UTC−5):
K1AF N2JFD R-18received 20:25:15/:45/20:26:15; sequencer loggednewOrder=-1(no-reply 1,2) for two cycles and only advanced on the third.R-23, with the smoking gun15:35:28 DECODE: kept=24 ... replyToMe=false slot=1— the fast pass contained no message addressed to us while the QSO panel displayed the R-23 (deep-pass decode).no-replyevents at orders 2–3 across the day.Fix
Deep passes now drive the sequencer through an evidence-only parse variant (
parseMessageToFunction(list, true)):order 5 + newOrder −1, RR73 no-reply caps) — those absence-of-evidence calls stay fast-pass-only, so a deep pass that finds nothing new is not a second no-reply. The completion decision is extracted intoshouldCompleteQso(...)for testability.Deep results arriving before TX keys (~:29.0–:29.8 vs key-up ~:29.9) are parsed immediately and can upgrade the very next transmission (send RR73 instead of repeating the report in the same cycle). Results arriving mid-TX are stashed in the new
PendingSequencerDecodes(age-capped at 60 s) and replayed at the first delivery after key-up.Evidence parses log as
QSO: cycle(deep) ...so the effect is auditable from a pulleddebug.logon the next activation.Tests
EvidenceOnlyParseTest(Robolectric, full sequencer instance): deep R-report advances 2→4 and resets the no-reply counter; deep RR73 → 73; deep 73 completes to CQ; deep caller lands in the queue; deep pass with nothing new does not increment no-reply (with the fast-pass contrast case) and does not complete an order-5 QSO; own-slot decodes are rejected; Hound mode ignores deep passes.FT8TransmitSignalTest:shouldCompleteQsoarms — positive evidence completes on any pass, silence-based arms fast-pass-only, thresholds honoured.PendingSequencerDecodesTest: stash/drain/clear, ordering, age eviction (boundary included).testDebugUnitTestsuite green.Field verification
Next activation: watch for QSOs advancing on the first received R-report and
QSO: cycle(deep)lines indebug.log. (Note: two of this outing's stalls were compounded by a separate USB TX-drop bug — fix in a follow-up PR.)🤖 Generated with Claude Code