mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-09-10 19:25:32 +02:00
Compare commits
6
Commits
047891e109
...
7ff7248e7d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ff7248e7d | ||
|
|
3193e4ac73 | ||
|
|
3ec6cab46a | ||
|
|
b280f1b0db | ||
|
|
7fe33613e9 | ||
|
|
9378bf2afd |
@@ -1,6 +1,6 @@
|
||||
# KST4Contest Project Context
|
||||
|
||||
Last reviewed: 2026-08-25
|
||||
Last reviewed: 2026-08-27
|
||||
|
||||
This file is the durable technical project context for KST4Contest. It is not a user manual and not a replacement for the changelog. Current code, tests and authoritative external specifications remain the source of truth when this document is stale or ambiguous.
|
||||
|
||||
@@ -65,6 +65,14 @@ Known integration areas include:
|
||||
|
||||
CR/LF framing, XML framing, ports/transports, callsign normalization and frequency formatting are protocol behaviour and must not be changed as incidental cleanup.
|
||||
|
||||
### ON4KST session liveness
|
||||
|
||||
- After 90 seconds without inbound data, the application keeps the established empty CRLF heartbeat.
|
||||
- At about 180 seconds of inbound idle time, the TCP session sends one `RDXQ|<main chat id>|` probe. The probe state belongs to the session, so a two-category login still sends only one probe per idle phase.
|
||||
- Any subsequent inbound server frame confirms the probe. `DXQ` is accepted as the expected internal response and is not published as chat content.
|
||||
- If no inbound frame arrives by about 210 seconds, the existing reconnect flow remains responsible for replacing the session.
|
||||
- Probe diagnostics contain the session id, main category, opcode and timing only. They must not include credentials, complete server frames or normal chat messages.
|
||||
|
||||
## User Workflow / UI Invariants
|
||||
|
||||
- Contest operating speed and low-friction interaction are primary goals.
|
||||
|
||||
@@ -70,7 +70,7 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
|
||||
|
||||
- **Exakte Sked-Ziele:** Timeline und automatische Erinnerungen verwenden das vollständige sichtbare KST-Rufzeichen. Ein Sked für `DN9APW-2` wird nicht versehentlich an eine andere Variante desselben Basisrufzeichens gesendet.
|
||||
|
||||
- **Beacon und Autoantwort überarbeitet:** Beide Chat-Kategorien verwenden einen gemeinsamen Timer, behalten aber getrennte Aktivierungsschalter und Texte. Das zulässige Mindestintervall beträgt zwei Minuten; Nachrichtentexte sind auf 120 Zeichen begrenzt. Die gespeicherte Beacon-Aktivierung wird beim Start aus der Konfiguration übernommen.
|
||||
- **Beacon und Autoantwort überarbeitet:** Beide Chat-Kategorien verwenden einen gemeinsamen Timer, behalten aber getrennte Aktivierungsschalter und Texte. Das zulässige Mindestintervall beträgt eine Minute; Nachrichtentexte sind auf 120 Zeichen begrenzt. Die gespeicherte Beacon-Aktivierung wird beim Start aus der Konfiguration übernommen.
|
||||
|
||||
- **Variablen zentral aufgelöst:** Nachrichtenvariablen für Beacons, Shortcuts, Snippets und andere automatisch erzeugte Texte werden über einen gemeinsamen Resolver verarbeitet.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ Im Klartext: Die Information muss nicht erst im Chat gefunden, gelesen, gemerkt
|
||||
|
||||
---
|
||||
|
||||
## Wie wird eine Richtungsgelegenheit hergeleitet?
|
||||
## Automatische Spots aus Richtungsgelegenheiten
|
||||
|
||||
Angenommen, Station A schreibt eine gerichtete Nachricht an Station B. KST4Contest verwendet die Richtung von A zu B als Näherung für die aktuelle Antennenrichtung von Station A. Anschließend wird geprüft, ob die eigene Station aus Sicht von A innerhalb des angenommenen Antennenkorridors liegt.
|
||||
|
||||
@@ -51,6 +51,22 @@ Das Verfahren berücksichtigt weder Gelände noch aktuelle Ausbreitungsbedingung
|
||||
|
||||
---
|
||||
|
||||
## Manueller Spot für die ausgewählte Kartenstation
|
||||
|
||||
Ein Spot kann auch bewusst ausgelöst werden. Wähle dazu eine Station auf der Stationskarte und verwende **Trigger cluster spot** im Detailbereich.
|
||||
|
||||
Diese manuelle Auslösung benötigt keine zuvor erkannte gerichtete Nachricht. Auch maximaler QRB und Antennen-Öffnungswinkel entscheiden in diesem Fall nicht darüber, ob der Spot gesendet wird. Erforderlich sind:
|
||||
|
||||
- ein aktivierter lokaler DX-Cluster-Server,
|
||||
- mindestens ein verbundener DX-Cluster-Client und
|
||||
- eine für die ausgewählte Kartenstation verwendbare QRG.
|
||||
|
||||
Damit kann der Operator eine bereits ausgewählte Station gezielt in die Bandmap übernehmen, auch wenn die Bedingungen für einen automatischen Richtungs-Spot nicht vorliegen. Die Bedienung der Karte ist unter [Stationskarte](de-Benutzeroberflaeche#stationskarte) beschrieben.
|
||||
|
||||
Automatische und manuelle Spots werden ausschließlich an die mit KST4Contest verbundenen Clients gesendet. Es erfolgt keine Weiterleitung an einen öffentlichen Internet-Cluster.
|
||||
|
||||
---
|
||||
|
||||
## Welche Frequenz wird verwendet?
|
||||
|
||||
Ein DX-Cluster-Spot benötigt eine eindeutige Frequenz. KST4Contest verwendet dafür dieselbe QRG-Erkennung wie die Benutzerliste und die übrigen bandbezogenen Funktionen.
|
||||
@@ -171,12 +187,11 @@ Ein Spot enthält:
|
||||
|
||||
- das konfigurierte Spotter-Rufzeichen,
|
||||
- die normalisierte Frequenz,
|
||||
- das Rufzeichen der erkannten Station,
|
||||
- das vollständige sichtbare Rufzeichen der erkannten oder ausgewählten Station,
|
||||
- den Locator,
|
||||
- Flugzeug-Scatter-Informationen, falls vorhanden,
|
||||
- die aktuelle UTC-Zeit.
|
||||
|
||||
Wenn für die Station aktuelle Aircraft-Scatter-Informationen vorliegen, kann KST4Contest diese als zusätzliche AP-Information in den Kommentar des Spots aufnehmen.
|
||||
Bei automatisch erzeugten Richtungs-Spots kann KST4Contest bis zu zwei aktuelle AirScout-Einträge als zusätzliche AP-Information in den Kommentar aufnehmen. Fehlende AirScout-Daten verhindern den Spot nicht. Ein manuell über die Stationskarte ausgelöster Spot verwendet den Locator der ausgewählten Station ohne diese optionale Ergänzung.
|
||||
|
||||
---
|
||||
|
||||
@@ -193,7 +208,7 @@ Prüfe:
|
||||
- Blockiert eine Firewall die Verbindung?
|
||||
- Ist im Logger das DX-Cluster-Fenster beziehungsweise die Bandmap aktiviert?
|
||||
|
||||
### Testspot funktioniert, aber reale Spots fehlen
|
||||
### Testspot funktioniert, aber automatische Spots fehlen
|
||||
|
||||
Dann funktioniert die Verbindung grundsätzlich. Für die betreffende Chat-Situation war wahrscheinlich mindestens eine fachliche Bedingung nicht erfüllt:
|
||||
|
||||
@@ -205,6 +220,10 @@ Dann funktioniert die Verbindung grundsätzlich. Für die betreffende Chat-Situa
|
||||
|
||||
KST4Contest sendet absichtlich nicht jede gefundene Frequenz an den Logger. Andernfalls würde aus einer Arbeitserleichterung sehr schnell eine lokale Spot-Schleuder.
|
||||
|
||||
### Manuell ausgelöster Spot fehlt
|
||||
|
||||
Prüfe, ob auf der Karte eine Station ausgewählt ist und für sie eine verwendbare QRG vorliegt. Der lokale Server muss aktiviert und mindestens ein Client verbunden sein. Die Geometrie einer gerichteten Nachricht, maximaler QRB und Antennen-Öffnungswinkel sind für die manuelle Auslösung keine Voraussetzungen.
|
||||
|
||||
### Der Spot erscheint auf dem falschen Band
|
||||
|
||||
Prüfe zuerst, welche Frequenzen für die betreffende Station innerhalb der letzten 30 Minuten erkannt wurden. Bei einer relativen Angabe hat dieser Stationskontext Vorrang vor dem globalen Fallback.
|
||||
|
||||
@@ -205,20 +205,6 @@ Worked-, NOT-QRV- und Großfeldinformationen werden in der internen SQLite-Daten
|
||||
|
||||
Ein manueller Reset unter **Workedstn database** entfernt sämtliche Worked-Markierungen, NOT-QRV-Tags und gespeicherten Worked-Großfelder. Die bekannten Rufzeichenzeilen bleiben dabei in der Datenbank erhalten. Einzelheiten: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
|
||||
|
||||
|
||||
---
|
||||
|
||||
## NOT-QRV-Tags (ab v1.2)
|
||||
|
||||
Wenn eine Station mitteilt, dass sie auf einem bestimmten Band nicht QRV ist, kann dies manuell markiert werden:
|
||||
|
||||
1. Station in der Benutzerliste auswählen.
|
||||
2. Rechtsklick → NOT-QRV für das entsprechende Band setzen.
|
||||
|
||||
Diese Tags werden in der internen Datenbank gespeichert und bleiben nach einem Neustart von KST4Contest erhalten. Zurücksetzen über die Einstellungen möglich.
|
||||
|
||||
**Nutzen**: Verhindert wiederholte Sked-Anfragen auf Bändern, auf denen die Station nicht QRV ist – schont sowohl die eigenen Nerven als auch die der Gegenstation.
|
||||
|
||||
---
|
||||
|
||||
## Richtungsfilter (Direction Filter)
|
||||
@@ -253,7 +239,21 @@ Bedienung und Aufbau der Filterleiste: [Benutzeroberfläche – Filter](de-Benut
|
||||
|
||||
## Farbige PM-Zeilen (ab v1.25)
|
||||
|
||||
Neue Privatnachrichten erscheinen in **Rot**. Die Farbe wechselt alle 30 Sekunden über Gelb bis Weiß – wie ein Regenbogen-Fade. So ist auf einen Blick erkennbar, wie aktuell eine Nachricht ist.
|
||||
Neue eingehende Privatnachrichten werden in mehreren grünen Altersstufen hervorgehoben. Mit zunehmendem Alter wird das Grün schrittweise gedämpfter:
|
||||
|
||||
| Alter der Nachricht | Darstellung |
|
||||
|---|---|
|
||||
| bis einschließlich 30 Sekunden | erste grüne Stufe |
|
||||
| 31 bis 60 Sekunden | zweite grüne Stufe |
|
||||
| 61 bis 90 Sekunden | dritte grüne Stufe |
|
||||
| 91 bis 120 Sekunden | vierte grüne Stufe |
|
||||
| 121 bis 180 Sekunden | fünfte grüne Stufe |
|
||||
| 181 bis 300 Sekunden | sechste grüne Stufe |
|
||||
| ab 301 Sekunden | normale Tabellenfarbe |
|
||||
|
||||
Die Tabelle aktualisiert die Altersdarstellung alle fünf Sekunden. Ein Grenzübergang kann deshalb erst beim nächsten Aktualisierungslauf sichtbar werden. Nach fünf Minuten bleibt keine Altersklasse an der Zeile haften; auch wiederverwendete oder leere Tabellenzeilen kehren zu ihrem normalen Stil zurück.
|
||||
|
||||
Eigene Nachrichten erhalten weiterhin eine separate Hervorhebung und verwenden nicht die grüne Altersskala.
|
||||
|
||||
*(Idee von IU3OAR, Gianluca Costantino – danke!)*
|
||||
|
||||
@@ -300,9 +300,9 @@ Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Be
|
||||
|
||||
## Dark Mode (ab v1.26)
|
||||
|
||||
Aktivierbar über: **Window → Use Dark Mode**
|
||||
Aktivierbar über **Windows → Use dark mode design**. Mit **Windows → Use default mode design** wird wieder auf das normale helle Farbschema umgeschaltet.
|
||||
|
||||
Für individuelle Farbanpassungen: CSS-Datei bearbeiten (Pfad in den Programmunterlagen).
|
||||
Die grüne Altersskala der Privatnachrichten bleibt in beiden Darstellungen erhalten. Textfarbe, normale Tabellenfarbe und die separate Hervorhebung eigener Nachrichten folgen dem jeweils geladenen Standarddesign.
|
||||
|
||||
---
|
||||
|
||||
@@ -994,4 +994,4 @@ Die Prüfung verwendet immer den **primären Bildschirm**. Sie stellt nicht die
|
||||
|
||||
Die automatische Größenbegrenzung gilt derzeit außerdem nur für das Hauptfenster. Das Einstellungsfenster, das separate Cluster- und QSO-Monitorfenster sowie weitere Zusatzfenster verwenden weiterhin ihre jeweils gespeicherten Größen, ohne dieselbe zusätzliche Prüfung gegen den primären Bildschirm.
|
||||
|
||||
Im Klartext: Die Schutzfunktion verhindert vor allem, dass das zentrale Hauptfenster nach einem Wechsel auf einen kleineren Bildschirm unbenutzbar startet. Sie ist keine vollständige Verwaltung aller Fensterpositionen in einem wechselnden Mehrmonitor-Setup.
|
||||
Im Klartext: Die Schutzfunktion verhindert vor allem, dass das zentrale Hauptfenster nach einem Wechsel auf einen kleineren Bildschirm unbenutzbar startet. Sie ist keine vollständige Verwaltung aller Fensterpositionen in einem wechselnden Mehrmonitor-Setup.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Nach dem ersten Start öffnet sich das **Einstellungsfenster** – dieses ist der zentrale Ausgangspunkt für alle Konfigurationen. Es empfiehlt sich, das Einstellungsfenster während des Betriebs geöffnet zu lassen (z. B. um den Beacon schnell ein- und auszuschalten).
|
||||
|
||||
> **Wichtig**: Nach jeder Änderung unbedingt **„Save Settings"** klicken! Die Einstellungen werden unter Linux in `~/.praktikst/preferences.xml` und unter Windows in `%USERPROFILE%\.praktikst\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktikst\preferences.xml`) gespeichert. Ab v1.21 werden auch Fenstergrößen und Divider-Positionen beim Speichern gesichert.
|
||||
> **Wichtig**: Nach jeder Änderung unbedingt **„Save Settings"** klicken! Die Einstellungen werden unter Linux und macOS in `~/.praktiKST/preferences.xml` und unter Windows in `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`) gespeichert. Ab v1.21 werden auch Fenstergrößen und Divider-Positionen beim Speichern gesichert.
|
||||
|
||||
---
|
||||
|
||||
@@ -871,7 +871,7 @@ Anzeige und Herleitung: [Gearbeitete Rufzeichen, neue Bänder und neue Großfeld
|
||||
|
||||
## Dark Mode (ab v1.26)
|
||||
|
||||
Umschaltbar über das Menü: **Window → Use Dark Mode**. Die Farben können über CSS individuell angepasst werden.
|
||||
Der Dark Mode wird über **Windows → Use dark mode design** aktiviert. Mit **Windows → Use default mode design** wird wieder das normale helle Farbschema geladen.
|
||||
|
||||
---
|
||||
|
||||
@@ -879,6 +879,6 @@ Umschaltbar über das Menü: **Window → Use Dark Mode**. Die Farben können ü
|
||||
|
||||
Nach **jeder** Änderung **„Save Settings"** klicken! Ohne Speichern gehen alle Änderungen beim nächsten Start verloren.
|
||||
|
||||
- Speicherort: unter Linux `~/.praktikst/preferences.xml` und unter Windows `%USERPROFILE%\.praktikst\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktikst\preferences.xml`)
|
||||
- Speicherort: unter Linux und macOS `~/.praktiKST/preferences.xml` und unter Windows `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`)
|
||||
- Ab v1.21: Fenstergrößen und Divider-Positionen werden ebenfalls gespeichert.
|
||||
- Bei Problemen: Konfigurationsdatei löschen → KST4Contest erstellt eine neue mit Standardwerten.
|
||||
|
||||
@@ -70,7 +70,7 @@ v1.42 brings several previously separate calculations together. Band information
|
||||
|
||||
- **Exact sked targets:** The timeline and automatic reminders use the complete visible KST callsign. A sked for `DN9APW-2` is not accidentally sent to another variant of the same base callsign.
|
||||
|
||||
- **Reworked beacon and automatic replies:** Both chat categories use one shared timer while retaining separate enable switches and message texts. The minimum permitted interval is two minutes and message texts are limited to 120 characters. The stored beacon state is restored from the configuration at startup.
|
||||
- **Reworked beacon and automatic replies:** Both chat categories use one shared timer while retaining separate enable switches and message texts. The minimum permitted interval is one minute and message texts are limited to 120 characters. The stored beacon state is restored from the configuration at startup.
|
||||
|
||||
- **Central variable resolution:** Message variables used by beacons, shortcuts, snippets and other generated text are processed by one shared resolver.
|
||||
|
||||
@@ -92,6 +92,8 @@ v1.42 brings several previously separate calculations together. Band information
|
||||
|
||||
- **Message-bus diagnostics:** Correctly processed ON4KST frames are no longer reported additionally as `Critical, detected unhandled Chatmessage`. Only genuinely unknown frames reach the fallback diagnostic branch.
|
||||
|
||||
- **Password in diagnostic output:** The ON4KST password is no longer written in plain text to the console or error log during connection setup.
|
||||
|
||||
- **Long-running station-selection failure:** Chat members managed by the message thread have been decoupled from the JavaFX view. Simultaneous data and table updates therefore no longer cause broken selection models or concurrent-modification problems after longer runtimes.
|
||||
|
||||
- **No phantom chat members from UM3:** Historical or additional server messages no longer create user-list entries for stations which are not actually logged into the chat.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
After the first start, the **settings window** opens – this is the central starting point for all configuration. It is recommended to keep the settings window open during operation (e.g. to quickly toggle the beacon on and off).
|
||||
|
||||
> **Important**: Always click **"Save Settings"** after any change! Settings are stored in `~/.praktikst/preferences.xml` on Linux and in `%USERPROFILE%\.praktikst\preferences.xml` (or `C:\Users\<Username>\.praktikst\preferences.xml`) on Windows. From v1.21 onwards, window sizes and divider positions are also saved when you click Save.
|
||||
> **Important**: Always click **"Save Settings"** after any change! Settings are stored in `~/.praktiKST/preferences.xml` on Linux and macOS and in `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows. From v1.21 onwards, window sizes and divider positions are also saved when you click Save.
|
||||
|
||||
---
|
||||
|
||||
@@ -40,13 +40,23 @@ After a change, click **Save Settings** and restart KST4Contest. Band columns an
|
||||
|
||||
### Antenna Beamwidth
|
||||
|
||||
Enter a realistic value for your antenna's beamwidth (in degrees). This value is used for the [Sked Direction Highlighting](en-Features#sked-direction-highlighting). A test value of 50° has proven effective; DM5M uses quads with 69°.
|
||||
Enter the complete horizontal beamwidth of the local antenna in degrees. KST4Contest applies half of this value to either side of the selected or derived antenna direction. A configured value of `70°` therefore produces a corridor of `±35°`.
|
||||
|
||||
> **Do not** enter fantasy values – the direction calculations will become useless.
|
||||
The value is used for:
|
||||
|
||||
- the QTF filter in the user list;
|
||||
- the display of the local antenna corridor; and
|
||||
- the assumed beamwidth of a remote station when [deriving directional opportunities](en-Features#directional-opportunities-from-directed-messages).
|
||||
|
||||
The final use is deliberately an approximation. ON4KST transmits neither the antenna being used nor its beamwidth. KST4Contest therefore uses the local value as a practical assumption for the remote station.
|
||||
|
||||
Choose a realistic value for the actual station setup. A value which is too large produces many geometrical matches with little practical meaning. A value which is too small may hide useful directional opportunities.
|
||||
|
||||
### Default Maximum QRB
|
||||
|
||||
Maximum distance (in km) for which direction warnings should be triggered. A realistic value for DM5M is 900 km. Stations farther away are ignored for highlighting purposes.
|
||||
Enter the maximum distance in kilometres within which KST4Contest should consider directional opportunities. The relevant distance is between the local station and the sender of the directed message, not between sender and receiver.
|
||||
|
||||
If the sender is farther away, the situation is neither highlighted nor forwarded as an automatic directional opportunity to the local DX Cluster server, even if the calculated angle would match.
|
||||
|
||||
### Path Analysis and Link Budget
|
||||
|
||||
@@ -916,7 +926,7 @@ Display and derivation: [Worked Callsigns, New Bands and New Grid Squares](en-Fe
|
||||
|
||||
## Dark Mode (from v1.26)
|
||||
|
||||
Toggle via the menu: **Window → Use Dark Mode**. The colors can be individually customized via CSS.
|
||||
Enable Dark Mode through **Windows → Use dark mode design**. Use **Windows → Use default mode design** to restore the normal light colour scheme.
|
||||
|
||||
---
|
||||
|
||||
@@ -924,6 +934,6 @@ Toggle via the menu: **Window → Use Dark Mode**. The colors can be individuall
|
||||
|
||||
Click **"Save Settings"** after **every** change! Without saving, all changes will be lost on the next start.
|
||||
|
||||
- Storage location: `~/.praktikst/preferences.xml` on Linux and `%USERPROFILE%\.praktikst\preferences.xml` (or `C:\Users\<Username>\.praktikst\preferences.xml`) on Windows
|
||||
- Storage location: `~/.praktiKST/preferences.xml` on Linux and macOS and `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows
|
||||
- From v1.21: Window sizes and divider positions are also saved.
|
||||
- If you encounter problems: delete the configuration file → KST4Contest will create a new one with default values.
|
||||
|
||||
@@ -2,50 +2,164 @@
|
||||
|
||||
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-DX-Cluster-Server)
|
||||
|
||||
From **version 1.23**, KST4Contest includes a built-in DX cluster server. It sends spots directly to the logging software whenever a direction warning is triggered.
|
||||
Since version 1.23, KST4Contest has included a local DX Cluster server. It forwards detected directional opportunities and their frequencies to the DX Cluster client of a logging programme.
|
||||
|
||||
*(Idea by OM0AAO, Viliam Petrik – thank you!)*
|
||||
The idea came from OM0AAO, Viliam Petrik. Thank you!
|
||||
|
||||
---
|
||||
|
||||
## What is the Built-in DX Cluster Server For?
|
||||
## Why Use a Local DX Cluster Server?
|
||||
|
||||
When KST4Contest detects that a station is requesting a sked from your direction and a QRG is known, it **automatically generates a DX cluster spot** and feeds it directly to the logging software's cluster client / band map.
|
||||
Finding an interesting frequency in the chat is only the first step. During a contest, that information needs to reach the place where it can be used immediately: the logging programme and its bandmap.
|
||||
|
||||
The logging software then displays the spot in the band map. Clicking the spot sets the transceiver's frequency and mode directly – without any manual typing.
|
||||
KST4Contest therefore combines two pieces of information it already has:
|
||||
|
||||
1. A directed chat message can indicate the approximate direction in which the sending station may be pointing its antenna.
|
||||
2. A frequency for that station may be known from the same message or an earlier one.
|
||||
|
||||
When both pieces fit, KST4Contest creates a local DX Cluster spot. The logger can display it in its bandmap and, depending on its own configuration, tune the transceiver to the frequency when the spot is clicked.
|
||||
|
||||
In practical terms, the operator does not have to find the information in the chat, read it, remember it and enter it again in the logger. These small interruptions consume a surprising amount of attention during a contest.
|
||||
|
||||
---
|
||||
|
||||
## Setup
|
||||
## Automatic Spots from Directional Opportunities
|
||||
|
||||
### In KST4Contest
|
||||
Assume that station A sends a directed message to station B. KST4Contest uses the direction from A to B as an approximation of the current antenna direction of station A. It then checks whether the local station lies inside the assumed antenna corridor as seen from A.
|
||||
|
||||
In Preferences → **DX Cluster Server Settings**:
|
||||
Two directions are compared:
|
||||
|
||||
1. Enter the **port** of the internal server (e.g. 7300 or 8000 – must match the logging software).
|
||||
2. Enter a **spotter callsign** – **this must be a different callsign than your contest callsign!**
|
||||
- Reason: Logging programs filter spots from your own callsign as "already worked". If the spotter uses the same callsign, the spots will not be displayed.
|
||||
3. Enter the **assumed MHz**: For frequency references like ".205" in the chat, KST4Contest needs to decide whether 144.205, 432.205 or 1296.205 is meant. For single-band contests, simply enter the corresponding band centre. Full frequency references like "144.205" or "1296.338" in the chat are always correctly identified.
|
||||
- the direction from station A to station B;
|
||||
- the direction from station A to the local station.
|
||||
|
||||
### In UCXLog
|
||||
The **Antenna Beamwidth** configured under the Station settings is the complete angle. Half of that value is applied on either side of the direction A → B. A setting of `70°` therefore produces a corridor of `±35°`.
|
||||
|
||||
- Configure a DX cluster server connection:
|
||||
- Host: `127.0.0.1` (or IP of the KST4Contest computer)
|
||||
- Port: As configured in KST4Contest
|
||||
- Password: can be left empty
|
||||
- Use the **"Send a test message to your log"** button to test the connection.
|
||||
ON4KST does not supply antenna data for the remote station. KST4Contest therefore also uses the locally configured beamwidth as an approximation for station A. This is not a measurement of the station's actual antenna direction. It is a deliberately simple geometrical assumption.
|
||||
|
||||
### In N1MM+
|
||||
An automatic DX Cluster spot is created only when all of the following conditions are met:
|
||||
|
||||
Similar settings:
|
||||
- Host: `127.0.0.1` (or IP of the KST4Contest computer)
|
||||
- Port: As configured in KST4Contest
|
||||
1. A directed message between two other stations has been detected.
|
||||
2. Valid locators are known for the sender and receiver.
|
||||
3. The sender is within the configured **Default Maximum QRB** from the local station.
|
||||
4. The local station lies inside the assumed antenna corridor as seen from the sender.
|
||||
5. A usable frequency is known for the sender or detected in the current message.
|
||||
6. The local DX Cluster server is enabled.
|
||||
|
||||
When these conditions are met, the spot is created while the message is processed. The green directional highlight shown in parallel remains visible for five minutes and may be extended or removed by later messages.
|
||||
|
||||
The calculation does not consider terrain, current propagation or the station's actual operating intention. It identifies a plausible opportunity. The full derivation and a numerical example are available under [Directional Opportunities from Directed Messages](en-Features#directional-opportunities-from-directed-messages).
|
||||
|
||||
---
|
||||
|
||||
## Manual Spot for the Selected Map Station
|
||||
|
||||
A spot can also be triggered deliberately. Select a station on the station map and use **Trigger cluster spot** in the detail panel.
|
||||
|
||||
This manual action does not require a previously detected directed message. The maximum QRB and antenna beamwidth also do not decide whether the spot is sent. It requires:
|
||||
|
||||
- the local DX Cluster server to be enabled;
|
||||
- at least one connected DX Cluster client; and
|
||||
- a usable QRG for the selected map station.
|
||||
|
||||
This lets the operator send an already selected station to the bandmap even when the conditions for an automatic directional spot are not present. Map operation is described under [Station Map](en-User-Interface#station-map).
|
||||
|
||||
Both automatic and manual spots are sent only to clients connected to KST4Contest. They are not forwarded to a public Internet cluster.
|
||||
|
||||
---
|
||||
|
||||
## Which Frequency Is Used?
|
||||
|
||||
A DX Cluster spot needs an unambiguous frequency. KST4Contest uses the same QRG detection as the user list and the other band-related functions.
|
||||
|
||||
Complete frequencies determine their band directly:
|
||||
|
||||
```text
|
||||
144.205
|
||||
432,088
|
||||
1296.338
|
||||
10368.100
|
||||
```
|
||||
|
||||
Relative values contain only the frequency part within a band:
|
||||
|
||||
```text
|
||||
.205
|
||||
,205
|
||||
qrg 205
|
||||
freq is 205
|
||||
on 205
|
||||
205 MHz
|
||||
```
|
||||
|
||||
A bare three-digit number such as `205` is not evaluated without frequency-related context. The same applies to `599`, `144` or text such as `worked 210 stations`. This prevents signal reports, band names or QSO totals from being stored as plausible-looking frequencies and later sent to the logger.
|
||||
|
||||
For a relative QRG, KST4Contest determines the band in this order:
|
||||
|
||||
1. It checks whether a suitable band context has been detected for the same sender during the previous 30 minutes.
|
||||
2. If several current bands are known, it uses the most recently updated plausible context.
|
||||
3. Only when no suitable station context exists does it use the band selected under **Fallback band for relative QRG detection**.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
Global fallback: 144 MHz
|
||||
Most recent complete QRG for the station: 432.088 MHz
|
||||
New chat value from the same station: .100
|
||||
Detected QRG: 432.100 MHz
|
||||
DX Cluster frequency: 432100.0 kHz
|
||||
```
|
||||
|
||||
Without the current 432 MHz context, the same value would use the global fallback and become `144.100 MHz`.
|
||||
|
||||
QRG detection runs before the direction and spot checks. If a station mentions its frequency for the first time in the directed message which also triggers a directional opportunity, the resulting spot can already contain that frequency. A newly detected QRG replaces an older value for the station.
|
||||
|
||||
The fallback band is a global QRG-detection setting. Its effect is not limited to the DX Cluster server. Configuration, supported bands and related behaviour are described under [Fallback Band for Relative QRG Detection](en-Configuration#fallback-band-for-relative-qrg-detection).
|
||||
|
||||
---
|
||||
|
||||
## Setting Up KST4Contest
|
||||
|
||||
Open the **Notification** tab in Preferences.
|
||||
|
||||

|
||||
|
||||
Configure the following:
|
||||
|
||||
1. Enable **Enable the local DX Cluster server …**.
|
||||
2. Enter a free **TCP port**. The default is `8000`.
|
||||
3. Select the appropriate band under **Fallback band for relative QRG detection**.
|
||||
4. Enter a **Spotter callsign**.
|
||||
|
||||
The spotter callsign should preferably differ from the contest callsign. Some loggers filter spots which appear to originate from the local station or handle them differently. Using the same callsign is not prohibited by KST4Contest, but it may make a correctly generated spot invisible in the bandmap.
|
||||
|
||||
Changes to the enabled state and TCP port take effect immediately while KST4Contest is connected to the chat. Changing the port disconnects existing DX Cluster clients; the logger must reconnect to the new port.
|
||||
|
||||
Use **Save Settings** to store the settings permanently in `preferences.xml`.
|
||||
|
||||
---
|
||||
|
||||
## Setting Up the Logging Programme
|
||||
|
||||
Configure the logging programme as a DX Cluster client connected to KST4Contest.
|
||||
|
||||
| Setting | KST4Contest and logger on the same computer | Logger on another computer |
|
||||
|---|---|---|
|
||||
| Host | `127.0.0.1` | IP address of the KST4Contest computer |
|
||||
| Port | TCP port configured in KST4Contest | TCP port configured in KST4Contest |
|
||||
| Login | Any callsign, if the logger requires one | Any callsign, if the logger requires one |
|
||||
| Password | Not required | Not required |
|
||||
|
||||
KST4Contest does not use the login sent by the logger for authentication. The connection is intended for the local computer or a trusted station network.
|
||||
|
||||
If the logger runs on another computer, its TCP connection must be allowed through the local firewall on the KST4Contest computer. Do not expose the port directly to the Internet without additional protection.
|
||||
|
||||
Several DX Cluster clients can be connected at the same time. Every generated spot is sent to all clients which are currently connected.
|
||||
|
||||
---
|
||||
|
||||
## Testing the Connection
|
||||
|
||||
After the logger's DX cluster client has connected, use **Send test spot** to generate the following entry:
|
||||
The **Send test spot** button creates the following test entry:
|
||||
|
||||
```text
|
||||
Spotted callsign: DO5AMF
|
||||
@@ -53,48 +167,92 @@ Comment: Testing DXC-Spot: Congrats, you donated $100!
|
||||
Frequency: .300 on the configured fallback band
|
||||
```
|
||||
|
||||
With `144 MHz` selected as the fallback band, the spot appears at approximately `144.300 MHz`.
|
||||
With `144 MHz` selected as the fallback band, the spot therefore appears at approximately `144.300 MHz`.
|
||||
|
||||
The comment is a deliberately retained Easter egg. It makes the test entry easy to identify but has no other function. In particular, no donation or other external action is triggered.
|
||||
The comment is a deliberately retained Easter egg. It only makes the test spot easy to recognise in the logger. No donation or other external action is triggered.
|
||||
|
||||
Three conditions must be met before running the test:
|
||||
|
||||
1. KST4Contest is connected to the ON4KST chat.
|
||||
2. The local DX cluster server is enabled.
|
||||
3. The logging software's DX cluster client is connected to KST4Contest.
|
||||
2. The local DX Cluster server is enabled.
|
||||
3. The logging programme's DX Cluster client is connected to KST4Contest.
|
||||
|
||||
If no client is connected, KST4Contest displays a corresponding message. A successful test therefore confirms that at least one connected client received the generated spot.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
## Content of a Generated Spot
|
||||
|
||||
A spot is generated when **both** conditions are met:
|
||||
A spot contains:
|
||||
|
||||
1. A **direction warning** has been triggered (station is making a sked in your direction).
|
||||
2. The **station's QRG is known** (read from the chat or manually entered).
|
||||
- the configured spotter callsign;
|
||||
- the normalised frequency;
|
||||
- the complete visible callsign of the detected or selected station;
|
||||
- the locator; and
|
||||
- the current UTC time.
|
||||
|
||||
The generated spot contains:
|
||||
- Station's callsign
|
||||
- Frequency
|
||||
- Spot time
|
||||
For automatically generated directional spots, KST4Contest can add up to two current AirScout entries to the comment. Missing AirScout data does not prevent the spot from being sent. A spot triggered manually from the station map uses the selected station's locator without this optional addition.
|
||||
|
||||
The logging software can then display the spot in the band map and tune the TRX to that frequency with a mouse click.
|
||||
An automatic comment with AirScout information may look like this:
|
||||
|
||||
```text
|
||||
JN49GL , AP: 1min, 100%; 4min, 75%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Computer Setup
|
||||
## If No Spot Appears
|
||||
|
||||
If KST4Contest runs on a separate computer (not the logging computer):
|
||||
### The Test Spot Does Not Reach the Logger
|
||||
|
||||
- Host in the logging software: IP of the KST4Contest computer (not `127.0.0.1`)
|
||||
- Same configuration as for the QSO UDP broadcast packets (see [Log Synchronisation](en-Log-Sync))
|
||||
Check:
|
||||
|
||||
- Is KST4Contest connected to the chat?
|
||||
- Is the local DX Cluster server enabled?
|
||||
- Does the logger use the same TCP port?
|
||||
- Does the logger use `127.0.0.1` when both programmes run locally?
|
||||
- Is a firewall blocking the connection?
|
||||
- Is the DX Cluster window or bandmap enabled in the logger?
|
||||
|
||||
### The Test Works, but Automatic Spots Are Missing
|
||||
|
||||
The TCP connection is then working in principle. At least one condition for the relevant chat situation was probably not met:
|
||||
|
||||
- no directed message between two other stations;
|
||||
- missing locator;
|
||||
- sender outside the maximum QRB;
|
||||
- direction outside the configured beamwidth;
|
||||
- no detected frequency.
|
||||
|
||||
KST4Contest deliberately does not send every frequency it finds to the logger. Otherwise, a feature intended to reduce distraction would quickly become a local spot generator with rather too much enthusiasm.
|
||||
|
||||
### A Manually Triggered Spot Is Missing
|
||||
|
||||
Check that a station is selected on the map and that it has a usable QRG. The local server must be enabled and at least one client must be connected. Directional-message geometry, maximum QRB and beamwidth are not prerequisites for the manual action.
|
||||
|
||||
### The Spot Appears on the Wrong Band
|
||||
|
||||
First check which frequencies were detected for the station during the previous 30 minutes. For a relative value, this station context takes priority over the global fallback.
|
||||
|
||||
If no current station context exists, check **Fallback band for relative QRG detection**. The fallback is used only when the band cannot be determined from a complete frequency or the sender's current context.
|
||||
|
||||
### The Logger Hides the Spot
|
||||
|
||||
Try a spotter callsign which differs from the contest callsign. Depending on the logger, spots from the local callsign may be filtered or handled specially. KST4Contest itself does not require the two callsigns to differ.
|
||||
|
||||
---
|
||||
|
||||
## Tested Logging Software
|
||||
## Tested Logging Programmes
|
||||
|
||||
- **UCXLog** ✓
|
||||
- **N1MM+** ✓
|
||||
The interface has been used with:
|
||||
|
||||
Further test reports are welcome – please send by email to DO5AMF.
|
||||
- UCXLog
|
||||
- N1MM+
|
||||
|
||||
Other loggers may work if they support a normal TCP connection to a DX Cluster server and accept conventional `DX de ...` spot lines.
|
||||
|
||||
Related reference pages:
|
||||
|
||||
- [User Interface](en-User-Interface)
|
||||
- [Features](en-Features)
|
||||
- [Configuration](en-Configuration)
|
||||
|
||||
+109
-24
@@ -6,45 +6,116 @@ Overview of all main features of KST4Contest.
|
||||
|
||||
---
|
||||
|
||||
## Sked Direction Highlighting
|
||||
## Directional Opportunities from Directed Messages
|
||||
|
||||
One of the core features: when a station makes a sked request **towards your direction**, it is highlighted **green and bold** in the user list.
|
||||
The ON4KST chat shows which station directs a message to which other station. It does not transmit the actual antenna direction. A directed message can nevertheless provide a useful indication during a contest: a station requesting, answering or preparing a sked will normally point its antenna at least approximately towards the station being addressed.
|
||||
|
||||
### How does it work?
|
||||
KST4Contest therefore evaluates directed messages between two other stations. The message does not have to be explicitly identified as a sked request. The relevant information is the sender, receiver and their locators.
|
||||
|
||||
The calculation is based on the following logic:
|
||||
### How Is the Direction Derived?
|
||||
|
||||
- When station A sends a sked request to station B, it is assumed that A is pointing its antenna towards B.
|
||||
- If the resulting direction from A to your own station is within half the beamwidth of your own antenna, A is highlighted.
|
||||
Assume that station A sends a directed message to station B:
|
||||
|
||||
**Example** (beamwidth 69°, half-angle 34.5°):
|
||||
1. KST4Contest calculates the direction from station A to station B.
|
||||
2. This direction is used as the likely antenna direction of station A.
|
||||
3. KST4Contest then calculates the direction from station A to the local station.
|
||||
4. The angular difference is compared with half of the configured antenna beamwidth.
|
||||
5. Station A must also be within the configured maximum QRB.
|
||||
|
||||
| Situation | Result for DO5AMF in JN49 |
|
||||
A configured beamwidth of `70°` therefore produces an assumed corridor of `35°` on either side of the direction from station A to station B.
|
||||
|
||||
| Example | Result |
|
||||
|---|---|
|
||||
| Sked from F5FEN → DM5M | ✅ Highlighted (F5FEN points towards DM5M, close to JN49) |
|
||||
| Sked from DM5M → F5FEN | ✅ Highlighted (DM5M replies towards F5FEN) |
|
||||
| F1DBN is uninvolved | ❌ No highlighting |
|
||||
| DO5AMF/P (different location) | ❌ No highlighting for sked reply |
|
||||
| Direction A → B: `120°`, direction A → local station: `145°` | Angular difference `25°`: directional opportunity detected |
|
||||
| Direction A → B: `120°`, direction A → local station: `165°` | Angular difference `45°`: outside the assumed corridor |
|
||||
| Locator of A or B is missing | Direction cannot be calculated |
|
||||
| A is outside the maximum QRB | No directional opportunity |
|
||||
|
||||
The calculation does not include topographic path calculations – this is a deliberate simplification. It may be added in a future version.
|
||||
### What Is Shown in the User List?
|
||||
|
||||
> Configuration: [Configuration – Antenna Beamwidth](en-Configuration#antenna-beamwidth)
|
||||
When a directional opportunity is detected, the sender's callsign appears in green and bold in the user list. Evening mode uses a lighter green. The receiver is not marked merely because it received the message; a reply in the opposite direction is evaluated as a separate message and therefore as a new case.
|
||||
|
||||

|
||||
|
||||
In the screenshot, DF0GEB sent a directed message to DN9APW and received a reply. KST4Contest detected the directional opportunity and highlighted DN9APW in the user list. The map is shown for context: the local station lies between the two stations and therefore receives the indication.
|
||||
|
||||
The mark remains visible for five minutes after the most recent matching message. Another matching message from the same station restarts that period. If the station sends another directed message which no longer satisfies the direction conditions, the mark is removed immediately.
|
||||
|
||||
If simple sound notifications are enabled, KST4Contest also plays a short indication when the directional opportunity is first detected. Further matching messages do not repeat the same sound while the station is already marked.
|
||||
|
||||
### What Does the Mark Mean – and What Does It Not Mean?
|
||||
|
||||
The calculation is a geometric derivation. It does not prove that station A is actually pointing its antenna towards station B. It also does not include terrain, current propagation, the real antenna pattern of the remote station or its rotator position.
|
||||
|
||||
ON4KST does not provide the beamwidth of the remote station. KST4Contest therefore uses the value configured for the local antenna as an approximation for station A. A value which is too large produces more possible directional opportunities with less practical meaning. A value which is too small may hide useful situations.
|
||||
|
||||
In plain terms: the green mark is a reasoned indication of a possible opportunity. It is neither a propagation forecast nor a guarantee of completing a QSO.
|
||||
|
||||
Configuration:
|
||||
|
||||
- [Antenna Beamwidth](en-Configuration#antenna-beamwidth)
|
||||
- [Default Maximum QRB](en-Configuration#default-maximum-qrb)
|
||||
|
||||
---
|
||||
|
||||
## Sked Direction Spots (Built-in DX Cluster)
|
||||
## Forwarding as a DX Cluster Spot
|
||||
|
||||
From **v1.23**: Direction warnings are forwarded as DX cluster spots to the logging software when a QRG is known. Details: [DX Cluster Server](en-DX-Cluster-Server).
|
||||
Since version 1.23, KST4Contest can forward a detected directional opportunity to the DX Cluster client of a logging programme. The local DX Cluster server must be enabled and a usable frequency must be known for the sender.
|
||||
|
||||
The frequency may come from an earlier message or may be detected for the first time in the message which triggers the opportunity. In both cases it is available when KST4Contest checks whether it can create a spot. The programme therefore does not forward every QRG found in the chat. Automatic spots require a geometrically matching directed message.
|
||||
|
||||
The five-minute user-list mark and the DX Cluster spot use the same direction calculation but have different lifetimes. The mark remains visible temporarily. The spot is generated immediately while the matching message is processed.
|
||||
|
||||
Setup, frequency handling and limitations: [Built-in DX Cluster Server](en-DX-Cluster-Server).
|
||||
|
||||
---
|
||||
|
||||
## QRG Detection (QRG Reading)
|
||||
## QRG Detection
|
||||
|
||||
KST4Contest processes every line of text flowing through the channel and automatically extracts **frequency references**. These are displayed in the user list in the **QRG column**.
|
||||
Frequencies are rarely written in a consistent form in the ON4KST chat. A station may first mention `432.088`, later write only `.100`, and use `qrg 120` in another message. The context is usually obvious to a human reader. Software still has to distinguish whether `120` is a frequency, a time, a distance or something else entirely.
|
||||
|
||||
Recognised formats: `144.205`, `432.088`, `.205` (with configured band assumption), etc.
|
||||
KST4Contest therefore evaluates the text of every public and directed chat message. A detected QRG is assigned to the sender and displayed in the user list's **QRG column**. The column shows the most recently detected frequency with at least three decimal places. A value stored internally as `144.21` is therefore displayed as `144.210`.
|
||||
|
||||
**Benefit**: Without asking, you can directly look up a station's calling frequency and decide whether a contact is possible.
|
||||
### Which Formats Are Recognised?
|
||||
|
||||
| Notation | Example | Processing |
|
||||
|---|---|---|
|
||||
| Complete frequency | `144.210`, `432,088`, `10368.100` | The frequency determines the band directly. |
|
||||
| Relative frequency with a dot or comma | `.210`, `,088` | The band is added from the station context or configured fallback. |
|
||||
| Three-digit frequency with text context | `qrg 210`, `freq is 210`, `on 210`, `210 MHz` | The number is treated as a relative frequency. |
|
||||
| Three-digit number without frequency context | `210`, `599`, `144` | The number is deliberately not accepted as a QRG. |
|
||||
|
||||
The final restriction prevents plausible-looking but incorrect results. With a fallback of `144 MHz`, a signal report of `599` could easily be turned into `144.599 MHz`. The result would be formally valid and operationally useless.
|
||||
|
||||
### How Is the Band of a Relative QRG Determined?
|
||||
|
||||
KST4Contest uses the following order:
|
||||
|
||||
1. If a suitable complete frequency has been detected for the same sender during the previous 30 minutes, KST4Contest uses its band.
|
||||
2. If several current bands are known, the most recently updated plausible band context is used.
|
||||
3. If no suitable station context exists, KST4Contest uses the band selected under **Fallback band for relative QRG detection**.
|
||||
|
||||
Example: the global fallback is `144 MHz`. A station first mentions `432.088` and writes `.100` a few minutes later. KST4Contest does not add the global fallback. It uses the more recent station context, producing `432.100 MHz`. If another station without previous band information writes `.100`, the result is `144.100 MHz`.
|
||||
|
||||
The fallback band really is the last resort. It is selected from the bands supported by KST4Contest and affects all QRG detection, not only the built-in DX Cluster server.
|
||||
|
||||
### Where Is a Detected QRG Used?
|
||||
|
||||
The most recently detected frequency appears in the user list. Its band context can also affect other functions, including:
|
||||
|
||||
- detection of the station's active bands;
|
||||
- the chat-member score and priority lists;
|
||||
- band-upgrade hints after a log entry;
|
||||
- frequency selection for skeds; and
|
||||
- a DX Cluster spot generated from a directional opportunity.
|
||||
|
||||
If the QRG first appears in the same message which triggers a directional opportunity, it is processed before the direction and spot checks. The resulting spot can therefore already use the frequency from that message.
|
||||
|
||||
Detection remains a text-based process. KST4Contest cannot prove that the station is still using the stated frequency or that an ambiguous value belongs to a different context. This is precisely why bare three-digit numbers without frequency-related text are no longer accepted.
|
||||
|
||||
Configuration and supported fallback bands: [Fallback Band for Relative QRG Detection](en-Configuration#fallback-band-for-relative-qrg-detection).
|
||||
|
||||
Use in a logger bandmap: [Built-in DX Cluster Server](en-DX-Cluster-Server).
|
||||
|
||||
---
|
||||
|
||||
@@ -167,7 +238,21 @@ Operation and layout of the filter bar: [User Interface – Filters](en-User-Int
|
||||
|
||||
## Coloured PM Rows (from v1.25)
|
||||
|
||||
New private messages appear in **red**. The colour fades every 30 seconds from yellow to white – like a rainbow fade. This makes it immediately clear how recent a message is.
|
||||
New incoming private messages use a series of green age highlights. The green becomes progressively more muted as the message gets older:
|
||||
|
||||
| Message age | Display |
|
||||
|---|---|
|
||||
| up to and including 30 seconds | first green level |
|
||||
| 31 to 60 seconds | second green level |
|
||||
| 61 to 90 seconds | third green level |
|
||||
| 91 to 120 seconds | fourth green level |
|
||||
| 121 to 180 seconds | fifth green level |
|
||||
| 181 to 300 seconds | sixth green level |
|
||||
| from 301 seconds | normal table colour |
|
||||
|
||||
The table refreshes the age display every five seconds, so a boundary may become visible only during the next refresh. No age class remains attached after five minutes; reused and empty table rows also return to their normal style.
|
||||
|
||||
Messages sent by the local station retain their separate highlight and do not use the green age scale.
|
||||
|
||||
*(Idea by IU3OAR, Gianluca Costantino – thank you!)*
|
||||
|
||||
@@ -214,9 +299,9 @@ Simultaneous login to **two chat categories** (e.g. 144 MHz and 432 MHz). Both c
|
||||
|
||||
## Dark Mode (from v1.26)
|
||||
|
||||
Toggle via: **Window → Use Dark Mode**
|
||||
Enable it through **Windows → Use dark mode design**. Use **Windows → Use default mode design** to return to the normal light colour scheme.
|
||||
|
||||
For individual colour adjustments: edit the CSS file (path in the program settings).
|
||||
The green private-message age scale remains available in both designs. Text colour, normal table colour and the separate highlight for locally sent messages follow the selected built-in design.
|
||||
|
||||
---
|
||||
|
||||
@@ -890,4 +975,4 @@ The check always uses the **primary screen**. It does not restore the previous p
|
||||
|
||||
The automatic size restriction currently applies to the main window only. The settings window, the separate cluster and QSO monitor window and other auxiliary windows continue to use their stored sizes without the same additional check against the primary screen.
|
||||
|
||||
In plain terms: the protection mainly prevents the central main window from becoming unusable after moving to a smaller display. It is not a complete window-position manager for a changing multi-monitor setup.
|
||||
In plain terms: the protection mainly prevents the central main window from becoming unusable after moving to a smaller display. It is not a complete window-position manager for a changing multi-monitor setup.
|
||||
|
||||
@@ -52,6 +52,8 @@ Downloads, supported operating systems and installation methods are described in
|
||||
|
||||
This manual describes the current stable release of KST4Contest.
|
||||
|
||||
The currently published Stable version is **v1.42.0**.
|
||||
|
||||
Functions that are only available in a Beta or Nightly build are marked accordingly. If no such note is present, the description applies to the stable release.
|
||||
|
||||
- [Download Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
|
||||
|
||||
@@ -82,7 +82,7 @@ The central table of all currently active chat users. Columns (depending on conf
|
||||
|
||||
The QRG column shows the frequency most recently detected for a station. Missing trailing zeros are added for display purposes, so `144.21`, for example, is shown as `144.210`. If KST4Contest detects frequencies on several bands in succession, the column shows the latest match. The internal band information may still contain several current bands for that station.
|
||||
|
||||
Relative frequency information is first combined with a band context from the same sender which is no more than 30 minutes old. Only if no such context exists does KST4Contest use the global fallback band. Detection rules, examples and limitations: [QRG Detection](en-Features#qrg-detection-qrg-reading).
|
||||
Relative frequency information is first combined with a band context from the same sender which is no more than 30 minutes old. Only if no such context exists does KST4Contest use the global fallback band. Detection rules, examples and limitations: [QRG Detection](en-Features#qrg-detection).
|
||||
|
||||
|
||||
### Worked, band and grid-square status
|
||||
|
||||
@@ -877,6 +877,12 @@ public class MessageBusManagementThread extends Thread {
|
||||
|| messageToProcess.getMessageText().isEmpty()) {
|
||||
// No processable data.
|
||||
} else {
|
||||
if (On4KstProtocol.isConnectionProbeResponse(
|
||||
messageToProcess.getMessageText())) {
|
||||
// DXQ is the internal response to the active connection probe.
|
||||
// Liveness was already recorded by the session manager.
|
||||
return;
|
||||
}
|
||||
|
||||
if (messageToProcess.getMessageText().startsWith(SRVR_LOGSTAT + "|")) {
|
||||
String[] logstatMessage =
|
||||
@@ -2177,8 +2183,11 @@ public class MessageBusManagementThread extends Thread {
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
|
||||
System.out.println(messageTextRaw.getMessageText() + " <- RXed"); // Stdout at
|
||||
// Console#######################################################TODO:Wichtig
|
||||
if (!On4KstProtocol.isConnectionProbeResponse(
|
||||
messageTextRaw.getMessageText())) {
|
||||
System.out.println(messageTextRaw.getMessageText() + " <- RXed"); // Stdout at
|
||||
// Console#######################################################TODO:Wichtig
|
||||
}
|
||||
|
||||
try {
|
||||
processRXMessage23001(messageTextRaw);
|
||||
@@ -2207,4 +2216,4 @@ public class MessageBusManagementThread extends Thread {
|
||||
System.out.println("Msgbusmgt: interrupt");
|
||||
this.interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ final class On4KstConnectionManager {
|
||||
static final long LOGIN_FALLBACK_MILLIS = 2_000L; //Login-Fallback
|
||||
static final long HANDSHAKE_TIMEOUT_MILLIS = 45_000L; //Handshake-Timeout
|
||||
static final long APPLICATION_HEARTBEAT_AFTER_MILLIS = 90_000L; //Application-Heartbeat
|
||||
/** Idle duration after which the server is asked for current DX data. */
|
||||
static final long CONNECTION_PROBE_AFTER_MILLIS = 180_000L; //Active connection probe
|
||||
static final long INBOUND_STALE_AFTER_MILLIS = 210_000L; //Stale-Timeout - time without rxed data
|
||||
static final List<Long> RECONNECT_DELAYS_MILLIS =
|
||||
List.of(2_000L, 5_000L, 10_000L, 20_000L, 30_000L); //Reconnect-Backoff if no connection possible
|
||||
@@ -178,7 +180,19 @@ final class On4KstConnectionManager {
|
||||
session.lastInboundMillis.set(now);
|
||||
session.lastProgressMillis.set(now);
|
||||
|
||||
String opcode = opcode(line);
|
||||
String opcode = On4KstProtocol.opcode(line);
|
||||
long probeResponseMillis = session.connectionProbe.acknowledge(now);
|
||||
if (probeResponseMillis >= 0L) {
|
||||
LOGGER.log(Level.INFO,
|
||||
"ON4KST connection probe confirmed: session {0}, "
|
||||
+ "received opcode {1}, response time {2} ms",
|
||||
new Object[] {
|
||||
sessionId,
|
||||
opcode,
|
||||
probeResponseMillis
|
||||
});
|
||||
}
|
||||
|
||||
if ("CK".equals(opcode)) {
|
||||
sendHeartbeat(session);
|
||||
}
|
||||
@@ -244,7 +258,15 @@ final class On4KstConnectionManager {
|
||||
new LinkedBlockingQueue<>();
|
||||
LinkedBlockingQueue<ChatMessage> transmitQueue =
|
||||
new LinkedBlockingQueue<>();
|
||||
Session session = new Session(token, socket, receiveQueue, transmitQueue);
|
||||
int mainCategory = controller.getChatPreferences()
|
||||
.getLoginChatCategoryMain()
|
||||
.getCategoryNumber();
|
||||
Session session = new Session(
|
||||
token,
|
||||
socket,
|
||||
receiveQueue,
|
||||
transmitQueue,
|
||||
mainCategory);
|
||||
|
||||
ReadThread readThread = new ReadThread(
|
||||
token, socket, receiveQueue, this::isActiveSession,
|
||||
@@ -252,8 +274,7 @@ final class On4KstConnectionManager {
|
||||
failure -> onConnectionFailure(token, failure));
|
||||
WriteThread writeThread = new WriteThread(
|
||||
token, socket, transmitQueue,
|
||||
controller.getChatPreferences().getLoginChatCategoryMain()
|
||||
.getCategoryNumber(),
|
||||
mainCategory,
|
||||
this::isActiveSession,
|
||||
failure -> onConnectionFailure(token, failure),
|
||||
controller::onOn4KstOutboundFrameRejected);
|
||||
@@ -535,6 +556,29 @@ final class On4KstConnectionManager {
|
||||
session.transmitQueue.offer(heartbeat);
|
||||
}
|
||||
|
||||
private void sendConnectionProbe(
|
||||
Session session,
|
||||
long now,
|
||||
long inboundIdle
|
||||
) {
|
||||
if (session == null || !isActiveSession(session.id)
|
||||
|| !session.connectionProbe.tryStart(now)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.log(Level.INFO,
|
||||
"Sending ON4KST connection probe: session {0}, main category "
|
||||
+ "{1}, inbound idle {2} seconds",
|
||||
new Object[] {
|
||||
session.id,
|
||||
session.mainCategory,
|
||||
inboundIdle / 1_000L
|
||||
});
|
||||
sendControl(
|
||||
session,
|
||||
On4KstProtocol.connectionProbe(session.mainCategory));
|
||||
}
|
||||
|
||||
private void onConnectionFailure(long sessionId, Throwable failure) {
|
||||
scheduler.execute(() -> failSession(sessionId, failure));
|
||||
}
|
||||
@@ -634,18 +678,47 @@ final class On4KstConnectionManager {
|
||||
return;
|
||||
}
|
||||
|
||||
long inboundIdle = now - session.lastInboundMillis.get();
|
||||
if (inboundIdle > INBOUND_STALE_AFTER_MILLIS) {
|
||||
failSession(session.id,
|
||||
new SocketException("No ON4KST data received for "
|
||||
+ inboundIdle / 1_000L + " seconds"));
|
||||
long lastInboundMillis = session.lastInboundMillis.get();
|
||||
long inboundIdle = now - lastInboundMillis;
|
||||
IdleAction idleAction = determineIdleAction(
|
||||
inboundIdle,
|
||||
session.lastHeartbeatMillis.get() >= lastInboundMillis,
|
||||
session.connectionProbe.isOutstanding());
|
||||
|
||||
if (session.lastInboundMillis.get() != lastInboundMillis) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (inboundIdle > APPLICATION_HEARTBEAT_AFTER_MILLIS
|
||||
&& session.lastHeartbeatMillis.get()
|
||||
< session.lastInboundMillis.get()) {
|
||||
sendHeartbeat(session);
|
||||
switch (idleAction) {
|
||||
case TIMEOUT -> {
|
||||
if (session.lastInboundMillis.get() != lastInboundMillis) {
|
||||
return;
|
||||
}
|
||||
long probeWaitMillis =
|
||||
session.connectionProbe.responseWaitMillis(now);
|
||||
if (probeWaitMillis >= 0L) {
|
||||
LOGGER.log(Level.WARNING,
|
||||
"ON4KST connection probe timed out: session "
|
||||
+ "{0}, main category {1}, no response "
|
||||
+ "for {2} ms, inbound idle {3} seconds; "
|
||||
+ "reconnecting",
|
||||
new Object[] {
|
||||
session.id,
|
||||
session.mainCategory,
|
||||
probeWaitMillis,
|
||||
inboundIdle / 1_000L
|
||||
});
|
||||
}
|
||||
failSession(session.id,
|
||||
new SocketException("No ON4KST data received for "
|
||||
+ inboundIdle / 1_000L + " seconds"));
|
||||
}
|
||||
case CONNECTION_PROBE ->
|
||||
sendConnectionProbe(session, now, inboundIdle);
|
||||
case HEARTBEAT -> sendHeartbeat(session);
|
||||
case NONE -> {
|
||||
// The session is active or already has the required idle action.
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
LOGGER.log(Level.WARNING,
|
||||
@@ -653,6 +726,28 @@ final class On4KstConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects at most one maintenance action for the current inbound idle phase.
|
||||
*/
|
||||
static IdleAction determineIdleAction(
|
||||
long inboundIdleMillis,
|
||||
boolean heartbeatSentForIdlePhase,
|
||||
boolean probeOutstanding
|
||||
) {
|
||||
if (inboundIdleMillis > INBOUND_STALE_AFTER_MILLIS) {
|
||||
return IdleAction.TIMEOUT;
|
||||
}
|
||||
if (inboundIdleMillis >= CONNECTION_PROBE_AFTER_MILLIS
|
||||
&& !probeOutstanding) {
|
||||
return IdleAction.CONNECTION_PROBE;
|
||||
}
|
||||
if (inboundIdleMillis > APPLICATION_HEARTBEAT_AFTER_MILLIS
|
||||
&& !heartbeatSentForIdlePhase) {
|
||||
return IdleAction.HEARTBEAT;
|
||||
}
|
||||
return IdleAction.NONE;
|
||||
}
|
||||
|
||||
private void validateConfiguration() {
|
||||
ChatPreferences preferences = controller.getChatPreferences();
|
||||
On4KstProtocol.login(
|
||||
@@ -782,15 +877,6 @@ final class On4KstConnectionManager {
|
||||
}
|
||||
}
|
||||
|
||||
private String opcode(String line) {
|
||||
if (line == null) {
|
||||
return "";
|
||||
}
|
||||
int separator = line.indexOf('|');
|
||||
return (separator < 0 ? line : line.substring(0, separator))
|
||||
.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String describeFailure(Throwable failure) {
|
||||
if (failure == null) {
|
||||
return "unknown error";
|
||||
@@ -808,12 +894,15 @@ final class On4KstConnectionManager {
|
||||
private final Socket socket;
|
||||
private final LinkedBlockingQueue<ChatMessage> receiveQueue;
|
||||
private final LinkedBlockingQueue<ChatMessage> transmitQueue;
|
||||
private final int mainCategory;
|
||||
private final long connectedMillis = System.currentTimeMillis();
|
||||
private final AtomicLong lastInboundMillis =
|
||||
new AtomicLong(connectedMillis);
|
||||
private final AtomicLong lastProgressMillis =
|
||||
new AtomicLong(connectedMillis);
|
||||
private final AtomicLong lastHeartbeatMillis = new AtomicLong();
|
||||
private final ConnectionProbeState connectionProbe =
|
||||
new ConnectionProbeState();
|
||||
private final Map<Integer, Map<String, ChatMember>> initialMembers =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@@ -831,12 +920,45 @@ final class On4KstConnectionManager {
|
||||
long id,
|
||||
Socket socket,
|
||||
LinkedBlockingQueue<ChatMessage> receiveQueue,
|
||||
LinkedBlockingQueue<ChatMessage> transmitQueue
|
||||
LinkedBlockingQueue<ChatMessage> transmitQueue,
|
||||
int mainCategory
|
||||
) {
|
||||
this.id = id;
|
||||
this.socket = socket;
|
||||
this.receiveQueue = receiveQueue;
|
||||
this.transmitQueue = transmitQueue;
|
||||
this.mainCategory = mainCategory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Maintenance action selected by the session monitor. */
|
||||
enum IdleAction {
|
||||
NONE,
|
||||
HEARTBEAT,
|
||||
CONNECTION_PROBE,
|
||||
TIMEOUT
|
||||
}
|
||||
|
||||
/** Tracks one outstanding liveness probe for the complete TCP session. */
|
||||
static final class ConnectionProbeState {
|
||||
private final AtomicLong sentMillis = new AtomicLong();
|
||||
|
||||
boolean tryStart(long now) {
|
||||
return now > 0L && sentMillis.compareAndSet(0L, now);
|
||||
}
|
||||
|
||||
long acknowledge(long now) {
|
||||
long sent = sentMillis.getAndSet(0L);
|
||||
return sent == 0L ? -1L : Math.max(0L, now - sent);
|
||||
}
|
||||
|
||||
boolean isOutstanding() {
|
||||
return sentMillis.get() > 0L;
|
||||
}
|
||||
|
||||
long responseWaitMillis(long now) {
|
||||
long sent = sentMillis.get();
|
||||
return sent == 0L ? -1L : Math.max(0L, now - sent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,27 @@ final class On4KstProtocol {
|
||||
+ "|0|";
|
||||
}
|
||||
|
||||
/** Builds the active liveness probe for the session's main chat. */
|
||||
static String connectionProbe(int category) {
|
||||
return "RDXQ|" + category(category) + "|";
|
||||
}
|
||||
|
||||
/** Returns whether a server frame is the expected liveness-probe response. */
|
||||
static boolean isConnectionProbeResponse(String frame) {
|
||||
return "DXQ".equals(opcode(frame));
|
||||
}
|
||||
|
||||
/** Extracts and normalizes the opcode without exposing the remaining frame. */
|
||||
static String opcode(String frame) {
|
||||
if (frame == null) {
|
||||
return "";
|
||||
}
|
||||
int separator = frame.indexOf('|');
|
||||
return (separator < 0 ? frame : frame.substring(0, separator))
|
||||
.trim()
|
||||
.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/** Builds a category-qualified locator command after validating Maidenhead syntax. */
|
||||
static String setLocator(int category, String locator) {
|
||||
return command(category, "/SETLOC " + locator(locator));
|
||||
@@ -189,4 +210,4 @@ final class On4KstProtocol {
|
||||
}
|
||||
return category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ import javafx.scene.control.TableRow; // For the priority coloring
|
||||
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.*;
|
||||
@@ -3754,67 +3753,54 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
});
|
||||
|
||||
//experimental row coloring on new private messages (and recolouring if they get older)
|
||||
// Color new private messages and restore the normal row style after five minutes.
|
||||
tbl_privateMSGTable.setRowFactory(tv -> new TableRow<ChatMessage>() {
|
||||
@Override
|
||||
protected void updateItem(ChatMessage item, boolean empty) {
|
||||
protected void updateItem(
|
||||
final ChatMessage item,
|
||||
final boolean empty
|
||||
) {
|
||||
super.updateItem(item, empty);
|
||||
try {
|
||||
if (item != null) {
|
||||
|
||||
if (item.getSender().getCallSign().equals(chatcontroller.getChatPreferences().getStn_loginCallSign())) {
|
||||
PseudoClass foo = PseudoClass.getPseudoClass("messageHighlightOwn-column");
|
||||
getStyleClass().removeAll(
|
||||
PrivateMessageRowStyleResolver.knownStyleClasses()
|
||||
);
|
||||
|
||||
// System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> pm row style " + this.getStyleClass());
|
||||
tv.setStyle(null);
|
||||
if (empty || item == null || item.getSender() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this.getStyleClass().clear();
|
||||
this.getStyleClass().add("messageHighlightOwn-column"); //add new special colored css reference
|
||||
// setStyle("-fx-background-color: #ADD8E6;");
|
||||
} else {
|
||||
final String ownCallsign = chatcontroller
|
||||
.getChatPreferences()
|
||||
.getStn_loginCallSign();
|
||||
|
||||
// System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> pm row style " + this.getStyleClass());
|
||||
final boolean ownMessage = Objects.equals(
|
||||
item.getSender().getCallSign(),
|
||||
ownCallsign
|
||||
);
|
||||
|
||||
if (( (new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime())) <= 30 ) { //after 30 seconds change color
|
||||
// setStyle("-fx-background-color: #FF6F00;");
|
||||
this.getStyleClass().clear();
|
||||
this.getStyleClass().add("messageHighlight30-column"); //add new special colored css reference
|
||||
final String styleClass;
|
||||
|
||||
} else if (( (new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime())) <= 60 ) { //after 60 seconds change color
|
||||
this.getStyleClass().clear();
|
||||
this.getStyleClass().add("messageHighlight60-column"); //add new special colored css reference
|
||||
// setStyle("-fx-background-color: #FFB300;");
|
||||
} else if (( (new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime())) <= 90 ) { //after 90 seconds change color
|
||||
this.getStyleClass().clear();
|
||||
this.getStyleClass().add("messageHighlight90-column"); //add new special colored css reference
|
||||
// setStyle("-fx-background-color: #FFB300;");
|
||||
} else if (( (new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime())) <= 120 ) { //after 120 seconds change color
|
||||
this.getStyleClass().clear();
|
||||
this.getStyleClass().add("messageHighlight120-column"); //add new special colored css reference
|
||||
// setStyle("-fx-background-color: #FFD54F;");
|
||||
} else if (( (new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime())) <= 180 ) { //after 180 seconds change color
|
||||
this.getStyleClass().clear();
|
||||
this.getStyleClass().add("messageHighlight180-column"); //add new special colored css reference
|
||||
// setStyle("-fx-background-color: #FFD54F;");
|
||||
} else if (( (new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime())) <= 300 ) { //after 300 seconds change color
|
||||
this.getStyleClass().clear();
|
||||
this.getStyleClass().add("messageHighlight300-column"); //add new special colored css reference
|
||||
// setStyle("-fx-background-color: #FFF176;");
|
||||
} else
|
||||
{
|
||||
if (ownMessage) {
|
||||
styleClass = PrivateMessageRowStyleResolver
|
||||
.resolveStyleClass(true, 0);
|
||||
} else {
|
||||
try {
|
||||
final long ageSeconds = new Utils4KST()
|
||||
.time_generateCurrentEpochTime()
|
||||
- Long.parseLong(
|
||||
item.getMessageGeneratedTime()
|
||||
);
|
||||
|
||||
// setStyle("");
|
||||
}
|
||||
}
|
||||
|
||||
// switch (Integer.parseInt("" + (((new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime()))))) {
|
||||
// case int i
|
||||
// } //TODO: update to JDK21 or bigger, then a range case is possible, improves speed maybe
|
||||
styleClass = PrivateMessageRowStyleResolver
|
||||
.resolveStyleClass(false, ageSeconds);
|
||||
} catch (NumberFormatException exception) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// System.out.println("---> messagealter ---> " + (((new Utils4KST().time_generateCurrentEpochTime())) - (Long.parseLong(item.getMessageGeneratedTime()))));
|
||||
} catch (Exception e) {
|
||||
;
|
||||
if (styleClass != null) {
|
||||
getStyleClass().add(styleClass);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12952,4 +12938,4 @@ class CheckBoxTableCell<S, T> extends TableCell<S, T> {
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Selects the CSS style class used for a private-message table row.
|
||||
*/
|
||||
public final class PrivateMessageRowStyleResolver {
|
||||
|
||||
/** Style used for messages sent by the local station. */
|
||||
public static final String OWN_STYLE_CLASS =
|
||||
"messageHighlightOwn-column";
|
||||
|
||||
/** Upper inclusive age bounds for the private-message color levels. */
|
||||
private static final List<Long> AGE_LIMITS = List.of(
|
||||
30L,
|
||||
60L,
|
||||
90L,
|
||||
120L,
|
||||
180L,
|
||||
300L
|
||||
);
|
||||
|
||||
/** Style classes corresponding to the configured age bounds. */
|
||||
private static final List<String> AGE_STYLES = List.of(
|
||||
"messageHighlight30-column",
|
||||
"messageHighlight60-column",
|
||||
"messageHighlight90-column",
|
||||
"messageHighlight120-column",
|
||||
"messageHighlight180-column",
|
||||
"messageHighlight300-column"
|
||||
);
|
||||
|
||||
/** All private-message row classes managed by the row factory. */
|
||||
private static final List<String> MANAGED_STYLES = List.of(
|
||||
OWN_STYLE_CLASS,
|
||||
AGE_STYLES.get(0),
|
||||
AGE_STYLES.get(1),
|
||||
AGE_STYLES.get(2),
|
||||
AGE_STYLES.get(3),
|
||||
AGE_STYLES.get(4),
|
||||
AGE_STYLES.get(5)
|
||||
);
|
||||
|
||||
private PrivateMessageRowStyleResolver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the complete set of private-message row classes managed by the
|
||||
* row factory.
|
||||
*
|
||||
* @return immutable list of managed style classes
|
||||
*/
|
||||
public static List<String> knownStyleClasses() {
|
||||
return MANAGED_STYLES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the private-message row class for the supplied message age.
|
||||
*
|
||||
* @param ownMessage whether the message was sent by the local station
|
||||
* @param ageSeconds message age in seconds
|
||||
* @return managed CSS class, or {@code null} after the five-minute window
|
||||
*/
|
||||
public static String resolveStyleClass(
|
||||
final boolean ownMessage,
|
||||
final long ageSeconds
|
||||
) {
|
||||
String styleClass = null;
|
||||
if (ownMessage) {
|
||||
styleClass = OWN_STYLE_CLASS;
|
||||
} else {
|
||||
for (int index = 0; index < AGE_LIMITS.size(); index++) {
|
||||
if (ageSeconds <= AGE_LIMITS.get(index)) {
|
||||
styleClass = AGE_STYLES.get(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return styleClass;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
class On4KstConnectionProbeTest {
|
||||
|
||||
@Test
|
||||
void buildsMainChatProbeAndAcceptsExpectedResponse() {
|
||||
assertEquals("RDXQ|2|", On4KstProtocol.connectionProbe(2));
|
||||
assertTrue(On4KstProtocol.isConnectionProbeResponse("DXQ|2|data|"));
|
||||
assertFalse(On4KstProtocol.isConnectionProbeResponse(
|
||||
"CH|2|123|DL1ABC|Name|0|text|0|"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectsHeartbeatProbeAndTimeoutAtIdleBoundaries() {
|
||||
assertEquals(
|
||||
On4KstConnectionManager.IdleAction.NONE,
|
||||
idleAction(90_000L, false, false));
|
||||
assertEquals(
|
||||
On4KstConnectionManager.IdleAction.HEARTBEAT,
|
||||
idleAction(90_001L, false, false));
|
||||
assertEquals(
|
||||
On4KstConnectionManager.IdleAction.NONE,
|
||||
idleAction(179_999L, true, false));
|
||||
assertEquals(
|
||||
On4KstConnectionManager.IdleAction.CONNECTION_PROBE,
|
||||
idleAction(180_000L, true, false));
|
||||
assertEquals(
|
||||
On4KstConnectionManager.IdleAction.NONE,
|
||||
idleAction(210_000L, true, true));
|
||||
assertEquals(
|
||||
On4KstConnectionManager.IdleAction.TIMEOUT,
|
||||
idleAction(210_001L, true, true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneSessionProbeIsAcknowledgedByAnyInboundTraffic() {
|
||||
On4KstConnectionManager.ConnectionProbeState probe =
|
||||
new On4KstConnectionManager.ConnectionProbeState();
|
||||
|
||||
assertTrue(probe.tryStart(1_000L));
|
||||
assertFalse(probe.tryStart(1_001L),
|
||||
"A second category must not start another session probe");
|
||||
assertTrue(probe.isOutstanding());
|
||||
|
||||
assertEquals(250L, probe.acknowledge(1_250L));
|
||||
assertFalse(probe.isOutstanding());
|
||||
assertEquals(-1L, probe.acknowledge(1_500L));
|
||||
|
||||
assertTrue(probe.tryStart(2_000L),
|
||||
"New inbound activity starts a new idle phase");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void writerUsesExactCrLfForHeartbeatAndConnectionProbe() throws Exception {
|
||||
byte[] expected = "\r\nRDXQ|2|\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
CompletableFuture<byte[]> received = CompletableFuture.supplyAsync(() -> {
|
||||
try (Socket accepted = server.accept()) {
|
||||
accepted.setSoTimeout(2_000);
|
||||
return accepted.getInputStream().readNBytes(expected.length);
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
});
|
||||
|
||||
try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {
|
||||
LinkedBlockingQueue<ChatMessage> queue =
|
||||
new LinkedBlockingQueue<>();
|
||||
AtomicBoolean active = new AtomicBoolean(true);
|
||||
WriteThread writer = new WriteThread(
|
||||
11L,
|
||||
client,
|
||||
queue,
|
||||
2,
|
||||
ignored -> active.get(),
|
||||
ignored -> { },
|
||||
ignored -> { });
|
||||
writer.start();
|
||||
|
||||
queue.add(serverFrame(""));
|
||||
queue.add(serverFrame(On4KstProtocol.connectionProbe(2)));
|
||||
|
||||
assertArrayEquals(
|
||||
expected,
|
||||
received.get(2, TimeUnit.SECONDS));
|
||||
|
||||
active.set(false);
|
||||
writer.interrupt();
|
||||
writer.join(Duration.ofSeconds(2).toMillis());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private On4KstConnectionManager.IdleAction idleAction(
|
||||
long inboundIdleMillis,
|
||||
boolean heartbeatSent,
|
||||
boolean probeOutstanding
|
||||
) {
|
||||
return On4KstConnectionManager.determineIdleAction(
|
||||
inboundIdleMillis,
|
||||
heartbeatSent,
|
||||
probeOutstanding);
|
||||
}
|
||||
|
||||
private ChatMessage serverFrame(String text) {
|
||||
ChatMessage message = new ChatMessage();
|
||||
message.setMessageDirectedToServer(true);
|
||||
message.setMessageText(text);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.view.PrivateMessageRowStyleResolver;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class PrivateMessageRowStyleResolverTest {
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"0, messageHighlight30-column",
|
||||
"30, messageHighlight30-column",
|
||||
"31, messageHighlight60-column",
|
||||
"60, messageHighlight60-column",
|
||||
"61, messageHighlight90-column",
|
||||
"90, messageHighlight90-column",
|
||||
"91, messageHighlight120-column",
|
||||
"120, messageHighlight120-column",
|
||||
"121, messageHighlight180-column",
|
||||
"180, messageHighlight180-column",
|
||||
"181, messageHighlight300-column",
|
||||
"300, messageHighlight300-column"
|
||||
})
|
||||
void selectsAgeStyleClassAtEveryBoundary(
|
||||
long ageSeconds,
|
||||
String expectedStyleClass
|
||||
) {
|
||||
assertEquals(
|
||||
expectedStyleClass,
|
||||
PrivateMessageRowStyleResolver.resolveStyleClass(
|
||||
false,
|
||||
ageSeconds
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsNoAgeStyleClassAfterFiveMinutes() {
|
||||
assertNull(
|
||||
PrivateMessageRowStyleResolver.resolveStyleClass(
|
||||
false,
|
||||
301
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsOwnMessageStyleAfterFiveMinutes() {
|
||||
assertEquals(
|
||||
PrivateMessageRowStyleResolver.OWN_STYLE_CLASS,
|
||||
PrivateMessageRowStyleResolver.resolveStyleClass(
|
||||
true,
|
||||
301
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ icon: 📡
|
||||
category: Logger Integration
|
||||
since: "1.23"
|
||||
summary: Forward detected directional opportunities and their known frequencies as local DX Cluster spots to compatible contest loggers.
|
||||
description: KST4Contest provides a local TCP DX Cluster server which turns selected ON4KST direction and frequency information into spots for a logger bandmap.
|
||||
description: KST4Contest provides a local TCP DX Cluster server which forwards automatic directional opportunities or a manually selected map station to a logger bandmap.
|
||||
tagsList:
|
||||
- DX Cluster
|
||||
- bandmap
|
||||
@@ -29,9 +29,9 @@ The purpose is practical: when a station appears to be pointing in the local dir
|
||||
|
||||
That is the entire idea. The function is a bridge between the KST4Contest analysis and the logger, not another source of general DX traffic.
|
||||
|
||||
## When is a spot generated?
|
||||
## Automatic and manual spots
|
||||
|
||||
A real spot is generated only when all of the following conditions are met:
|
||||
An automatic spot is generated only when all of the following conditions are met:
|
||||
|
||||
1. A directed message between two other stations has been detected.
|
||||
2. Valid locators are available for the sender and receiver.
|
||||
@@ -39,10 +39,12 @@ A real spot is generated only when all of the following conditions are met:
|
||||
4. The local station lies inside the assumed antenna corridor of the sender.
|
||||
5. A usable frequency is known for the sender.
|
||||
6. The local DX Cluster server is enabled.
|
||||
7. At least one DX-Cluster client is connected to the server.
|
||||
|
||||
KST4Contest deliberately does not forward every frequency mentioned in the chat. Otherwise, a function intended to reduce distraction would produce its own local spot flood.
|
||||
|
||||
A spot can also be triggered deliberately. Select a station on the station map and use **Trigger cluster spot** in the detail panel. This manual action does not require a directed message, a match with the maximum QRB or a match with the configured antenna beamwidth. It uses the selected station and its known QRG directly.
|
||||
|
||||
For either route, at least one DX-Cluster client must be connected to receive the spot. Both automatic and manual spots remain inside the local or trusted station network; KST4Contest does not forward them to a public DX Cluster.
|
||||
|
||||
## How is the directional opportunity derived?
|
||||
|
||||
Assume that station A sends a directed message to station B. KST4Contest uses the direction from A to B as an approximation of the current antenna direction of station A.
|
||||
@@ -124,22 +126,23 @@ A bare three-digit value is accepted only when the surrounding text identifies i
|
||||
|
||||
## What does the spot contain?
|
||||
|
||||
The generated spot contains:
|
||||
Every generated spot contains:
|
||||
|
||||
- the configured spotter callsign;
|
||||
- the normalised frequency;
|
||||
- the complete callsign of the detected station;
|
||||
- the sender's locator;
|
||||
- up to two optional AirScout entries; and
|
||||
- the complete visible callsign of the detected or selected station;
|
||||
- the locator of that station;
|
||||
- the current UTC time.
|
||||
|
||||
Automatically generated directional spots can additionally include up to two current AirScout entries. A manually triggered map spot uses the selected station's locator without this optional addition.
|
||||
|
||||
An example comment with AirScout information may look like this:
|
||||
|
||||
```text
|
||||
JN49GL , AP: 1min, 100%; 4min, 75%
|
||||
```
|
||||
|
||||
AirScout information is optional. A missing AirScout response does not prevent the spot from being sent.
|
||||
AirScout information is optional. A missing AirScout response does not prevent an automatic directional spot from being sent.
|
||||
|
||||
> AP-independent spot creation, corrected sender-locator handling and band-generic frequency conversion are included from v1.42 onwards.
|
||||
|
||||
@@ -199,6 +202,8 @@ A successful test confirms that at least one client received the generated spot.
|
||||
- Was a valid frequency known?
|
||||
- Did a station-specific band context change the relative QRG?
|
||||
|
||||
For a manual spot, check that the station remains selected on the map and has a usable QRG. Maximum QRB, beamwidth and directed-message geometry are not prerequisites for **Trigger cluster spot**.
|
||||
|
||||
## What the spot means — and what it does not
|
||||
|
||||
The spot means that KST4Contest detected a plausible directional opportunity and knew a frequency for the sender.
|
||||
@@ -226,6 +231,6 @@ Other loggers may work if they can open a normal TCP connection to a DX Cluster
|
||||
|
||||
[Read the complete setup and troubleshooting section in the manual.](/manual/en/dx-cluster-server/)
|
||||
|
||||
[Read how directional opportunities are derived.](/manual/en/features/#sked-direction-highlighting)
|
||||
[Read how directional opportunities are derived.](/manual/en/features/#directional-opportunities-from-directed-messages)
|
||||
|
||||
[Read how relative QRG information is configured.](/manual/en/configuration/#fallback-band-for-relative-qrg-detection)
|
||||
[Read how relative QRG information is configured.](/manual/en/configuration/#fallback-band-for-relative-qrg-detection)
|
||||
|
||||
Reference in New Issue
Block a user