iOS: bound a WSJT-X reply's requested audio frequency to the TX passband#583
Merged
Merged
Conversation
An inbound WSJT-X Reply (companion apps like GridTracker/JTAlert/N1MM, when "Accept UDP requests" is on) carries a `df` = the audio Hz it wants us to answer on. `LiveEngine`'s `onReply` handler adopted any `deltaFreq > 0` straight into `txFreqHz` with no bound. The decoder's band runs to 3500 Hz, so double-clicking a routine 3000-3500 Hz spot sends a df above the SSB TX passband; a stray/garbled datagram can send anything up to UInt32.max. The result is a reply keyed up outside the passband (attenuated / effectively off-air) while the TX marker is separately pinned to the band edge by `WaterfallAxis.clampedFraction` -- so the marker and the actual tone disagree. The desktop (`reply_tx_audio_hz`, #564) and Android (`replyTxFrequencyHz`, #563) ports already bound this untrusted df; the iOS port dropped that guard. Root cause fix: add pure `WaterfallAxis.boundedReplyTxHz` (Foundation-only, Kit-testable) that honors a df only inside [minTxHz, maxTxHz] = [200, 3000] -- the same range `tunedTxHz` clamps tap-to-tune to -- and returns nil (keep the operator's current offset) for 0/unspecified or out-of-band requests. In-band replies behave exactly as before; the app's own click-to-answer (df=0) is unchanged. Tests: WaterfallAxisTests gains in-band-honored and dropped-out-of-band cases (4 fail against the old unbounded behavior). Full FT8AFKit suite 131/131 via swift test on Linux. The onReply wiring lives in the Xcode-only app target (not Linux-buildable); the extracted helper + its tests are. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #583 +/- ##
============================================
+ Coverage 29.36% 29.40% +0.04%
Complexity 197 197
============================================
Files 179 179
Lines 24068 24083 +15
Branches 3263 3263
============================================
+ Hits 7067 7082 +15
Misses 16780 16780
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 hardens the iOS WSJT-X UDP Reply handling by validating the companion-app requested audio offset (df / deltaFreq) before adopting it as the TX tone, ensuring replies never key up outside the SSB transmit passband and keeping the TX marker/tone consistent.
Changes:
- Add
WaterfallAxis.boundedReplyTxHz(_:)to accept reply-requested TX offsets only when they fall withinminTxHz...maxTxHz(otherwise keep the current offset). - Update
LiveEngine.onReplyto use the new helper instead of accepting anydeltaFreq > 0. - Add unit tests covering in-band acceptance and 0/out-of-band/garbage-value rejection.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| ios/FT8AFKit/Sources/FT8Audio/WaterfallAxis.swift | Adds a passband-bounding helper for untrusted WSJT-X Reply df values. |
| ios/FT8AFKit/Tests/FT8AudioTests/WaterfallAxisTests.swift | Adds test coverage asserting in-band values are honored and 0/out-of-band values are ignored. |
| ios/FT8AF/FT8AF/Engine/LiveEngine.swift | Wires reply handling to adopt df only when it’s within the TX passband. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This was referenced Jul 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
An inbound WSJT-X Reply request — sent by companion apps (GridTracker, JTAlert, N1MM) when the Accept UDP requests setting is on — carries a
dffield: the audio-offset (Hz) the companion wants us to answer on. The iOSLiveEngine.onReplyhandler adopted anydeltaFreq > 0straight intoappState.waterfall.txFreqHzwith no range check:The decoder's display band runs to 3500 Hz (
FT8Decoder/WaterfallAxis.displayMaxHz), so double-clicking a routine 3000–3500 Hz spot in a companion sends adfabove the SSB TX passband; a stray or garbled datagram can send anything up toUInt32.max.Root cause
WaterfallAxisalready documents (onclampedFraction, lines 38-45) thattxFreqHz"can be set externally without any range check — a WSJT-X UDP reply carries a rawdeltaFreq" and defensively clamps the TX marker to the band edge. But the actual TX tone was still set to the raw out-of-band value. Net effect: the reply keys up outside the passband (attenuated / effectively off-air) while the red TX marker points to the band edge — marker and tone disagree.Both sibling ports already guard this exact untrusted value:
reply_tx_audio_hz→ honorsdfonly in[200, 3000](MIN/MAX_TX_AUDIO_HZ), else keeps the current offset (Desktop WSJT-X UDP: honor an inbound Reply's requested df (answer on the asked tone) #564)replyTxFrequencyHz→ bounds the replydf(Honor WSJT-X Reply df on Android (answer on the requested audio tone) #563)The iOS port dropped that guard. This is the same cross-port "one port omitted a safeguard the others have" class as #519 / #554 / #559 / #561 / #573 / #576.
Fix
Add a pure, Foundation-only, Kit-testable helper mirroring the desktop guard, using iOS's own passband constants (the same range
tunedTxHzclamps tap-to-tune to):onReplynow adopts the requested tone only when it's in-band; adfof 0 (unspecified — the app's own click-to-answer sends 0) or out-of-band keeps the operator's current offset.Float(deltaFreq)for aUInt32never traps (unlike an integer cast), so a garbage hugedfis simply rejected.Testing
WaterfallAxisTestsgainstestBoundedReplyTxHzHonorsInBandRequestsandtestBoundedReplyTxHzDropsUnspecifiedAndOutOfBand; 4 assertions fail against the old unbounded behavior, pass with the fix (verified by reverting only the helper).swift teston Linux.onReplywiring is in the Xcode-only app target (ios/FT8AF/**, importsNetwork/AVFoundation), which is not Linux-buildable; the extracted helper and its tests are, and were run.Risk
Low. Input-validation-only on untrusted UDP data; the sole behavioral change is that an out-of-band/garbage reply
dfis ignored (current offset kept) instead of keying TX off-air. In-band replies and the desktop-UI click-to-answer path are unchanged.🤖 Generated with Claude Code