Skip to content

Desktop: key queued TX at the slot boundary so fast decodes don't drop the reply#586

Open
patrickrb wants to merge 1 commit into
devfrom
optio/task-61f4bf70-1f1e-42d6-95fd-4bb54ddea452
Open

Desktop: key queued TX at the slot boundary so fast decodes don't drop the reply#586
patrickrb wants to merge 1 commit into
devfrom
optio/task-61f4bf70-1f1e-42d6-95fd-4bb54ddea452

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

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_transmit is the only thing that keys a steady-state transmission, and it is reached from just two places:

  • the operator command handlers (StartCq / Answer / SetStage / FreeText) — immediate, and
  • handle_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, 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 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 rules maybe_transmit already enforced (active QSO, locked-parity alternation, once-per-slot, fits-the-window), extracted into a pure function so maybe_transmit and 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_decode flag — 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 case handle_decoded still keys the fresh message on arrival, so weak CPUs are unaffected.

maybe_transmit remains idempotent per slot (the txed_slot guard), 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

  • Added pure unit tests: tx_slot_eligible_enforces_active_parity_once_and_window and boundary_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).
  • The desktop crate can't be fully built in the sandbox (tauri/serialport/cpal need system libs), so the two extracted pure functions + their tests were verified standalone (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 under cargo 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

…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

codecov Bot commented Jul 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 65.33333% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 29.49%. Comparing base (647b12e) to head (e8d7362).

Files with missing lines Patch % Lines
desktop/src-tauri/src/engine.rs 65.33% 26 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             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              
Flag Coverage Δ
desktop 61.14% <65.33%> (+0.29%) ⬆️
native 9.93% <ø> (ø)

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

Files with missing lines Coverage Δ
desktop/src-tauri/src/engine.rs 27.48% <65.33%> (+4.65%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

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 by maybe_transmit and 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_decode gate 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.

Comment on lines +586 to +590
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;
Comment on lines +96 to +99
/// `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.
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