Files
kst4contest/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java
T

518 lines
19 KiB
Java

package kst4contest.controller;
import javafx.application.Platform;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ReadUDPByWintestThread extends Thread {
private static final Pattern STATUS_TOKEN_PATTERN = Pattern.compile("\"([^\"]*)\"|(\\S+)");
private DatagramSocket socket;
private ChatController client;
private volatile boolean running = true;
private int PORT = 9871; //default
private static final int BUFFER_SIZE = 4096;
private final Map<Integer, String> receivedQsos = new ConcurrentHashMap<>();
private long lastPacketTime = 0;
private String myStation = "DO5AMF";
private String targetStation = "";
private String stationID = "";
private int lastKnownQso = 0;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "Wintest-msg";
public ReadUDPByWintestThread(ChatController client, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.client = client;
this.myStation = client.getChatPreferences().getStn_loginCallSignRaw(); //callsign of the logging stn
this.PORT = client.getChatPreferences().getLogsynch_wintestNetworkPort();
}
@Override
public void interrupt() {
running = false;
if (socket != null && !socket.isClosed()) socket.close();
super.interrupt();
}
@Override
public void run() {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("ReadUDPByWintestThread");
byte[] buffer = new byte[BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
socket = new DatagramSocket(null); //first init with null, then make ready for reuse
socket.setReuseAddress(true);
// socket = new DatagramSocket(PORT);
int boundPort = client.getChatPreferences().getLogsynch_wintestNetworkPort();
socket.bind(new InetSocketAddress(boundPort));
socket.setSoTimeout(3000);
System.out.println("[WinTest UDP listener] started at port: " + boundPort);
} catch (SocketException e) {
e.printStackTrace();
return;
}
while (running) {
try {
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
processWinTestMessage(msg);
} catch (SocketTimeoutException e) {
// checkForMissingQsos();
} catch (IOException e) {
//TODO: here is something to catch
}
}
}
void processWinTestMessage(String msg) {
// System.out.println("Wintest-Message received: " + msg);
lastPacketTime = System.currentTimeMillis();
if (msg.startsWith("HELLO:")) { //Client Signon of wintest
parseHello(msg);
try {
// send_needqso();
}catch (Exception e) {
System.out.println("Error: ");
e.printStackTrace();
}
} else if (msg.startsWith("ADDQSO:")) { //adding qso to wintest log
try {
parseAddQso(msg);
} catch (Exception e) {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Parsing ERROR: " + Arrays.toString(e.getStackTrace()), true);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
} else if (msg.startsWith("STATUS")) {
parseStatus(msg);
} else if (msg.startsWith("IHAVE:")) { //periodical message of wintest, which qsos are in the log
// parseIHave(msg); //TODO
}
else if (msg.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
System.out.println("ReadUdpByWintest, Info: got poison, now dieing....");
socket.close();
running = false;
}
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "message received\n" + msg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
/**
* parsing of the hello message of wintest:
* "HELLO: "STN1" "" 6667 130 "SLAVE" 1 0 1762201985"
* @param msg
*/
private void parseHello(String msg) {
try {
String[] tokens = msg.split("\"");
if (tokens.length >= 2) {
targetStation = tokens[1];
System.out.println("[WinTest rcv: found logger instance: " + targetStation);
}
} catch (Exception e) {
System.out.println("[WinTest] ERROR on HELLO-Parsing: " + e.getMessage());
}
}
private byte util_calculateChecksum(byte[] bytes) {
int sum = 0;
for (byte b : bytes) sum += b;
return (byte) ((sum | 0x80) & 0xFF);
}
/**
* Formats a frequency from a Win-Test STATUS packet for use as MYQRG.
*
* <p>Win-Test transmits the frequency in units of 0.1 kHz. KST4Contest
* displays frequencies as {@code MHz.kHz.10Hz}, for example
* {@code 144.300.00} or {@code 10368.100.00}. The MHz part may contain
* between one and five digits. Deriving its length from the complete value
* avoids separate and incomplete handling for individual bands.</p>
*
* @param frequencyIn100Hz frequency received from Win-Test in units of
* 0.1 kHz, equivalent to 100 Hz
* @return frequency formatted for MYQRG
* @throws IllegalArgumentException if the supplied frequency is not positive
* or too small to be formatted
* @throws ArithmeticException if the supplied value exceeds the supported
* numeric range
*/
private String helper_formatWinTestFrequency(long frequencyIn100Hz) {
if (frequencyIn100Hz <= 0) {
throw new IllegalArgumentException(
"Win-Test frequency must be greater than zero"
);
}
/*
* Multiplication by ten creates a digit sequence whose final five
* digits represent kHz and 10-Hz groups:
*
* 1443210 -> 14432100 -> 144.321.00
* 103681000 -> 1036810000 -> 10368.100.00
*/
long frequencyIn10Hz = Math.multiplyExact(
frequencyIn100Hz,
10L
);
String frequencyDigits = Long.toString(frequencyIn10Hz);
if (frequencyDigits.length() < 6) {
throw new IllegalArgumentException(
"Win-Test frequency is too small: " + frequencyIn100Hz
);
}
int mhzEndIndex = frequencyDigits.length() - 5;
int khzEndIndex = frequencyDigits.length() - 2;
return frequencyDigits.substring(0, mhzEndIndex)
+ "."
+ frequencyDigits.substring(mhzEndIndex, khzEndIndex)
+ "."
+ frequencyDigits.substring(khzEndIndex);
}
/**
* Parses a Win-Test STATUS packet and optionally updates MYQRG.
*
* <p>The packet is tokenised while preserving quoted station names.
* The configured station-name filter is applied before any frequency is
* processed. An empty filter accepts STATUS packets from every Win-Test
* station.</p>
*
* <p>The main frequency is read from token 7. If pass-frequency use is
* enabled and token 11 contains a valid frequency, the pass frequency is
* used instead. A missing or invalid pass frequency deliberately falls
* back to the main frequency.</p>
*
* @param msg complete Win-Test STATUS packet
*/
private void parseStatus(String msg) {
try {
ArrayList<String> parts = new ArrayList<>();
Matcher matcher = STATUS_TOKEN_PATTERN.matcher(msg);
while (matcher.find()) {
if (matcher.group(1) != null) {
parts.add(matcher.group(1));
} else {
parts.add(matcher.group(2));
}
}
if (parts.size() < 8) {
System.out.println(
"[WinTest] STATUS too short: " + msg
);
return;
}
String stationName = parts.get(1);
String stationFilter = client
.getChatPreferences()
.getLogsynch_wintestNetworkStationNameOfWintestClient1();
if (stationFilter != null
&& !stationFilter.isBlank()
&& !stationName.equalsIgnoreCase(stationFilter)) {
return;
}
String modeValue = parts.get(5);
long mainFrequencyRaw = Long.parseLong(parts.get(7));
double mainFrequencyKHz = mainFrequencyRaw / 10.0;
String mode;
if ("1".equals(modeValue)) {
mode = mainFrequencyKHz > 10000.0 ? "usb" : "lsb";
} else {
mode = "cw";
}
String formattedMainQrg =
helper_formatWinTestFrequency(mainFrequencyRaw);
/*
* Token 11 may contain the pass frequency, depending on the
* Win-Test STATUS packet. Small numeric flag values must not be
* interpreted as frequencies.
*/
String formattedPassQrg = null;
if (parts.size() > 11) {
try {
long passFrequencyRaw =
Long.parseLong(parts.get(11));
double passFrequencyKHz =
passFrequencyRaw / 10.0;
if (passFrequencyKHz > 100.0) {
formattedPassQrg =
helper_formatWinTestFrequency(
passFrequencyRaw
);
}
} catch (IllegalArgumentException
| ArithmeticException ignored) {
/*
* Token 11 does not contain a usable frequency.
* The main frequency remains the safe fallback.
*/
}
}
boolean usePassQrg = client
.getChatPreferences()
.isLogsynch_wintestUsePassQrg();
final String qrgToSet =
usePassQrg && formattedPassQrg != null
? formattedPassQrg
: formattedMainQrg;
if (client
.getChatPreferences()
.isLogsynch_wintestQrgSyncEnabled()) {
Platform.runLater(
() -> client
.getChatPreferences()
.getMYQRGFirstCat()
.set(qrgToSet)
);
}
System.out.println(
"[WinTest STATUS] stn=" + stationName
+ ", mode=" + mode
+ ", qrg=" + formattedMainQrg
+ (formattedPassQrg != null
? ", passQrg=" + formattedPassQrg
: "")
+ ", selectedQrg=" + qrgToSet
+ ", syncActive="
+ client
.getChatPreferences()
.isLogsynch_wintestQrgSyncEnabled()
);
} catch (Exception exception) {
System.out.println(
"[WinTest] STATUS parsing error: "
+ exception.getMessage()
);
}
}
// private void send_needqso() throws IOException {
// String payload = String.format("NEEDQSO:\"%s\" \"%s\" \"%s\" %d %d?\0",
// "DO5AMF", "STN1", stationID, 1, 9999);
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
// }
// private void send_hello() throws IOException {
// String payload = String.format("HELLO:\"%s\" \"%s\" \"%s\" %d %d?\0",
// "DO5AMF", "", stationID, "SLAVE", 1, 14);
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
// }
/**
* Extracts the unchanged Win-Test band ID from the unquoted ADDQSO fields.
*
* @param message complete ADDQSO packet
* @return raw band ID or an empty value when the field is missing
*/
static String extractBandIdFromWinTestAddQso(String message) {
if (message == null) {
return "";
}
String[] quotedParts = message.split("\"");
if (quotedParts.length <= 6) {
return "";
}
String unquotedFields = quotedParts[6].trim();
if (unquotedFields.isEmpty()) {
return "";
}
String[] packetFields = unquotedFields.split("\\s+");
return packetFields.length > 3 ? packetFields[3] : "";
}
/**
* Extracts the locator from a Win-Test ADDQSO packet.
*
* <p>Current parser model based on the existing split-by-quotes code:
* <ul>
* <li>{@code split("\"")[7]} = callsign</li>
* <li>{@code split("\"")[11]} = received exchange, e.g. 599001</li>
* <li>{@code split("\"")[13]} = locator, e.g. JO51UM</li>
* </ul>
*
* <p>If the dedicated locator field is empty, the exchange is used as fallback.</p>
*
* @param msg raw ADDQSO message
* @return normalized six-character locator or null
*/
private String helper_resolveLocatorFromWinTestAddQso(String msg) {
if (msg == null) {
return null;
}
String[] quotedParts = msg.split("\"");
if (quotedParts.length > 13) {
String locator = WorkedGrossFieldCache.extractLocator6(quotedParts[13]);
if (locator != null) {
return locator;
}
}
if (quotedParts.length > 11) {
return WorkedGrossFieldCache.extractLocator6(quotedParts[11]);
}
return null;
}
/**
* Catches add-qso messages of wintest if a new qso gets into the log<br/>
*
* String is like this:<br/><br/>
*ADDQSO: "STN1" "" "STN1" 1762202297 1440000 0 12 0 0 0 2 2 "DM2RN" "599" "599001" "JO51UM" "" "" 0 "" "" "" 44510
*
* ^^^^sentby<br/>
* ^^^^^^^^^^time<br/>
* ^^^^^^qrg<br/>
* ^^band<br/>
* ^^^^^callsign logged<br/>
* stn-id ^^^^
* @param msg
*/
private void parseAddQso(String msg) {
try {
String[] quotedParts = msg == null ? new String[0] : msg.split("\"");
String callSign = quotedParts.length > 7 ? quotedParts[7] : "";
String rawBandId = extractBandIdFromWinTestAddQso(msg);
String locatorFromLogger = helper_resolveLocatorFromWinTestAddQso(msg);
LoggedQsoBand loggedBand = LoggedQsoBand.fromWinTestBandId(rawBandId);
ExternalLoggedQso loggedQso = ExternalLoggedQso.create(
callSign, loggedBand, locatorFromLogger, "WINTEST").orElse(null);
if (loggedQso == null) {
System.out.println("[WinTestUDPRcvr: warning] ADDQSO without usable callsign ignored");
return;
}
if (loggedBand == null && !rawBandId.isEmpty()) {
System.out.println("[WinTestUDPRcvr: warning] Unknown band ID: " + rawBandId);
}
ChatMember workedCall = loggedQso.toWorkedChatMember();
if (loggedBand != null
&& loggedBand.getProjectBand() != null
&& locatorFromLogger != null) {
this.client.registerWorkedGrossField(
loggedBand.getProjectBand(), locatorFromLogger, workedCall, loggedQso.getSource());
}
client.applyExternalLoggedQso(loggedQso);
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
// This will update the worked info on a worked chatmember. DBHandler will
// check, if an entry at the db had been modified. If not, then the worked
// station had not been stored. DBHandler will store the information then.
if (!isInChat) {
workedCall.setName("unknown");
if (workedCall.getQra() == null || workedCall.getQra().isBlank()) {
workedCall.setQra("unknown");
}
workedCall.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
this.client.getDbHandler().storeChatMember(workedCall);
}
File logUDPMessageToThisFile = new File(this.client.getChatPreferences()
.getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
FileWriter fileWriterPersistUDPToFile = null;
BufferedWriter bufwrtrRawMSGOut;
try {
fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);
} catch (IOException e1) {
e1.printStackTrace();
}
bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile);
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
System.out.println("[WinTest, Info: Marking Chatmember as worked: " + workedCall.toString());
// markChatMemberAsWorked(call, band); //TODO
} catch (Exception e) {
System.out.println("[WinTest] Fehler beim ADDQSO-Parsing: " + e.getMessage());
}
}
}