diff --git a/docs/PROJECT_CONTEXT.md b/docs/PROJECT_CONTEXT.md index 32a3cef0..0689d5ae 100644 --- a/docs/PROJECT_CONTEXT.md +++ b/docs/PROJECT_CONTEXT.md @@ -74,6 +74,10 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen - 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. - 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. +- External logger threads do not read or mutate the JavaFX user-list projection. `ChatController` applies global and per-band Worked state to every active variant of the base callsign on the JavaFX Application Thread before evaluating a band-upgrade notice. +- The established Win-Test handling for 24, 47 and 76 GHz remains unchanged. Their Worked flags are retained, while only frequencies represented by the project `Band` model can create worked-grid state. ### Terrain data providers diff --git a/github_docs/de-Changelog.md b/github_docs/de-Changelog.md index 2d7c9d1a..3a7183ca 100644 --- a/github_docs/de-Changelog.md +++ b/github_docs/de-Changelog.md @@ -82,6 +82,8 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen, - **DXLog-Gesamtlog übernommen:** Der UCXLog-kompatible UDP-Listener verarbeitet neben `contactinfo` auch `contactreplace`. Dadurch kann ein von DXLog.net als vollständiges Log ausgesendeter Datenbestand eingelesen werden. +- **Logger-Bandwerte vereinheitlicht:** Numerische sowie Meter- und Zentimeterangaben aus UCXLog-kompatiblen QSO-Paketen und die Band-IDs von Win-Test werden einmal normalisiert und danach einheitlich für Worked-Markierungen und Worked-Großfelder verwendet. Dadurch setzen insbesondere `2320`, `5760` und `10368` zuverlässig ihre vorhandenen Bandmarkierungen. Bei einem fehlenden oder unbekannten Band bleibt es beim globalen Worked-Status. + - **Simplelogfile-Verhalten präzisiert:** Die ausgewählte Textdatei wird einmal pro Minute mit einem festen Rufzeichenmuster ausgewertet. Treffer setzen den globalen Worked-Status aller aktiven Varianten des Basisrufzeichens, werden aber nicht in SQLite persistiert. Eine fehlende Datei wird angelegt; Lese- und Erstellungsfehler beenden die periodische Auswertung nicht. Ein Datenbank-Reset verändert die Datei nicht, sodass enthaltene Rufzeichen bei der nächsten Auswertung erneut als gearbeitet markiert werden. - **Automatische QRG-Übernahme abgesichert:** `MYQRG` wird nur von einer aktivierten Schnittstelle aktualisiert, die tatsächlich gültige `RadioInfo`- beziehungsweise Win-Test-`STATUS`-Pakete liefert. Eine aktivierte, aber nicht liefernde Quelle ersetzt die notwendige Funktionsprüfung oder manuelle QRG-Pflege nicht. diff --git a/github_docs/en-Changelog.md b/github_docs/en-Changelog.md index a35b8a34..b9bf9e57 100644 --- a/github_docs/en-Changelog.md +++ b/github_docs/en-Changelog.md @@ -82,6 +82,8 @@ v1.42 brings several previously separate calculations together. Band information - **DXLog full-log import:** In addition to `contactinfo`, the UCXLog-compatible UDP listener processes `contactreplace`. This allows a complete log broadcast by DXLog.net to be imported. +- **Consistent logger band values:** Numeric, metre and centimetre values from UCXLog-compatible QSO packets and Win-Test band IDs are normalised once and then used consistently for Worked marks and worked grid squares. In particular, `2320`, `5760` and `10368` now reliably set their existing band marks. A missing or unknown band continues to set only the global Worked status. + - **Defined Simplelogfile behaviour:** The selected text file is evaluated once per minute using a fixed callsign pattern. Matches set the global Worked status for all active variants of the base callsign but are not persisted in SQLite. A missing file is created, and read or creation errors do not terminate the periodic task. A database reset does not change the file, so callsigns contained in it are marked as worked again during the next evaluation. - **Guarded automatic QRG updates:** `MYQRG` is updated only by an enabled interface which actually supplies valid `RadioInfo` or Win-Test `STATUS` packets. An enabled source which provides no data does not remove the need for a functional check or manual QRG maintenance. diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index 6d2f0647..b179ac52 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -2017,6 +2017,68 @@ private ObservableList return matchingMembers; } + /** + * Applies a QSO received from an external logger to all active callsign + * variants. Active member objects back JavaFX views, so their state is changed + * only on the JavaFX Application Thread. The band-upgrade hint is evaluated + * afterwards and therefore sees the new Worked state. + * + * @param loggedQso validated external QSO + */ + public void applyExternalLoggedQso(ExternalLoggedQso loggedQso) { + if (loggedQso == null) { + return; + } + + runOnFxThread(() -> { + int updatedMembers = markExternalLoggedQsoMembers( + activeChatMembersByCallAndCategory.values(), loggedQso); + if (updatedMembers == 0) { + return; + } + + fireUserListUpdate("External logger Worked state updated"); + try { + onExternalLogEntryReceived(loggedQso.getCallSign()); + } catch (Exception exception) { + LOGGER.log(Level.WARNING, + "Band-upgrade hint failed after external logger update for " + + loggedQso.getCallSign(), exception); + } + }); + } + + static int markExternalLoggedQsoMembers( + Collection members, + ExternalLoggedQso loggedQso + ) { + if (members == null || loggedQso == null) { + return 0; + } + + String workedBaseCall = ChatMember.normalizeCallSignToBaseCallSign( + loggedQso.getCallSign()); + if (workedBaseCall == null || workedBaseCall.isBlank()) { + return 0; + } + + int updatedMembers = 0; + for (ChatMember member : members) { + if (member == null) { + continue; + } + + String memberCall = member.getCallSignRaw() != null + ? member.getCallSignRaw() : member.getCallSign(); + String memberBaseCall = ChatMember.normalizeCallSignToBaseCallSign(memberCall); + if (memberBaseCall != null && memberBaseCall.equalsIgnoreCase(workedBaseCall)) { + loggedQso.applyToActiveMember(member); + updatedMembers++; + } + } + return updatedMembers; + } + /** * Applies the global Worked state from the Simplelogfile to every active * callsign variant with the same base callsign. UI-backed state is changed only diff --git a/src/main/java/kst4contest/controller/ExternalLoggedQso.java b/src/main/java/kst4contest/controller/ExternalLoggedQso.java new file mode 100644 index 00000000..849af546 --- /dev/null +++ b/src/main/java/kst4contest/controller/ExternalLoggedQso.java @@ -0,0 +1,116 @@ +package kst4contest.controller; + +import java.util.Optional; + +import kst4contest.model.ChatMember; + +/** + * Validated QSO state shared by external logger listeners and the controller. + */ +public final class ExternalLoggedQso { + private final String callSign; + private final LoggedQsoBand band; + private final String locator; + private final String source; + + private ExternalLoggedQso( + String callSign, + LoggedQsoBand band, + String locator, + String source + ) { + this.callSign = callSign; + this.band = band; + this.locator = locator; + this.source = source; + } + + /** + * Creates a QSO only when the logger supplied a usable callsign. Missing band + * and locator values remain unavailable and never receive guessed defaults. + * + * @param rawCallSign callsign received from the logger + * @param band normalized band or {@code null} + * @param rawLocator locator received from the logger or {@code null} + * @param source logger name used for persistence diagnostics + * @return validated QSO, or an empty value for a missing callsign + */ + public static Optional create( + String rawCallSign, + LoggedQsoBand band, + String rawLocator, + String source + ) { + if (rawCallSign == null) { + return Optional.empty(); + } + + String normalizedCallSign = rawCallSign.trim(); + if (normalizedCallSign.isEmpty()) { + return Optional.empty(); + } + String normalizedLocator = WorkedGrossFieldCache.extractLocator6(rawLocator); + return Optional.of(new ExternalLoggedQso( + normalizedCallSign, + band, + normalizedLocator, + source + )); + } + + public String getCallSign() { + return callSign; + } + + public LoggedQsoBand getBand() { + return band; + } + + public String getLocator() { + return locator; + } + + public String getSource() { + return source; + } + + /** + * Builds the database representation of this QSO. + * + * @return chat-member state with global and optional band Worked flags + */ + public ChatMember toWorkedChatMember() { + ChatMember workedCall = new ChatMember(); + workedCall.setCallSign(callSign); + workedCall.setWorked(true); + if (band != null) { + band.applyWorkedFlag(workedCall); + } + if (locator != null) { + workedCall.setQra(locator); + } + return workedCall; + } + + /** + * Applies this QSO to one active callsign variant. + * + * @param member active variant with the same base callsign + */ + public void applyToActiveMember(ChatMember member) { + if (member == null) { + return; + } + + member.setWorked(true); + if (band != null) { + band.applyWorkedFlag(member); + } + if (locator != null + && (member.getQra() == null + || member.getQra().isBlank() + || "unknown".equalsIgnoreCase(member.getQra()))) { + member.setQra(locator); + } + } +} diff --git a/src/main/java/kst4contest/controller/LoggedQsoBand.java b/src/main/java/kst4contest/controller/LoggedQsoBand.java new file mode 100644 index 00000000..29cb39d2 --- /dev/null +++ b/src/main/java/kst4contest/controller/LoggedQsoBand.java @@ -0,0 +1,129 @@ +package kst4contest.controller; + +import java.util.Locale; + +import kst4contest.model.Band; +import kst4contest.model.ChatMember; + +/** + * Normalized band of a QSO received from an external logger. + * + *

