mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-11-12 07:42:27 +01:00
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
This commit is contained in:
25 files changed
+600
-39
No files matched your search
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user