Fixed AS preferences implementation, DXcluster Server and the documentation

This commit is contained in:
Marc Froehlich
2026-07-25 03:01:49 +02:00
parent 5b7897c872
commit 00496b56e9
10 changed files with 2010 additions and 935 deletions
@@ -1,150 +1,198 @@
package kst4contest.controller;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.NoRouteToHostException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.collections.ObservableList;
import kst4contest.locatorUtils.Location;
import kst4contest.model.ChatMember;
/**
* Sends periodical path requests and an AirScout watchlist for the currently
* active ON4KST stations.
*/
public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
private static final Logger LOGGER = Logger.getLogger(AirScoutPeriodicalAPReflectionInquirerTask.class.getName());
private ChatController client;
private static final Logger LOGGER = Logger.getLogger(
AirScoutPeriodicalAPReflectionInquirerTask.class.getName()
);
public AirScoutPeriodicalAPReflectionInquirerTask(ChatController client) {
private static final String BROADCAST_ADDRESS = "255.255.255.255";
private final ChatController client;
public AirScoutPeriodicalAPReflectionInquirerTask(
ChatController client
) {
this.client = client;
}
@Override
public void run() {
Thread.currentThread().setName("AirscoutPeriodicalReflectionInquirierTask");
Thread.currentThread().setName(
"AirscoutPeriodicalReflectionInquirerTask"
);
String KSTClientsNameForQuery = this.client.getChatPreferences().getAirScout_asClientNameString();
String ASServerNameStringForAnswer = this.client.getChatPreferences().getAirScout_asServerNameString();
//TODO: Manage prefixes kst and as via preferences file and instance
//TODO: Check if locator is changeable via the preferences object, need to be correct if it changes
DatagramSocket dsocket;
// String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" "; //working original
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String prefix_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) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Error parsing callsign", e);
}
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
String asWatchListStringSuffix = asWatchListString;
String host = "255.255.255.255";
// int port = 9872;
int port = client.getChatPreferences().getAirScout_asCommunicationPort();
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
InetAddress address;
/**
* Iterate over chatmemberlist and asking airscout for plane reflection information
* To avoid a concurrentmodifyexception, we have to convert the original list to an array at first
* since the iterator brakes if the list changing during the iteration time
/*
* Keep the scheduled task installed so that AirScout can be enabled at
* runtime, but do not send anything while the integration is disabled.
*/
ObservableList<ChatMember> praktiKSTActiveUserList = this.client.getLst_chatMemberList();
ChatMember[] ary_threadSafeChatMemberArray = new ChatMember[praktiKSTActiveUserList.size()];
praktiKSTActiveUserList.toArray(ary_threadSafeChatMemberArray);
for (ChatMember i : ary_threadSafeChatMemberArray) {
if (!client.getChatPreferences().isAirScout_asUDPListenerEnabled()) {
return;
}
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() + " ";
String clientIdentifier =
client.getChatPreferences().getAirScout_asClientNameString();
String serverIdentifier =
client.getChatPreferences().getAirScout_asServerNameString();
String bandValue =
client.getChatPreferences().getAirScout_asBandString();
String queryStringToAirScout = "";
String ownCallSign = normalizeOwnCallSign(
client.getChatPreferences().getStn_loginCallSign()
);
String ownLocator =
client.getChatPreferences().getStn_loginLocatorMainCat();
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
if (ownCallSign == null
|| ownCallSign.isBlank()
|| ownLocator == null
|| ownLocator.isBlank()) {
LOGGER.warning(
"AirScout queries were skipped because the own callsign "
+ "or locator is missing."
);
return;
}
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
String setPathPrefix =
"ASSETPATH: \"" + clientIdentifier
+ "\" \"" + serverIdentifier + "\" ";
try {
address = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
dsocket = new DatagramSocket();
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (UnknownHostException e1) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Unknown host", e1);
} catch (NoRouteToHostException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] No route to host", e);
} catch (SocketException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Socket error", e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending query", e);
String watchListPrefix =
"ASWATCHLIST: \"" + clientIdentifier
+ "\" \"" + serverIdentifier + "\" ";
String ownStation = ownCallSign + "," + ownLocator;
StringBuilder watchListMessage = new StringBuilder(
watchListPrefix
+ bandValue
+ ","
+ ownStation
);
List<ChatMember> activeMembers = client.snapshotChatMembers();
int port = client.getChatPreferences()
.getAirScout_asCommunicationPort();
try (
DatagramSocket socket = new DatagramSocket()
) {
socket.setBroadcast(true);
InetAddress broadcastAddress =
InetAddress.getByName(BROADCAST_ADDRESS);
for (ChatMember member : activeMembers) {
if (!isUsableAirScoutTarget(member)) {
continue;
}
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra();
if (member.getQrb()
>= client.getChatPreferences().getStn_maxQRBDefault()) {
continue;
}
String targetStation =
member.getCallSign() + "," + member.getQra();
String pathQuery =
setPathPrefix
+ bandValue
+ ","
+ ownStation
+ ","
+ targetStation
+ " ";
sendPacket(
socket,
broadcastAddress,
port,
pathQuery
);
watchListMessage
.append(",")
.append(targetStation);
}
watchListMessage.append(" ");
sendPacket(
socket,
broadcastAddress,
port,
watchListMessage.toString()
);
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"Could not send the periodical AirScout queries.",
exception
);
}
/**
* As next we will set the ASWatchlist. All stations in chat will be watched by airscout causing following code.\n\n
* ASWATCHLIST: "KST" "AS" 4320000,DO5AMF,JN49GL,DF9QX,JO42HD,DG2KBC,JN58MI,DJ0PY,JO32MF,DL1YDI,JO42FA,DL6BF,JO32QI,F1NZC,JN15MR,F4TXU,JN23CX,F5GHP,IN96LE,F6HTJ,JN12KQ,G0GGG,IO81VE,G0JCC,IO82MA,G0JDL,JO02SI,G0MBL,JO01QH,G4AEP,IO91MB,G4CLA,IO92JL,G4DCV,IO91OF,G4LOH,IO70JC,G4MKF,IO91HJ,G4TRA,IO81WN,G8GXP,IO93FQ,G8VHI,IO92FM,GW0RHC,IO71UN,HA4ND,JN97MJ,I5/HB9SJV/P,JN52JS,IW2DAL,JN45NN,OK1FPR,JO80CE,OK6M,JN99CR,OV3T,JO46CM,OZ2M,JO65FR,PA0V,JO33II,PA2RU,JO32LT,PA3DOL,JO22MT,PA9R,JO22JK,PE1EVX,JO22MP,S51AT,JN75GW,SM7KOJ,JO66ND,SP9TTG,JO90KW
* The watchlist-String is bult by the for loop which builds the AP queries
*/
asWatchListStringSuffix += " ";
byte[] queryStringToAirScoutMSG = asWatchListStringSuffix.getBytes();
try {
address = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
dsocket = new DatagramSocket();
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending watchlist", e);
}
// System.out.println("[ASUDPTask, info:] set watchlist: " + asWatchListStringSuffix);
}
}
/**
* Removes the ON4KST login suffix because AirScout expects the actual
* station callsign, for example 9A1W instead of 9A1W-2.
*
* @param callSign configured ON4KST login callsign
* @return callsign without an ON4KST login suffix
*/
private String normalizeOwnCallSign(String callSign) {
if (callSign == null) {
return null;
}
String normalizedCallSign = callSign.trim();
int suffixSeparator = normalizedCallSign.indexOf("-");
if (suffixSeparator > 0) {
return normalizedCallSign.substring(0, suffixSeparator);
}
return normalizedCallSign;
}
private boolean isUsableAirScoutTarget(ChatMember member) {
return member != null
&& member.getCallSign() != null
&& !member.getCallSign().isBlank()
&& member.getQra() != null
&& !member.getQra().isBlank();
}
private void sendPacket(
DatagramSocket socket,
InetAddress address,
int port,
String message
) throws IOException {
byte[] payload = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(
payload,
payload.length,
address,
port
);
socket.send(packet);
}
}
@@ -32,6 +32,7 @@ import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.nio.charset.StandardCharsets;
@@ -472,66 +473,106 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
* @param remoteChatMember with callsign of the foreign station
*/
public void airScout_SendAsShowPathPacket(ChatMember remoteChatMember) {
/**
* Requests AirScout to display the path between the own station and the
* selected remote station.
*
* The configured server identifier, client identifier, band and UDP port are
* used for every request. This allows several AirScout servers or clients to
* coexist in the same network.
*
* @param remoteChatMember selected remote station
*/
public void airScout_SendAsShowPathPacket(
ChatMember remoteChatMember
) {
if (!chatPreferences.isAirScout_asUDPListenerEnabled()) {
System.out.println(
"[AirScout, info]: Show-path request ignored because "
+ "the AirScout integration is disabled."
);
return;
}
DatagramSocket dsocket;
if (remoteChatMember == null
|| remoteChatMember.getCallSign() == null
|| remoteChatMember.getCallSign().isBlank()
|| remoteChatMember.getQra() == null
|| remoteChatMember.getQra().isBlank()) {
System.out.println(
"[AirScout, warning]: Show-path request ignored because "
+ "the selected station has no usable callsign or locator."
);
return;
}
String prefix_asSetpath ="ASSHOWPATH: \""+ this.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String ownCallSign = chatPreferences.getStn_loginCallSign();
if (ownCallSign == null || ownCallSign.isBlank()) {
return;
}
// String prefix_asSetpath ="ASSHOWPATH: \"KST\" \"AS\" "; Old hard coded
String bandString = "1440000";
// String myCallAndMyLocString = chatPreferences.getStn_loginCallSign() + "," + chatPreferences.getStn_loginLocatorMainCat(); // original b4 bugfix 1266
String remoteCallAndLocString = remoteChatMember.getCallSign() +"," + remoteChatMember.getQra();
int suffixSeparator = ownCallSign.indexOf("-");
if (suffixSeparator > 0) {
ownCallSign = ownCallSign.substring(0, suffixSeparator);
}
String ownCallSign="";
try {
if (chatPreferences.getStn_loginCallSign().contains("-")) {
ownCallSign = chatPreferences.getStn_loginCallSign().split("-")[0];
} else {
ownCallSign = chatPreferences.getStn_loginCallSign();
}
} catch (Exception e) {
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
}
String ownLocator =
chatPreferences.getStn_loginLocatorMainCat();
String myCallAndMyLocString = ownCallSign + "," + chatPreferences.getStn_loginLocatorMainCat(); // original b4 bugfix 1266
if (ownLocator == null || ownLocator.isBlank()) {
return;
}
String host = "255.255.255.255";
// int port = 9872;
int port = chatPreferences.getAirScout_asCommunicationPort();
// System.out.println("<<<<<<<<<<<<<<<<<<<<ASPERI: " + port);
String clientIdentifier =
chatPreferences.getAirScout_asClientNameString();
String serverIdentifier =
chatPreferences.getAirScout_asServerNameString();
String bandValue =
chatPreferences.getAirScout_asBandString();
int port =
chatPreferences.getAirScout_asCommunicationPort();
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
InetAddress address;
String query =
"ASSHOWPATH: \""
+ clientIdentifier
+ "\" \""
+ serverIdentifier
+ "\" "
+ bandValue
+ ","
+ ownCallSign
+ ","
+ ownLocator
+ ","
+ remoteChatMember.getCallSign()
+ ","
+ remoteChatMember.getQra()
+ " ";
String queryStringToAirScout = "";
byte[] payload = query.getBytes(StandardCharsets.UTF_8);
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + remoteCallAndLocString+ "Å";
try (
DatagramSocket socket = new DatagramSocket()
) {
socket.setBroadcast(true);
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
try {
address = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
dsocket = new DatagramSocket();
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (UnknownHostException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (NoRouteToHostException e) {
e.printStackTrace();
}
catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
InetAddress address =
InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(
payload,
payload.length,
address,
port
);
socket.send(packet);
} catch (IOException exception) {
System.out.println(
"[AirScout, error]: Could not send show-path request: "
+ exception.getMessage()
);
}
}
/**
@@ -582,7 +623,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
stopScoreScheduler();
this.dxClusterServer.stop();
stopDxClusterServer();
this.setDisconnectionPerformedByUser(true);
@@ -692,8 +733,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
dbHandler.closeDBConnection();
dxClusterServer.stop();
rotatorClient.stopRotor();
rotatorClient.stop();
@@ -1132,7 +1171,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
// mine
private FilteredList<ChatMessage> lst_toOtherMessageList = new FilteredList<>(lst_globalChatMessageList);
private ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
// private ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
private ObservableList<String>
lstNotify_QSOSniffer_sniffedCallSignList;
/**
* we do some trick here with the chatmemberlist to not make it neccessary to change all boolean properties if the
* chatmember object to observables. We trigger the list for changes on an object which we change whenever a list
@@ -1864,6 +1905,9 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
chatPreferences = new ChatPreferences();
chatPreferences.readPreferencesFromXmlFile();
// this.statusListener = listener;
lstNotify_QSOSniffer_sniffedCallSignList =
chatPreferences
.getLstNotify_QSOSniffer_sniffedCallSignList();
String dnsFromPrefs = chatPreferences.getStn_on4kstServersDns();
if (dnsFromPrefs != null && !dnsFromPrefs.isEmpty()) {
@@ -2004,7 +2048,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
});
lstNotify_QSOSniffer_sniffedCallSignList.add("DF0GEB");
// lstNotify_QSOSniffer_sniffedCallSignList.add("DF0GEB");
lst_toMeMessageList.setPredicate(chatFilterPredicate); //sniffed callsign filter predicate is here!
@@ -2147,6 +2191,38 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
return dxClusterServer;
}
public synchronized void startDxClusterServerIfEnabled() {
if (!chatPreferences.isNotify_dxClusterServerEnabled()
|| dxClusterServer != null) {
return;
}
dxClusterServer = new DXClusterThreadPooledServer(
chatPreferences.getNotify_dxclusterServerPort(),
this,
this
);
Thread serverThread = new Thread(dxClusterServer);
serverThread.setName("DXCluster-thread-pooled-server");
serverThread.setDaemon(true);
serverThread.start();
}
public synchronized void stopDxClusterServer() {
DXClusterThreadPooledServer serverToStop = dxClusterServer;
dxClusterServer = null;
if (serverToStop != null) {
serverToStop.stop();
}
}
public synchronized void restartDxClusterServerIfEnabled() {
stopDxClusterServer();
startDxClusterServerIfEnabled();
}
// public void setChatMemberTable(Hashtable<String, ChatMember> chatMemberTable) {
// this.chatMemberTable = chatMemberTable;
// }
@@ -2230,7 +2306,11 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
messageProcessor.setName("messagebusManagementThread");
messageProcessor.start();
airScoutUDPReaderThread = new ReadUDPbyAirScoutMessageThread(chatPreferences.getAirScout_asCommunicationPort(), this, this.getChatPreferences().getAirScout_asServerNameString(), this.getChatPreferences().getAirScout_asServerNameString(), this); //working original
airScoutUDPReaderThread = new ReadUDPbyAirScoutMessageThread(
chatPreferences.getAirScout_asCommunicationPort(),
this,
this
);
airScoutUDPReaderThread.setName("airscoutudpreaderThread");
airScoutUDPReaderThread.start();
@@ -2249,8 +2329,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
/**
* DX cluster service running config
*/
dxClusterServer = new DXClusterThreadPooledServer(this.getChatPreferences().getNotify_dxclusterServerPort(), this, this);
new Thread(dxClusterServer).start();
startDxClusterServerIfEnabled();
this.setConnectedAndLoggedIn(true);
@@ -1,238 +1,363 @@
package kst4contest.controller;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ThreadStateMessage;
import java.io.*;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DXClusterThreadPooledServer implements Runnable{
public class DXClusterThreadPooledServer implements Runnable {
private static final Logger LOGGER = Logger.getLogger(DXClusterThreadPooledServer.class.getName());
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
private static final Logger LOGGER =
Logger.getLogger(DXClusterThreadPooledServer.class.getName());
private static final String THREAD_NICKNAME = "DXCluster-Server";
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
ChatController chatController = null;
protected int serverPort = 8080;
protected ServerSocket serverSocket = null;
protected boolean isStopped = false;
protected Thread runningThread= null;
protected ExecutorService threadPool =
private final List<Socket> clientSockets =
Collections.synchronizedList(new ArrayList<>());
private final ChatController chatController;
private final ThreadStatusCallback callBackToController;
private final int serverPort;
private final ExecutorService threadPool =
Executors.newFixedThreadPool(10);
Socket clientSocket;
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
private final ScheduledExecutorService keepAliveExecutor =
Executors.newSingleThreadScheduledExecutor();
private volatile boolean stopped;
private ServerSocket serverSocket;
public DXClusterThreadPooledServer(
int port,
ChatController chatController,
ThreadStatusCallback callback
) {
this.serverPort = port;
this.chatController = chatController;
this.callBackToController = callback;
}
public void run(){
@Override
public void run() {
Thread.currentThread().setName("DXCluster-thread-pooled-server");
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
try {
serverSocket = new ServerSocket(serverPort);
synchronized(this){
this.runningThread = Thread.currentThread();
runningThread.setName("DXCluster-thread-pooled-server");
}
openServerSocket();
while(! isStopped()){
clientSocket = null;
try {
clientSocket = this.serverSocket.accept();
synchronized(clientSockets) {
clientSockets.add(clientSocket); // add dx cluster client to the "clients list" for broadcasting
}
} catch (IOException e) {
if(isStopped()) {
System.out.println("Server Stopped.") ;
break;
}
throw new RuntimeException(
"Error accepting client connection", e);
if (stopped) {
return;
}
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController);
callBackToController.onThreadStatus(
THREAD_NICKNAME,
new ThreadStateMessage(
THREAD_NICKNAME,
true,
"Listening on TCP port " + serverPort,
false
)
);
this.threadPool.execute(worker);
keepAliveExecutor.scheduleAtFixedRate(
this::sendKeepAlive,
30,
30,
TimeUnit.SECONDS
);
}
this.threadPool.shutdown();
System.out.println("Server Stopped.") ;
}
while (!stopped) {
try {
Socket clientSocket = serverSocket.accept();
clientSockets.add(clientSocket);
private synchronized boolean isStopped() {
return this.isStopped;
}
public synchronized void stop(){
this.isStopped = true;
try {
this.serverSocket.close();
synchronized(clientSockets) {
for (Socket socket : clientSockets) {
socket.close(); // close all client connections
threadPool.execute(
new DXClusterServerWorkerRunnable(
clientSocket,
clientSockets
)
);
} catch (IOException exception) {
if (!stopped) {
LOGGER.log(
Level.SEVERE,
"Error accepting DX Cluster client connection",
exception
);
}
}
}
} catch (IOException e) {
throw new RuntimeException("DXCCSERVER Error closing server", e);
} catch (IOException exception) {
if (!stopped) {
LOGGER.log(
Level.SEVERE,
"Cannot open DX Cluster TCP port " + serverPort,
exception
);
callBackToController.onThreadStatus(
THREAD_NICKNAME,
new ThreadStateMessage(
THREAD_NICKNAME,
false,
"Cannot open TCP port "
+ serverPort
+ ": "
+ exception.getMessage(),
true
)
);
}
} finally {
closeServerSocket();
closeClientSockets();
keepAliveExecutor.shutdownNow();
threadPool.shutdownNow();
}
}
private void openServerSocket() {
try {
this.serverSocket = new ServerSocket(this.serverPort);
} catch (IOException e) {
throw new RuntimeException("DXCCSERVER Cannot open port ", e);
public synchronized void stop() {
stopped = true;
closeServerSocket();
closeClientSockets();
keepAliveExecutor.shutdownNow();
threadPool.shutdownNow();
}
public boolean hasConnectedClients() {
synchronized (clientSockets) {
removeClosedClients();
return !clientSockets.isEmpty();
}
}
/**
* Sends a DX cluster message to ALL connected log programs via telnet, returns true if sent
* Sends one DX Cluster spot to all currently connected clients.
*
* @param aChatMember
* @return boolean true if message had been sent
* @return true if the spot was delivered to at least one client
*/
public boolean broadcastSingleDXClusterEntryToLoggers(ChatMember aChatMember) {
synchronized(clientSockets) {
public boolean broadcastSingleDXClusterEntryToLoggers(
ChatMember chatMember
) {
final String clusterMessage;
System.out.println("DXClusterSrvr: broadcasting message to clients: " + clientSockets.size());
try {
String frequency = Utils4KST.normalizeFrequencyString(
chatMember.getFrequency().getValue(),
chatController
.getChatPreferences()
.getNotify_optionalFrequencyPrefix()
);
try {
clusterMessage =
"DX de "
+ chatController
.getChatPreferences()
.getNotify_DXCSrv_SpottersCallSign()
.getValue()
+ ": "
+ frequency
+ " "
+ chatMember.getCallSign().toUpperCase()
+ " "
+ chatMember.getQra().toUpperCase()
+ " "
+ new Utils4KST()
.time_generateCurrenthhmmZTimeStringForClusterMessage()
+ ((char) 7)
+ ((char) 7)
+ "\r\n";
} catch (Exception exception) {
LOGGER.log(
Level.SEVERE,
"Cannot build DX Cluster message",
exception
);
return false;
}
System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue());
System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ");
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "DXCThPooledServer: Error accessing value in chatmember object", e);
}
int deliveredClients = 0;
for (Socket socket : clientSockets) {
synchronized (clientSockets) {
Iterator<Socket> iterator = clientSockets.iterator();
while (iterator.hasNext()) {
Socket socket = iterator.next();
if (socket == null || socket.isClosed()) {
iterator.remove();
continue;
}
try {
OutputStream output = socket.getOutputStream();
output.write(
clusterMessage.getBytes(
StandardCharsets.US_ASCII
)
);
output.flush();
deliveredClients++;
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"DX Cluster client disconnected while sending a spot",
exception
);
OutputStream output = socket.getOutputStream();
String singleDXClusterMessage = "DX de ";
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
singleDXClusterMessage += this.chatController.getChatPreferences().getNotify_DXCSrv_SpottersCallSign().getValue() + ": ";
singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " "; //we need such an amount of spaces for n1mm to work, otherwise bullshit happens
singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
// singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
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) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] broadcasting DXC-message to clients went wrong", e);
return false;
closeSocket(socket);
iterator.remove();
}
}
}
return true; //if message had been sent, return true for "ok"
if (deliveredClients > 0) {
callBackToController.onThreadStatus(
THREAD_NICKNAME,
new ThreadStateMessage(
THREAD_NICKNAME,
true,
"Last spot sent to "
+ deliveredClients
+ " DX Cluster client(s):\n"
+ clusterMessage,
false
)
);
}
return deliveredClients > 0;
}
private void sendKeepAlive() {
synchronized (clientSockets) {
Iterator<Socket> iterator = clientSockets.iterator();
while (iterator.hasNext()) {
Socket socket = iterator.next();
if (socket == null || socket.isClosed()) {
iterator.remove();
continue;
}
try {
OutputStream output = socket.getOutputStream();
output.write(
"\r\n".getBytes(StandardCharsets.US_ASCII)
);
output.flush();
} catch (IOException exception) {
closeSocket(socket);
iterator.remove();
}
}
}
}
private void removeClosedClients() {
clientSockets.removeIf(
socket -> socket == null || socket.isClosed()
);
}
private synchronized void closeServerSocket() {
if (serverSocket == null || serverSocket.isClosed()) {
return;
}
try {
serverSocket.close();
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"Error closing DX Cluster server socket",
exception
);
}
}
private void closeClientSockets() {
synchronized (clientSockets) {
for (Socket socket : clientSockets) {
closeSocket(socket);
}
clientSockets.clear();
}
}
private static void closeSocket(Socket socket) {
if (socket == null || socket.isClosed()) {
return;
}
try {
socket.close();
} catch (IOException ignored) {
// The connection is already unusable.
}
}
}
class DXClusterServerWorkerRunnable implements Runnable{
class DXClusterServerWorkerRunnable implements Runnable {
private static final Logger LOGGER = Logger.getLogger(DXClusterServerWorkerRunnable.class.getName());
protected Socket clientSocket = null;
protected String serverText = null;
private ChatController client = null;
private List<Socket> dxClusterClientSocketsConnectedList;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
private static final Logger LOGGER =
Logger.getLogger(DXClusterServerWorkerRunnable.class.getName());
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) {
private final Socket clientSocket;
private final List<Socket> clientSockets;
DXClusterServerWorkerRunnable(
Socket clientSocket,
List<Socket> clientSockets
) {
this.clientSocket = clientSocket;
this.serverText = serverText;
this.client = chatController;
this.dxClusterClientSocketsConnectedList = clientSockets;
this.callBackToController = callback;
this.clientSockets = clientSockets;
}
@Override
public void run() {
try {
OutputStream output = clientSocket.getOutputStream();
dxClusterClientSocketsConnectedList.add(clientSocket);
output.write(
"login: ".getBytes(StandardCharsets.US_ASCII)
);
output.flush();
Timer dXCkeepAliveTimer = new Timer();
dXCkeepAliveTimer.schedule(new TimerTask() {
System.out.println(
"[DXClusterServer] New client connected: "
+ clientSocket.getInetAddress()
);
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"Cannot initialise DX Cluster client connection",
exception
);
@Override
public void run() {
synchronized (clientSockets) {
clientSockets.remove(clientSocket);
}
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());
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] keep-alive broadcast to client failed", e);
dXCkeepAliveTimer.purge();
try {
socket.close();
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error closing client socket", ex);
}
finally {
this.cancel();
}
dxClusterClientSocketsConnectedList.remove(socket); //if socket is closed by client, remove it from the broadcast list and close it
}
}
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
}, 30000, 30000);
output.write(("login: ").getBytes()); //say hello to the client, it will answer with a callsign
System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error in worker runnable", e);
} finally {
synchronized(dxClusterClientSocketsConnectedList) {
dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung
try {
clientSocket.close();
} catch (IOException ignored) {
// The connection is already unusable.
}
}
}
}
}
@@ -1,9 +1,13 @@
package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Comparator;
import java.util.List;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -21,188 +25,245 @@ import kst4contest.model.ThreadStateMessage;
* @author www.codejava.net
*/
public class ReadUDPbyAirScoutMessageThread extends Thread {
private BufferedReader reader;
private Socket socket;
private ChatController client;
private int localPort;
private String ASIdentificator, ChatClientIdentificator;
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, ThreadStatusCallback callback) {
private final ChatController client;
private final int localPort;
private final ThreadStatusCallback callBackToController;
this.callBackToController = callback;
private final String threadNickName = "AirScout msg";
private DatagramSocket socket;
public ReadUDPbyAirScoutMessageThread(
int localPort,
ChatController client,
ThreadStatusCallback callback
) {
this.localPort = localPort;
this.client = client;
this.ASIdentificator = ASIdentificator;
this.ChatClientIdentificator = ChatClientIdentificator;
this.callBackToController = callback;
}
@Override
public void interrupt() {
System.out.println("ReadUDP");
super.interrupt();
try {
if (this.socket != null) {
this.socket.close();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
if (socket != null && !socket.isClosed()) {
socket.close();
}
}
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) {
if (callBackToController != null) {
//update the visual control of running thread
callBackToController.onThreadStatus("AirScout", threadStateMessage);
}
}
/**
* Checks whether an AirScout response is addressed to the currently
* configured server and client identifiers.
*
* Outgoing message:
* ASSETPATH: "client" "server" ...
*
* Corresponding response:
* ASNEAREST: "server" "client" ...
*
* The comparison is deliberately case-sensitive. In a setup with several
* clients, KST-A and kst-a must not silently become the same destination.
*
* @param message received UDP message
* @return true if the response belongs to this KST4Contest instance
*/
private boolean isMessageForConfiguredClient(String message) {
if (message == null || !message.startsWith("ASNEAREST:")) {
return false;
}
String[] quotedParts = message.split("\"");
if (quotedParts.length < 4) {
return false;
}
String receivedServerIdentifier = quotedParts[1].trim();
String receivedClientIdentifier = quotedParts[3].trim();
String configuredServerIdentifier =
client.getChatPreferences()
.getAirScout_asServerNameString();
String configuredClientIdentifier =
client.getChatPreferences()
.getAirScout_asClientNameString();
if (configuredServerIdentifier == null
|| configuredClientIdentifier == null) {
return false;
}
return configuredServerIdentifier.equals(
receivedServerIdentifier
) && configuredClientIdentifier.equals(
receivedClientIdentifier
);
}
@Override
public void run() {
Thread.currentThread().setName("ReadUDPByAirScoutThread");
DatagramSocket socket = null;
boolean running;
byte[] buf = new byte[1777];
DatagramPacket packet;
// DatagramPacket packet = new DatagramPacket(buf, buf.length); //changed due to save memory
packet = new DatagramPacket(buf, buf.length);
Thread.currentThread().setName(
"ReadUDPByAirScoutThread"
);
try {
socket = new DatagramSocket(null);
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(localPort));
socket.receive(packet);
socket.setSoTimeout(3000);
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
}
while (true) {
// packet = new DatagramPacket(buf, buf.length);
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH);
try {
if (this.client.isDisconnectionPerformedByUser()) {
break;//TODO: what if it´s not the finally closage but a band channel change?
while (!Thread.currentThread().isInterrupted()) {
if (client.isDisconnectionPerformedByUser()) {
break;
}
socket.receive(packet);
byte[] buffer = new byte[1777];
DatagramPacket packet = new DatagramPacket(
buffer,
buffer.length
);
} catch (SocketTimeoutException e2) {
// this will catch the repeating Sockettimeoutexception...nothing to do
// e2.printStackTrace();
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
InetAddress address = packet.getAddress();
int port = packet.getPort();
// packet = new DatagramPacket(buf, buf.length, address, port);
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
received = received.trim();
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
System.out.println("ReadUdpByASMsgTh, Info: got poison, now dieing....");
try {
terminateConnection();
} catch (Exception e) {
System.out.println("ASUDPRDR: catched error " + e.getMessage());
}
break;
}
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
// do nothing, that is your own message
} else if (received.contains("ASNEAREST:")) { //answer by airscout
// processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
AirPlaneReflectionInfo apReflectInfoForChatMember;
apReflectInfoForChatMember = processASUDPMessage(received);
if (!this.client.getLst_chatMemberList().isEmpty()) {
try {
// 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");
/**
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10"
* java.util.ConcurrentModificationException at
* java.base/java.util.AbstractList$Itr.checkForComodification(AbstractList.java:399)
* at java.base/java.util.AbstractList$Itr.next(AbstractList.java:368) at
* kst4contest.controller.ChatController.checkListForChatMemberIndexByCallSign(ChatController.java:173)
* at
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
*
*/
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
// }
} catch (Exception e) {
System.out.println("ReadUdpByAsMsgTh, Warning:"
+ apReflectInfoForChatMember.getReceiver().getCallSign()
+ " is not in the Chatmemberlist or the Chatmemberlist is modified by another 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);
socket.receive(packet);
} catch (SocketTimeoutException timeoutException) {
/*
* AirScout may remain silent for some time. Continue with a
* new packet instead of processing the previous packet again.
*/
continue;
}
}
// packet = null; //reset packet
buf = new byte[1777]; // reset buffer for future smaller packets
String received = new String(
packet.getData(),
packet.getOffset(),
packet.getLength()
).trim();
if (received.contains(
ApplicationConstants.DISCONNECT_RDR_POISONPILL
)) {
System.out.println(
"[AirScout UDP, info]: Received shutdown packet."
);
break;
}
/*
* The socket remains bound so that AirScout can be enabled
* without reconnecting. Disabled means that received data is
* discarded and no station state is changed.
*/
if (!client.getChatPreferences()
.isAirScout_asUDPListenerEnabled()) {
continue;
}
if (!isMessageForConfiguredClient(received)) {
continue;
}
processAirScoutResponse(received);
}
} catch (SocketException exception) {
if (!Thread.currentThread().isInterrupted()) {
System.out.println(
"[AirScout UDP, error]: Could not use UDP port "
+ localPort
+ ": "
+ exception.getMessage()
);
}
} catch (IOException exception) {
if (!Thread.currentThread().isInterrupted()) {
System.out.println(
"[AirScout UDP, error]: Communication failed: "
+ exception.getMessage()
);
}
} finally {
if (socket != null && !socket.isClosed()) {
socket.close();
}
socket = null;
}
}
/**
* Parses an AirScout response and applies it to every active category
* instance of the reported station.
*
* @param received received ASNEAREST message
*/
private void processAirScoutResponse(String received) {
try {
AirPlaneReflectionInfo reflectionInfo =
processASUDPMessage(received);
if (reflectionInfo == null
|| reflectionInfo.getReceiver() == null) {
return;
}
String receiverCallSign =
reflectionInfo.getReceiver().getCallSignRaw();
if (receiverCallSign == null
|| receiverCallSign.isBlank()) {
receiverCallSign =
reflectionInfo.getReceiver().getCallSign();
}
if (receiverCallSign == null
|| receiverCallSign.isBlank()) {
return;
}
List<ChatMember> matchingMembers =
client.findActiveChatMembersByRawCall(
receiverCallSign
);
for (ChatMember matchingMember : matchingMembers) {
matchingMember.setAirPlaneReflectInfo(
reflectionInfo
);
}
if (!matchingMembers.isEmpty()) {
client.getScoreService().requestRecompute(
"airscout-update"
);
}
if (callBackToController != null) {
ThreadStateMessage threadStateMessage =
new ThreadStateMessage(
threadNickName,
true,
"Received AirScout response\n"
+ reflectionInfo,
false
);
callBackToController.onThreadStatus(
threadNickName,
threadStateMessage
);
}
} catch (RuntimeException exception) {
System.out.println(
"[AirScout UDP, warning]: Could not process response: "
+ exception.getMessage()
);
}
}
public AirPlaneReflectionInfo processASUDPMessage(String udpStringToProcess) {
// System.out.println("RDUDPAS RECV: " + udpStringToProcess);
// TODO: filter messages which are directed to another client
/*
* Example mesage: ASNEAREST: "AS" "KST"
@@ -304,11 +365,8 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
}
public boolean terminateConnection() {
try {
this.socket.close();
} catch (Exception e) {
System.out.println("udpbyas: catched " + e.getMessage());
if (socket != null && !socket.isClosed()) {
socket.close();
}
return true;
@@ -50,7 +50,7 @@ public class ChatPreferences {
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
*/
// private static final int CONFIG_VERSION = 2;
public static final int CONFIG_VERSION = 3;
public static final int CONFIG_VERSION = 4;
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
private static final String TAG_CONFIG_VERSION = "configVersion";
@@ -240,9 +240,11 @@ public class ChatPreferences {
/**
* AirScout prefs
*/
boolean AirScout_asUDPListenerEnabled;
String AirScout_asServerNameString, AirScout_asClientNameString, AirScout_asBandString;
int AirScout_asCommunicationPort;
boolean AirScout_asUDPListenerEnabled = true;
String AirScout_asServerNameString = "AS";
String AirScout_asClientNameString = "KST";
String AirScout_asBandString = "1440000";
int AirScout_asCommunicationPort = 9872;
/**
* Notification prefs
@@ -265,6 +267,9 @@ public class ChatPreferences {
boolean notify_DXClusterServerTriggerBearing;
boolean notify_DXClusterServerTriggerOnQRGDetect;
ObservableList<String>
lstNotify_QSOSniffer_sniffedCallSignList =
FXCollections.observableArrayList();
// ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
ObservableList<String> lstNotify_QSOSniffer_sniffedWordsList = FXCollections.observableArrayList();
ObservableList<String> lstNotify_QSOSniffer_sniffedPrefixLocList = FXCollections.observableArrayList();
@@ -453,6 +458,11 @@ public class ChatPreferences {
this.stn_on4kstServersDns = stn_on4kstServersDns;
}
public ObservableList<String>
getLstNotify_QSOSniffer_sniffedCallSignList() {
return lstNotify_QSOSniffer_sniffedCallSignList;
}
public ObservableList<String> getLstNotify_QSOSniffer_sniffedWordsList() {
return lstNotify_QSOSniffer_sniffedWordsList;
}
@@ -799,8 +809,16 @@ public class ChatPreferences {
return notify_dxclusterServerPort;
}
public void setNotify_dxclusterServerPort(int notify_dxclusterServerPort) {
this.notify_dxclusterServerPort = notify_dxclusterServerPort;
public void setNotify_dxclusterServerPort(
int notify_dxclusterServerPort
) {
if (notify_dxclusterServerPort < 1
|| notify_dxclusterServerPort > 65535) {
this.notify_dxclusterServerPort = 8000;
} else {
this.notify_dxclusterServerPort =
notify_dxclusterServerPort;
}
}
public double[] getGUIscn_ChatwindowMainSceneSizeHW() {
@@ -921,35 +939,100 @@ public class ChatPreferences {
return stn_loginCallSignRaw;
}
/**
* Normalizes an AirScout routing identifier.
*
* AirScout encloses the identifiers in quotation marks. Empty identifiers,
* quotation marks and line breaks would therefore produce an invalid protocol
* message and are replaced with the supplied default value.
*
* @param identifier configured identifier
* @param defaultIdentifier fallback value
* @return normalized identifier
*/
private String normalizeAirScoutIdentifier(
String identifier,
String defaultIdentifier
) {
if (identifier == null) {
return defaultIdentifier;
}
String normalizedIdentifier = identifier.trim();
if (normalizedIdentifier.isEmpty()
|| normalizedIdentifier.contains("\"")
|| normalizedIdentifier.contains("\r")
|| normalizedIdentifier.contains("\n")) {
return defaultIdentifier;
}
return normalizedIdentifier;
}
public String getAirScout_asBandString() {
return AirScout_asBandString;
}
public void setAirScout_asBandString(String airScout_asBandString) {
AirScout_asBandString = airScout_asBandString;
if (airScout_asBandString == null) {
AirScout_asBandString = "1440000";
return;
}
try {
long parsedBandValue = Long.parseLong(
airScout_asBandString.trim()
);
AirScout_asBandString = parsedBandValue > 0
? Long.toString(parsedBandValue)
: "1440000";
} catch (NumberFormatException exception) {
AirScout_asBandString = "1440000";
}
}
public String getAirScout_asServerNameString() {
return AirScout_asServerNameString;
}
public void setAirScout_asServerNameString(String airScout_asServerNameString) {
AirScout_asServerNameString = airScout_asServerNameString;
public void setAirScout_asServerNameString(
String airScout_asServerNameString
) {
AirScout_asServerNameString = normalizeAirScoutIdentifier(
airScout_asServerNameString,
"AS"
);
}
public String getAirScout_asClientNameString() {
return AirScout_asClientNameString;
}
public void setAirScout_asClientNameString(String airScout_asClientNameString) {
AirScout_asClientNameString = airScout_asClientNameString;
public void setAirScout_asClientNameString(
String airScout_asClientNameString
) {
AirScout_asClientNameString = normalizeAirScoutIdentifier(
airScout_asClientNameString,
"KST"
);
}
public int getAirScout_asCommunicationPort() {
return AirScout_asCommunicationPort;
}
public void setAirScout_asCommunicationPort(int airScout_asCommunicationPort) {
public void setAirScout_asCommunicationPort(
int airScout_asCommunicationPort
) {
if (airScout_asCommunicationPort < 1
|| airScout_asCommunicationPort > 65535) {
AirScout_asCommunicationPort = 9872;
return;
}
AirScout_asCommunicationPort = airScout_asCommunicationPort;
}
@@ -957,10 +1040,13 @@ public class ChatPreferences {
return AirScout_asUDPListenerEnabled;
}
public void setAirScout_asUDPListenerEnabled(boolean airScout_asUDPListenerEnabled) {
public void setAirScout_asUDPListenerEnabled(
boolean airScout_asUDPListenerEnabled
) {
AirScout_asUDPListenerEnabled = airScout_asUDPListenerEnabled;
}
public String getChatState() {
return chatState;
}
@@ -1667,6 +1753,18 @@ public class ChatPreferences {
snifferWords.appendChild(temp);
}
Element snifferCallSigns =
doc.createElement("snifferCallSigns");
rootElement.appendChild(snifferCallSigns);
for (String callSign
: lstNotify_QSOSniffer_sniffedCallSignList) {
Element temp = doc.createElement("callSign");
temp.setTextContent(callSign);
snifferCallSigns.appendChild(temp);
}
Element snifferPrefixes = doc.createElement("snifferPrefixes");
rootElement.appendChild(snifferPrefixes);
@@ -2265,7 +2363,13 @@ public class ChatPreferences {
notify_dxClusterServerEnabled = getBoolean(notificationsEl, notify_dxClusterServerEnabled, "notify_dxClusterServerEnabled");
notify_DXClusterServerTriggerBearing = getBoolean(notificationsEl, notify_DXClusterServerTriggerBearing, "notify_DXClusterServerTriggerBearing");
notify_DXClusterServerTriggerOnQRGDetect = getBoolean(notificationsEl, notify_DXClusterServerTriggerOnQRGDetect, "notify_DXClusterServerTriggerOnQRGDetect");
notify_dxclusterServerPort = getInt(notificationsEl, notify_dxclusterServerPort, "notify_dxclusterServerPort");
setNotify_dxclusterServerPort(
getInt(
notificationsEl,
notify_dxclusterServerPort,
"notify_dxclusterServerPort"
)
);
String spotter = getText(notificationsEl, null, "notify_DXCSrv_SpottersCallSign");
if (spotter != null) {
@@ -2316,15 +2420,58 @@ public class ChatPreferences {
Element airScoutEl = getFirstElement(doc, "AirScoutQuerier");
if (airScoutEl != null) {
AirScout_asUDPListenerEnabled = getBoolean(airScoutEl, AirScout_asUDPListenerEnabled, "asQry_airScoutCommunicationEnabled");
AirScout_asServerNameString = getText(airScoutEl, AirScout_asServerNameString, "asQry_airScoutServerName");
AirScout_asClientNameString = getText(airScoutEl, AirScout_asClientNameString, "asQry_airScoutClientName");
AirScout_asCommunicationPort = getInt(airScoutEl, AirScout_asCommunicationPort, "asQry_airScoutUDPPort");
AirScout_asBandString = getText(airScoutEl, AirScout_asBandString, "asQry_airScoutBandValue");
setAirScout_asUDPListenerEnabled(
getBoolean(
airScoutEl,
AirScout_asUDPListenerEnabled,
"asQry_airScoutCommunicationEnabled"
)
);
setAirScout_asServerNameString(
getText(
airScoutEl,
AirScout_asServerNameString,
"asQry_airScoutServerName"
)
);
setAirScout_asClientNameString(
getText(
airScoutEl,
AirScout_asClientNameString,
"asQry_airScoutClientName"
)
);
setAirScout_asCommunicationPort(
getInt(
airScoutEl,
AirScout_asCommunicationPort,
"asQry_airScoutUDPPort"
)
);
setAirScout_asBandString(
getText(
airScoutEl,
AirScout_asBandString,
"asQry_airScoutBandValue"
)
);
System.out.println(
"[ChatPreferences, info]: AirScout querier enabled=" + AirScout_asUDPListenerEnabled
+ ", band=" + AirScout_asBandString);
"[ChatPreferences, info]: AirScout integration enabled="
+ AirScout_asUDPListenerEnabled
+ ", server identifier="
+ AirScout_asServerNameString
+ ", client identifier="
+ AirScout_asClientNameString
+ ", port="
+ AirScout_asCommunicationPort
+ ", band="
+ AirScout_asBandString
);
}
/**
@@ -2374,6 +2521,45 @@ public class ChatPreferences {
/**
* Case QSO-sniffer lists (added later; older configs won't have them)
*/
list = doc.getElementsByTagName("snifferCallSigns");
if (list != null && list.getLength() != 0) {
lstNotify_QSOSniffer_sniffedCallSignList.clear();
for (int temp = 0; temp < list.getLength(); temp++) {
Node node = list.item(temp);
if (node.getNodeType() == Node.ELEMENT_NODE) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType()
== Node.ELEMENT_NODE) {
String callSign =
child.getTextContent();
if (callSign != null
&& !callSign.isBlank()) {
String normalizedCallSign =
callSign
.trim()
.toUpperCase();
if (!lstNotify_QSOSniffer_sniffedCallSignList
.contains(normalizedCallSign)) {
lstNotify_QSOSniffer_sniffedCallSignList
.add(normalizedCallSign);
}
}
}
}
}
}
}
list = doc.getElementsByTagName("snifferWords");
if (list != null && list.getLength() != 0) {
// reset to avoid duplicates when reloading
File diff suppressed because it is too large Load Diff