diff --git a/pom.xml b/pom.xml index cc71a1e..5384cb4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.x08 praktiKST - 1.40.0-nightly + 1.41.0-nightly praktiKST diff --git a/src/main/java/kst4contest/controller/ChatController.java b/src/main/java/kst4contest/controller/ChatController.java index f5ae14a..68a65d3 100644 --- a/src/main/java/kst4contest/controller/ChatController.java +++ b/src/main/java/kst4contest/controller/ChatController.java @@ -852,17 +852,30 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList try { String qrgStr = chatPreferences.getMYQRGFirstCat().get(); if (qrgStr != null && !qrgStr.isBlank()) { - freqKHz = Double.parseDouble(qrgStr.trim()); + // 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) { } - // Build notes string with locator/azimuth info + // Build notes string with target locator/azimuth info like reference: [JO02OB - 279°] + String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign()); String notes = "sked via KST4Contest"; - if (sked.getTargetAzimuth() > 0) { + if (targetLocator != null && !targetLocator.isBlank() && sked.getTargetAzimuth() > 0) { + notes = String.format("[%s - %.0f°] %s", targetLocator, sked.getTargetAzimuth(), notes); + } else if (targetLocator != null && !targetLocator.isBlank()) { + notes = String.format("[%s] %s", targetLocator, notes); + } else if (sked.getTargetAzimuth() > 0) { notes = String.format("[%.0f°] %s", sked.getTargetAzimuth(), notes); } - sender.pushSkedToWinTest(sked, freqKHz, notes); + // Determine mode: -1 = auto-detect, 0 = CW, 1 = SSB + String modeStr = chatPreferences.getLogsynch_wintestSkedMode(); + int modeOverride = -1; // AUTO + if ("CW".equalsIgnoreCase(modeStr)) modeOverride = 0; + else if ("SSB".equalsIgnoreCase(modeStr)) modeOverride = 1; + + sender.pushSkedToWinTest(sked, freqKHz, notes, modeOverride); } catch (Exception e) { System.out.println("[ChatController] Error pushing sked to Win-Test: " + e.getMessage()); e.printStackTrace(); @@ -870,6 +883,27 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList }, "WinTestSkedPush").start(); } + private String resolveSkedTargetLocator(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)) continue; + + String locator = member.getQra(); + if (locator != null && !locator.isBlank()) { + return locator.trim().toUpperCase(Locale.ROOT); + } + } + } + + return null; + } + public StationMetricsService getStationMetricsService() { return stationMetricsService; } diff --git a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java index 70c71f4..5c5044d 100644 --- a/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java +++ b/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java @@ -204,7 +204,26 @@ public class ReadUDPByWintestThread extends Thread { mode = "cw"; } - String formattedQRG = String.format(Locale.US, "%.1f", freqFloat); + // Format as MMM.KKK.HH display format (e.g. 144.300.00) consistent with UCX thread + // freqFloat is in kHz (e.g. 144300.0), convert to Hz-string for formatting + long freqHzTimes100 = Math.round(freqFloat * 100.0); // e.g. 14430000 + String hzStr = String.valueOf(freqHzTimes100); + String formattedQRG; + if (hzStr.length() == 8) { + // 144MHz range: 14430000 -> 144.300.00 + formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 3), hzStr.substring(3, 6), hzStr.substring(6, 8)); + } else if (hzStr.length() == 9) { + // 1296MHz range: 129600000 -> 1296.000.00 + formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 4), hzStr.substring(4, 7), hzStr.substring(7, 9)); + } else if (hzStr.length() == 7) { + // 70MHz range: 7010000 -> 70.100.00 + formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 2), hzStr.substring(2, 5), hzStr.substring(5, 7)); + } else if (hzStr.length() == 6) { + // 50MHz range: 5030000 but 6 digits: 503000 -> 5.030.00 + formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 1), hzStr.substring(1, 4), hzStr.substring(4, 6)); + } else { + formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback + } this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG); System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG); diff --git a/src/main/java/kst4contest/controller/WinTestSkedSender.java b/src/main/java/kst4contest/controller/WinTestSkedSender.java index 8366057..338909d 100644 --- a/src/main/java/kst4contest/controller/WinTestSkedSender.java +++ b/src/main/java/kst4contest/controller/WinTestSkedSender.java @@ -46,10 +46,10 @@ public class WinTestSkedSender { * @param frequencyKHz current operating frequency in kHz (e.g. 144321.0) * @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST") */ - public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes) { + public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) { try { sendLockSked(); - sendAddSked(sked, frequencyKHz, notes); + sendAddSked(sked, frequencyKHz, notes, modeOverride); sendUnlockSked(); reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false); @@ -95,7 +95,7 @@ public class WinTestSkedSender { * Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch). * The C# code adds 60 seconds to compensate. */ - private void sendAddSked(ContestSked sked, double frequencyKHz, String notes) throws Exception { + private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception { // Win-Test timestamp: epoch seconds with 60s offset long epochSeconds = sked.getSkedTimeEpoch() / 1000; long wtTimestamp = epochSeconds + 60; @@ -106,9 +106,13 @@ public class WinTestSkedSender { // Win-Test band ID int bandId = toWinTestBandId(sked.getBand()); - // Mode: 0 = CW, 1 = SSB. Default to CW; detect SSB from frequency. - int mode = (frequencyKHz > 144_000 || isInSsbSegment(frequencyKHz)) ? 0 : 0; - // Simple heuristic: could be refined with actual mode info later + // Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB + int mode; + if (modeOverride >= 0) { + mode = modeOverride; + } else { + mode = isInSsbSegment(frequencyKHz) ? 1 : 0; + } String data = wtTimestamp + " " + freqTenthKHz @@ -167,9 +171,10 @@ public class WinTestSkedSender { * A more complete implementation would check actual mode from Win-Test STATUS. */ private boolean isInSsbSegment(double frequencyKHz) { - // Example: 144.300+ is typically SSB on 2m - if (frequencyKHz >= 144.300 && frequencyKHz <= 144.400) return true; - if (frequencyKHz >= 432.200 && frequencyKHz <= 432.400) return true; + // SSB segments (kHz ranges) + if (frequencyKHz >= 144300 && frequencyKHz <= 144399) return true; // 2m SSB + if (frequencyKHz >= 432200 && frequencyKHz <= 432399) return true; // 70cm SSB + if (frequencyKHz >= 1296200 && frequencyKHz <= 1296399) return true; // 23cm SSB return false; } diff --git a/src/main/java/kst4contest/model/ChatPreferences.java b/src/main/java/kst4contest/model/ChatPreferences.java index a8ff831..d6b9b35 100644 --- a/src/main/java/kst4contest/model/ChatPreferences.java +++ b/src/main/java/kst4contest/model/ChatPreferences.java @@ -203,6 +203,7 @@ public class ChatPreferences { boolean logsynch_wintestNetworkListenerEnabled = true; // default true = bisheriges Verhalten 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 @@ -472,6 +473,14 @@ public class ChatPreferences { this.logsynch_wintestNetworkSkedPushEnabled = logsynch_wintestNetworkSkedPushEnabled; } + public String getLogsynch_wintestSkedMode() { + return logsynch_wintestSkedMode; + } + + public void setLogsynch_wintestSkedMode(String logsynch_wintestSkedMode) { + this.logsynch_wintestSkedMode = logsynch_wintestSkedMode; + } + public String getStn_loginLocatorSecondCat() { return stn_loginLocatorSecondCat; } @@ -1325,6 +1334,10 @@ public class ChatPreferences { logsynch_wintestNetworkSkedPushEnabled.setTextContent(this.logsynch_wintestNetworkSkedPushEnabled + ""); logsynch.appendChild(logsynch_wintestNetworkSkedPushEnabled); + Element logsynch_wintestSkedMode = doc.createElement("logsynch_wintestSkedMode"); + logsynch_wintestSkedMode.setTextContent(this.logsynch_wintestSkedMode); + logsynch.appendChild(logsynch_wintestSkedMode); + /** * trxSynchUCX @@ -1894,6 +1907,11 @@ public class ChatPreferences { logsynch_wintestNetworkSkedPushEnabled, "logsynch_wintestNetworkSkedPushEnabled"); + logsynch_wintestSkedMode = getText( + logsynchEl, + logsynch_wintestSkedMode, + "logsynch_wintestSkedMode"); + 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 0b9578e..eab53b7 100644 --- a/src/main/java/kst4contest/view/Kst4ContestApplication.java +++ b/src/main/java/kst4contest/view/Kst4ContestApplication.java @@ -391,6 +391,22 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL ChoiceBox cbSkedMinutes = new ChoiceBox<>(FXCollections.observableArrayList(2, 3, 4, 5, 6,7,8,9, 10,11,12,13,14, 15, 20)); cbSkedMinutes.getSelectionModel().select(Integer.valueOf(5)); + ChoiceBox cbSkedMode = new ChoiceBox<>(FXCollections.observableArrayList("AUTO", "SSB", "CW")); + String configuredSkedMode = this.chatcontroller.getChatPreferences().getLogsynch_wintestSkedMode(); + if (configuredSkedMode == null || configuredSkedMode.isBlank()) { + configuredSkedMode = "AUTO"; + } + String configuredSkedModeUpper = configuredSkedMode.trim().toUpperCase(java.util.Locale.ROOT); + if (!"AUTO".equals(configuredSkedModeUpper) + && !"SSB".equals(configuredSkedModeUpper) + && !"CW".equals(configuredSkedModeUpper)) { + configuredSkedModeUpper = "AUTO"; + } + cbSkedMode.setValue(configuredSkedModeUpper); + cbSkedMode.setTooltip(new Tooltip("Mode for Win-Test ADDSKED packets")); + cbSkedMode.setOnAction(e -> + chatcontroller.getChatPreferences().setLogsynch_wintestSkedMode(cbSkedMode.getValue())); + ChoiceBox cbReminderOffsets = new ChoiceBox<>(FXCollections.observableArrayList("2+1", "5+2+1", "10+5+2+1")); cbReminderOffsets.getSelectionModel().select("2+1"); @@ -403,6 +419,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL ChatMember sel = chatcontroller.getScoreService().selectedChatMemberProperty().get(); if (sel == null) return; + if (cbSkedMode.getValue() != null) { + chatcontroller.getChatPreferences().setLogsynch_wintestSkedMode(cbSkedMode.getValue()); + } + int minutes = cbSkedMinutes.getValue() == null ? 5 : cbSkedMinutes.getValue(); long skedTime = System.currentTimeMillis() + minutes * 60_000L; @@ -425,6 +445,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL new Label("Sked in"), cbSkedMinutes, new Label("min"), + new Label("Mode"), + cbSkedMode, btnCreateSked, chkPmReminders, cbReminderOffsets @@ -6860,6 +6882,86 @@ 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() + ); + chkBxEnableSkedPush.selectedProperty().addListener((obs, oldVal, newVal) -> { + chatcontroller.getChatPreferences().setLogsynch_wintestNetworkSkedPushEnabled(newVal); + System.out.println("[Main.java, Info]: Win-Test SKED push enabled: " + newVal); + }); + + Label lblWtStationName = new Label("KST station name in Win-Test network (src of SKED packets)"); + TextField txtFldWtStationName = new TextField( + this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkStationNameOfKST() + ); + txtFldWtStationName.setFocusTraversable(false); + txtFldWtStationName.focusedProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal) { // focus lost + chatcontroller.getChatPreferences() + .setLogsynch_wintestNetworkStationNameOfKST(txtFldWtStationName.getText().trim()); + System.out.println("[Main.java, Info]: Win-Test KST station name set to: " + + txtFldWtStationName.getText().trim()); + } + }); + + Label lblWtStationFilter = new Label("Win-Test station name filter (e.g. STN1, empty = accept all)"); + TextField txtFldWtStationFilter = new TextField( + this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkStationNameOfWintestClient1() + ); + txtFldWtStationFilter.setFocusTraversable(false); + txtFldWtStationFilter.focusedProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal) { + chatcontroller.getChatPreferences() + .setLogsynch_wintestNetworkStationNameOfWintestClient1(txtFldWtStationFilter.getText().trim()); + System.out.println("[Main.java, Info]: Win-Test station filter set to: " + + txtFldWtStationFilter.getText().trim()); + } + }); + + Label lblWtBroadcastAddr = new Label("UDP broadcast address for Win-Test (default = internet interface broadcast)"); + TextField txtFldWtBroadcastAddr = new TextField( + this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress() + ); + txtFldWtBroadcastAddr.setFocusTraversable(false); + txtFldWtBroadcastAddr.focusedProperty().addListener((obs, oldVal, newVal) -> { + if (!newVal) { + chatcontroller.getChatPreferences() + .setLogsynch_wintestNetworkBroadcastAddress(txtFldWtBroadcastAddr.getText().trim()); + System.out.println("[Main.java, Info]: Win-Test broadcast address set to: " + + txtFldWtBroadcastAddr.getText().trim()); + } + }); + + 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); + + // Auto-detect subnet broadcast if preference is still the default + String currentBroadcast = this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress(); + if ("255.255.255.255".equals(currentBroadcast)) { + try { + String detected = detectPreferredWintestBroadcastAddress(); + if (detected != null && !detected.isBlank()) { + this.chatcontroller.getChatPreferences().setLogsynch_wintestNetworkBroadcastAddress(detected); + System.out.println("[Main.java, Info]: Auto-detected WT broadcast: " + detected); + } + } catch (Exception ex) { + System.out.println("[Main.java, Warning]: Could not auto-detect broadcast: " + ex.getMessage()); + } + } + // Re-read (may have been auto-detected) + txtFldWtBroadcastAddr.setText(this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress()); + + grdPnlLog.add(lblWtBroadcastAddr, 0, 13); + grdPnlLog.add(txtFldWtBroadcastAddr, 1, 13); + VBox vbxLog = new VBox(); vbxLog.setPadding(new Insets(10, 10, 10, 10)); vbxLog.getChildren().addAll(grdPnlLog); @@ -8525,6 +8627,75 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL } } + private String detectPreferredWintestBroadcastAddress() { + String internetRouteBroadcast = detectInternetRouteBroadcastAddress(); + if (internetRouteBroadcast != null && !internetRouteBroadcast.isBlank()) { + return internetRouteBroadcast; + } + return detectFirstUsableBroadcastAddress(); + } + + private String detectInternetRouteBroadcastAddress() { + java.net.DatagramSocket routeProbe = null; + try { + routeProbe = new java.net.DatagramSocket(); + routeProbe.connect(java.net.InetAddress.getByName("8.8.8.8"), 53); + + java.net.InetAddress localAddress = routeProbe.getLocalAddress(); + if (localAddress == null || localAddress.isAnyLocalAddress() || localAddress.isLoopbackAddress()) { + return null; + } + + java.net.NetworkInterface networkInterface = java.net.NetworkInterface.getByInetAddress(localAddress); + if (networkInterface == null || !networkInterface.isUp()) { + return null; + } + + for (java.net.InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) { + if (!(interfaceAddress.getAddress() instanceof java.net.Inet4Address)) { + continue; + } + if (!localAddress.equals(interfaceAddress.getAddress())) { + continue; + } + if (interfaceAddress.getBroadcast() != null) { + return interfaceAddress.getBroadcast().getHostAddress(); + } + } + + for (java.net.InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) { + if (interfaceAddress.getBroadcast() != null && interfaceAddress.getAddress() instanceof java.net.Inet4Address) { + return interfaceAddress.getBroadcast().getHostAddress(); + } + } + } catch (Exception ignored) { + // Fallback to generic detection if internet-route probing fails + } finally { + if (routeProbe != null && !routeProbe.isClosed()) { + routeProbe.close(); + } + } + return null; + } + + private String detectFirstUsableBroadcastAddress() { + try { + for (java.net.NetworkInterface networkInterface : java.util.Collections.list(java.net.NetworkInterface.getNetworkInterfaces())) { + if (!networkInterface.isUp() || networkInterface.isLoopback() || networkInterface.isVirtual() || networkInterface.isPointToPoint()) { + continue; + } + for (java.net.InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) { + if (interfaceAddress.getBroadcast() != null && interfaceAddress.getAddress() instanceof java.net.Inet4Address) { + return interfaceAddress.getBroadcast().getHostAddress(); + } + } + } + } catch (Exception ignored) { + // Keep configured value if no interface can be detected + } + return null; + } + } /**