mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-03-30 04:31:04 +02:00
Add Win-Test SKED push via UDP (ported from wtKST)
Implements sending SKEDs to Win-Test via the LOCKSKED/ADDSKED/UNLOCKSKED UDP protocol sequence, ported from the C# wtSked class in wtKST. New files: - WinTestMessage.java: Win-Test network message format with checksum - WinTestSkedSender.java: UDP broadcast sender for SKED messages Modified: - ChatController: addSked() now pushes to Win-Test when enabled - ChatPreferences: new settings for broadcast address and sked push toggle The feature is disabled by default (logsynch_wintestNetworkSkedPushEnabled=false). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -824,7 +824,50 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
this.activeSkeds.add(sked);
|
this.activeSkeds.add(sked);
|
||||||
scoreService.requestRecompute("sked-added");
|
scoreService.requestRecompute("sked-added");
|
||||||
}); //TODO: Addsked muss noch genutzt werden
|
});
|
||||||
|
|
||||||
|
// Push sked to Win-Test via UDP if enabled
|
||||||
|
if (chatPreferences.isLogsynch_wintestNetworkSkedPushEnabled()
|
||||||
|
&& chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
||||||
|
pushSkedToWinTest(sked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes a sked to Win-Test via UDP broadcast (LOCKSKED / ADDSKED / UNLOCKSKED).
|
||||||
|
* Runs on a background thread to avoid blocking the UI.
|
||||||
|
*/
|
||||||
|
private void pushSkedToWinTest(ContestSked sked) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
InetAddress broadcastAddr = InetAddress.getByName(
|
||||||
|
chatPreferences.getLogsynch_wintestNetworkBroadcastAddress());
|
||||||
|
int port = chatPreferences.getLogsynch_wintestNetworkPort();
|
||||||
|
String stationName = chatPreferences.getLogsynch_wintestNetworkStationNameOfKST();
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
freqKHz = Double.parseDouble(qrgStr.trim());
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) { }
|
||||||
|
|
||||||
|
// Build notes string with locator/azimuth info
|
||||||
|
String notes = "sked via KST4Contest";
|
||||||
|
if (sked.getTargetAzimuth() > 0) {
|
||||||
|
notes = String.format("[%.0f°] %s", sked.getTargetAzimuth(), notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.pushSkedToWinTest(sked, freqKHz, notes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.out.println("[ChatController] Error pushing sked to Win-Test: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}, "WinTestSkedPush").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StationMetricsService getStationMetricsService() {
|
public StationMetricsService getStationMetricsService() {
|
||||||
|
|||||||
80
src/main/java/kst4contest/controller/WinTestMessage.java
Normal file
80
src/main/java/kst4contest/controller/WinTestMessage.java
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package kst4contest.controller;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Win-Test network protocol message.
|
||||||
|
* <p>
|
||||||
|
* Ported from the C# wtMessage class in wtKST.
|
||||||
|
* <p>
|
||||||
|
* Win-Test uses a simple ASCII-based UDP protocol with a checksum byte.
|
||||||
|
* Message format (for sending):
|
||||||
|
* <pre>
|
||||||
|
* MESSAGETYPE: "src" "dst" data{checksum}\0
|
||||||
|
* </pre>
|
||||||
|
* The checksum is calculated over all bytes before the checksum position,
|
||||||
|
* then OR'd with 0x80.
|
||||||
|
*/
|
||||||
|
public class WinTestMessage {
|
||||||
|
|
||||||
|
/** Win-Test message types relevant for SKED management. */
|
||||||
|
public enum MessageType {
|
||||||
|
LOCKSKED,
|
||||||
|
UNLOCKSKED,
|
||||||
|
ADDSKED,
|
||||||
|
DELETESKED,
|
||||||
|
UPDATESKED
|
||||||
|
}
|
||||||
|
|
||||||
|
private final MessageType type;
|
||||||
|
private final String src;
|
||||||
|
private final String dst;
|
||||||
|
private final String data;
|
||||||
|
|
||||||
|
public WinTestMessage(MessageType type, String src, String dst, String data) {
|
||||||
|
this.type = type;
|
||||||
|
this.src = src;
|
||||||
|
this.dst = dst;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes this message to bytes for UDP transmission.
|
||||||
|
* <p>
|
||||||
|
* Format: {@code MESSAGETYPE: "src" "dst" data{checksum}\0}
|
||||||
|
* <p>
|
||||||
|
* The '?' placeholder is replaced by the calculated checksum,
|
||||||
|
* followed by a NUL terminator.
|
||||||
|
* Degree signs (°) are escaped as \260 per Win-Test convention.
|
||||||
|
*/
|
||||||
|
public byte[] toBytes() {
|
||||||
|
String escapedData = data.replace("°", "\\260");
|
||||||
|
|
||||||
|
// Format: MESSAGETYPE: "src" "dst" data?\0
|
||||||
|
// The '?' is a placeholder for the checksum byte
|
||||||
|
String raw = type.name() + ": \"" + src + "\" \"" + dst + "\" " + escapedData + "?\0";
|
||||||
|
|
||||||
|
byte[] bytes = raw.getBytes(StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
// Calculate checksum over everything before the checksum position (length - 2)
|
||||||
|
int sum = 0;
|
||||||
|
for (int i = 0; i < bytes.length - 2; i++) {
|
||||||
|
sum += (bytes[i] & 0xFF);
|
||||||
|
}
|
||||||
|
byte checksum = (byte) ((sum | 0x80) & 0xFF);
|
||||||
|
bytes[bytes.length - 2] = checksum;
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters for debugging/logging
|
||||||
|
public MessageType getType() { return type; }
|
||||||
|
public String getSrc() { return src; }
|
||||||
|
public String getDst() { return dst; }
|
||||||
|
public String getData() { return data; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return type.name() + ": src=" + src + " dst=" + dst + " data=" + data;
|
||||||
|
}
|
||||||
|
}
|
||||||
182
src/main/java/kst4contest/controller/WinTestSkedSender.java
Normal file
182
src/main/java/kst4contest/controller/WinTestSkedSender.java
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
package kst4contest.controller;
|
||||||
|
|
||||||
|
import kst4contest.model.Band;
|
||||||
|
import kst4contest.model.ContestSked;
|
||||||
|
import kst4contest.model.ThreadStateMessage;
|
||||||
|
|
||||||
|
import java.net.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends SKED entries to Win-Test via UDP broadcast.
|
||||||
|
* <p>
|
||||||
|
* Ported from the C# wtSked class in wtKST.
|
||||||
|
* <p>
|
||||||
|
* Win-Test expects a LOCKSKED / ADDSKED / UNLOCKSKED sequence
|
||||||
|
* to safely insert a new sked into its schedule window.
|
||||||
|
*/
|
||||||
|
public class WinTestSkedSender {
|
||||||
|
|
||||||
|
private final String stationName;
|
||||||
|
private final InetAddress broadcastAddress;
|
||||||
|
private final int port;
|
||||||
|
private final ThreadStatusCallback callback;
|
||||||
|
|
||||||
|
private static final String THREAD_NICKNAME = "WT-SkedSend";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param stationName our station name in the Win-Test network (e.g. "KST4Contest")
|
||||||
|
* @param broadcastAddress UDP broadcast address (e.g. 255.255.255.255 or subnet broadcast)
|
||||||
|
* @param port Win-Test network port (default 9871)
|
||||||
|
* @param callback optional callback for status reporting (may be null)
|
||||||
|
*/
|
||||||
|
public WinTestSkedSender(String stationName, InetAddress broadcastAddress, int port,
|
||||||
|
ThreadStatusCallback callback) {
|
||||||
|
this.stationName = stationName;
|
||||||
|
this.broadcastAddress = broadcastAddress;
|
||||||
|
this.port = port;
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
|
||||||
|
* sequence via UDP broadcast.
|
||||||
|
*
|
||||||
|
* @param sked the sked to push
|
||||||
|
* @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) {
|
||||||
|
try {
|
||||||
|
sendLockSked();
|
||||||
|
sendAddSked(sked, frequencyKHz, notes);
|
||||||
|
sendUnlockSked();
|
||||||
|
|
||||||
|
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false);
|
||||||
|
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign()
|
||||||
|
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand());
|
||||||
|
} catch (Exception e) {
|
||||||
|
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
|
||||||
|
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a LOCKSKED message to lock the Win-Test sked window.
|
||||||
|
*/
|
||||||
|
private void sendLockSked() throws Exception {
|
||||||
|
WinTestMessage msg = new WinTestMessage(
|
||||||
|
WinTestMessage.MessageType.LOCKSKED,
|
||||||
|
stationName, "",
|
||||||
|
"\"" + stationName + "\"");
|
||||||
|
sendUdp(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an UNLOCKSKED message to unlock the Win-Test sked window.
|
||||||
|
*/
|
||||||
|
private void sendUnlockSked() throws Exception {
|
||||||
|
WinTestMessage msg = new WinTestMessage(
|
||||||
|
WinTestMessage.MessageType.UNLOCKSKED,
|
||||||
|
stationName, "",
|
||||||
|
"\"" + stationName + "\"");
|
||||||
|
sendUdp(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an ADDSKED message with the sked details.
|
||||||
|
* <p>
|
||||||
|
* Win-Test ADDSKED data format (from wtKST):
|
||||||
|
* <pre>
|
||||||
|
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
|
||||||
|
* </pre>
|
||||||
|
* <p>
|
||||||
|
* 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 {
|
||||||
|
// Win-Test timestamp: epoch seconds with 60s offset
|
||||||
|
long epochSeconds = sked.getSkedTimeEpoch() / 1000;
|
||||||
|
long wtTimestamp = epochSeconds + 60;
|
||||||
|
|
||||||
|
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
|
||||||
|
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
String data = wtTimestamp
|
||||||
|
+ " " + freqTenthKHz
|
||||||
|
+ " " + bandId
|
||||||
|
+ " " + mode
|
||||||
|
+ " \"" + sked.getTargetCallsign() + "\""
|
||||||
|
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||||
|
|
||||||
|
WinTestMessage msg = new WinTestMessage(
|
||||||
|
WinTestMessage.MessageType.ADDSKED,
|
||||||
|
stationName, "",
|
||||||
|
data);
|
||||||
|
sendUdp(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a WinTestMessage via UDP broadcast.
|
||||||
|
*/
|
||||||
|
private void sendUdp(WinTestMessage msg) throws Exception {
|
||||||
|
try (DatagramSocket socket = new DatagramSocket()) {
|
||||||
|
socket.setBroadcast(true);
|
||||||
|
socket.setReuseAddress(true);
|
||||||
|
|
||||||
|
byte[] bytes = msg.toBytes();
|
||||||
|
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcastAddress, port);
|
||||||
|
socket.send(packet);
|
||||||
|
|
||||||
|
System.out.println("[WinTestSkedSender] sent: " + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps the kst4contest Band enum to Win-Test band IDs.
|
||||||
|
* <p>
|
||||||
|
* Win-Test band IDs (reverse-engineered from wtKST):
|
||||||
|
* 10=50MHz, 11=70MHz, 12=144MHz, 14=432MHz, 16=1.2GHz,
|
||||||
|
* 17=2.3GHz, 18=3.4GHz, 19=5.7GHz, 20=10GHz, 21=24GHz,
|
||||||
|
* 22=47GHz, 23=76GHz
|
||||||
|
*/
|
||||||
|
public static int toWinTestBandId(Band band) {
|
||||||
|
if (band == null) return 12; // default to 144 MHz
|
||||||
|
return switch (band) {
|
||||||
|
case B_144 -> 12;
|
||||||
|
case B_432 -> 14;
|
||||||
|
case B_1296 -> 16;
|
||||||
|
case B_2320 -> 17;
|
||||||
|
case B_3400 -> 18;
|
||||||
|
case B_5760 -> 19;
|
||||||
|
case B_10G -> 20;
|
||||||
|
case B_24G -> 21;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very simple SSB segment heuristic.
|
||||||
|
* 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;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportStatus(String text, boolean isError) {
|
||||||
|
if (callback != null) {
|
||||||
|
callback.onThreadStatus(THREAD_NICKNAME,
|
||||||
|
new ThreadStateMessage(THREAD_NICKNAME, !isError, text, isError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -201,6 +201,8 @@ public class ChatPreferences {
|
|||||||
int logsynch_wintestNetworkStationIDOfKST = 55555;
|
int logsynch_wintestNetworkStationIDOfKST = 55555;
|
||||||
int logsynch_wintestNetworkPort = 9871;
|
int logsynch_wintestNetworkPort = 9871;
|
||||||
boolean logsynch_wintestNetworkListenerEnabled = true; // default true = bisheriges Verhalten
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -454,6 +456,22 @@ public class ChatPreferences {
|
|||||||
this.logsynch_wintestNetworkListenerEnabled = logsynch_wintestNetworkListenerEnabled;
|
this.logsynch_wintestNetworkListenerEnabled = logsynch_wintestNetworkListenerEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getLogsynch_wintestNetworkBroadcastAddress() {
|
||||||
|
return logsynch_wintestNetworkBroadcastAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogsynch_wintestNetworkBroadcastAddress(String logsynch_wintestNetworkBroadcastAddress) {
|
||||||
|
this.logsynch_wintestNetworkBroadcastAddress = logsynch_wintestNetworkBroadcastAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLogsynch_wintestNetworkSkedPushEnabled() {
|
||||||
|
return logsynch_wintestNetworkSkedPushEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLogsynch_wintestNetworkSkedPushEnabled(boolean logsynch_wintestNetworkSkedPushEnabled) {
|
||||||
|
this.logsynch_wintestNetworkSkedPushEnabled = logsynch_wintestNetworkSkedPushEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public String getStn_loginLocatorSecondCat() {
|
public String getStn_loginLocatorSecondCat() {
|
||||||
return stn_loginLocatorSecondCat;
|
return stn_loginLocatorSecondCat;
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1317,14 @@ public class ChatPreferences {
|
|||||||
logsynch_wintestNetworkListenerEnabled.setTextContent(this.logsynch_wintestNetworkListenerEnabled + "");
|
logsynch_wintestNetworkListenerEnabled.setTextContent(this.logsynch_wintestNetworkListenerEnabled + "");
|
||||||
logsynch.appendChild(logsynch_wintestNetworkListenerEnabled);
|
logsynch.appendChild(logsynch_wintestNetworkListenerEnabled);
|
||||||
|
|
||||||
|
Element logsynch_wintestNetworkBroadcastAddress = doc.createElement("logsynch_wintestNetworkBroadcastAddress");
|
||||||
|
logsynch_wintestNetworkBroadcastAddress.setTextContent(this.logsynch_wintestNetworkBroadcastAddress);
|
||||||
|
logsynch.appendChild(logsynch_wintestNetworkBroadcastAddress);
|
||||||
|
|
||||||
|
Element logsynch_wintestNetworkSkedPushEnabled = doc.createElement("logsynch_wintestNetworkSkedPushEnabled");
|
||||||
|
logsynch_wintestNetworkSkedPushEnabled.setTextContent(this.logsynch_wintestNetworkSkedPushEnabled + "");
|
||||||
|
logsynch.appendChild(logsynch_wintestNetworkSkedPushEnabled);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* trxSynchUCX
|
* trxSynchUCX
|
||||||
@@ -1858,6 +1884,15 @@ public class ChatPreferences {
|
|||||||
logsynch_wintestNetworkListenerEnabled,
|
logsynch_wintestNetworkListenerEnabled,
|
||||||
"logsynch_wintestNetworkListenerEnabled");
|
"logsynch_wintestNetworkListenerEnabled");
|
||||||
|
|
||||||
|
logsynch_wintestNetworkBroadcastAddress = getText(
|
||||||
|
logsynchEl,
|
||||||
|
logsynch_wintestNetworkBroadcastAddress,
|
||||||
|
"logsynch_wintestNetworkBroadcastAddress");
|
||||||
|
|
||||||
|
logsynch_wintestNetworkSkedPushEnabled = getBoolean(
|
||||||
|
logsynchEl,
|
||||||
|
logsynch_wintestNetworkSkedPushEnabled,
|
||||||
|
"logsynch_wintestNetworkSkedPushEnabled");
|
||||||
|
|
||||||
System.out.println(
|
System.out.println(
|
||||||
"[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled);
|
"[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled);
|
||||||
|
|||||||
Reference in New Issue
Block a user