mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 05:42:58 +02:00
Integrate latest local development state and clean repository artifacts
This commit is contained in:
1 parent
ee5ee535bb
commit
7f9b1bfc4d
40 files changed
+8173
-2237
No files matched your search
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user