Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/clear-cq-slot-selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ tunables below are constructor parameters with the defaults shown.
- Off by default (config key `autoClearTxFreq`); the manual waterfall tap
always wins — it just becomes the new "current" offset that the keep-current
bias protects.
- **Minimum auto TX audio frequency** (config key `minAutoTxFreq`, Hz, 0 =
off; choices 500/1000/1500/2000). A rig that clips or squares the TX audio
radiates harmonics of the tone, and from a low offset they still fall inside
the ~3 kHz TX filter (3rd harmonic below 1000 Hz, 2nd below 1500 Hz). With a
floor set, the picker scans only from the floor up, tie-breaks toward the
middle of the remaining range, and at CQ start lifts a current offset that
sits below the floor even when it is clear or nothing has been decoded yet.
The mid-CQ re-check still fires only on "occupied", so a deliberate waterfall
tap below the floor survives until the next CQ start. The same floor stops
the TX=RX follow from moving onto a station below it (we answer split from
our own offset). Fox/Hound QSYs are protocol-mandated and ignore it.

## Validation

Expand Down
6 changes: 6 additions & 0 deletions ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,12 @@ public static boolean isConfiguredUsbAudioOutput(int vid, int pid) {
//the decode-delivery thread inside recordBandActivity.
public static volatile boolean autoClearTxFreq = false;

//Floor (Hz) for automatically chosen TX audio offsets — the clear-CQ picker
//and the TX=RX follow. 0 = off. For rigs whose TX audio is clipped/squared,
//where low offsets put harmonics inside the TX passband. One of
//ClearFrequencyFinder.minAutoTxChoicesHz(). volatile: same threads as above.
public static volatile int minAutoTxFreqHz = 0;

//Tune button (issue #408): hard cap on a single tune carrier in seconds
//(clamped by TuneController), whether the tune level is independent of the
//FT8 drive, the global independent level (0..100), and the per-band
Expand Down
11 changes: 11 additions & 0 deletions ft8af/app/src/main/java/com/k1af/ft8af/database/DatabaseOpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -2984,6 +2984,17 @@ protected Void doInBackground(Void... voids) {
if (name.equalsIgnoreCase("autoClearTxFreq")) {//Auto-select clear CQ offset (issue #418)
GeneralVariables.autoClearTxFreq = "1".equals(result);
}
if (name.equalsIgnoreCase("minAutoTxFreq")) {//Floor for auto-picked TX offsets, 0 = off
//Defensive parse: settings import (#382) can feed anything here.
if (result != null) {
try {
GeneralVariables.minAutoTxFreqHz =
com.k1af.ft8af.ft8transmit.ClearFrequencyFinder.clampMinAutoTxHz(
Integer.parseInt(result.trim()));
Comment on lines +2989 to +2993
} catch (NumberFormatException ignored) {
}
}
}
if (name.equalsIgnoreCase("tuneMaxOnSeconds")) {//Tune carrier hard cap (issue #408)
//Defensive parse: settings import (#382) can feed anything here.
//Null/non-numeric keeps the default; TuneController clamps the range.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,40 @@ public final class ClearFrequencyFinder {
public static final float PREFERRED_CENTER_HZ = 1500f;
public static final long DEFAULT_MOVE_HOLDDOWN_MS = 45_000L;

/**
* Choices for the "minimum auto TX audio frequency" setting, in Hz; 0 is
* off (no floor beyond {@link #DEFAULT_MIN_OFFSET_HZ}). A rig that clips
* or squares the TX audio radiates harmonics of the tone: with a ~3 kHz
* TX filter the 3rd harmonic is rejected above 1000 Hz and the 2nd above
* 1500 Hz, so the floor keeps automatic picks out of the low offsets
* whose harmonics would still land in the passband.
*/
private static final int[] MIN_AUTO_TX_CHOICES_HZ = {0, 500, 1000, 1500, 2000};

/** A copy of the setting's choices (index 0 = off). */
public static int[] minAutoTxChoicesHz() {
return MIN_AUTO_TX_CHOICES_HZ.clone();
}

/**
* Index of the largest choice not above {@code hz} — maps any stored or
* imported value (negative, off-grid, oversized) onto a valid choice.
*/
public static int minAutoTxChoiceIndex(int hz) {
int index = 0;
for (int i = 0; i < MIN_AUTO_TX_CHOICES_HZ.length; i++) {
if (MIN_AUTO_TX_CHOICES_HZ[i] <= hz) {
index = i;
}
}
return index;
}

/** Snap a stored/imported setting value onto one of the choices. */
public static int clampMinAutoTxHz(int hz) {
return MIN_AUTO_TX_CHOICES_HZ[minAutoTxChoiceIndex(hz)];
}

/** One observed signal: base offset + the slot UTC it was decoded in. */
private static final class Observation {
final float freqHz;
Expand Down Expand Up @@ -148,14 +182,28 @@ public synchronized boolean isOccupied(float offsetHz) {
* current offset's clearance.
*/
public synchronized Float selectClearOffset(float currentOffsetHz) {
if (history.isEmpty()) {
return selectClearOffset(currentOffsetHz, minOffsetHz);
}

/**
* {@link #selectClearOffset(float)} with an operator floor: no candidate
* below {@code floorHz} is considered, and a current offset below the floor
* always warrants a move (even with no history, and even when it is clear)
* — the floor exists because the low offsets are unusable on this rig, not
* because they are busy. A floor at or below the grid minimum is no floor.
*/
public synchronized Float selectClearOffset(float currentOffsetHz, float floorHz) {
float lo = Math.max(minOffsetHz,
Math.min(floorHz, maxOffsetHz - SIGNAL_BANDWIDTH_HZ));
boolean belowFloor = lo > minOffsetHz && currentOffsetHz < lo;
if (history.isEmpty() && !belowFloor) {
return null;
}
float currentClearance = clearanceAt(currentOffsetHz);
float currentClearance = belowFloor ? -1f : clearanceAt(currentOffsetHz);
if (currentClearance > 0f) {
return null; // already clear: a gratuitous QSY loses spotted callers
}
Candidate best = bestCandidate();
Candidate best = bestCandidate(lo);
if (best == null || best.clearance <= currentClearance) {
return null; // whole band as bad or worse: stay put
}
Expand All @@ -173,19 +221,24 @@ private static final class Candidate {
}

/**
* Best candidate on the grid: max clearance, ties broken toward the band
* centre (1500 Hz, flat in every rig's passband), then toward the lower
* offset for determinism.
* Best candidate on the grid from {@code lo} up: max clearance, ties broken
* toward the band centre (1500 Hz, flat in every rig's passband), then
* toward the lower offset for determinism. With a floor raised above the
* grid minimum the tie-break targets the middle of what is left instead,
* so the pick doesn't hug the floor it was told to stay clear of.
*/
private Candidate bestCandidate() {
private Candidate bestCandidate(float lo) {
float preferred = lo > minOffsetHz
? (lo + maxOffsetHz - SIGNAL_BANDWIDTH_HZ) / 2f
: PREFERRED_CENTER_HZ;
Candidate best = null;
for (float f = minOffsetHz; f + SIGNAL_BANDWIDTH_HZ <= maxOffsetHz; f += stepHz) {
for (float f = lo; f + SIGNAL_BANDWIDTH_HZ <= maxOffsetHz; f += stepHz) {
float clearance = clearanceAt(f);
if (best == null
|| clearance > best.clearance
|| (clearance == best.clearance
&& Math.abs(f - PREFERRED_CENTER_HZ)
< Math.abs(best.offsetHz - PREFERRED_CENTER_HZ))) {
&& Math.abs(f - preferred)
< Math.abs(best.offsetHz - preferred))) {
best = new Candidate(f, clearance);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@ public void setTransmit(TransmitCallsign transmitCallsign
// 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)) {
if (shouldFollowTargetFreq(GeneralVariables.synFrequency, GeneralVariables.holdTxFreq,
transmitCallsign.frequency, GeneralVariables.minAutoTxFreqHz)) {
setBaseFrequency(transmitCallsign.frequency);
}

Expand All @@ -700,10 +701,16 @@ 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 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.
* A target below the "minimum auto TX audio frequency" floor (0 = off) is not
* followed either: we answer split from our own offset instead.
* Pure decision logic so it can be unit-tested without the Android runtime.
*/
static boolean shouldFollowTargetFreq(boolean synFrequency, boolean holdTxFreq) {
return synFrequency && !holdTxFreq;
static boolean shouldFollowTargetFreq(boolean synFrequency, boolean holdTxFreq,
float targetFreqHz, int minAutoTxFreqHz) {
if (!synFrequency || holdTxFreq) {
return false;
}
return minAutoTxFreqHz <= 0 || targetFreqHz >= minAutoTxFreqHz;
}

/**
Expand Down Expand Up @@ -2625,7 +2632,8 @@ public void recordBandActivity(ArrayList<Ft8Message> messages) {
&& clearFrequencyFinder.shouldRelocate(
GeneralVariables.getBaseFrequency(), newestUtc)) {
Float pick = clearFrequencyFinder.selectClearOffset(
GeneralVariables.getBaseFrequency());
GeneralVariables.getBaseFrequency(),
GeneralVariables.minAutoTxFreqHz);
if (pick != null) {
applyClearCqOffset(pick, newestUtc, "occupied");
}
Expand Down Expand Up @@ -2663,7 +2671,8 @@ private void maybeSelectClearCqOffset() {
return;
}
Float pick = clearFrequencyFinder.selectClearOffset(
GeneralVariables.getBaseFrequency());
GeneralVariables.getBaseFrequency(),
GeneralVariables.minAutoTxFreqHz);
Comment on lines 2673 to +2675
if (pick != null) {
applyClearCqOffset(pick, UtcTimer.getSystemTime(), "cq-start");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.sp
import com.k1af.ft8af.GeneralVariables
import com.k1af.ft8af.MainViewModel
import com.k1af.ft8af.R
import com.k1af.ft8af.ft8transmit.ClearFrequencyFinder
import com.k1af.ft8af.ft8transmit.MeterProtectionController
import com.k1af.ft8af.ft8transmit.TuneController
import com.k1af.ft8af.ft8transmit.TuneMethod
Expand Down Expand Up @@ -93,6 +94,7 @@ fun TransmissionSettings(
) {
var synFrequency by remember { mutableStateOf(GeneralVariables.synFrequency) }
var holdTxFreq by remember { mutableStateOf(GeneralVariables.holdTxFreq) }
var minAutoTxFreqHz by remember { mutableIntStateOf(GeneralVariables.minAutoTxFreqHz) }
var clearOnBandModeChange by remember { mutableStateOf(GeneralVariables.clearOnBandModeChange) }
var watchdogMs by remember { mutableIntStateOf(GeneralVariables.launchSupervision) }
var noReplyLimit by remember { mutableIntStateOf(GeneralVariables.noReplyLimit) }
Expand Down Expand Up @@ -123,6 +125,7 @@ fun TransmissionSettings(
var showWatchdog by remember { mutableStateOf(false) }
var showStopAfter by remember { mutableStateOf(false) }
var showMax73 by remember { mutableStateOf(false) }
var showMinAutoTxFreq by remember { mutableStateOf(false) }
var showTuneMethod by remember { mutableStateOf(false) }

// Index == TuneMethod.AUTOMATIC/INTERNAL/TONE
Expand Down Expand Up @@ -209,6 +212,30 @@ fun TransmissionSettings(
)
}

// -- Minimum auto TX audio frequency picker --
if (showMinAutoTxFreq) {
val choices = ClearFrequencyFinder.minAutoTxChoicesHz()
val labels = choices.map { hz ->
if (hz == 0) stringResource(R.string.common_off)
else stringResource(R.string.settings_hz_format, hz)
}
ListPickerDialog(
title = stringResource(R.string.settings_min_auto_tx_freq),
items = labels,
selectedIndex = ClearFrequencyFinder.minAutoTxChoiceIndex(minAutoTxFreqHz),
onDismiss = { showMinAutoTxFreq = false },
onSelect = { index ->
showMinAutoTxFreq = false
val hz = choices[index]
GeneralVariables.minAutoTxFreqHz = hz
minAutoTxFreqHz = hz
mainViewModel.databaseOpr.writeConfig(
"minAutoTxFreq", hz.toString(), null,
)
},
)
}

// -- Max 73 Sends Picker --
if (showMax73) {
val max73Options = mutableListOf(stringResource(R.string.settings_max_73_auto))
Expand Down Expand Up @@ -267,6 +294,15 @@ fun TransmissionSettings(
},
)
SectionDivider()
SettingsRow(
label = stringResource(R.string.settings_min_auto_tx_freq),
description = stringResource(R.string.settings_min_auto_tx_freq_desc),
value = if (minAutoTxFreqHz == 0) stringResource(R.string.common_off)
else stringResource(R.string.settings_hz_format, minAutoTxFreqHz),
showChevron = true,
onClick = { showMinAutoTxFreq = true },
)
SectionDivider()
SettingsRow(
label = stringResource(R.string.settings_clear_on_change),
description = stringResource(R.string.settings_clear_on_change_desc),
Expand Down
2 changes: 2 additions & 0 deletions ft8af/app/src/main/res/values-ar/strings_compose.xml
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,8 @@
<string name="cq_offset_moved">تم نقل إزاحة CQ إلى موضع خالٍ: %1$d هرتز</string>
<string name="settings_auto_clear_tx">اختيار إزاحة إرسال خالية تلقائيًا</string>
<string name="settings_auto_clear_tx_desc">عند نداء CQ يختار موضعًا خاليًا وفق نشاط النطاق الأخير وينتقل إذا أصبح مشغولًا</string>
<string name="settings_min_auto_tx_freq">أدنى تردد صوتي للإرسال التلقائي</string>
<string name="settings_min_auto_tx_freq_desc">أدنى إزاحة صوتية يختارها التطبيق تلقائيًا (اختيار الإزاحة الخالية، متابعة TX=RX). ارفعها إذا كان جهازك يقصّ أو يربّع صوت الإرسال لتقع التوافقيات خارج مرشح الإرسال</string>
<string name="tune_content_description">Tune: يبدأ موالف الجهاز أو يرسل موجة حاملة مستمرة لضبط الهوائي</string>
<string name="tune_stopped_timeout">توقف Tune بعد %1$d ث</string>
<string name="tune_blocked_tx">أوقف CQ/QSO قبل استخدام Tune</string>
Expand Down
2 changes: 2 additions & 0 deletions ft8af/app/src/main/res/values-cs/strings_compose.xml
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@
<string name="cq_offset_moved">Offset CQ přesunut na volné místo: %1$d Hz</string>
<string name="settings_auto_clear_tx">Automaticky volný TX offset</string>
<string name="settings_auto_clear_tx_desc">Při volání CQ vybere volné místo podle nedávné aktivity v pásmu a uhne, pokud se obsadí</string>
<string name="settings_min_auto_tx_freq">Minimální automatický TX audio kmitočet</string>
<string name="settings_min_auto_tx_freq_desc">Nejnižší audio offset, který aplikace sama zvolí (výběr volného offsetu, následování TX=RX). Zvyšte jej, pokud rádio TX audio ořezává nebo převádí na obdélník, aby harmonické padly mimo TX filtr</string>
<string name="tune_content_description">Tune: spustí tuner rádia nebo vysílá souvislou nosnou pro ladění antény</string>
<string name="tune_stopped_timeout">Tune zastaven po %1$d s</string>
<string name="tune_blocked_tx">Před použitím Tune zastavte CQ/QSO</string>
Expand Down
2 changes: 2 additions & 0 deletions ft8af/app/src/main/res/values-es/strings_compose.xml
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@
<string name="cq_offset_moved">Desplazamiento de CQ movido a un hueco libre: %1$d Hz</string>
<string name="settings_auto_clear_tx">Offset de TX libre automático</string>
<string name="settings_auto_clear_tx_desc">Al llamar CQ, elige un hueco libre según la actividad reciente de la banda y se aparta si se ocupa</string>
<string name="settings_min_auto_tx_freq">Frecuencia de audio TX automática mínima</string>
<string name="settings_min_auto_tx_freq_desc">Offset de audio más bajo que la app elegirá por sí sola (selector de offset libre, seguimiento TX=RX). Súbelo si tu equipo recorta o cuadra el audio de TX, para que los armónicos queden fuera del filtro de TX</string>
<string name="tune_content_description">Tune: inicia el sintonizador del equipo o transmite una portadora continua para ajustar la antena</string>
<string name="tune_stopped_timeout">Tune detenido tras %1$d s</string>
<string name="tune_blocked_tx">Detén el CQ/QSO antes de usar Tune</string>
Expand Down
2 changes: 2 additions & 0 deletions ft8af/app/src/main/res/values-fr/strings_compose.xml
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@
<string name="cq_offset_moved">Décalage CQ déplacé vers un créneau libre : %1$d Hz</string>
<string name="settings_auto_clear_tx">Décalage TX libre automatique</string>
<string name="settings_auto_clear_tx_desc">En appelant CQ, choisit un créneau libre selon l\'activité récente de la bande et se déplace s\'il devient occupé</string>
<string name="settings_min_auto_tx_freq">Fréquence audio TX automatique minimale</string>
<string name="settings_min_auto_tx_freq_desc">Décalage audio le plus bas que l\'application choisira d\'elle-même (sélection du décalage libre, suivi TX=RX). Augmentez-le si votre poste écrête ou met en créneaux l\'audio TX, afin que les harmoniques tombent hors du filtre TX</string>
<string name="tune_content_description">Tune : démarre le coupleur de la radio ou émet une porteuse continue pour l\'accord d\'antenne</string>
<string name="tune_stopped_timeout">Tune arrêté après %1$d s</string>
<string name="tune_blocked_tx">Arrêtez le CQ/QSO avant d\'utiliser Tune</string>
Expand Down
2 changes: 2 additions & 0 deletions ft8af/app/src/main/res/values-in/strings_compose.xml
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,8 @@
<string name="cq_offset_moved">Offset CQ dipindahkan ke tempat kosong: %1$d Hz</string>
<string name="settings_auto_clear_tx">Offset TX kosong otomatis</string>
<string name="settings_auto_clear_tx_desc">Saat memanggil CQ, memilih tempat kosong dari aktivitas band terkini dan berpindah jika terisi</string>
<string name="settings_min_auto_tx_freq">Frekuensi audio TX otomatis minimum</string>
<string name="settings_min_auto_tx_freq_desc">Offset audio terendah yang akan dipilih aplikasi sendiri (pemilih offset kosong, ikuti TX=RX). Naikkan jika radio Anda memotong atau mengubah audio TX menjadi gelombang kotak, agar harmonik jatuh di luar filter TX</string>
<string name="tune_content_description">Tune: memulai tuner radio atau memancarkan gelombang pembawa kontinu untuk penalaan antena</string>
<string name="tune_stopped_timeout">Tune dihentikan setelah %1$d dtk</string>
<string name="tune_blocked_tx">Hentikan CQ/QSO sebelum menggunakan Tune</string>
Expand Down
Loading
Loading