diff --git a/docs/clear-cq-slot-selection.md b/docs/clear-cq-slot-selection.md index 65e3379d1..edc798724 100644 --- a/docs/clear-cq-slot-selection.md +++ b/docs/clear-cq-slot-selection.md @@ -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 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 d0ee31b75..4ff2c9dd9 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/GeneralVariables.java @@ -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 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 6f69c32ae..44005e452 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 @@ -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())); + } 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. diff --git a/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinder.java b/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinder.java index 9bebcf167..31ffe171f 100644 --- a/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinder.java +++ b/ft8af/app/src/main/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinder.java @@ -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; @@ -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 } @@ -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); } } 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 e2417580c..cb83cf6a5 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 @@ -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); } @@ -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; } /** @@ -2625,7 +2632,8 @@ public void recordBandActivity(ArrayList messages) { && clearFrequencyFinder.shouldRelocate( GeneralVariables.getBaseFrequency(), newestUtc)) { Float pick = clearFrequencyFinder.selectClearOffset( - GeneralVariables.getBaseFrequency()); + GeneralVariables.getBaseFrequency(), + GeneralVariables.minAutoTxFreqHz); if (pick != null) { applyClearCqOffset(pick, newestUtc, "occupied"); } @@ -2663,7 +2671,8 @@ private void maybeSelectClearCqOffset() { return; } Float pick = clearFrequencyFinder.selectClearOffset( - GeneralVariables.getBaseFrequency()); + GeneralVariables.getBaseFrequency(), + GeneralVariables.minAutoTxFreqHz); if (pick != null) { applyClearCqOffset(pick, UtcTimer.getSystemTime(), "cq-start"); } 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 aaf8a644c..4bac7a1b1 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 @@ -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 @@ -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) } @@ -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 @@ -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)) @@ -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), diff --git a/ft8af/app/src/main/res/values-ar/strings_compose.xml b/ft8af/app/src/main/res/values-ar/strings_compose.xml index bb4674a8c..da479e639 100644 --- a/ft8af/app/src/main/res/values-ar/strings_compose.xml +++ b/ft8af/app/src/main/res/values-ar/strings_compose.xml @@ -600,6 +600,8 @@ تم نقل إزاحة CQ إلى موضع خالٍ: %1$d هرتز اختيار إزاحة إرسال خالية تلقائيًا عند نداء CQ يختار موضعًا خاليًا وفق نشاط النطاق الأخير وينتقل إذا أصبح مشغولًا + أدنى تردد صوتي للإرسال التلقائي + أدنى إزاحة صوتية يختارها التطبيق تلقائيًا (اختيار الإزاحة الخالية، متابعة TX=RX). ارفعها إذا كان جهازك يقصّ أو يربّع صوت الإرسال لتقع التوافقيات خارج مرشح الإرسال Tune: يبدأ موالف الجهاز أو يرسل موجة حاملة مستمرة لضبط الهوائي توقف Tune بعد %1$d ث أوقف CQ/QSO قبل استخدام Tune diff --git a/ft8af/app/src/main/res/values-cs/strings_compose.xml b/ft8af/app/src/main/res/values-cs/strings_compose.xml index 5b99879ac..fa8247cc0 100644 --- a/ft8af/app/src/main/res/values-cs/strings_compose.xml +++ b/ft8af/app/src/main/res/values-cs/strings_compose.xml @@ -592,6 +592,8 @@ Offset CQ přesunut na volné místo: %1$d Hz Automaticky volný TX offset Při volání CQ vybere volné místo podle nedávné aktivity v pásmu a uhne, pokud se obsadí + Minimální automatický TX audio kmitočet + 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 Tune: spustí tuner rádia nebo vysílá souvislou nosnou pro ladění antény Tune zastaven po %1$d s Před použitím Tune zastavte CQ/QSO diff --git a/ft8af/app/src/main/res/values-es/strings_compose.xml b/ft8af/app/src/main/res/values-es/strings_compose.xml index fff40b6d2..943efb763 100644 --- a/ft8af/app/src/main/res/values-es/strings_compose.xml +++ b/ft8af/app/src/main/res/values-es/strings_compose.xml @@ -545,6 +545,8 @@ Desplazamiento de CQ movido a un hueco libre: %1$d Hz Offset de TX libre automático Al llamar CQ, elige un hueco libre según la actividad reciente de la banda y se aparta si se ocupa + Frecuencia de audio TX automática mínima + 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 Tune: inicia el sintonizador del equipo o transmite una portadora continua para ajustar la antena Tune detenido tras %1$d s Detén el CQ/QSO antes de usar Tune diff --git a/ft8af/app/src/main/res/values-fr/strings_compose.xml b/ft8af/app/src/main/res/values-fr/strings_compose.xml index b81cb69ab..36dd623dd 100644 --- a/ft8af/app/src/main/res/values-fr/strings_compose.xml +++ b/ft8af/app/src/main/res/values-fr/strings_compose.xml @@ -545,6 +545,8 @@ Décalage CQ déplacé vers un créneau libre : %1$d Hz Décalage TX libre automatique En appelant CQ, choisit un créneau libre selon l\'activité récente de la bande et se déplace s\'il devient occupé + Fréquence audio TX automatique minimale + 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 Tune : démarre le coupleur de la radio ou émet une porteuse continue pour l\'accord d\'antenne Tune arrêté après %1$d s Arrêtez le CQ/QSO avant d\'utiliser Tune diff --git a/ft8af/app/src/main/res/values-in/strings_compose.xml b/ft8af/app/src/main/res/values-in/strings_compose.xml index d64cda32e..20f884edd 100644 --- a/ft8af/app/src/main/res/values-in/strings_compose.xml +++ b/ft8af/app/src/main/res/values-in/strings_compose.xml @@ -580,6 +580,8 @@ Offset CQ dipindahkan ke tempat kosong: %1$d Hz Offset TX kosong otomatis Saat memanggil CQ, memilih tempat kosong dari aktivitas band terkini dan berpindah jika terisi + Frekuensi audio TX otomatis minimum + 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 Tune: memulai tuner radio atau memancarkan gelombang pembawa kontinu untuk penalaan antena Tune dihentikan setelah %1$d dtk Hentikan CQ/QSO sebelum menggunakan Tune diff --git a/ft8af/app/src/main/res/values-it/strings_compose.xml b/ft8af/app/src/main/res/values-it/strings_compose.xml index 40b15edf5..00117430b 100644 --- a/ft8af/app/src/main/res/values-it/strings_compose.xml +++ b/ft8af/app/src/main/res/values-it/strings_compose.xml @@ -577,6 +577,8 @@ Offset CQ spostato in uno spazio libero: %1$d Hz Offset TX libero automatico Chiamando CQ, sceglie uno spazio libero dall\'attività recente della banda e si sposta se viene occupato + Frequenza audio TX automatica minima + Offset audio più basso che l\'app sceglierà da sola (selezione offset libero, inseguimento TX=RX). Aumentalo se la radio taglia o squadra l\'audio TX, così le armoniche cadono fuori dal filtro TX Tune: avvia l\'accordatore della radio o trasmette una portante continua per l\'accordo dell\'antenna Tune fermato dopo %1$d s Ferma il CQ/QSO prima di usare Tune diff --git a/ft8af/app/src/main/res/values-ja/strings_compose.xml b/ft8af/app/src/main/res/values-ja/strings_compose.xml index 59074d56c..e09367504 100644 --- a/ft8af/app/src/main/res/values-ja/strings_compose.xml +++ b/ft8af/app/src/main/res/values-ja/strings_compose.xml @@ -537,6 +537,8 @@ CQオフセットを空いている周波数へ移動:%1$d Hz 空きTXオフセット自動選択 CQ呼び出し時に直近のバンド活動から空き周波数を選び、占有されたら移動します + 自動TXオーディオ周波数の下限 + アプリが自動で選ぶ最低のオーディオオフセット(空きオフセット選択、TX=RX追従)。リグがTXオーディオをクリップまたは矩形波化する場合は上げて、高調波をTXフィルターの外に出します Tune:無線機のチューナーを開始、またはアンテナ調整用の連続キャリアを送信します Tuneは%1$d秒後に停止しました Tuneの前にCQ/QSOを停止してください diff --git a/ft8af/app/src/main/res/values-ko/strings_compose.xml b/ft8af/app/src/main/res/values-ko/strings_compose.xml index 7ab55b5db..d1807ad43 100644 --- a/ft8af/app/src/main/res/values-ko/strings_compose.xml +++ b/ft8af/app/src/main/res/values-ko/strings_compose.xml @@ -580,6 +580,8 @@ CQ 오프셋을 빈 자리로 이동: %1$d Hz 빈 TX 오프셋 자동 선택 CQ 호출 시 최근 밴드 활동을 바탕으로 빈 자리를 선택하고, 점유되면 이동합니다 + 자동 TX 오디오 주파수 최솟값 + 앱이 스스로 선택하는 가장 낮은 오디오 오프셋(빈 오프셋 선택, TX=RX 추종). 리그가 TX 오디오를 클리핑하거나 구형파로 만드는 경우 높여서 고조파가 TX 필터 밖으로 나가게 하세요 Tune: 무전기 튜너를 시작하거나 안테나 조정용 연속 반송파를 송신합니다 Tune이 %1$d초 후 정지되었습니다 Tune 전에 CQ/QSO를 정지하세요 diff --git a/ft8af/app/src/main/res/values-nl/strings_compose.xml b/ft8af/app/src/main/res/values-nl/strings_compose.xml index 0e3142711..2192caf08 100644 --- a/ft8af/app/src/main/res/values-nl/strings_compose.xml +++ b/ft8af/app/src/main/res/values-nl/strings_compose.xml @@ -584,6 +584,8 @@ CQ-offset verplaatst naar een vrije plek: %1$d Hz Automatisch vrije TX-offset Kiest bij het roepen van CQ een vrije plek op basis van recente bandactiviteit en wijkt uit als die bezet raakt + Minimale automatische TX-audiofrequentie + Laagste audio-offset die de app zelf kiest (vrije-offsetkiezer, TX=RX volgen). Verhoog deze als je set de TX-audio clipt of blokvormig maakt, zodat harmonischen buiten het TX-filter vallen Tune: start de tuner van de radio of zendt een continue draaggolf uit voor antenne-afstemming Tune gestopt na %1$d s Stop de CQ/QSO voordat je Tune gebruikt diff --git a/ft8af/app/src/main/res/values-pl/strings_compose.xml b/ft8af/app/src/main/res/values-pl/strings_compose.xml index 819af84c6..4fa20864e 100644 --- a/ft8af/app/src/main/res/values-pl/strings_compose.xml +++ b/ft8af/app/src/main/res/values-pl/strings_compose.xml @@ -581,6 +581,8 @@ Offset CQ przeniesiony w wolne miejsce: %1$d Hz Automatyczny wolny offset TX Przy wołaniu CQ wybiera wolne miejsce na podstawie ostatniej aktywności pasma i przenosi się, gdy zostanie zajęte + Minimalna automatyczna częstotliwość audio TX + Najniższy offset audio, jaki aplikacja wybierze sama (wybór wolnego offsetu, podążanie TX=RX). Zwiększ, jeśli radio obcina lub zamienia audio TX w prostokąt, aby harmoniczne wypadały poza filtr TX Tune: uruchamia tuner radia lub nadaje ciągłą nośną do strojenia anteny Tune zatrzymany po %1$d s Zatrzymaj CQ/QSO przed użyciem Tune diff --git a/ft8af/app/src/main/res/values-pt-rBR/strings_compose.xml b/ft8af/app/src/main/res/values-pt-rBR/strings_compose.xml index 9e76fe3fb..f536aa11a 100644 --- a/ft8af/app/src/main/res/values-pt-rBR/strings_compose.xml +++ b/ft8af/app/src/main/res/values-pt-rBR/strings_compose.xml @@ -663,6 +663,8 @@ Offset de CQ movido para um espaço livre: %1$d Hz Limpar offset de TX automaticamente Ao chamar CQ, escolhe um espaço livre com base na atividade recente da banda e se afasta se ele ficar ocupado + Frequência de áudio TX automática mínima + Menor offset de áudio que o app escolherá sozinho (seletor de offset livre, seguir TX=RX). Aumente se o seu rádio ceifa ou torna quadrado o áudio de TX, para que os harmônicos fiquem fora do filtro de TX Tune — inicia o sintonizador do rig ou transmite uma portadora contínua para sintonia de antena Tune interrompido após %1$d s Pare o CQ/QSO antes de sintonizar diff --git a/ft8af/app/src/main/res/values-ru/strings_compose.xml b/ft8af/app/src/main/res/values-ru/strings_compose.xml index a1c8b75f6..91ebfc81b 100644 --- a/ft8af/app/src/main/res/values-ru/strings_compose.xml +++ b/ft8af/app/src/main/res/values-ru/strings_compose.xml @@ -549,6 +549,8 @@ Смещение CQ перенесено на свободное место: %1$d Гц Автовыбор свободного смещения TX При вызове CQ выбирает свободное место по недавней активности диапазона и уходит, если оно занимается + Минимальная авто-частота аудио TX + Наименьшее аудиосмещение, которое приложение выберет само (выбор свободного смещения, следование TX=RX). Увеличьте, если трансивер ограничивает или превращает аудио TX в меандр, чтобы гармоники оказались вне фильтра TX Tune: запускает тюнер трансивера или передаёт непрерывную несущую для настройки антенны Tune остановлен через %1$d с Остановите CQ/QSO перед использованием Tune diff --git a/ft8af/app/src/main/res/values-tr/strings_compose.xml b/ft8af/app/src/main/res/values-tr/strings_compose.xml index 820c2f717..2da7ec1cd 100644 --- a/ft8af/app/src/main/res/values-tr/strings_compose.xml +++ b/ft8af/app/src/main/res/values-tr/strings_compose.xml @@ -573,6 +573,8 @@ CQ ofseti boş bir noktaya taşındı: %1$d Hz Otomatik boş TX ofseti CQ çağrılırken son bant etkinliğine göre boş bir nokta seçer ve doldurulursa başka yere taşınır + En düşük otomatik TX ses frekansı + Uygulamanın kendi seçeceği en düşük ses ofseti (boş ofset seçici, TX=RX takibi). Cihazınız TX sesini kırpıyor veya kare dalgaya çeviriyorsa yükseltin; böylece harmonikler TX filtresinin dışında kalır Tune: telsizin tunerini başlatır veya anten ayarı için sürekli taşıyıcı gönderir Tune %1$d s sonra durduruldu Tune kullanmadan önce CQ/QSO\'yu durdurun diff --git a/ft8af/app/src/main/res/values-uk/strings_compose.xml b/ft8af/app/src/main/res/values-uk/strings_compose.xml index d193f5ab5..2f46c954f 100644 --- a/ft8af/app/src/main/res/values-uk/strings_compose.xml +++ b/ft8af/app/src/main/res/values-uk/strings_compose.xml @@ -581,6 +581,8 @@ Зсув CQ перенесено на вільне місце: %1$d Гц Автовибір вільного зсуву TX Під час виклику CQ обирає вільне місце за нещодавньою активністю діапазону і зміщується, якщо воно займається + Мінімальна авто-частота аудіо TX + Найнижчий аудіозсув, який застосунок обере сам (вибір вільного зсуву, слідування TX=RX). Збільште, якщо трансивер обмежує або перетворює аудіо TX на меандр, щоб гармоніки опинилися поза фільтром TX Tune: запускає тюнер трансивера або передає безперервну несучу для налаштування антени Tune зупинено через %1$d с Зупиніть CQ/QSO перед використанням Tune diff --git a/ft8af/app/src/main/res/values-zh-rCN/strings_compose.xml b/ft8af/app/src/main/res/values-zh-rCN/strings_compose.xml index 143a037db..133c1196f 100644 --- a/ft8af/app/src/main/res/values-zh-rCN/strings_compose.xml +++ b/ft8af/app/src/main/res/values-zh-rCN/strings_compose.xml @@ -537,6 +537,8 @@ CQ频偏已移至空闲位置:%1$d Hz 自动选择空闲TX频偏 呼叫CQ时根据近期频段活动选择空闲位置,被占用时自动移开 + 自动TX音频频率下限 + 应用自动选择的最低音频频偏(空闲频偏选择、TX=RX跟随)。如果电台会削波或把TX音频变成方波,请调高此值,使谐波落在TX滤波器之外 Tune:启动电台调谐器或发射连续载波用于天线调谐 Tune已在%1$d秒后停止 使用Tune前请先停止CQ/QSO diff --git a/ft8af/app/src/main/res/values-zh-rTW/strings_compose.xml b/ft8af/app/src/main/res/values-zh-rTW/strings_compose.xml index 89fc6ef30..4708c8e49 100644 --- a/ft8af/app/src/main/res/values-zh-rTW/strings_compose.xml +++ b/ft8af/app/src/main/res/values-zh-rTW/strings_compose.xml @@ -537,6 +537,8 @@ CQ頻偏已移至空閒位置:%1$d Hz 自動選擇空閒TX頻偏 呼叫CQ時根據近期頻段活動選擇空閒位置,被佔用時自動移開 + 自動TX音訊頻率下限 + 應用程式自動選擇的最低音訊頻偏(空閒頻偏選擇、TX=RX跟隨)。如果電台會削波或把TX音訊變成方波,請調高此值,使諧波落在TX濾波器之外 Tune:啟動電台調諧器或發射連續載波用於天線調諧 Tune已在%1$d秒後停止 使用Tune前請先停止CQ/QSO diff --git a/ft8af/app/src/main/res/values/strings_compose.xml b/ft8af/app/src/main/res/values/strings_compose.xml index 4978e8c07..d0076e403 100644 --- a/ft8af/app/src/main/res/values/strings_compose.xml +++ b/ft8af/app/src/main/res/values/strings_compose.xml @@ -1072,6 +1072,8 @@ CQ offset moved to a clear spot: %1$d Hz Auto clear TX offset When calling CQ, pick a clear spot from recent band activity and move away if it becomes occupied + Minimum auto TX audio frequency + Lowest audio offset the app will pick on its own (clear-offset picker, TX=RX follow). Raise it if your rig clips or squares the TX audio, so harmonics fall outside the TX filter TUNE Tune — start the rig\'s tuner or transmit a steady carrier for antenna tuning Tune stopped after %1$d s diff --git a/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinderTest.java b/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinderTest.java index ee4d2c79b..a3f598d80 100644 --- a/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinderTest.java +++ b/ft8af/app/src/test/java/com/k1af/ft8af/ft8transmit/ClearFrequencyFinderTest.java @@ -190,6 +190,98 @@ public void holdDownPacesRepeatedMoves() { // ---- the CQ-idle gate (wiring predicate on FT8TransmitSignal) -------------- + // ---- minimum auto TX audio frequency (operator floor) -------------------- + + @Test + public void floor_keepsPicksAtOrAboveIt() { + ClearFrequencyFinder f = finder(); + // Busy above the floor, wide open below it: without a floor the pick + // would land in the low half. + for (float hz = 1500f; hz <= 2700f; hz += 100f) { + occupy(f, hz, T0); + } + assertThat(f.selectClearOffset(1510f)).isLessThan(1500f); + + ClearFrequencyFinder floored = finder(); + occupy(floored, 2000f, T0); + Float pick = floored.selectClearOffset(2010f, 1500f); + assertThat(pick).isNotNull(); + assertThat(pick).isAtLeast(1500f); + assertThat(floored.isOccupied(pick)).isFalse(); + } + + @Test + public void floor_staysPutWhenNothingAboveItIsBetter() { + ClearFrequencyFinder f = finder(); + // Everything from the floor up is occupied; the free space below the + // floor must not be used, so there is nowhere better to go. + for (float hz = 1450f; hz <= 2800f; hz += 50f) { + occupy(f, hz, T0); + } + assertThat(f.selectClearOffset(2000f, 1500f)).isNull(); + } + + @Test + public void floor_movesAClearCurrentOffsetThatSitsBelowIt() { + ClearFrequencyFinder f = finder(); + occupy(f, 2500f, T0); + // 800 Hz is clear, and without a floor that means "stay"... + assertThat(f.selectClearOffset(800f)).isNull(); + // ...but it is below the floor, so the picker lifts it out. + Float pick = f.selectClearOffset(800f, 1500f); + assertThat(pick).isNotNull(); + assertThat(pick).isAtLeast(1500f); + assertThat(f.isOccupied(pick)).isFalse(); + } + + @Test + public void floor_appliesEvenWithNoHistory() { + ClearFrequencyFinder f = finder(); + Float pick = f.selectClearOffset(800f, 1500f); + // Nothing heard: the middle of the allowed range, not the floor edge. + assertThat(pick).isWithin(10f).of((1500f + 2750f) / 2f); + // An offset already above the floor still needs history to move. + assertThat(f.selectClearOffset(1800f, 1500f)).isNull(); + } + + @Test + public void floor_atOrBelowTheGridMinimumIsNoFloor() { + ClearFrequencyFinder f = finder(); + // "Off" (0) must behave exactly like the unfloored call, including + // for an offset under the grid minimum and with no history. + assertThat(f.selectClearOffset(100f, 0f)).isNull(); + occupy(f, 1500f, T0); + assertThat(f.selectClearOffset(1510f, 0f)).isEqualTo(f.selectClearOffset(1510f)); + assertThat(f.selectClearOffset(100f, 0f)).isNull(); + } + + @Test + public void floor_isCappedSoACandidateAlwaysFits() { + ClearFrequencyFinder f = finder(); + Float pick = f.selectClearOffset(800f, 9000f); + assertThat(pick).isNotNull(); + assertThat(pick + ClearFrequencyFinder.SIGNAL_BANDWIDTH_HZ) + .isAtMost(ClearFrequencyFinder.DEFAULT_MAX_OFFSET_HZ); + } + + @Test + public void minAutoTxChoices_snapStoredValuesOntoTheList() { + int[] choices = ClearFrequencyFinder.minAutoTxChoicesHz(); + assertThat(choices[0]).isEqualTo(0); // index 0 is "Off" + for (int i = 0; i < choices.length; i++) { + assertThat(ClearFrequencyFinder.minAutoTxChoiceIndex(choices[i])).isEqualTo(i); + assertThat(ClearFrequencyFinder.clampMinAutoTxHz(choices[i])).isEqualTo(choices[i]); + } + // Garbage from a settings import: negative, off-grid, oversized. + assertThat(ClearFrequencyFinder.clampMinAutoTxHz(-5)).isEqualTo(0); + assertThat(ClearFrequencyFinder.clampMinAutoTxHz(1499)).isEqualTo(1000); + assertThat(ClearFrequencyFinder.clampMinAutoTxHz(99_999)) + .isEqualTo(choices[choices.length - 1]); + // The accessor hands out a copy, not the backing array. + choices[0] = 123; + assertThat(ClearFrequencyFinder.minAutoTxChoicesHz()[0]).isEqualTo(0); + } + @Test public void autoPickGate_requiresEnabledActivatedIdleCq() { TransmitCallsign cq = new TransmitCallsign(1, 0, "CQ", 0); 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 cd95c9012..36db7505d 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 @@ -353,20 +353,33 @@ public void huntFilter_potaOnlyOn_keepsPota() { @Test public void followTarget_splitOn_holdOff_follows() { assertThat(FT8TransmitSignal.shouldFollowTargetFreq( - /*synFrequency*/ true, /*holdTxFreq*/ false)).isTrue(); + /*synFrequency*/ true, /*holdTxFreq*/ false, + /*targetFreqHz*/ 300f, /*minAutoTxFreqHz*/ 0)).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(); + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(true, true, 1500f, 0)).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(); + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(false, false, 1500f, 0)).isFalse(); + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(false, true, 1500f, 0)).isFalse(); + } + + @Test + public void followTarget_minAutoTxFloor_blocksTargetsBelowIt() { + // A rig that squares the TX audio must not be dragged onto a low + // offset whose harmonics land inside the TX filter. + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(true, false, 800f, 1500)).isFalse(); + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(true, false, 1500f, 1500)).isTrue(); + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(true, false, 2200f, 1500)).isTrue(); + // The floor never turns a held/non-split TX into a follow. + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(true, true, 2200f, 1500)).isFalse(); + assertThat(FT8TransmitSignal.shouldFollowTargetFreq(false, false, 2200f, 1500)).isFalse(); } // ---- shouldStopAfterOneShot ---------------------------------------------