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(() -> {
|
||||
this.activeSkeds.add(sked);
|
||||
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() {
|
||||
|
||||
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_wintestNetworkPort = 9871;
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
return stn_loginLocatorSecondCat;
|
||||
}
|
||||
@@ -1299,6 +1317,14 @@ public class ChatPreferences {
|
||||
logsynch_wintestNetworkListenerEnabled.setTextContent(this.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
|
||||
@@ -1858,6 +1884,15 @@ public class ChatPreferences {
|
||||
logsynch_wintestNetworkListenerEnabled,
|
||||
"logsynch_wintestNetworkListenerEnabled");
|
||||
|
||||
logsynch_wintestNetworkBroadcastAddress = getText(
|
||||
logsynchEl,
|
||||
logsynch_wintestNetworkBroadcastAddress,
|
||||
"logsynch_wintestNetworkBroadcastAddress");
|
||||
|
||||
logsynch_wintestNetworkSkedPushEnabled = getBoolean(
|
||||
logsynchEl,
|
||||
logsynch_wintestNetworkSkedPushEnabled,
|
||||
"logsynch_wintestNetworkSkedPushEnabled");
|
||||
|
||||
System.out.println(
|
||||
"[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled);
|
||||
|
||||
Reference in New Issue
Block a user