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