Desktop: key queued TX at the slot boundary so fast decodes don't drop the reply#586
Desktop: key queued TX at the slot boundary so fast decodes don't drop the reply#586patrickrb wants to merge 1 commit into
Conversation
…p the reply
The desktop engine only reaches `maybe_transmit` (the sole steady-state TX
trigger) from two places: the operator command handlers and `handle_decoded`
when a decode batch arrives. There is no per-tick / per-slot-boundary call.
A slot's audio is handed to the decoder at DECODE_AT_MS (13.2 s) and the reply
it produces is meant to go out in the *next* slot. But on a normal/fast CPU the
decode batch comes back well before the 15 s boundary, so `handle_decoded`
advances the QSO and calls `maybe_transmit` mid-slot — where the window check
(`now % 15000 > TX_LATEST_MS`, 2260 ms) correctly refuses to start a 12.64 s
waveform that late. Nothing then re-attempts at the top of the next slot, so the
queued reply/CQ is silently dropped: the auto-sequence stalls and CQ never
retransmits. It only worked when decode latency happened to land the batch
inside the next slot's window (~1.8–4.0 s), which the design comment even
assumes ("returns results around the 15 s boundary"). Only the first
operator-tap transmission (the immediate command-handler path) worked.
Fix: add a per-tick boundary trigger in the run loop that keys a queued message
at the top of its slot. The keying rules `maybe_transmit` already enforced
(active QSO, locked-parity alternation, once-per-slot, fits-the-window) are
factored into a pure `tx_slot_eligible` shared by both paths. A new
`awaiting_decode` flag (set when a slot is handed to the worker, cleared when its
decodes return or capture stops) gates the boundary trigger via `boundary_tx_ready`
so it never keys a *stale* message ahead of a slow decode — in that case
`handle_decoded` still keys the fresh message on arrival, so weak CPUs are
unaffected. `maybe_transmit` stays idempotent per slot (the `txed_slot` guard),
so the extra call can't double-key.
Behaviour-identical for the operator-tap and slow-decode paths; the change only
adds the missing boundary keying for the fast-decode case.
Tests: pure-unit tests for `tx_slot_eligible` (active/parity/once/window) and
`boundary_tx_ready` (keys at slot top only after this slot's decode; blocks on a
pending decode, closed window, already-txed, wrong parity, inactive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## dev #586 +/- ##
============================================
+ Coverage 29.36% 29.49% +0.13%
Complexity 197 197
============================================
Files 179 179
Lines 24068 24127 +59
Branches 3263 3263
============================================
+ Hits 7067 7116 +49
- Misses 16780 16790 +10
Partials 221 221
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 fixes a desktop (Tauri) engine timing issue where fast decode turnaround can cause auto-sequencing/CQ retransmits to stall because maybe_transmit is only reached on operator commands or decode arrival (which can occur mid-slot, outside the TX start window). The change adds a per-tick “slot boundary” attempt so queued replies can still be keyed at the start of the next eligible slot.
Changes:
- Extracts the TX gating logic into a pure
tx_slot_eligible(...)helper (shared bymaybe_transmitand the run loop). - Adds a boundary trigger (
boundary_tx_ready(...)+ run-loop call) to re-attempt keying at the top of a slot when eligible. - Introduces an
awaiting_decodegate plus unit tests for the extracted keying conditions and boundary trigger behavior.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if self.slot_tx.send(slot).is_ok() { | ||
| // Hold off the boundary TX trigger until this slot's decodes | ||
| // come back, so it can't key a stale message ahead of a slow | ||
| // decode (the fresh one is keyed by handle_decoded on arrival). | ||
| self.awaiting_decode = true; |
| /// `handle_decoded` has set the fresh `tx_message` this fires on. This is what lets | ||
| /// a reply computed early in the previous cycle (fast CPU) still go out at the top | ||
| /// of its slot — `maybe_transmit` is otherwise only reached on decode arrival, | ||
| /// which on a quick decode lands mid-slot, past the TX window, dropping the reply. |
Summary
On the desktop (Tauri) client, FT8 auto-sequencing and CQ retransmit silently stop working on a normal/fast CPU: after the first (operator-tap) transmission, no further messages go out. The reply the engine computes each slot is dropped instead of being keyed in the next TX slot.
Root cause
maybe_transmitis the only thing that keys a steady-state transmission, and it is reached from just two places:StartCq/Answer/SetStage/FreeText) — immediate, andhandle_decoded, when a decode batch arrives from the worker.There is no per-tick / per-slot-boundary call in the run loop.
A slot's audio is handed to the decoder at
DECODE_AT_MS(13.2 s) and the reply it produces is intended for the next slot. But on a normal/fast CPU the decode batch returns well before the 15 s boundary, sohandle_decodedadvances the QSO and callsmaybe_transmitmid-slot — where the window check (now % 15000 > TX_LATEST_MS, 2260 ms) correctly refuses to start a 12.64 s waveform that late. Nothing re-attempts at the top of the next slot, so the queued reply/CQ is dropped: the auto-sequence (grid → report → R-report → RR73 → 73) stalls and CQ never retransmits.It only worked when decode latency happened to land the batch inside the next slot's window (~1.8–4.0 s). The existing design comment even assumes this ("decoding ~0.6 s later returns results around the 15 s boundary even on a slow CPU"), but 13.2 s + ~0.6 s = ~13.8 s — well before the boundary — so the assumption doesn't hold on fast hardware.
Fix
Add the missing boundary trigger, keeping the change minimal and behaviour-preserving:
tx_slot_eligible(...)— the keying rulesmaybe_transmitalready enforced (active QSO, locked-parity alternation, once-per-slot, fits-the-window), extracted into a pure function somaybe_transmitand the run loop share them exactly.maybe_transmit's behaviour is unchanged.boundary_tx_ready(...)+ a per-tick call in the run loop (after the decode drain) that keys a queued message at the top of its slot.awaiting_decodeflag — set when a slot is handed to the decode worker, cleared when its decodes return (or capture stops). It gates the boundary trigger so it never keys a stale message ahead of a slow decode; in that casehandle_decodedstill keys the fresh message on arrival, so weak CPUs are unaffected.maybe_transmitremains idempotent per slot (thetxed_slotguard), so the extra call can never double-key. The operator-tap and slow-decode paths are behaviour-identical; the change only adds the boundary keying that the fast-decode case was missing.Testing
tx_slot_eligible_enforces_active_parity_once_and_windowandboundary_tx_keys_at_slot_top_only_after_this_slots_decode(keys at slot top only once this slot's decode is in; blocks on a pending decode, a closed window, an already-transmitted slot, wrong parity, or an inactive QSO).rustc --edition 2021 --test, both pass); the rest is a localized, behaviour-preserving edit. Desktop CI compiles the crate (tauri build --no-bundle, 3 OSes) and runs the tests undercargo llvm-cov.Risk
Low. No protocol/DSP change; no change to encode/decode or audio math. Steady-state TX timing is the only affected behaviour, and the fix restores the intended "reply lands in the very next slot" semantics while leaving the operator-tap and slow-CPU paths exactly as they were.
🤖 Generated with Claude Code