From 04275d580f4fdf86a1b069dfb48fe7c6340953f8 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Fri, 26 Jun 2026 10:23:29 -0500 Subject: [PATCH 1/2] TX: add "Hold TX freq" setting (WSJT-X-style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In same-frequency (TX/RX split) mode, setTransmit() moves your TX offset onto the frequency of the station you answer. A tester found this inverted: turning on TX/RX split unexpectedly moved their TX offset to the QSO target. Add a holdTxFreq setting (default off, persisted as "holdTxFreq", with a toggle in Transmission settings under TX/RX split). When on, the TX offset is kept on your own frequency when answering a station — the WSJT-X "Hold Tx Freq" behavior. The follow now happens only when split is on AND hold is off, via the pure shouldFollowTargetFreq() predicate, which is unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../java/com/k1af/ft8af/GeneralVariables.java | 1 + .../com/k1af/ft8af/database/DatabaseOpr.java | 3 +++ .../ft8af/ft8transmit/FT8TransmitSignal.java | 15 +++++++++++- .../ft8af/ui/settings/TransmissionSettings.kt | 14 +++++++++++ .../src/main/res/values/strings_compose.xml | 2 ++ .../ft8transmit/FT8TransmitSignalTest.java | 24 +++++++++++++++++++ 6 files changed, 58 insertions(+), 1 deletion(-) diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java b/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java index f01b72a7..9858eb4e 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java @@ -379,6 +379,7 @@ public static Context getMainContext() { public static String qrzXmlPassword = ""; //QRZ XML API password public static boolean pskOverlayEnabled = false; //PSK Reporter map overlay (issue #33) public static boolean synFrequency = false;//Same-frequency transmit + public static boolean holdTxFreq = false;//Hold TX freq: don't move the TX offset to a station you answer (WSJT-X "Hold Tx Freq") public static int transmitDelay = 500;//Transmit delay; also allows decoding time for the previous cycle public static int pttDelay = 100;//PTT response time; radios typically need some response time after PTT command, default 100ms public static int lateStartTolerance = 2000;//Max ms into a cycle that a manual TX may start; leading audio is clipped so TX still ends on the cycle boundary. 0-4000. diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java b/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java index 07d846c4..c068fff7 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java @@ -2378,6 +2378,9 @@ protected Void doInBackground(Void... voids) { if (name.equalsIgnoreCase("synFreq")) { GeneralVariables.synFrequency = !(result.equals("") || result.equals("0")); } + if (name.equalsIgnoreCase("holdTxFreq")) { + GeneralVariables.holdTxFreq = result.equals("1"); + } if (name.equalsIgnoreCase("transDelay")) { if (result.matches("^\\d{1,4}$")) {//Regex: 1-4 digit number GeneralVariables.transmitDelay = Integer.parseInt(result); diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java b/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java index 907cf56f..79a61d4e 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java @@ -325,7 +325,10 @@ public void setTransmit(TransmitCallsign transmitCallsign if (transmitCallsign.frequency == 0) { transmitCallsign.frequency = GeneralVariables.getBaseFrequency(); } - if (GeneralVariables.synFrequency) {// if same-frequency mode, match target callsign frequency + // Same-frequency mode moves our TX offset onto the station we answer — unless + // "Hold TX freq" is on (WSJT-X "Hold Tx Freq"), in which case we keep calling + // on our own offset. Tester reported the auto-move felt inverted. + if (shouldFollowTargetFreq(GeneralVariables.synFrequency, GeneralVariables.holdTxFreq)) { setBaseFrequency(transmitCallsign.frequency); } @@ -345,6 +348,16 @@ public void setBaseFrequency(float freq) { databaseOpr.writeConfig("freq", String.format("%.0f", freq), null); } + /** + * Whether to move our TX offset onto the frequency of the station we're about to + * answer. True only in same-frequency (TX/RX split) mode AND when "Hold TX freq" + * is off — holding TX freq (WSJT-X "Hold Tx Freq") keeps us calling on our own + * offset. Pure decision logic so it can be unit-tested without the Android runtime. + */ + static boolean shouldFollowTargetFreq(boolean synFrequency, boolean holdTxFreq) { + return synFrequency && !holdTxFreq; + } + /** * Build an Ft8Message for a Field Day exchange (i3=0, n3=3 or 4). * diff --git a/ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/TransmissionSettings.kt b/ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/TransmissionSettings.kt index 33876ca6..87fc0c66 100644 --- a/ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/TransmissionSettings.kt +++ b/ft8af/app/src/main/kotlin/radio/ks3ckc/ft8af/ui/settings/TransmissionSettings.kt @@ -40,6 +40,7 @@ fun TransmissionSettings( onBack: () -> Unit, ) { var synFrequency by remember { mutableStateOf(GeneralVariables.synFrequency) } + var holdTxFreq by remember { mutableStateOf(GeneralVariables.holdTxFreq) } var watchdogMs by remember { mutableIntStateOf(GeneralVariables.launchSupervision) } var noReplyLimit by remember { mutableIntStateOf(GeneralVariables.noReplyLimit) } @@ -141,6 +142,19 @@ fun TransmissionSettings( }, ) SectionDivider() + SettingsRow( + label = stringResource(R.string.settings_hold_tx_freq), + description = stringResource(R.string.settings_hold_tx_freq_desc), + toggle = holdTxFreq, + onToggleChange = { checked -> + holdTxFreq = checked + GeneralVariables.holdTxFreq = checked + mainViewModel.databaseOpr.writeConfig( + "holdTxFreq", if (checked) "1" else "0", null, + ) + }, + ) + SectionDivider() SettingsRow( label = stringResource(R.string.settings_tx_watchdog), description = stringResource(R.string.settings_tx_watchdog_desc), diff --git a/ft8af/app/src/main/res/values/strings_compose.xml b/ft8af/app/src/main/res/values/strings_compose.xml index 05e7d083..75541602 100644 --- a/ft8af/app/src/main/res/values/strings_compose.xml +++ b/ft8af/app/src/main/res/values/strings_compose.xml @@ -423,6 +423,8 @@ TX/RX Split Transmit on a different frequency than receive + Hold TX freq + Keep your TX offset when answering a station instead of moving it to theirs TX Watchdog Auto-stop transmit after timeout Stop After diff --git a/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java b/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java index cbe0b5aa..e8c39e31 100644 --- a/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java +++ b/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java @@ -340,4 +340,28 @@ public void huntFilter_potaOnlyOn_keepsPota() { // A genuine POTA CQ stays eligible when the filter is active. assertThat(FT8TransmitSignal.huntFilterExcludes(true, true)).isFalse(); } + + // ---- shouldFollowTargetFreq --------------------------------------------- + // Same-frequency (TX/RX split) mode moves our TX offset onto the station we + // answer. "Hold TX freq" (WSJT-X Hold Tx Freq) must override that and keep us on + // our own offset. So we follow the target ONLY when split is on AND hold is off. + + @Test + public void followTarget_splitOn_holdOff_follows() { + assertThat(FT8TransmitSignal.shouldFollowTargetFreq( + /*synFrequency*/ true, /*holdTxFreq*/ false)).isTrue(); + } + + @Test + public void followTarget_splitOn_holdOn_holds() { + // The reported request: keep my TX offset even with split on. + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(true, true)).isFalse(); + } + + @Test + public void followTarget_splitOff_neverFollows() { + // Without split there's nothing to follow, hold flag irrelevant. + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(false, false)).isFalse(); + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(false, true)).isFalse(); + } } From 6941f50e37179a2ca9e80ad27f39d72d334ef090 Mon Sep 17 00:00:00 2001 From: Patrick Burns Date: Fri, 26 Jun 2026 13:52:12 -0500 Subject: [PATCH 2/2] Hold TX freq: clarify TX=RX wording and align config parsing Review nits: - shouldFollowTargetFreq Javadoc + test comment called synFrequency "same-frequency (TX/RX split)", which is contradictory (split == TX!=RX). Reword to "TX=RX (synFrequency)" to match strings.xml ("TX=RX"). - Parse holdTxFreq like synFreq (any non-empty, non-"0" is true) instead of only exact "1", so the two boolean configs handle stored values consistently. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/main/java/com/k1af/ft8af/database/DatabaseOpr.java | 4 +++- .../java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java | 6 +++--- .../com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java b/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java index c068fff7..3af7c15a 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java @@ -2379,7 +2379,9 @@ protected Void doInBackground(Void... voids) { GeneralVariables.synFrequency = !(result.equals("") || result.equals("0")); } if (name.equalsIgnoreCase("holdTxFreq")) { - GeneralVariables.holdTxFreq = result.equals("1"); + // Parse like synFreq above: any non-empty, non-"0" value is true, + // so the two boolean configs handle stored values consistently. + GeneralVariables.holdTxFreq = !(result.equals("") || result.equals("0")); } if (name.equalsIgnoreCase("transDelay")) { if (result.matches("^\\d{1,4}$")) {//Regex: 1-4 digit number diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java b/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java index 79a61d4e..ecd38f01 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignal.java @@ -350,9 +350,9 @@ public void setBaseFrequency(float freq) { /** * Whether to move our TX offset onto the frequency of the station we're about to - * answer. True only in same-frequency (TX/RX split) mode AND when "Hold TX freq" - * is off — holding TX freq (WSJT-X "Hold Tx Freq") keeps us calling on our own - * offset. Pure decision logic so it can be unit-tested without the Android runtime. + * answer. True only in TX=RX (synFrequency) mode AND when "Hold TX freq" is off — + * holding TX freq (WSJT-X "Hold Tx Freq") keeps us calling on our own offset. + * Pure decision logic so it can be unit-tested without the Android runtime. */ static boolean shouldFollowTargetFreq(boolean synFrequency, boolean holdTxFreq) { return synFrequency && !holdTxFreq; diff --git a/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java b/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java index e8c39e31..25fbc969 100644 --- a/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java +++ b/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/FT8TransmitSignalTest.java @@ -342,9 +342,9 @@ public void huntFilter_potaOnlyOn_keepsPota() { } // ---- shouldFollowTargetFreq --------------------------------------------- - // Same-frequency (TX/RX split) mode moves our TX offset onto the station we - // answer. "Hold TX freq" (WSJT-X Hold Tx Freq) must override that and keep us on - // our own offset. So we follow the target ONLY when split is on AND hold is off. + // TX=RX (synFrequency) mode moves our TX offset onto the station we answer. + // "Hold TX freq" (WSJT-X Hold Tx Freq) must override that and keep us on our own + // offset. So we follow the target ONLY when synFrequency is on AND hold is off. @Test public void followTarget_splitOn_holdOff_follows() {