Logger protocols use different representations for the same amateur-radio + * band. UCXLog-compatible XML may contain a frequency-like number or a metre / + * centimetre label, while Win-Test uses its own numeric band IDs. Resolving the + * raw value once prevents the Worked flag and gross-field update from interpreting + * the same packet differently.

+ */ +public enum LoggedQsoBand { + BAND_50(Band.B_50), + BAND_70(Band.B_70), + BAND_144(Band.B_144), + BAND_432(Band.B_432), + BAND_1296(Band.B_1296), + BAND_2320(Band.B_2320), + BAND_3400(Band.B_3400), + BAND_5760(Band.B_5760), + BAND_10G(Band.B_10G), + BAND_24G(Band.B_24G), + BAND_47G(null), + BAND_76G(null); + + private final Band projectBand; + + LoggedQsoBand(Band projectBand) { + this.projectBand = projectBand; + } + + /** + * Resolves aliases used by UCXLog, N1MM+, DXLog.net, QARTest and compatible + * loggers. The input is trimmed and case-normalized exactly once here. + * + * @param rawBand band value received in the XML packet + * @return normalized band, or {@code null} for a missing or unknown value + */ + public static LoggedQsoBand fromLoggerValue(String rawBand) { + if (rawBand == null) { + return null; + } + + String normalizedBand = rawBand.trim().toLowerCase(Locale.ROOT); + return switch (normalizedBand) { + case "50", "6m" -> BAND_50; + case "70", "4m" -> BAND_70; + case "144", "2m" -> BAND_144; + case "432", "70cm" -> BAND_432; + case "1240", "1296", "23cm" -> BAND_1296; + case "2300", "2320", "13cm" -> BAND_2320; + case "3400", "9cm" -> BAND_3400; + case "5600", "5760", "6cm" -> BAND_5760; + case "10g", "10368", "3cm" -> BAND_10G; + default -> null; + }; + } + + /** + * Resolves the established Win-Test ADDQSO band IDs. IDs 22 and 23 retain + * their existing Worked flags even though the project Band enum has no 47 GHz + * or 76 GHz value for gross-field storage. + * + * @param rawBandId Win-Test band ID + * @return normalized band, or {@code null} for a missing or unknown ID + */ + public static LoggedQsoBand fromWinTestBandId(String rawBandId) { + if (rawBandId == null) { + return null; + } + + String normalizedBandId = rawBandId.trim(); + return switch (normalizedBandId) { + case "10" -> BAND_50; + case "11" -> BAND_70; + case "12" -> BAND_144; + case "14" -> BAND_432; + case "16" -> BAND_1296; + case "17" -> BAND_2320; + case "18" -> BAND_3400; + case "19" -> BAND_5760; + case "20" -> BAND_10G; + case "21" -> BAND_24G; + case "22" -> BAND_47G; + case "23" -> BAND_76G; + default -> null; + }; + } + + /** + * Returns the common Band value used by worked-grid persistence. + * + * @return project band, or {@code null} where no matching Band value exists + */ + public Band getProjectBand() { + return projectBand; + } + + /** + * Applies the established per-band Worked flag to a chat member. + * + * @param member worked station or active callsign variant + */ + public void applyWorkedFlag(ChatMember member) { + if (member == null) { + return; + } + + switch (this) { + case BAND_50 -> member.setWorked50(true); + case BAND_70 -> member.setWorked70(true); + case BAND_144 -> member.setWorked144(true); + case BAND_432 -> member.setWorked432(true); + case BAND_1296 -> member.setWorked1240(true); + case BAND_2320 -> member.setWorked2300(true); + case BAND_3400 -> member.setWorked3400(true); + case BAND_5760 -> member.setWorked5600(true); + case BAND_10G -> member.setWorked10G(true); + case BAND_24G -> member.setWorked24G(true); + case BAND_47G -> member.setWorked47G(true); + case BAND_76G -> member.setWorked76G(true); + } + } +} diff --git a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java index e480a64e..256eb010 100644 --- a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java +++ b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java @@ -2,10 +2,8 @@ package kst4contest.controller; import javafx.application.Platform; import kst4contest.ApplicationConstants; -import kst4contest.model.Band; import kst4contest.model.ChatMember; import kst4contest.model.ThreadStateMessage; -import kst4contest.view.GuiUtils; import java.io.BufferedWriter; import java.io.File; @@ -98,7 +96,7 @@ public class ReadUDPByWintestThread extends Thread { } } - private void processWinTestMessage(String msg) { + void processWinTestMessage(String msg) { // System.out.println("Wintest-Message received: " + msg); lastPacketTime = System.currentTimeMillis(); @@ -368,33 +366,28 @@ public class ReadUDPByWintestThread extends Thread { /** - * Resolves the project Band enum from Win-Test band IDs. + * Extracts the unchanged Win-Test band ID from the unquoted ADDQSO fields. * - *

Only bands that exist in the current Band enum are returned. The existing - * 50 MHz and 70 MHz values are resolved in the same way as the other supported - * VHF, UHF and microwave bands.

- * - * @param bandId Win-Test band id from ADDQSO - * @return matching Band or null + * @param message complete ADDQSO packet + * @return raw band ID or an empty value when the field is missing */ - private Band helper_resolveBandFromWinTestBandId(String bandId) { - if (bandId == null) { - return null; + static String extractBandIdFromWinTestAddQso(String message) { + if (message == null) { + return ""; } - return switch (bandId.trim()) { - case "10" -> Band.B_50; - case "11" -> Band.B_70; - case "12" -> Band.B_144; - case "14" -> Band.B_432; - case "16" -> Band.B_1296; - case "17" -> Band.B_2320; - case "18" -> Band.B_3400; - case "19" -> Band.B_5760; - case "20" -> Band.B_10G; - case "21" -> Band.B_24G; - default -> null; - }; + String[] quotedParts = message.split("\""); + if (quotedParts.length <= 6) { + return ""; + } + + String unquotedFields = quotedParts[6].trim(); + if (unquotedFields.isEmpty()) { + return ""; + } + + String[] packetFields = unquotedFields.split("\\s+"); + return packetFields.length > 3 ? packetFields[3] : ""; } /** @@ -449,112 +442,32 @@ public class ReadUDPByWintestThread extends Thread { * @param msg */ private void parseAddQso(String msg) { - - - ChatMember modifyThat = null; - try { -// int qsoNumber = extractQsoNumber(msg); -// receivedQsos.put(qsoNumber, msg); -// lastKnownQso = Math.max(lastKnownQso, qsoNumber); - String callSignCatched = msg.split("\"") [7]; + String[] quotedParts = msg == null ? new String[0] : msg.split("\""); + String callSign = quotedParts.length > 7 ? quotedParts[7] : ""; + String rawBandId = extractBandIdFromWinTestAddQso(msg); String locatorFromLogger = helper_resolveLocatorFromWinTestAddQso(msg); - - ChatMember workedCall = new ChatMember(); - workedCall.setCallSign(callSignCatched); - workedCall.setWorked(true); //its worked at this place, for sure! - - if (locatorFromLogger != null) { - workedCall.setQra(locatorFromLogger); + LoggedQsoBand loggedBand = LoggedQsoBand.fromWinTestBandId(rawBandId); + ExternalLoggedQso loggedQso = ExternalLoggedQso.create( + callSign, loggedBand, locatorFromLogger, "WINTEST").orElse(null); + if (loggedQso == null) { + System.out.println("[WinTestUDPRcvr: warning] ADDQSO without usable callsign ignored"); + return; } - ArrayList markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall); - - String bandId; - bandId = msg.split("\"")[6].split(" ")[4].trim(); - - Band workedBand = helper_resolveBandFromWinTestBandId(bandId); - switch (bandId) { - case "10" -> workedCall.setWorked50(true); - case "11" -> workedCall.setWorked70(true); - case "12" -> workedCall.setWorked144(true); - case "14" -> workedCall.setWorked432(true); - case "16" -> workedCall.setWorked1240(true); - case "17" -> workedCall.setWorked2300(true); - case "18" -> workedCall.setWorked3400(true); - case "19" -> workedCall.setWorked5600(true); - case "20" -> workedCall.setWorked10G(true); - case "21" -> workedCall.setWorked24G(true); - case "22" -> workedCall.setWorked47G(true); - case "23" -> workedCall.setWorked76G(true); - default -> System.out.println("[WinTestUDPRcvr: warning] Unbekannte Band-ID: " + bandId); + if (loggedBand == null && !rawBandId.isEmpty()) { + System.out.println("[WinTestUDPRcvr: warning] Unknown band ID: " + rawBandId); } - if (workedBand != null && locatorFromLogger != null) { - this.client.registerWorkedGrossField(workedBand, locatorFromLogger, workedCall, "WINTEST"); - } - - if (!markTheseChattersAsWorked.isEmpty()) { - //Worked call is part of the current chatmember list - - for (int index : markTheseChattersAsWorked) { - //iterate through the logged in chatmembers callsigns and set the worked markers - modifyThat = client.getLst_chatMemberList().get(index); - - modifyThat.setWorked(true); //worked its for sure - - if (locatorFromLogger != null - && (modifyThat.getQra() == null - || modifyThat.getQra().isBlank() - || "unknown".equalsIgnoreCase(modifyThat.getQra()))) { - modifyThat.setQra(locatorFromLogger); - } - - if (workedCall.isWorked50()) { - modifyThat.setWorked50(true); - } else if (workedCall.isWorked70()) { - modifyThat.setWorked70(true); - } else if (workedCall.isWorked144()) { - modifyThat.setWorked144(true); - } else if (workedCall.isWorked432()) { - modifyThat.setWorked432(true); - } else if (workedCall.isWorked1240()) { - modifyThat.setWorked1240(true); - } else if (workedCall.isWorked2300()) { - modifyThat.setWorked2300(true); - } else if (workedCall.isWorked3400()) { - modifyThat.setWorked3400(true); - } else if (workedCall.isWorked5600()) { - modifyThat.setWorked5600(true); - } else if (workedCall.isWorked10G()) { - modifyThat.setWorked10G(true); - } else if (workedCall.isWorked24G()) { - modifyThat.setWorked24G(true); - } else if (workedCall.isWorked47G()) { - modifyThat.setWorked47G(true); - } else if (workedCall.isWorked76G()) { - modifyThat.setWorked76G(true); - } else { - System.out.println("[WinTestUDPRcvr: warning] found no new worked-flag for this band: " + workedCall.getCallSignRaw() + bandId); - } - } - - try { - - GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all - - // trigger band-upgrade hint after log entry (Win-Test) - try { - client.onExternalLogEntryReceived(workedCall.getCallSignRaw()); - } catch (Exception e) { - System.out.println("[WinTestUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage()); - } - - } catch (Exception IllegalStateException) { - //do nothing, as it works... - } + ChatMember workedCall = loggedQso.toWorkedChatMember(); + if (loggedBand != null + && loggedBand.getProjectBand() != null + && locatorFromLogger != null) { + this.client.registerWorkedGrossField( + loggedBand.getProjectBand(), locatorFromLogger, workedCall, loggedQso.getSource()); } + client.applyExternalLoggedQso(loggedQso); boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall); // This will update the worked info on a worked chatmember. DBHandler will @@ -587,17 +500,9 @@ public class ReadUDPByWintestThread extends Thread { bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile); - if (modifyThat != null) { - bufwrtrRawMSGOut.write("\n" + modifyThat.toString()); - bufwrtrRawMSGOut.flush(); - bufwrtrRawMSGOut.close(); - - } else { - bufwrtrRawMSGOut.write("\n" + workedCall.toString()); - bufwrtrRawMSGOut.flush(); - bufwrtrRawMSGOut.close(); - - } + bufwrtrRawMSGOut.write("\n" + workedCall.toString()); + bufwrtrRawMSGOut.flush(); + bufwrtrRawMSGOut.close(); System.out.println("[WinTest, Info: Marking Chatmember as worked: " + workedCall.toString()); diff --git a/src/main/java/kst4contest/controller/ReadUDPbyUCXMessageThread.java b/src/main/java/kst4contest/controller/ReadUDPbyUCXMessageThread.java index de7e63bc..b09970f3 100644 --- a/src/main/java/kst4contest/controller/ReadUDPbyUCXMessageThread.java +++ b/src/main/java/kst4contest/controller/ReadUDPbyUCXMessageThread.java @@ -3,7 +3,6 @@ package kst4contest.controller; import java.io.*; import java.net.*; import java.sql.SQLException; -import java.util.ArrayList; import java.util.Arrays; import javax.xml.XMLConstants; @@ -13,7 +12,6 @@ import javax.xml.parsers.ParserConfigurationException; import kst4contest.ApplicationConstants; import kst4contest.model.ThreadStateMessage; -import kst4contest.view.GuiUtils; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -132,54 +130,6 @@ public class ReadUDPbyUCXMessageThread extends Thread { return WorkedGrossFieldCache.extractLocator6(helper_getOptionalElementText(element, "rcvnr")); } - /** - * Resolves the project Band enum from logger band values. - * - * @param band logger band text - * @return matching Band or null - */ - private Band helper_resolveBandFromLoggerBand(String band) { - if (band == null) { - return null; - } - - switch (band.trim()) { - case "50": - case "6m": - return Band.B_50; - case "70": - case "4m": - return Band.B_70; - case "144": - case "2m": - return Band.B_144; - case "432": - case "70cm": - return Band.B_432; - case "1240": - case "1296": - case "23cm": - return Band.B_1296; - case "2300": - case "2320": - case "13cm": - return Band.B_2320; - case "3400": - case "9cm": - return Band.B_3400; - case "5600": - case "5760": - case "6cm": - return Band.B_5760; - case "10G": - case "10368": - case "3cm": - return Band.B_10G; - default: - return null; - } - } - public void run() { System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP"); @@ -295,8 +245,6 @@ public class ReadUDPbyUCXMessageThread extends Thread { String udpMsg = helper_extractXmlPayload(udpPacketToProcess); - ChatMember modifyThat = null; - ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false); callBackToController.onThreadStatus(ThreadNickName,threadStateMessage); @@ -342,95 +290,27 @@ public class ReadUDPbyUCXMessageThread extends Thread { Element element = (Element) node; - String call = element.getElementsByTagName("call").item(0).getTextContent(); - String band = helper_getOptionalElementText(element, "band"); + String call = helper_getOptionalElementText(element, "call"); + String rawBand = helper_getOptionalElementText(element, "band"); String gridSquare = helper_resolveLocatorFromContactInfo(element); String points = helper_getOptionalElementText(element, "points"); + LoggedQsoBand loggedBand = LoggedQsoBand.fromLoggerValue(rawBand); + ExternalLoggedQso loggedQso = ExternalLoggedQso.create( + call, loggedBand, gridSquare, "UCXLOG").orElse(null); + if (loggedQso == null) { + System.out.println("[ReadUDPFromUCX, warning]: QSO packet without usable callsign ignored"); + continue; + } System.out.println("[Readudp, info ]: received Current Element :" + node.getNodeName() - + "call: " + call + " / " + band + " ----> " + points + " POINTS"); + + "call: " + call + " / " + rawBand + " ----> " + points + " POINTS"); // client.getChatPreferences().setBcn_contestScoreSum(Long.parseLong(points)); - ChatMember workedCall = new ChatMember(); - workedCall.setCallSign(call); - workedCall.setWorked(true); - - if (gridSquare != null) { - workedCall.setQra(gridSquare); - } - - Band workedBand = helper_resolveBandFromLoggerBand(band); - - switch (band) { - case "50": - case "6m": - { - workedCall.setWorked50(true); - break; - } - - case "70": - case "4m": - { - workedCall.setWorked70(true); - break; - } - - case "144": - case "2m": //minos contest logger - { - workedCall.setWorked144(true); - break; - } - - case "432": - case "70cm": - { - workedCall.setWorked432(true); - break; - } - - case "1240": //ucxlog style - case "1296": //used for n1mm / Dxlog - case "23cm": //minos contest logger - { - workedCall.setWorked1240(true); - break; - } - - case "2300": - case "13cm": - { - workedCall.setWorked2300(true); - break; - } - - case "3400": - case "9cm": - { - workedCall.setWorked3400(true); - break; - } - - case "5600": - case "6cm": - { - workedCall.setWorked5600(true); - break; - } - - case "10G": - case "3cm": - { - workedCall.setWorked10G(true); - break; - } - - - default: - System.out.println("[ReadUDPFromUCX, Error:] unexpected band value: \"" + band + "\""); - break; + ChatMember workedCall = loggedQso.toWorkedChatMember(); + Band workedBand = loggedBand == null ? null : loggedBand.getProjectBand(); + if (loggedBand == null && !rawBand.isEmpty()) { + System.out.println("[ReadUDPFromUCX, warning]: unexpected band value: \"" + rawBand + "\""); } { @@ -444,60 +324,7 @@ public class ReadUDPbyUCXMessageThread extends Thread { // asd //TODO: Check if callsign and callsignraw is similar, then mark first and further via new checklistforchatmembermultiplemethod with array of indize - ArrayList markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall); - - if (markTheseChattersAsWorked.isEmpty()) { - //Worked call is not part of the chatmember list - } else { - for (int index : markTheseChattersAsWorked) { - modifyThat = client.getLst_chatMemberList().get(index); - - modifyThat.setWorked(true); - - if (workedCall.isWorked50()) { - modifyThat.setWorked50(true); - - } else if (workedCall.isWorked70()) { - modifyThat.setWorked70(true); - - } else if (workedCall.isWorked144()) { - modifyThat.setWorked144(true); - - } else if (workedCall.isWorked432()) { - modifyThat.setWorked432(true); - - } else if (workedCall.isWorked1240()) { - modifyThat.setWorked1240(true); - - } else if (workedCall.isWorked2300()) { - modifyThat.setWorked2300(true); - - } else if (workedCall.isWorked3400()) { - modifyThat.setWorked3400(true); - - } else if (workedCall.isWorked5600()) { - modifyThat.setWorked5600(true); - - } else if (workedCall.isWorked10G()) { - modifyThat.setWorked10G(true); - } - } - - try { - - GuiUtils.triggerGUIFilteredChatMemberListChange(this.client); - // BEGIN PATCH: trigger band-upgrade hint after log entry (UCXLog) - try { - client.onExternalLogEntryReceived(workedCall.getCallSignRaw()); - } catch (Exception e) { - System.out.println("[UCXUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage()); - } - - - } catch (Exception IllegalStateException) { - //do nothing, as it works... - } - } + client.applyExternalLoggedQso(loggedQso); /** * old mechanic to markup worked stations in the chatmember table @@ -574,7 +401,8 @@ public class ReadUDPbyUCXMessageThread extends Thread { } if (workedBand != null && gridSquare != null) { - this.client.registerWorkedGrossField(workedBand, gridSquare, workedCall, "UCXLOG"); + this.client.registerWorkedGrossField( + workedBand, gridSquare, workedCall, loggedQso.getSource()); } boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall); @@ -609,17 +437,9 @@ public class ReadUDPbyUCXMessageThread extends Thread { bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile); - if (modifyThat != null) { - bufwrtrRawMSGOut.write("\n" + modifyThat.toString()); - bufwrtrRawMSGOut.flush(); - bufwrtrRawMSGOut.close(); - - } else { - bufwrtrRawMSGOut.write("\n" + workedCall.toString()); - bufwrtrRawMSGOut.flush(); - bufwrtrRawMSGOut.close(); - - } + bufwrtrRawMSGOut.write("\n" + workedCall.toString()); + bufwrtrRawMSGOut.flush(); + bufwrtrRawMSGOut.close(); } } @@ -771,4 +591,4 @@ public class ReadUDPbyUCXMessageThread extends Thread { } } -} \ No newline at end of file +} diff --git a/src/test/java/kst4contest/controller/ExternalLoggedQsoTest.java b/src/test/java/kst4contest/controller/ExternalLoggedQsoTest.java new file mode 100644 index 00000000..bfe85811 --- /dev/null +++ b/src/test/java/kst4contest/controller/ExternalLoggedQsoTest.java @@ -0,0 +1,173 @@ +package kst4contest.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.stream.Stream; + +import kst4contest.model.ChatMember; +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; + +class ExternalLoggedQsoTest { + + @ParameterizedTest + @MethodSource("loggerBandAliases") + void resolvesEveryLoggerBandAlias(String rawValue, LoggedQsoBand expectedBand) { + assertEquals(expectedBand, LoggedQsoBand.fromLoggerValue(rawValue)); + } + + @ParameterizedTest + @MethodSource("winTestBandIds") + void resolvesEveryExistingWinTestBandId(String rawValue, LoggedQsoBand expectedBand) { + assertEquals(expectedBand, LoggedQsoBand.fromWinTestBandId(rawValue)); + } + + @Test + void unknownOrMissingBandRemainsUnavailable() { + assertNull(LoggedQsoBand.fromLoggerValue(null)); + assertNull(LoggedQsoBand.fromLoggerValue("")); + assertNull(LoggedQsoBand.fromLoggerValue("unknown")); + assertNull(LoggedQsoBand.fromWinTestBandId(null)); + assertNull(LoggedQsoBand.fromWinTestBandId("99")); + } + + @Test + void missingCallsignIsRejectedBeforeAnyStateIsBuilt() { + assertTrue(ExternalLoggedQso.create(null, LoggedQsoBand.BAND_144, "JO50AA", "TEST").isEmpty()); + assertTrue(ExternalLoggedQso.create(" ", LoggedQsoBand.BAND_144, "JO50AA", "TEST").isEmpty()); + } + + @Test + void missingBandSetsOnlyGlobalWorkedState() { + ExternalLoggedQso qso = qso("DL1ABC", null, null); + ChatMember workedCall = qso.toWorkedChatMember(); + + assertTrue(workedCall.isWorked()); + assertFalse(workedCall.isWorked50()); + assertFalse(workedCall.isWorked70()); + assertFalse(workedCall.isWorked144()); + assertFalse(workedCall.isWorked2300()); + assertNull(workedCall.getQra()); + } + + @Test + void winTest50And70MhzIdsSetTheirExistingFlags() { + String qso50Packet = winTestAddQsoPacket("10", "DL1ABC"); + String qso70Packet = winTestAddQsoPacket("11", "DL1ABC"); + String band50Id = ReadUDPByWintestThread.extractBandIdFromWinTestAddQso(qso50Packet); + String band70Id = ReadUDPByWintestThread.extractBandIdFromWinTestAddQso(qso70Packet); + ChatMember worked50 = qso("DL1ABC", LoggedQsoBand.fromWinTestBandId(band50Id), null) + .toWorkedChatMember(); + ChatMember worked70 = qso("DL1ABC", LoggedQsoBand.fromWinTestBandId(band70Id), null) + .toWorkedChatMember(); + + assertEquals("10", band50Id); + assertEquals("11", band70Id); + assertTrue(worked50.isWorked50()); + assertTrue(worked70.isWorked70()); + } + + @Test + void winTest47And76GhzFlagsRemainAvailableWithoutInventingProjectBands() { + LoggedQsoBand band47 = LoggedQsoBand.fromWinTestBandId("22"); + LoggedQsoBand band76 = LoggedQsoBand.fromWinTestBandId("23"); + ChatMember worked47 = qso("DL1ABC", band47, "JO50AA").toWorkedChatMember(); + ChatMember worked76 = qso("DL1ABC", band76, "JO50AA").toWorkedChatMember(); + + assertTrue(worked47.isWorked47G()); + assertTrue(worked76.isWorked76G()); + assertNull(band47.getProjectBand()); + assertNull(band76.getProjectBand()); + } + + @Test + void normalized2320AliasUpdatesEveryActiveCallsignVariant() { + ChatMember firstVariant = member("9A0BB-2"); + ChatMember secondVariant = member("9A0BB-70"); + ChatMember unrelated = member("DL1ABC"); + ExternalLoggedQso qso = qso( + "9A0BB", LoggedQsoBand.fromLoggerValue(" 2320 "), "JO50AA"); + + int updated = ChatController.markExternalLoggedQsoMembers( + List.of(firstVariant, secondVariant, unrelated), qso); + + assertEquals(2, updated); + assertTrue(firstVariant.isWorked()); + assertTrue(firstVariant.isWorked2300()); + assertEquals("JO50AA", firstVariant.getQra()); + assertTrue(secondVariant.isWorked()); + assertTrue(secondVariant.isWorked2300()); + assertEquals("JO50AA", secondVariant.getQra()); + assertFalse(unrelated.isWorked()); + } + + private static Stream loggerBandAliases() { + return Stream.of( + Arguments.of("50", LoggedQsoBand.BAND_50), + Arguments.of("6m", LoggedQsoBand.BAND_50), + Arguments.of("70", LoggedQsoBand.BAND_70), + Arguments.of("4m", LoggedQsoBand.BAND_70), + Arguments.of("144", LoggedQsoBand.BAND_144), + Arguments.of("2m", LoggedQsoBand.BAND_144), + Arguments.of("432", LoggedQsoBand.BAND_432), + Arguments.of("70cm", LoggedQsoBand.BAND_432), + Arguments.of("1240", LoggedQsoBand.BAND_1296), + Arguments.of("1296", LoggedQsoBand.BAND_1296), + Arguments.of("23cm", LoggedQsoBand.BAND_1296), + Arguments.of("2300", LoggedQsoBand.BAND_2320), + Arguments.of(" 2320 ", LoggedQsoBand.BAND_2320), + Arguments.of("13cm", LoggedQsoBand.BAND_2320), + Arguments.of("3400", LoggedQsoBand.BAND_3400), + Arguments.of("9cm", LoggedQsoBand.BAND_3400), + Arguments.of("5600", LoggedQsoBand.BAND_5760), + Arguments.of(" 5760 ", LoggedQsoBand.BAND_5760), + Arguments.of("6cm", LoggedQsoBand.BAND_5760), + Arguments.of("10G", LoggedQsoBand.BAND_10G), + Arguments.of(" 10368 ", LoggedQsoBand.BAND_10G), + Arguments.of("3cm", LoggedQsoBand.BAND_10G) + ); + } + + private static Stream winTestBandIds() { + return Stream.of( + Arguments.of(" 10 ", LoggedQsoBand.BAND_50), + Arguments.of("11", LoggedQsoBand.BAND_70), + Arguments.of("12", LoggedQsoBand.BAND_144), + Arguments.of("14", LoggedQsoBand.BAND_432), + Arguments.of("16", LoggedQsoBand.BAND_1296), + Arguments.of("17", LoggedQsoBand.BAND_2320), + Arguments.of("18", LoggedQsoBand.BAND_3400), + Arguments.of("19", LoggedQsoBand.BAND_5760), + Arguments.of("20", LoggedQsoBand.BAND_10G), + Arguments.of("21", LoggedQsoBand.BAND_24G), + Arguments.of("22", LoggedQsoBand.BAND_47G), + Arguments.of("23", LoggedQsoBand.BAND_76G) + ); + } + + private static ExternalLoggedQso qso( + String callSign, + LoggedQsoBand band, + String locator + ) { + return ExternalLoggedQso.create(callSign, band, locator, "TEST").orElseThrow(); + } + + private static ChatMember member(String callSign) { + ChatMember member = new ChatMember(); + member.setCallSign(callSign); + return member; + } + + private static String winTestAddQsoPacket(String bandId, String callSign) { + return "ADDQSO: \"STN1\" \"\" \"STN1\" 1762202297 1440000 0 " + + bandId + " 0 0 0 2 2 \"" + callSign + + "\" \"599\" \"599001\" \"JO51UM\" \"\" \"\" 0 \"\" \"\" \"\" 44510"; + } +} diff --git a/src/test/java/kst4contest/controller/ExternalLoggerListenerValidationTest.java b/src/test/java/kst4contest/controller/ExternalLoggerListenerValidationTest.java new file mode 100644 index 00000000..e1fc723d --- /dev/null +++ b/src/test/java/kst4contest/controller/ExternalLoggerListenerValidationTest.java @@ -0,0 +1,61 @@ +package kst4contest.controller; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import kst4contest.model.ChatPreferences; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ExternalLoggerListenerValidationTest { + + @Mock + ChatController controller; + + @Mock + ChatPreferences preferences; + + @Mock + ThreadStatusCallback statusCallback; + + @BeforeEach + void configurePreferences() { + when(controller.getChatPreferences()).thenReturn(preferences); + } + + @Test + void ucxLogPacketWithoutCallsignIsDiscardedWithoutStoppingProcessing() { + when(preferences.isLogsynch_ucxUDPWkdCallListenerEnabled()).thenReturn(true); + ReadUDPbyUCXMessageThread listener = new ReadUDPbyUCXMessageThread( + 12060, controller, statusCallback); + String packet = "" + + "144 JO50AA" + + ""; + + assertDoesNotThrow(() -> listener.processUCXUDPMessage(packet)); + + verify(controller, never()).applyExternalLoggedQso(any()); + verify(controller, never()).getDbHandler(); + } + + @Test + void winTestPacketWithoutCallsignIsDiscardedWithoutStoppingProcessing() { + when(preferences.getStn_loginCallSignRaw()).thenReturn("DL0TEST"); + when(preferences.getLogsynch_wintestNetworkPort()).thenReturn(9871); + ReadUDPByWintestThread listener = new ReadUDPByWintestThread(controller, statusCallback); + String packet = "ADDQSO: \"STN1\" \"\" \"STN1\" 1762202297 1440000 0 12 0 0 0 2 2 " + + "\" \" \"599\" \"599001\" \"JO51UM\" \"\" \"\" 0 \"\" \"\" \"\" 44510"; + + assertDoesNotThrow(() -> listener.processWinTestMessage(packet)); + + verify(controller, never()).applyExternalLoggedQso(any()); + verify(controller, never()).getDbHandler(); + } +}