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 1186cc9a..41866499 100644
--- a/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java
+++ b/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java
@@ -381,6 +381,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 47c418d3..8127f61c 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,6 +2379,11 @@ protected Void doInBackground(Void... voids) {
if (name.equalsIgnoreCase("synFreq")) {
GeneralVariables.synFrequency = !(result.equals("") || result.equals("0"));
}
+ if (name.equalsIgnoreCase("holdTxFreq")) {
+ // 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
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 ee670677..05de3cdb 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
@@ -329,7 +329,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);
}
@@ -349,6 +352,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 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;
+ }
+
/**
* 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 14992a39..931cb2dd 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 clearOnBandModeChange by remember { mutableStateOf(GeneralVariables.clearOnBandModeChange) }
var watchdogMs by remember { mutableIntStateOf(GeneralVariables.launchSupervision) }
var noReplyLimit by remember { mutableIntStateOf(GeneralVariables.noReplyLimit) }
@@ -142,6 +143,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_clear_on_change),
description = stringResource(R.string.settings_clear_on_change_desc),
diff --git a/ft8af/app/src/main/res/values/strings_compose.xml b/ft8af/app/src/main/res/values/strings_compose.xml
index f6534c17..f8c45c5a 100644
--- a/ft8af/app/src/main/res/values/strings_compose.xml
+++ b/ft8af/app/src/main/res/values/strings_compose.xml
@@ -431,6 +431,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
Clear decodes on band/mode change
Wipe the decode list and reset the TX target to CQ when you change band or mode
TX Watchdog
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 de3c1f1d..89911d8b 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
@@ -341,6 +341,30 @@ public void huntFilter_potaOnlyOn_keepsPota() {
assertThat(FT8TransmitSignal.huntFilterExcludes(true, true)).isFalse();
}
+ // ---- shouldFollowTargetFreq ---------------------------------------------
+ // 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() {
+ 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();
+ }
+
// ---- shouldStopAfterOneShot ---------------------------------------------
// Free text is a one-shot (WSJT-X Tx5): it transmits once and then the
// sequencer stops, rather than repeating every cycle like a CQ. The auto-stop