Win-Test: improve broadcast defaults and SKED workflow

Co-authored-by: GitHub Copilot <github-copilot[bot]@users.noreply.github.com>
This commit is contained in:
2026-03-24 16:53:13 +01:00
committed by Philipp Wagner
parent c0b8aa61a9
commit d5b8508aa6
6 changed files with 262 additions and 15 deletions

View File

@@ -6,7 +6,7 @@
<groupId>de.x08</groupId>
<artifactId>praktiKST</artifactId>
<version>1.40.0-nightly</version>
<version>1.41.0-nightly</version>
<name>praktiKST</name>

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -391,6 +391,22 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ChoiceBox<Integer> 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<String> 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<String> 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;
}
}
/**