Compare commits

...
Author SHA1 Message Date
Claude 86289609e8 fix: make beacons session-only, slower and QSY-aware (#171)
Forgotten beacons kept filling the chat while nobody was at the station.

- The two enable switches are session state now: they start off, are
  never read from preferences.xml and are always stored as "false", so
  neither a stored switch nor an older release reading a newer file can
  start a beacon. The in-code default was "on" and started a beacon
  whenever the file was unreadable or had no <beaconCQ> block.
- The minimum interval rises from 1 to 5 minutes; the bundled template
  ships 10 instead of 1. Smaller stored values are raised when the
  timer is scheduled.
- BeaconFrequencyGuard switches a beacon off once a logger-reported
  transceiver frequency lies more than 2 kHz from the one reported when
  the beacon was switched on. UCXLog RadioInfo and Win-Test STATUS feed
  it wherever they already set MYQRG/SECONDQRG. Without SO2R the single
  radio stands for both categories, with SO2R each radio for its own.

Manual (DE/EN), changelog, website and PROJECT_CONTEXT updated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NJ1tUJVmWChD9V5Pg7uH8X
2026-11-11 09:53:27 +00:00
25 changed files with 600 additions and 39 deletions

No files matched your search

@@ -473,7 +473,7 @@ profiles.changeAfterSwitch=Die Änderung wirkt erst nach einem erneuten Wechsel
profiles.alreadyActive=Dieses Profil ist bereits aktiv.
profiles.selectFirst=Zuerst ein Profil in der Tabelle wählen.
beacon.invalidMessage=Der Bakentext ist ungültig: {0}
beacon.intervalTooShort=Einen ganzzahligen Bakenabstand von mindestens {0} Minute eingeben.
beacon.intervalTooShort=Einen ganzzahligen Bakenabstand von mindestens {0} Minuten eingeben.
notification.invalidPort=„{0}" ist kein gültiger TCP-Port. Einen Wert von 1 bis 65535 eingeben.
notification.invalidSpotter=„{0}" ist kein gültiges Spotter-Rufzeichen.
notification.invalidCallsign=Bitte ein gültiges Rufzeichen eingeben.
@@ -482,7 +482,7 @@ profiles.changeAfterSwitch=The change takes effect after switching to this profi
profiles.alreadyActive=This profile is already active.
profiles.selectFirst=Select a profile in the table first.
beacon.invalidMessage=The beacon message is invalid: {0}
beacon.intervalTooShort=Enter a whole beacon interval of at least {0} minute.
beacon.intervalTooShort=Enter a whole beacon interval of at least {0} minutes.
notification.invalidPort="{0}" is not a valid TCP port. Enter a value from 1 to 65535.
notification.invalidSpotter="{0}" is not a valid spotter callsign.
notification.invalidCallsign=Please enter a valid callsign.
@@ -180,6 +180,20 @@ class BeaconTabStateTest(@TempDir private val tempDir: Path) {
"restarting the timer per rejected keystroke would disturb the cooldown")
}
/** Issue #171: one minute was allowed and was what the template shipped. */
@Test
fun `an interval below five minutes is refused and five is accepted`() {
val s = Session()
s.prefs.setBcn_beaconIntervalInMinutesMainCat(9)
val state = s.state()
assertNotNull(state.commitBeaconInterval("4"))
assertEquals(9, state.beaconIntervalMinutes)
assertNull(state.commitBeaconInterval("5"))
assertEquals(5, state.beaconIntervalMinutes)
}
@Test
fun `an interval that is not a whole number is refused`() {
val s = Session()
@@ -73,7 +73,7 @@ class RemainingTextsFollowTheLanguageTest {
assertEquals("Wertung", score.title(), "the column title stayed in the old language")
assertEquals(
"Einen ganzzahligen Bakenabstand von mindestens 1 Minute eingeben.",
"Einen ganzzahligen Bakenabstand von mindestens 5 Minuten eingeben.",
beacon.commitBeaconInterval("0"),
"the refusal from the tab state stayed in the old language",
)
@@ -0,0 +1,100 @@
package kst4contest.controller;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
/**
* Notices that a transceiver has left the frequency a beacon was switched on at.
*
* <p>A beacon announces where the station is calling. It keeps doing so after the
* operator has tuned away, and nothing in the chat window shows that the announced
* frequency has become wrong. This guard remembers the frequency each radio reported
* when a beacon was switched on and names the beacons whose radio has since moved by
* more than {@link #LIMIT_HZ}.</p>
*
* <p>The comparison is made against the frequency at switch-on, not against the
* previous report: tuning slowly across the band never jumps, yet it ends just as far
* away.</p>
*
* <p>Pure and free of preferences, so the rule is tested without a controller. The
* caller says which beacons are on and switches off the ones returned.</p>
*/
final class BeaconFrequencyGuard {
/** The two beacons, named after the chat category they send in. */
enum Beacon {
MAIN,
SECOND
}
/** A move of exactly this size still counts as staying on the frequency. */
static final long LIMIT_HZ = 2_000L;
private final Map<RadioSlot, Long> lastReportedHz = new EnumMap<>(RadioSlot.class);
private final Map<Beacon, Map<RadioSlot, Long>> referenceHz = new EnumMap<>(Beacon.class);
/**
* Takes the frequencies last reported as the reference of a beacon that is being
* switched on. A radio that has not reported yet contributes its first report.
*/
synchronized void arm(Beacon beacon) {
referenceHz.put(beacon, new EnumMap<>(lastReportedHz));
}
/**
* Records the frequency a radio reported and compares it with the references.
*
* <p>Without SO2R only one radio is known, so its move concerns both beacons. With
* SO2R radio 1 belongs to the main category and radio 2 to the second one, the same
* assignment MYQRG and SECONDQRG follow.</p>
*
* @param slot the radio the report belongs to
* @param frequencyHz the reported frequency
* @param so2rEnabled whether the two radios are told apart
* @param enabledBeacons the beacons that are switched on right now
* @return the enabled beacons whose radio moved beyond the limit; the caller
* switches these off
*/
synchronized Set<Beacon> report(
RadioSlot slot,
long frequencyHz,
boolean so2rEnabled,
Set<Beacon> enabledBeacons
) {
lastReportedHz.put(slot, frequencyHz);
Set<Beacon> moved = EnumSet.noneOf(Beacon.class);
for (Beacon beacon : Beacon.values()) {
if (!enabledBeacons.contains(beacon)) {
/*
* A reference only means something while its beacon is on. Dropping it
* here keeps a switch that was flipped without arm() from being judged
* against the frequency of an earlier activation.
*/
referenceHz.remove(beacon);
continue;
}
if (!watches(so2rEnabled, slot, beacon)) {
continue;
}
Long reference = referenceHz
.computeIfAbsent(beacon, unused -> new EnumMap<>(RadioSlot.class))
.putIfAbsent(slot, frequencyHz);
if (reference != null && Math.abs(frequencyHz - reference) > LIMIT_HZ) {
moved.add(beacon);
referenceHz.remove(beacon);
}
}
return moved;
}
private static boolean watches(boolean so2rEnabled, RadioSlot slot, Beacon beacon) {
if (!so2rEnabled) {
return true;
}
return (slot == RadioSlot.FIRST) == (beacon == Beacon.MAIN);
}
}
@@ -65,10 +65,15 @@ public class ChatController implements ThreadStatusCallback {
public static final String BEACON_STATUS_SOURCE = "MyBeacon";
public static final int MIN_BEACON_INTERVAL_MINUTES = 1;
/**
* Shortest interval the shared beacon timer accepts. One minute was allowed until
* issue #171: forgotten beacons at that rate flooded the chat.
*/
public static final int MIN_BEACON_INTERVAL_MINUTES = 5;
public static final int MAX_BEACON_TEXT_LENGTH = 120;
private static final long INITIAL_BEACON_DELAY_MILLIS = 10_000L;
private final BeaconStatus beaconStatus = new BeaconStatus();
private final BeaconFrequencyGuard beaconFrequencyGuard = new BeaconFrequencyGuard();
private Consumer<Double> viewRotorCallback;
@@ -3385,12 +3390,18 @@ private SimpleRoster<String>
/** Applies the main-category beacon switch to the running session immediately. */
public void setMainBeaconEnabled(boolean enabled) {
if (enabled) {
beaconFrequencyGuard.arm(BeaconFrequencyGuard.Beacon.MAIN);
}
getChatPreferences().setBcn_beaconsEnabledMainCat(enabled);
publishBeaconStatus("Beacon configuration changed.");
}
/** Applies the second-category beacon switch to the running session immediately. */
public void setSecondBeaconEnabled(boolean enabled) {
if (enabled) {
beaconFrequencyGuard.arm(BeaconFrequencyGuard.Beacon.SECOND);
}
getChatPreferences().setBcn_beaconsEnabledSecondCat(enabled);
publishBeaconStatus("Beacon configuration changed.");
}
@@ -3410,6 +3421,12 @@ private SimpleRoster<String>
&& preferences.isBcn_beaconsEnabledSecondCat());
boolean enable = !anyEnabled;
if (enable) {
beaconFrequencyGuard.arm(BeaconFrequencyGuard.Beacon.MAIN);
if (secondAvailable) {
beaconFrequencyGuard.arm(BeaconFrequencyGuard.Beacon.SECOND);
}
}
preferences.setBcn_beaconsEnabledMainCat(enable);
if (secondAvailable) {
preferences.setBcn_beaconsEnabledSecondCat(enable);
@@ -3417,6 +3434,54 @@ private SimpleRoster<String>
publishBeaconStatus("Beacon status toggled from the main window.");
}
/**
* Takes a frequency a logging program reported for one of the transceivers and
* switches off every beacon whose radio has left the frequency it was switched on
* at by more than {@link BeaconFrequencyGuard#LIMIT_HZ}.
*
* <p>A beacon keeps announcing its frequency after the operator has tuned away.
* Only reports that also update MYQRG or SECONDQRG arrive here, so a disabled
* frequency synchronisation leaves the beacons alone, and so does typing into the
* frequency field.</p>
*
* @param slot the radio the report belongs to
* @param frequencyHz the reported frequency in Hz
*/
public void onTrxFrequencyReported(RadioSlot slot, long frequencyHz) {
ChatPreferences preferences = getChatPreferences();
if (preferences == null || slot == null) {
return;
}
Set<BeaconFrequencyGuard.Beacon> enabled =
EnumSet.noneOf(BeaconFrequencyGuard.Beacon.class);
if (preferences.isBcn_beaconsEnabledMainCat()) {
enabled.add(BeaconFrequencyGuard.Beacon.MAIN);
}
if (preferences.isBcn_beaconsEnabledSecondCat()) {
enabled.add(BeaconFrequencyGuard.Beacon.SECOND);
}
Set<BeaconFrequencyGuard.Beacon> moved = beaconFrequencyGuard.report(
slot,
frequencyHz,
preferences.isTrxSynch_so2rEnabled(),
enabled
);
if (moved.isEmpty()) {
return;
}
if (moved.contains(BeaconFrequencyGuard.Beacon.MAIN)) {
preferences.setBcn_beaconsEnabledMainCat(false);
}
if (moved.contains(BeaconFrequencyGuard.Beacon.SECOND)) {
preferences.setBcn_beaconsEnabledSecondCat(false);
}
publishBeaconStatus(
"Beacon switched off: the transceiver moved by more than 2 kHz.");
}
/** Publishes the loaded state without changing either category switch. */
public void refreshBeaconStatus() {
publishBeaconStatus("Beacon configuration loaded.");
@@ -535,6 +535,7 @@ public class ReadUDPByWintestThread extends Thread {
* interpreted as frequencies.
*/
String formattedPassQrg = null;
long usablePassFrequencyRaw = 0L;
if (parts.size() > 11) {
try {
@@ -548,6 +549,7 @@ public class ReadUDPByWintestThread extends Thread {
helper_formatWinTestFrequency(
passFrequencyRaw
);
usablePassFrequencyRaw = passFrequencyRaw;
}
} catch (IllegalArgumentException
| ArithmeticException ignored) {
@@ -569,7 +571,18 @@ public class ReadUDPByWintestThread extends Thread {
if (prefs.isLogsynch_wintestQrgSyncEnabled()) {
RadioSlot target = slot.get();
client.uiDispatcher().runOnUi(() -> target.qrgIn(prefs).set(qrgToSet));
/*
* The beacon guard is told the frequency that is shown. STATUS
* counts in 100 Hz.
*/
final long shownFrequencyHz = 100L
* (usePassQrg && formattedPassQrg != null
? usablePassFrequencyRaw
: mainFrequencyRaw);
client.uiDispatcher().runOnUi(() -> {
target.qrgIn(prefs).set(qrgToSet);
client.onTrxFrequencyReported(target, shownFrequencyHz);
});
}
System.out.println(
@@ -463,9 +463,13 @@ public class ReadUDPbyUCXMessageThread extends Thread {
final String finalFormattedQRG = formattedQRG;
final RadioSlot target = slot.get();
helper_runOnFxThread(() ->
target.qrgIn(this.client.getChatPreferences()).set(finalFormattedQRG)
);
final Long reportedHz = helper_radioInfoFrequencyHz(qrg);
helper_runOnFxThread(() -> {
target.qrgIn(this.client.getChatPreferences()).set(finalFormattedQRG);
if (reportedHz != null) {
this.client.onTrxFrequencyReported(target, reportedHz);
}
});
// System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
}
@@ -535,6 +539,21 @@ public class ReadUDPbyUCXMessageThread extends Thread {
}
/**
* Reads the {@code Freq} of a RadioInfo, which counts in 10 Hz, as Hz.
*
* @return the frequency, or {@code null} when the text is no usable number; the
* text is then still shown as received, but nothing can be compared with it
*/
private static Long helper_radioInfoFrequencyHz(String radioInfoFreq) {
try {
long tensOfHz = Long.parseLong(radioInfoFreq.trim());
return tensOfHz > 0 ? Math.multiplyExact(tensOfHz, 10L) : null;
} catch (NumberFormatException | ArithmeticException unusable) {
return null;
}
}
/**
* Runs UI-bound changes on the JavaFX application thread.
*
@@ -78,6 +78,9 @@ public class ChatPreferences {
*/
public static final String PREFERENCE_RESOURCE = "/praktiKSTpreferences.xml";
/** What the file stores for a beacon switch, whatever the running session has set. */
private static final String BEACON_SWITCH_AS_STORED = "false";
/**
@@ -366,7 +369,14 @@ public class ChatPreferences {
* Beacon prefs
*/
MutableValue<Boolean> bcn_beaconsEnabledMainCat = new SimpleValue<>(true);
/*
* The two switches are state of the running session and nothing else: they start
* off, the preferences file is never read into them and always stores "false".
* A stored switch started beacons on every program start that nobody had asked
* for in that session, and the former default of "on" started one whenever the
* file was unreadable (issue #171).
*/
MutableValue<Boolean> bcn_beaconsEnabledMainCat = new SimpleValue<>(false);
MutableValue<Boolean> bcn_beaconsEnabledSecondCat = new SimpleValue<>(false);
int bcn_beaconIntervalInMinutesMainCat = 20;
int bcn_beaconIntervalInMinutesSecondCat = 20;
@@ -2197,12 +2207,17 @@ public class ChatPreferences {
bcn_beaconIntervalInMinutesMainCat.setTextContent(this.getBcn_beaconIntervalInMinutesMainCat()+"");
beaconCQ.appendChild(bcn_beaconIntervalInMinutesMainCat);
/*
* The four switch elements stay in the file so that its layout does not
* change, but they always say "false": a release that still reads them
* must not start a beacon from a file this one has written.
*/
Element beaconCQEnabled = doc.createElement("beaconCQEnabled");
beaconCQEnabled.setTextContent(this.isBcn_beaconsEnabledMainCat()+"");
beaconCQEnabled.setTextContent(BEACON_SWITCH_AS_STORED);
beaconCQ.appendChild(beaconCQEnabled);
Element bcn_beaconsEnabledMainCat = doc.createElement("bcn_beaconsEnabledMainCat");
bcn_beaconsEnabledMainCat.setTextContent(this.isBcn_beaconsEnabledMainCat()+"");
bcn_beaconsEnabledMainCat.setTextContent(BEACON_SWITCH_AS_STORED);
beaconCQ.appendChild(bcn_beaconsEnabledMainCat);
@@ -2223,11 +2238,11 @@ public class ChatPreferences {
beaconCQ.appendChild(bcn_beaconIntervalInMinutesSecondCat);
Element beaconCQEnabledSecondCat = doc.createElement("beaconCQEnabledSecondCat");
beaconCQEnabledSecondCat.setTextContent(this.isBcn_beaconsEnabledSecondCat()+"");
beaconCQEnabledSecondCat.setTextContent(BEACON_SWITCH_AS_STORED);
beaconCQ.appendChild(beaconCQEnabledSecondCat);
Element bcn_beaconsEnabledSecondCat = doc.createElement("bcn_beaconsEnabledSecondCat");
bcn_beaconsEnabledSecondCat.setTextContent(this.isBcn_beaconsEnabledSecondCat()+"");
bcn_beaconsEnabledSecondCat.setTextContent(BEACON_SWITCH_AS_STORED);
beaconCQ.appendChild(bcn_beaconsEnabledSecondCat);
/**
@@ -3178,11 +3193,14 @@ public class ChatPreferences {
Element beaconCQEl = getFirstElement(doc, "beaconCQ");
if (beaconCQEl != null) {
setBcn_beaconsEnabledMainCat(getBoolean(beaconCQEl, isBcn_beaconsEnabledMainCat(), "bcn_beaconsEnabledMainCat", "beaconCQEnabled"));
/*
* The enable switches of both categories are deliberately not read. They
* belong to the running session, so a file of an earlier release cannot
* switch a beacon on and reading again cannot switch a running one off.
*/
bcn_beaconIntervalInMinutesMainCat = getInt(beaconCQEl, bcn_beaconIntervalInMinutesMainCat, "bcn_beaconIntervalInMinutesMainCat", "beaconCQIntervalMinutes");
bcn_beaconTextMainCat = getText(beaconCQEl, bcn_beaconTextMainCat, "bcn_beaconTextMainCat", "beaconCQText");
setBcn_beaconsEnabledSecondCat(getBoolean(beaconCQEl, isBcn_beaconsEnabledSecondCat(), "bcn_beaconsEnabledSecondCat", "beaconCQEnabledSecondCat"));
bcn_beaconIntervalInMinutesSecondCat = getInt(beaconCQEl, bcn_beaconIntervalInMinutesSecondCat, "bcn_beaconIntervalInMinutesSecondCat", "beaconCQIntervalMinutesSecondCat");
bcn_beaconTextSecondCat = getText(beaconCQEl, bcn_beaconTextSecondCat, "bcn_beaconTextSecondCat", "beaconCQTextSecondText");
@@ -112,10 +112,10 @@
</textSnippets>
<beaconCQ>
<beaconCQText>_.~'"´"'~.__.~'´ CQ CQ CQ NAC de MYCALL, pse try 144.317 for JO54 `'~.__.~'"`"'~._</beaconCQText>
<beaconCQIntervalMinutes>1</beaconCQIntervalMinutes>
<beaconCQIntervalMinutes>10</beaconCQIntervalMinutes>
<beaconCQEnabled>false</beaconCQEnabled>
<beaconCQTextSecondText>_.~'"´"'~.__.~'´ CQ CQ CQ de MYCALL, pse sked or SECONDQRG `'~.__.~'"`"'~._</beaconCQTextSecondText>
<beaconCQIntervalMinutesSecondCat>3</beaconCQIntervalMinutesSecondCat>
<beaconCQIntervalMinutesSecondCat>10</beaconCQIntervalMinutesSecondCat>
<beaconCQEnabledSecondCat>false</beaconCQEnabledSecondCat>
</beaconCQ>
<beaconUnworkedstations>
@@ -86,6 +86,85 @@ class BeaconControlTest {
assertLastDescription("1/2");
}
@Test
void aTransceiverMoveBeyondTwoKilohertzSwitchesTheBeaconsOffAndSaysWhy() {
preferences.setLoginToSecondChatEnabled(true);
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_300_000L);
controller.setMainBeaconEnabled(true);
controller.setSecondBeaconEnabled(true);
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_301_500L);
assertTrue(preferences.isBcn_beaconsEnabledMainCat(), "1.5 kHz is within the limit");
assertTrue(preferences.isBcn_beaconsEnabledSecondCat());
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_302_500L);
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
assertFalse(preferences.isBcn_beaconsEnabledSecondCat(),
"without SO2R the one reported radio stands for both categories");
assertLastDescription("off");
assertTrue(lastMessage().getRunningInformation().contains("2 kHz"),
lastMessage().getRunningInformation());
}
@Test
void withSo2rARadioSwitchesOnlyTheBeaconOfItsOwnCategoryOff() {
preferences.setLoginToSecondChatEnabled(true);
preferences.setTrxSynch_so2rEnabled(true);
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_300_000L);
controller.onTrxFrequencyReported(RadioSlot.SECOND, 432_200_000L);
controller.toggleUsableBeacons();
controller.onTrxFrequencyReported(RadioSlot.SECOND, 432_250_000L);
assertTrue(preferences.isBcn_beaconsEnabledMainCat());
assertFalse(preferences.isBcn_beaconsEnabledSecondCat());
assertLastDescription("1/2");
}
@Test
void aBeaconSwitchedOnAgainAfterTheMoveStaysOnAtTheNewFrequency() {
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_300_000L);
controller.setMainBeaconEnabled(true);
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_350_000L);
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
controller.toggleUsableBeacons();
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_350_500L);
assertTrue(preferences.isBcn_beaconsEnabledMainCat());
}
@Test
void aTransceiverMoveWhileNoBeaconIsOnPublishesNothing() {
controller.onTrxFrequencyReported(RadioSlot.FIRST, 144_300_000L);
controller.onTrxFrequencyReported(RadioSlot.FIRST, 432_200_000L);
org.mockito.Mockito.verifyNoInteractions(listener);
}
/** Stored values of earlier releases may lie below the minimum of five minutes. */
@Test
void aStoredIntervalBelowTheMinimumIsRaisedWhenTheTimerStarts() {
preferences.setBcn_beaconIntervalInMinutesMainCat(1);
preferences.setBcn_beaconIntervalInMinutesSecondCat(3);
controller.onOn4KstConnectionOnline();
controller.onOn4KstConnectionLost();
assertEquals(5, ChatController.MIN_BEACON_INTERVAL_MINUTES);
assertEquals(5, preferences.getBcn_beaconIntervalInMinutesMainCat());
assertEquals(5, preferences.getBcn_beaconIntervalInMinutesSecondCat());
}
private ThreadStateMessage lastMessage() {
ArgumentCaptor<ThreadStateMessage> message = ArgumentCaptor.forClass(ThreadStateMessage.class);
org.mockito.Mockito.verify(listener, org.mockito.Mockito.atLeastOnce())
.onThreadStatusChanged(org.mockito.ArgumentMatchers.anyString(), message.capture());
return message.getValue();
}
private void assertLastDescription(String expected) {
ArgumentCaptor<String> key = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<ThreadStateMessage> message = ArgumentCaptor.forClass(ThreadStateMessage.class);
@@ -0,0 +1,107 @@
package kst4contest.controller;
import static kst4contest.controller.BeaconFrequencyGuard.Beacon.MAIN;
import static kst4contest.controller.BeaconFrequencyGuard.Beacon.SECOND;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.EnumSet;
import java.util.Set;
import org.junit.jupiter.api.Test;
import kst4contest.controller.BeaconFrequencyGuard.Beacon;
class BeaconFrequencyGuardTest {
private static final long QRG = 144_300_000L;
private static final Set<Beacon> NONE = EnumSet.noneOf(Beacon.class);
private static final Set<Beacon> BOTH = EnumSet.allOf(Beacon.class);
private final BeaconFrequencyGuard guard = new BeaconFrequencyGuard();
@Test
void aMoveOfExactlyTheLimitKeepsTheBeaconAndOneStepMoreSwitchesItOff() {
guard.report(RadioSlot.FIRST, QRG, false, NONE);
guard.arm(MAIN);
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG + 2_000, false, EnumSet.of(MAIN)));
assertEquals(EnumSet.of(MAIN), guard.report(RadioSlot.FIRST, QRG + 2_010, false, EnumSet.of(MAIN)));
}
@Test
void aMoveDownwardsCountsLikeAMoveUpwards() {
guard.report(RadioSlot.FIRST, QRG, false, NONE);
guard.arm(MAIN);
assertEquals(EnumSet.of(MAIN), guard.report(RadioSlot.FIRST, QRG - 2_010, false, EnumSet.of(MAIN)));
}
/** Tuning slowly across the band never jumps, so the reference must not follow the radio. */
@Test
void smallStepsAddUpAgainstTheFrequencyTheBeaconWasSwitchedOnAt() {
guard.report(RadioSlot.FIRST, QRG, false, NONE);
guard.arm(MAIN);
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG + 900, false, EnumSet.of(MAIN)));
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG + 1_800, false, EnumSet.of(MAIN)));
assertEquals(EnumSet.of(MAIN), guard.report(RadioSlot.FIRST, QRG + 2_700, false, EnumSet.of(MAIN)));
}
@Test
void switchingABeaconOnAgainTakesTheCurrentFrequencyAsTheNewReference() {
guard.report(RadioSlot.FIRST, QRG, false, NONE);
guard.arm(MAIN);
guard.report(RadioSlot.FIRST, QRG + 50_000, false, EnumSet.of(MAIN));
guard.arm(MAIN);
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG + 50_500, false, EnumSet.of(MAIN)));
}
@Test
void aBeaconSwitchedOnBeforeAnyReportTakesTheFirstReportAsItsReference() {
guard.arm(MAIN);
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG, false, EnumSet.of(MAIN)));
assertEquals(EnumSet.of(MAIN), guard.report(RadioSlot.FIRST, QRG + 5_000, false, EnumSet.of(MAIN)));
}
/** A switch that was flipped without arm() must not be judged against a stale reference. */
@Test
void aReferenceIsForgottenWhileItsBeaconIsOff() {
guard.report(RadioSlot.FIRST, QRG, false, NONE);
guard.arm(MAIN);
guard.report(RadioSlot.FIRST, QRG + 50_000, false, NONE);
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG + 50_100, false, EnumSet.of(MAIN)));
}
@Test
void withoutSo2rTheOneRadioSwitchesBothBeaconsOff() {
guard.report(RadioSlot.FIRST, QRG, false, NONE);
guard.arm(MAIN);
guard.arm(SECOND);
assertEquals(BOTH, guard.report(RadioSlot.FIRST, QRG + 10_000, false, BOTH));
}
@Test
void withSo2rEachRadioSwitchesOnlyTheBeaconOfItsOwnCategory() {
guard.report(RadioSlot.FIRST, QRG, true, NONE);
guard.report(RadioSlot.SECOND, 432_200_000L, true, NONE);
guard.arm(MAIN);
guard.arm(SECOND);
assertEquals(EnumSet.of(SECOND), guard.report(RadioSlot.SECOND, 432_250_000L, true, BOTH));
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG + 500, true, EnumSet.of(MAIN)));
assertEquals(EnumSet.of(MAIN), guard.report(RadioSlot.FIRST, QRG + 10_000, true, EnumSet.of(MAIN)));
}
@Test
void aBeaconThatIsOffIsNeverReported() {
guard.report(RadioSlot.FIRST, QRG, false, NONE);
guard.arm(MAIN);
assertEquals(NONE, guard.report(RadioSlot.FIRST, QRG + 10_000, false, NONE));
}
}
@@ -310,6 +310,18 @@ class ReadUDPbyUCXMessageThreadLoopbackTest {
assertEquals(List.of("1296.200.00"), firstWrites, "only the marker may write MYQRG");
}
/** RadioInfo counts in 10 Hz; the beacon guard of issue #171 is told in Hz. */
@Test
void aRadioInfoReportsTheTransceiverFrequencyOfItsRadioToTheBeaconGuard(@TempDir Path tempDir) throws Exception {
givenRadioInfoMarkerEnabled();
givenSo2r(true);
sendThenAwaitMarker("<RadioInfo><RadioNr>2</RadioNr><Freq>43220000</Freq><Mode>SSB</Mode></RadioInfo>");
org.mockito.Mockito.verify(controller).onTrxFrequencyReported(RadioSlot.SECOND, 432_200_000L);
org.mockito.Mockito.verify(controller).onTrxFrequencyReported(RadioSlot.FIRST, 1_296_200_000L);
}
@Test
void withSo2rARadioInfoWithoutRadioNumberIsRadioOne(@TempDir Path tempDir) throws Exception {
givenRadioInfoMarkerEnabled();
@@ -10,7 +10,11 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/** A Win-Test STATUS of the second station writes SECONDQRG when SO2R is on. */
@@ -50,6 +54,24 @@ class WinTestStatusSlotTest {
assertTrue(first.isEmpty());
}
/** STATUS counts in 100 Hz; the beacon guard of issue #171 is told in Hz. */
@Test
void aStatusReportsTheTransceiverFrequencyOfItsRadioToTheBeaconGuard() {
thread(true).parseStatus(STATUS_STN2);
verify(controller).onTrxFrequencyReported(RadioSlot.SECOND, 432_200_000L);
}
@Test
void withQrgSyncSwitchedOffAStatusReportsNothingToTheBeaconGuard() {
ReadUDPByWintestThread thread = thread(true);
when(prefs.isLogsynch_wintestQrgSyncEnabled()).thenReturn(false);
thread.parseStatus(STATUS_STN2);
verify(controller, never()).onTrxFrequencyReported(any(), anyLong());
}
@Test
void withoutSo2rTheSecondStationIsFilteredOutAsBefore() {
thread(false).parseStatus(STATUS_STN2);
@@ -4,6 +4,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
@@ -24,31 +27,93 @@ class ChatPreferencesBeaconStateTest {
preferences.bcnBeaconsEnabledMainCatProperty().addListener(mainChanges::add);
preferences.bcnBeaconsEnabledSecondCatProperty().addListener(secondChanges::add);
preferences.setBcn_beaconsEnabledMainCat(false);
preferences.setBcn_beaconsEnabledMainCat(true);
preferences.setBcn_beaconsEnabledSecondCat(true);
preferences.setBcn_beaconsEnabledMainCat(false);
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
assertTrue(preferences.isBcn_beaconsEnabledSecondCat());
assertEquals(List.of(false), mainChanges);
assertEquals(List.of(true, false), mainChanges);
assertEquals(List.of(true), secondChanges);
}
@Test
void explicitWritePersistsBeaconActivationButAnUnsavedChangeDoesNot() {
ChatPreferences preferences = preferences("persistence.xml");
preferences.setBcn_beaconsEnabledMainCat(false);
void aNewPreferencesObjectStartsWithBothBeaconsOff() {
ChatPreferences preferences = preferences("fresh.xml");
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
assertFalse(preferences.isBcn_beaconsEnabledSecondCat());
}
/** Issue #171: a beacon has to be switched on by the operator in every session. */
@Test
void beaconActivationIsNeitherWrittenToTheFileNorRestoredFromIt() throws IOException {
ChatPreferences preferences = preferences("session-only.xml");
preferences.setBcn_beaconsEnabledMainCat(true);
preferences.setBcn_beaconsEnabledSecondCat(true);
assertTrue(preferences.writePreferencesToXmlFile());
preferences.setBcn_beaconsEnabledMainCat(true);
preferences.setBcn_beaconsEnabledSecondCat(false);
assertTrue(preferences.writeLayoutPreferencesToXmlFile());
String written = Files.readString(tempDir.resolve("session-only.xml"));
assertTrue(written.contains("<bcn_beaconsEnabledMainCat>false</bcn_beaconsEnabledMainCat>"), written);
assertTrue(written.contains("<beaconCQEnabled>false</beaconCQEnabled>"), written);
assertTrue(written.contains("<bcn_beaconsEnabledSecondCat>false</bcn_beaconsEnabledSecondCat>"), written);
assertTrue(written.contains("<beaconCQEnabledSecondCat>false</beaconCQEnabledSecondCat>"), written);
ChatPreferences restored = preferences("persistence.xml");
ChatPreferences restored = preferences("session-only.xml");
restored.readPreferencesFromXmlFile();
assertFalse(restored.isBcn_beaconsEnabledMainCat());
assertTrue(restored.isBcn_beaconsEnabledSecondCat());
assertFalse(restored.isBcn_beaconsEnabledSecondCat());
}
/** Files of earlier releases carry the stored switch; it must not start a beacon any more. */
@Test
void aFileOfAnEarlierReleaseWithEnabledBeaconsLoadsThemOffAndKeepsTextAndInterval() throws IOException {
Files.writeString(tempDir.resolve("legacy.xml"),
"<praktiKST><beaconCQ>"
+ "<beaconCQText>CQ LEGACY</beaconCQText>"
+ "<beaconCQIntervalMinutes>15</beaconCQIntervalMinutes>"
+ "<beaconCQEnabled>true</beaconCQEnabled>"
+ "<bcn_beaconsEnabledMainCat>true</bcn_beaconsEnabledMainCat>"
+ "<beaconCQEnabledSecondCat>true</beaconCQEnabledSecondCat>"
+ "<bcn_beaconsEnabledSecondCat>true</bcn_beaconsEnabledSecondCat>"
+ "</beaconCQ></praktiKST>");
ChatPreferences restored = preferences("legacy.xml");
restored.readPreferencesFromXmlFile();
assertFalse(restored.isBcn_beaconsEnabledMainCat());
assertFalse(restored.isBcn_beaconsEnabledSecondCat());
assertEquals("CQ LEGACY", restored.getBcn_beaconTextMainCat());
assertEquals(15, restored.getBcn_beaconIntervalInMinutesMainCat());
}
/** The switch is session state: reading the file again must not end a running beacon. */
@Test
void readingTheFileAgainLeavesABeaconOfTheRunningSessionOn() {
ChatPreferences preferences = preferences("reread.xml");
assertTrue(preferences.writePreferencesToXmlFile());
preferences.setBcn_beaconsEnabledMainCat(true);
preferences.readPreferencesFromXmlFile();
assertTrue(preferences.isBcn_beaconsEnabledMainCat());
}
/** The template seeds every first installation and every new operator profile. */
@Test
void theBundledTemplateShipsBothBeaconsOffAndAnIntervalAboveTheMinimum() throws IOException {
try (InputStream template = ChatPreferences.class.getResourceAsStream(ChatPreferences.PREFERENCE_RESOURCE)) {
Files.copy(template, tempDir.resolve("template.xml"));
}
ChatPreferences seeded = preferences("template.xml");
seeded.readPreferencesFromXmlFile();
assertFalse(seeded.isBcn_beaconsEnabledMainCat());
assertFalse(seeded.isBcn_beaconsEnabledSecondCat());
assertEquals(10, seeded.getBcn_beaconIntervalInMinutesMainCat());
assertEquals(10, seeded.getBcn_beaconIntervalInMinutesSecondCat());
}
private ChatPreferences preferences(String fileName) {
@@ -64,6 +64,13 @@ class ChatPreferencesRoundTripTest {
Map.entry("setMessageHandling_autoAnswerEnabledSecondCat",
"Same deliberate mirroring as the second-category auto-answer text; the "
+ "main-category flag wins after every read"),
Map.entry("setBcn_beaconsEnabledMainCat",
"Session state since issue #171: the file always stores 'false' and "
+ "readPreferencesFromXmlFile never reads the switch, so a beacon has "
+ "to be switched on in every session (see the comment above "
+ "'bcn_beaconsEnabledMainCat')"),
Map.entry("setBcn_beaconsEnabledSecondCat",
"Session state like the main-category beacon switch"),
Map.entry("setBcn_contestScoreSum",
"DEAD FIELD: declared with accessors only, no caller in core or app-desktop, "
+ "never written nor read")
+3 -1
View File
@@ -217,7 +217,9 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen
- Automated-message loops must be prevented.
- Cooldown/minimum-interval rules must be preserved.
- A reply rejected before a complete valid TX item is queued must not consume cooldown.
- Both category beacons share one timer, while their enable flags and templates remain profile-scoped and independent. The enable flags are observable runtime preferences: Settings changes and the `MyBeacon` status-chip action update them immediately, and every timer run reads their current values without restarting the timer or causing an immediate transmission. Only the explicit full settings write persists those flags; layout autosave deliberately leaves the on-disk functional state untouched.
- Both category beacons share one timer. Their templates remain profile-scoped and their enable flags independent of each other. The enable flags are observable runtime preferences: Settings changes and the `MyBeacon` status-chip action update them immediately, and every timer run reads their current values without restarting the timer or causing an immediate transmission. The enable flags are session state and nothing else (issue #171): they start `false`, `readPreferencesFromXmlFile` never reads them, and every write stores `false` in the four unchanged XML elements so that an older release cannot start a beacon from a newer file. A beacon therefore has to be switched on in every program run and after every profile switch; an automatic reconnect within one run keeps the flags. Do not reintroduce a persisted or default-on enable state: the former in-code default `true` started a beacon whenever the file was unreadable or had no `<beaconCQ>` block.
- The shared beacon interval has a minimum of five minutes (`ChatController.MIN_BEACON_INTERVAL_MINUTES`); the bundled template ships ten. Smaller stored values are raised when the timer is scheduled.
- `BeaconFrequencyGuard` switches a beacon off when a logger-reported transceiver frequency lies more than 2 kHz from the frequency reported when that beacon was switched on. The reference is the frequency at switch-on, not the previous report, so slow tuning triggers it as well. Only the reports that also set `MYQRG`/`SECONDQRG` reach `ChatController.onTrxFrequencyReported` (UCXLog `RadioInfo` with TRX sync, Win-Test `STATUS` with QRG sync), on the UI dispatcher next to the QRG write; manual QRG entry never triggers it. Without SO2R the single reported radio stands for both categories; with SO2R radio 1 belongs to the main-category beacon and radio 2 to the second-category beacon. The 2 kHz limit is a constant, not a setting.
- `MyBeacon` owns one aggregate thread-status entry for both categories (`off`, a partial `enabled/available` count, `on`, or `FAILED`). Its action disables all currently usable beacons if any is enabled, otherwise it enables all currently usable beacons. A disabled second chat is not usable and its dormant enable flag must not be changed by the aggregate action. Category-specific timer failures remain authoritative while the affected beacon stays active; independent switch changes cannot hide them. The status bar resolves the aggregate from current controller state when a queued UI update is applied, so a delayed timer snapshot cannot overwrite a newer manual switch. Open Settings views observe the same preference values, so changes from the status chip remain synchronized without a parallel UI state.
- Current implementation/tests define the exact message markers and timer details.
@@ -2,6 +2,12 @@
Freigegeben von Marc am 2026-11-04.
> **Teilweise abgelöst durch Issue #171 (2026-11-11):** Die Beacon-Zustände werden nicht mehr
> gespeichert und nicht mehr beim Profilstart geladen; jede Sitzung beginnt mit „aus“. Das
> Mindestintervall beträgt fünf Minuten, und eine TRX-Frequenzänderung über 2 kHz schaltet die
> Beacon ab. Der aktuelle Stand steht in `docs/PROJECT_CONTEXT.md` unter „Autoanswer / Beacon“.
> Alles Übrige in diesem Dokument gilt weiter.
## Ziel
Die beiden Beacon-Aktivierungsschalter wirken in der laufenden Sitzung sofort. Der gemeinsame
+3
View File
@@ -52,6 +52,8 @@ Mehrere Operateure an einem Rechner können jetzt eigene Rufzeichen, Locators un
- **Einstellungen wirken erst mit Übernehmen:** Änderungen im Einstellungsfenster wirken nicht mehr während der Eingabe, sondern erst mit **Übernehmen/Schließen** oder **Einstellungen speichern**; **Verbinden** übernimmt sie ebenfalls vorher. Das gilt auch während einer laufenden Sitzung. Wer mit offenen Änderungen das Fenster schließt, das Profil wechselt oder das Programm beendet, wird zuerst gefragt, ob sie übernommen oder verworfen werden sollen, oder kann zurück. Farben, Sprache und die Anmeldedaten im Reiter **Station** wirken wie bisher sofort.
- **Beacon nur noch bewusst und seltener:** Vergessene Beacons haben den Chat gefüllt, während niemand am Gerät saß ([Issue #171](https://github.com/HamRadioOnline/kst4contest/issues/171)). Drei Dinge ändern sich deshalb. Die Aktivierung gilt nur noch für die laufende Programmsitzung: Nach jedem Programmstart und jedem Profilwechsel sind beide Beacons aus, und **Einstellungen speichern** speichert die Schalter nicht mehr. Das Mindestintervall steigt von einer auf fünf Minuten, eine neue Installation beginnt mit zehn. Und KST4Contest schaltet den Beacon selbst ab, sobald das Logprogramm eine Frequenz meldet, die mehr als 2 kHz von der Frequenz beim Einschalten abweicht; ohne SO2R betrifft das beide Kategorien, mit SO2R nur die Kategorie des jeweiligen Funkgeräts. Der Status-Chip **MyBeacon** zeigt die Abschaltung, sein Tooltip nennt den Grund.
- **Fenster öffnen auf dem Bildschirm des Hauptfensters:** Stationskarte, Monitor, Einstellungen, Update-Fenster, Top-Kandidaten und die Hinweisdialoge öffneten bisher auf dem primären Bildschirm, auch wenn das Hauptfenster auf einem anderen lag oder der primäre Bildschirm ausgeschaltet war. Sie öffnen jetzt zentriert auf dem Bildschirm, auf dem das Hauptfenster gerade steht. Ein Fenster, das in der laufenden Sitzung verschoben wurde, öffnet beim nächsten Mal wieder dort, sofern seine Titelleiste noch auf einem angeschlossenen Bildschirm erreichbar ist. Mit der neuen Option **Fenster nach einem Neustart dort öffnen, wohin sie zuletzt geschoben wurden** im Reiter **GUI** (standardmäßig aus) behalten das Hauptfenster und alle anderen Fenster ihre Position auch über einen Neustart; eine gespeicherte Position wird nur verwendet, wenn genau derselbe Bildschirm (gleiche Lage und Auflösung) wieder angeschlossen ist. Ist ein Fenster größer als der Bildschirm, wird es auf dessen nutzbare Fläche verkleinert, auch wenn es später auf einen kleineren Bildschirm geschoben wird, etwa einen 640×480-Monitor; die gespeicherte Fenstergröße bleibt dabei unverändert. Das behebt [Issue #59](https://github.com/HamRadioOnline/kst4contest/issues/59).
### Behoben
@@ -86,6 +88,7 @@ Mehrere Operateure an einem Rechner können jetzt eigene Rufzeichen, Locators un
- Eine Profil-Registry entsteht erst beim Anlegen des zweiten Profils. Wer nur ein Profil benutzt, merkt von der Änderung nichts.
- Eine Rückkehr zu einer älteren KST4Contest-Version bleibt möglich; sie findet ihre Dateien unverändert vor.
- Passwörter stehen weiterhin im Klartext in der `preferences.xml` des jeweiligen Profils. Profile trennen die Konfiguration, sie sind kein Zugriffsschutz.
- Ein in einer früheren Version gespeicherter, eingeschalteter Beacon startet nicht mehr von selbst; er muss nach dem Programmstart neu eingeschaltet werden. Ein gespeichertes Beacon-Intervall unter fünf Minuten wird auf fünf Minuten angehoben. Texte und größere Intervalle bleiben unverändert.
---
+1 -1
View File
@@ -95,7 +95,7 @@ Bei einer weitgehend festen CQ-QRG sollten `MYQRG` beziehungsweise `SECONDQRG` d
Der Beacon kann die aktuelle QRG, den Locator und die Antennenrichtung regelmäßig im Chat veröffentlichen. Seine Variablen werden bei jedem Sendedurchlauf erneut ausgewertet.
Wird während des CQ-Betriebs über mehrere Frequenzen gescannt, sollte der Beacon deaktiviert werden. Eine automatisch veröffentlichte QRG ist nur hilfreich, solange sie noch stimmt.
Wird während des CQ-Betriebs über mehrere Frequenzen gescannt, sollte der Beacon deaktiviert werden. Eine automatisch veröffentlichte QRG ist nur hilfreich, solange sie noch stimmt. Liefert das Logprogramm die Frequenz, schaltet KST4Contest den Beacon bei einer Abweichung von mehr als 2 kHz [selbst ab](de-Konfiguration#automatische-abschaltung-beim-frequenzwechsel). Nach jedem Programmstart ist der Beacon aus und muss neu eingeschaltet werden.
Shortcuts und Snippets sollten die regelmäßig benötigten Nachrichten abdecken, beispielsweise:
+17 -4
View File
@@ -603,13 +603,15 @@ KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivi
Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in ihren jeweiligen Kategorien gesendet. Der zweite Beacon wird nur berücksichtigt, wenn die zweite Kategorie in derselben ON4KST-Sitzung aktiviert und vollständig synchronisiert ist.
Die beiden Aktivierungsschalter wirken sofort auf den laufenden Timer. Sie starten weder den Countdown neu noch senden sie unmittelbar eine Nachricht; der nächste reguläre Timer-Lauf prüft den aktuellen Zustand. Erst **Save Settings** speichert die Aktivierung für den nächsten Programmstart. Nachrichtentexte und Intervall folgen weiterhin der normalen Apply-/Save-Logik.
Die beiden Aktivierungsschalter wirken sofort auf den laufenden Timer. Sie starten weder den Countdown neu noch senden sie unmittelbar eine Nachricht; der nächste reguläre Timer-Lauf prüft den aktuellen Zustand. Nachrichtentexte und Intervall folgen weiterhin der normalen Apply-/Save-Logik.
Der Status-Chip **MyBeacon** im Hauptfenster fasst beide nutzbaren Kategorien zusammen: **off** bedeutet kein aktiver Beacon, **1/2** genau eine von zwei aktiven Kategorien und **on** alle derzeit nutzbaren Kategorien. Ein Klick auf den Chip schaltet alle nutzbaren Beacons gemeinsam ein oder aus. Ist der zweite Chat deaktiviert, ändert der Chip nur den Beacon der Hauptkategorie und lässt die gespeicherte Einstellung des zweiten Beacons unverändert.
Die Aktivierung gilt nur für die laufende Programmsitzung. Nach jedem Programmstart und nach jedem Profilwechsel sind beide Beacons ausgeschaltet und müssen bewusst wieder eingeschaltet werden. **Save Settings** speichert Nachrichtentexte und Intervall, aber nicht die Aktivierung. Ein Beacon kann dadurch nicht mehr unbemerkt aus einer früheren Sitzung weiterlaufen.
Der Status-Chip **MyBeacon** im Hauptfenster fasst beide nutzbaren Kategorien zusammen: **off** bedeutet kein aktiver Beacon, **1/2** genau eine von zwei aktiven Kategorien und **on** alle derzeit nutzbaren Kategorien. Ein Klick auf den Chip schaltet alle nutzbaren Beacons gemeinsam ein oder aus. Ist der zweite Chat deaktiviert, ändert der Chip nur den Beacon der Hauptkategorie und lässt den Schalter des zweiten Beacons unverändert.
### Intervall und Timer-Verhalten
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert ist eine Minute.
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert sind fünf Minuten; eine neue Installation beginnt mit zehn Minuten. Ein kleinerer Wert aus einer früheren Version wird beim Start des Timers auf fünf Minuten angehoben.
Nach dem Aufbau der Chat-Verbindung prüft KST4Contest die Beacons erstmals nach ungefähr zehn Sekunden. Anschließend gilt das eingestellte Intervall.
@@ -668,7 +670,18 @@ Der Beacon ist nur dann hilfreich, wenn seine QRG-Angabe zum tatsächlichen Betr
Im Klartext: Solange auf einer festen QRG CQ gerufen wird, spart der Beacon Arbeit. Beim „Schleichen“ über das Band sollte er ausgeschaltet werden.
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung, Texte und Intervall auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
### Automatische Abschaltung beim Frequenzwechsel
Meldet das Logprogramm die Frequenz des Transceivers, schaltet KST4Contest den Beacon selbst ab, sobald sich das Funkgerät um mehr als 2 kHz von der Frequenz entfernt hat, auf der der Beacon eingeschaltet wurde. Verglichen wird mit der Frequenz beim Einschalten, nicht mit der vorherigen Meldung. Langsames Drehen über das Band löst die Abschaltung deshalb ebenso aus wie ein Sprung.
- Ausgewertet werden die Meldungen, die auch `MYQRG` und `SECONDQRG` füllen: UCXLog-`RadioInfo` bei eingeschaltetem TRX-Abgleich und Win-Test-`STATUS` bei eingeschalteter QRG-Übernahme. Ohne diese Kopplung bleibt der Beacon an, bis er von Hand ausgeschaltet wird.
- Eine von Hand in das QRG-Feld eingetragene Frequenz schaltet nichts ab.
- Ohne **SO2R** steht das eine gemeldete Funkgerät für beide Kategorien: Beide Beacons werden abgeschaltet.
- Mit **SO2R** schaltet Funkgerät 1 nur den Beacon der Hauptkategorie ab und Funkgerät 2 nur den der zweiten Kategorie.
Nach der Abschaltung zeigt der Status-Chip **MyBeacon** den neuen Zustand, sein Tooltip nennt den Grund. Wird der Beacon wieder eingeschaltet, gilt die dann gemeldete Frequenz als neuer Bezug.
Änderungen wirken während der laufenden Verbindung. Damit Texte und Intervall auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden. Die Aktivierung wird nicht gespeichert.
---
+3
View File
@@ -52,6 +52,8 @@ Several operators sharing one computer can now use their own callsigns, locators
- **Settings take effect with Apply:** changes in the settings window no longer take effect while they are typed, but with **Apply/Close prefs** or **Save settings**; **Connect** applies them first as well. This also works during a running session. Closing the window, switching the operator profile or exiting with changes pending first asks whether to apply or discard them, or to go back. Colours, language and the login data on the **Station** tab still take effect immediately.
- **Beacons only deliberately and less often:** forgotten beacons filled the chat while nobody was at the station ([Issue #171](https://github.com/HamRadioOnline/kst4contest/issues/171)). Three things change because of that. The enable state now applies to the running program session only: after every program start and every profile switch both beacons are off, and **Save settings** no longer stores the switches. The minimum interval rises from one to five minutes, and a new installation starts with ten. And KST4Contest switches the beacon off by itself as soon as the logger reports a frequency more than 2 kHz away from the one at the time of enabling; without SO2R this concerns both categories, with SO2R only the category of the respective radio. The **MyBeacon** status chip shows the switch-off and its tooltip names the reason.
- **Windows open on the main window's screen:** the station map, monitor, settings, update window, top candidates and the notice dialogs used to open on the primary screen, even when the main window was on another one or the primary screen was switched off. They now open centred on the screen the main window is currently on. A window that was moved during the running session reopens there next time, as long as its title bar can still be reached on a connected screen. With the new option **Reopen windows across restarts where they were last moved to** on the **GUI** tab (off by default), the main window and all other windows also keep their position across restarts; a stored position is only used when the very same screen (same place and resolution) is connected again. A window larger than the screen is reduced to its usable area, also when it is moved onto a smaller screen later, for example a 640x480 monitor; the stored window size stays unchanged. This fixes [Issue #59](https://github.com/HamRadioOnline/kst4contest/issues/59).
### Fixed
@@ -86,6 +88,7 @@ Several operators sharing one computer can now use their own callsigns, locators
- A profile registry only appears when the second profile is created. Anyone using a single profile will not notice the change.
- Going back to an older KST4Contest release stays possible; it finds its files unchanged.
- Passwords are still stored in clear text in each profile's `preferences.xml`. Profiles separate configuration; they are not an access control mechanism.
- A beacon that was stored as enabled by an earlier version no longer starts by itself; it has to be enabled again after the program start. A stored beacon interval below five minutes is raised to five minutes. Message texts and longer intervals stay unchanged.
---
+17 -4
View File
@@ -603,13 +603,15 @@ KST4Contest uses one shared timer for both chat categories. Each category nevert
When both beacons are enabled, they are sent one after the other in their respective categories during the same timer run. The second beacon is only considered while the second category is enabled and fully synchronised in the same ON4KST session.
Both enable settings affect the running timer immediately. They neither restart the countdown nor send a message straight away; the next regular timer run checks the current state. Only **Save Settings** stores the enable state for the next program start. Message templates and the interval continue to follow the normal Apply/Save workflow.
Both enable settings affect the running timer immediately. They neither restart the countdown nor send a message straight away; the next regular timer run checks the current state. Message templates and the interval continue to follow the normal Apply/Save workflow.
The **MyBeacon** status chip in the main window combines the currently usable categories: **off** means that no beacon is enabled, **1/2** that one of two categories is enabled, and **on** that every usable category is enabled. Clicking the chip enables or disables all usable beacons together. If the second chat is disabled, the chip changes only the primary-category beacon and leaves the stored secondary-beacon setting untouched.
The enable state applies to the running program session only. After every program start and after every profile switch both beacons are off and have to be enabled again deliberately. **Save Settings** stores the message templates and the interval, but not the enable state. A beacon can therefore no longer keep running unnoticed from an earlier session.
The **MyBeacon** status chip in the main window combines the currently usable categories: **off** means that no beacon is enabled, **1/2** that one of two categories is enabled, and **on** that every usable category is enabled. Clicking the chip enables or disables all usable beacons together. If the second chat is disabled, the chip changes only the primary-category beacon and leaves the secondary-beacon switch untouched.
### Interval and timer behaviour
The interval is entered in whole minutes. The minimum permitted value is one minute.
The interval is entered in whole minutes. The minimum permitted value is five minutes; a new installation starts with ten minutes. A smaller value from an earlier version is raised to five minutes when the timer starts.
After the chat connection has been established, KST4Contest performs the first beacon check after approximately ten seconds. The configured interval applies after that initial check.
@@ -668,7 +670,18 @@ The beacon is useful only while its QRG matches the actual operation. Leaving it
In plain terms: the beacon saves work while calling CQ on a fixed QRG. It should be disabled while moving around the band.
Changes take effect during the current connection. Use **Save Settings** afterwards to retain the enable settings, message templates and interval for the next program start.
### Automatic switch-off after a frequency change
When the logger reports the transceiver frequency, KST4Contest switches the beacon off by itself as soon as the radio has moved more than 2 kHz away from the frequency on which the beacon was enabled. The comparison is made with the frequency at the time of enabling, not with the previous report. Tuning slowly across the band therefore triggers the switch-off just as a jump does.
- The reports evaluated are those which also fill `MYQRG` and `SECONDQRG`: UCXLog `RadioInfo` while TRX synchronisation is enabled, and Win-Test `STATUS` while QRG synchronisation is enabled. Without this coupling the beacon stays on until it is disabled by hand.
- A frequency typed into the QRG field by hand switches nothing off.
- Without **SO2R** the one reported radio stands for both categories: both beacons are switched off.
- With **SO2R**, radio 1 switches off only the primary-category beacon and radio 2 only the secondary-category beacon.
After the switch-off the **MyBeacon** status chip shows the new state and its tooltip names the reason. When the beacon is enabled again, the frequency reported at that time becomes the new reference.
Changes take effect during the current connection. Use **Save Settings** afterwards to retain the message templates and interval for the next program start. The enable state is not stored.
---
+1 -1
View File
@@ -95,7 +95,7 @@ During operation on a mainly fixed CQ frequency, `MYQRG` and `SECONDQRG` should
The beacon can publish the current QRG, locator and antenna direction in the chat at regular intervals. Its variables are evaluated again for every transmission.
Disable the beacon while scanning across several frequencies. An automatically published QRG is useful only while it is still correct.
Disable the beacon while scanning across several frequencies. An automatically published QRG is useful only while it is still correct. When the logger supplies the frequency, KST4Contest switches the beacon off [by itself](en-Configuration#automatic-switch-off-after-a-frequency-change) once the radio has moved by more than 2 kHz. After every program start the beacon is off and has to be enabled again.
Shortcuts and snippets should cover the messages required regularly, for example:
+1 -1
View File
@@ -174,7 +174,7 @@ Each chat category has its own enable setting and message template. Both categor
Global variables are evaluated again on every timer run. A QRG changed by the logger can therefore be included in the next beacon without editing its template.
The completely resolved beacon text must not exceed 120 characters. The configured minimum interval is one minute.
The completely resolved beacon text must not exceed 120 characters. The configured minimum interval is five minutes.
A suitable template is: