diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index 68a65d3..fb53f8c 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -827,8 +827,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList }); // Push sked to Win-Test via UDP if enabled - if (chatPreferences.isLogsynch_wintestNetworkSkedPushEnabled() - && chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) { + if (chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) { pushSkedToWinTest(sked); } } @@ -847,16 +846,40 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this); - // Get current frequency from QRG property (set by Win-Test STATUS or user) - double freqKHz = 144300.0; // fallback default - 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; + // 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. + 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()); + 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; } - } catch (NumberFormatException ignored) { } + } + + // 2. Use our own QRG (Win-Test STATUS or manually set by user) + if (freqKHz < 0) { + 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; + } + } catch (NumberFormatException ignored) { } + } + + // 3. Fallback + if (freqKHz < 0) { + freqKHz = 144300.0; + } // Build notes string with target locator/azimuth info like reference: [JO02OB - 279°] String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign()); @@ -883,6 +906,22 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList }, "WinTestSkedPush").start(); } + private ChatMember resolveSkedTargetMember(String targetCallsignRaw) { + if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) { + return null; + } + String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw); + synchronized (getLst_chatMemberList()) { + for (ChatMember member : getLst_chatMemberList()) { + if (member == null || member.getCallSignRaw() == null) continue; + if (normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) { + return member; + } + } + } + return null; + } + private String resolveSkedTargetLocator(String targetCallsignRaw) { if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) { return null; diff --git a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java index 5c5044d..1e082dd 100644 --- a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java +++ b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java @@ -224,9 +224,41 @@ public class ReadUDPByWintestThread extends Thread { } else { formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback } - this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG); + // Parse pass frequency from parts[8] if available + String formattedPassQRG = null; + if (parts.size() >= 9) { + try { + String passFreqRaw = parts.get(8); + 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); + } + } catch (Exception ignored) { + // parts[8] not a valid frequency, leave formattedPassQRG as null + } + } - System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG); + 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); + } + } + + System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG + + (formattedPassQRG != null ? ", passQrg=" + formattedPassQRG : "") + + ", syncActive=" + this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()); } catch (Exception e) { System.out.println("[WinTest] STATUS parsing error: " + e.getMessage()); } diff --git a/src/main/java/kst4contest/model/ChatPreferences.java b/src/main/java/kst4contest/model/ChatPreferences.java index d6b9b35..02cbacf 100644 --- a/src/main/java/kst4contest/model/ChatPreferences.java +++ b/src/main/java/kst4contest/model/ChatPreferences.java @@ -204,6 +204,8 @@ public class ChatPreferences { String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO + boolean logsynch_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet + boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet @@ -481,6 +483,22 @@ public class ChatPreferences { this.logsynch_wintestSkedMode = logsynch_wintestSkedMode; } + public boolean isLogsynch_wintestQrgSyncEnabled() { + return logsynch_wintestQrgSyncEnabled; + } + + public void setLogsynch_wintestQrgSyncEnabled(boolean logsynch_wintestQrgSyncEnabled) { + this.logsynch_wintestQrgSyncEnabled = logsynch_wintestQrgSyncEnabled; + } + + public boolean isLogsynch_wintestUsePassQrg() { + return logsynch_wintestUsePassQrg; + } + + public void setLogsynch_wintestUsePassQrg(boolean logsynch_wintestUsePassQrg) { + this.logsynch_wintestUsePassQrg = logsynch_wintestUsePassQrg; + } + public String getStn_loginLocatorSecondCat() { return stn_loginLocatorSecondCat; } @@ -1338,6 +1356,14 @@ public class ChatPreferences { logsynch_wintestSkedMode.setTextContent(this.logsynch_wintestSkedMode); logsynch.appendChild(logsynch_wintestSkedMode); + Element logsynch_wintestQrgSyncEnabled = doc.createElement("logsynch_wintestQrgSyncEnabled"); + logsynch_wintestQrgSyncEnabled.setTextContent(this.logsynch_wintestQrgSyncEnabled + ""); + logsynch.appendChild(logsynch_wintestQrgSyncEnabled); + + Element logsynch_wintestUsePassQrg = doc.createElement("logsynch_wintestUsePassQrg"); + logsynch_wintestUsePassQrg.setTextContent(this.logsynch_wintestUsePassQrg + ""); + logsynch.appendChild(logsynch_wintestUsePassQrg); + /** * trxSynchUCX @@ -1912,6 +1938,16 @@ public class ChatPreferences { logsynch_wintestSkedMode, "logsynch_wintestSkedMode"); + logsynch_wintestQrgSyncEnabled = getBoolean( + logsynchEl, + logsynch_wintestQrgSyncEnabled, + "logsynch_wintestQrgSyncEnabled"); + + logsynch_wintestUsePassQrg = getBoolean( + logsynchEl, + logsynch_wintestUsePassQrg, + "logsynch_wintestUsePassQrg"); + System.out.println( "[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled); System.out.println( diff --git a/src/main/java/kst4contest/view/Kst4ContestApplication.java b/src/main/java/kst4contest/view/Kst4ContestApplication.java index 758becd..bd73f61 100644 --- a/src/main/java/kst4contest/view/Kst4ContestApplication.java +++ b/src/main/java/kst4contest/view/Kst4ContestApplication.java @@ -3582,7 +3582,57 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL Menu fileMenu = new Menu("File"); - // create menuitems + // build "Connect to " label from saved preferences + ChatCategory mainCat = chatcontroller.getChatPreferences().getLoginChatCategoryMain(); + String connectLabel = "Connect to " + mainCat.getChatCategoryName(mainCat.getCategoryNumber()); + if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()) { + ChatCategory secCat = chatcontroller.getChatPreferences().getLoginChatCategorySecond(); + if (secCat != null) { + connectLabel += " & " + secCat.getChatCategoryName(secCat.getCategoryNumber()); + } + } + menuItemFileConnect = new MenuItem(connectLabel); + menuItemFileConnect.setDisable(false); + + if (chatcontroller.isConnectedAndLoggedIn() || chatcontroller.isConnectedAndNOTLoggedIn()) { + menuItemFileConnect.setDisable(true); + } + + menuItemFileConnect.setOnAction(event -> { + System.out.println("[Info] File menu: Connect clicked, using saved preferences"); + + String call = chatcontroller.getChatPreferences().getStn_loginCallSign(); + String pass = chatcontroller.getChatPreferences().getStn_loginPassword(); + + if (call == null || call.isBlank() || pass == null || pass.isBlank()) { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Cannot connect"); + alert.setHeaderText("Login credentials missing"); + alert.setContentText("Please configure your callsign and password in Settings first."); + alert.show(); + return; + } + + try { + chatcontroller.execute(); + + menuItemFileConnect.setDisable(true); + menuItemFileDisconnect.setDisable(false); + menuItemOptionsAwayBack.setDisable(false); + menuItemOptionsSetFrequencyAsName.setDisable(false); + + chatcontroller.setConnectedAndLoggedIn(true); + chatcontroller.setDisconnected(false); + + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Connection failed"); + alert.setContentText("Could not connect: " + e.getMessage()); + alert.show(); + } + }); + menuItemFileDisconnect = new MenuItem("Disconnect"); menuItemFileDisconnect.setDisable(true); @@ -3595,6 +3645,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL public void handle(ActionEvent event) { chatcontroller.disconnect(ApplicationConstants.DISCSTRING_DISCONNECTONLY); menuItemFileDisconnect.setDisable(true); + menuItemFileConnect.setDisable(false); } }); @@ -3607,6 +3658,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL }); // add menu items to menu + fileMenu.getItems().add(menuItemFileConnect); fileMenu.getItems().add(menuItemFileDisconnect); fileMenu.getItems().add(m10); @@ -4010,6 +4062,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL Scene clusterAndQSOMonScene; Scene settingsScene; + MenuItem menuItemFileConnect; MenuItem menuItemFileDisconnect; MenuItem menuItemOptionsAwayBack; @@ -4170,10 +4223,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL timer_updatePrivatemessageTable.purge(); timer_updatePrivatemessageTable.cancel(); - chatcontroller.disconnect("CLOSEALL"); + + try { + chatcontroller.disconnect("CLOSEALL"); + } catch (Exception e) { + System.out.println("[Main.java, Warning:] Exception during disconnect: " + e.getMessage()); + } // Platform.exit(); - + System.exit(0); } private Queue musicList = new LinkedList(); @@ -6273,11 +6331,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL } }); + boolean isSecondChatEnabled = this.chatcontroller.getChatPreferences().isLoginToSecondChatEnabled(); Label lblNameSecondCat = new Label("Name in Chat 2:"); - lblNameSecondCat.setVisible(false); + lblNameSecondCat.setVisible(isSecondChatEnabled); + lblNameSecondCat.setDisable(!isSecondChatEnabled); TextField txtFldNameInChatSecondCat = new TextField(this.chatcontroller.getChatPreferences().getStn_loginNameSecondCat()); txtFldNameInChatSecondCat.setFocusTraversable(false); - txtFldNameInChatSecondCat.setVisible(false); + txtFldNameInChatSecondCat.setVisible(isSecondChatEnabled); + txtFldNameInChatSecondCat.setDisable(!isSecondChatEnabled); txtFldNameInChatSecondCat.textProperty().addListener(new ChangeListener() { @@ -6397,11 +6458,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL CheckBox station_chkBxEnableSecondChat = new CheckBox("2nd Chat: "); - station_chkBxEnableSecondChat.setSelected(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()); + boolean isSecondChatEnabledForCheckbox = chatcontroller.getChatPreferences().isLoginToSecondChatEnabled(); + station_chkBxEnableSecondChat.setSelected(isSecondChatEnabledForCheckbox); - stn_choiceBxChatChategorySecond.setDisable(true); + stn_choiceBxChatChategorySecond.setDisable(!isSecondChatEnabledForCheckbox); station_chkBxEnableSecondChat.selectedProperty().addListener(new ChangeListener() { @Override public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { @@ -6431,12 +6493,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL } }); - if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()) { - stn_choiceBxChatChategorySecond.setVisible(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()); - stn_choiceBxChatChategorySecond.setDisable(!chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()); - txtFldNameInChatSecondCat.setVisible(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()); - } TextField txtFldstn_antennaBeamWidthDeg = new TextField(this.chatcontroller.getChatPreferences().getStn_antennaBeamWidthDeg() + ""); txtFldstn_antennaBeamWidthDeg.setFocusTraversable(false); @@ -6882,15 +6939,24 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL grdPnlLog.add(lblUDPByWintest, 0, 8); grdPnlLog.add(txtFldUDPPortforWintest, 1, 8); - // --- Win-Test SKED push settings --- - Label lblEnableSkedPush = new Label("Push SKEDs to Win-Test via UDP (ADDSKED)"); - CheckBox chkBxEnableSkedPush = new CheckBox(); - chkBxEnableSkedPush.setSelected( - this.chatcontroller.getChatPreferences().isLogsynch_wintestNetworkSkedPushEnabled() + // --- QRG sync from Win-Test STATUS --- + Label lblWtQrgSync = new Label("Win-Test STATUS QRG Sync (updates own QRG from Win-Test transceiver frequency)"); + CheckBox chkBxWtQrgSync = new CheckBox(); + chkBxWtQrgSync.setSelected( + this.chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled() ); - chkBxEnableSkedPush.selectedProperty().addListener((obs, oldVal, newVal) -> { - chatcontroller.getChatPreferences().setLogsynch_wintestNetworkSkedPushEnabled(newVal); - System.out.println("[Main.java, Info]: Win-Test SKED push enabled: " + newVal); + chkBxWtQrgSync.selectedProperty().addListener((obs, oldVal, newVal) -> { + chatcontroller.getChatPreferences().setLogsynch_wintestQrgSyncEnabled(newVal); + System.out.println("[Main.java, Info]: Win-Test QRG sync enabled: " + newVal); + }); + Label lblWtUsePassQrg = new Label("Use pass frequency from Win-Test STATUS (instead of own QRG)"); + CheckBox chkBxWtUsePassQrg = new CheckBox(); + chkBxWtUsePassQrg.setSelected( + this.chatcontroller.getChatPreferences().isLogsynch_wintestUsePassQrg() + ); + chkBxWtUsePassQrg.selectedProperty().addListener((obs, oldVal, newVal) -> { + chatcontroller.getChatPreferences().setLogsynch_wintestUsePassQrg(newVal); + System.out.println("[Main.java, Info]: Win-Test use pass QRG: " + newVal); }); Label lblWtStationName = new Label("KST station name in Win-Test network (src of SKED packets)"); @@ -6935,13 +7001,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL } }); - grdPnlLog.add(lblEnableSkedPush, 0, 9); - grdPnlLog.add(chkBxEnableSkedPush, 1, 9); - - grdPnlLog.add(lblWtStationName, 0, 11); - grdPnlLog.add(txtFldWtStationName, 1, 11); - grdPnlLog.add(lblWtStationFilter, 0, 12); - grdPnlLog.add(txtFldWtStationFilter, 1, 12); + grdPnlLog.add(lblWtStationName, 0, 9); + grdPnlLog.add(txtFldWtStationName, 1, 9); // Auto-detect subnet broadcast if preference is still the default String currentBroadcast = this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress(); @@ -6959,8 +7020,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL // Re-read (may have been auto-detected) txtFldWtBroadcastAddr.setText(this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress()); - grdPnlLog.add(lblWtBroadcastAddr, 0, 13); - grdPnlLog.add(txtFldWtBroadcastAddr, 1, 13); + grdPnlLog.add(lblWtBroadcastAddr, 0, 10); + grdPnlLog.add(txtFldWtBroadcastAddr, 1, 10); VBox vbxLog = new VBox(); vbxLog.setPadding(new Insets(10, 10, 10, 10)); @@ -7040,6 +7101,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL grdPnltrx.add(lblEnableTRXMsgbyUCX, 0, 1); grdPnltrx.add(chkBxEnableTRXMsgbyUCX, 1, 1); + grdPnltrx.add(generateLabeledSeparator(100, "Win-Test TRX sync"), 0, 2, 2, 1); + grdPnltrx.add(lblWtQrgSync, 0, 3); + grdPnltrx.add(chkBxWtQrgSync, 1, 3); + grdPnltrx.add(lblWtUsePassQrg, 0, 4); + grdPnltrx.add(chkBxWtUsePassQrg, 1, 4); + grdPnltrx.add(lblWtStationFilter, 0, 5); + grdPnltrx.add(txtFldWtStationFilter, 1, 5); + VBox vbxTRXSynch = new VBox(); vbxTRXSynch.setPadding(new Insets(10, 10, 10, 10)); vbxTRXSynch.getChildren().addAll(grdPnltrx); @@ -8124,6 +8193,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL else if (chatcontroller.isConnectedAndLoggedIn()) { btnOptionspnlDisconnectOnly.setDisable(false); menuItemFileDisconnect.setDisable(false); + menuItemFileConnect.setDisable(true); menuItemOptionsAwayBack.setDisable(false); } @@ -8147,6 +8217,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL txtFldstn_maxQRBDefault.setDisable(false); menuItemOptionsSetFrequencyAsName.setDisable(true); menuItemOptionsAwayBack.setDisable(true); + menuItemFileConnect.setDisable(false); station_chkBxEnableSecondChat.setDisable(false); stn_choiceBxChatChategorySecond.setDisable(false); } @@ -8185,6 +8256,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL btnOptionspnlDisconnectOnly.setDisable(false); menuItemFileDisconnect.setDisable(false); + menuItemFileConnect.setDisable(true); menuItemOptionsAwayBack.setDisable(false); menuItemOptionsSetFrequencyAsName.setDisable(false);