mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-08-24 02:57:43 +02:00
Compare commits
2
Commits
00496b56e9
...
333063f350
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
333063f350 | ||
|
|
ee974bbd73 |
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -64,9 +64,18 @@ Die Filter-Leiste (ab v1.21 als Flowpane für kleine Bildschirme):
|
||||
|
||||
## Stationsinfo-Panel (Further Info)
|
||||
|
||||
Rechts unten: Zeigt alle Nachrichten einer ausgewählten Station (CQ-Nachrichten und PMs in einem Panel). Ein Nachrichtenfilter lässt sich über den Standard-Filter in den Preferences vorbelegen.
|
||||
Rechts unten werden die Nachrichten der ausgewählten Station zusammengeführt. Dazu gehören öffentliche Nachrichten, Privatnachrichten an die eigene Station und – soweit im Chat sichtbar – Privatnachrichten an andere Stationen.
|
||||
|
||||
Hier können auch **Sked-Erinnerungen** aktiviert werden.
|
||||
Der im Panel gewählte Filter bestimmt, welche dieser Nachrichten angezeigt werden. Unter **Settings → GUI** lässt sich festlegen, welcher Filter beim Öffnen einer Stationsinformation vorausgewählt ist:
|
||||
|
||||
- alle Nachrichten,
|
||||
- Privatnachrichten an die eigene Station,
|
||||
- Privatnachrichten an andere Stationen oder
|
||||
- öffentliche Nachrichten.
|
||||
|
||||
Die Einstellung verändert nur die Darstellung im Stationsinfo-Panel. Nachrichten werden dadurch weder verworfen noch aus den übrigen Nachrichtentabellen entfernt. Der Filter kann im Panel jederzeit für die aktuell betrachtete Station gewechselt werden.
|
||||
|
||||
Hier können auch **Sked-Erinnerungen / Wecker** für beide Skkedpartner aktiviert werden.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -115,6 +115,22 @@ KST4Contest erkennt solche Nachrichten, die das eigene Rufzeichen enthalten, und
|
||||
|
||||
---
|
||||
|
||||
## Automatische Antworten auf Privatnachrichten (ab v1.25)
|
||||
|
||||
Nicht jede im ON4KST-Chat eingeloggte Station nimmt am gerade laufenden Contest teil. Trotzdem werden Sked-Anfragen während größerer Contests teilweise unkoordiniert und in großer Zahl an erreichbare Rufzeichen verteilt. Ohne automatische Antwort müssten diese Stationen immer wieder von Hand erklären, dass sie nicht mitfunken oder keine Skeds fahren.
|
||||
|
||||
KST4Contest kann darauf mit einem vorher festgelegten Text reagieren. Die eingehende Privatnachricht bleibt dabei sichtbar; sie wird weder blockiert noch verworfen. Davon getrennt lässt sich eine QRG-Antwort aktivieren, die auf typische Fragen wie `qrg?`, `freq?` oder `pse qrg` reagiert.
|
||||
|
||||
Bei zwei gleichzeitig geöffneten Chat-Kategorien bleibt der Zusammenhang erhalten: Die Antwort wird in der Kategorie der eingegangenen Nachricht gesendet. Eine QRG-Anfrage erhält außerdem nur die QRG dieser Kategorie und nicht eine Liste aller konfigurierten Frequenzen.
|
||||
|
||||
Automatische Antworten benötigen Grenzen. KST4Contest versieht sie daher mit `[KST4C Automsg]`, ignoriert entsprechend gekennzeichnete Nachrichten bei der allgemeinen und QRG-bezogenen Antwort und begrenzt weitere Antworten an dieselbe Station in derselben Kategorie auf eine Nachricht innerhalb von zwei Minuten. Der Schutz gilt gemeinsam für beide Antwortarten.
|
||||
|
||||
Im Klartext: Die Funktion verhindert keine Massenanfragen. Sie verhindert aber, dass der Empfänger jede davon einzeln mit derselben Absage beantworten muss. Sie soll keine Unterhaltung simulieren und erst recht keine endlose Diskussion mit einem zweiten automatischen Client beginnen.
|
||||
|
||||
Konfiguration, erkannte QRG-Anfragen und genaue Kategorienzuordnung: [Konfiguration – Messagehandling Settings](de-Konfiguration#messagehandling-settings-ab-v125).
|
||||
|
||||
---
|
||||
|
||||
## Multi-Channel-Login (ab v1.26)
|
||||
|
||||
Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Beide Chats werden parallel überwacht.
|
||||
@@ -228,8 +244,9 @@ So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und ü
|
||||
|
||||
## Intervall-Beacon
|
||||
|
||||
Automatische CQ-Meldungen im öffentlichen Kanal in konfigurierbarem Intervall. Empfohlene Verwendung mit der Variable `MYQRG` für aktuelle Frequenzangabe. Details: [Konfiguration – Beacon Settings](Konfiguration#beacon-settings-automatischer-beacon).
|
||||
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Globale Variablen wie `MYQRG`, `SECONDQRG` oder `MYLOCATOR` werden unmittelbar vor jeder Aussendung aktualisiert.
|
||||
|
||||
Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim Absuchen oder häufigen Wechseln der QRG sollte er ausgeschaltet werden, damit keine inzwischen falsche Frequenz verbreitet wird. Details: [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
|
||||
---
|
||||
|
||||
## Simplelogfile
|
||||
|
||||
@@ -183,19 +183,111 @@ Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als D
|
||||
|
||||
## Beacon Settings (Automatischer Beacon)
|
||||
|
||||
Konfiguration eines automatischen Intervall-Beacons im öffentlichen Chat-Kanal. Empfohlen: Variable `MYQRG` im Text verwenden, damit die aktuelle Frequenz immer aktuell ist. Intervall und Text sind frei konfigurierbar.
|
||||

|
||||
|
||||
> **Tipp**: Beacon beim CQ-Rufen aktivieren und im Einstellungsfenster schnell deaktivieren, wenn kein CQ gerufen wird.
|
||||
Ein Beacon sendet in regelmäßigen Abständen eine öffentliche CQ-Nachricht. Er ist für Betriebssituationen gedacht, in denen die eigene Station über längere Zeit auf einer festen Frequenz ruft. Andere Stationen erhalten dadurch eine aktuelle QRG-Information, ohne dass der Operator denselben Text wiederholt von Hand in den Chat schreiben muss.
|
||||
|
||||
KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivierung und Nachrichtentext werden trotzdem getrennt konfiguriert:
|
||||
|
||||
- **Enable CQ beacon** aktiviert den Beacon der betreffenden Kategorie.
|
||||
- **Beacon message** enthält den öffentlichen Nachrichtentext dieser Kategorie.
|
||||
- **Shared beacon interval** legt das gemeinsame Intervall für beide Kategorien fest.
|
||||
|
||||
Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in ihren jeweiligen Kategorien gesendet. Der zweite Beacon wird nur berücksichtigt, wenn auch der zweite Chat aktiviert und verbunden ist.
|
||||
|
||||
### Intervall und Timer-Verhalten
|
||||
|
||||
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert ist eine Minute.
|
||||
|
||||
Nach dem Aufbau der Chat-Verbindung prüft KST4Contest die Beacons erstmals nach ungefähr zehn Sekunden. Anschließend gilt das eingestellte Intervall. Wird der Wert während einer laufenden Verbindung geändert, beginnt der Countdown mit dem neuen Intervall erneut. Die Änderung selbst löst keine sofortige Nachricht aus.
|
||||
|
||||
### Nachrichtentext und Variablen
|
||||
|
||||
Ein Beacon darf nach der Variablenauflösung höchstens 120 Zeichen enthalten. KST4Contest prüft deshalb nicht nur das eingetragene Template, sondern den tatsächlich zu sendenden Text.
|
||||
|
||||
Im Beacon können alle [globalen Variablen](de-Makros-und-Variablen#variablen-im-beacon) verwendet werden, beispielsweise:
|
||||
|
||||
```text
|
||||
calling cq at MYQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Die Variablen werden bei jedem Timer-Lauf neu aufgelöst. Ändert die Logsoftware zwischenzeitlich die in `MYQRG` gespeicherte Frequenz, verwendet bereits der nächste Beacon den neuen Wert.
|
||||
|
||||
Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` benötigen dagegen eine ausgewählte Gegenstation. Da ein öffentlicher Beacon keine Gegenstation adressiert, werden diese Variablen im Beacon nicht aufgelöst.
|
||||
|
||||
### Wann sollte der Beacon ausgeschaltet werden?
|
||||
|
||||
Der Beacon ist nur dann hilfreich, wenn seine QRG-Angabe zum tatsächlichen Betrieb passt. Bleibt er beim Absuchen oder häufigen Wechseln von Frequenzen aktiviert, können andere Stationen auf einer inzwischen falschen Frequenz nach der eigenen Station suchen.
|
||||
|
||||
Im Klartext: Solange auf einer festen QRG CQ gerufen wird, spart der Beacon Arbeit. Beim „Schleichen“ über das Band sollte er ausgeschaltet werden.
|
||||
|
||||
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung, Texte und Intervall auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
|
||||
---
|
||||
|
||||
## Messagehandling Settings (ab v1.25)
|
||||
|
||||
Neuer Einstellungsbereich mit folgenden Optionen:
|
||||

|
||||
|
||||
- **Auto-Antwort auf alle eingehenden Nachrichten**: Automatische Antwort auf Privatnachrichten konfigurierbar.
|
||||
- **Auto-Antwort mit eigener CQ-QRG**: Wenn jemand nach der eigenen QRG fragt, antwortet KST4Contest automatisch mit dem Inhalt der `MYQRG`-Variable.
|
||||
- **Standard-Filter für das Userinfo-Fenster**: Voreingestellter Nachrichtenfilter für das Stationsinfo-Fenster konfigurierbar *(für Gianluca :-) )*.
|
||||
Die wichtigste Anwendung der allgemeinen automatischen Antwort betrifft Stationen, die zwar im ON4KST-Chat eingeloggt sind, den laufenden Contest aber nicht mitfunken. Gerade während größerer Contests werden Sked-Anfragen teilweise unkoordiniert und in großer Zahl an eingeloggte Stationen verteilt, ohne vorher zu prüfen, ob sie überhaupt teilnehmen. Die Empfänger müssten sonst immer wieder dieselbe Absage schreiben.
|
||||
|
||||
KST4Contest kann darauf mit einem vorher festgelegten Text reagieren. Davon getrennt steht eine gezielte QRG-Auskunft zur Verfügung. Beide Funktionen können unabhängig voneinander aktiviert werden.
|
||||
|
||||
### Allgemeine automatische Antwort
|
||||
|
||||
**Enable automatic reply to all private messages** beantwortet eingehende Privatnachrichten mit dem Text im Feld rechts daneben. Es gibt einen gemeinsamen Text für beide Chat-Kategorien. Die beim Eingeben verwendete Groß- und Kleinschreibung bleibt erhalten.
|
||||
|
||||
Eine zweckmäßige Nachricht ist beispielsweise:
|
||||
|
||||
```text
|
||||
Sri, I am not taking part in this contest. No skeds.
|
||||
```
|
||||
|
||||
Die eingegangene Privatnachricht bleibt sichtbar. Die Funktion blockiert oder verwirft keine Anfrage, sondern erspart lediglich die wiederholte manuelle Antwort.
|
||||
|
||||
Die Antwort wird in derselben Chat-Kategorie gesendet, in der die Privatnachricht eingegangen ist. Das ist bei einem parallelen Login in zwei Kategorien entscheidend: Eine Nachricht aus dem Microwave-Chat darf nicht versehentlich im VHF/UHF-Chat beantwortet werden.
|
||||
|
||||
### Automatische QRG-Antwort
|
||||
|
||||
**Enable automatic QRG replies** reagiert auf typische QRG-Anfragen. Die Erkennung unterscheidet nicht zwischen Groß- und Kleinschreibung und sucht nach folgenden Textbestandteilen:
|
||||
|
||||
```text
|
||||
ur qrg?
|
||||
your qrg?
|
||||
qrg?
|
||||
freq?
|
||||
pse qrg
|
||||
```
|
||||
|
||||
Die Antwort enthält nur die QRG der Kategorie, in der die Anfrage eingegangen ist:
|
||||
|
||||
| Eingegangene Privatnachricht | Verwendete QRG |
|
||||
|---|---|
|
||||
| Hauptkategorie | aktuelle QRG der Hauptkategorie |
|
||||
| zweite Chat-Kategorie | aktuelle QRG der zweiten Kategorie |
|
||||
|
||||
Die Werte stammen aus denselben QRG-Feldern, die auch von `MYQRG` und `SECONDQRG` verwendet werden. Die Haupt-QRG kann manuell eingetragen oder durch die [TRX-Synchronisation](#trx-sync-einstellungen) aktualisiert werden. Für die zweite Kategorie wird der dort konfigurierte beziehungsweise manuell eingetragene Wert verwendet.
|
||||
|
||||
Sind die allgemeine und die QRG-bezogene Antwort gleichzeitig aktiviert, hat die QRG-Antwort Vorrang. Eine erkannte QRG-Anfrage erzeugt daher nicht zusätzlich den allgemeinen Antworttext.
|
||||
|
||||
### Schutz vor wiederholten Antworten
|
||||
|
||||
Jede automatisch erzeugte Nachricht trägt das feste Präfix:
|
||||
|
||||
```text
|
||||
[KST4C Automsg]
|
||||
```
|
||||
|
||||
Die allgemeine und die QRG-bezogene Antwort reagieren nicht auf Nachrichten, die dieses Präfix bereits enthalten. Dadurch beantworten sich zwei entsprechend arbeitende Clients nicht gegenseitig in einer Schleife.
|
||||
|
||||
Zusätzlich gilt eine gemeinsame Sperrzeit von zwei Minuten für beide Antwortarten. Die Sperre wird getrennt je Rufzeichen und Chat-Kategorie geführt. Hat eine Station gerade in der Hauptkategorie eine automatische Antwort erhalten, kann sie deshalb weiterhin eine Antwort in der zweiten Kategorie erhalten. Weitere Nachrichten derselben Station in derselben Kategorie lösen während der folgenden zwei Minuten dagegen keine neue automatische Antwort aus.
|
||||
|
||||
Die Sperrzeit beginnt nur, wenn KST4Contest tatsächlich eine Antwort sendet.
|
||||
|
||||
> **Hinweis**: Der Antworttext sollte den tatsächlichen Status eindeutig benennen. Wer den Contest nur beobachtet und keine Skeds fahren möchte, sollte genau das mitteilen. Eine vage Nachricht erzeugt im Zweifel nur die nächste Rückfrage – und damit exakt die Arbeit, welche die Funktion vermeiden soll.
|
||||
|
||||
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung und Text nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
|
||||
|
||||
Weitere Hintergründe: [Automatische Antworten auf Privatnachrichten](de-Funktionen#automatische-antworten-auf-privatnachrichten-ab-v125).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -138,18 +138,34 @@ Wird durch die aktuelle Antennenrichtung in Worten ersetzt (z. B. `north`, `nort
|
||||
|
||||
---
|
||||
|
||||
## Variablen im Beacon
|
||||
## Variablen im Beacon
|
||||
|
||||
Alle Variablen können auch im **automatischen Beacon** (Intervall-Nachrichten) verwendet werden. Empfohlene Beacon-Konfiguration:
|
||||
Ein öffentlicher Beacon besitzt keine ausgewählte Gegenstation. Deshalb können hier ausschließlich Variablen verwendet werden, die nur von der eigenen Station und ihrer aktuellen Konfiguration abhängen:
|
||||
|
||||
| Variable | Wert im Beacon |
|
||||
|---|---|
|
||||
| `MYQRG` | aktuelle QRG der ersten Chat-Kategorie |
|
||||
| `MYQRGSHORT` | auf sieben Zeichen gekürzte QRG der ersten Kategorie |
|
||||
| `SECONDQRG` | aktuelle QRG der zweiten Chat-Kategorie |
|
||||
| `MYLOCATOR` | eigener vollständiger Locator |
|
||||
| `MYLOCATORSHORT` | eigener vierstelliger Locator |
|
||||
| `MYCALL` | eigenes Rufzeichen |
|
||||
| `MYQTF` | aktuelle Antennenrichtung |
|
||||
|
||||
`QRZNAME`, `FIRSTAP` und `SECONDAP` benötigen eine ausgewählte Station. In einem öffentlichen Beacon werden sie daher nicht aufgelöst.
|
||||
|
||||
Eine zweckmäßige Konfiguration ist beispielsweise:
|
||||
|
||||
```
|
||||
calling cq at MYQRG, loc MYLOCATOR, GL all!
|
||||
```
|
||||
|
||||
Da KST4Contest QRG-Daten automatisch aus Chat-Nachrichten ausliest: Wenn andere Stationen ebenfalls KST4Contest nutzen, sehen sie die eigene QRG sofort in der QRG-Spalte der Benutzerliste.
|
||||
Die globalen Variablen werden bei jedem Timer-Lauf neu ausgewertet. Dadurch kann eine vom Logprogramm aktualisierte QRG bereits in der nächsten Beacon-Nachricht erscheinen.
|
||||
|
||||
Der vollständig aufgelöste Nachrichtentext darf höchstens 120 Zeichen enthalten. Weitere Angaben zum gemeinsamen Intervall und zum Verhalten beider Chat-Kategorien stehen unter [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
|
||||
|
||||
---
|
||||
|
||||
## Beispiel-Workflow mit Makros im Contest
|
||||
|
||||
1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt.
|
||||
|
||||
@@ -150,20 +150,32 @@
|
||||
<version>${jetbrains.annotations.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
<dependency>
|
||||
<groupId>xml-apis</groupId>
|
||||
<artifactId>xml-apis</artifactId>
|
||||
<version>2.0.2</version>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.12.2</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.17.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.xml.parsers</groupId>
|
||||
<artifactId>jaxp-api</artifactId>
|
||||
<version>1.4.5</version>
|
||||
</dependency>
|
||||
-->
|
||||
<!--
|
||||
<dependency>
|
||||
<groupId>xml-apis</groupId>
|
||||
<artifactId>xml-apis</artifactId>
|
||||
<version>2.0.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.xml.parsers</groupId>
|
||||
<artifactId>jaxp-api</artifactId>
|
||||
<version>1.4.5</version>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ public class ApplicationConstants {
|
||||
|
||||
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)
|
||||
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg]"; // hard-coded marker (user cannot remove it)
|
||||
|
||||
/**
|
||||
* UI message retention limits.
|
||||
|
||||
@@ -1,122 +1,178 @@
|
||||
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
|
||||
* preferences and instances via the Chatpreferences-object of the
|
||||
* Chatcontroller.
|
||||
* <br/><br/>
|
||||
* The task will be runned out of the singleton ChatController instance in an
|
||||
* intervall as specified by the Chatpreferences-instance (typically as
|
||||
* configured in the xml file.
|
||||
*
|
||||
*
|
||||
* @author prakt
|
||||
* Sends the configured public-chat beacons for both active chat categories.
|
||||
*
|
||||
* <p>Both categories deliberately share one timer and therefore the same
|
||||
* interval. Their enable flags and message templates remain independent. Every
|
||||
* run reads the current preferences, resolves global message variables and
|
||||
* sends only the categories which are currently enabled.</p>
|
||||
*/
|
||||
public class BeaconTask extends TimerTask {
|
||||
|
||||
private ChatController chatController;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "MyBeacon";
|
||||
private static final String THREAD_NICKNAME = "MyBeacon";
|
||||
|
||||
public BeaconTask(ChatController client, ThreadStatusCallback callback) {
|
||||
this.callBackToController = callback;
|
||||
this.chatController = client;
|
||||
private final ChatController chatController;
|
||||
private final ThreadStatusCallback callbackToController;
|
||||
|
||||
/**
|
||||
* Creates one execution of the shared beacon timer.
|
||||
*
|
||||
* @param chatController controller providing preferences and the TX queue
|
||||
* @param callbackToController callback used by the thread-status display
|
||||
*/
|
||||
public BeaconTask(
|
||||
ChatController chatController,
|
||||
ThreadStatusCallback callbackToController
|
||||
) {
|
||||
this.chatController = chatController;
|
||||
this.callbackToController = callbackToController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
reportStatus(THREAD_NICKNAME, true, "initialized", false);
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
|
||||
ChatMessage beaconMSG = new ChatMessage();
|
||||
|
||||
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
|
||||
|
||||
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
|
||||
|
||||
|
||||
|
||||
beaconMSG.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG.setMessageDirectedToServer(true);
|
||||
|
||||
|
||||
|
||||
|
||||
ChatMessage beaconMSG2 = new ChatMessage();
|
||||
|
||||
String replaceVariables2 = this.chatController.getChatPreferences().getBcn_beaconTextSecondCat();
|
||||
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables2 = replaceVariables2.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
|
||||
|
||||
|
||||
beaconMSG2.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG2.setMessageDirectedToServer(true);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* beacon 1st Chatcategory
|
||||
*/
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [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 {
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* beacon 2nd Chatcategory
|
||||
*/
|
||||
if (this.chatController.getChatPreferences().isLoginToSecondChatEnabled()) { //only send if 2nd cat enabled
|
||||
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledSecondCat()) {
|
||||
|
||||
beaconMSG2.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables2 + "|0|");
|
||||
beaconMSG2.setMessageDirectedToServer(true);
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [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 {
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
}
|
||||
MessageVariableResolver variableResolver =
|
||||
new MessageVariableResolver(chatController.getChatPreferences());
|
||||
|
||||
sendMainCategoryBeacon(variableResolver);
|
||||
sendSecondCategoryBeacon(variableResolver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the main-category beacon if it is currently enabled.
|
||||
*/
|
||||
private void sendMainCategoryBeacon(MessageVariableResolver variableResolver) {
|
||||
if (!chatController.getChatPreferences().isBcn_beaconsEnabledMainCat()) {
|
||||
reportStatus(THREAD_NICKNAME + " 1", false, "off", false);
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextMainCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategoryMain()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
"main category"
|
||||
);
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 1", false, "invalid text", true);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(
|
||||
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending main-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
reportStatus(THREAD_NICKNAME + " 1", true, "on", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the second-category beacon if the second login and its beacon are
|
||||
* currently enabled.
|
||||
*/
|
||||
private void sendSecondCategoryBeacon(
|
||||
MessageVariableResolver variableResolver
|
||||
) {
|
||||
if (!chatController.getChatPreferences().isLoginToSecondChatEnabled()
|
||||
|| !chatController.getChatPreferences()
|
||||
.isBcn_beaconsEnabledSecondCat()) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "off", false);
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextSecondCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategorySecond()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
"second category"
|
||||
);
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "invalid text", true);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(
|
||||
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending second-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
reportStatus(THREAD_NICKNAME + " 2", true, "on", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the server-directed message after validating the resolved payload.
|
||||
*
|
||||
* <p>The resolved text is checked rather than only the configured template
|
||||
* because inserted values can increase the final message length.</p>
|
||||
*
|
||||
* @param categoryNumber ON4KST category number
|
||||
* @param resolvedText fully resolved beacon payload
|
||||
* @param categoryDescription text used in diagnostic output
|
||||
* @return prepared message, or {@code null} if the payload is invalid
|
||||
*/
|
||||
private ChatMessage buildBeaconMessage(
|
||||
int categoryNumber,
|
||||
String resolvedText,
|
||||
String categoryDescription
|
||||
) {
|
||||
if (resolvedText == null
|
||||
|| resolvedText.length() > ChatController.MAX_BEACON_TEXT_LENGTH) {
|
||||
int actualLength = resolvedText == null ? 0 : resolvedText.length();
|
||||
System.out.println(
|
||||
"[BeaconTask, Warning]: Beacon for "
|
||||
+ categoryDescription
|
||||
+ " was not sent because the resolved text contains "
|
||||
+ actualLength
|
||||
+ " characters; maximum is "
|
||||
+ ChatController.MAX_BEACON_TEXT_LENGTH
|
||||
+ "."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
ChatMessage beaconMessage = new ChatMessage();
|
||||
beaconMessage.setMessageText(
|
||||
"MSG|" + categoryNumber + "|0|" + resolvedText + "|0|"
|
||||
);
|
||||
beaconMessage.setMessageDirectedToServer(true);
|
||||
return beaconMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards one state update to the existing thread-status display.
|
||||
*/
|
||||
private void reportStatus(
|
||||
String threadName,
|
||||
boolean running,
|
||||
String information,
|
||||
boolean criticalState
|
||||
) {
|
||||
ThreadStateMessage stateMessage = new ThreadStateMessage(
|
||||
threadName,
|
||||
running,
|
||||
information,
|
||||
criticalState
|
||||
);
|
||||
callbackToController.onThreadStatus(THREAD_NICKNAME, stateMessage);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,10 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
private static final boolean DEBUG_BAND_UPGRADE_HINT = true; //for new band hint
|
||||
|
||||
|
||||
public static final int MIN_BEACON_INTERVAL_MINUTES = 1;
|
||||
public static final int MAX_BEACON_TEXT_LENGTH = 120;
|
||||
private static final long INITIAL_BEACON_DELAY_MILLIS = 10_000L;
|
||||
|
||||
private PstRotatorClient rotatorClient;
|
||||
private Consumer<Double> viewRotorCallback;
|
||||
|
||||
@@ -708,8 +712,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
// writeThread.interrupt();
|
||||
// readThread.interrupt();
|
||||
|
||||
beaconTimer.purge();
|
||||
beaconTimer.cancel();
|
||||
stopBeaconTimer();
|
||||
ASQueryTimer.purge();
|
||||
ASQueryTimer.cancel();
|
||||
socketCheckTimer.purge();
|
||||
@@ -2239,6 +2242,78 @@ private ObservableList<String>
|
||||
this.dbHandler = dbHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the shared beacon timer with the interval currently stored in the
|
||||
* preferences.
|
||||
*
|
||||
* <p>Both chat categories deliberately use the same timer. The task checks the
|
||||
* individual enable flag and text for each category every time it runs.</p>
|
||||
*
|
||||
* @param initialDelayMillis delay before the next beacon run
|
||||
*/
|
||||
private synchronized void scheduleBeaconTimer(long initialDelayMillis) {
|
||||
stopBeaconTimer();
|
||||
|
||||
int configuredInterval =
|
||||
chatPreferences.getBcn_beaconIntervalInMinutesMainCat();
|
||||
int effectiveInterval =
|
||||
Math.max(MIN_BEACON_INTERVAL_MINUTES, configuredInterval);
|
||||
|
||||
/*
|
||||
* Keep both legacy XML values synchronized. The second value remains in the
|
||||
* configuration for backward compatibility, but no longer represents an
|
||||
* independent timer.
|
||||
*/
|
||||
chatPreferences.setBcn_beaconIntervalInMinutesMainCat(effectiveInterval);
|
||||
chatPreferences.setBcn_beaconIntervalInMinutesSecondCat(effectiveInterval);
|
||||
|
||||
long intervalMillis = TimeUnit.MINUTES.toMillis(effectiveInterval);
|
||||
long safeInitialDelay = Math.max(0L, initialDelayMillis);
|
||||
|
||||
beaconTimer = new Timer("BeaconTimer");
|
||||
beaconTimer.schedule(
|
||||
new BeaconTask(this, this),
|
||||
safeInitialDelay,
|
||||
intervalMillis
|
||||
);
|
||||
|
||||
System.out.println("[ChatController, Info]: Shared beacon timer scheduled every "
|
||||
+ effectiveInterval + " minute(s).");
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a changed beacon interval while the chat connection is running.
|
||||
*
|
||||
* <p>The countdown starts again with the new interval. No beacon is sent
|
||||
* immediately merely because the setting was changed.</p>
|
||||
*/
|
||||
public synchronized void restartBeaconTimer() {
|
||||
if (!isConnectedAndLoggedIn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
long intervalMillis = TimeUnit.MINUTES.toMillis(
|
||||
Math.max(
|
||||
MIN_BEACON_INTERVAL_MINUTES,
|
||||
chatPreferences.getBcn_beaconIntervalInMinutesMainCat()
|
||||
)
|
||||
);
|
||||
scheduleBeaconTimer(intervalMillis);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the shared beacon timer if it is currently running.
|
||||
*/
|
||||
private synchronized void stopBeaconTimer() {
|
||||
if (beaconTimer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
beaconTimer.cancel();
|
||||
beaconTimer.purge();
|
||||
beaconTimer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* execute is the main entry point where the application starts.
|
||||
* @throws InterruptedException
|
||||
@@ -2338,11 +2413,7 @@ private ObservableList<String>
|
||||
* The CQ-beacon-Task will be executed every time but checks for itself whether
|
||||
* CQ messages are enabled or not
|
||||
*/
|
||||
// Timer beaconTimer;
|
||||
beaconTimer = new Timer();
|
||||
beaconTimer.schedule(new BeaconTask(this, this), 10000,
|
||||
this.getChatPreferences().getBcn_beaconIntervalInMinutesMainCat() * 60000);
|
||||
// 60000 * intervalInMinutes = IntervalInMillis
|
||||
scheduleBeaconTimer(INITIAL_BEACON_DELAY_MILLIS);
|
||||
|
||||
/**
|
||||
* The AS querier task will be executed every time but checks for itself whether
|
||||
|
||||
@@ -47,13 +47,12 @@ public class MessageBusManagementThread extends Thread {
|
||||
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
|
||||
// ==== Auto-answer flood/ping-pong protection ====
|
||||
private static final String AUTOANSWER_PREFIX = ApplicationConstants.AUTOANSWER_PREFIX;
|
||||
private static final long AUTOANSWER_COOLDOWN_MS = 120_000L; // two minutes
|
||||
|
||||
// Cooldown per opponent station (and ChatCategory) – only setted if this client sends
|
||||
// Cooldown per remote station and chat category; updated only after this client sends.
|
||||
private final Hashtable<String, Long> lastLocalAutoAnswerPerRemoteMs = new Hashtable<>();
|
||||
|
||||
// BufferedWriter bufwrtrDBGMSGOut;
|
||||
|
||||
// private String text;
|
||||
@@ -829,7 +828,11 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
versionInfo.setSender(itsMe);
|
||||
versionInfo.setReceiver(newMessageArrived.getSender());
|
||||
versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + ApplicationConstants.AUTOANSWER_PREFIX + " " + "KST4Contest " + " v" + ApplicationConstants.APPLICATION_CURRENT_VERSION + " by DO5AMF");
|
||||
versionInfo.setChatCategory(newMessageArrived.getChatCategory());
|
||||
versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
|
||||
+ " " + AUTOANSWER_PREFIX
|
||||
+ " KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION
|
||||
+ " by DO5AMF");
|
||||
|
||||
this.client.getMessageTXBus().add(versionInfo);
|
||||
}
|
||||
@@ -876,11 +879,11 @@ public class MessageBusManagementThread extends Thread {
|
||||
// }
|
||||
// }
|
||||
|
||||
// ==== Unified Autoanswer (Generic + QRG) with Pingpong-Guard + per-Remote Cooldown ====
|
||||
// ==== Unified auto-answer (generic + QRG) with ping-pong guard and per-remote cooldown ====
|
||||
final String incomingText = newMessageArrived.getMessageText();
|
||||
final String incomingLower = (incomingText == null) ? "" : incomingText.toLowerCase(Locale.ROOT);
|
||||
|
||||
// 1) Pingpong-security: never ever react to auto generated messages
|
||||
// Never answer another automatically generated message.
|
||||
if (!isAutoMessage(newMessageArrived)) {
|
||||
|
||||
boolean qrgRequested = false;
|
||||
@@ -896,24 +899,17 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
boolean genericEnabled = this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled();
|
||||
|
||||
// 2) Entscheide, ob überhaupt geantwortet wird (QRG hat Vorrang vor Generic)
|
||||
// A QRG reply takes precedence over the generic reply.
|
||||
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();
|
||||
}
|
||||
|
||||
payload = "QRG is: " + getAutoAnswerQrgForCategory(newMessageArrived.getChatCategory());
|
||||
} else if (genericEnabled) {
|
||||
|
||||
payload = this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat();
|
||||
}
|
||||
|
||||
// 3) Cooldown pro Gegenstation: nur wenn DIESER Client jetzt wirklich sendet
|
||||
// Apply the cooldown only when this client is about to send a reply.
|
||||
if (payload != null && isAutoAnswerAllowedNow(newMessageArrived)) {
|
||||
|
||||
ChatMessage automaticAnswer = new ChatMessage();
|
||||
@@ -922,15 +918,15 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
automaticAnswer.setSender(itsMe);
|
||||
automaticAnswer.setReceiver(newMessageArrived.getSender());
|
||||
automaticAnswer.setChatCategory(newMessageArrived.getChatCategory());
|
||||
|
||||
// Prefix fest + nicht entfernbar, damit Auto↔Auto nicht pingpongt
|
||||
// The fixed prefix prevents automatic clients from answering each other.
|
||||
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.
|
||||
// Record only locally generated replies, not the later server echo.
|
||||
markLocalAutoAnswerSent(newMessageArrived);
|
||||
}
|
||||
}
|
||||
@@ -1544,9 +1540,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
|
||||
/**
|
||||
* check if message had been auto generated
|
||||
* @param msg
|
||||
* @return
|
||||
* Returns whether a message carries the fixed marker used for automatic replies.
|
||||
*/
|
||||
private boolean isAutoMessage(ChatMessage msg) {
|
||||
return msg != null
|
||||
@@ -1554,6 +1548,22 @@ public class MessageBusManagementThread extends Thread {
|
||||
&& msg.getMessageText().contains(AUTOANSWER_PREFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configured QRG for the category in which the request was received.
|
||||
* If the category cannot be resolved, the main category remains the safe fallback.
|
||||
*/
|
||||
private String getAutoAnswerQrgForCategory(ChatCategory incomingCategory) {
|
||||
ChatCategory secondCategory = this.client.getChatCategorySecondChat();
|
||||
|
||||
if (incomingCategory != null
|
||||
&& secondCategory != null
|
||||
&& incomingCategory.getCategoryNumber() == secondCategory.getCategoryNumber()) {
|
||||
return this.client.getChatPreferences().getMYQRGSecondCat().getValue();
|
||||
}
|
||||
|
||||
return this.client.getChatPreferences().getMYQRGFirstCat().getValue();
|
||||
}
|
||||
|
||||
private String autoAnswerCooldownKey(ChatMessage incoming) {
|
||||
|
||||
String remoteCall = "UNKNOWN";
|
||||
@@ -1561,13 +1571,16 @@ public class MessageBusManagementThread extends Thread {
|
||||
remoteCall = incoming.getSender().getCallSign().toUpperCase();
|
||||
}
|
||||
|
||||
int cat = 0; // fallback
|
||||
if (incoming != null && incoming.getSender() != null && incoming.getSender().getChatCategory() != null) {
|
||||
cat = incoming.getSender().getChatCategory().getCategoryNumber();
|
||||
int categoryNumber = 0;
|
||||
if (incoming != null && incoming.getChatCategory() != null) {
|
||||
categoryNumber = incoming.getChatCategory().getCategoryNumber();
|
||||
} else if (incoming != null
|
||||
&& incoming.getSender() != null
|
||||
&& incoming.getSender().getChatCategory() != null) {
|
||||
categoryNumber = incoming.getSender().getChatCategory().getCategoryNumber();
|
||||
}
|
||||
|
||||
// pro Gegenstation + pro Chat-Kategorie (falls derselbe Call in Cat2/Cat3 PMs macht)
|
||||
return remoteCall + "|" + cat;
|
||||
return remoteCall + "|" + categoryNumber;
|
||||
}
|
||||
|
||||
private boolean isAutoAnswerAllowedNow(ChatMessage incoming) {
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import kst4contest.model.AirPlane;
|
||||
import kst4contest.model.AirPlaneReflectionInfo;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
/**
|
||||
* Resolves variables used in operator messages, shortcuts, snippets and
|
||||
* beacons.
|
||||
*
|
||||
* <p>Global variables only depend on the local station configuration and can
|
||||
* therefore be used in every message context. Station variables additionally
|
||||
* require a selected remote station. Keeping both groups in one resolver
|
||||
* prevents the send field and the beacon tasks from implementing different
|
||||
* replacement rules.</p>
|
||||
*/
|
||||
public final class MessageVariableResolver {
|
||||
|
||||
private static final int SHORT_VALUE_LENGTH = 7;
|
||||
|
||||
private final ChatPreferences chatPreferences;
|
||||
|
||||
/**
|
||||
* Creates a resolver backed by the live application preferences.
|
||||
*
|
||||
* @param chatPreferences preferences that provide current station values
|
||||
*/
|
||||
public MessageVariableResolver(ChatPreferences chatPreferences) {
|
||||
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves variables which do not require a selected remote station.
|
||||
*
|
||||
* <p>The replacement is literal rather than regular-expression based.
|
||||
* Callsigns, locators and frequencies are data, not regular expressions.</p>
|
||||
*
|
||||
* @param template text that may contain variables
|
||||
* @return text with all available global variables resolved, or {@code null}
|
||||
* when the supplied template is {@code null}
|
||||
*/
|
||||
public String resolveGlobalVariables(String template) {
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String primaryFrequency = valueOrEmpty(chatPreferences.getMYQRGFirstCat().getValue());
|
||||
String secondaryFrequency = valueOrEmpty(chatPreferences.getMYQRGSecondCat().getValue());
|
||||
String ownLocator = valueOrEmpty(chatPreferences.getStn_loginLocatorMainCat());
|
||||
|
||||
String resolvedText = template;
|
||||
resolvedText = resolvedText.replace("MYQRGSHORT", abbreviate(primaryFrequency));
|
||||
resolvedText = resolvedText.replace("MYQRG", primaryFrequency);
|
||||
resolvedText = resolvedText.replace("SECONDQRG", secondaryFrequency);
|
||||
resolvedText = resolvedText.replace("MYLOCATORSHORT", abbreviateLocator(ownLocator));
|
||||
resolvedText = resolvedText.replace("MYLOCATOR", ownLocator);
|
||||
resolvedText = resolvedText.replace("MYCALL", valueOrEmpty(chatPreferences.getStn_loginCallSign()));
|
||||
resolvedText = resolvedText.replace("MYQTF", formatHeading(chatPreferences.getActualQTF().getValue().doubleValue()));
|
||||
return resolvedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves global variables and variables derived from a selected station.
|
||||
*
|
||||
* <p>If no station is selected, station-specific placeholders remain visible.
|
||||
* This is intentional: silently removing {@code QRZNAME}, {@code FIRSTAP} or
|
||||
* {@code SECONDAP} could create a plausible-looking but incomplete message.</p>
|
||||
*
|
||||
* @param template text that may contain variables
|
||||
* @param selectedStation currently selected remote station, may be {@code null}
|
||||
* @return resolved message text
|
||||
*/
|
||||
public String resolveForSelectedStation(String template, ChatMember selectedStation) {
|
||||
String resolvedText = resolveGlobalVariables(template);
|
||||
|
||||
if (resolvedText == null || selectedStation == null) {
|
||||
return resolvedText;
|
||||
}
|
||||
|
||||
resolvedText = resolvedText.replace("QRZNAME", resolveStationName(selectedStation));
|
||||
resolvedText = resolvedText.replace("FIRSTAP", resolveFirstAirPlane(selectedStation));
|
||||
resolvedText = resolvedText.replace("SECONDAP", resolveSecondAirPlane(selectedStation));
|
||||
return resolvedText;
|
||||
}
|
||||
|
||||
private String resolveStationName(ChatMember selectedStation) {
|
||||
String stationName = valueOrEmpty(selectedStation.getName()).trim();
|
||||
|
||||
if (!stationName.isEmpty()) {
|
||||
return stationName;
|
||||
}
|
||||
|
||||
return valueOrEmpty(selectedStation.getCallSign());
|
||||
}
|
||||
|
||||
private String resolveFirstAirPlane(ChatMember selectedStation) {
|
||||
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
|
||||
|
||||
if (risingAirPlanes.isEmpty()) {
|
||||
return "no ap available";
|
||||
}
|
||||
|
||||
AirPlane firstAirPlane = risingAirPlanes.get(0);
|
||||
return "a " + firstAirPlane.getPotencialDescriptionAsWord()
|
||||
+ " in " + firstAirPlane.getArrivingDurationMinutes() + " min";
|
||||
}
|
||||
|
||||
private String resolveSecondAirPlane(ChatMember selectedStation) {
|
||||
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
|
||||
|
||||
if (risingAirPlanes.size() < 2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
AirPlane secondAirPlane = risingAirPlanes.get(1);
|
||||
return "Next " + secondAirPlane.getPotencialDescriptionAsWord()
|
||||
+ " in " + secondAirPlane.getArrivingDurationMinutes() + " min";
|
||||
}
|
||||
|
||||
private List<AirPlane> getRisingAirPlanes(ChatMember selectedStation) {
|
||||
AirPlaneReflectionInfo reflectionInfo = selectedStation.getAirPlaneReflectInfo();
|
||||
|
||||
if (reflectionInfo == null || reflectionInfo.getRisingAirplanes() == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return reflectionInfo.getRisingAirplanes();
|
||||
}
|
||||
|
||||
private String abbreviate(String value) {
|
||||
return value.substring(0, Math.min(value.length(), SHORT_VALUE_LENGTH));
|
||||
}
|
||||
|
||||
private String abbreviateLocator(String locator) {
|
||||
return locator.substring(0, Math.min(locator.length(), 4));
|
||||
}
|
||||
|
||||
private String formatHeading(double headingDegrees) {
|
||||
if (!Double.isFinite(headingDegrees)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return BigDecimal.valueOf(headingDegrees).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private String valueOrEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
@@ -35,13 +35,11 @@ public class ScoreboardUpdateTask extends TimerTask {
|
||||
|
||||
ChatMessage beaconMSG = new ChatMessage();
|
||||
|
||||
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
|
||||
// replaceVariables = bcn_beaconText;
|
||||
|
||||
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
MessageVariableResolver variableResolver =
|
||||
new MessageVariableResolver(this.chatController.getChatPreferences());
|
||||
String replaceVariables = variableResolver.resolveGlobalVariables(
|
||||
this.chatController.getChatPreferences().getBcn_beaconTextMainCat()
|
||||
);
|
||||
|
||||
|
||||
beaconMSG.setMessageText(
|
||||
|
||||
@@ -2692,6 +2692,14 @@ public class ChatPreferences {
|
||||
messageHandling_autoAnswerEnabledSecondCat,
|
||||
"messageHandling_autoAnswerEnabledSecondCat",
|
||||
"autoAnswerEnabledSecondCat");
|
||||
|
||||
/*
|
||||
* The user interface intentionally exposes one shared generic auto-answer
|
||||
* setting for both chat categories. Keep the legacy second-category XML
|
||||
* fields synchronized so existing configuration files remain compatible.
|
||||
*/
|
||||
messageHandling_autoAnswerTextSecondCat = messageHandling_autoAnswerTextMainCat;
|
||||
messageHandling_autoAnswerEnabledSecondCat = messageHandling_autoAnswerEnabled;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import kst4contest.controller.MessageVariableResolver;
|
||||
import kst4contest.model.AirPlane;
|
||||
import kst4contest.model.AirPlaneReflectionInfo;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
class MessageVariableResolverTest {
|
||||
|
||||
private ChatPreferences chatPreferences;
|
||||
private MessageVariableResolver resolver;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
chatPreferences = mock(ChatPreferences.class);
|
||||
|
||||
when(chatPreferences.getMYQRGFirstCat()).thenReturn(new SimpleStringProperty("144.388.03"));
|
||||
when(chatPreferences.getMYQRGSecondCat()).thenReturn(new SimpleStringProperty("432.088.00"));
|
||||
when(chatPreferences.getStn_loginLocatorMainCat()).thenReturn("JO51IJ");
|
||||
when(chatPreferences.getStn_loginCallSign()).thenReturn("DO5AMF");
|
||||
when(chatPreferences.getActualQTF()).thenReturn(new SimpleDoubleProperty(135.0));
|
||||
|
||||
resolver = new MessageVariableResolver(chatPreferences);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesAllGlobalVariablesInEveryMessageContext() {
|
||||
String template = "MYCALL MYQRGSHORT MYQRG SECONDQRG MYLOCATORSHORT MYLOCATOR MYQTF";
|
||||
|
||||
assertEquals(
|
||||
"DO5AMF 144.388 144.388.03 432.088.00 JO51 JO51IJ 135",
|
||||
resolver.resolveGlobalVariables(template)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shortValuesRemainSafeWhenTheConfiguredValueIsShorterThanExpected() {
|
||||
when(chatPreferences.getMYQRGFirstCat()).thenReturn(new SimpleStringProperty("144"));
|
||||
when(chatPreferences.getStn_loginLocatorMainCat()).thenReturn("JO5");
|
||||
|
||||
assertEquals("144 JO5", resolver.resolveGlobalVariables("MYQRGSHORT MYLOCATORSHORT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesSelectedStationNameAndTheFirstTwoAirPlanes() {
|
||||
ChatMember selectedStation = new ChatMember();
|
||||
selectedStation.setCallSign("DL0TEST");
|
||||
selectedStation.setName("Test Operator");
|
||||
|
||||
AirPlane firstAirPlane = new AirPlane();
|
||||
firstAirPlane.setPotential(100);
|
||||
firstAirPlane.setArrivingDurationMinutes(1);
|
||||
|
||||
AirPlane secondAirPlane = new AirPlane();
|
||||
secondAirPlane.setPotential(75);
|
||||
secondAirPlane.setArrivingDurationMinutes(9);
|
||||
|
||||
AirPlaneReflectionInfo reflectionInfo = new AirPlaneReflectionInfo();
|
||||
reflectionInfo.setRisingAirplanes(FXCollections.observableArrayList(firstAirPlane, secondAirPlane));
|
||||
selectedStation.setAirPlaneReflectInfo(reflectionInfo);
|
||||
|
||||
assertEquals(
|
||||
"Hi Test Operator, a very big AP in 1 min; Next big AP in 9 min",
|
||||
resolver.resolveForSelectedStation(
|
||||
"Hi QRZNAME, FIRSTAP; SECONDAP",
|
||||
selectedStation
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesCallsignWhenTheSelectedStationHasNoName() {
|
||||
ChatMember selectedStation = new ChatMember();
|
||||
selectedStation.setCallSign("DL0TEST");
|
||||
selectedStation.setName(" ");
|
||||
|
||||
assertEquals(
|
||||
"Hi DL0TEST",
|
||||
resolver.resolveForSelectedStation("Hi QRZNAME", selectedStation)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsStationVariablesVisibleWhenNoStationIsSelected() {
|
||||
assertEquals(
|
||||
"QRZNAME FIRSTAP SECONDAP",
|
||||
resolver.resolveForSelectedStation("QRZNAME FIRSTAP SECONDAP", null)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsUsefulFallbacksWhenNoAirPlaneIsAvailable() {
|
||||
ChatMember selectedStation = new ChatMember();
|
||||
selectedStation.setCallSign("DL0TEST");
|
||||
|
||||
assertEquals(
|
||||
"no ap available ",
|
||||
resolver.resolveForSelectedStation("FIRSTAP SECONDAP", selectedStation)
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ module praktiKST {
|
||||
requires java.net.http;
|
||||
requires java.desktop;
|
||||
requires jdk.crypto.ec;
|
||||
requires org.junit.jupiter.api;
|
||||
requires org.mockito;
|
||||
exports kst4contest.controller.interfaces;
|
||||
exports kst4contest.controller;
|
||||
exports kst4contest.locatorUtils;
|
||||
|
||||
Reference in New Issue
Block a user