Skip to content

Auto-sync the clock by default (one-slot acquisition), drop callers who gave up, swap the TX over on a late reply - #758

Merged
patrickrb merged 3 commits into
devfrom
fix/qso-timing-queue-rr73
Aug 21, 2026
Merged

Auto-sync the clock by default (one-slot acquisition), drop callers who gave up, swap the TX over on a late reply#758
patrickrb merged 3 commits into
devfrom
fix/qso-timing-queue-rr73

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Three field complaints from the same activation, all in the QSO timing loop.

1. Auto-sync still needed the manual "Apply suggested correction"

ClockSelfSync stopped acting once |median DT| ≤ 0.30 s and required ≥ 4 decodes per slot — so on a quiet POTA band it never fired, and on a busy one it parked the clock ~0.25 s off, which is exactly the residual the operator kept applying by hand.

  • Deadband 0.30 s → 0.15 s (two DT measurement steps; still doesn't chase noise).
  • A slot with 1–3 surviving decodes now counts, but needs three agreeing slots instead of two (a lone station's DT is also that station's clock error).
  • The Suggestion card no longer offers the manual apply while auto-sync (or GPS discipline) owns the clock; it shows the last auto-correction and its time instead (GeneralVariables.mutableSelfSyncApplied).

2. Queued callers who gave up were still called back

Nothing ever pruned the caller queue, and it was only fed on cycles where the partner went silent (the scan lived behind the no-reply branch).

  • QueuedCaller.lastHeardUtc tracks the slot of their latest call; every pass (fast and evidence-only) refreshes it via refreshCallerQueue().
  • CallerQueueOrdering.pruneGivenUp() drops anyone not heard in the most recent receive slot (one full cycle) before the queue is worked — on QSO completion, on give-up, and while idling on CQ. Nobody fresh → back to CQ, or answer whoever is calling right now. Strongest-first picks among live callers only.
  • Operator-tapped callers (dequeueSpecificCaller) are never pruned.
  • The QUEUE chips now reflect who's actually still calling.

3. Re-sending the report instead of RR73

When the partner's R-report decoded just after key-up, #704's mid-cycle TX swap was supposed to fix the over — but it could never fire: both decode paths in MainViewModel.afterDecode stashed anything that landed while transmitting, and the drain refused to run while transmitting, so the reply was replayed a full cycle later (our own-slot fast pass ~14 s after key-up).

  • Late decodes are now parsed immediately as evidence-only (same mode the replay used), so the order advances at once and shouldRestartForNewOrder can swap the over inside the audio slack. Past the slack the advanced order keys the next cycle — no stash needed.
  • The TX worker also picks up a swap requested during the PTT settle before the first sample plays; otherwise playFT8Signal() resets the cancel flags, plays the old over in full, and replays the new one ~13 s in where the clip math strips it.
  • PendingSequencerDecodes removed; FastPassDisposition.Action.STASHEVIDENCE_ONLY.

Tests

New/updated: ClockSelfSyncTest (sparse slots, tighter deadband, convergence), CallerQueueOrderingTest (pruning), CallerQueueLivenessTest (sequencer-level: queue fed on smooth QSOs, stale callers pruned, completion goes to CQ / works the live caller / strongest-first ignores a loud caller who left), FastPassDispositionTest, TimeCorrectionTest.showSuggestionApply.

Full unit suite: 3396 tests, 0 failures.

On-air check (please)

  • Time Sync: with auto-sync on, the suggestion card should now read "Auto-sync is on…" and the DT residual should settle under ±0.15 s within a few RX slots, even with 1–2 stations heard.
  • debug.log: look for QSO: TX restart … / TX message updated before audio start after a fast decode landed after key-up line, and dropped queued caller(s) no longer calling.

🤖 Generated with Claude Code

…TX over on a late reply

Three field complaints from the same activation, all in the QSO timing loop.

1. "Auto-sync clock from decodes" still left me applying the suggestion by hand.
   ClockSelfSync stopped acting once |median DT| <= 0.30 s and needed >= 4
   decodes in a slot, so on a quiet band it never fired and on a busy one it
   parked the clock ~0.25 s off -- exactly the residual the operator kept
   tapping "Apply suggested correction" for. Deadband is now 0.15 s (two DT
   measurement steps), a slot with 1-3 surviving decodes counts but needs three
   agreeing slots instead of two, and the Suggestion card stops offering the
   manual apply while auto-sync (or GPS discipline) owns the clock -- it shows
   the last auto-correction instead.

2. Queued callers who had given up were still called back after the QSO.
   Nothing ever removed a station from the caller queue, and the queue was only
   fed on cycles where the partner went silent. Every pass now records who is
   calling me (other than the partner) and refreshes their last-heard slot; a
   caller not heard in the most recent receive slot (one full cycle) is pruned
   before the queue is worked, so we go back to CQ -- or answer whoever is
   calling right now -- instead of calling someone who left. Strongest-first
   picks among live callers only. A caller the operator taps is never pruned.

3. After sending a report we sometimes re-sent it instead of RR73 when the
   partner's R-report decoded just after key-up. PR #704's mid-cycle TX swap
   handles precisely this, but it could never fire: both decode paths stashed
   anything that landed while transmitting and the drain refused to run while
   transmitting, so the reply was replayed a whole cycle later. Decodes that
   land after key-up are now parsed immediately (evidence-only, as the replay
   already was), which advances the order at once and lets the swap replace the
   over inside the audio slack. The TX worker also picks up a swap requested
   during the PTT settle before the first sample plays, instead of playing the
   old over in full and replaying the new one too late to fit. The stash
   (PendingSequencerDecodes) is gone; FastPassDisposition's second outcome is
   EVIDENCE_ONLY.

Tests: ClockSelfSync sparse/deadband cases, CallerQueueOrdering pruning,
CallerQueueLivenessTest (sequencer-level queue liveness), FastPassDisposition,
TimeCorrection showSuggestionApply. Full unit suite: 3396 tests, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 8.47458% with 54 lines in your changes missing coverage. Please review.
✅ Project coverage is 41.65%. Comparing base (d24b408) to head (075dab8).

Files with missing lines Patch % Lines
...radio/ks3ckc/ft8af/ui/settings/TimeSyncSettings.kt 0.00% 33 Missing ⚠️
...tlin/radio/ks3ckc/ft8af/ui/components/ClockSync.kt 19.04% 17 Missing ⚠️
...n/radio/ks3ckc/ft8af/ui/components/SlotTimerBar.kt 0.00% 4 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #758      +/-   ##
============================================
- Coverage     41.69%   41.65%   -0.05%     
- Complexity      284      290       +6     
============================================
  Files           278      278              
  Lines         32960    33166     +206     
  Branches       3940     3994      +54     
============================================
+ Hits          13742    13814      +72     
- Misses        18868    18996     +128     
- Partials        350      356       +6     
Flag Coverage Δ
android 17.94% <8.47%> (+0.18%) ⬆️
native 9.93% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...n/radio/ks3ckc/ft8af/ui/settings/TimeCorrection.kt 100.00% <100.00%> (ø)
...n/radio/ks3ckc/ft8af/ui/components/SlotTimerBar.kt 21.81% <0.00%> (-1.26%) ⬇️
...tlin/radio/ks3ckc/ft8af/ui/components/ClockSync.kt 22.66% <19.04%> (-0.97%) ⬇️
...radio/ks3ckc/ft8af/ui/settings/TimeSyncSettings.kt 0.00% <0.00%> (ø)

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The self-syncing clock is now the default (autoSyncClockFromDecodes=true;
hydration only acts on rows that exist, so fresh installs and pre-feature
backups come up with it on). To earn that, it has to feel instant:

- Acquire fast, track gently. A well-populated slot (>= 4 survivors) whose
  median DT is more than 0.5 s out acts on its own -- no second slot to
  confirm -- and removes the whole error in one step (ACQUIRE_GAIN 1.0).
  Inside 0.5 s the loop tracks as before: two agreeing slots, half the
  residual per step. Sparse slots still need three agreeing slots, but take
  the full step when plainly off, so a quiet band 1 s out is fixed in three
  slots instead of nine.
- The DT pill in the slot bar shows an AUTO badge (GPS when GPS discipline
  owns the clock), so the operator can see the offset is being handled.
- Settings copy says it is on by default and what to expect.

Convergence from a cold start, busy band: 1.5 s -> in deadband after ONE
slot (was 8). Quiet band, one station: 1.0 s -> three slots (was never).

Tests: ClockSelfSyncTest rewritten around the acquire/track regimes
(41 tests), ClockSyncTest badge cases, hydration default flipped.
Full unit suite: 3410 tests, 0 failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@patrickrb patrickrb changed the title Auto-sync the clock for real, drop callers who gave up, swap the TX over on a late reply Auto-sync the clock by default (one-slot acquisition), drop callers who gave up, swap the TX over on a late reply Aug 21, 2026
@patrickrb

Copy link
Copy Markdown
Owner Author

Second commit: auto-sync is now on by default and acquires in one slot. A well-populated slot (≥4 decodes) whose median DT is >0.5 s out is corrected in full immediately; inside 0.5 s it tracks with two-slot confirmation and half-steps. Sparse slots (1–3 decodes) need three agreeing slots but take the full step when plainly off. The DT pill in the slot bar shows an AUTO badge (or GPS) so you can see it's being handled. Cold start on a busy band: 1.5 s off → in deadband after one slot. Full suite: 3410 tests, 0 failures.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It changes timing-sensitive TX/decoder interactions and default clock-discipline behavior in core operating paths, which warrants final human review and on-air validation.

Pull request overview

This PR tightens the QSO timing loop by (1) making decode-based clock self-sync effective on sparse bands and enabled by default, (2) keeping the caller queue “live” by pruning stations that stopped calling, and (3) ensuring late decodes are acted on immediately as evidence so the sequencer can swap the on-air message within the audio slack.

Changes:

  • Enable and surface decode-based auto clock sync by default, with a tighter deadband and acquisition/track behavior for sparse vs busy slots.
  • Add caller-queue liveness tracking (lastHeardUtc) plus pruning of callers who gave up, and refresh the queue on every sequencer pass.
  • Remove decode stashing and parse late decodes immediately as evidence-only to allow mid-cycle TX restarts/swaps.
File summaries
File Description
ft8af/app/src/main/java/com/k1af/ft8af/timer/ClockSelfSync.java Implements acquisition vs tracking, tighter deadband, and sparse-slot confirmation rules.
ft8af/app/src/test/java/com/k1af/ft8af/timer/ClockSelfSyncTest.java Expands estimator unit tests for sparse slots, acquisition, tracking, and convergence.
ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java Turns auto-sync on by default and adds runtime status fields for UI (“last step”, “last applied”).
ft8af/app/src/test/java/com/k1af/ft8af/database/DatabaseOprConfigHydrationTest.java Updates hydration expectations to reflect the new default-on auto-sync behavior.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/TimeCorrection.kt Adds showSuggestionApply() gate so manual apply is hidden when an automatic source owns the clock.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/settings/TimeCorrectionTest.kt Tests the manual-apply visibility logic for manual vs auto-sync vs GPS discipline.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/TimeSyncSettings.kt Updates the Suggestion card to show auto-sync status and last auto-correction instead of a manual action.
ft8af/app/src/main/res/values/strings_compose.xml Adds strings for auto-sync status messaging and clock-sync accessibility text/badges.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/ClockSync.kt Adds AUTO/GPS badge selection + accessibility string composition for the DT pill.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/components/ClockSyncTest.kt Tests AUTO/GPS badge selection behavior.
ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/components/SlotTimerBar.kt Wires auto-sync/GPS flags into the DT indicator so the badge reflects current clock ownership.
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/QueuedCaller.java Adds lastHeardUtc to track caller freshness for pruning.
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/CallerQueueOrdering.java Adds “given up” detection and in-place pruning helpers.
ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/CallerQueueOrderingTest.java Tests given-up detection, pruning, FT4/FT8 scaling, and strongest-first behavior post-prune.
ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java Refreshes caller queue every parse pass, prunes given-up callers, and supports pre-audio TX message updates on restart.
ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/CallerQueueLivenessTest.java Adds sequencer-level Robolectric coverage for queue refresh + pruning across QSO completion/CQ.
ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/ui/components/CallerQueueDisplayTest.kt Updates QueuedCaller construction to include lastHeardUtc.
ft8af/app/src/main/java/com/k1af/ft8af/ft8listener/FastPassDisposition.java Changes fast-pass handling from PARSE/STASH to PARSE/EVIDENCE_ONLY.
ft8af/app/src/test/java/com/k1af/ft8af/ft8listener/FastPassDispositionTest.java Updates tests to reflect “never dropped, never deferred” fast-pass behavior.
ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java Removes pending-decode stashing and parses late decodes immediately as evidence; posts auto-sync UI status.
ft8af/app/src/main/java/com/k1af/ft8af/PendingSequencerDecodes.java Deleted: no longer needed after switching late decodes to immediate evidence-only parsing.
ft8af/app/src/test/java/com/k1af/ft8af/PendingSequencerDecodesTest.java Deleted along with the stashing mechanism.
Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

enqueueCaller() posted a fresh queue snapshot every time an already-queued
station was seen again, and refreshCallerQueue() now feeds it every pass,
so a deep pass re-delivering the same decode republished an identical list.
QueuedCaller.refresh() updates SNR / last-heard (forward only) and reports
whether anything visible changed; the snapshot is posted only then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@patrickrb
patrickrb merged commit 610acaa into dev Aug 21, 2026
17 checks passed
@patrickrb
patrickrb deleted the fix/qso-timing-queue-rr73 branch August 21, 2026 19:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants