Integrate latest local development state and clean repository artifacts

This commit is contained in:
Marc Froehlich
2026-03-20 11:24:28 +01:00
parent ee5ee535bb
commit 7f9b1bfc4d
40 changed files with 8173 additions and 2237 deletions

16
.gitignore vendored
View File

@@ -16,3 +16,19 @@ target
debug.out debug.out
.DS_Store .DS_Store
#Logfiles
SimpleLogFile.txt
udpReaderBackup.txt
#tempfiles
.idea/
out/
#targetfiles - mvn wrapper
target/
#builds
build/
#zip files for local backups
*.zip

View File

@@ -1,6 +1,17 @@
package kst4contest; package kst4contest;
import java.util.Random;
public class ApplicationConstants { public class ApplicationConstants {
/**
* default constructor generates runtime id
*/
ApplicationConstants() {
sessionRuntimeUniqueId = generateRuntimeId();
};
public static int sessionRuntimeUniqueId = generateRuntimeId();
/** /**
* Name of the Application. * Name of the Application.
*/ */
@@ -9,7 +20,7 @@ public class ApplicationConstants {
/** /**
* Name of file to store preferences in. * Name of file to store preferences in.
*/ */
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.263; public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.33;
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml"; public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml";
public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml"; public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
@@ -21,6 +32,23 @@ public class ApplicationConstants {
public static final String DISCSTRING_DISCONNECT_DUE_PAWWORDERROR = "JUSTDSICCAUSEPWWRONG"; public static final String DISCSTRING_DISCONNECT_DUE_PAWWORDERROR = "JUSTDSICCAUSEPWWRONG";
public static final String DISCSTRING_DISCONNECTONLY = "ONLYDISCONNECT"; public static final String DISCSTRING_DISCONNECTONLY = "ONLYDISCONNECT";
public static final String DISCONNECT_RDR_POISONPILL = "POISONPILL_KILLTHREAD"; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop // public static final String DISCONNECT_RDR_POISONPILL = "POISONPILL_KILLTHREAD: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String DISCONNECT_RDR_POISONPILL = "UNKNOWN: KST4C KILL POISONPILL_KILLTHREAD=: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg] "; // hard-coded marker (user can't remove it)
/**
* generates a unique runtime id per session. Its used to feed the poison pill in order to kill only this one and
* only instance if the program and not multiple instances
* @return
*/
public static int generateRuntimeId() {
Random ran = new Random();
return ran.nextInt(6) + 100;
}
} }

View File

@@ -41,10 +41,25 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original // String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" "; String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String prefix_asWatchList = "ASWATCHLIST:\" "+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" "; String prefix_asWatchList = "ASWATCHLIST: \""+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String bandString = "1440000"; //TODO: this must variable in case of higher bands! ... default: 1440000
// String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //before fix 1.266
String ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
try {
if (this.client.getChatPreferences().getStn_loginCallSign().contains("-")) {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign().split("-")[0];
} else {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
}
} catch (Exception e) {
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
}
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
String bandString = "1440000";
String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat();
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!! String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString; String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
String asWatchListStringSuffix = asWatchListString; String asWatchListStringSuffix = asWatchListString;
@@ -70,10 +85,9 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
for (ChatMember i : ary_threadSafeChatMemberArray) { for (ChatMember i : ary_threadSafeChatMemberArray) {
if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault()) if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault())
//Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower! //Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower!
//this counts for AS request and Aswatchlist
{ {
suffix = i.getCallSign() + "," + i.getQra() + " "; suffix = i.getCallSign() + "," + i.getQra() + " ";

View File

@@ -1,8 +1,10 @@
package kst4contest.controller; package kst4contest.controller;
import java.util.Arrays;
import java.util.TimerTask; import java.util.TimerTask;
import kst4contest.model.ChatMessage; import kst4contest.model.ChatMessage;
import kst4contest.model.ThreadStateMessage;
/** /**
* This class is for sending beacons intervalled to the public chat. Gets all * This class is for sending beacons intervalled to the public chat. Gets all
@@ -20,17 +22,24 @@ import kst4contest.model.ChatMessage;
public class BeaconTask extends TimerTask { public class BeaconTask extends TimerTask {
private ChatController chatController; private ChatController chatController;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "MyBeacon";
public BeaconTask(ChatController client) { public BeaconTask(ChatController client, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.chatController = client; this.chatController = client;
} }
@Override @Override
public void run() { public void run() {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("BeaconTask"); Thread.currentThread().setName("BeaconTask");
ChatMessage beaconMSG = new ChatMessage(); ChatMessage beaconMSG = new ChatMessage();
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat(); String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
@@ -75,8 +84,12 @@ public class BeaconTask extends TimerTask {
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText()); + " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG); this.chatController.getMessageTXBus().add(beaconMSG);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else { } else {
//do nothing, CQ is disabled threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} }
/** /**
@@ -94,14 +107,15 @@ public class BeaconTask extends TimerTask {
+ " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText()); + " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG2); this.chatController.getMessageTXBus().add(beaconMSG2);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else { } else {
//do nothing, CQ is disabled threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} }
} }
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,7 @@ package kst4contest.controller;
import kst4contest.model.ChatMember; import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences; import kst4contest.model.ChatPreferences;
import kst4contest.model.ThreadStateMessage;
import java.io.*; import java.io.*;
import java.net.ServerSocket; import java.net.ServerSocket;
@@ -14,6 +15,8 @@ public class DXClusterThreadPooledServer implements Runnable{
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
ChatController chatController = null; ChatController chatController = null;
protected int serverPort = 8080; protected int serverPort = 8080;
protected ServerSocket serverSocket = null; protected ServerSocket serverSocket = null;
@@ -23,13 +26,17 @@ public class DXClusterThreadPooledServer implements Runnable{
Executors.newFixedThreadPool(10); Executors.newFixedThreadPool(10);
Socket clientSocket; Socket clientSocket;
public DXClusterThreadPooledServer(int port, ChatController chatController){ public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
this.serverPort = port; this.serverPort = port;
this.chatController = chatController; this.chatController = chatController;
this.callBackToController = callback;
} }
public void run(){ public void run(){
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
synchronized(this){ synchronized(this){
this.runningThread = Thread.currentThread(); this.runningThread = Thread.currentThread();
runningThread.setName("DXCluster-thread-pooled-server"); runningThread.setName("DXCluster-thread-pooled-server");
@@ -53,7 +60,7 @@ public class DXClusterThreadPooledServer implements Runnable{
"Error accepting client connection", e); "Error accepting client connection", e);
} }
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets); DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController);
this.threadPool.execute(worker); this.threadPool.execute(worker);
@@ -111,6 +118,7 @@ public class DXClusterThreadPooledServer implements Runnable{
for (Socket socket : clientSockets) { for (Socket socket : clientSockets) {
try { try {
OutputStream output = socket.getOutputStream(); OutputStream output = socket.getOutputStream();
String singleDXClusterMessage = "DX de "; String singleDXClusterMessage = "DX de ";
@@ -134,6 +142,9 @@ public class DXClusterThreadPooledServer implements Runnable{
output.write((singleDXClusterMessage).getBytes()); output.write((singleDXClusterMessage).getBytes());
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last msg to " + clientSockets.size() + " Cluster Clients:\n" + singleDXClusterMessage, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!"); System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
@@ -152,12 +163,16 @@ class DXClusterServerWorkerRunnable implements Runnable{
protected String serverText = null; protected String serverText = null;
private ChatController client = null; private ChatController client = null;
private List<Socket> dxClusterClientSocketsConnectedList; private List<Socket> dxClusterClientSocketsConnectedList;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets) { public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) {
this.clientSocket = clientSocket; this.clientSocket = clientSocket;
this.serverText = serverText; this.serverText = serverText;
this.client = chatController; this.client = chatController;
this.dxClusterClientSocketsConnectedList = clientSockets; this.dxClusterClientSocketsConnectedList = clientSockets;
this.callBackToController = callback;
} }
public void run() { public void run() {
@@ -171,8 +186,12 @@ class DXClusterServerWorkerRunnable implements Runnable{
@Override @Override
public void run() { public void run() {
StringBuilder connectedClients = new StringBuilder(); //only for statistics
for (Socket socket : dxClusterClientSocketsConnectedList) { for (Socket socket : dxClusterClientSocketsConnectedList) {
connectedClients.append(socket.getInetAddress()).append("\n");
try { try {
OutputStream output = socket.getOutputStream(); OutputStream output = socket.getOutputStream();
output.write(("\r\n").getBytes()); output.write(("\r\n").getBytes());
@@ -194,6 +213,9 @@ class DXClusterServerWorkerRunnable implements Runnable{
} }
} }
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} }
}, 30000, 30000); }, 30000, 30000);

View File

@@ -13,7 +13,7 @@ public class DXClusterThreadPooledServerTest {
testPreferences.setStn_loginCallSign("DM5M"); testPreferences.setStn_loginCallSign("DM5M");
client.setChatPreferences(testPreferences); client.setChatPreferences(testPreferences);
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client); DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client, client);
new Thread(dxClusterServer).start(); new Thread(dxClusterServer).start();

View File

@@ -5,8 +5,10 @@ import java.io.PrintWriter;
import java.sql.SQLException; import java.sql.SQLException;
//import java.net.Socket; //import java.net.Socket;
//import java.util.ArrayList; //import java.util.ArrayList;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Locale;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -28,6 +30,9 @@ public class MessageBusManagementThread extends Thread {
int index; int index;
private String ThreadNickName = "MessageBus";
private ThreadStatusCallback callBackToController;
private PrintWriter writer; private PrintWriter writer;
// private Socket socket; // private Socket socket;
private ChatController client; private ChatController client;
@@ -40,6 +45,15 @@ public class MessageBusManagementThread extends Thread {
private final String PTRN_USERLISTENTRY = "([a-zA-Z0-9]{2}/{1})?([a-zA-Z0-9]{1,3}[0-9][a-zA-Z0-9]{0,3}[a-zA-Z]{0,3})(/p)? [a-zA-Z]{2}[0-9]{2}[a-zA-Z]{2} [ -~]{1,20}"; private final String PTRN_USERLISTENTRY = "([a-zA-Z0-9]{2}/{1})?([a-zA-Z0-9]{1,3}[0-9][a-zA-Z0-9]{0,3}[a-zA-Z]{0,3})(/p)? [a-zA-Z]{2}[0-9]{2}[a-zA-Z]{2} [ -~]{1,20}";
private final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; private final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
private final String PTRN_QRG_CAT3 = "(([0-9]{3,5}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; private final String PTRN_QRG_CAT3 = "(([0-9]{3,5}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
// ==== Autoanswer Flood/Pingpong Protection ====
private static final String AUTOANSWER_PREFIX = ApplicationConstants.AUTOANSWER_PREFIX; // hard-coded marker (user can't remove it)
private static final long AUTOANSWER_COOLDOWN_MS = 45_000L; // 45_000L = 45s
// Cooldown per opponent station (and ChatCategory) only setted if this client sends
private final Hashtable<String, Long> lastLocalAutoAnswerPerRemoteMs = new Hashtable<>();
// BufferedWriter bufwrtrDBGMSGOut; // BufferedWriter bufwrtrDBGMSGOut;
// private String text; // private String text;
@@ -60,10 +74,14 @@ public class MessageBusManagementThread extends Thread {
this.serverReady = serverReady; this.serverReady = serverReady;
} }
public MessageBusManagementThread(ChatController client) { public MessageBusManagementThread(ChatController client, ThreadStatusCallback callBack) {
this.callBackToController = callBack;
this.client = client; this.client = client;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} }
/** /**
@@ -182,6 +200,184 @@ public class MessageBusManagementThread extends Thread {
return stringAggregation; return stringAggregation;
} }
/**
* Smart Frequency Parser (V1.32)
* Replaces the old RegEx logic.
* Features:
* 1. Handles full frequencies (144.210) and short forms (.210, 210).
* 2. Handles extended precision/weird formatting (144.210.10, 144,210,10).
* 3. Prioritizes USER CONTEXT (History) over GLOBAL CONTEXT (Preferences).
*/
private void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
// Regex Explanation:
// Part 1 (Full): Start (not digit), 3-5 digits, sep, 1-3 digits, OPTIONAL (sep, 1-3 digits)
// Matches: 144.210, 144.210.10, 10368.100
// Part 2 (Short1): Start (not digit), sep, 3 digits, OPTIONAL (sep, 1-3 digits)
// Matches: .210, .210.10, ,210
// Part 3 (Short2): Whitespace/Start, 3 digits, Whitespace/End
// Matches: " 210 ", " 144 "
String smartPattern = "(?<![\\d])(\\d{3,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])|(?<=\\s|^)(\\d{3})(?=\\s|$)";
Pattern pattern = Pattern.compile(smartPattern);
Matcher matcher = pattern.matcher(message.getMessageText());
ChatMember sender = message.getSender();
// Safety check, in case sender is null (e.g., server message)
if (sender == null) return;
while (matcher.find()) {
String foundRaw = matcher.group().trim();
// --- PRE-PROCESSING: Normalize separators ---
// 1. Replace all commas with dots to unify format (144,210,10 -> 144.210.10)
foundRaw = foundRaw.replace(",", ".");
double finalDetectedFrequency = 0.0;
Band finalDetectedBand = null;
boolean isShortForm = false;
// --- STEP 1: Type Determination (Short or Full?) ---
// Check if it starts with a dot (e.g. ".210") OR is just 3 digits ("210")
if (foundRaw.startsWith(".") || foundRaw.length() == 3) {
// It is a short form.
// We strip the leading dot for calculation if present -> "210.10" or "210"
if (foundRaw.startsWith(".")) foundRaw = foundRaw.substring(1);
isShortForm = true;
} else {
// It is a full frequency (e.g., 144.210.10 or 144.210)
try {
// Normalize "144.210.10" to "144.21010" for Double.parseDouble
String normalizedFull = normalizeFrequencyString(foundRaw);
finalDetectedFrequency = Double.parseDouble(normalizedFull);
finalDetectedBand = Band.fromFrequency(finalDetectedFrequency);
} catch (NumberFormatException e) { continue; }
}
// --- STEP 2: Context Resolution (Only needed for Short Forms) ---
if (isShortForm) {
// A) HISTORY CHECK (Priority 1: What did THIS USER do recently?)
// We search for the most recent band where this short form makes physical sense.
long bestTimestamp = 0;
// Iterate over all bands where the user is known
// (Assumption: ChatMember has a getter getKnownActiveBands())
if (sender.getKnownActiveBands() != null) {
for (java.util.Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry : sender.getKnownActiveBands().entrySet()) {
Band candidateBand = entry.getKey();
ChatMember.ActiveFrequencyInfo info = entry.getValue();
// Timeout Check: Info must not be older than 30 mins (1,800,000 ms)
if (System.currentTimeMillis() - info.timestampEpoch > 1800000) continue;
// Try Reconstruction: Band Prefix + ShortForm
// Example: Band 144 (Prefix "144") + "." + "210.10" -> "144.210.10"
try {
String reconstructedStr = candidateBand.getPrefix() + "." + foundRaw;
String normalizedReconstruction = normalizeFrequencyString(reconstructedStr);
double attemptFreq = Double.parseDouble(normalizedReconstruction);
// Does this frequency fit into the candidate band?
if (candidateBand.isPlausible(attemptFreq)) {
// If we have multiple matches, pick the most recent one
if (info.timestampEpoch > bestTimestamp) {
finalDetectedFrequency = attemptFreq;
finalDetectedBand = candidateBand;
bestTimestamp = info.timestampEpoch;
}
}
} catch (Exception e) { /* Ignore parsing errors */ }
}
}
// B) GLOBAL PREFERENCES CHECK (Priority 2: Fallback if history is empty/old)
if (finalDetectedBand == null) {
// Get standard band from prefs (e.g., "144" or "432")
String defaultPrefix = prefs.getNotify_optionalFrequencyPrefix().get();
try {
String reconstructedStr = defaultPrefix + "." + foundRaw;
String normalizedReconstruction = normalizeFrequencyString(reconstructedStr);
double attemptFreq = Double.parseDouble(normalizedReconstruction);
// Check if this results in a valid amateur radio band
Band defaultBandCandidate = Band.fromFrequency(attemptFreq);
if (defaultBandCandidate != null) {
finalDetectedFrequency = attemptFreq;
finalDetectedBand = defaultBandCandidate;
}
} catch (NumberFormatException e) {
// Number was likely not a frequency (e.g., "73" or "599") and didn't fit any band
continue;
}
}
}
// --- STEP 3: Process Result ---
if (finalDetectedBand != null && finalDetectedFrequency > 0) {
// 1. Store in the new Map (for future context/history)
sender.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
//propagate known frequency to all instances of the same callsign (callRaw may exist multiple times)
try {
ArrayList<Integer> sameCallIdx = client.checkListForChatMemberIndexesByCallSign(sender);
for (int idx : sameCallIdx) {
ChatMember cm = client.getLst_chatMemberList().get(idx);
if (cm != null && cm != sender) {
cm.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
}
}
} catch (Exception e) {
System.out.println("[SmartParser, warning]: failed to propagate known frequency across duplicates: " + e.getMessage());
}
// 2. Set the old String-Property for GUI compatibility
// We assume standard display format (MHz)
sender.setFrequency(new javafx.beans.property.SimpleStringProperty(String.valueOf(finalDetectedFrequency)));
System.out.println("[SmartParser] Detected for " + sender.getCallSign() + ": " +
finalDetectedFrequency + " MHz (" + finalDetectedBand + ") " +
(isShortForm ? "[derived from " + foundRaw + "]" : "[full match]"));
// Optional: Trigger Cluster-Spot here if enabled
}
}
}
/**
* Helper: Normalizes weird frequency formats to valid Double strings.
* Example: "144.210.10" -> "144.21010"
* Example: "144.210" -> "144.210"
*/
private String normalizeFrequencyString(String rawInput) {
// Input is already guaranteed to have only dots as separators (commas replaced earlier)
int firstDotIndex = rawInput.indexOf(".");
if (firstDotIndex != -1) {
// Check if there are more dots after the first one
String decimalPart = rawInput.substring(firstDotIndex + 1);
if (decimalPart.contains(".")) {
// Remove all subsequent dots to make it a valid double
decimalPart = decimalPart.replace(".", "");
return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
}
}
return rawInput;
}
/** /**
* Builds UserList and gets meta informations out of the chat, as far as it is * Builds UserList and gets meta informations out of the chat, as far as it is
* possible. \n This is the only place where the Chatmember-List will be written * possible. \n This is the only place where the Chatmember-List will be written
@@ -300,11 +496,14 @@ public class MessageBusManagementThread extends Thread {
*/ */
private void processRXMessage23001(ChatMessage messageToProcess) throws IOException, SQLException { private void processRXMessage23001(ChatMessage messageToProcess) throws IOException, SQLException {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last message processed:\n" + messageToProcess.getMessageText(), false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
final String INITIALUSERLISTENTRY = "UA0"; final String INITIALUSERLISTENTRY = "UA0";
final String USERENTEREDCHAT = "UA5"; final String USERENTEREDCHAT = "UA5";
final String USERENTEREDCHAT2 = "UA2"; // seen at 50MHZ Chat final String USERENTEREDCHAT2 = "UA2"; // seen at 50MHZ Chat
final String initialChatHistoryEntry = "CR"; final String initialChatHistoryEntry = "CR";
final String SERVERMESSAGE = "CR"; final String SERVERMESSAGEHISTORIC = "CR"; //takes messages out of the ON4KST history
final String USERLEFTCHAT = "UR6"; final String USERLEFTCHAT = "UR6";
final String USERLEFTCHAT2 = "UR7"; final String USERLEFTCHAT2 = "UR7";
final String CHATCHANNELMESSAGE = "CH"; final String CHATCHANNELMESSAGE = "CH";
@@ -335,7 +534,7 @@ public class MessageBusManagementThread extends Thread {
qrgQuestionTexts.add("your qrg?"); qrgQuestionTexts.add("your qrg?");
qrgQuestionTexts.add("qrg?"); qrgQuestionTexts.add("qrg?");
qrgQuestionTexts.add("freq?"); qrgQuestionTexts.add("freq?");
qrgQuestionTexts.add("pse QRG"); qrgQuestionTexts.add("pse qrg");
/** /**
@@ -343,7 +542,7 @@ public class MessageBusManagementThread extends Thread {
*/ */
if (messageToProcess.getMessageText().isEmpty()) { if (messageToProcess.getMessageText().isEmpty()) {
System.out.println("[MSGBUSMGTT:] ######################no processable data"); // System.out.println("[MSGBUSMGTT:] no processable data");
} else { } else {
@@ -364,6 +563,7 @@ public class MessageBusManagementThread extends Thread {
* Initializes the Userlist if entry fits UA0 * Initializes the Userlist if entry fits UA0
* UA0|3|DL6SAQ|walter not qrv|JN58CK|1| <- RXed * UA0|3|DL6SAQ|walter not qrv|JN58CK|1| <- RXed
* *
*
*/ */
if (splittedMessageLine[0].contains(INITIALUSERLISTENTRY)) { if (splittedMessageLine[0].contains(INITIALUSERLISTENTRY)) {
// System.out.println("MSGBUS: User detected"); // System.out.println("MSGBUS: User detected");
@@ -384,16 +584,15 @@ public class MessageBusManagementThread extends Thread {
newMember.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());//TODO evt obsolete! newMember.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());//TODO evt obsolete!
newMember.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime()); newMember.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// this.client.getChatMemberTable().put(splittedMessageLine[2], newMember); //TODO: map -> List
//the own call will not be in the list
if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) { if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) {
this.client.getLst_chatMemberList().add(newMember); this.client.getLst_chatMemberList().add(newMember); //the own call will not be in the list
} }
this.client.getDbHandler().storeChatMember(newMember); this.client.getDbHandler().storeChatMember(newMember);
// bufwrtrDBGMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() // bufwrtrDBGMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
// + "[MSGBUSMGT:] User detected and added to list [" + this.client.getChatMemberTable().size() // + "[MSGBUSMGT:] User detected and added to list [" + this.client.getChatMemberTable().size()
// + "] :" + newMember.getCallSign() + "\n"); // + "] :" + newMember.getCallSign() + "\n");
@@ -438,6 +637,8 @@ public class MessageBusManagementThread extends Thread {
} }
this.client.fireUserListUpdate("User entered the chat");
// this.client.getChatMemberTable().put(splittedMessageLine[2], newMember); // this.client.getChatMemberTable().put(splittedMessageLine[2], newMember);
// System.out.println("[MSGBUSMGT:] New entered User detected and added to list [" // System.out.println("[MSGBUSMGT:] New entered User detected and added to list ["
@@ -462,29 +663,14 @@ public class MessageBusManagementThread extends Thread {
this.client.getLst_chatMemberList().remove( this.client.getLst_chatMemberList().remove(
checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), newMember)); checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), newMember));
//TODO: since 1.26 new method design to detect chatcategory, too! //since 1.26 new method design to detect chatcategory, too!
} catch (Exception e) { } catch (Exception e) {
System.out.println("[MSGBUSMGT, EXC!, Error:] User sent left chat but had not been there ... [" System.out.println("[MSGBUSMGT, EXC!, Error:] User sent left chat but had not been there ... ["
+ this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign() + "\n" + this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign() + "\n"
+ e.getStackTrace()); + e.getStackTrace());
// e.printStackTrace();
} }
// int indexToDelete = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(),
// newMember);
// if (indexToDelete != -1) {
// System.out.println("[MSGBUSMGT:] User left Chat and is removed from list ["
// + this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign());
//
// this.client.getLst_chatMemberList().remove(indexToDelete);
//
// } else {
// System.out.println("[MSGBUSMGT:] Error, user sent left chat but had not been there ... ["
// + this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign());
//
// }
} else } else
/** /**
@@ -524,8 +710,28 @@ public class MessageBusManagementThread extends Thread {
if (index != -1) { if (index != -1) {
//user not found in the chatmember list //user not found in the chatmember list
try { try {
newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of // newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime()); // this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
newMessageArrived.setSender(senderObj);
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// Remember last inbound category per callsignRaw (required for correct send-routing later)
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
// Metrics for scoring: momentum, response-time, no-reply, positive signals
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
newMessageArrived.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
);
// Activity/category changes influence priority => request recompute
this.client.getScoreService().requestRecompute("rx-chat-message");
} catch (Exception exc) { } catch (Exception exc) {
ChatMember aSenderDummy = new ChatMember(); ChatMember aSenderDummy = new ChatMember();
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]"); aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
@@ -611,8 +817,7 @@ public class MessageBusManagementThread extends Thread {
if (newMessageArrived.getReceiver().getCallSign() if (newMessageArrived.getReceiver().getCallSign()
.equals(this.client.getChatPreferences().getStn_loginCallSign())) { .equals(this.client.getChatPreferences().getStn_loginCallSign())) {
// this.client.getLst_toMeMessageList().add(0, newMessageArrived); //TODO: change, moved to globalmessagelist, original this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
this.client.getLst_globalChatMessageList().add(0, newMessageArrived); //TODO: change, moved to globalmessagelist, original
if (this.client.getChatPreferences().isNotify_playSimpleSounds()) { if (this.client.getChatPreferences().isNotify_playSimpleSounds()) {
this.client.getPlayAudioUtils().playNoiseLauncher('P'); this.client.getPlayAudioUtils().playNoiseLauncher('P');
@@ -629,28 +834,100 @@ public class MessageBusManagementThread extends Thread {
this.client.getPlayAudioUtils().playVoiceLauncher("!"); this.client.getPlayAudioUtils().playVoiceLauncher("!");
} }
} }
if (newMessageArrived.getMessageText().toUpperCase().contains("//VER")) {
if (this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled()) { ChatMessage versionInfo = new ChatMessage();
ChatMessage automaticAnswer = new ChatMessage();
ChatMember itsMe = new ChatMember(); ChatMember itsMe = new ChatMember();
itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign()); itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
automaticAnswer.setSender(itsMe); versionInfo.setSender(itsMe);
automaticAnswer.setReceiver(newMessageArrived.getSender()); versionInfo.setReceiver(newMessageArrived.getSender());
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat()); versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + ApplicationConstants.AUTOANSWER_PREFIX + " " + "KST4Contest " + " v" + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER + " by DO5AMF");
this.client.getMessageTXBus().add(automaticAnswer);
this.client.getMessageTXBus().add(versionInfo);
} }
// if (this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled()) {
//
// ChatMessage automaticAnswer = new ChatMessage();
// ChatMember itsMe = new ChatMember();
// itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
//
// automaticAnswer.setSender(itsMe);
// automaticAnswer.setReceiver(newMessageArrived.getSender());
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat());
//
// this.client.getMessageTXBus().add(automaticAnswer);
//
// }
/** /**
* auto reply/answer to QRG requests is here * auto reply/answer to QRG requests is here
*/ */
if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) { // if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) {
//
// for (String lookForQRGString : qrgQuestionTexts) {
// if (newMessageArrived.getMessageText().contains(lookForQRGString)) {
//
// ChatMessage automaticAnswer = new ChatMessage();
// ChatMember itsMe = new ChatMember();
// itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
//
// automaticAnswer.setSender(itsMe);
// automaticAnswer.setReceiver(newMessageArrived.getSender());
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
//
// if (this.client.getChatPreferences().isLoginToSecondChatEnabled()) {
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRGs: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue() + " / " + this.client.getChatPreferences().getMYQRGSecondCat().getValue());
// } else {
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
// }
//
// this.client.getMessageTXBus().add(automaticAnswer);
//
// }
// }
// }
// ==== Unified Autoanswer (Generic + QRG) with Pingpong-Guard + per-Remote Cooldown ====
final String incomingText = newMessageArrived.getMessageText();
final String incomingLower = (incomingText == null) ? "" : incomingText.toLowerCase(Locale.ROOT);
// 1) Pingpong-security: never ever react to auto generated messages
if (!isAutoMessage(newMessageArrived)) {
boolean qrgRequested = false;
if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) {
for (String lookForQRGString : qrgQuestionTexts) { for (String lookForQRGString : qrgQuestionTexts) {
if (newMessageArrived.getMessageText().contains(lookForQRGString)) { if (incomingLower.contains(lookForQRGString)) {
qrgRequested = true;
break;
}
}
}
boolean genericEnabled = this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled();
// 2) Entscheide, ob überhaupt geantwortet wird (QRG hat Vorrang vor Generic)
String payload = null;
if (qrgRequested) {
if (this.client.getChatPreferences().isLoginToSecondChatEnabled()) {
payload = "QRGs: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue()
+ " / " + this.client.getChatPreferences().getMYQRGSecondCat().getValue();
} else {
payload = "QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue();
}
} else if (genericEnabled) {
payload = this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat();
}
// 3) Cooldown pro Gegenstation: nur wenn DIESER Client jetzt wirklich sendet
if (payload != null && isAutoAnswerAllowedNow(newMessageArrived)) {
ChatMessage automaticAnswer = new ChatMessage(); ChatMessage automaticAnswer = new ChatMessage();
ChatMember itsMe = new ChatMember(); ChatMember itsMe = new ChatMember();
@@ -658,19 +935,19 @@ public class MessageBusManagementThread extends Thread {
automaticAnswer.setSender(itsMe); automaticAnswer.setSender(itsMe);
automaticAnswer.setReceiver(newMessageArrived.getSender()); automaticAnswer.setReceiver(newMessageArrived.getSender());
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
if (this.client.getChatPreferences().isLoginToSecondChatEnabled()) { // Prefix fest + nicht entfernbar, damit Auto↔Auto nicht pingpongt
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRGs: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue() + " / " + this.client.getChatPreferences().getMYQRGSecondCat().getValue()); automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
} else { + " " + AUTOANSWER_PREFIX + " " + payload);
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
}
this.client.getMessageTXBus().add(automaticAnswer); this.client.getMessageTXBus().add(automaticAnswer);
// Cooldown wird NUR hier gesetzt (nicht bei 'message sent by me' Echo),
// damit nur lokale Auto-Sends zählen.
markLocalAutoAnswerSent(newMessageArrived);
} }
} }
}
System.out.println("message directed to me: " + newMessageArrived.getReceiver().getCallSign() + "."); System.out.println("message directed to me: " + newMessageArrived.getReceiver().getCallSign() + ".");
@@ -690,7 +967,6 @@ public class MessageBusManagementThread extends Thread {
} else { } else {
//message sent to other user //message sent to other user
// this.client.getLst_toOtherMessageList().add(0, newMessageArrived); //TODO: change, moved to globalmessagelist, original
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(), if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
newMessageArrived.getSender().getQra(), newMessageArrived.getSender().getQra(),
newMessageArrived.getReceiver().getQra(), newMessageArrived.getReceiver().getQra(),
@@ -709,11 +985,31 @@ public class MessageBusManagementThread extends Thread {
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) { if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
try { try {
if (newMessageArrived.getSender().getFrequency() != null) { if (newMessageArrived.getSender().getFrequency() != null) {
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(newMessageArrived.getSender()); //tells the DXCluster server to send a DXC message for this member to the logbook software //TODO: testing for next version 3.33: addinitional information will be displayed in cluster if there is such an information
ChatMember onlyForSpottingObject = new ChatMember();
onlyForSpottingObject.setCallSign(newMessageArrived.getSender().getCallSign());
onlyForSpottingObject.setFrequency(newMessageArrived.getSender().getFrequency());
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 0) {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + " , AP: " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getArrivingDurationMinutes() + "min, " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getPotential() + "%");
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 1) {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + "; " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getArrivingDurationMinutes() + "min, " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getPotential() + "%");
}
} else {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra());
}
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject); //tells the DXCluster server to send a DXC message for this member to the logbook software
} }
} catch (Exception exception) { } catch (Exception exception) {
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0" + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage()); System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
exception.printStackTrace(); // exception.printStackTrace();
} }
} }
@@ -755,50 +1051,8 @@ public class MessageBusManagementThread extends Thread {
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace()); System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
} }
String locatedFrequencies = checkIfMessageInhibitsFrequency(newMessageArrived); // --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
smartFrequencyExtraction(newMessageArrived, this.client.getChatPreferences());
SimpleStringProperty qrg = new SimpleStringProperty(locatedFrequencies);
if (!splittedMessageLine[3].equals("SERVER")) {
if (locatedFrequencies.equals("")) {
// no qrg found, nothing to do
} else {
ChatMember temp3 = new ChatMember();
temp3.setCallSign(splittedMessageLine[3]);
temp3.setChatCategory(chategoryForMessageAndMessageSender);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), temp3);
if (index == -1) { // user is not in the userlist but sent message...
/**
* CH|2|1664663240|IK7LMX|Gilberto QRO|0|pse ant to jn80|YT5W| Caused this line
*/
System.out.println("[MSGBUSMGT <<<catched ERROR>>>]:, Frequency for " + splittedMessageLine[3]
+ " is not settable, Callsign is not in the Member-list!");
//create dummy user to display the message but it wont be hit an existing user object
ChatMember newMember = new ChatMember();
newMember.setCallSign(splittedMessageLine[3]);
newMember.setName(splittedMessageLine[4]);
newMember.setFrequency(qrg);
} else {
/**
* User is in the list...
*/
this.client.getLst_chatMemberList().get(index).setFrequency(qrg);
System.out.println("[MSGBUSMGT:] Frequency for " + splittedMessageLine[3] + " setted: "
+ locatedFrequencies);
// this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(this.client.getLst_chatMemberList().get(index)); //tells the DXCluster server to send a DXC message for this member to the logbook software
}
}
}
// TODO: Next: get frequency infos out of name? // TODO: Next: get frequency infos out of name?
} else } else
@@ -1006,6 +1260,214 @@ public class MessageBusManagementThread extends Thread {
this.client.getLst_chatMemberList().get(index).setState(stateChangeMember.getState()); this.client.getLst_chatMemberList().get(index).setState(stateChangeMember.getState());
} }
} else
/**
* Handled like normal messages, but historic...will not trigger any functions
*
* Chat history line like:
* CR|6|1771165971|DF0GEB|test|0|ok|0|
* ^^hist
* ^chan
* ^^^^^^^^^^time ...
*/
if (splittedMessageLine[0].contains(SERVERMESSAGEHISTORIC)) {
ChatMessage newMessageArrived = new ChatMessage();
ChatCategory chategoryForMessageAndMessageSender;
newMessageArrived.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
if (splittedMessageLine[3].equals("SERVER")) {
ChatMember dummy = new ChatMember();
dummy.setCallSign("SERVER");
dummy.setName("Sysop");
newMessageArrived.setSender(dummy);
newMessageArrived.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
dummy.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
// System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> servers cat " + newMessageArrived.getChatCategory());
} else {
ChatMember sender = new ChatMember();
sender.setCallSign(splittedMessageLine[3]);
sender.setChatCategory(chategoryForMessageAndMessageSender);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), sender);
if (index != -1) {
//user not found in the chatmember list
try {
// newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
// this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
newMessageArrived.setSender(senderObj);
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// Remember last inbound category per callsignRaw (required for correct send-routing later)
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
// Metrics for scoring: momentum, response-time, no-reply, positive signals
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
newMessageArrived.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
);
// Activity/category changes influence priority => request recompute
this.client.getScoreService().requestRecompute("rx-chat-message");
} catch (Exception exc) {
ChatMember aSenderDummy = new ChatMember();
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
aSenderDummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
newMessageArrived.setSender(aSenderDummy);
System.out.println("MsgBusmgtT: Catched Error! " + exc.getMessage() + " // " + splittedMessageLine[3] + " is not in the list! Faking sender!");
exc.printStackTrace();
}
// b4 init list
} else {
//user not found in chatmember list, mark it, sender can not be set
if (!sender.getCallSign().equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
sender.setCallSign("[n/a]" + sender.getCallSign());
// if someone sent a message without being in the userlist (cause
// on4kst missed implementing....), callsign will be marked
} else {
//that means, message was by own station, broadcasted to all other
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
AirPlaneReflectionInfo preventNullpointerExc = new AirPlaneReflectionInfo();
preventNullpointerExc.setAirPlanesReachableCntr(0);
sender.setAirPlaneReflectInfo(preventNullpointerExc);
newMessageArrived.setSender(sender); //my own call is the sender
}
}
// newMessageArrived.setSender(this.client.getChatMemberTable().get(splittedMessageLine[3]));
}
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
newMessageArrived.setMessageText(splittedMessageLine[6]);
if (splittedMessageLine[7].equals("0")) {
// message is not directed to anyone, move it to the cq messages!
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
} else {
//message is directed to another chatmember, process as such!
ChatMember receiver = new ChatMember();
receiver.setChatCategory(chategoryForMessageAndMessageSender); //got out of message itself
receiver.setCallSign(splittedMessageLine[7]);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), receiver);
if (index != -1) {
newMessageArrived.setReceiver(this.client.getLst_chatMemberList().get(index));// -1: Member left Chat
// before...
} else { //found in active member list
if (receiver.getCallSign().equals(client.getChatPreferences().getStn_loginCallSign())) {
/**
* If mycallsign sent a message to the server, server will publish that message and
* send it to all chatmember including me.
* As mycall is not in the userlist, the message would not been displayed if I handle
* it in the next case (marking left user, just for information). But I want an echo.
*/
receiver.setCallSign(client.getChatPreferences().getStn_loginCallSign());
newMessageArrived.setReceiver(receiver);
} else {
//this are user which left chat but had been adressed by this message
receiver.setCallSign(receiver.getCallSign() + "(left)");
newMessageArrived.setReceiver(receiver);
}
}
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
try {
/**
* message is directed to me, will be put in the "to me" messagelist
*/
if (newMessageArrived.getReceiver().getCallSign()
.equals(this.client.getChatPreferences().getStn_loginCallSign())) {
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
System.out.println("Historic message directed to me: " + newMessageArrived.getReceiver().getCallSign() + ".");
} else if (newMessageArrived.getSender().getCallSign().toUpperCase()
.equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
/**
* message sent by me!
* message from me will appear in the PM window, too, with (>CALLSIGN) before
*/
String originalMessage = newMessageArrived.getMessageText();
newMessageArrived
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
this.client.getLst_globalChatMessageList().add(0,newMessageArrived);
// if you sent the message to another station, it will be sorted in to
// the "to me message list" with modified messagetext, added rxers callsign
} else {
//message sent to other user
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
newMessageArrived.getSender().getQra(),
newMessageArrived.getReceiver().getQra(),
client.getChatPreferences().getStn_maxQRBDefault(),
client.getChatPreferences().getStn_antennaBeamWidthDeg())) {
newMessageArrived.getSender().setInAngleAndRange(true);
} else {
newMessageArrived.getSender().setInAngleAndRange(false);
}
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
}
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: " + referenceDeletedByUserLeftChatDuringMessageprocessing.getStackTrace());
// referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
}
// sdtout to me message-List
}
try {
System.out.println("[MSGBUSMGT:] processed message: " + newMessageArrived.getChatCategory().getCategoryNumber()
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
} catch (Exception exceptionOccured) {
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
}
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
smartFrequencyExtraction(newMessageArrived, this.client.getChatPreferences());
} else } else
/** /**
@@ -1120,6 +1582,47 @@ public class MessageBusManagementThread extends Thread {
} }
/**
* check if message had been auto generated
* @param msg
* @return
*/
private boolean isAutoMessage(ChatMessage msg) {
return msg != null
&& msg.getMessageText() != null
&& msg.getMessageText().contains(AUTOANSWER_PREFIX);
}
private String autoAnswerCooldownKey(ChatMessage incoming) {
String remoteCall = "UNKNOWN";
if (incoming != null && incoming.getSender() != null && incoming.getSender().getCallSign() != null) {
remoteCall = incoming.getSender().getCallSign().toUpperCase();
}
int cat = 0; // fallback
if (incoming != null && incoming.getSender() != null && incoming.getSender().getChatCategory() != null) {
cat = incoming.getSender().getChatCategory().getCategoryNumber();
}
// pro Gegenstation + pro Chat-Kategorie (falls derselbe Call in Cat2/Cat3 PMs macht)
return remoteCall + "|" + cat;
}
private boolean isAutoAnswerAllowedNow(ChatMessage incoming) {
String key = autoAnswerCooldownKey(incoming);
Long last = lastLocalAutoAnswerPerRemoteMs.get(key);
long now = System.currentTimeMillis();
return last == null || (now - last) >= AUTOANSWER_COOLDOWN_MS;
}
private void markLocalAutoAnswerSent(ChatMessage incoming) {
lastLocalAutoAnswerPerRemoteMs.put(autoAnswerCooldownKey(incoming), System.currentTimeMillis());
}
public void run() { public void run() {
// fileLogRAW = new File(new Utils4KST().time_generateCurrentMMddString() + "_praktiKST_raw.txt"); // fileLogRAW = new File(new Utils4KST().time_generateCurrentMMddString() + "_praktiKST_raw.txt");
@@ -1178,7 +1681,7 @@ public class MessageBusManagementThread extends Thread {
try { try {
messageTextRaw = client.getMessageRXBus().take(); messageTextRaw = client.getMessageRXBus().take();
if (messageTextRaw.getMessageText().equals("POISONPILL_KILLTHREAD") && messageTextRaw.getMessageSenderName().equals("POISONPILL_KILLTHREAD")) { if (messageTextRaw.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL) && messageTextRaw.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
client.getMessageRXBus().clear(); client.getMessageRXBus().clear();
break; break;
} }
@@ -1422,7 +1925,6 @@ public class MessageBusManagementThread extends Thread {
// } //end tx.peek != null // } //end tx.peek != null
} }
// System.out.println("messagebusmgt while performed");
} // while true end } // while true end
System.out.println("Msgbusmgt: interrupt"); System.out.println("Msgbusmgt: interrupt");

View File

@@ -0,0 +1,237 @@
package kst4contest.controller;
import kst4contest.controller.interfaces.PstRotatorEventListener;
import kst4contest.model.ThreadStateMessage;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PstRotatorClient implements Runnable {
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "PSTRotator";
private static final Logger LOGGER = Logger.getLogger(PstRotatorClient.class.getName());
private static final int BUFFER_SIZE = 1024;
// Konfiguration
private final String host;
private final int remotePort; // Port, auf dem PSTRotator hört (z.B. 12060)
private final int localPort; // Port, auf dem wir hören (z.B. 12061)
private DatagramSocket socket;
private volatile boolean running = false;
private PstRotatorEventListener listener;
// Executor für Polling (Status-Abfrage)
private ScheduledExecutorService poller;
/**
* Konstruktor
* @param host IP Adresse von PSTRotator (meist "127.0.0.1")
* @param remotePort Der Port, der in PSTRotator eingestellt ist (User-Wunsch: 12060)
* @param listener Callback für den Chatcontroller
*/
public PstRotatorClient(String host, int remotePort, PstRotatorEventListener listener, ThreadStatusCallback callBack) {
this.callBackToController = callBack;
this.host = host;
this.remotePort = remotePort;
// Laut Manual antwortet PSTRotator oft auf Port+1.
// Wir binden uns also standardmäßig auf remotePort + 1.
this.localPort = remotePort + 1;
this.listener = listener;
}
/**
* alternative constructor for seting the remote port explicitely
*/
public PstRotatorClient(String host, int remotePort, int localPort, PstRotatorEventListener listener) {
this.host = host;
this.remotePort = remotePort;
this.localPort = localPort;
this.listener = listener;
}
/**
* Startet den Empfangs-Thread und das Polling
*/
public void start() {
try {
// Socket binden
// socket = new DatagramSocket(null);
// socket.setReuseAddress(true);
// socket = new DatagramSocket(localPort);
//
socket = new DatagramSocket(null);
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(localPort)); //bind to port
running = true;
// 1. Empfangs-Thread starten (dieses Runnable)
Thread thread = new Thread(this, "PSTRotator-Listener-" + remotePort);
thread.start();
// 2. Polling starten (z.B. alle 2 Sekunden Status abfragen)
poller = Executors.newSingleThreadScheduledExecutor();
poller.scheduleAtFixedRate(this::pollStatus, 1, 2, TimeUnit.SECONDS);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, running, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
LOGGER.info("PstRotatorClient started. Remote: " + remotePort + ", Local: " + localPort);
} catch (SocketException e) {
LOGGER.log(Level.SEVERE, "Fehler beim Öffnen des UDP Sockets", e);
}
}
/**
* Stopping threads and closing sockets of pstRotator communicator
*/
public void stop() {
running = false;
if (poller != null && !poller.isShutdown()) {
poller.shutdownNow();
}
if (socket != null && !socket.isClosed()) {
socket.close();
}
}
/**
* Main loop in thread which listens fpr PSTrotator packets
*/
@Override
public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
while (running && !socket.isClosed()) {
try {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // Blockiert bis Daten kommen
String received = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + received, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
parseResponse(received);
} catch (IOException e) {
if (running) {
LOGGER.log(Level.WARNING, "Fehler beim Empfangen des Pakets", e);
}
}
}
}
/**
* parses a pst rotatpr message to fit the PST listener interface
* @param msg
*/
private void parseResponse(String msg) {
// Debug
if (listener != null) listener.onMessageReceived(msg);
// Example answer: "AZ:145.0<CR>", "EL:010.0<CR>", "MODE:1<CR>"
msg = msg.replace("<CR>", "").trim();
try {
if (msg.startsWith("AZ:")) {
String val = msg.substring(3);
if (listener != null) listener.onAzimuthUpdate(Double.parseDouble(val));
}
else if (msg.startsWith("EL:")) {
String val = msg.substring(3);
if (listener != null) listener.onElevationUpdate(Double.parseDouble(val));
}
else if (msg.startsWith("MODE:")) {
// MODE:1 = Tracking, MODE:0 = Manual
String val = msg.substring(5);
boolean tracking = "1".equals(val);
if (listener != null) listener.onModeUpdate(tracking);
}
else if (msg.startsWith("OK:")) {
// Bestätigung von Befehlen, z.B. OK:STOP:1
LOGGER.fine("Befehl bestätigt: " + msg);
}
} catch (NumberFormatException e) {
LOGGER.warning("Konnte Wert nicht parsen: " + msg);
}
}
// --- Sende Methoden (API für den Chatcontroller) ---
private void sendUdp(String message) {
if (socket == null || socket.isClosed()) return;
try {
byte[] data = message.getBytes(StandardCharsets.US_ASCII);
InetAddress address = InetAddress.getByName(host);
DatagramPacket packet = new DatagramPacket(data, data.length, address, remotePort);
socket.send(packet);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Fehler beim Senden an PstRotator", e);
}
}
/**
* Sendet den generischen XML Befehl.
* Bsp: <PST><AZIMUTH>85</AZIMUTH></PST>
*/
private void sendCommand(String tag, String value) {
String xml = String.format("<PST><%s>%s</%s></PST>", tag, value, tag);
System.out.println("PSTRotatorClient: sent: " + xml);
sendUdp(xml);
}
// Öffentliche Steuermethoden
public void setAzimuth(double degrees) {
// Formatierung ohne unnötige Nachkommastellen, falls nötig
sendCommand("AZIMUTH", String.valueOf((int) degrees));
}
public void setElevation(double degrees) {
sendCommand("ELEVATION", String.valueOf(degrees));
}
public void stopRotor() {
sendCommand("STOP", "1");
}
public void park() {
sendCommand("PARK", "1");
}
public void setTrackingMode(boolean enable) {
sendCommand("TRACK", enable ? "1" : "0");
}
/**
* Method for polling rotators status via PSTRotator software. Asks only for AZ value!<br/>
* Scheduled in a fixed time by executor
*/
public void pollStatus() {
// PSTRotator Dokumentation:
// <PST>AZ?</PST>
// <PST>EL?</PST>
// <PST>MODE?</PST>
// Man kann mehrere Befehle in einem Paket senden
String query = "<PST><AZ?></AZ?><EL?></EL?><MODE?></MODE?></PST>";
// HINWEIS: Laut Doku ist die Syntax für Abfragen etwas anders: <PST>AZ?</PST>
// Daher bauen wir den String manuell, da sendCommand Tags schließt.
sendUdp("<PST>AZ?</PST>");
// sendUdp("<PST>EL?</PST>");
sendUdp("<PST>MODE?</PST>");
}
}

View File

@@ -0,0 +1,328 @@
package kst4contest.controller;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
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;
public class ReadUDPByWintestThread extends Thread {
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);
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_wintestNetworkPort()));
socket.setSoTimeout(3000);
System.out.println("[WinTest UDP listener] started at port: " + PORT);
} 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
}
}
}
private 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("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);
}
// 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));
// }
/**
* 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) {
ChatMember modifyThat = null;
try {
// int qsoNumber = extractQsoNumber(msg);
// receivedQsos.put(qsoNumber, msg);
// lastKnownQso = Math.max(lastKnownQso, qsoNumber);
String callSignCatched = msg.split("\"") [7];
ChatMember workedCall = new ChatMember();
workedCall.setCallSign(callSignCatched);
workedCall.setWorked(true); //its worked at this place, for sure!
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
String bandId;
bandId = msg.split("\"")[6].split(" ")[4].trim();
switch (bandId) {
case "10" -> workedCall.setWorked50(true);
case "11" -> workedCall.setWorked70(true);
case "12" -> workedCall.setWorked144(true);
case "14" -> workedCall.setWorked432(true);
case "16" -> workedCall.setWorked1240(true);
case "17" -> workedCall.setWorked2300(true);
case "18" -> workedCall.setWorked3400(true);
case "19" -> workedCall.setWorked5600(true);
case "20" -> workedCall.setWorked10G(true);
case "21" -> workedCall.setWorked24G(true);
case "22" -> workedCall.setWorked47G(true);
case "23" -> workedCall.setWorked76G(true);
default -> System.out.println("[WinTestUDPRcvr: warning] Unbekannte Band-ID: " + bandId);
}
if (!markTheseChattersAsWorked.isEmpty()) {
//Worked call is part of the current chatmember list
for (int index : markTheseChattersAsWorked) {
//iterate through the logged in chatmembers callsigns and set the worked markers
modifyThat = client.getLst_chatMemberList().get(index);
modifyThat.setWorked(true); //worked its for sure
if (workedCall.isWorked50()) {
modifyThat.setWorked50(true);
} else if (workedCall.isWorked70()) {
modifyThat.setWorked70(true);
} else if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
} else if (workedCall.isWorked24G()) {
modifyThat.setWorked24G(true);
} else if (workedCall.isWorked47G()) {
modifyThat.setWorked47G(true);
} else if (workedCall.isWorked76G()) {
modifyThat.setWorked76G(true);
} else {
System.out.println("[WinTestUDPRcvr: warning] found no new worked-flag for this band: " + workedCall.getCallSignRaw() + bandId);
}
}
try {
GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
// trigger band-upgrade hint after log entry (Win-Test)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[WinTestUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
} catch (Exception IllegalStateException) {
//do nothing, as it works...
}
}
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");
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);
if (modifyThat != null) {
bufwrtrRawMSGOut.write("\n" + modifyThat.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
} else {
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());
}
}
}

View File

@@ -0,0 +1,18 @@
package kst4contest.controller;
import kst4contest.model.ChatPreferences;
public class ReadUDPByWintestThreadTest {
public static void main(String[] args) {
ChatController ctrl1 = new ChatController();
ChatPreferences prefs = new ChatPreferences();
ctrl1.setChatPreferences(prefs);
// ReadUDPByWintestThread test = new ReadUDPByWintestThread(ctrl1);
// test.run();
}
}

View File

@@ -2,6 +2,7 @@ package kst4contest.controller;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@@ -10,6 +11,7 @@ import kst4contest.ApplicationConstants;
import kst4contest.model.AirPlane; import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo; import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember; import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
/** /**
* This thread is responsible for reading server's input and printing it to the * This thread is responsible for reading server's input and printing it to the
@@ -24,15 +26,16 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
private ChatController client; private ChatController client;
private int localPort; private int localPort;
private String ASIdentificator, ChatClientIdentificator; private String ASIdentificator, ChatClientIdentificator;
private ThreadStatusCallback callBackToController;
public ReadUDPbyAirScoutMessageThread(int localPort) { private String ThreadNickName = "AirScout msg";
this.localPort = localPort; // public ReadUDPbyAirScoutMessageThread(int localPort) {
} // this.localPort = localPort;
// }
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator, public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator,
String ChatClientIdentificator) { String ChatClientIdentificator, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.localPort = localPort; this.localPort = localPort;
this.client = client; this.client = client;
this.ASIdentificator = ASIdentificator; this.ASIdentificator = ASIdentificator;
@@ -54,6 +57,12 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
} }
} }
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) {
if (callBackToController != null) {
//update the visual control of running thread
callBackToController.onThreadStatus("AirScout", threadStateMessage);
}
}
public void run() { public void run() {
@@ -128,26 +137,30 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) { if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
// do nothing, that is your own message // do nothing, that is your own message
} else if (received.contains("ASNEAREST:")) { //answer by airscout } else if (received.contains("ASNEAREST:")) { //answer by airscout
processASUDPMessage(received);
// System.out.println("[ReadUSPASTh, info:] received AS String " + received); // processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
AirPlaneReflectionInfo apReflectInfoForChatMember; AirPlaneReflectionInfo apReflectInfoForChatMember;
apReflectInfoForChatMember = processASUDPMessage(received); apReflectInfoForChatMember = processASUDPMessage(received);
if (this.client.getLst_chatMemberList().size() != 0) { if (!this.client.getLst_chatMemberList().isEmpty()) {
try { try {
// if (this.client.checkListForChatMemberIndexByCallSign(apReflectInfoForChatMember.getReceiver()) != -1) { // this.client.getLst_chatMemberList()
// .get(this.client.checkListForChatMemberIndexByCallSign(
// apReflectInfoForChatMember.getReceiver()))
// .setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
// // the central instance of
// // chatmember list .... -1 is a
// // problem!
ArrayList<Integer> addApInfoToThese = this.client.checkListForChatMemberIndexesByCallSign(apReflectInfoForChatMember.getReceiver());
addApInfoToThese.forEach((integerIndex) -> {this.client.getLst_chatMemberList().get(integerIndex).setAirPlaneReflectInfo(apReflectInfoForChatMember); });
// AirScout availability strongly affects priority => request recompute the score of the chatmember
this.client.getScoreService().requestRecompute("airscout-update");
this.client.getLst_chatMemberList()
.get(this.client.checkListForChatMemberIndexByCallSign(
apReflectInfoForChatMember.getReceiver()))
.setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
// the central instance of
// chatmember list .... -1 is a
// problem!
/** /**
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10" * CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10"
* java.util.ConcurrentModificationException at * java.util.ConcurrentModificationException at
@@ -158,6 +171,7 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93) * kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
* *
*/ */
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
// } // }
} catch (Exception e) { } catch (Exception e) {
@@ -167,6 +181,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
// TODO: handle exception // TODO: handle exception
} }
// String[] newState = new String[3];
// newState[0] = "On";
// newState[1] = "received line";
// newState[2] = apReflectInfoForChatMember.toString();
// callThreadStateToUi(newState);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + apReflectInfoForChatMember.toString(), false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} }
} }

