From 5fc0e7d02d312d1e1d2b4a4e07c4932aed3527e0 Mon Sep 17 00:00:00 2001 From: Philipp Wagner Date: Sat, 4 Apr 2026 21:49:58 +0200 Subject: [PATCH] Win-Test Fix QRG Sked --- .../controller/ChatController.java | 69 ++++++++++++++++--- .../MessageBusManagementThread.java | 7 ++ .../controller/ReadUDPByWintestThread.java | 50 +++++++------- .../view/Kst4ContestApplication.java | 56 ++++++++------- 4 files changed, 119 insertions(+), 63 deletions(-) diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index fb53f8c..ca72731 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -810,6 +810,24 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList private final Map lastInboundCategoryByCallSignRaw = new java.util.concurrent.ConcurrentHashMap<>(); + /** Tracks the last time WE sent a message containing a QRG to a specific callsign (UPPERCASE). + * Compared against knownActiveBands.timestampEpoch to decide whose QRG to use in a SKED. */ + private final Map lastSentQRGToCallsign = + new java.util.concurrent.ConcurrentHashMap<>(); + + /** Call this whenever we send a PM to {@code receiverCallsign} that contains our QRG. */ + public void recordOutboundQRG(String receiverCallsign) { + if (receiverCallsign == null) return; + lastSentQRGToCallsign.put(receiverCallsign.trim().toUpperCase(), System.currentTimeMillis()); + System.out.println("[ChatController] Recorded outbound QRG to: " + receiverCallsign); + } + + /** Returns epoch-ms of when we last sent our QRG to this callsign, or 0 if never. */ + public long getLastSentQRGTimestamp(String callsign) { + if (callsign == null) return 0L; + return lastSentQRGToCallsign.getOrDefault(callsign.trim().toUpperCase(), 0L); + } + private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15); private ScheduledExecutorService scoreScheduler; private final StationMetricsService stationMetricsService = new StationMetricsService(); @@ -847,36 +865,65 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this); // Frequency resolution: - // If the other station mentioned their QRG in a recent message (they proposed it), - // use their frequency. Otherwise use our own QRG (we proposed it or no QRG was - // exchanged). Only consider their QRG fresh if mentioned within the last 60 minutes, - // to avoid picking up a stale entry from a completely different earlier conversation. + // Compare WHO sent a QRG most recently in the PM conversation: + // - OM sent their QRG last → use OM's Last Known QRG (ChatMember.frequency) + // - WE sent our QRG last → use our own Win-Test QRG (MYQRG) + // Fallback chain if no timestamps exist: OM's Last Known QRG → hardcoded default double freqKHz = -1.0; final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes - // 1. Other station mentioned their QRG in a recent message for this band ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign()); + + // Collect timestamps: when did the OM last mention their QRG? When did WE last send ours? + long omLastQRGTimestamp = 0L; + double omLastQRGMhz = 0.0; if (targetMember != null && sked.getBand() != null) { ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand()); if (fi != null && fi.frequency > 0 && (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) { - freqKHz = fi.frequency; + omLastQRGTimestamp = fi.timestampEpoch; + omLastQRGMhz = fi.frequency; } } + long ourLastQRGTimestamp = getLastSentQRGTimestamp(sked.getTargetCallsign()); - // 2. Use our own QRG (Win-Test STATUS or manually set by user) - if (freqKHz < 0) { + // Decision: who was more recent? + if (omLastQRGTimestamp > 0 && omLastQRGTimestamp >= ourLastQRGTimestamp) { + // OM mentioned their QRG MORE RECENTLY (or at same time) → use their QRG + freqKHz = omLastQRGMhz * 1000.0; + System.out.println("[ChatController] SKED freq: OM sent last → " + + omLastQRGMhz + " MHz → " + freqKHz + " kHz"); + + } else if (ourLastQRGTimestamp > 0) { + // WE sent our QRG more recently → use our Win-Test QRG try { String qrgStr = chatPreferences.getMYQRGFirstCat().get(); if (qrgStr != null && !qrgStr.isBlank()) { - // QRG is in display format like "144.300.00" – strip dots → "14430000" → / 100 → 144300.0 kHz String cleaned = qrgStr.trim().replace(".", ""); - freqKHz = Double.parseDouble(cleaned) / 100.0; + double parsed = Double.parseDouble(cleaned) / 100.0; + if (parsed > 50000) { + freqKHz = parsed; + System.out.println("[ChatController] SKED freq: WE sent last → " + + freqKHz + " kHz (raw: " + qrgStr + ")"); + } } } catch (NumberFormatException ignored) { } } - // 3. Fallback + // Fallback A: OM's Last Known QRG from KST field (if no PM QRG exchange found at all) + if (freqKHz < 0 && targetMember != null) { + try { + String memberQrg = targetMember.getFrequency().get(); + if (memberQrg != null && !memberQrg.isBlank()) { + double mhz = Double.parseDouble(memberQrg.trim()); + freqKHz = mhz * 1000.0; + System.out.println("[ChatController] SKED freq: fallback Last Known QRG → " + + mhz + " MHz → " + freqKHz + " kHz"); + } + } catch (NumberFormatException ignored) { } + } + + // Fallback B: hardcoded default if (freqKHz < 0) { freqKHz = 144300.0; } diff --git a/src/main/java/kst4contest/controller/MessageBusManagementThread.java b/src/main/java/kst4contest/controller/MessageBusManagementThread.java index 87356b6..8d8cb83 100644 --- a/src/main/java/kst4contest/controller/MessageBusManagementThread.java +++ b/src/main/java/kst4contest/controller/MessageBusManagementThread.java @@ -962,6 +962,13 @@ public class MessageBusManagementThread extends Thread { .setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage); this.client.getLst_globalChatMessageList().add(0,newMessageArrived); + // If our message contained a frequency (e.g. "QRG is: 144.375"), record that + // WE sent our QRG to this OM – used by SKED frequency resolution. + if (originalMessage != null && newMessageArrived.getReceiver() != null + && originalMessage.matches(".*\\b\\d{3,5}[.,]\\d{1,3}.*")) { + this.client.recordOutboundQRG(newMessageArrived.getReceiver().getCallSign()); + } + // if you sent the message to another station, it will be sorted in to // the "to me message list" with modified messagetext, added rxers callsign diff --git a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java index 1e082dd..f53e1ff 100644 --- a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java +++ b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java @@ -1,5 +1,6 @@ package kst4contest.controller; +import javafx.application.Platform; import kst4contest.ApplicationConstants; import kst4contest.model.ChatMember; import kst4contest.model.ThreadStateMessage; @@ -75,9 +76,10 @@ public class ReadUDPByWintestThread extends Thread { socket = new DatagramSocket(null); //first init with null, then make ready for reuse socket.setReuseAddress(true); // socket = new DatagramSocket(PORT); - socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_wintestNetworkPort())); + int boundPort = client.getChatPreferences().getLogsynch_wintestNetworkPort(); + socket.bind(new InetSocketAddress(boundPort)); socket.setSoTimeout(3000); - System.out.println("[WinTest UDP listener] started at port: " + PORT); + System.out.println("[WinTest UDP listener] started at port: " + boundPort); } catch (SocketException e) { e.printStackTrace(); return; @@ -224,36 +226,38 @@ public class ReadUDPByWintestThread extends Thread { } else { formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback } - // Parse pass frequency from parts[8] if available + // Parse pass frequency from parts[11] if available (WT STATUS format) String formattedPassQRG = null; - if (parts.size() >= 9) { + if (parts.size() > 11) { try { - String passFreqRaw = parts.get(8); + String passFreqRaw = parts.get(11); double passFreqFloat = Integer.parseInt(passFreqRaw) / 10.0; - long passFreqHzTimes100 = Math.round(passFreqFloat * 100.0); - String passHzStr = String.valueOf(passFreqHzTimes100); - if (passHzStr.length() == 8) { - formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 3), passHzStr.substring(3, 6), passHzStr.substring(6, 8)); - } else if (passHzStr.length() == 9) { - formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 4), passHzStr.substring(4, 7), passHzStr.substring(7, 9)); - } else if (passHzStr.length() == 7) { - formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 2), passHzStr.substring(2, 5), passHzStr.substring(5, 7)); - } else if (passHzStr.length() == 6) { - formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 1), passHzStr.substring(1, 4), passHzStr.substring(4, 6)); - } else { - formattedPassQRG = String.format(Locale.US, "%.1f", passFreqFloat); + if (passFreqFloat > 100) { // Must be a valid radio frequency (> 100 kHz), protects against parsing boolean flag tokens + long passFreqHzTimes100 = Math.round(passFreqFloat * 100.0); + String passHzStr = String.valueOf(passFreqHzTimes100); + if (passHzStr.length() == 8) { + formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 3), passHzStr.substring(3, 6), passHzStr.substring(6, 8)); + } else if (passHzStr.length() == 9) { + formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 4), passHzStr.substring(4, 7), passHzStr.substring(7, 9)); + } else if (passHzStr.length() == 7) { + formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 2), passHzStr.substring(2, 5), passHzStr.substring(5, 7)); + } else if (passHzStr.length() == 6) { + formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 1), passHzStr.substring(1, 4), passHzStr.substring(4, 6)); + } else { + formattedPassQRG = String.format(Locale.US, "%.1f", passFreqFloat); + } } } catch (Exception ignored) { - // parts[8] not a valid frequency, leave formattedPassQRG as null + // parts[11] not a valid frequency, leave formattedPassQRG as null } } if (this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) { - if (this.client.getChatPreferences().isLogsynch_wintestUsePassQrg() && formattedPassQRG != null) { - this.client.getChatPreferences().getMYQRGFirstCat().set(formattedPassQRG); - } else { - this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG); - } + final String qrgToSet = (this.client.getChatPreferences().isLogsynch_wintestUsePassQrg() && formattedPassQRG != null) + ? formattedPassQRG + : formattedQRG; + // JavaFX StringProperty must be updated on the FX Application Thread + Platform.runLater(() -> this.client.getChatPreferences().getMYQRGFirstCat().set(qrgToSet)); } System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG diff --git a/src/main/java/kst4contest/view/Kst4ContestApplication.java b/src/main/java/kst4contest/view/Kst4ContestApplication.java index bd73f61..ca518f5 100644 --- a/src/main/java/kst4contest/view/Kst4ContestApplication.java +++ b/src/main/java/kst4contest/view/Kst4ContestApplication.java @@ -6724,7 +6724,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL grdPnlStation_bands.add(settings_chkbx_QRV3400, 1, 2); grdPnlStation_bands.add(settings_chkbx_QRV5600, 2, 2); grdPnlStation_bands.add(settings_chkbx_QRV10G, 0, 3); - grdPnlStation_bands.setMaxWidth(555.0); grdPnlStation_bands.setStyle(" -fx-border-color: lightgray;\n" + " -fx-vgap: 5;\n" + @@ -6948,6 +6947,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL chkBxWtQrgSync.selectedProperty().addListener((obs, oldVal, newVal) -> { chatcontroller.getChatPreferences().setLogsynch_wintestQrgSyncEnabled(newVal); System.out.println("[Main.java, Info]: Win-Test QRG sync enabled: " + newVal); + boolean anyActive = chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled() || newVal; + if (!anyActive) { + txt_ownqrgMainCategory.textProperty().unbind(); + txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by hand (watch prefs!)")); + } else { + txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat()); + txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)")); + } }); Label lblWtUsePassQrg = new Label("Use pass frequency from Win-Test STATUS (instead of own QRG)"); CheckBox chkBxWtUsePassQrg = new CheckBox(); @@ -7056,45 +7063,31 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL chkBxEnableTRXMsgbyUCX.selectedProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { -// chk2.setSelected(!newValue); - if (!newValue) { - chatcontroller.getChatPreferences() - .setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected()); + chatcontroller.getChatPreferences().setTrxSynch_ucxLogUDPListenerEnabled(newValue); + boolean anyActive = newValue || chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled(); + if (!anyActive) { txt_ownqrgMainCategory.textProperty().unbind(); txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by hand (watch prefs!)")); System.out.println("[Main.java, Info]: MYQRG will be changed only by User input"); - System.out.println("[Main.java, Info]: setted the trx-frequency updated by ucxlog to: " - + chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()); - } else { - chatcontroller.getChatPreferences() - .setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected()); txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat()); txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)")); - System.out.println("[Main.java, Info]: setted the trx-frequency updated by ucxlog to: " - + chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()); } } }); - // Thats the default behaviour of the myqrg textfield - if (this.chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()) { + // Unconditionally add listener to manually sync the textfield input to the button + // (this listener also fires correctly when the value is updated by the binding) + txt_ownqrgMainCategory.textProperty().addListener((observable, oldValue, newValue) -> { + MYQRGButton.textProperty().set(newValue); + }); + + // That's the default behaviour of the myqrg textfield + if (this.chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled() || this.chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) { txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)")); - txt_ownqrgMainCategory.textProperty().bind(this.chatcontroller.getChatPreferences().getMYQRGFirstCat());// TODO: Bind darf nur - // gemacht werden, wenn - // ucxlog-Frequenznachrichten - // ausgewerttet werden! -// System.out.println("[Main.java, Info]: MYQRG will be changed only by UCXListener"); + txt_ownqrgMainCategory.textProperty().bind(this.chatcontroller.getChatPreferences().getMYQRGFirstCat()); } else { txt_ownqrgMainCategory.setTooltip(new Tooltip("enter your cq qrg here")); -// System.out.println("[Main.java, Info]: MYQRG will be changed only by User input"); - txt_ownqrgMainCategory.textProperty().addListener((observable, oldValue, newValue) -> { - - System.out.println( - "[Main.java, Info]: MYQRG Text changed from " + oldValue + " to " + newValue + " by hand"); - MYQRGButton.textProperty().set(newValue); - }); - } grdPnltrx.add(generateLabeledSeparator(100, "Receive UCXLog TRX info"), 0, 0, 2, 1); @@ -8223,8 +8216,13 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL } }); - btnOptionspnlConnect = new Button("Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain() - .getChatCategoryName(choiceBxChatChategory.getSelectionModel().getSelectedItem().getCategoryNumber())); + String btnText = "Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain() + .getChatCategoryName(choiceBxChatChategory.getSelectionModel().getSelectedItem().getCategoryNumber()); + ChatCategory secCat = chatcontroller.getChatPreferences().getLoginChatCategorySecond(); + if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled() && secCat != null) { + btnText += " & " + secCat.getChatCategoryName(secCat.getCategoryNumber()); + } + btnOptionspnlConnect = new Button(btnText); btnOptionspnlConnect.setOnAction(new EventHandler() { @Override public void handle(ActionEvent event) {