Honor WSJT-X Reply df on Android (answer on the requested audio tone)#563
Merged
Merged
Conversation
When "Accept UDP requests" is enabled and a companion app (GridTracker,
JTAlert, N1MM+, …) double-clicks a decode, it sends a WSJT-X Reply
request whose `df` field is the audio frequency it wants us to answer
on. The Android codec read that `df` and threw it away, so the reply
keyed up on whatever the current TX offset happened to be instead of the
requested tone. The iOS port already honors `df`
(LiveEngine.swift: `if deltaFreq > 0 { waterfall.txFreqHz = ... }`);
Android diverged.
Fix, mirroring iOS and keeping the change scoped to the inbound-reply
path (zero effect on any existing flow):
- WsjtxCodec.Inbound.Reply now carries `deltaFreq`; parseInbound captures
the `df` field it previously discarded.
- WsjtxUdpService.ReplyHandler.onReply forwards it.
- MainViewModel.handleWsjtxReply adopts it as the TX audio offset via
GeneralVariables.setBaseFrequency (the shared RX/TX offset
FT8TransmitSignal reads when generating the waveform), before calling
the station.
Because the request arrives over an open UDP socket, the untrusted df is
bounded to the usable audio passband [1, MAX_SPECTRUM_WIDTH_HZ] via the
extracted pure helper MainViewModel.replyTxFrequencyHz; a non-positive,
absent, or out-of-range df keeps the current frequency so a malformed
datagram can never push the TX tone off the band (stronger than iOS's
bare `> 0` check).
Tests: WsjtxCodecTest.parseReplyCapturesRequestedDeltaFreq (asserts a
distinct df=2100 is surfaced, not discarded) plus the existing Reply
vectors updated for the new field; new ReplyTxFrequencyTest covers the
bound (honored mid-band/edge values, ignored zero/negative/over-max).
Verified: :app:testDebugUnitTest (full suite) and :app:assembleDebug
(4 ABIs) green.
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 #563 +/- ##
============================================
+ Coverage 26.09% 26.43% +0.34%
- Complexity 165 197 +32
============================================
Files 171 173 +2
Lines 22720 23117 +397
Branches 3192 3266 +74
============================================
+ Hits 5928 6111 +183
- Misses 16599 16784 +185
- Partials 193 222 +29
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 updates the Android WSJT-X UDP “Reply” request handling to honor the inbound df (requested audio TX frequency) instead of discarding it, so replies transmit on the companion-app requested tone.
Changes:
- Extend
WsjtxCodec.Inbound.Replyto includedeltaFreqand parsedffrom inbound Reply datagrams. - Thread
deltaFreqthroughWsjtxUdpServiceintoMainViewModel.handleWsjtxReply, applying it to the TX base frequency. - Add/adjust unit tests to assert
dfis captured and validated.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/wsjtx/WsjtxCodec.kt | Parse inbound Reply df and surface it via Inbound.Reply.deltaFreq. |
| ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/wsjtx/WsjtxUdpService.kt | Forward deltaFreq through the Reply callback to the ViewModel. |
| ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java | Apply requested Reply TX tone via setBaseFrequency, with a helper for bounds checking. |
| ft8af/app/src/test/kotlin/radio/ks3ckc/ft8af/wsjtx/WsjtxCodecTest.kt | Update Reply parsing expectations and add a test proving df is captured. |
| ft8af/app/src/test/java/com/k1af/ft8af/ReplyTxFrequencyTest.java | Add unit tests for replyTxFrequencyHz validation logic. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…dth-100] Copilot review of PR #563: the Reply-df bound accepted 1 Hz and used the fixed MAX_SPECTRUM_WIDTH_HZ (5000) regardless of the current spectrum width, so an untrusted UDP df could land the TX marker/offset outside the range the tuning UI ever permits. WaterfallView clamps click-to-tune freq_hz to [100, spectrumWidth-100]; the current width is readily available via GeneralVariables.getSpectrumWidth() (always clamped to [2500, 5000]). - replyTxFrequencyHz now bounds df to [100, spectrumWidthHz-100], honoring a plausible tone and ignoring anything the UI could never dial in. - handleWsjtxReply passes GeneralVariables.getSpectrumWidth() instead of the fixed MAX constant. - ReplyTxFrequencyTest updated for the new bounds: min (100) and max (spectrumWidth-100) honored; adds a below-min assertion (99, 1 ignored) per review comment; above-max/zero/negative still ignored. Verified: :app:testDebugUnitTest --tests ReplyTxFrequencyTest green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Root cause
When Accept UDP requests is enabled and a companion app (GridTracker, JTAlert, N1MM+, Log4OM…) double-clicks a decode, it sends a WSJT-X Reply request whose
dffield is the audio frequency it wants us to answer on. The Android codec parsed thatdfand discarded it (r.i32() ?: return null // df), sohandleWsjtxReplykeyed up on whatever the current TX offset happened to be — the reply went out on the wrong audio tone.This is a cross-port divergence: the iOS port already honors
df(LiveEngine.swift:441—if deltaFreq > 0 { appState.waterfall.txFreqHz = Float(deltaFreq) }). Android (and desktop) lagged.Fix
Scoped entirely to the inbound-reply path — no effect on any existing flow (only runs on an opt-in UDP Reply request):
WsjtxCodec.Inbound.Replynow carriesdeltaFreq;parseInboundcaptures thedfit previously threw away.WsjtxUdpService.ReplyHandler.onReplyforwards it to the ViewModel.MainViewModel.handleWsjtxReplyadopts it as the TX audio offset viaGeneralVariables.setBaseFrequency(the shared RX/TX offsetFT8TransmitSignalreads when it generates the waveform) before calling the station.Defensive bound on untrusted input
The request arrives over an open UDP socket, so the
dfis validated before use. Extracted a pure helperMainViewModel.replyTxFrequencyHz(deltaFreq, maxHz)that returns the offset only when1 ≤ df ≤ MAX_SPECTRUM_WIDTH_HZ(else-1→ keep current frequency). A non-positive, absent, or out-of-range df therefore can never push the TX tone off the audio band — stronger than iOS's bare> 0check, consistent with this repo's validate-network-input posture.Testing performed
WsjtxCodecTest.parseReplyCapturesRequestedDeltaFreq— asserts a distinctdf=2100is surfaced (not discarded); existing Reply golden vectors updated for the new field.ReplyTxFrequencyTest(pure JUnit, mirrorsCallStationOrderTest) — 6 cases covering honored mid-band/edge values and ignored zero/negative/over-max../gradlew :app:testDebugUnitTest(full suite) — green../gradlew :app:assembleDebug(all 4 ABIs) — green.Risk assessment
Low. The change only executes on an inbound WSJT-X Reply when the operator has enabled "Accept UDP requests"; every other transmit path is untouched (byte-identical). The untrusted df is bounded before it can influence the TX tone. No DSP/protocol/encoder behavior changes.
Scope note
Android-only by design. The desktop (Rust) port has the same
df-discard gap and a separate, unrelated inbound-Halt robustness issue — both are better handled in their own PR to keep this focused and avoid colliding with concurrent desktop work.