View File

@@ -4,6 +4,7 @@ import java.io.*;
import java.net.*; import java.net.*;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import javax.xml.XMLConstants; import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilder;
@@ -11,6 +12,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.ParserConfigurationException;
import kst4contest.ApplicationConstants; import kst4contest.ApplicationConstants;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils; import kst4contest.view.GuiUtils;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.Element; import org.w3c.dom.Element;
@@ -32,13 +34,19 @@ public class ReadUDPbyUCXMessageThread extends Thread {
private BufferedReader reader; private BufferedReader reader;
private Socket socket; private Socket socket;
private ChatController client; private ChatController client;
private int udpPortNr = 12060;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "UDP-Log msg";
public ReadUDPbyUCXMessageThread(int localPort) { // public ReadUDPbyUCXMessageThread(int localPort , ThreadStatusCallback callback) {
//
//// this.callBackToController = callback;
// }
} public ReadUDPbyUCXMessageThread(int localPort, ChatController client, ThreadStatusCallback callback) {
this.udpPortNr = localPort;
public ReadUDPbyUCXMessageThread(int localPort, ChatController client) {
this.client = client; this.client = client;
this.callBackToController = callback;
} }
@Override @Override
@@ -48,6 +56,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
if (this.socket != null) { if (this.socket != null) {
System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket"); System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket");
terminateConnection(); terminateConnection();
// callBackToController.onThreadStatus("UDPReceiver", new String[]);
} }
} catch (Exception e) { } catch (Exception e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
@@ -57,17 +66,22 @@ public class ReadUDPbyUCXMessageThread extends Thread {
public void run() { public void run() {
System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
Thread.currentThread().setName("ReadUDPByUCXLogThread"); Thread.currentThread().setName("ReadUDPByUCXLogThread");
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
DatagramSocket socket = null; DatagramSocket socket = null;
boolean running; boolean running;
byte[] buf = new byte[1777]; byte[] buf = new byte[1777];
DatagramPacket packet = new DatagramPacket(buf, buf.length); DatagramPacket packet = new DatagramPacket(buf, buf.length);
try { try {
socket = new DatagramSocket(12060); // socket = new DatagramSocket(12060);
socket = new DatagramSocket(udpPortNr);
socket.setSoTimeout(2000); //TODO try for end properly socket.setSoTimeout(2000); //TODO try for end properly
} }
@@ -99,8 +113,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
nE.printStackTrace(); nE.printStackTrace();
System.out.println("ReadUdpByUCXTH: Socket not ready"); System.out.println("ReadUdpByUCXTH: Socket not ready");
try { try {
socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort()); socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort());
socket.setSoTimeout(2000); socket.setSoTimeout(2000);
@@ -136,6 +148,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
System.out.println("ReadUdpByUCX, Info: got poison, now dieing...."); System.out.println("ReadUdpByUCX, Info: got poison, now dieing....");
socket.close(); socket.close();
timeOutIndicator = true; timeOutIndicator = true;
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "stopped";
// threadStatusMessage[1] = "by poisonpill message (disconnect on purpose)";
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "stopped by Poisonpill", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
break; break;
} }
@@ -163,7 +181,17 @@ public class ReadUDPbyUCXMessageThread extends Thread {
ChatMember modifyThat = null; ChatMember modifyThat = null;
// System.out.println(udpMsg); // System.out.println("ReadUDPByUCX, message catched: " + udpMsg);
// String[] threadStatusMessage = new String[2];
// threadStatusMessage = new String[3];
// threadStatusMessage[0] = "on";
// threadStatusMessage[1] = "received message:";
// threadStatusMessage[2] = udpMsg;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try { try {
@@ -240,7 +268,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
case "10G": { case "10G": {
workedCall.setWorked10G(true); workedCall.setWorked10G(true);
break;
} }
/** /**
@@ -310,56 +338,41 @@ public class ReadUDPbyUCXMessageThread extends Thread {
modifyThat = client.getLst_chatMemberList().get(index); modifyThat = client.getLst_chatMemberList().get(index);
modifyThat.setWorked(true); modifyThat.setWorked(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
if (workedCall.isWorked144()) { if (workedCall.isWorked144()) {
modifyThat.setWorked144(true); modifyThat.setWorked144(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked144(true);
} else if (workedCall.isWorked432()) { } else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true); modifyThat.setWorked432(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked432(true);
} else if (workedCall.isWorked1240()) { } else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true); modifyThat.setWorked1240(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked1240(true);
} else if (workedCall.isWorked2300()) { } else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true); modifyThat.setWorked2300(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked2300(true);
} else if (workedCall.isWorked3400()) { } else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true); modifyThat.setWorked3400(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked3400(true);
} else if (workedCall.isWorked5600()) { } else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true); modifyThat.setWorked5600(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked5600(true);
} else if (workedCall.isWorked10G()) { } else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true); modifyThat.setWorked10G(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked10G(true);
} }
} }
try { try {
GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all GuiUtils.triggerGUIFilteredChatMemberListChange(this.client);
// BEGIN PATCH: trigger band-upgrade hint after log entry (UCXLog)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[UCXUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
} catch (Exception IllegalStateException) { } catch (Exception IllegalStateException) {
//do nothing, as it works... //do nothing, as it works...
} }
@@ -539,7 +552,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG); this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG); // System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
} }
} }
@@ -549,9 +562,27 @@ public class ReadUDPbyUCXMessageThread extends Thread {
e.printStackTrace(); e.printStackTrace();
System.out.println(e.getCause()); System.out.println(e.getCause());
System.out.println(e.getMessage()); System.out.println(e.getMessage());
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (SQLException e) { } catch (SQLException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
} }
// System.out.println("[ReadUDPbyUCXTh: ] worked size = " + this.client.getMap_ucxLogInfoWorkedCalls().size()); // System.out.println("[ReadUDPbyUCXTh: ] worked size = " + this.client.getMap_ucxLogInfoWorkedCalls().size());
@@ -561,6 +592,14 @@ public class ReadUDPbyUCXMessageThread extends Thread {
} }
public boolean terminateConnection() throws IOException { public boolean terminateConnection() throws IOException {
// String[] threadStatusMessage = new String[2];
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = "Connection terminated for purpose.";
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "terminated", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
this.socket.close(); this.socket.close();

View File

@@ -0,0 +1,309 @@
package kst4contest.controller;
import javafx.application.Platform;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.SimpleLongProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import kst4contest.logic.PriorityCalculator;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ContestSked;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
/**
* Calculates priority scores off the JavaFX thread and publishes a small UI model.
*
* Design goals:
* - No per-member Platform.runLater flooding.
* - Score is computed once per callsignRaw (e.g. "SM6VTZ"), even if it exists in multiple chat categories.
* - A routing hint (preferred ChatCategory) is kept using "last inbound category" if available.
*/
public final class ScoreService {
public static final int DEFAULT_TOP_N = 15; //how many top places we have?
/** Force a refresh at least every X ms (some scoring inputs are time dependent). */
private static final long MAX_SNAPSHOT_AGE_MS = 10_000L;
private final ChatController controller;
private final PriorityCalculator priorityCalculator;
private final AtomicBoolean recomputeRequested = new AtomicBoolean(true);
private final AtomicReference<ScoreSnapshot> latestSnapshot = new AtomicReference<>(ScoreSnapshot.empty());
// UI outputs
private final ObservableList<TopCandidate> topCandidatesFx = FXCollections.observableArrayList();
private final ReadOnlyDoubleWrapper selectedCallPriorityScore = new ReadOnlyDoubleWrapper(Double.NaN);
private final LongProperty uiPulse = new SimpleLongProperty(0);
private volatile String selectedCallSignRaw;
private volatile long lastComputedEpochMs = 0L;
private final int topN;
private final ObjectProperty<ChatMember> selectedChatMember = new SimpleObjectProperty<>(null);
public ScoreService(ChatController controller, PriorityCalculator priorityCalculator, int topN) {
this.controller = Objects.requireNonNull(controller, "controller");
this.priorityCalculator = Objects.requireNonNull(priorityCalculator, "priorityCalculator");
this.topN = topN > 0 ? topN : DEFAULT_TOP_N;
}
public ObservableList<TopCandidate> getTopCandidatesFx() {
return topCandidatesFx;
}
public ReadOnlyDoubleProperty selectedCallPriorityScoreProperty() {
return selectedCallPriorityScore.getReadOnlyProperty();
}
/**
* A lightweight UI invalidation signal that increments after every published snapshot.
* Consumers can refresh small panels (timeline/toplist), but should avoid refreshing huge tables.
*/
public LongProperty uiPulseProperty() {
return uiPulse;
}
public ScoreSnapshot getLatestSnapshot() {
return latestSnapshot.get();
}
/** Coalesced recompute request (safe to call frequently from other threads). */
public void requestRecompute(String reason) {
recomputeRequested.set(true);
}
/** Called by UI when selection changes. */
public void setSelectedChatMember(ChatMember member) {
// keep a central selection for UI actions (FurtherInfo buttons, timeline clicks, etc.)
if (Platform.isFxApplicationThread()) {
selectedChatMember.set(member);
} else {
Platform.runLater(() -> selectedChatMember.set(member));
}
selectedCallSignRaw = member == null ? null : normalizeCallRaw(member.getCallSignRaw());
// Update score immediately from the latest snapshot
if (Platform.isFxApplicationThread()) {
updateSelectedScoreFromSnapshot(latestSnapshot.get());
} else {
Platform.runLater(() -> updateSelectedScoreFromSnapshot(latestSnapshot.get()));
}
}
/**
* Called periodically by the scheduler thread.
* Recomputes only if explicitly requested or if the snapshot is too old.
*/
public void tick() {
long now = System.currentTimeMillis();
boolean shouldRecompute = recomputeRequested.getAndSet(false) || (now - lastComputedEpochMs) > MAX_SNAPSHOT_AGE_MS;
if (!shouldRecompute) return;
try {
// Apply "no reply" strikes (operator pinged via /cq but no inbound line arrived)
controller.getStationMetricsService().evaluateNoReplyTimeouts(now, controller.getChatPreferences());
recompute(now);
} catch (Exception e) {
System.err.println("[ScoreService] CRITICAL error while recomputing scores");
e.printStackTrace();
}
}
private void recompute(long nowEpochMs) {
// Keep sked list clean (must happen on FX thread)
controller.requestRemoveExpiredSkeds(nowEpochMs);
final List<ChatMember> members = controller.snapshotChatMembers();
final List<ContestSked> activeSkeds = controller.snapshotActiveSkeds();
final ChatPreferences prefs = controller.getChatPreferences();
final Map<String, ChatCategory> lastInbound = controller.snapshotLastInboundCategoryMap();
StationMetricsService.Snapshot metricsSnapshot =
controller.getStationMetricsService().snapshot(nowEpochMs, prefs);
// 1) Choose one representative per callsignRaw
Map<String, ChatMember> representativeByCallRaw = chooseRepresentativeMembers(members, lastInbound);
// 2) Compute score once per callsignRaw
Map<String, Double> scoreByCallRaw = new HashMap<>(representativeByCallRaw.size());
Map<String, ChatCategory> preferredCategoryByCallRaw = new HashMap<>(representativeByCallRaw.size());
List<TopCandidate> topAll = new ArrayList<>(representativeByCallRaw.size());
for (Map.Entry<String, ChatMember> e : representativeByCallRaw.entrySet()) {
String callRaw = e.getKey();
ChatMember representative = e.getValue();
if (representative == null) continue;
double score = priorityCalculator.calculatePriority(
representative,
prefs,
activeSkeds,
metricsSnapshot,
nowEpochMs
);
scoreByCallRaw.put(callRaw, score);
preferredCategoryByCallRaw.put(callRaw, representative.getChatCategory());
topAll.add(new TopCandidate(callRaw, representative.getCallSign(), representative.getChatCategory(), score));
}
// 3) Build Top-N
topAll.sort(Comparator.comparingDouble(TopCandidate::getScore).reversed());
List<TopCandidate> topNList = topAll.size() <= topN ? topAll : new ArrayList<>(topAll.subList(0, topN));
ScoreSnapshot snap = new ScoreSnapshot(
nowEpochMs,
Collections.unmodifiableMap(scoreByCallRaw),
Collections.unmodifiableMap(preferredCategoryByCallRaw),
Collections.unmodifiableList(topNList)
);
latestSnapshot.set(snap);
lastComputedEpochMs = nowEpochMs;
// 4) Publish to UI in ONE batched runLater
Platform.runLater(() -> {
topCandidatesFx.setAll(snap.getTopCandidates());
updateSelectedScoreFromSnapshot(snap);
uiPulse.set(uiPulse.get() + 1);
});
}
/**
* Picks one ChatMember object per callsignRaw.
* Preference order:
* 1) Variant in last inbound chat category (stable reply routing)
* 2) Most recently active variant (fallback)
*/
private Map<String, ChatMember> chooseRepresentativeMembers(
List<ChatMember> members,
Map<String, ChatCategory> lastInboundCategoryByCallRaw
) {
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
for (ChatMember m : members) {
if (m == null) continue;
String callRaw = normalizeCallRaw(m.getCallSignRaw());
if (callRaw == null || callRaw.isEmpty()) continue;
byCallRaw.computeIfAbsent(callRaw, k -> new ArrayList<>()).add(m);
}
Map<String, ChatMember> representative = new HashMap<>(byCallRaw.size());
for (Map.Entry<String, List<ChatMember>> entry : byCallRaw.entrySet()) {
String callRaw = entry.getKey();
List<ChatMember> variants = entry.getValue();
ChatCategory preferredCat = lastInboundCategoryByCallRaw.get(callRaw);
ChatMember chosen = null;
if (preferredCat != null) {
for (ChatMember v : variants) {
if (v != null && v.getChatCategory() == preferredCat) {
chosen = v;
break;
}
}
}
if (chosen == null) {
chosen = variants.stream()
.filter(Objects::nonNull)
.max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
.orElse(null);
}
if (chosen != null) representative.put(callRaw, chosen);
}
return representative;
}
private void updateSelectedScoreFromSnapshot(ScoreSnapshot snap) {
if (snap == null || selectedCallSignRaw == null) {
selectedCallPriorityScore.set(Double.NaN);
return;
}
Double v = snap.getScoreByCallSignRaw().get(selectedCallSignRaw);
selectedCallPriorityScore.set(v == null ? Double.NaN : v);
}
private static String normalizeCallRaw(String callRaw) {
if (callRaw == null) return null;
return callRaw.trim().toUpperCase();
}
// ------------------------- DTOs -------------------------
public static final class TopCandidate {
private final String callSignRaw;
private final String displayCallSign;
private final ChatCategory preferredChatCategory;
private final double score;
public TopCandidate(String callSignRaw, String displayCallSign, ChatCategory preferredChatCategory, double score) {
this.callSignRaw = callSignRaw;
this.displayCallSign = displayCallSign;
this.preferredChatCategory = preferredChatCategory;
this.score = score;
}
public String getCallSignRaw() { return callSignRaw; }
public String getDisplayCallSign() { return displayCallSign; }
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
public double getScore() { return score; }
}
public static final class ScoreSnapshot {
private final long computedAtEpochMs;
private final Map<String, Double> scoreByCallSignRaw;
private final Map<String, ChatCategory> preferredCategoryByCallSignRaw;
private final List<TopCandidate> topCandidates;
public ScoreSnapshot(long computedAtEpochMs,
Map<String, Double> scoreByCallSignRaw,
Map<String, ChatCategory> preferredCategoryByCallSignRaw,
List<TopCandidate> topCandidates) {
this.computedAtEpochMs = computedAtEpochMs;
this.scoreByCallSignRaw = scoreByCallSignRaw;
this.preferredCategoryByCallSignRaw = preferredCategoryByCallSignRaw;
this.topCandidates = topCandidates;
}
public static ScoreSnapshot empty() {
return new ScoreSnapshot(System.currentTimeMillis(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList());
}
public long getComputedAtEpochMs() { return computedAtEpochMs; }
public Map<String, Double> getScoreByCallSignRaw() { return scoreByCallSignRaw; }
public Map<String, ChatCategory> getPreferredCategoryByCallSignRaw() { return preferredCategoryByCallSignRaw; }
public List<TopCandidate> getTopCandidates() { return topCandidates; }
}
public ReadOnlyObjectProperty<ChatMember> selectedChatMemberProperty() {
return selectedChatMember;
}
public ChatMember getSelectedChatMember() {
return selectedChatMember.get();
}
}

View File

@@ -0,0 +1,124 @@
package kst4contest.controller;
import javafx.application.Platform;
import kst4contest.model.ChatCategory;
import kst4contest.model.ThreadStateMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* Schedules PM reminders for a specific sked time.
*
* Requirements:
* - Reminder goes out as PM to the station (via "/cq CALL ...").
* - Reminders are armed manually from FurtherInfo.
*/
public final class SkedReminderService {
private final ChatController controller;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("SkedReminderService");
return t;
});
private final ConcurrentHashMap<String, List<ScheduledFuture<?>>> scheduledByCallRaw = new ConcurrentHashMap<>();
public SkedReminderService(ChatController controller) {
this.controller = controller;
}
/**
* Arms reminders for one sked. Existing reminders for this call are cancelled.
*
* @param callSignRaw target call
* @param preferredCategory where to send (if null, controller resolves via lastInbound category)
* @param skedTimeEpochMs sked time
* @param offsetsMinutes e.g. [5,2,1] => reminders 5,2,1 minutes before
*/
public void armReminders(String callSignRaw,
ChatCategory preferredCategory,
long skedTimeEpochMs,
List<Integer> offsetsMinutes) {
String callRaw = normalize(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
cancelReminders(callRaw);
long now = System.currentTimeMillis();
List<Integer> offsets = (offsetsMinutes == null) ? List.of() : offsetsMinutes;
List<ScheduledFuture<?>> futures = new ArrayList<>();
for (Integer offMin : offsets) {
if (offMin == null) continue;
long fireAt = skedTimeEpochMs - (offMin * 60_000L);
long delayMs = fireAt - now;
if (delayMs <= 0) continue;
ScheduledFuture<?> f = scheduler.schedule(
() -> fireReminder(callRaw, preferredCategory, offMin),
delayMs,
TimeUnit.MILLISECONDS
);
futures.add(f);
}
scheduledByCallRaw.put(callRaw, futures);
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", true,
"Armed for " + callRaw + " (" + offsets + " min before)", false));
}
public void cancelReminders(String callSignRaw) {
String callRaw = normalize(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
List<ScheduledFuture<?>> futures = scheduledByCallRaw.remove(callRaw);
if (futures != null) {
for (ScheduledFuture<?> f : futures) {
if (f != null) f.cancel(false);
}
}
}
private void fireReminder(String callRaw, ChatCategory preferredCategory, int minutesBefore) {
try {
controller.queuePrivateCqMessage(callRaw, preferredCategory, "[KST4C Autoreminder] sked in " + minutesBefore + " min");
controller.fireUiReminderEvent(callRaw, minutesBefore); //triggers some blingbling in the UI
///Local acoustic hint (reuse existing project audio utilities, no AWT, no extra JavaFX modules)
try {
if (controller.getChatPreferences().isNotify_playSimpleSounds()) {
controller.getPlayAudioUtils().playNoiseLauncher('!'); // choose a suitable char you already use
}
// Optional: voice/cw hint (short, not too intrusive)
// controller.getPlayAudioUtils().playCWLauncher(" SKED " + minutesBefore);
} catch (Exception ignore) {
// never block reminder sending because of audio issues
}
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", true,
"PM reminder sent to " + callRaw + " (" + minutesBefore + " min)", false));
} catch (Exception e) {
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", false,
"ERROR sending reminder to " + callRaw + ": " + e.getMessage(), true));
e.printStackTrace();
}
}
private static String normalize(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
}

