mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-10 19:25:32 +02:00
Fix compact QRG parsing and initial worked-state restore - parse digit-only full frequencies across supported bands - restore persisted Worked state before publishing initial user lists - add regression tests and update documentation Fixes #85
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# KST4Contest Project Context
|
||||
|
||||
Last reviewed: 2026-08-28
|
||||
Last reviewed: 2026-09-02
|
||||
|
||||
This file is the durable technical project context for KST4Contest. It is not a user manual and not a replacement for the changelog. Current code, tests and authoritative external specifications remain the source of truth when this document is stale or ambiguous.
|
||||
|
||||
@@ -34,6 +34,7 @@ KST4Contest is a Java/JavaFX desktop client for ON4KST chat focused on VHF/UHF/m
|
||||
- `NOT-QRV` overrides positive inferred band-availability hints.
|
||||
- Unknown/missing frequency, QRB, QTF or similar external data must remain unavailable rather than becoming a fabricated zero/default.
|
||||
- Features that depend on frequency should use the current/actual QRG according to current implemented rules; do not silently revert to a fixed 144 MHz default.
|
||||
- Complete digit-only frequencies use their final three digits as the kHz part and are accepted only when the resulting MHz value lies within a supported `Band` range. The same full-frequency parser is used for station names and public or directed chat messages. Relative QRG rules and bare three-digit context handling remain separate.
|
||||
|
||||
### JavaFX/threading
|
||||
|
||||
@@ -73,6 +74,7 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen
|
||||
- The interpreter only adds positive runtime marks. It does not remove existing marks during the current session and does not reset automatically when a new contest starts. A database reset does not modify the file; callsigns contained in it are marked as worked again during the next periodic evaluation.
|
||||
- A missing selected file is created. Read, path and creation failures are contained so the periodic timer remains alive; successful creation triggers a one-time, non-blocking UI notice with the exact path and setup/contest checks.
|
||||
- Network-derived and manually assigned Worked, NOT-QRV and worked-grid state continues to use SQLite with its established lifetime and reset behaviour.
|
||||
- Each completed initial ON4KST user list loads one SQLite Worked/NOT-QRV snapshot. `ChatController` applies that snapshot by normalized base callsign to every new category and suffix variant before the completed category is published. The same event-driven path runs again after a reconnect; startup synchronization does not depend on a fixed-delay timer.
|
||||
- Automatic QRG updates require both an enabled source and valid incoming `RadioInfo` or Win-Test `STATUS` data. Merely enabling a source does not provide or validate a current QRG.
|
||||
- UCXLog-compatible QSO packets and Win-Test `ADDQSO` packets are converted into one validated external-QSO state. Logger-specific numeric, metre and centimetre values and Win-Test band IDs are normalised once; the resolved band is then the sole source for per-band Worked and worked-grid state.
|
||||
- A missing or unknown logger band sets only the global Worked state. Worked-grid state requires both a recognised project band and a valid locator; no band or locator is inferred. Packets without a usable callsign are discarded without terminating the listener.
|
||||
|
||||
@@ -48,7 +48,7 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
|
||||
|
||||
- **ON4KST-Protokollbefehle abgesichert:** Ausgehende Befehle werden zentral aufgebaut und auf gültige Kategorien, Locatoren und unerlaubte Frame-Trennzeichen geprüft. Da ON4KST pro TCP-Session nur einen Locator verwaltet, wird für beide Chat-Kategorien der Hauptlocator verwendet und eine abweichende zweite Konfiguration protokolliert, statt widersprüchliche Befehle an den Server zu senden.
|
||||
|
||||
- **QRG-Erkennung präzisiert:** Vollständige und relative Frequenzangaben werden weiterhin erkannt. Nackte dreistellige Zahlen gelten nur noch bei erkennbarem Frequenzkontext als QRG. Signalrapporte, Bandangaben und andere Zahlen erzeugen dadurch seltener falsche Frequenzen.
|
||||
- **QRG-Erkennung präzisiert:** Vollständige Frequenzangaben werden auch ohne Dezimaltrenner erkannt; die letzten drei Ziffern bilden dabei den kHz-Anteil. Relative Frequenzen bleiben unverändert, und nackte dreistellige Zahlen gelten weiterhin nur bei erkennbarem Frequenzkontext als QRG. Signalrapporte, Bandangaben und andere Zahlen erzeugen dadurch seltener falsche Frequenzen.
|
||||
|
||||
- **Stationsbezogener Frequenzkontext:** Bei relativen QRGs verwendet KST4Contest zuerst einen höchstens 30 Minuten alten Bandkontext derselben Station. Erst wenn dieser fehlt, wird das global konfigurierte Fallback-Band verwendet.
|
||||
|
||||
@@ -94,6 +94,8 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
|
||||
|
||||
- **Zuverlässige Benutzerliste beim Login:** Ungültige oder unvollständige `UA0`-Teilnehmerdatensätze werden einzeln verworfen und protokolliert, ohne die Verarbeitung der alphabetisch folgenden Teilnehmer abzubrechen. Die gültigen Einträge werden zunächst pro Kategorie gesammelt und erst mit dem ersten zugehörigen `UE`-Abschlussframe vollständig veröffentlicht.
|
||||
|
||||
- **Persistierter Worked-Status beim Listenaufbau:** Beim Abschluss jeder initialen ON4KST-Benutzerliste wird der SQLite-Zustand einmal geladen und vor der Veröffentlichung auf die neuen Chatmember angewendet. Das gilt für beide Kategorien, erneute Verbindungen und alle aktiven Varianten eines Basisrufzeichens.
|
||||
|
||||
- **Benutzerliste verschwindet nach dem Login:** ON4KST kann nach Namens-, Status- oder anderen Live-Änderungen weitere `UE`-Frames für dieselbe Kategorie senden. Wiederholte Abschlussframes werden jetzt erkannt und ignoriert, damit eine bereits gefüllte Benutzerliste nicht durch eine leere Momentaufnahme ersetzt wird.
|
||||
|
||||
- **Fehlgeschlagener Erstaufbau und Verbindungsverlust:** Wenn beim Programmstart keine Verbindung zum Server hergestellt werden kann, läuft KST4Contest nicht mehr in eine Endlos- oder Busy-Wait-Schleife. Die Oberfläche bleibt bedienbar und weitere Versuche erfolgen mit begrenztem Backoff. Auch ein vom Server geschlossener oder über längere Zeit stummer Socket wird zuverlässig erkannt.
|
||||
|
||||
@@ -84,13 +84,15 @@ KST4Contest wertet deshalb den Text jeder öffentlichen und gerichteten Chat-Nac
|
||||
|
||||
| Schreibweise | Beispiel | Verarbeitung |
|
||||
|---|---|---|
|
||||
| Vollständige Frequenz | `144.210`, `432,088`, `10368.100` | Das Band ergibt sich direkt aus der Frequenz. |
|
||||
| Vollständige Frequenz | `144.210`, `432,088`, `144307`, `10368100` | Das Band ergibt sich direkt aus der Frequenz. |
|
||||
| Relative Frequenz mit Punkt oder Komma | `.210`, `,088` | Das Band wird aus dem Stationskontext oder dem konfigurierten Fallback ergänzt. |
|
||||
| Dreistellige Frequenz mit Textkontext | `qrg 210`, `freq is 210`, `on 210`, `210 MHz` | Die Zahl wird als relative Frequenz behandelt. |
|
||||
| Dreistellige Zahl ohne Frequenzkontext | `210`, `599`, `144` | Die Zahl wird absichtlich nicht als QRG übernommen. |
|
||||
|
||||
Die letzte Einschränkung verhindert plausible, aber falsche Ergebnisse. Mit einem Fallback von `144 MHz` ließe sich ein Signalrapport `599` technisch problemlos zu `144.599 MHz` zusammensetzen. Das Ergebnis wäre formal gültig und fachlich trotzdem Unsinn.
|
||||
|
||||
Eine vollständige Frequenz kann auch ohne Punkt oder Komma geschrieben sein. KST4Contest behandelt dabei die letzten drei Ziffern als kHz-Anteil: `144307` im Namensfeld wird zu `144.307 MHz`, `10368100` in einer öffentlichen oder gerichteten Chatnachricht zu `10368.100 MHz`. Der Wert wird nur übernommen, wenn die daraus entstehende Frequenz innerhalb eines unterstützten Bandbereichs liegt.
|
||||
|
||||
### Wie wird das Band einer relativen QRG bestimmt?
|
||||
|
||||
KST4Contest verwendet folgende Reihenfolge:
|
||||
|
||||
@@ -48,7 +48,7 @@ v1.42 brings several previously separate calculations together. Band information
|
||||
|
||||
- **Validated ON4KST protocol commands:** Outgoing frames are built centrally and checked for valid categories, locators and prohibited frame delimiters. Because ON4KST maintains one locator per TCP session, the main locator is used for both chat categories and a conflicting secondary configuration is logged instead of sending contradictory commands to the server.
|
||||
|
||||
- **More precise QRG recognition:** Complete and relative frequency references continue to be recognised. Bare three-digit numbers are treated as QRGs only when a frequency context is available. Signal reports, band designators and unrelated numbers therefore produce fewer false frequencies.
|
||||
- **More precise QRG recognition:** Complete frequencies are also recognised without a decimal separator, with the final three digits interpreted as the kHz part. Relative frequencies remain unchanged, and bare three-digit numbers still require recognisable frequency context. Signal reports, band designators and unrelated numbers therefore produce fewer false frequencies.
|
||||
|
||||
- **Station-specific frequency context:** For relative QRGs, KST4Contest first uses a band context for the same station which is no more than 30 minutes old. The globally configured fallback band is used only when this context is unavailable.
|
||||
|
||||
@@ -94,6 +94,8 @@ v1.42 brings several previously separate calculations together. Band information
|
||||
|
||||
- **Reliable initial user list:** Invalid or incomplete `UA0` member records are rejected and logged individually without preventing alphabetically following members from being processed. Valid entries are staged per category and published as one complete snapshot when the first corresponding `UE` end marker is received.
|
||||
|
||||
- **Persisted Worked state during initial-list setup:** At the end of each initial ON4KST user list, the SQLite state is loaded once and applied to the new chat members before publication. This covers both categories, reconnects and every active variant of a base callsign.
|
||||
|
||||
- **User list disappearing after login:** ON4KST may send additional `UE` frames for the same category after name, state or other live updates. Repeated end markers are now detected and ignored so that an already populated user list cannot be replaced by an empty snapshot.
|
||||
|
||||
- **Failed initial connection and lost sockets:** An unavailable server during startup no longer sends KST4Contest into an endless or busy-wait loop. The user interface remains responsive and further attempts use bounded reconnect backoff. Sockets closed by the server, or connections without inbound traffic for an excessive period, are also detected reliably.
|
||||
|
||||
@@ -84,13 +84,15 @@ KST4Contest therefore evaluates the text of every public and directed chat messa
|
||||
|
||||
| Notation | Example | Processing |
|
||||
|---|---|---|
|
||||
| Complete frequency | `144.210`, `432,088`, `10368.100` | The frequency determines the band directly. |
|
||||
| Complete frequency | `144.210`, `432,088`, `144307`, `10368100` | The frequency determines the band directly. |
|
||||
| Relative frequency with a dot or comma | `.210`, `,088` | The band is added from the station context or configured fallback. |
|
||||
| Three-digit frequency with text context | `qrg 210`, `freq is 210`, `on 210`, `210 MHz` | The number is treated as a relative frequency. |
|
||||
| Three-digit number without frequency context | `210`, `599`, `144` | The number is deliberately not accepted as a QRG. |
|
||||
|
||||
The final restriction prevents plausible-looking but incorrect results. With a fallback of `144 MHz`, a signal report of `599` could easily be turned into `144.599 MHz`. The result would be formally valid and operationally useless.
|
||||
|
||||
A complete frequency may also be written without a dot or comma. KST4Contest treats the final three digits as the kHz part: `144307` in a station name becomes `144.307 MHz`, while `10368100` in a public or directed chat message becomes `10368.100 MHz`. The value is accepted only if the resulting frequency falls within a supported band range.
|
||||
|
||||
### How Is the Band of a Relative QRG Determined?
|
||||
|
||||
KST4Contest uses the following order:
|
||||
|
||||
@@ -1848,7 +1848,7 @@ private ObservableList<String>
|
||||
* assumption which one is the current run frequency. Therefore the legacy
|
||||
* frequency property is initialized only when exactly one explicit QRG exists.</p>
|
||||
*/
|
||||
private void initializeFrequencyFromStationNameIfUnambiguous(
|
||||
/* package */ void initializeFrequencyFromStationNameIfUnambiguous(
|
||||
ChatMember member
|
||||
) {
|
||||
if (member == null) {
|
||||
@@ -1942,6 +1942,8 @@ private ObservableList<String>
|
||||
int categoryNumber = category.getCategoryNumber();
|
||||
List<ChatMember> safeMembers = completeMembers == null
|
||||
? List.of() : new ArrayList<>(completeMembers);
|
||||
Map<String, ChatMember> workedDataFromDatabase =
|
||||
loadWorkedStateForInitialUserList(safeMembers);
|
||||
for (ChatMember member : safeMembers) {
|
||||
initializeFrequencyFromStationNameIfUnambiguous(member);
|
||||
}
|
||||
@@ -1966,10 +1968,48 @@ private ObservableList<String>
|
||||
&& member.getChatCategory() != null
|
||||
&& member.getChatCategory().getCategoryNumber() == categoryNumber);
|
||||
lst_chatMemberList.addAll(safeMembers);
|
||||
if (workedDataFromDatabase != null) {
|
||||
getLst_DBBasedWkdCallSignList().setAll(
|
||||
workedDataFromDatabase.values());
|
||||
}
|
||||
fireUserListUpdate("Complete ON4KST user list received");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads one database snapshot for a completed initial ON4KST user list and
|
||||
* applies it before those members are published to the active model or UI.
|
||||
* Every callsign variant receives the state stored for its normalized base
|
||||
* callsign.
|
||||
*
|
||||
* @param initialMembers completed members of one chat category
|
||||
* @return loaded snapshot, or {@code null} when the database read failed
|
||||
*/
|
||||
/* package */ Map<String, ChatMember> loadWorkedStateForInitialUserList(
|
||||
Collection<ChatMember> initialMembers
|
||||
) {
|
||||
if (dbHandler == null) {
|
||||
LOGGER.warning(
|
||||
"Cannot load Worked state for initial ON4KST user list: "
|
||||
+ "database is not initialized");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, ChatMember> workedDataFromDatabase =
|
||||
dbHandler.fetchChatMemberWkdDataFromDB();
|
||||
applyWorkedAndQrvStateFromDatabase(
|
||||
initialMembers, workedDataFromDatabase);
|
||||
return workedDataFromDatabase;
|
||||
} catch (SQLException | RuntimeException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Could not load Worked state for completed initial ON4KST user list",
|
||||
exception);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a member from the thread-safe active model. This avoids reading the
|
||||
* TableView backing list from MessageBusManagementThread.
|
||||
@@ -3854,15 +3894,6 @@ private ObservableList<String>
|
||||
|
||||
}
|
||||
|
||||
new Timer().schedule(new TimerTask() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("fetchWorkedFromDBTimer");
|
||||
refreshWorkedStateAndDatabaseListFromDatabase();
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// new Timer().schedule(new TimerTask() {
|
||||
// HashMap<String, ChatMember> getWorkedDataFromDb;
|
||||
//
|
||||
@@ -3966,23 +3997,34 @@ private ObservableList<String>
|
||||
HashMap<String, ChatMember> finalWorkedDataFromDatabase = workedDataFromDatabase;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
helper_applyWorkedAndQrvStateFromDatabase(finalWorkedDataFromDatabase);
|
||||
applyWorkedAndQrvStateFromDatabase(
|
||||
activeChatMembersByCallAndCategory.values(),
|
||||
finalWorkedDataFromDatabase);
|
||||
getLst_DBBasedWkdCallSignList().setAll(finalWorkedDataFromDatabase.values());
|
||||
fireUserListUpdate("Worked database state refreshed");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the worked and not-QRV state from the database snapshot to all active
|
||||
* chatmember objects that are currently visible in the live chat list.
|
||||
* Applies the worked and not-QRV state from a database snapshot to chat members.
|
||||
* Database rows are keyed by normalized base callsign, so all active category
|
||||
* and suffix variants receive the same persisted state.
|
||||
*
|
||||
* @param chatMembers members that should receive persisted state
|
||||
* @param workedDataFromDatabase map keyed by normalized raw callsign
|
||||
*/
|
||||
private void helper_applyWorkedAndQrvStateFromDatabase(HashMap<String, ChatMember> workedDataFromDatabase) {
|
||||
/* package */ static void applyWorkedAndQrvStateFromDatabase(
|
||||
Collection<ChatMember> chatMembers,
|
||||
Map<String, ChatMember> workedDataFromDatabase
|
||||
) {
|
||||
if (chatMembers == null || workedDataFromDatabase == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Iterator iterator = getLst_chatMemberList().iterator(); iterator.hasNext();) {
|
||||
|
||||
ChatMember activeChatMember = (ChatMember) iterator.next();
|
||||
for (ChatMember activeChatMember : chatMembers) {
|
||||
if (activeChatMember == null) {
|
||||
continue;
|
||||
}
|
||||
ChatMember storedChatMemberState = workedDataFromDatabase.get(activeChatMember.getCallSignRaw());
|
||||
|
||||
if (storedChatMemberState == null) {
|
||||
|
||||
@@ -57,7 +57,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
/*
|
||||
* Frequency formats handled by the smart parser:
|
||||
*
|
||||
* Group 1: full frequencies, for example 144.210 or 10368.100
|
||||
* Group 1: full frequencies, for example 144.210, 144210 or 10368100
|
||||
* Group 2: relative frequencies with a separator, for example .210 or ,210
|
||||
* Group 3: bare three-digit values, for example 210
|
||||
*
|
||||
@@ -66,7 +66,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
* would be converted into plausible but incorrect frequencies.
|
||||
*/
|
||||
private static final Pattern SMART_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![\\d])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
"(?<![A-Z0-9])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?|\\d{5,8})(?![A-Z0-9])"
|
||||
+ "|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
+ "|(?<=\\s|^)(\\d{3})(?=\\s|$)"
|
||||
);
|
||||
@@ -279,7 +279,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
* @param message message whose text is inspected
|
||||
* @param prefs preferences containing the global fallback band
|
||||
*/
|
||||
private void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
|
||||
/* package */ void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
|
||||
if (message == null || message.getMessageText() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ import java.util.regex.Pattern;
|
||||
* Common parser for explicit amateur-radio frequencies embedded in text.
|
||||
*
|
||||
* <p>This parser deliberately handles only complete frequencies such as
|
||||
* 144.300, 432.357 or 10368.100. Relative forms such as ".210" or ambiguous
|
||||
* bare values such as "210" require additional message context and remain the
|
||||
* responsibility of the chat-message parser.</p>
|
||||
* 144.300, 432.357, 10368.100 or their compact digit-only forms. Relative
|
||||
* forms such as ".210" or ambiguous bare values such as "210" require
|
||||
* additional message context and remain the responsibility of the
|
||||
* chat-message parser.</p>
|
||||
*/
|
||||
public final class FrequencyTextParser {
|
||||
|
||||
@@ -26,14 +27,18 @@ public final class FrequencyTextParser {
|
||||
* 432,357
|
||||
* 10368.100
|
||||
* 144.300.03
|
||||
* 144300
|
||||
* 10368100
|
||||
*
|
||||
* At least two digits are required before the decimal separator. This
|
||||
* intentionally prevents "1.2" from being interpreted as a frequency.
|
||||
* At least two digits are required before a decimal separator. Compact
|
||||
* values need at least five digits because their final three digits form
|
||||
* the kHz part. This intentionally prevents "1.2" and bare values such as
|
||||
* "210" from being interpreted as complete frequencies.
|
||||
*/
|
||||
private static final Pattern EXPLICIT_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![A-Z0-9])"
|
||||
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)"
|
||||
+ "(?!\\d)",
|
||||
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?|\\d{5,8})"
|
||||
+ "(?![A-Z0-9])",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
@@ -93,12 +98,12 @@ public final class FrequencyTextParser {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized =
|
||||
normalizeFrequencyString(
|
||||
rawFrequency
|
||||
.trim()
|
||||
.replace(',', '.')
|
||||
);
|
||||
final String trimmedFrequency = rawFrequency.trim();
|
||||
final String normalized = trimmedFrequency.matches("\\d{5,8}")
|
||||
? trimmedFrequency.substring(0, trimmedFrequency.length() - 3)
|
||||
+ "."
|
||||
+ trimmedFrequency.substring(trimmedFrequency.length() - 3)
|
||||
: normalizeFrequencyString(trimmedFrequency.replace(',', '.'));
|
||||
|
||||
try {
|
||||
double frequencyMHz =
|
||||
@@ -188,4 +193,4 @@ public final class FrequencyTextParser {
|
||||
return sourceText;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ChatControllerInitialWorkedStateTest {
|
||||
|
||||
@Test
|
||||
void loadsEachCompletedInitialListOnceAndAppliesStateToEveryVariant()
|
||||
throws SQLException {
|
||||
DBController database = mock(DBController.class);
|
||||
ChatMember stored = member("9A0BB", 2);
|
||||
stored.setWorked(true);
|
||||
stored.setWorked144(true);
|
||||
stored.setWorked10G(true);
|
||||
stored.setQrv432(false);
|
||||
|
||||
HashMap<String, ChatMember> databaseSnapshot = new HashMap<>();
|
||||
databaseSnapshot.put(stored.getCallSignRaw(), stored);
|
||||
when(database.fetchChatMemberWkdDataFromDB())
|
||||
.thenReturn(databaseSnapshot);
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setDbHandler(database);
|
||||
|
||||
ChatMember mainVariant = member("9A0BB-2", 2);
|
||||
ChatMember secondVariant = member("9A0BB-70", 3);
|
||||
ChatMember reconnectMainVariant = member("9A0BB-144", 2);
|
||||
ChatMember reconnectSecondVariant = member("9A0BB-432", 3);
|
||||
|
||||
controller.loadWorkedStateForInitialUserList(List.of(mainVariant));
|
||||
controller.loadWorkedStateForInitialUserList(List.of(secondVariant));
|
||||
controller.loadWorkedStateForInitialUserList(
|
||||
List.of(reconnectMainVariant));
|
||||
controller.loadWorkedStateForInitialUserList(
|
||||
List.of(reconnectSecondVariant));
|
||||
|
||||
verify(database, times(4)).fetchChatMemberWkdDataFromDB();
|
||||
for (ChatMember variant : List.of(
|
||||
mainVariant,
|
||||
secondVariant,
|
||||
reconnectMainVariant,
|
||||
reconnectSecondVariant
|
||||
)) {
|
||||
assertTrue(variant.isWorked());
|
||||
assertTrue(variant.isWorked144());
|
||||
assertTrue(variant.isWorked10G());
|
||||
assertFalse(variant.isQrv432());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsAmbiguousStationNameFromReplacingCompatibilityFrequency() {
|
||||
ChatController controller = new ChatController();
|
||||
ChatMember member = member("DL1ABC", 2);
|
||||
member.setName("144307 and 432100");
|
||||
|
||||
controller.initializeFrequencyFromStationNameIfUnambiguous(member);
|
||||
|
||||
assertTrue(
|
||||
member.getFrequency() == null
|
||||
|| member.getFrequency().get() == null
|
||||
|| member.getFrequency().get().isBlank()
|
||||
);
|
||||
}
|
||||
|
||||
private static ChatMember member(String callSign, int categoryNumber) {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setCallSign(callSign);
|
||||
member.setChatCategory(new ChatCategory(categoryNumber));
|
||||
return member;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatMessage;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
class MessageBusManagementThreadFrequencyTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void detectsCompactMicrowaveFrequencyInPublicAndDirectedMessages(
|
||||
boolean directedMessage
|
||||
) {
|
||||
ChatController controller = mock(ChatController.class);
|
||||
ThreadStatusCallback callback = mock(ThreadStatusCallback.class);
|
||||
MessageBusManagementThread messageBus =
|
||||
new MessageBusManagementThread(
|
||||
controller,
|
||||
callback,
|
||||
1L,
|
||||
new LinkedBlockingQueue<>(),
|
||||
ignored -> true
|
||||
);
|
||||
|
||||
ChatMember sender = member("DL1ABC");
|
||||
ChatMessage message = new ChatMessage();
|
||||
message.setSender(sender);
|
||||
message.setReceiver(member(directedMessage ? "DL2XYZ" : "ALL"));
|
||||
message.setMessageText("pse try 10368100");
|
||||
|
||||
messageBus.smartFrequencyExtraction(message, null);
|
||||
|
||||
verify(controller).applyDetectedFrequencyToActiveMembers(
|
||||
sender,
|
||||
Band.B_10G,
|
||||
10368.100
|
||||
);
|
||||
}
|
||||
|
||||
private static ChatMember member(String callSign) {
|
||||
ChatMember member = new ChatMember();
|
||||
member.setCallSign(callSign);
|
||||
return member;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
import kst4contest.model.Band;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FrequencyTextParserRegressionTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("compactFrequenciesAcrossSupportedBands")
|
||||
void detectsCompactFrequenciesAcrossSupportedBands(
|
||||
String compactFrequency,
|
||||
Band expectedBand,
|
||||
double expectedFrequencyMHz
|
||||
) {
|
||||
FrequencyTextParser.DetectedFrequency detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"QRV " + compactFrequency
|
||||
).get(0);
|
||||
|
||||
assertEquals(expectedBand, detected.getBand());
|
||||
assertEquals(
|
||||
expectedFrequencyMHz,
|
||||
detected.getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsReferenceFrequencyInStationName() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"Operator 144307"
|
||||
);
|
||||
|
||||
assertEquals(1, detected.size());
|
||||
assertEquals(Band.B_144, detected.get(0).getBand());
|
||||
assertEquals(
|
||||
144.307,
|
||||
detected.get(0).getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsCompactValuesOutsideSupportedBandRanges() {
|
||||
assertTrue(
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"146100 434100 99999"
|
||||
).isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsBareThreeDigitValuesOutOfCompleteFrequencyDetection() {
|
||||
assertTrue(
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"210 599 144"
|
||||
).isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> compactFrequenciesAcrossSupportedBands() {
|
||||
return Stream.of(
|
||||
Arguments.of("50278", Band.B_50, 50.278),
|
||||
Arguments.of("70200", Band.B_70, 70.200),
|
||||
Arguments.of("145500", Band.B_144, 145.500),
|
||||
Arguments.of("432100", Band.B_432, 432.100),
|
||||
Arguments.of("1296100", Band.B_1296, 1296.100),
|
||||
Arguments.of("2320100", Band.B_2320, 2320.100),
|
||||
Arguments.of("3400100", Band.B_3400, 3400.100),
|
||||
Arguments.of("5760100", Band.B_5760, 5760.100),
|
||||
Arguments.of("10368100", Band.B_10G, 10368.100),
|
||||
Arguments.of("24048100", Band.B_24G, 24048.100)
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -2390,4 +2390,5 @@ DK0MM;Jens/Alex;JN49IU;StringProperty [value: 432.305]; wkd true; wkd144 false;
|
||||
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 144.135]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 432.135]; wkd true; wkd144 true; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
LA0BY;Stefan @ hilltop;JO59IX;StringProperty [value: 144.062]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
LA0BY;null;JO49ML;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
|
||||
LA0BY;null;JO49ML;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
|
||||
OV3T;null;JO46CM;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
|
||||
@@ -37,9 +37,13 @@ Complete frequencies provide their band directly. Examples include:
|
||||
```text
|
||||
144.210
|
||||
432,088
|
||||
144307
|
||||
10368.100
|
||||
10368100
|
||||
```
|
||||
|
||||
The separator is optional for a complete frequency. For a digit-only value, KST4Contest treats the final three digits as the kHz part. This turns `144307` in a station name into `144.307 MHz` and `10368100` in a public or directed chat message into `10368.100 MHz`. The result still has to fall within one of the supported band ranges.
|
||||
|
||||
Relative forms omit the band and need additional context:
|
||||
|
||||
```text
|
||||
|
||||
Reference in New Issue
Block a user