Skip to content

Honor WSJT-X Reply df on Android (answer on the requested audio tone)#563

Merged
patrickrb merged 2 commits into
devfrom
optio/task-5f39a1ad-a8cc-414c-a9d1-3c9f62f9edee
Jul 13, 2026
Merged

Honor WSJT-X Reply df on Android (answer on the requested audio tone)#563
patrickrb merged 2 commits into
devfrom
optio/task-5f39a1ad-a8cc-414c-a9d1-3c9f62f9edee

Conversation

@patrickrb

Copy link
Copy Markdown
Owner

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 df field is the audio frequency it wants us to answer on. The Android codec parsed that df and discarded it (r.i32() ?: return null // df), so handleWsjtxReply keyed 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:441if 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.Reply now carries deltaFreq; parseInbound captures the df it previously threw away.
  • WsjtxUdpService.ReplyHandler.onReply forwards it to the ViewModel.
  • MainViewModel.handleWsjtxReply adopts it as the TX audio offset via GeneralVariables.setBaseFrequency (the shared RX/TX offset FT8TransmitSignal reads when it generates the waveform) before calling the station.

Defensive bound on untrusted input

The request arrives over an open UDP socket, so the df is validated before use. Extracted a pure helper MainViewModel.replyTxFrequencyHz(deltaFreq, maxHz) that returns the offset only when 1 ≤ 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 > 0 check, consistent with this repo's validate-network-input posture.

Testing performed

  • WsjtxCodecTest.parseReplyCapturesRequestedDeltaFreq — asserts a distinct df=2100 is surfaced (not discarded); existing Reply golden vectors updated for the new field.
  • New ReplyTxFrequencyTest (pure JUnit, mirrors CallStationOrderTest) — 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.

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

codecov Bot commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 26.43%. Comparing base (5773692) to head (c7fc745).
⚠️ Report is 10 commits behind head on dev.

Files with missing lines Patch % Lines
...main/kotlin/radio/ks3ckc/ft8af/wsjtx/WsjtxCodec.kt 75.00% 0 Missing and 1 partial ⚠️
...kotlin/radio/ks3ckc/ft8af/wsjtx/WsjtxUdpService.kt 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@             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     
Flag Coverage Δ
android 14.57% <60.00%> (+0.83%) ⬆️
native 9.93% <ø> (ø)

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

Files with missing lines Coverage Δ
...main/kotlin/radio/ks3ckc/ft8af/wsjtx/WsjtxCodec.kt 85.91% <75.00%> (ø)
...kotlin/radio/ks3ckc/ft8af/wsjtx/WsjtxUdpService.kt 0.00% <0.00%> (ø)

... and 1 file with indirect coverage changes

🚀 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 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.Reply to include deltaFreq and parse df from inbound Reply datagrams.
  • Thread deltaFreq through WsjtxUdpService into MainViewModel.handleWsjtxReply, applying it to the TX base frequency.
  • Add/adjust unit tests to assert df is 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.

Comment thread ft8af/app/src/main/java/com/k1af/ft8af/MainViewModel.java Outdated
Comment thread ft8af/app/src/test/java/com/k1af/ft8af/ReplyTxFrequencyTest.java Outdated
Comment thread ft8af/app/src/test/java/com/k1af/ft8af/ReplyTxFrequencyTest.java
…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>
@patrickrb patrickrb merged commit ef2db46 into dev Jul 13, 2026
17 checks passed
@patrickrb patrickrb deleted the optio/task-5f39a1ad-a8cc-414c-a9d1-3c9f62f9edee branch July 13, 2026 14:14
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