View File

@@ -0,0 +1,268 @@
package kst4contest.controller;
import kst4contest.logic.SignalDetector;
import kst4contest.model.ChatPreferences;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Thread-safe metrics store keyed by normalized callsignRaw (e.g. "SM6VTZ").
*
* Purpose:
* - Provide inputs for scoring (momentum, reply time, no-reply strikes, manual sked-fail, positive signals).
* - Decouple MessageBus / TX from ScoreService (only data flows, no UI calls here).
*/
public final class StationMetricsService {
/** /cq <CALL> ... */
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/]+)\\b.*");
/** Rolling window timestamps for momentum scoring. */
private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32;
private final ConcurrentHashMap<String, StationMetrics> byCallRaw = new ConcurrentHashMap<>();
/**
* Called when the operator sends a message.
* If it is a "/cq CALL ..." message, this arms a pending ping for response-time / no-reply tracking.
*/
public Optional<String> tryRecordOutboundCq(String messageText, long nowEpochMs) {
if (messageText == null) return Optional.empty();
Matcher m = OUTBOUND_CQ_PATTERN.matcher(messageText.trim());
if (!m.matches()) return Optional.empty();
String callRaw = normalizeCallRaw(m.group(1));
if (callRaw == null || callRaw.isBlank()) return Optional.empty();
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.pendingCqSentAtEpochMs = nowEpochMs;
metrics.lastOutboundCqEpochMs = nowEpochMs;
}
return Optional.of(callRaw);
}
/**
* Called for EVERY inbound line from a station (CH or PM).
* "Any line counts as activity"
*/
public void onInboundMessage(String senderCallSignRaw,
long nowEpochMs,
String messageText,
ChatPreferences prefs,
String ownCallSignRaw) {
String callRaw = normalizeCallRaw(senderCallSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
// ignore own echoed messages
if (ownCallSignRaw != null && callRaw.equalsIgnoreCase(normalizeCallRaw(ownCallSignRaw))) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.lastInboundEpochMs = nowEpochMs;
// rolling timestamps (momentum)
metrics.recentInboundEpochMs.addLast(nowEpochMs);
while (metrics.recentInboundEpochMs.size() > MAX_STORED_INBOUND_TIMESTAMPS) {
metrics.recentInboundEpochMs.removeFirst();
}
// positive signal detection (extendable by prefs)
if (messageText != null && prefs != null) {
if (SignalDetector.containsPositiveSignal(messageText, prefs.getNotify_positiveSignalsPatterns())) {
metrics.lastPositiveSignalEpochMs = nowEpochMs;
}
}
// response time measurement: any inbound line ends a pending ping
if (metrics.pendingCqSentAtEpochMs > 0) {
long rttMs = Math.max(0, nowEpochMs - metrics.pendingCqSentAtEpochMs);
metrics.pendingCqSentAtEpochMs = 0;
// EWMA for response time (stable, no spikes)
final double alpha = 0.25;
if (metrics.avgResponseTimeMs <= 0) {
metrics.avgResponseTimeMs = rttMs;
} else {
metrics.avgResponseTimeMs = alpha * rttMs + (1.0 - alpha) * metrics.avgResponseTimeMs;
}
}
}
}
/**
* Called periodically (e.g. from ScoreService.tick()).
* Applies a "no reply" strike if the pending ping is older than prefs timeout.
*/
public void evaluateNoReplyTimeouts(long nowEpochMs, ChatPreferences prefs) {
if (prefs == null) return;
long timeoutMs = Math.max(1, prefs.getNotify_noReplyPenaltyMinutes()) * 60_000L;
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
StationMetrics metrics = e.getValue();
if (metrics == null) continue;
synchronized (metrics) {
if (metrics.pendingCqSentAtEpochMs <= 0) continue;
long age = nowEpochMs - metrics.pendingCqSentAtEpochMs;
if (age >= timeoutMs) {
metrics.pendingCqSentAtEpochMs = 0;
metrics.noReplyStrikes++;
metrics.lastNoReplyStrikeEpochMs = nowEpochMs;
}
}
}
}
/** Manual sked fail: permanent until reset. */
public void markManualSkedFail(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.manualSkedFailed = true;
metrics.manualSkedFailCount++;
}
}
public void resetManualSkedFail(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.manualSkedFailed = false;
metrics.manualSkedFailCount = 0;
}
}
public boolean isManualSkedFailed(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return false;
StationMetrics metrics = byCallRaw.get(callRaw);
if (metrics == null) return false;
synchronized (metrics) {
return metrics.manualSkedFailed;
}
}
/** Immutable snapshot for scoring */
public Snapshot snapshot(long nowEpochMs, ChatPreferences prefs) {
long momentumWindowMs = (prefs != null ? prefs.getNotify_momentumWindowSeconds() : 180) * 1000L;
Snapshot snap = new Snapshot(nowEpochMs, momentumWindowMs);
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
String callRaw = e.getKey();
StationMetrics m = e.getValue();
if (m == null) continue;
synchronized (m) {
snap.byCallRaw.put(callRaw, new Snapshot.Metrics(
m.lastInboundEpochMs,
countRecent(m.recentInboundEpochMs, nowEpochMs, momentumWindowMs),
m.avgResponseTimeMs,
m.noReplyStrikes,
m.manualSkedFailed,
m.manualSkedFailCount,
m.lastPositiveSignalEpochMs
));
}
}
return snap;
}
private static int countRecent(Deque<Long> timestamps, long nowEpochMs, long windowMs) {
if (timestamps == null || timestamps.isEmpty()) return 0;
int cnt = 0;
for (Long t : timestamps) {
if (t == null) continue;
if (nowEpochMs - t <= windowMs) cnt++;
}
return cnt;
}
private static String normalizeCallRaw(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
private static final class StationMetrics {
long lastInboundEpochMs;
long lastOutboundCqEpochMs;
long pendingCqSentAtEpochMs; // 0 = none
int noReplyStrikes;
long lastNoReplyStrikeEpochMs;
double avgResponseTimeMs; // EWMA
final Deque<Long> recentInboundEpochMs = new ArrayDeque<>();
long lastPositiveSignalEpochMs;
boolean manualSkedFailed;
int manualSkedFailCount;
}
public static final class Snapshot {
private final long snapshotEpochMs;
private final long momentumWindowMs;
private final ConcurrentHashMap<String, Metrics> byCallRaw = new ConcurrentHashMap<>();
private Snapshot(long snapshotEpochMs, long momentumWindowMs) {
this.snapshotEpochMs = snapshotEpochMs;
this.momentumWindowMs = momentumWindowMs;
}
public Metrics get(String callSignRaw) {
if (callSignRaw == null) return null;
return byCallRaw.get(normalizeCallRaw(callSignRaw));
}
public long getSnapshotEpochMs() {
return snapshotEpochMs;
}
public long getMomentumWindowMs() {
return momentumWindowMs;
}
public static final class Metrics {
public final long lastInboundEpochMs;
public final int inboundCountInWindow;
public final double avgResponseTimeMs;
public final int noReplyStrikes;
public final boolean manualSkedFailed;
public final int manualSkedFailCount;
public final long lastPositiveSignalEpochMs;
public Metrics(long lastInboundEpochMs,
int inboundCountInWindow,
double avgResponseTimeMs,
int noReplyStrikes,
boolean manualSkedFailed,
int manualSkedFailCount,
long lastPositiveSignalEpochMs) {
this.lastInboundEpochMs = lastInboundEpochMs;
this.inboundCountInWindow = inboundCountInWindow;
this.avgResponseTimeMs = avgResponseTimeMs;
this.noReplyStrikes = noReplyStrikes;
this.manualSkedFailed = manualSkedFailed;
this.manualSkedFailCount = manualSkedFailCount;
this.lastPositiveSignalEpochMs = lastPositiveSignalEpochMs;
}
}
}
}

