Skip to content

iOS: retransmit courtesy 73 if it missed its TX slot (don't clobber it)#581

Open
patrickrb wants to merge 1 commit into
devfrom
optio/task-fe30d42e-9cb9-416a-9797-e794cbf1a5e3
Open

iOS: retransmit courtesy 73 if it missed its TX slot (don't clobber it)#581
patrickrb wants to merge 1 commit into
devfrom
optio/task-fe30d42e-9cb9-416a-9797-e794cbf1a5e3

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

Summary

Fixes a cross-port state-machine divergence in the iOS QSO auto-sequencer: the answerer's courtesy 73 (and a manual Tx5 73) could be silently skipped when it missed its TX slot, and the app would prematurely return to calling CQ.

Root cause

QsoEngine.processRx (ios/FT8AFKit/Sources/FT8Engine/QsoEngine.swift) wrapped the QSO up (returned to CQ / stopped) whenever stage == .bye73, assuming the intervening TX slot always sent the queued 73. But LiveEngine.scheduleTx can bail — an FT8 encode failure, or the slot's parity/timing not lining up — leaving the 73 queued but unsent. The next processRx then clobbered it by returning to CQ.

The desktop reference port already fixed exactly this in PR #570 / commit 8843c396 (desktop/src-tauri/src/qso.rs): finish_qso() is gated on a bye73_sent flag that the scheduler latches via notify_transmitted() only when the transmitter actually keys up (maybe_transmitstart_transmit returns bool). The iOS port dropped that safeguard. This is the follow-up catalogued in the PR #580 sweep notes.

Fix

Faithful port of the desktop structure:

  • QsoEngine — add a bye73Sent flag and a notifyTransmitted() method; gate finishQso() on the flag; reset it everywhere a fresh 73 is queued (setStage(.bye73), the .rReport → RR73 completion path, resetQso). Until the 73 is actually transmitted, processRx keeps it queued so the scheduler retransmits it next eligible slot — matching how every other stage retries. notifyTransmitted() is a no-op off .bye73, so in-progress sequences are unaffected.
  • LiveEngine.scheduleTx — now returns whether it actually started TX; the scheduler calls qso.notifyTransmitted() on success (mirrors the desktop engine's maybe_transmit). Happy path byte-identical.

Testing

  • ios/FT8AFKit built and tested on Linux via swift test (the Kit imports only Foundation + CFT8): 131/131 green.
  • Updated testAnswererFullSequenceLogsQso to signal the transmit (mirrors the desktop test change).
  • Added testBye73NotFinishedUntilActuallyTransmitted (missed-slot stays armed, no double-log, finishes only after notifyTransmitted) and testNotifyTransmittedIsNoOpOffBye73. Both fail pre-fix, verified by reverting only the processRx gate.

Risk

Low. Pure state-machine change scoped to the courtesy-73 wrap-up; every other stage and the happy-path TX flow are unchanged. The LiveEngine app-target wiring is a minimal @discardableResult signature change (one call site) — it is Xcode-only and was not Linux-build-verified (the FT8AF app target imports AVFoundation and cannot compile on Linux); the QSO-engine logic + its tests were fully verified.

🤖 Generated with Claude Code

iOS QsoEngine.processRx wrapped the QSO up (returned to CQ / stopped)
whenever stage == .bye73, on the assumption that the intervening TX slot
always sent the queued courtesy "73". But scheduleTx can bail — an
encode failure, or the slot's parity/timing not lining up — leaving the
73 queued but unsent. The next processRx then clobbered it by returning
to CQ, so the answerer's courtesy 73 (and a manual Tx5 73) could be
skipped in exactly those missed-slot cases, and the app prematurely
started calling CQ over the top.

The desktop reference port already fixed this (qso.rs, PR #570 commit
8843c39): finish_qso() is gated on a bye73_sent flag that the scheduler
latches via notify_transmitted() only when the TX actually keys up. The
iOS port dropped that safeguard.

Mirror the desktop fix on iOS: add a bye73Sent flag + notifyTransmitted()
to QsoEngine, gate finishQso() on it, and reset the flag everywhere a
fresh 73 is queued (setStage(.bye73), the .rReport -> RR73 completion
path, resetQso). Until the 73 is actually transmitted, processRx keeps it
queued so the scheduler retransmits it next eligible slot — matching how
every other stage retries. notifyTransmitted() is a no-op off .bye73, so
in-progress sequences are unaffected. LiveEngine.scheduleTx now returns
whether it actually started TX and the scheduler calls notifyTransmitted()
on success (mirrors the desktop engine's maybe_transmit).

Tests (FT8AFKit, runs on Linux via swift test): the existing answerer
sequence now signals the transmit; added
testBye73NotFinishedUntilActuallyTransmitted (missed-slot stays armed, no
double-log, finishes only after notify) and testNotifyTransmittedIsNoOpOffBye73.
Both fail pre-fix, verified by reverting only the gate. Full Kit suite
131/131 green. The LiveEngine app-target wiring is Xcode-only and was not
Linux-build-verified.

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

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 29.53%. Comparing base (647b12e) to head (c09a812).

Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##                dev     #581      +/-   ##
============================================
+ Coverage     29.36%   29.53%   +0.17%     
  Complexity      197      197              
============================================
  Files           179      179              
  Lines         24068    24128      +60     
  Branches       3263     3263              
============================================
+ Hits           7067     7127      +60     
  Misses        16780    16780              
  Partials        221      221              
Flag Coverage Δ
ios 93.13% <100.00%> (+0.19%) ⬆️
native 9.93% <ø> (ø)

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

Files with missing lines Coverage Δ
ios/FT8AFKit/Sources/FT8Engine/QsoEngine.swift 94.30% <100.00%> (+0.37%) ⬆️
...FT8AFKit/Tests/FT8EngineTests/QsoEngineTests.swift 100.00% <100.00%> (ø)
🚀 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

Fixes an iOS QSO auto-sequencer edge case where a queued courtesy “73” (Bye73 stage) could be incorrectly clobbered if it missed its TX slot, by gating QSO wrap-up on an explicit “actually transmitted” signal from the scheduler.

Changes:

  • Add bye73Sent state and notifyTransmitted() to QsoEngine, and gate Bye73 wrap-up on that flag.
  • Update LiveEngine.scheduleTx to return whether TX started, and only then notify the sequencer.
  • Extend/adjust unit tests to model missed-slot behavior and verify Bye73 completion semantics.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
ios/FT8AFKit/Tests/FT8EngineTests/QsoEngineTests.swift Adds regression tests ensuring Bye73 isn’t finalized until TX is actually reported, and that notify is a no-op off Bye73.
ios/FT8AFKit/Sources/FT8Engine/QsoEngine.swift Introduces bye73Sent + notifyTransmitted() and prevents premature wrap-up when Bye73 hasn’t actually been transmitted.
ios/FT8AF/FT8AF/Engine/LiveEngine.swift Makes TX scheduling report “did TX start?” and uses that to trigger notifyTransmitted() only on success.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +323 to +329
/// Generate FT8 audio and play it through the speaker. Returns `true` if a
/// transmission was actually started, `false` if it bailed (encode failure) —
/// the caller uses this to tell the sequencer the message really went out.
@discardableResult
private func scheduleTx(message: String, freqHz: Float) -> Bool {
guard let appState else { return false }
guard let samples = FT8Encoder.generateFT8(message, baseFreqHz: freqHz) else { return false }
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