mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-03-29 20:20:56 +02:00
Integrate latest local development state and clean repository artifacts
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -16,3 +16,19 @@ target
|
||||
debug.out
|
||||
.DS_Store
|
||||
|
||||
#Logfiles
|
||||
SimpleLogFile.txt
|
||||
udpReaderBackup.txt
|
||||
|
||||
#tempfiles
|
||||
.idea/
|
||||
out/
|
||||
|
||||
#targetfiles - mvn wrapper
|
||||
target/
|
||||
|
||||
#builds
|
||||
build/
|
||||
|
||||
#zip files for local backups
|
||||
*.zip
|
||||
@@ -1,6 +1,17 @@
|
||||
package kst4contest;
|
||||
|
||||
import java.util.Random;
|
||||
|
||||
public class ApplicationConstants {
|
||||
|
||||
/**
|
||||
* default constructor generates runtime id
|
||||
*/
|
||||
ApplicationConstants() {
|
||||
sessionRuntimeUniqueId = generateRuntimeId();
|
||||
};
|
||||
|
||||
public static int sessionRuntimeUniqueId = generateRuntimeId();
|
||||
/**
|
||||
* Name of the Application.
|
||||
*/
|
||||
@@ -9,7 +20,7 @@ public class ApplicationConstants {
|
||||
/**
|
||||
* 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 VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
|
||||
@@ -21,6 +32,23 @@ public class ApplicationConstants {
|
||||
public static final String DISCSTRING_DISCONNECT_DUE_PAWWORDERROR = "JUSTDSICCAUSEPWWRONG";
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,25 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
|
||||
|
||||
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 asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
|
||||
String asWatchListStringSuffix = asWatchListString;
|
||||
@@ -70,10 +85,9 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
|
||||
for (ChatMember i : ary_threadSafeChatMemberArray) {
|
||||
|
||||
|
||||
|
||||
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!
|
||||
//this counts for AS request and Aswatchlist
|
||||
{
|
||||
suffix = i.getCallSign() + "," + i.getQra() + " ";
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
/**
|
||||
* This class is for sending beacons intervalled to the public chat. Gets all
|
||||
@@ -20,16 +22,23 @@ import kst4contest.model.ChatMessage;
|
||||
public class BeaconTask extends TimerTask {
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
|
||||
ChatMessage beaconMSG = new ChatMessage();
|
||||
|
||||
@@ -75,8 +84,12 @@ public class BeaconTask extends TimerTask {
|
||||
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
|
||||
this.chatController.getMessageTXBus().add(beaconMSG);
|
||||
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} 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());
|
||||
this.chatController.getMessageTXBus().add(beaconMSG2);
|
||||
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} 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
@@ -2,6 +2,7 @@ package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
import java.io.*;
|
||||
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 ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "DXCluster-Server";
|
||||
ChatController chatController = null;
|
||||
protected int serverPort = 8080;
|
||||
protected ServerSocket serverSocket = null;
|
||||
@@ -23,13 +26,17 @@ public class DXClusterThreadPooledServer implements Runnable{
|
||||
Executors.newFixedThreadPool(10);
|
||||
Socket clientSocket;
|
||||
|
||||
public DXClusterThreadPooledServer(int port, ChatController chatController){
|
||||
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
|
||||
this.serverPort = port;
|
||||
this.chatController = chatController;
|
||||
this.callBackToController = callback;
|
||||
}
|
||||
|
||||
public void run(){
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
synchronized(this){
|
||||
this.runningThread = Thread.currentThread();
|
||||
runningThread.setName("DXCluster-thread-pooled-server");
|
||||
@@ -53,7 +60,7 @@ public class DXClusterThreadPooledServer implements Runnable{
|
||||
"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);
|
||||
|
||||
@@ -111,6 +118,7 @@ public class DXClusterThreadPooledServer implements Runnable{
|
||||
for (Socket socket : clientSockets) {
|
||||
|
||||
try {
|
||||
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String singleDXClusterMessage = "DX de ";
|
||||
@@ -134,6 +142,9 @@ public class DXClusterThreadPooledServer implements Runnable{
|
||||
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
|
||||
@@ -152,12 +163,16 @@ class DXClusterServerWorkerRunnable implements Runnable{
|
||||
protected String serverText = null;
|
||||
private ChatController client = null;
|
||||
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.serverText = serverText;
|
||||
this.client = chatController;
|
||||
this.dxClusterClientSocketsConnectedList = clientSockets;
|
||||
this.callBackToController = callback;
|
||||
|
||||
}
|
||||
|
||||
public void run() {
|
||||
@@ -171,8 +186,12 @@ class DXClusterServerWorkerRunnable implements Runnable{
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
StringBuilder connectedClients = new StringBuilder(); //only for statistics
|
||||
|
||||
for (Socket socket : dxClusterClientSocketsConnectedList) {
|
||||
|
||||
connectedClients.append(socket.getInetAddress()).append("\n");
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
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);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public class DXClusterThreadPooledServerTest {
|
||||
testPreferences.setStn_loginCallSign("DM5M");
|
||||
|
||||
client.setChatPreferences(testPreferences);
|
||||
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client);
|
||||
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client, client);
|
||||
|
||||
new Thread(dxClusterServer).start();
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import java.io.PrintWriter;
|
||||
import java.sql.SQLException;
|
||||
//import java.net.Socket;
|
||||
//import java.util.ArrayList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -28,6 +30,9 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
int index;
|
||||
|
||||
private String ThreadNickName = "MessageBus";
|
||||
private ThreadStatusCallback callBackToController;
|
||||
|
||||
private PrintWriter writer;
|
||||
// private Socket socket;
|
||||
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_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)?)";
|
||||
|
||||
|
||||
// ==== 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;
|
||||
|
||||
// private String text;
|
||||
@@ -60,10 +74,14 @@ public class MessageBusManagementThread extends Thread {
|
||||
this.serverReady = serverReady;
|
||||
}
|
||||
|
||||
public MessageBusManagementThread(ChatController client) {
|
||||
public MessageBusManagementThread(ChatController client, ThreadStatusCallback callBack) {
|
||||
|
||||
this.callBackToController = callBack;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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 {
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last message processed:\n" + messageToProcess.getMessageText(), false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
final String INITIALUSERLISTENTRY = "UA0";
|
||||
final String USERENTEREDCHAT = "UA5";
|
||||
final String USERENTEREDCHAT2 = "UA2"; // seen at 50MHZ Chat
|
||||
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 USERLEFTCHAT2 = "UR7";
|
||||
final String CHATCHANNELMESSAGE = "CH";
|
||||
@@ -335,7 +534,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
qrgQuestionTexts.add("your qrg?");
|
||||
qrgQuestionTexts.add("qrg?");
|
||||
qrgQuestionTexts.add("freq?");
|
||||
qrgQuestionTexts.add("pse QRG");
|
||||
qrgQuestionTexts.add("pse qrg");
|
||||
|
||||
|
||||
/**
|
||||
@@ -343,7 +542,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
*/
|
||||
|
||||
if (messageToProcess.getMessageText().isEmpty()) {
|
||||
System.out.println("[MSGBUSMGTT:] ######################no processable data");
|
||||
// System.out.println("[MSGBUSMGTT:] no processable data");
|
||||
|
||||
} else {
|
||||
|
||||
@@ -364,6 +563,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
* Initializes the Userlist if entry fits UA0
|
||||
* UA0|3|DL6SAQ|walter not qrv|JN58CK|1| <- RXed
|
||||
*
|
||||
*
|
||||
*/
|
||||
if (splittedMessageLine[0].contains(INITIALUSERLISTENTRY)) {
|
||||
// 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.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())) {
|
||||
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);
|
||||
|
||||
|
||||
// bufwrtrDBGMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
// + "[MSGBUSMGT:] User detected and added to list [" + this.client.getChatMemberTable().size()
|
||||
// + "] :" + 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);
|
||||
|
||||
// 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(
|
||||
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) {
|
||||
System.out.println("[MSGBUSMGT, EXC!, Error:] User sent left chat but had not been there ... ["
|
||||
+ this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign() + "\n"
|
||||
+ 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
|
||||
|
||||
/**
|
||||
@@ -524,8 +710,28 @@ public class MessageBusManagementThread extends Thread {
|
||||
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());
|
||||
// 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]");
|
||||
@@ -611,8 +817,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
if (newMessageArrived.getReceiver().getCallSign()
|
||||
.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); //TODO: change, moved to globalmessagelist, original
|
||||
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
|
||||
|
||||
if (this.client.getChatPreferences().isNotify_playSimpleSounds()) {
|
||||
this.client.getPlayAudioUtils().playNoiseLauncher('P');
|
||||
@@ -629,49 +834,121 @@ public class MessageBusManagementThread extends Thread {
|
||||
this.client.getPlayAudioUtils().playVoiceLauncher("!");
|
||||
}
|
||||
}
|
||||
if (newMessageArrived.getMessageText().toUpperCase().contains("//VER")) {
|
||||
|
||||
if (this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled()) {
|
||||
|
||||
ChatMessage automaticAnswer = new ChatMessage();
|
||||
ChatMessage versionInfo = 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);
|
||||
versionInfo.setSender(itsMe);
|
||||
versionInfo.setReceiver(newMessageArrived.getSender());
|
||||
versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + ApplicationConstants.AUTOANSWER_PREFIX + " " + "KST4Contest " + " v" + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER + " by DO5AMF");
|
||||
|
||||
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
|
||||
*/
|
||||
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);
|
||||
//
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
for (String lookForQRGString : qrgQuestionTexts) {
|
||||
if (newMessageArrived.getMessageText().contains(lookForQRGString)) {
|
||||
// ==== Unified Autoanswer (Generic + QRG) with Pingpong-Guard + per-Remote Cooldown ====
|
||||
final String incomingText = newMessageArrived.getMessageText();
|
||||
final String incomingLower = (incomingText == null) ? "" : incomingText.toLowerCase(Locale.ROOT);
|
||||
|
||||
ChatMessage automaticAnswer = new ChatMessage();
|
||||
ChatMember itsMe = new ChatMember();
|
||||
itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
|
||||
// 1) Pingpong-security: never ever react to auto generated messages
|
||||
if (!isAutoMessage(newMessageArrived)) {
|
||||
|
||||
automaticAnswer.setSender(itsMe);
|
||||
automaticAnswer.setReceiver(newMessageArrived.getSender());
|
||||
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
boolean qrgRequested = false;
|
||||
|
||||
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());
|
||||
if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) {
|
||||
for (String lookForQRGString : qrgQuestionTexts) {
|
||||
if (incomingLower.contains(lookForQRGString)) {
|
||||
qrgRequested = true;
|
||||
break;
|
||||
}
|
||||
|
||||
this.client.getMessageTXBus().add(automaticAnswer);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
ChatMember itsMe = new ChatMember();
|
||||
itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
|
||||
|
||||
automaticAnswer.setSender(itsMe);
|
||||
automaticAnswer.setReceiver(newMessageArrived.getSender());
|
||||
|
||||
// Prefix fest + nicht entfernbar, damit Auto↔Auto nicht pingpongt
|
||||
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
|
||||
+ " " + AUTOANSWER_PREFIX + " " + payload);
|
||||
|
||||
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() + ".");
|
||||
|
||||
} else if (newMessageArrived.getSender().getCallSign().toUpperCase()
|
||||
@@ -690,7 +967,6 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
} else {
|
||||
//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(),
|
||||
newMessageArrived.getSender().getQra(),
|
||||
newMessageArrived.getReceiver().getQra(),
|
||||
@@ -709,11 +985,31 @@ public class MessageBusManagementThread extends Thread {
|
||||
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
|
||||
try {
|
||||
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) {
|
||||
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0" + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
|
||||
exception.printStackTrace();
|
||||
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
|
||||
// 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());
|
||||
}
|
||||
|
||||
String locatedFrequencies = checkIfMessageInhibitsFrequency(newMessageArrived);
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
|
||||
smartFrequencyExtraction(newMessageArrived, this.client.getChatPreferences());
|
||||
|
||||
// TODO: Next: get frequency infos out of name?
|
||||
} else
|
||||
@@ -1006,6 +1260,214 @@ public class MessageBusManagementThread extends Thread {
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -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() {
|
||||
|
||||
// fileLogRAW = new File(new Utils4KST().time_generateCurrentMMddString() + "_praktiKST_raw.txt");
|
||||
@@ -1178,7 +1681,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
try {
|
||||
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();
|
||||
break;
|
||||
}
|
||||
@@ -1422,7 +1925,6 @@ public class MessageBusManagementThread extends Thread {
|
||||
// } //end tx.peek != null
|
||||
}
|
||||
|
||||
// System.out.println("messagebusmgt while performed");
|
||||
|
||||
} // while true end
|
||||
System.out.println("Msgbusmgt: interrupt");
|
||||
|
||||
237
src/main/java/kst4contest/controller/PstRotatorClient.java
Normal file
237
src/main/java/kst4contest/controller/PstRotatorClient.java
Normal 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>");
|
||||
}
|
||||
}
|
||||
328
src/main/java/kst4contest/controller/ReadUDPByWintestThread.java
Normal file
328
src/main/java/kst4contest/controller/ReadUDPByWintestThread.java
Normal 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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -10,6 +11,7 @@ import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.AirPlane;
|
||||
import kst4contest.model.AirPlaneReflectionInfo;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
/**
|
||||
* 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 int localPort;
|
||||
private String ASIdentificator, ChatClientIdentificator;
|
||||
|
||||
public ReadUDPbyAirScoutMessageThread(int localPort) {
|
||||
this.localPort = localPort;
|
||||
}
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "AirScout msg";
|
||||
// public ReadUDPbyAirScoutMessageThread(int localPort) {
|
||||
// this.localPort = localPort;
|
||||
// }
|
||||
|
||||
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator,
|
||||
String ChatClientIdentificator) {
|
||||
|
||||
String ChatClientIdentificator, ThreadStatusCallback callback) {
|
||||
|
||||
this.callBackToController = callback;
|
||||
this.localPort = localPort;
|
||||
this.client = client;
|
||||
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() {
|
||||
@@ -128,26 +137,30 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
|
||||
// do nothing, that is your own message
|
||||
} 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;
|
||||
|
||||
apReflectInfoForChatMember = processASUDPMessage(received);
|
||||
if (this.client.getLst_chatMemberList().size() != 0) {
|
||||
if (!this.client.getLst_chatMemberList().isEmpty()) {
|
||||
|
||||
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"
|
||||
* java.util.ConcurrentModificationException at
|
||||
@@ -158,6 +171,7 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
|
||||
*
|
||||
*/
|
||||
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
|
||||
// }
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -167,6 +181,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.io.*;
|
||||
import java.net.*;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
@@ -11,6 +12,7 @@ import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
import kst4contest.view.GuiUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
@@ -32,13 +34,19 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
private BufferedReader reader;
|
||||
private Socket socket;
|
||||
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) {
|
||||
this.client = client;
|
||||
public ReadUDPbyUCXMessageThread(int localPort, ChatController client, ThreadStatusCallback callback) {
|
||||
this.udpPortNr = localPort;
|
||||
this.client = client;
|
||||
this.callBackToController = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,6 +56,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
if (this.socket != null) {
|
||||
System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket");
|
||||
terminateConnection();
|
||||
// callBackToController.onThreadStatus("UDPReceiver", new String[]);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
@@ -57,17 +66,22 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
public void run() {
|
||||
|
||||
|
||||
|
||||
System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
|
||||
Thread.currentThread().setName("ReadUDPByUCXLogThread");
|
||||
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
DatagramSocket socket = null;
|
||||
boolean running;
|
||||
|
||||
boolean running;
|
||||
byte[] buf = new byte[1777];
|
||||
DatagramPacket packet = new DatagramPacket(buf, buf.length);
|
||||
|
||||
try {
|
||||
socket = new DatagramSocket(12060);
|
||||
// socket = new DatagramSocket(12060);
|
||||
socket = new DatagramSocket(udpPortNr);
|
||||
socket.setSoTimeout(2000); //TODO try for end properly
|
||||
}
|
||||
|
||||
@@ -99,8 +113,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
nE.printStackTrace();
|
||||
System.out.println("ReadUdpByUCXTH: Socket not ready");
|
||||
|
||||
|
||||
|
||||
try {
|
||||
socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort());
|
||||
socket.setSoTimeout(2000);
|
||||
@@ -136,6 +148,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
System.out.println("ReadUdpByUCX, Info: got poison, now dieing....");
|
||||
socket.close();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -163,7 +181,17 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
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();
|
||||
try {
|
||||
@@ -240,7 +268,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
case "10G": {
|
||||
workedCall.setWorked10G(true);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -310,56 +338,41 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
modifyThat = client.getLst_chatMemberList().get(index);
|
||||
|
||||
modifyThat.setWorked(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
|
||||
|
||||
if (workedCall.isWorked144()) {
|
||||
modifyThat.setWorked144(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked144(true);
|
||||
|
||||
} else if (workedCall.isWorked432()) {
|
||||
modifyThat.setWorked432(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked432(true);
|
||||
|
||||
} else if (workedCall.isWorked1240()) {
|
||||
modifyThat.setWorked1240(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked1240(true);
|
||||
|
||||
} else if (workedCall.isWorked2300()) {
|
||||
modifyThat.setWorked2300(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked2300(true);
|
||||
|
||||
} else if (workedCall.isWorked3400()) {
|
||||
modifyThat.setWorked3400(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked3400(true);
|
||||
|
||||
} else if (workedCall.isWorked5600()) {
|
||||
modifyThat.setWorked5600(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked5600(true);
|
||||
|
||||
} else if (workedCall.isWorked10G()) {
|
||||
modifyThat.setWorked10G(true);
|
||||
// client.getLst_chatMemberList()
|
||||
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
|
||||
// .setWorked10G(true);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
//do nothing, as it works...
|
||||
}
|
||||
@@ -539,7 +552,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
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();
|
||||
System.out.println(e.getCause());
|
||||
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) {
|
||||
// TODO Auto-generated catch block
|
||||
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());
|
||||
@@ -561,6 +592,14 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
|
||||
309
src/main/java/kst4contest/controller/ScoreService.java
Normal file
309
src/main/java/kst4contest/controller/ScoreService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
124
src/main/java/kst4contest/controller/SkedReminderService.java
Normal file
124
src/main/java/kst4contest/controller/SkedReminderService.java
Normal 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();
|
||||
}
|
||||
}
|
||||
268
src/main/java/kst4contest/controller/StationMetricsService.java
Normal file
268
src/main/java/kst4contest/controller/StationMetricsService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
public interface ThreadStatusCallback {
|
||||
void onThreadStatus(String threadName, ThreadStateMessage threadStateMessage);
|
||||
|
||||
}
|
||||
@@ -15,7 +15,10 @@ import kst4contest.model.ChatMember;
|
||||
public class UCXLogFileToHashsetParser {
|
||||
|
||||
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) {
|
||||
|
||||
|
||||
@@ -51,21 +51,10 @@ public class UserActualizationTask extends TimerTask {
|
||||
UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser(
|
||||
this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly());
|
||||
|
||||
// UCXLogFileToHashsetParser getWorkedCallsignsOfUDPBackupFile = new UCXLogFileToHashsetParser(
|
||||
// this.client.getChatPreferences().getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
|
||||
|
||||
try {
|
||||
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: fetchedWorkedSetudpbckup size: " + fetchedWorkedSetUdpBckup.size());
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
|
||||
@@ -52,7 +52,8 @@ public class Utils4KST {
|
||||
// Instant instant = Instant.ofEpochSecond(epoch);
|
||||
|
||||
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"));
|
||||
String formatted = format.format(date);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.net.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
/**
|
||||
@@ -169,8 +170,8 @@ public class WriteThread extends Thread {
|
||||
try {
|
||||
messageToBeSend = client.getMessageTXBus().take();
|
||||
|
||||
if (messageToBeSend.getMessageText().equals("POISONPILL_KILLTHREAD")
|
||||
&& messageToBeSend.getMessageSenderName().equals("POISONPILL_KILLTHREAD")) {
|
||||
if (messageToBeSend.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)
|
||||
&& messageToBeSend.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
client.getMessageRXBus().clear();
|
||||
this.interrupt();
|
||||
break;
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
292
src/main/java/kst4contest/logic/PriorityCalculator.java
Normal file
292
src/main/java/kst4contest/logic/PriorityCalculator.java
Normal 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();
|
||||
}
|
||||
}
|
||||
51
src/main/java/kst4contest/logic/SignalDetector.java
Normal file
51
src/main/java/kst4contest/logic/SignalDetector.java
Normal 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;
|
||||
}
|
||||
}
|
||||
49
src/main/java/kst4contest/model/Band.java
Normal file
49
src/main/java/kst4contest/model/Band.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package kst4contest.model;
|
||||
|
||||
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.BooleanProperty;
|
||||
@@ -9,7 +13,10 @@ import javafx.beans.property.StringProperty;
|
||||
|
||||
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;
|
||||
String callSign;
|
||||
String qra;
|
||||
@@ -45,6 +52,12 @@ public class ChatMember {
|
||||
boolean worked3400;
|
||||
boolean worked5600;
|
||||
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
|
||||
@@ -58,9 +71,35 @@ public class ChatMember {
|
||||
boolean qrv10G = 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() {
|
||||
return isInAngleAndRange;
|
||||
@@ -270,8 +309,129 @@ public class ChatMember {
|
||||
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) {
|
||||
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() {
|
||||
@@ -313,9 +473,51 @@ public class ChatMember {
|
||||
return worked;
|
||||
}
|
||||
|
||||
public void setWorked(boolean 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) {
|
||||
this.worked = worked;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,13 +526,15 @@ public class ChatMember {
|
||||
*/
|
||||
public String getCallSignRaw() {
|
||||
|
||||
String raw = "";
|
||||
|
||||
try {
|
||||
return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m
|
||||
} catch (Exception e) {
|
||||
return getCallSign();
|
||||
}
|
||||
return callSignRaw;
|
||||
// String raw = "";
|
||||
//
|
||||
// try {
|
||||
// return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m
|
||||
// } catch (Exception e) {
|
||||
// return getCallSign();
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
@@ -342,13 +546,20 @@ public class ChatMember {
|
||||
|
||||
this.setWorked(false);
|
||||
this.setWorked144(false);
|
||||
this.setWorked50(false);
|
||||
this.setWorked70(false);
|
||||
this.setWorked432(false);
|
||||
this.setWorked1240(false);
|
||||
this.setWorked2300(false);
|
||||
this.setWorked3400(false);
|
||||
this.setWorked5600(false);
|
||||
this.setWorked10G(false);
|
||||
}
|
||||
this.setWorked24G(false);
|
||||
this.setWorked47G(false);
|
||||
this.setWorked76G(false);
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all worked information of this object to false. Scope: GUI, Reset Button
|
||||
@@ -391,4 +602,56 @@ public class ChatMember {
|
||||
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
52
src/main/java/kst4contest/model/ContestSked.java
Normal file
52
src/main/java/kst4contest/model/ContestSked.java
Normal 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;
|
||||
}
|
||||
}
|
||||
103
src/main/java/kst4contest/model/ThreadStateMessage.java
Normal file
103
src/main/java/kst4contest/model/ThreadStateMessage.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
256
src/main/java/kst4contest/test/MockKstServer.java
Normal file
256
src/main/java/kst4contest/test/MockKstServer.java
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -44,25 +44,33 @@ public class GuiUtils {
|
||||
|
||||
public static void triggerGUIFilteredChatMemberListChange(ChatController chatController) {
|
||||
|
||||
{
|
||||
//trick to trigger gui changes on property changes of obects
|
||||
|
||||
Predicate<ChatMember> dummyPredicate = new Predicate<ChatMember>() {
|
||||
@Override
|
||||
public boolean test(ChatMember chatMember) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* //TODO: following 2 lines are a quick fix to making disappear worked chatmembers of the list
|
||||
* Thats uncomfortable due to this also causes selection changes,
|
||||
* Better way is to change all worked and qrv values to observables and then trigger the underlying
|
||||
* list to fire an invalidationevent. Really Todo!
|
||||
*/
|
||||
chatController.getLst_chatMemberListFilterPredicates().add(dummyPredicate);
|
||||
chatController.getLst_chatMemberListFilterPredicates().remove(dummyPredicate);
|
||||
|
||||
}
|
||||
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
|
||||
|
||||
Predicate<ChatMember> dummyPredicate = new Predicate<ChatMember>() {
|
||||
@Override
|
||||
public boolean test(ChatMember chatMember) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* //TODO: following 2 lines are a quick fix to making disappear worked chatmembers of the list
|
||||
* Thats uncomfortable due to this also causes selection changes,
|
||||
* Better way is to change all worked and qrv values to observables and then trigger the underlying
|
||||
* list to fire an invalidationevent. Really Todo!
|
||||
*/
|
||||
chatController.getLst_chatMemberListFilterPredicates().add(dummyPredicate);
|
||||
chatController.getLst_chatMemberListFilterPredicates().remove(dummyPredicate);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
369
src/main/java/kst4contest/view/TimelineView.java
Normal file
369
src/main/java/kst4contest/view/TimelineView.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ module praktiKST {
|
||||
requires jdk.xml.dom;
|
||||
requires java.sql;
|
||||
requires javafx.media;
|
||||
exports kst4contest.controller.interfaces;
|
||||
exports kst4contest.controller;
|
||||
exports kst4contest.locatorUtils;
|
||||
exports kst4contest.model;
|
||||
|
||||
@@ -6,6 +6,52 @@
|
||||
-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 {
|
||||
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
|
||||
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
|
||||
@@ -29,6 +75,7 @@
|
||||
.text-input-MYQRG1 {
|
||||
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, orange 0%, red 100%);
|
||||
-fx-font-weight: 300;
|
||||
-fx-padding: 1,1,1,1;
|
||||
}
|
||||
|
||||
.button{
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
.text-input-MYQRG1 {
|
||||
-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{
|
||||
-fx-text-fill: white;
|
||||
-fx-border-color: #ff7777;
|
||||
}
|
||||
|
||||
.separator *.line {
|
||||
@@ -82,6 +85,54 @@
|
||||
-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 {
|
||||
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
|
||||
|
||||
@@ -8,8 +8,8 @@ public class TestReadUDPASListenerThread {
|
||||
@Test
|
||||
public static void main(String[] args) {
|
||||
|
||||
ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
|
||||
asUDPReader.start();
|
||||
// ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
|
||||
// asUDPReader.start();
|
||||
|
||||
String testThis;
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ public class TestReadUDPUCXListenerThread {
|
||||
@Test
|
||||
public static void main(String[] args) {
|
||||
|
||||
ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
|
||||
ucxUDPReader.start();
|
||||
// ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
|
||||
// ucxUDPReader.start();
|
||||
|
||||
|
||||
String testThis;
|
||||
|
||||
Reference in New Issue
Block a user