View File

@@ -0,0 +1,20 @@
package kst4contest.controller;
import kst4contest.model.ThreadStateMessage;
public interface StatusUpdateListener {
/**
* Thread (key) will send update status (value) to the view via this interface.
*
*/
void onThreadStatusChanged(String key, ThreadStateMessage threadStateMessage);
/**
* Called on change if the userlist to update the UI (sort the chatmembers list)
*/
void onUserListUpdated(String reason);
// new: userlist-update
}

View File

@@ -0,0 +1,8 @@
package kst4contest.controller;
import kst4contest.model.ThreadStateMessage;
public interface ThreadStatusCallback {
void onThreadStatus(String threadName, ThreadStateMessage threadStateMessage);
}

View File

@@ -15,7 +15,10 @@ import kst4contest.model.ChatMember;
public class UCXLogFileToHashsetParser { public class UCXLogFileToHashsetParser {
public BufferedReader fileReader; public BufferedReader fileReader;
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"; // private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"; //OLD, S51AR for example will not work
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)|([A-Z]\\d{2}[A-Z]{1,3})";
public UCXLogFileToHashsetParser(String filePathAndName) { public UCXLogFileToHashsetParser(String filePathAndName) {

View File

@@ -51,21 +51,10 @@ public class UserActualizationTask extends TimerTask {
UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser( UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser(
this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly()); this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly());
// UCXLogFileToHashsetParser getWorkedCallsignsOfUDPBackupFile = new UCXLogFileToHashsetParser(
// this.client.getChatPreferences().getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
try { try {
fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse(); fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse();
// fetchedWorkedSetUdpBckup = getWorkedCallsignsOfUDPBackupFile.parse();
// for (HashMap.Entry entry : fetchedWorkedSet.entrySet()) {
// String key = (String) entry.getKey();
// Object value = entry.getValue();
// System.out.println("key " + key);
// }
System.out.println("USERACT: fetchedWorkedSet size: " + fetchedWorkedSet.size()); System.out.println("USERACT: fetchedWorkedSet size: " + fetchedWorkedSet.size());
// System.out.println("USERACT: fetchedWorkedSetudpbckup size: " + fetchedWorkedSetUdpBckup.size());
} catch (IOException e) { } catch (IOException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block

View File

@@ -52,7 +52,8 @@ public class Utils4KST {
// Instant instant = Instant.ofEpochSecond(epoch); // Instant instant = Instant.ofEpochSecond(epoch);
Date date = new Date(epoch * 1000L); Date date = new Date(epoch * 1000L);
DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss"); // DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss"); //old value which is too long
DateFormat format = new SimpleDateFormat("H:mm:ss");
format.setTimeZone(TimeZone.getTimeZone("Etc/UTC")); format.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
String formatted = format.format(date); String formatted = format.format(date);

View File

@@ -5,6 +5,7 @@ import java.net.*;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMessage; import kst4contest.model.ChatMessage;
/** /**
@@ -169,8 +170,8 @@ public class WriteThread extends Thread {
try { try {
messageToBeSend = client.getMessageTXBus().take(); messageToBeSend = client.getMessageTXBus().take();
if (messageToBeSend.getMessageText().equals("POISONPILL_KILLTHREAD") if (messageToBeSend.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)
&& messageToBeSend.getMessageSenderName().equals("POISONPILL_KILLTHREAD")) { && messageToBeSend.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
client.getMessageRXBus().clear(); client.getMessageRXBus().clear();
this.interrupt(); this.interrupt();
break; break;

View File

@@ -0,0 +1,13 @@
package kst4contest.controller.interfaces;
public interface PstRotatorEventListener {
void onAzimuthUpdate(double azimuth);
void onElevationUpdate(double elevation);
void onModeUpdate(boolean isTracking); // true = Tracking, false = Manual
void onMessageReceived(String rawMessage); // Debugging usage
// void setRotorPosition(double azimuth);
}

View File

@@ -0,0 +1,292 @@
package kst4contest.logic;
import kst4contest.controller.StationMetricsService;
import kst4contest.model.*;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Priority score calculation (off FX-thread).
*
* Notes:
* - Score is computed once per callsignRaw by ScoreService.
* - This calculator MUST be pure (no UI calls) and fast.
*/
public class PriorityCalculator {
/** Max age for "known active bands" (derived from chat history). */
private static final long RX_BANDS_MAX_AGE_MS = 30L * 60L * 1000L; // 30 minutes
public double calculatePriority(ChatMember member,
ChatPreferences prefs,
List<ContestSked> activeSkeds,
StationMetricsService.Snapshot metricsSnapshot,
long nowEpochMs) {
if (member == null || prefs == null) return 0.0;
final String callRaw = normalize(member.getCallSignRaw());
if (callRaw == null || callRaw.isBlank()) return 0.0;
// --------------------------------------------------------------------
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
// --------------------------------------------------------------------
// --------------------------------------------------------------------
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
// --------------------------------------------------------------------
EnumSet<Band> myEnabledBands = getMyEnabledBands(prefs);
// "worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
// IMPORTANT: ChatMember.worked is UI-only and NOT used in scoring.
EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
// Remaining bands that are:
// - recently offered by the station (from knownActiveBands history)
// - enabled at our station
// - NOT worked yet (per-band flags)
// If we do not know offered bands (history empty), this remains empty.
EnumSet<Band> unworkedPossible = EnumSet.noneOf(Band.class);
EnumSet<Band> stationOfferedBands = getStationOfferedBandsFromHistory(member, nowEpochMs);
EnumSet<Band> possibleBands = stationOfferedBands.isEmpty()
? EnumSet.noneOf(Band.class) // unknown => don't hard-filter
: EnumSet.copyOf(stationOfferedBands);
if (!possibleBands.isEmpty()) {
possibleBands.retainAll(myEnabledBands);
if (possibleBands.isEmpty()) {
// We know their bands, but none of them are enabled at our station.
return 0.0;
}
unworkedPossible = EnumSet.copyOf(possibleBands);
unworkedPossible.removeAll(workedBandsForScoring);
// If already worked on all possible bands => no priority on them anymore (contest logic).
if (unworkedPossible.isEmpty()) {
return 0.0;
}
}
// --------------------------------------------------------------------
// 2) BASE SCORE
// --------------------------------------------------------------------
double score = 100.0;
// if (!member.isWorked()) {
// score += 200.0;
// }
//"worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
// EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
if (workedBandsForScoring.isEmpty()) {
score += 200.0; // never worked on any supported band -> higher priority
} else {
score -= 150.0; // already worked on at least one band -> lower base priority
}
// Multi-band bonus: if they offer >1 possible band and we worked at least one, prefer them
if (!possibleBands.isEmpty()) {
int bandCount = possibleBands.size();
score += (bandCount - 1) * 80.0;
}
// Optional: band-upgrade visibility boost
// If the station is already worked on at least one band, but is still QRV on other unworked enabled band(s),
// we can optionally add a boost so it remains visible in the list.
if (prefs.isNotify_bandUpgradePriorityBoostEnabled()
&& !workedBandsForScoring.isEmpty()
&& !unworkedPossible.isEmpty()) {
score += 180.0; // tuned visibility boost
}
// --------------------------------------------------------------------
// 3) DISTANCE ("Goldilocks Zone")
// --------------------------------------------------------------------
double distKm = member.getQrb() == null ? 0.0 : member.getQrb();
if (distKm > 0) {
if (distKm < 200) {
score *= 0.7;
} else if (distKm > prefs.getStn_maxQRBDefault()) {
score *= 0.3;
} else {
score *= 1.15;
}
}
// --------------------------------------------------------------------
// 4) AIRSCOUT BOOST
// --------------------------------------------------------------------
AirPlaneReflectionInfo apInfo = member.getAirPlaneReflectInfo();
if (apInfo != null && apInfo.getAirPlanesReachableCntr() > 0) {
score += 200;
int nextMinutes = findNextAirplaneArrivingMinutes(apInfo);
if (nextMinutes == 0) score += 120;
else if (nextMinutes == 1) score += 60;
else if (nextMinutes == 2) score += 30;
}
// --------------------------------------------------------------------
// 5) BOOST IDEA #1: Beam direction match (within beamwidth)
// --------------------------------------------------------------------
if (member.getQTFdirection() != null) {
double myAz = prefs.getActualQTF().getValue();
double targetAz = member.getQTFdirection();
double diff = minimalAngleDiffDeg(myAz, targetAz);
double halfBeam = Math.max(1.0, prefs.getStn_antennaBeamWidthDeg()) / 2.0;
if (diff <= halfBeam) {
double centerFactor = 1.0 - (diff / halfBeam); // 1.0 center -> 0.0 edge
score += 80.0 + (120.0 * centerFactor);
}
}
// --------------------------------------------------------------------
// 6) BOOST IDEA #3: Conversation momentum (recent inbound burst)
// --------------------------------------------------------------------
if (metricsSnapshot != null) {
StationMetricsService.Snapshot.Metrics mx = metricsSnapshot.get(callRaw);
if (mx != null) {
long ageMs = mx.lastInboundEpochMs > 0 ? (nowEpochMs - mx.lastInboundEpochMs) : Long.MAX_VALUE;
// "Active now" bonus
if (ageMs < 60_000) score += 120;
else if (ageMs < 3 * 60_000) score += 60;
// Momentum bonus: multiple lines in the configured window
int cnt = mx.inboundCountInWindow;
if (cnt >= 6) score += 160;
else if (cnt >= 4) score += 110;
else if (cnt >= 2) score += 60;
// Positive signal (configurable)
if (mx.lastPositiveSignalEpochMs > 0 && (nowEpochMs - mx.lastPositiveSignalEpochMs) < 5 * 60_000) {
score += 120;
}
// Reply time: prefer fast responders
if (mx.avgResponseTimeMs > 0) {
if (mx.avgResponseTimeMs < 60_000) score += 80;
else if (mx.avgResponseTimeMs < 3 * 60_000) score += 40;
}
// No-reply penalty (automatic failed attempt)
if (mx.noReplyStrikes > 0) {
score /= (1.0 + (mx.noReplyStrikes * 0.6));
}
// Manual sked fail (path likely bad) => strong, permanent penalty until reset
if (mx.manualSkedFailed) {
score *= 0.15;
}
}
}
// --------------------------------------------------------------------
// 7) BOOST IDEA #4: Sked commitment ramp-up
// --------------------------------------------------------------------
if (activeSkeds != null && !activeSkeds.isEmpty()) {
for (ContestSked sked : activeSkeds) {
if (sked == null) continue;
if (!callRaw.equals(normalize(sked.getTargetCallsign()))) continue;
long seconds = sked.getTimeUntilSkedSeconds();
// Imminent sked: absolute priority (T-3min..T+1min)
if (seconds < 180 && seconds > -60) {
score += 5000;
continue;
}
// Ramp: 0..15 minutes before => up to +1200
if (seconds >= 0 && seconds <= 15 * 60) {
double t = (15 * 60 - seconds) / (15.0 * 60.0); // 0.0..1.0
score += 300 + (900 * t);
} else if (seconds > 15 * 60) {
score += 40;
}
}
}
// --------------------------------------------------------------------
// 8) Legacy penalty: failed attempts in ChatMember
// --------------------------------------------------------------------
if (member.getFailedQSOAttempts() > 0) {
score = score / (member.getFailedQSOAttempts() + 1);
}
return Math.max(0.0, score);
}
private static EnumSet<Band> getMyEnabledBands(ChatPreferences prefs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (prefs.isStn_bandActive144()) out.add(Band.B_144);
if (prefs.isStn_bandActive432()) out.add(Band.B_432);
if (prefs.isStn_bandActive1240()) out.add(Band.B_1296);
if (prefs.isStn_bandActive2300()) out.add(Band.B_2320);
if (prefs.isStn_bandActive3400()) out.add(Band.B_3400);
if (prefs.isStn_bandActive5600()) out.add(Band.B_5760);
if (prefs.isStn_bandActive10G()) out.add(Band.B_10G);
return out;
}
private static EnumSet<Band> getStationOfferedBandsFromHistory(ChatMember member, long nowEpochMs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
Map<Band, ChatMember.ActiveFrequencyInfo> map = member.getKnownActiveBands();
if (map == null || map.isEmpty()) return out;
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> e : map.entrySet()) {
if (e == null || e.getKey() == null || e.getValue() == null) continue;
long age = nowEpochMs - e.getValue().timestampEpoch;
if (age <= RX_BANDS_MAX_AGE_MS) {
out.add(e.getKey());
}
}
return out;
}
private static EnumSet<Band> getWorkedBands(ChatMember member) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (member.isWorked144()) out.add(Band.B_144);
if (member.isWorked432()) out.add(Band.B_432);
if (member.isWorked1240()) out.add(Band.B_1296);
if (member.isWorked2300()) out.add(Band.B_2320);
if (member.isWorked3400()) out.add(Band.B_3400);
if (member.isWorked5600()) out.add(Band.B_5760);
if (member.isWorked10G()) out.add(Band.B_10G);
if (member.isWorked24G()) out.add(Band.B_24G);
return out;
}
private static int findNextAirplaneArrivingMinutes(AirPlaneReflectionInfo apInfo) {
try {
if (apInfo.getRisingAirplanes() == null || apInfo.getRisingAirplanes().isEmpty()) return -1;
int min = Integer.MAX_VALUE;
for (AirPlane ap : apInfo.getRisingAirplanes()) {
if (ap == null) continue;
min = Math.min(min, ap.getArrivingDurationMinutes());
}
return min == Integer.MAX_VALUE ? -1 : min;
} catch (Exception ignore) {
return -1;
}
}
private static double minimalAngleDiffDeg(double a, double b) {
double diff = Math.abs((a - b) % 360.0);
return diff > 180.0 ? 360.0 - diff : diff;
}
private static String normalize(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
}

View File

@@ -0,0 +1,51 @@
package kst4contest.logic;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
/**
* Lightweight positive-signal detector.
*
* Patterns are configured via a single preference string, delimited by ';' or newlines.
* Examples: "QRV;READY;RX OK;RGR;TNX;TU;HRD"
*/
public final class SignalDetector {
private static final AtomicReference<String> lastPatterns = new AtomicReference<>("");
private static final AtomicReference<List<Pattern>> cached = new AtomicReference<>(List.of());
private SignalDetector() {}
public static boolean containsPositiveSignal(String messageText, String patternsDelimited) {
if (messageText == null || messageText.isBlank()) return false;
List<Pattern> patterns = compileIfChanged(patternsDelimited);
String txt = messageText.toUpperCase();
for (Pattern p : patterns) {
if (p.matcher(txt).find()) return true;
}
return false;
}
private static List<Pattern> compileIfChanged(String patternsDelimited) {
String p = patternsDelimited == null ? "" : patternsDelimited.trim();
String prev = lastPatterns.get();
if (p.equals(prev)) return cached.get();
List<Pattern> out = new ArrayList<>();
for (String token : p.split("[;\\n\\r]+")) {
String t = token.trim();
if (t.isEmpty()) continue;
// plain substring match, but regex-safe
String regex = Pattern.quote(t.toUpperCase());
out.add(Pattern.compile(regex));
}
lastPatterns.set(p);
cached.set(List.copyOf(out));
return out;
}
}

View File

@@ -0,0 +1,49 @@
package kst4contest.model;
/**
* Represents Amateur Radio Bands and their physical limits.
* Used for plausibility checks in the Smart Parser.
*/
public enum Band {
B_144(144.000, 146.000, "144"),
B_432(432.000, 434.000, "432"),
B_1296(1296.000, 1298.000, "1296"),
B_2320(2320.000, 2322.000, "2320"),
B_3400(3400.000, 3410.000, "3400"),
B_5760(5760.000, 5762.000, "5760"),
B_10G(10368.000, 10370.000, "10368"),
B_24G(24048.000, 24050.000, "24048");
// more space for future usage
private final double minFreq;
private final double maxFreq;
private final String prefix; // Default prefix for "short value" parsing (e.g., .210)
Band(double min, double max, String prefix) {
this.minFreq = min;
this.maxFreq = max;
this.prefix = prefix;
}
public String getPrefix() {
return prefix;
}
/**
* Checks if a specific frequency falls within this band's limits.
*/
public boolean isPlausible(double freq) {
return freq >= minFreq && freq <= maxFreq;
}
/**
* Helper to find the matching Band enum for a given frequency.
* Returns null if no band matches.
*/
public static Band fromFrequency(double freq) {
for (Band b : values()) {
if (b.isPlausible(freq)) return b;
}
return null;
}
}

View File

@@ -1,6 +1,10 @@
package kst4contest.model; package kst4contest.model;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@@ -9,7 +13,10 @@ import javafx.beans.property.StringProperty;
public class ChatMember { public class ChatMember {
// private final BooleanProperty workedInfoChangeFireListEventTrigger = new SimpleBooleanProperty();
long lastFlagsChangeEpochMs; // timestamp of the last worked/not-QRV flag change in the internal DB
// private final BooleanProperty workedInfoChangeFireListEventTrigger = new SimpleBooleanProperty();
AirPlaneReflectionInfo airPlaneReflectInfo; AirPlaneReflectionInfo airPlaneReflectInfo;
String callSign; String callSign;
String qra; String qra;
@@ -45,6 +52,12 @@ public class ChatMember {
boolean worked3400; boolean worked3400;
boolean worked5600; boolean worked5600;
boolean worked10G; boolean worked10G;
boolean Worked50;
boolean Worked70;
boolean Worked24G;
boolean Worked47G;
boolean Worked76G;
/** /**
* Chatmember is qrv at all band except we initialize anything other, depending to user entry * Chatmember is qrv at all band except we initialize anything other, depending to user entry
@@ -58,9 +71,35 @@ public class ChatMember {
boolean qrv10G = true; boolean qrv10G = true;
boolean qrvAny = true; boolean qrvAny = true;
// Stores the last known frequency per band (Context History)
private final Map<Band, ActiveFrequencyInfo> knownActiveBands = new ConcurrentHashMap<>();
// --- INNER CLASS FOR QRG HISTORY ---
public class ActiveFrequencyInfo {
public double frequency;
public long timestampEpoch;
public ActiveFrequencyInfo(double freq) {
this.frequency = freq;
this.timestampEpoch = System.currentTimeMillis();
}
}
// Counter for failed calls (Penalty Logic)
private int failedQSOAttempts = 0;
// Calculated Score for sorting the user list
private double currentPriorityScore = 0.0;
public long getLastFlagsChangeEpochMs() {
return lastFlagsChangeEpochMs;
}
public void setLastFlagsChangeEpochMs(long lastFlagsChangeEpochMs) {
this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs;
}
public boolean isInAngleAndRange() { public boolean isInAngleAndRange() {
return isInAngleAndRange; return isInAngleAndRange;
@@ -270,8 +309,129 @@ public class ChatMember {
return callSign; return callSign;
} }
/**
* Sets the original callsign and derives the normalized base callsign which is
* used as the database key. Prefixes like EA5/ and suffixes like /P or -70 are
* ignored for the raw-key handling.
*
* @param callSign callsign as received from chat or database
*/
public void setCallSign(String callSign) { public void setCallSign(String callSign) {
this.callSign = callSign;
if (callSign == null) {
this.callSign = null;
this.callSignRaw = null;
return;
}
this.callSign = callSign.trim().toUpperCase(Locale.ROOT);
this.callSignRaw = normalizeCallSignToBaseCallSign(this.callSign);
}
/**
* Normalizes a callsign to the base callsign which is used as the unique key in
* the internal database. The method removes KST suffixes like "-2", portable
* suffixes like "/P" and prefix additions like "EA5/".
*
* @param callSign callsign to normalize
* @return normalized base callsign in upper case
*/
public static String normalizeCallSignToBaseCallSign(String callSign) {
if (callSign == null) {
return null;
}
String normalizedCallSign = callSign.trim().toUpperCase(Locale.ROOT);
if (normalizedCallSign.isBlank()) {
return normalizedCallSign;
}
String callSignWithoutDashSuffix = normalizedCallSign.split("-", 2)[0].trim();
if (!callSignWithoutDashSuffix.contains("/")) {
return callSignWithoutDashSuffix;
}
String[] callSignParts = callSignWithoutDashSuffix.split("/");
String bestMatchingCallsignPart = helper_selectBestCallsignPart(callSignParts);
if (bestMatchingCallsignPart == null || bestMatchingCallsignPart.isBlank()) {
return callSignWithoutDashSuffix;
}
return bestMatchingCallsignPart;
}
/**
* Selects the most plausible base callsign segment from a slash-separated
* callsign. In strings like "EA5/G8MBI/P" the segment "G8MBI" is preferred over
* prefix or portable markers.
*
* @param callSignParts slash-separated callsign parts
* @return best matching base callsign segment
*/
private static String helper_selectBestCallsignPart(String[] callSignParts) {
String bestLikelyBaseCallsignPart = null;
int bestLikelyBaseCallsignLength = -1;
String bestFallbackCallsignPart = null;
int bestFallbackCallsignLength = -1;
for (String rawCallsignPart : callSignParts) {
String currentCallsignPart = rawCallsignPart == null ? "" : rawCallsignPart.trim().toUpperCase(Locale.ROOT);
if (currentCallsignPart.isBlank()) {
continue;
}
if (currentCallsignPart.length() > bestFallbackCallsignLength) {
bestFallbackCallsignPart = currentCallsignPart;
bestFallbackCallsignLength = currentCallsignPart.length();
}
if (helper_isLikelyBaseCallsignSegment(currentCallsignPart)
&& currentCallsignPart.length() > bestLikelyBaseCallsignLength) {
bestLikelyBaseCallsignPart = currentCallsignPart;
bestLikelyBaseCallsignLength = currentCallsignPart.length();
}
}
if (bestLikelyBaseCallsignPart != null) {
return bestLikelyBaseCallsignPart;
}
return bestFallbackCallsignPart;
}
/**
* Checks whether a slash-separated segment looks like a real base callsign. A
* normal amateur-radio callsign typically contains letters and digits and is
* longer than one-character postfix markers.
*
* @param callsignSegment segment to inspect
* @return true if the segment looks like a base callsign
*/
private static boolean helper_isLikelyBaseCallsignSegment(String callsignSegment) {
boolean containsLetter = false;
boolean containsDigit = false;
for (int currentIndex = 0; currentIndex < callsignSegment.length(); currentIndex++) {
char currentCharacter = callsignSegment.charAt(currentIndex);
if (Character.isLetter(currentCharacter)) {
containsLetter = true;
}
if (Character.isDigit(currentCharacter)) {
containsDigit = true;
}
}
return containsLetter && containsDigit && callsignSegment.length() >= 3;
} }
public String getQra() { public String getQra() {
@@ -313,9 +473,51 @@ public class ChatMember {
return worked; return worked;
} }
public boolean isWorked50() {
return Worked50;
}
public void setWorked50(boolean worked50) {
Worked50 = worked50;
}
public boolean isWorked70() {
return Worked70;
}
public void setWorked70(boolean worked70) {
Worked70 = worked70;
}
public boolean isWorked24G() {
return Worked24G;
}
public void setWorked24G(boolean worked24G) {
Worked24G = worked24G;
}
public boolean isWorked47G() {
return Worked47G;
}
public void setWorked47G(boolean worked47G) {
Worked47G = worked47G;
}
public boolean isWorked76G() {
return Worked76G;
}
public void setWorked76G(boolean worked76G) {
Worked76G = worked76G;
}
public void setWorked(boolean worked) { public void setWorked(boolean worked) {
this.worked = worked; this.worked = worked;
} }
/** /**
@@ -324,13 +526,15 @@ public class ChatMember {
*/ */
public String getCallSignRaw() { public String getCallSignRaw() {
String raw = "";
try { return callSignRaw;
return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m // String raw = "";
} catch (Exception e) { //
return getCallSign(); // try {
} // return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m
// } catch (Exception e) {
// return getCallSign();
// }
} }
@@ -342,12 +546,19 @@ public class ChatMember {
this.setWorked(false); this.setWorked(false);
this.setWorked144(false); this.setWorked144(false);
this.setWorked50(false);
this.setWorked70(false);
this.setWorked432(false); this.setWorked432(false);
this.setWorked1240(false); this.setWorked1240(false);
this.setWorked2300(false); this.setWorked2300(false);
this.setWorked3400(false); this.setWorked3400(false);
this.setWorked5600(false); this.setWorked5600(false);
this.setWorked10G(false); this.setWorked10G(false);
this.setWorked24G(false);
this.setWorked47G(false);
this.setWorked76G(false);
} }
/** /**
@@ -391,4 +602,56 @@ public class ChatMember {
return false; return false;
} }
/**
* Adds a new recognized frequency by band to the internal band/qrg map
* @param band
* @param freq
*/
public void addKnownFrequency(Band band, double freq) {
this.knownActiveBands.put(band, new ActiveFrequencyInfo(freq));
}
/**
* represents a map of bands which are known of this chatmember
*
* @return Band
*/
public Map<Band, ActiveFrequencyInfo> getKnownActiveBands() {
return knownActiveBands;
}
/**
* If a sked fails and the user tells this to the client, this counter will be increased to give the station a
* lower score
*/
public void incrementFailedAttempts() {
this.failedQSOAttempts++;
}
public void resetFailedAttempts() {
this.failedQSOAttempts = 0;
}
public int getFailedQSOAttempts() {
return failedQSOAttempts;
}
/**
* Sets the working-priority score of a chatmember for the "Todo-List"
* @param score
*/
public void setCurrentPriorityScore(double score) {
this.currentPriorityScore = score;
}
/**
* Gets the working-priority score of a chatmember for the "Todo-List"
*
*/
public double getCurrentPriorityScore() {
return currentPriorityScore;
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
package kst4contest.model;
/**
* Represents a scheduled event or an AirScout opportunity in the future.
* Used for the Timeline View and Priority Calculation.
*/
public class ContestSked {
private String targetCallsign;
private double targetAzimuth; // Required for Antenna-Visuals
private long skedTimeEpoch; // The peak time (e.g., AP)
private Band band;
// Opportunity potential (0..100). -1 means "unknown".
int opportunityPotentialPercent = -1;
// Status flags to prevent spamming alarms
private boolean warning3MinSent = false;
private boolean warningNowSent = false;
public ContestSked(String call, double azimuth, long time, Band b) {
this.targetCallsign = call;
this.targetAzimuth = azimuth;
this.skedTimeEpoch = time;
this.band = b;
}
/**
* Returns the seconds remaining until the event.
* Negative values mean the event is in the past.
*/
public long getTimeUntilSkedSeconds() {
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
}
// Getters and Setters...
public String getTargetCallsign() { return targetCallsign; }
public double getTargetAzimuth() { return targetAzimuth; }
public long getSkedTimeEpoch() { return skedTimeEpoch; }
public Band getBand() { return band; }
public boolean isWarning3MinSent() { return warning3MinSent; }
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
public boolean isWarningNowSent() { return warningNowSent; }
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
public int getOpportunityPotentialPercent() {
return opportunityPotentialPercent;
}
public void setOpportunityPotentialPercent(int opportunityPotentialPercent) {
this.opportunityPotentialPercent = opportunityPotentialPercent;
}
}

View File

@@ -0,0 +1,103 @@
package kst4contest.model;
/**
* Object for the description of the activity of a Thread to show these information in a View.
* <br/><br/>
* If state is critical, there could be used a further information field for the stacktrace
*/
public class ThreadStateMessage {
String threadNickName;
String threadDescription;
boolean running;
String runningInformationTextDescription;
String runningInformation;
boolean criticalState;
String criticalStateFurtherInfo;
public ThreadStateMessage(String threadNickName, boolean running, String runningInformation, boolean criticalState) {
this.threadNickName = threadNickName;
this.running = running;
this.criticalState = criticalState;
this.runningInformation = runningInformation;
}
/**
* This triggers the message for "Sked armed"
*
* @return
*/
public String getRunningInformationTextDescription() {
// If a custom description was set (e.g. for UI indicator buttons), prefer it.
if (runningInformationTextDescription != null && !runningInformationTextDescription.isBlank()) {
return runningInformationTextDescription;
}
// Fallback (legacy behavior)
if (isRunning()) {
return "on";
} else if (!isRunning() && isCriticalState()) {
return "FAILED";
} else {
return "off";
}
}
public void setRunningInformationTextDescription(String runningInformationTextDescription) {
this.runningInformationTextDescription = runningInformationTextDescription;
}
public String getThreadNickName() {
return threadNickName;
}
public void setThreadNickName(String threadNickName) {
this.threadNickName = threadNickName;
}
public String getThreadDescription() {
return threadDescription;
}
public void setThreadDescription(String threadDescription) {
this.threadDescription = threadDescription;
}
public boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
public String getRunningInformation() {
return runningInformation;
}
public void setRunningInformation(String runningInformation) {
this.runningInformation = runningInformation;
}
public boolean isCriticalState() {
return criticalState;
}
public void setCriticalState(boolean criticalState) {
this.criticalState = criticalState;
}
public String getCriticalStateFurtherInfo() {
return criticalStateFurtherInfo;
}
public void setCriticalStateFurtherInfo(String criticalStateFurtherInfo) {
this.criticalStateFurtherInfo = criticalStateFurtherInfo;
}
}

View File

@@ -0,0 +1,256 @@
package kst4contest.test;
import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class MockKstServer {
private static final int PORT = 23001;
private static final String CHAT_ID = "2"; // 2 = 144/432 MHz
// Thread-sichere Liste aller verbundenen Clients (OutputStreams)
private final List<PrintWriter> clients = new CopyOnWriteArrayList<>();
// Permanente User (Ihre Test-Callsigns)
private final Map<String, User> onlineUsers = new HashMap<>();
// Historien müssen synchronisiert werden
private final List<String> historyChat = Collections.synchronizedList(new ArrayList<>());
private final List<String> historyDx = Collections.synchronizedList(new ArrayList<>());
private boolean running = false;
private ServerSocket serverSocket;
public MockKstServer() {
// Initiale Permanente User
addUser("DK5EW", "Erwin", "JN47NX");
addUser("DL1TEST", "TestOp", "JO50XX");
addUser("ON4KST", "Alain", "JO20HI");
addUser("PA9R-2", "2", "JO20HI");
addUser("PA9R-70", "70", "JO20HI");
addUser("PA9R", "general", "JO20HI");
}
// Startet den Server im Hintergrund (Non-Blocking)
public void start() {
if (running) return;
running = true;
new Thread(() -> {
try {
serverSocket = new ServerSocket(PORT);
System.out.println("[Server] ON4KST Simulation gestartet auf Port " + PORT);
// Startet den Simulator für Zufallstraffic
new Thread(this::simulationLoop).start();
while (running) {
Socket clientSocket = serverSocket.accept();
System.out.println("[Server] Neuer Client verbunden: " + clientSocket.getInetAddress());
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
if (running) e.printStackTrace();
}
}).start();
}
public void stop() {
running = false;
try {
if (serverSocket != null) serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void addUser(String call, String name, String loc) {
onlineUsers.put(call, new User(call, name, loc));
}
private void removeUser(String call) {
onlineUsers.remove(call);
}
// Sendet Nachricht an ALLE verbundenen Clients (inkl. Sender)
private void broadcast(String message) {
if (!message.endsWith("\r\n")) message += "\r\n";
String finalMsg = message;
for (PrintWriter writer : clients) {
try {
writer.print(finalMsg);
writer.flush(); // WICHTIG: Sofort senden!
} catch (Exception e) {
// Client wohl weg, wird beim nächsten Schreibversuch oder im Handler entfernt
}
}
}
// --- Innere Logik: Client Handler ---
private class ClientHandler implements Runnable {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private String myCall = "MYCLIENT"; // Default, wird bei LOGIN überschrieben
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// ISO-8859-1 ist Standard für KST/Telnet Cluster
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ISO-8859-1"));
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"), true);
clients.add(out);
String line;
boolean loginComplete = false;
while ((line = in.readLine()) != null) {
// System.out.println("[RECV] " + line); // Debugging aktivieren falls nötig
String[] parts = line.split("\\|");
String cmd = parts[0];
if (cmd.equals("LOGIN") || cmd.equals("LOGINC")) {
// Protokoll: LOGIN|callsign|password|... [cite: 21]
if (parts.length > 1) myCall = parts[1];
// 1. Login Bestätigung
// Format: LOGSTAT|100|chat id|client software version|session key|config|dx option|
send("LOGSTAT|100|" + CHAT_ID + "|JavaSim|KEY123|Config|3|");
// Bei LOGIN senden wir die Daten sofort
// Bei LOGINC warten wir eigentlich auf SDONE, senden hier aber vereinfacht direkt
if (cmd.equals("LOGIN")) {
sendInitialData();
loginComplete = true;
}
}
else if (cmd.equals("SDONE")) {
// Abschluss der Settings (bei LOGINC) [cite: 34]
sendInitialData();
loginComplete = true;
}
else if (cmd.equals("MSG")) {
// MSG|chat id|destination|command|0| [cite: 42]
if (parts.length >= 4) {
String text = parts[3];
// Nachricht sofort als CH Frame an alle verteilen (Echo)
handleChatMessage(myCall, "Me", text);
}
}
else if (cmd.equals("CK")) {
// Keepalive [cite: 20]
// Server muss nicht zwingend antworten, aber Connection bleibt offen
}
}
} catch (IOException e) {
// System.out.println("Client getrennt");
} finally {
clients.remove(out);
try { socket.close(); } catch (IOException e) {}
}
}
private void send(String msg) {
if (!msg.endsWith("\r\n")) msg += "\r\n";
out.print(msg);
out.flush();
}
private void sendInitialData() {
// 1. User Liste UA0 [cite: 14]
for (User u : onlineUsers.values()) {
send("UA0|" + CHAT_ID + "|" + u.call + "|" + u.name + "|" + u.loc + "|0|");
}
// 2. Chat History CR [cite: 7]
synchronized(historyChat) {
for (String h : historyChat) send(h);
}
// 3. DX History DL [cite: 10]
synchronized(historyDx) {
for (String d : historyDx) send(d);
}
// 4. Ende User Liste UE [cite: 15]
send("UE|" + CHAT_ID + "|" + onlineUsers.size() + "|");
}
}
// --- Hilfsmethoden für Traffic ---
private void handleChatMessage(String call, String name, String text) {
// CH|chat id|date|callsign|firstname|destination|msg|highlight|
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String frame = String.format("CH|%s|%s|%s|%s|0|%s|0|", CHAT_ID, date, call, name, text);
synchronized(historyChat) {
historyChat.add(frame);
if (historyChat.size() > 50) historyChat.remove(0);
}
broadcast(frame);
}
private void handleDxSpot(String spotter, String dx, String freq) {
// DL|Unix time|dx utc|spotter|qrg|dx|info|spotter locator|dx locator| [cite: 10]
long unixTime = System.currentTimeMillis() / 1000;
String utc = new SimpleDateFormat("HHmm").format(new Date());
// Simple Dummy Locators
String frame = String.format("DL|%d|%s|%s|%s|%s|Simulated|JO00|JO99|",
unixTime, utc, spotter, freq, dx);
synchronized(historyDx) {
historyDx.add(frame);
if (historyDx.size() > 20) historyDx.remove(0);
}
broadcast(frame);
}
private void simulationLoop() {
String[] randomCalls = {"PA0GUS", "F6APE", "OH8K", "OZ2M", "G4CBW"};
String[] msgs = {"CQ 144.300", "Tnx for QSO", "Any sked?", "QRV 432.200"};
Random rand = new Random();
while (running) {
try {
Thread.sleep(3000 + rand.nextInt(5000)); // 3-8 Sek Pause
int action = rand.nextInt(10);
String call = randomCalls[rand.nextInt(randomCalls.length)];
if (action < 4) { // 40% Chat
handleChatMessage(call, "SimOp", msgs[rand.nextInt(msgs.length)]);
} else if (action < 7) { // 30% DX Spot
handleDxSpot(call, randomCalls[rand.nextInt(randomCalls.length)], "144." + rand.nextInt(400));
} else if (action == 8) { // Login Simulation UA5
if (!onlineUsers.containsKey(call)) {
addUser(call, "SimOp", "JO11");
broadcast("UA5|" + CHAT_ID + "|" + call + "|SimOp|JO11|2|");
}
} else if (action == 9) { // Logout Simulation UR6
if (onlineUsers.containsKey(call) && !call.equals("DK5EW")) { // DK5EW nicht kicken
removeUser(call);
broadcast("UR6|" + CHAT_ID + "|" + call + "|");
}
}
// Ping ab und zu
if (rand.nextInt(5) == 0) broadcast("CK|");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Kleine Datenklasse
private static class User {
String call, name, loc;
User(String c, String n, String l) { this.call=c; this.name=n; this.loc=l; }
}
}

View File

@@ -44,6 +44,14 @@ public class GuiUtils {
public static void triggerGUIFilteredChatMemberListChange(ChatController chatController) { public static void triggerGUIFilteredChatMemberListChange(ChatController chatController) {
if (javafx.application.Platform.isFxApplicationThread()) {
triggerUpdate(chatController);
} else{
javafx.application.Platform.runLater(() -> triggerUpdate(chatController));
}
}
private static void triggerUpdate(ChatController chatController) {
{ {
//trick to trigger gui changes on property changes of obects //trick to trigger gui changes on property changes of obects

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,369 @@
package kst4contest.view;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.paint.Color;
import javafx.scene.effect.DropShadow;
import javafx.scene.Node;
import javafx.scene.Group;
import javafx.scene.text.Font;
import kst4contest.model.ContestSked;
import kst4contest.model.ChatCategory;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* A custom UI Component that visualizes future events (Skeds/AP).
* It changes opacity based on the current antenna direction.
*
* Extended:
* - Can also render "priority candidates" (ScoreService top list) with time base = next AirScout airplane minute.
* - Clicking a candidate triggers a callback (selection + /cq preparation happens in Kst4ContestApplication).
*/
public class TimelineView extends Pane {
private double currentAntennaAzimuth = 0;
private double beamWidth = 50.0; // TODO: from Prefs (later)
private final long PREVIEW_TIME_MS = 30 * 60 * 1000; // 30 Minutes Preview
double margin = 30; // enough space for the callsign label
private Function<ContestSked, String> skedTooltipExtraTextProvider; //used for further info in sked tooltip
private Consumer<CandidateEvent> onCandidateClicked;
public TimelineView() {
this.setPrefHeight(40);
this.setStyle("-fx-background-color: #2b2b2b;");
// weitere init defaults, falls du welche hattest
}
/**
* Backward compatibility: if some code still calls the old ctor.
* Potential/tooltip are not view-wide properties; they belong to markers/events.
*/
@Deprecated
public TimelineView(int opportunityPotentialPercent, String tooltipText) {
this(); // ignore args on purpose
}
//
// public int getOpportunityPotentialPercent() {
// return opportunityPotentialPercent;
// }
public void setCurrentAntennaAzimuth(double az) {
this.currentAntennaAzimuth = az;
}
public long getPreviewTimeMs() {
return PREVIEW_TIME_MS;
}
public void setOnCandidateClicked(Consumer<CandidateEvent> handler) {
this.onCandidateClicked = handler;
}
/** Backward compatible call (Skeds only) */
public void updateVisuals(List<ContestSked> skeds) {
updateVisuals(skeds, Collections.emptyList());
}
/**
* Redraws the timeline based on the list of active skeds AND priority candidates.
*/
public void updateVisuals(List<ContestSked> skeds, List<CandidateEvent> candidates) {
this.getChildren().clear();
double width = this.getWidth();
if (width <= 5) {
// Layout not ready yet; will be updated by caller later (uiPulse/list change)
return;
}
// Draw Axis
double axisY = 30;
Line axis = new Line(0, axisY, width, axisY);
axis.setStroke(Color.GRAY);
this.getChildren().add(axis);
long now = System.currentTimeMillis();
// 1) Draw Priority Candidates (upper lanes)
for (CandidateEvent ev : candidates) {
long timeDiff = ev.getTimeUntilMs();
if (timeDiff < 0 || timeDiff > PREVIEW_TIME_MS) continue;
double percent = (double) timeDiff / PREVIEW_TIME_MS;
double xPos = percent * width;
xPos = Math.max(margin, Math.min(this.getWidth() - margin, xPos)); //starting point of the diagram
Node marker = createCandidateMarker(ev);
applyAntennaEffect(marker, ev.getTargetAzimuth());
// Upper lanes so they don't overlap skeds
double laneBaseY = 2;
double laneOffsetY = 14.0 * ev.getLaneIndex();
marker.setLayoutX(xPos);
marker.setLayoutY(laneBaseY + laneOffsetY);
this.getChildren().add(marker);
}
// 2) Draw Skeds (lower lane)
for (ContestSked sked : skeds) {
long timeDiff = sked.getSkedTimeEpoch() - now;
// Only draw if within the next 30 mins
if (timeDiff >= 0 && timeDiff <= PREVIEW_TIME_MS) {
double percent = (double) timeDiff / PREVIEW_TIME_MS;
double xPos = percent * width;
xPos = clamp(xPos, 10, width - 10);
Node marker = createSkedMarker(sked);
applyAntennaEffect(marker, sked.getTargetAzimuth());
marker.setLayoutX(xPos);
marker.setLayoutY(axisY - 18); // below candidate lanes, near axis
this.getChildren().add(marker);
}
}
}
private double clamp(double v, double min, double max) {
return Math.max(min, Math.min(max, v));
}
/**
* Logic:
* If Antenna is ON TARGET -> Bright & Glowing.
* If Antenna is OFF TARGET -> Transparent (Ghost).
*/
private void applyAntennaEffect(Node marker, double targetAz) {
// invalid azimuth -> keep readable
if (!Double.isFinite(targetAz) || targetAz < 0) {
return;
}
double delta = Math.abs(currentAntennaAzimuth - targetAz);
if (delta > 180) delta = 360 - delta;
final boolean onTarget = delta <= (beamWidth / 2.0);
final boolean inBeam = delta <= beamWidth;
// Rule: only fade when we are clearly NOT pointing there
final double iconOpacity = inBeam ? 1.0 : 0.30;
if (marker instanceof Group g) {
// Never fade the whole group -> text stays readable
g.setOpacity(1.0);
for (Node child : g.getChildren()) {
if (child instanceof Label) {
child.setOpacity(1.0);
} else {
child.setOpacity(iconOpacity);
}
}
// Add glow only if well centered (optional)
if (onTarget) {
g.setEffect(new DropShadow(10, Color.LIMEGREEN));
g.setScaleX(1.10);
g.setScaleY(1.10);
} else {
g.setEffect(null);
g.setScaleX(1.0);
g.setScaleY(1.0);
}
return;
}
// fallback
marker.setOpacity(iconOpacity);
marker.setEffect(onTarget ? new DropShadow(10, Color.LIMEGREEN) : null);
}
public void setSkedTooltipExtraTextProvider(Function<ContestSked, String> provider) {
this.skedTooltipExtraTextProvider = provider;
}
/** Existing marker for Skeds (diamond + label) */
private Node createSkedMarker(ContestSked sked) {
Polygon diamond = new Polygon(0.0, 0.0, 6.0, 6.0, 0.0, 12.0, -6.0, 6.0);
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent()));
String baseToolTipFallBack = sked.getTargetCallsign() + " (" + sked.getBand() + ")\nAz: " + sked.getTargetAzimuth();
if (skedTooltipExtraTextProvider != null) {
String extra = skedTooltipExtraTextProvider.apply(sked);
if (extra != null && !extra.isBlank()) {
baseToolTipFallBack += "\n" + extra;
}
}
Tooltip t = new Tooltip(baseToolTipFallBack);
Tooltip.install(diamond, t);
Label lbl = new Label("SKED: " + sked.getTargetCallsign());
// lbl.setFont(new Font(9));
// lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(14);
lbl.setLayoutX(-10);
lbl.setStyle(
"-fx-text-fill: white;" +
"-fx-font-weight: bold;" +
"-fx-background-color: rgba(0,0,0,0.65);" +
"-fx-background-radius: 6;" +
"-fx-padding: 1 4 1 4;"
);
lbl.setEffect(new DropShadow(2, Color.BLACK));
return new Group(diamond, lbl);
}
/**
* Give me a color for a given potencial of a AP reflection
*
* @param p AS potencial
* @return
*/
private Color colorForPotential(int p) {
if (p >= 95) return Color.MAGENTA; // ~100%
if (p >= 75) return Color.RED;
if (p >= 50) return Color.YELLOW;
return Color.DEEPSKYBLUE; // low potential
}
/** New marker for Priority Candidates (triangle + label) */
private Node createCandidateMarker(CandidateEvent ev) {
// Color derived from airplane potential (not urgency)
Color markerColor = colorForPotential(ev.getOpportunityPotentialPercent());
// small triangle marker (points downwards)
Polygon tri = new Polygon(-6.0, 0.0, 6.0, 0.0, 0.0, 10.0);
tri.setFill(markerColor);
// Optional: small dot behind triangle (makes it easier to see)
Circle dot = new Circle(4, markerColor);
dot.setLayoutY(5); // center behind triangle
Label lbl = new Label(ev.getDisplayCallSign());
lbl.setFont(new Font(9));
lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(10);
lbl.setLayoutX(-12);
lbl.setStyle(
"-fx-text-fill: white;" +
"-fx-font-weight: bold;" +
"-fx-background-color: rgba(0,0,0,0.65);" +
"-fx-background-radius: 6;" +
"-fx-padding: 1 4 1 4;"
);
lbl.setEffect(new DropShadow(8, Color.BLACK));
// IMPORTANT: include dot + triangle + label in the Group
Group g = new Group(dot, tri, lbl);
if (ev.getTooltipText() != null && !ev.getTooltipText().isBlank()) {
Tooltip.install(g, new Tooltip(ev.getTooltipText()));
}
g.setOnMouseClicked(e -> {
if (onCandidateClicked != null) {
onCandidateClicked.accept(ev);
}
});
return g;
}
/**
* Data object rendered by the Timeline ("priority candidate").
* Created in Kst4ContestApplication from ScoreService TopCandidates + AirScout next-AP minute.
*/
public static class CandidateEvent {
private final String callSignRaw;
private final String displayCallSign;
private final ChatCategory preferredChatCategory;
private final long timeUntilMs;
private final int minuteBucket;
private final int laneIndex;
private final double targetAzimuth;
private final double score;
private final String tooltipText;
private final int opportunityPotentialPercent;
public CandidateEvent(
String callSignRaw,
String displayCallSign,
ChatCategory preferredChatCategory,
long timeUntilMs,
int minuteBucket,
int laneIndex,
double targetAzimuth,
double score,
int opportunityPotentialPercent,
String tooltipText
) {
this.callSignRaw = callSignRaw;
this.displayCallSign = displayCallSign;
this.preferredChatCategory = preferredChatCategory;
this.timeUntilMs = timeUntilMs;
this.minuteBucket = minuteBucket;
this.laneIndex = laneIndex;
this.targetAzimuth = targetAzimuth;
this.score = score;
this.opportunityPotentialPercent = opportunityPotentialPercent;
this.tooltipText = tooltipText;
}
public String getCallSignRaw() { return callSignRaw; }
public String getDisplayCallSign() { return displayCallSign; }
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
public long getTimeUntilMs() { return timeUntilMs; }
public int getMinuteBucket() { return minuteBucket; }
public int getLaneIndex() { return laneIndex; }
public double getTargetAzimuth() { return targetAzimuth; }
public double getScore() { return score; }
public String getTooltipText() { return tooltipText; }
public int getOpportunityPotentialPercent() { return opportunityPotentialPercent; }
}
public void setBeamWidthDeg(double beamWidthDeg) {
if (beamWidthDeg > 0 && Double.isFinite(beamWidthDeg)) {
this.beamWidth = beamWidthDeg;
}
}
}

View File

@@ -3,6 +3,7 @@ module praktiKST {
requires jdk.xml.dom; requires jdk.xml.dom;
requires java.sql; requires java.sql;
requires javafx.media; requires javafx.media;
exports kst4contest.controller.interfaces;
exports kst4contest.controller; exports kst4contest.controller;
exports kst4contest.locatorUtils; exports kst4contest.locatorUtils;
exports kst4contest.model; exports kst4contest.model;

View File

@@ -6,6 +6,52 @@
-fx-border-color: #ff7777; -fx-border-color: #ff7777;
} }
.btn-showstate-enabled-default {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: black;
}
.btn-showstate-enabled-default:hover {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: black;
}
.btn-showstate-enabled-furtherInfo {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: green;
}
.btn-showstate-enabled-furtherInfo:hover {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: green;
}
.btn-showstate-disabled {
-fx-background-color:linear-gradient(#f0ff35, #111111),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: red;
}
.toggle-button:selected { .toggle-button:selected {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00), -fx-background-color:linear-gradient(#f0ff35, #a9ff00),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%); radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
@@ -29,6 +75,7 @@
.text-input-MYQRG1 { .text-input-MYQRG1 {
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, orange 0%, red 100%); -fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, orange 0%, red 100%);
-fx-font-weight: 300; -fx-font-weight: 300;
-fx-padding: 1,1,1,1;
} }
.button{ .button{

View File

@@ -30,6 +30,8 @@
.text-input-MYQRG1 { .text-input-MYQRG1 {
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/ -fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/
-fx-font-weight: 300;
-fx-padding: 1,1,1,1;
} }
@@ -44,6 +46,7 @@
.button:hover{ .button:hover{
-fx-text-fill: white; -fx-text-fill: white;
-fx-border-color: #ff7777;
} }
.separator *.line { .separator *.line {
@@ -82,6 +85,54 @@
-fx-text-fill: #395306; -fx-text-fill: #395306;
} }
.btn-showstate-enabled-default {
-fx-base: #373e43;
-fx-text-fill: lightgray;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-default:hover {
-fx-base: #373e43;
-fx-text-fill: black;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-furtherInfo {
-fx-base: #373e43;
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-furtherInfo:hover {
-fx-base: #373e43;
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-disabled {
-fx-base: #373e43 ;
-fx-font-weight: bold;
-fx-text-fill: red;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.toggle-button:selected { .toggle-button:selected {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00), -fx-background-color:linear-gradient(#f0ff35, #a9ff00),

View File

@@ -8,8 +8,8 @@ public class TestReadUDPASListenerThread {
@Test @Test
public static void main(String[] args) { public static void main(String[] args) {
ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST"); // ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
asUDPReader.start(); // asUDPReader.start();
String testThis; String testThis;

View File

@@ -8,8 +8,8 @@ public class TestReadUDPUCXListenerThread {
@Test @Test
public static void main(String[] args) { public static void main(String[] args) {
ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060); // ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
ucxUDPReader.start(); // ucxUDPReader.start();
String testThis; String testThis;