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;
+ }
+
}
/**