31 Commits
Author SHA1 Message Date
Marc Froehlich 7ee50267ec Document v1.43 and fix release metadata 2026-09-03 23:42:17 +02:00
Marc Froehlich 113e843111 updated version strings now corrected 2026-09-03 22:31:05 +02:00
github-actions[bot] 79161d2afa chore: update AUR packages to v1.43.0 [skip ci] 2026-09-03 20:23:04 +00:00
Marc Froehlich 3ed1cad5ab updated version strings 2026-09-03 22:20:26 +02:00
Marc Froehlich 75fe45b50b updated version string 2026-09-03 22:16:23 +02:00
Marc Froehlich 53555cbe69 Add optional station map clustering toggle (solves #79) and added documentation 2026-09-03 01:26:19 +02:00
Marc Froehlich 595fb84362 Persist table layouts just after changing the real used sizes and show truncated cell tooltips in the whole applications tables v2 2026-09-03 00:26:16 +02:00
Marc Froehlich 9eb0550106 Persist table layouts just after changing the real used sizes and show truncated cell tooltips in the whole applications tables 2026-09-02 23:45:19 +02:00
Marc Froehlich 51aa04bfb5 Fix DX Cluster spot formatting - emit fixed 75-character DXSpider-compatible lines - align callsign, comment, and UTC fields - support frequencies up to 24 GHz - reject overlong callsigns instead of truncating them - add protocol tests and update documentation Fixes #86 2026-09-02 20:03:55 +02:00
Marc Froehlich 6b0d699a98 Fix compact QRG parsing and initial worked-state restore - parse digit-only full frequencies across supported bands - restore persisted Worked state before publishing initial user lists - add regression tests and update documentation Fixes #85 2026-09-02 19:17:52 +02:00
Marc Froehlich 6b29ffe3ba refactored the ugly switch case statement in logged-band-recognition 2026-08-31 00:15:26 +02:00
Marc Froehlich 08b109106e manual: corrected behaviour description of simplelogfile, Airscout, MYQTF, corrected some typos 2026-08-29 10:07:24 +02:00
Marc Froehlich 21b2d9970a manual: decribed SimpleLogFile-parser 2026-08-29 00:02:57 +02:00
Marc Froehlich 94fc13e3c3 refactoring of the SimpleLogFile-parser 2026-08-28 23:19:56 +02:00
Marc Froehlich d475c6b2c4 manual: wording corrected for the settings at different places 2026-08-28 22:22:35 +02:00
Marc Froehlich 211be6081d manual: described dual chat 2026-08-28 22:08:31 +02:00
Marc Froehlich b550d79b4b manual: described pm catching better... 2026-08-28 01:14:46 +02:00
Marc Froehlich 352cdcceb2 manual: described pm catching 2026-08-28 01:02:49 +02:00
Marc Froehlich 905ce5766e webseite: described global message views 2026-08-28 00:46:35 +02:00
Marc Froehlich 2803f7d8e3 webseite: described qrg synch 2026-08-28 00:29:01 +02:00
Marc Froehlich 4a8e08a331 website: described filters better now 2026-08-28 00:14:31 +02:00
Marc Froehlich a24db90ad2 website: added qrg stuff to the features list and linked it to other articles 2026-08-28 00:03:04 +02:00
Marc Froehlich d91b119112 website: added map to the features and described g1ybb workflow 2026-08-27 23:50:27 +02:00
Marc Froehlich 345c4adfc3 website: added direction opportunities to the features and added neccessary picture 2026-08-27 23:27:00 +02:00
Marc Froehlich 0e8cd06e43 website: added Band opportunities as a feature 2026-08-27 23:01:04 +02:00
Marc Froehlich 7ff7248e7d Fixed KST4Contest caused disconnect on getting no activities of ON4KST chatservers if no new lines arriving 2026-08-27 01:26:50 +02:00
Marc Froehlich 3193e4ac73 manual and code: configfile path corrected 2026-08-27 00:23:09 +02:00
Marc Froehlich 3ec6cab46a manual and code: fixed colour handling of the messages and better descripted it in the manual 2026-08-27 00:02:02 +02:00
Marc Froehlich b280f1b0db manual: overhaul features, config, dxcluster server 2026-08-26 22:57:49 +02:00
Marc Froehlich 7fe33613e9 manual: overhaul cluster-server and feature list 2026-08-26 22:43:16 +02:00
Marc Froehlich 9378bf2afd manual: overhaul en-home and changelog de and en 2026-08-26 22:19:27 +02:00
92 changed files with 5462 additions and 1413 deletions
+38 -38
View File
@@ -786,45 +786,45 @@ jobs:
release-assets/macos/KST4Contest-${{ github.ref_name }}-macos-*.dmg,
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
# The update feed is generated only after GitHub has published the
# release. Otherwise the Releases API cannot return the release notes
# belonging to the tag which triggered this workflow.
- name: Set up Node.js for website build
uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: website/package-lock.json
- name: Build and validate website after release publication
working-directory: website
run: |
npm ci
npm test
npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# The update feed is generated only after GitHub has published the
# release. Otherwise the Releases API cannot return the release notes
# belonging to the tag which triggered this workflow.
- name: Set up Node.js for website build
uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: website/package-lock.json
- name: Verify Stable release in update feed
if: ${{ !startsWith(github.ref_name, 'beta-') }}
working-directory: website
run: npm run validate:version-info
env:
EXPECTED_STABLE_VERSION: ${{ github.ref_name }}
- name: Build and validate website after release publication
working-directory: website
run: |
npm ci
npm test
npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Attach verified version info to tagged release
run: >-
gh release upload "${GITHUB_REF_NAME}"
website/_site/kst4ContestVersionInfo.xml
--clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Verify Stable release in update feed
if: ${{ !startsWith(github.ref_name, 'beta-') }}
working-directory: website
run: npm run validate:version-info
env:
EXPECTED_STABLE_VERSION: ${{ github.ref_name }}
- name: Upload verified website artifact
uses: actions/upload-artifact@v4.3.4
with:
name: kst4contest-website-${{ github.ref_name }}
path: website/_site/
if-no-files-found: error
retention-days: 14
- name: Attach verified version info to tagged release
run: >-
gh release upload "${GITHUB_REF_NAME}"
website/_site/kst4ContestVersionInfo.xml
--clobber
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload verified website artifact
uses: actions/upload-artifact@v4.3.4
with:
name: kst4contest-website-${{ github.ref_name }}
path: website/_site/
if-no-files-found: error
retention-days: 14
+67 -3
View File
@@ -1,6 +1,6 @@
# KST4Contest Project Context
Last reviewed: 2026-08-25
Last reviewed: 2026-09-03
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.
@@ -21,8 +21,8 @@ KST4Contest is a Java/JavaFX desktop client for ON4KST chat focused on VHF/UHF/m
### Chat identity
- Full callsign variants can be distinct chat-member identities.
- Category is part of chat identity.
- For remote chat participants, the complete visible callsign plus category forms the chat-member identity.
- Full callsign variants can therefore be distinct chat-member identities.
- Base-call normalization is permitted only for explicitly base-call-wide functions.
- Worked status is shared across suffix variants of the same base call.
- Monitoring a variant such as `DN9APW-2` or `DN9APW-70` intentionally monitors the base call `DN9APW`.
@@ -34,6 +34,7 @@ KST4Contest is a Java/JavaFX desktop client for ON4KST chat focused on VHF/UHF/m
- `NOT-QRV` overrides positive inferred band-availability hints.
- Unknown/missing frequency, QRB, QTF or similar external data must remain unavailable rather than becoming a fabricated zero/default.
- Features that depend on frequency should use the current/actual QRG according to current implemented rules; do not silently revert to a fixed 144 MHz default.
- Complete digit-only frequencies use their final three digits as the kHz part and are accepted only when the resulting MHz value lies within a supported `Band` range. The same full-frequency parser is used for station names and public or directed chat messages. Relative QRG rules and bare three-digit context handling remain separate.
### JavaFX/threading
@@ -49,6 +50,17 @@ JavaFX ObservableList / UI state
`MessageBusManagementThread` must not directly iterate or mutate UI-bound JavaFX collections. UI-visible changes should cross the controller/UI boundary and run on the JavaFX Application Thread.
## Configuration and Layout Persistence
- The current `preferences.xml` configuration version is 7. Version 6 introduced optional managed leaf-column widths below `guiOptions`, identified by stable table and column IDs. Parent-column widths remain derived from their leaf columns.
- `GUIstationMapClusteringEnabled` is a layout preference below `guiOptions`. It defaults to `true`, is selectively autosaved and controls only screen-based clustering of nearby map markers. Missing or malformed values retain the enabled default for backward compatibility.
- Stored widths take precedence. Without a usable entry, a managed column is sized once when meaningful table data first becomes available. Message and similar free-text columns use a flexible initial width instead of following the longest value.
- Main-window and separate-monitor DXCluster/QSO tables use distinct layout IDs even though they share the underlying message stores.
- Window sizes and positions, relevant divider positions and managed column widths are selectively autosaved after a short debounce. A pending write is flushed during application shutdown.
- Selective layout writes update the XML already on disk, preserve unknown XML nodes and must not persist unconfirmed functional settings from the current UI. **Save Settings** remains the full settings writer and includes the current layout.
- Full and selective writes are synchronized and replace `preferences.xml` atomically. Missing, unknown or malformed width entries do not prevent loading and fall back to initial sizing.
- Older configuration files require no migration. Older KST4Contest versions can ignore the additional elements; a complete rewrite by such a version may discard column widths without invalidating the remaining file.
## External Interfaces
Treat current implementation/tests and authoritative upstream documentation as source of truth before modifying any interface.
@@ -65,11 +77,58 @@ 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.
### Local DX Cluster output
- Local spots use a fixed 75-character, DXSpider-compatible payload line followed by two BEL characters and CRLF.
- The DX callsign begins in column 27 and occupies up to 12 characters. The 30-character comment begins in column 40, and the five-character UTC time begins in column 71.
- Spotter and frequency padding is calculated dynamically so frequencies from 50 MHz through 24 GHz do not shift the following fields.
- Comments are padded or truncated to exactly 30 characters. Automatic AirScout comments retain the locator first and use the compact form `JO51HK AP 1m/100%;4m/75%`.
- A DX callsign longer than 12 characters is rejected and logged rather than truncated.
- Trigger conditions, QRG recognition and normalisation, login, keepalive, multi-client delivery and the local-only trust boundary remain separate from line formatting.
### Logging and Worked-state persistence
- The Simplelogfile interpreter reads the selected text file after connection startup and then once per minute using a fixed built-in callsign pattern.
- Simplelogfile callsigns are normalized to base callsigns and set only the global Worked state for every active variant. They do not create per-band Worked or grid-square state.
- Simplelogfile-derived Worked state is not persisted in SQLite. The selected file is the durable source and is read again in each application session.
- The interpreter only adds positive runtime marks. It does not remove existing marks during the current session and does not reset automatically when a new contest starts. A database reset does not modify the file; callsigns contained in it are marked as worked again during the next periodic evaluation.
- A missing selected file is created. Read, path and creation failures are contained so the periodic timer remains alive; successful creation triggers a one-time, non-blocking UI notice with the exact path and setup/contest checks.
- Network-derived and manually assigned Worked, NOT-QRV and worked-grid state continues to use SQLite with its established lifetime and reset behaviour.
- Each completed initial ON4KST user list loads one SQLite Worked/NOT-QRV snapshot. `ChatController` applies that snapshot by normalized base callsign to every new category and suffix variant before the completed category is published. The same event-driven path runs again after a reconnect; startup synchronization does not depend on a fixed-delay timer.
- Automatic QRG updates require both an enabled source and valid incoming `RadioInfo` or Win-Test `STATUS` data. Merely enabling a source does not provide or validate a current QRG.
- UCXLog-compatible QSO packets and Win-Test `ADDQSO` packets are converted into one validated external-QSO state. Logger-specific numeric, metre and centimetre values and Win-Test band IDs are normalised once; the resolved band is then the sole source for per-band Worked and worked-grid state.
- A missing or unknown logger band sets only the global Worked state. Worked-grid state requires both a recognised project band and a valid locator; no band or locator is inferred. Packets without a usable callsign are discarded without terminating the listener.
- External logger threads do not read or mutate the JavaFX user-list projection. `ChatController` applies global and per-band Worked state to every active variant of the base callsign on the JavaFX Application Thread before evaluating a band-upgrade notice.
- The established Win-Test handling for 24, 47 and 76 GHz remains unchanged. Their Worked flags are retained, while only frequencies represented by the project `Band` model can create worked-grid state.
### Terrain data providers
- The active terrain profile provider is Open-Meteo using Copernicus GLO-90 data.
- `OfflineDemImportService` only prepares a local directory and copies manually selected Copernicus GLO-30 GeoTIFF files into it. Importing files does not activate an offline provider or change the active calculation chain.
### ON4KST session and authentication
- One KST4Contest connection authenticates one ON4KST TCP session with one local login callsign and one password.
- The TCP session uses one common locator for both categories; the locator is not part of authentication.
- The primary category is part of the initial login. A distinct second category is added to the same session through ON4KST Single Sign-on; it must not create a second TCP connection or local login.
- **Name in Chat** is a visible category-specific name field, not a login callsign or message destination. Private messages to the local station are addressed to the local login callsign.
- The visible **Name in Chat** field, message context, QRG and beacon configuration remain category-specific.
### 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 session 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.
- Incidental code changes must not unexpectedly change selection, focus, sorting, tab state, map zoom or prefilled text.
- Map reset clears the selected target without changing zoom unless explicitly redesigned.
- **Group nearby stations** re-renders only the existing station-marker layer from JavaScript `stationData`. It must not reload the WebView, tiles or station data, request a new controller snapshot, or change zoom, viewport or selection.
- Base-callsign aggregation into one geographical marker happens before screen-based clustering. Disabling clustering displays each resulting positionable map station individually but never splits active variants of the same normalised base callsign into separate geographical markers.
- Station selection preserves the established `/cq callsign` prefill behaviour.
- Sending without an explicitly selected send category preserves the established Main-category fallback unless explicitly changed.
@@ -103,6 +162,10 @@ The repository contains the KST4Contest website under `website/`, published sepa
Current website/deployment scripts and update-feed behaviour must be inspected before changes; do not rely on historical assumptions.
- `APPLICATION_CURRENT_VERSION` is the user-visible semantic version and must use the dotted `major.minor.patch` form. `APPLICATION_CURRENTVERSIONNUMBER` is retained only for older feeds and encodes patch releases by appending the patch digit, for example `1.43.1` as `1.431`.
- The tagged-release workflow creates the GitHub Release before building the website update feed. This ordering is required because `versionInfo.js` reads the published release body through the GitHub Releases API.
- After publication, the workflow tests and builds the website, validates the expected Stable version, attaches `kst4ContestVersionInfo.xml` to the release and uploads the complete website build as a workflow artifact.
## Important Decisions and Workarounds
- Preserve full callsign/category identity while applying base-call normalisation only to specifically defined features.
@@ -128,6 +191,7 @@ Before implementing planned items, re-check current decisions and obtain a fresh
- Historical project context is useful but may be stale; current code/tests win.
- External service/API behaviour must be verified against current upstream documentation when uncertain.
- Screenshots in manuals/website may need targeted replacement after visible UI changes; never fabricate them.
- `station_map_path_analysis.png` and `station_map_reset.png` predate the **Group nearby stations** checkbox in the map header. Replace them with current screenshots when suitable source images are available; the website reuses `station_map_path_analysis.png` through `/manual/assets/`.
## Recent Significant Changes
+18 -10
View File
@@ -2,7 +2,7 @@
> 🇬🇧 [English version](en-AirScout-Integration) | 🇩🇪 Du liest gerade die deutsche Version
AirScout (von DL2ALF) ist ein Programm zur Erkennung von Flugzeugen für den Aircraft-Scatter-Betrieb. KST4Contest ist eng mit AirScout integriert und zeigt reflektierbare Flugzeuge direkt in der Benutzerliste an.
AirScout (von DL2ALF) berechnet Aircraft-Scatter-Gelegenheiten anhand aktueller Flugzeugpositionen. KST4Contest übernimmt diese Ergebnisse und zeigt für die jeweilige Gegenstation geeignete Flugzeuge direkt in der Benutzerliste an.
> **Aircraft Scatter** ermöglicht sehr weitreichende Verbindungen auf VHF und höher auch für Stationen mit geringer Höhe über NN oder ungünstigen topografischen Verhältnissen.
@@ -15,9 +15,9 @@ Download von AirScout:
---
## Flugzeugdaten-Feeds (ADSB)
## Flugzeugdaten-Feeds (ADS-B)
Öffentliche Flugzeugdaten-Feeds im Internet sind oft unzuverlässig und begrenzt nutzbar. Eine empfohlene Alternative bietet **OV3T (Thomas)** mit einem dedizierten ADSB-Feed-Dienst:
Öffentliche Flugzeugdaten-Feeds im Internet sind oft unzuverlässig und nur eingeschränkt nutzbar. Eine empfohlene Alternative bietet **OV3T (Thomas)** mit einem dedizierten ADS-B-Feed-Dienst:
- https://airscatter.dk/
- https://www.facebook.com/groups/825093981868542
@@ -28,7 +28,7 @@ Für diesen Dienst ist ein Account erforderlich. Bitte eine Spende für Thomas i
## AirScout einrichten
### Schritt 1: ADSB-Feed in AirScout konfigurieren
### Schritt 1: ADS-B-Feed in AirScout konfigurieren
1. AirScout starten.
2. In den AirScout-Einstellungen den OV3T-Feed-Account eintragen (Benutzername, Passwort, URL).
@@ -85,32 +85,40 @@ Andernfalls kann es zu fehlerhaften AP-Daten kommen.
## AP-Spalte in der Benutzerliste
Nach der Einrichtung erscheint in der Benutzerliste eine **AP-Spalte** mit bis zu zwei reflektierbaren Flugzeugen pro Station.
Nach der Einrichtung erscheint in der Benutzerliste eine **AP-Spalte**. Sie zeigt für jede Station die Ankunftszeit und das von AirScout berechnete Reflexionspotenzial der ersten beiden geeigneten Flugzeuge.
Beispiel-Darstellung:
| Station | AP-Info |
|---|---|
| DF9QX | 2 Planes: 0 min / 0 min, je 100% |
| F5DYD | 2 Planes: 14 min / 31 min, je 50% |
| DF9QX | 0 (100 %) / 0 (100 %) |
| F5DYD | 14 (50 %) / 31 (50 %) |
Die Zahl vor der Klammer gibt die verbleibenden Minuten bis zur berechneten Gelegenheit an. Die Prozentzahl beschreibt das von AirScout gemeldete Reflexionspotenzial. Sie ist keine QSO-Wahrscheinlichkeit.
Die AP-Informationen sind auch im **Privatnachrichten-Fenster** verfügbar.
Die Prozentzahl gibt das Reflexionspotenzial an (Größe des Flugzeugs, Höhe, Entfernung).
## Einfluss auf Priority Score und Timeline
Mindestens ein von AirScout als erreichbar gemeldetes Flugzeug erhöht den Priority Score der Station. Eine unmittelbar bevorstehende Gelegenheit in null, einer oder zwei Minuten wird zusätzlich zeitabhängig gewichtet. AirScout ist dabei nur ein Faktor neben Worked-Status, verfügbaren Bändern, QRB, Antennenrichtung, Chat-Aktivität und Skeds.
Die AP- und Sked-Timeline verwendet den Priority Score zur Auswahl interessanter Stationen und ordnet die nächste geeignete Aircraft-Scatter-Gelegenheit zeitlich ein. Das Reflexionspotenzial bestimmt zusätzlich die Darstellung des AP-Symbols. Die Timeline bleibt eine Vorschau; sie garantiert kein QSO.
---
## AP-Variablen in Nachrichten
Die Flugzeugdaten können direkt in Nachrichten eingefügt werden:
Die Flugzeugdaten der ausgewählten Station können direkt in Shortcuts, Snippets und andere stationsbezogene Nachrichten eingefügt werden:
- `FIRSTAP` → z. B. `a very big AP in 1 min`
- `SECONDAP` → z. B. `Next big AP in 9 min`
Details: [Makros und Variablen](de-Makros-und-Variablen#variablen)
Da die Werte eine ausgewählte Gegenstation benötigen, stehen `FIRSTAP` und `SECONDAP` nicht als globale Beacon-Variablen zur Verfügung.
---
## „Show Path in AirScout"-Button
In der Benutzerliste gibt es einen Button mit einem Pfeil, der die Richtung (QTF) zur ausgewählten Station anzeigt. Ein Klick maximiert AirScout und zeigt den Pfad mit reflektierbaren Flugzeugen zum ausgewählten Gesprächspartner.
In der Benutzerliste gibt es einen Button mit einem Pfeil, der die Richtung (QTF) zur ausgewählten Station anzeigt. Ein Klick maximiert das externe AirScout-Fenster und lässt dort den Pfad zur ausgewählten Gegenstation mit den berechneten Aircraft-Scatter-Gelegenheiten anzeigen. Die Schaltfläche startet keine eigene Gelände- oder Ausbreitungsberechnung in KST4Contest.
+30 -9
View File
@@ -4,7 +4,7 @@
## Verbinden mit dem Chat
Vor dem ersten Verbindungsaufbau müssen im Einstellungsfenster mindestens Rufzeichen, Passwort, Locator und primäre Chat-Kategorie konfiguriert werden. Soll zusätzlich eine zweite Kategorie verwendet werden, muss auch deren Login aktiviert und vollständig eingerichtet sein.
Vor dem ersten Verbindungsaufbau müssen im Einstellungsfenster das lokale Login-Rufzeichen und Passwort, der gemeinsame Locator der TCP-Sitzung sowie die primäre Chat-Kategorie konfiguriert werden. Soll zusätzlich eine zweite Kategorie verwendet werden, wird sie dort aktiviert und ausgewählt. KST4Contest meldet sich einmal an und ergänzt die zweite Kategorie per Single Sign-on innerhalb derselben TCP-Sitzung.
Die Verbindung kann auf zwei Wegen aufgebaut werden:
@@ -54,11 +54,20 @@ Beide Hinweise blinken ungefähr zwölf Sekunden und verschwinden anschließend
### PM-Fenster (oben links)
Das PM-Fenster zeigt die an die eigenen Chat-Logins gerichteten Privatnachrichten und die zugehörigen ausgehenden Antworten.
Das PM-Fenster zeigt die an das lokale Login-Rufzeichen adressierten Privatnachrichten und die zugehörigen ausgehenden Antworten.
Nicht selbst gesendete Nachrichten erscheinen dort zusätzlich, wenn ihr Text ohne Beachtung der Groß- und Kleinschreibung das konfigurierte eigene Login-Rufzeichen enthält. Das gilt für öffentliche Nachrichten an `ALL` ebenso wie für gerichtete Nachrichten zwischen anderen Chatteilnehmern. Gerade im zweiten Fall lässt sich dieses PM-Catching flapsig als **„Lästererkennung“** bezeichnen.
Der tatsächliche Empfänger, der Nachrichtentext, die Chat-Kategorie und das Routing bleiben unverändert. Die Nachricht erhält lediglich eine zusätzliche Darstellung im PM-Fenster.
Ist das [QSO-Monitoring](de-Funktionen#qso-monitoring-ab-v131) aktiviert, erscheinen dort zusätzlich die erfassten Nachrichten der überwachten Basisrufzeichen. Diese Einträge erhalten eine `Sniffed:`-Kennzeichnung mit dem vollständigen sichtbaren Absender und Empfänger.
Neue Nachrichten werden zunächst auffällig dargestellt und wechseln anschließend schrittweise zur normalen Tabellenfarbe. Die farbliche Hervorhebung dient nur als zeitlicher Hinweis; sie verändert weder Inhalt noch Routing der Nachricht.
Neue, nicht selbst gesendete Zeilen durchlaufen sechs grüne Altersstufen und kehren nach fünf Minuten zur normalen Tabellenfarbe zurück. Eigene Nachrichten behalten ihre separate Hervorhebung. Die farbliche Darstellung ist nur ein zeitlicher Hinweis; sie verändert weder Inhalt noch Routing der Nachricht.
Die Auswahl einer eingehenden Zeile bereitet eine Antwort an den Absender vor. Bei einer eigenen ausgehenden Nachricht wird stattdessen der ursprüngliche Empfänger als Nachrichtenziel wiederhergestellt. Caught- und Monitoring-Zeilen lösen keine PM-Audioausgabe aus.
Altersstufen: [Farbige PM-Zeilen](de-Funktionen#farbige-pm-zeilen-ab-v125). Erkennung und Grenzen: [PM-Abfang](de-Funktionen#pm-abfang-catching-personal-messages-ab-v11).
### Benutzerliste (Chat Members)
Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfiguration):
@@ -143,7 +152,15 @@ Der Wert beeinflusst unter anderem:
---
## Nachrichtentabellen
## Tabellenbreiten und gekürzte Zellinhalte
Beim ersten brauchbaren Datenbestand richtet KST4Contest die Spalten der Benutzerliste, der Nachrichtenansichten, der DXCluster- und QSO-Tabellen sowie der Worked-Datenbank einmalig nach Überschrift und vorhandenem Inhalt aus. Bereits gespeicherte Breiten haben Vorrang. **Name**, **AP** und **NOT QRV @** werden dabei begrenzt, damit einzelne lange Werte nicht den restlichen Tabellenbereich verdrängen. **Message** und vergleichbare Freitextspalten bleiben flexibel und richten sich nicht nach der längsten Nachricht.
Manuell geänderte Spaltenbreiten werden automatisch gespeichert und beim nächsten Start wiederhergestellt. Spätere Nachrichten oder Stationsaktualisierungen überschreiben diese Auswahl nicht.
Passt ein normaler Textwert nicht vollständig in seine Zelle, zeigt ein Tooltip den vollständigen Wert. Der Tooltip erscheint nur bei tatsächlich gekürztem Text. Funktionale Tooltips, etwa für QRA-, Worked- oder Bandzustände, bleiben erhalten; bei gekürztem Zelltext stehen Volltext und Erklärung gemeinsam im Tooltip.
### Nachrichtentext und Links
KST4Contest zeigt Nachrichtentexte bewusst einzeilig an. So bleiben auch bei hohem Chat-Aufkommen viele Einträge gleichzeitig sichtbar. Der Nachteil liegt auf der Hand: Bei einer schmalen **Message**-Spalte passt nicht jede Nachricht vollständig in die Zeile.
@@ -318,7 +335,11 @@ Ein einzelner Stationsmarker kann direkt angeklickt werden. KST4Contest:
3. aktualisiert den **Further Info**-Bereich und
4. bereitet das vollständige sichtbare Rufzeichen als `/cq`-Empfänger vor.
Marker, die bei der aktuellen Zoomstufe zu dicht beieinanderliegen, werden als Cluster mit einer Stationsanzahl dargestellt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus.
Ist **Group nearby stations** ausgewählt, werden Marker, die bei niedrigen Zoomstufen zu dicht beieinanderliegen, als Cluster mit einer Stationsanzahl dargestellt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus. Wird die Checkbox ausgeschaltet, zeigt die Karte unabhängig von der Zoomstufe alle positionierbaren Stationen als einzelne Marker.
Das Umschalten wirkt sofort und verändert weder Zoom noch Kartenausschnitt oder Stationsauswahl. Die Einstellung wird automatisch gespeichert und beim nächsten Programmstart wiederhergestellt. Ohne gespeicherte Einstellung bleibt **Group nearby stations** ausgewählt, damit bestehende Installationen zunächst das bisherige Verhalten behalten.
Der Schalter betrifft nur die räumlichen Cluster auf dem Bildschirm. Aktive Chatvarianten desselben normalisierten Basisrufzeichens können weiterhin einen gemeinsamen geografischen Marker verwenden und bleiben unabhängig davon getrennte Nachrichtenziele.
Die Kopfzeile ergänzt bei ausgewählter Station:
@@ -423,7 +444,7 @@ Zusätzlich öffnet KST4Contest das Fenster **Cluster & QSO of the other**. Es z
![Separates Cluster- und QSO-Monitorfenster](cluster_qso_monitor.png)
Die Position des vertikalen Dividers sowie die Fenstergröße werden zusammen mit den übrigen UI-Einstellungen gespeichert. Nach einer Änderung **Save Settings** verwenden.
Die Position des vertikalen Dividers sowie die Fenstergröße werden automatisch gespeichert. Die DXCluster- und QSO-Tabellen besitzen hier eigene Spaltenbreiten; Änderungen im Monitorfenster verändern daher nicht das Tabellenlayout der Hauptfenster-Tabs.
Das Fenster lässt sich über das Menü aus- und wieder einblenden:
@@ -469,13 +490,13 @@ Die serverbezogenen Funktionen sind nur bei vollständig aufgebauter ON4KST-Verb
## Fenstergrößen und Divider
Beim Klick auf **Save Settings** speichert KST4Contest die Größen der Programmfenster und die Positionen der relevanten Divider in der Konfigurationsdatei. Diese Werte werden beim nächsten Programmstart wiederverwendet.
KST4Contest speichert Größen und Positionen der Programmfenster, die relevanten Divider sowie manuell geänderte Tabellenbreiten automatisch nach einer kurzen Verzögerung in der Konfigurationsdatei. Ein ausstehender Layoutstand wird beim Programmende noch geschrieben. **Save Settings** ist dafür nicht erforderlich, speichert aber weiterhin den vollständigen aktuellen Stand einschließlich Layout.
Das Hauptfenster wird beim Start zusätzlich gegen den sichtbaren Bereich des primären Bildschirms geprüft. Ist die gespeicherte Größe zu groß, verkleinert und verschiebt KST4Contest das Fenster so, dass es wieder erreichbar bleibt. Die genaue Herleitung ist unter [Bildschirmgerechte Größe des Hauptfensters](de-Funktionen#bildschirmgerechte-größe-des-hauptfensters-ab-v141) beschrieben.
Für die übrigen Programmfenster gilt diese zusätzliche Größenbegrenzung derzeit nicht. Wird beispielsweise das separate Monitorfenster nach einem Wechsel auf einen kleineren Bildschirm zu groß dargestellt, muss seine Größe manuell korrigiert und anschließend erneut mit **Save Settings** gespeichert werden.
Für die übrigen Programmfenster gilt diese zusätzliche Größenbegrenzung derzeit nicht. Wird beispielsweise das separate Monitorfenster nach einem Wechsel auf einen kleineren Bildschirm zu groß dargestellt, genügt eine manuelle Korrektur; die neue Größe wird automatisch gespeichert.
Bei einer ungünstigen Aufteilung sollten zuerst die Divider an eine brauchbare Position verschoben und die Einstellungen erneut gespeichert werden. Das Löschen der Konfigurationsdatei setzt zwar die UI-Werte zurück, entfernt aber auch die übrigen gespeicherten Programmeinstellungen und sollte deshalb nur verwendet werden, wenn sich die Oberfläche auf anderem Weg nicht mehr herstellen lässt.
Bei einer ungünstigen Aufteilung sollten zuerst die Divider und Spaltenbreiten wieder an brauchbare Positionen verschoben werden. Das Löschen der Konfigurationsdatei setzt zwar die UI-Werte zurück, entfernt aber auch die übrigen gespeicherten Programmeinstellungen und sollte deshalb nur verwendet werden, wenn sich die Oberfläche auf anderem Weg nicht mehr herstellen lässt.
---
+71 -2
View File
@@ -8,6 +8,72 @@ Die veröffentlichten Stable-Versionen und ihre Programmpakete stehen unter [Git
---
## v1.43.1 (2026-09-03)
**Korrigierte Versionsmetadaten**
v1.43.1 enthält dieselben funktionalen Änderungen wie v1.43.0. Korrigiert wurden die Anwendungs- und Build-Metadaten für Anzeige, ON4KST-Kennung und Update-Vergleich. Ein Teil der Metadaten des ersten v1.43.0-Pakets wies den Build noch als Version 1.42 aus.
### Behoben
- **Einheitliche semantische Version:** Die sichtbare Anwendungsversion verwendet jetzt vollständig `1.43.1`. Der kompakte Wert `1.431` bleibt ausschließlich im veralteten numerischen Feld für die Kompatibilität mit älteren Update-Feeds erhalten.
- **Update-Feed im Tagged-Release-Workflow:** Website-Build, Prüfung des Versionsfeeds und Upload der Artefakte laufen jetzt tatsächlich nach der Veröffentlichung des GitHub Releases. Diese Schritte waren zuvor versehentlich in die Artefaktliste der Release-Action eingerückt und wurden deshalb übersprungen.
Wer v1.43.0 installiert hat, sollte v1.43.1 verwenden. Die Funktionen bleiben unverändert; korrigiert werden nur die Versionsmetadaten und der Release-Workflow.
Die korrigierte Version ist als [Release v1.43.1](https://github.com/praktimarc/kst4contest/releases/tag/v1.43.1) verfügbar.
---
## v1.43.0 (2026-09-03)
**Zuverlässigere Log-Synchronisation, gespeicherte Tabellenlayouts und bessere DX-Cluster-Kompatibilität**
v1.43 konzentriert sich auf die zuverlässige Verarbeitung externer Logdaten und den stabilen Betrieb während längerer Conteste. Hinzu kommen praktische Einstellmöglichkeiten für Tabellenlayouts und die Stationsgruppierung auf der Karte.
### Neu
- **Optionale Kartencluster:** **Group nearby stations** schaltet die räumliche Gruppierung bei niedrigen Zoomstufen unmittelbar ein oder aus. Die gespeicherte Auswahl verändert weder Kartenausschnitt noch Stationsauswahl; die Zusammenfassung aktiver Varianten desselben Basisrufzeichens bleibt davon unabhängig. Damit ist [Issue #79](https://github.com/praktimarc/kst4contest/issues/79) umgesetzt.
- **Tabellenlayout automatisch gesichert:** Tabellen erhalten beim ersten brauchbaren Inhalt sinnvolle Breiten. Manuell geänderte Spaltenbreiten, Fenstergrößen und relevante Divider werden nach kurzer Verzögerung in `preferences.xml` geschrieben. Haupt- und Monitorfenster behalten getrennte Layouts für DXCluster und QSO of the other.
- **Tooltips für gekürzte Tabellenwerte:** Normale Tabellenzellen zeigen ihren vollständigen Inhalt, wenn die sichtbare Spalte zu schmal ist. Funktionale Tooltips und anklickbare Links bleiben davon unberührt.
- **Hinweis bei neu angelegtem Simplelogfile:** Fehlt die ausgewählte Datei, legt KST4Contest sie an und zeigt einen Hinweis mit Dateipfad, konkretem Testablauf und Link zum passenden Abschnitt des Handbuchs.
### Geändert
- **Robuste Simplelogfile-Auswertung:** Die ausgewählte Datei wird einmal pro Minute ausgewertet und nach jedem Durchlauf geschlossen, damit das Logprogramm sie ersetzen oder rotieren kann. Erkannte Rufzeichen setzen den globalen Worked-Status für alle aktiven Suffixvarianten des Basisrufzeichens. Bei deaktivierter Funktion findet kein Dateizugriff statt; Lese- oder Erstellungsfehler beenden die periodische Aufgabe nicht mehr.
- **Einheitliche Bandwerte externer Logger:** UCXLog-kompatible Pakete und Win-Test-Ereignisse verwenden eine gemeinsame Bandnormalisierung. Numerische Werte, Meter- und Zentimeterangaben sowie die vorhandenen Win-Test-IDs setzen damit dieselben Worked-Markierungen und Worked-Großfelder. Insbesondere `2320`, `5760` und `10368` werden zuverlässig verarbeitet; bei fehlendem oder unbekanntem Band wird nur der globale Worked-Status gesetzt.
- **Kompakte vollständige Frequenzen erkannt:** Vollständige Frequenzangaben ohne Dezimaltrenner werden auf allen unterstützten Bändern akzeptiert; die letzten drei Ziffern bilden den kHz-Anteil. Nackte dreistellige Zahlen benötigen weiterhin einen erkennbaren Frequenzkontext, damit Signalrapporte und andere Zahlen nicht als QRG behandelt werden.
- **DXSpider-kompatibles Spotformat:** Lokale DX-Cluster-Spots verwenden eine feste 75-Zeichen-Nutzzeile mit dem DX-Rufzeichen ab Spalte 27, einem 30 Zeichen breiten Kommentarfeld und der UTC-Zeit ab Spalte 71. Das Format bleibt bis 24 GHz stabil. Überlange DX-Rufzeichen werden verworfen und protokolliert, statt unbemerkt abgeschnitten zu werden. Damit ist [Issue #86](https://github.com/praktimarc/kst4contest/issues/86) behoben.
- **Aktive ON4KST-Verbindungsprüfung:** Ein ruhiger Chatserver wird mit einer expliziten, sitzungsweiten Abfrage geprüft, bevor die Verbindung als unterbrochen gilt. Heartbeat und Prüftelegramme behalten das erforderliche CR/LF-Framing.
- **Zuverlässige Altersmarkierung privater Nachrichten:** Eingehende Privatnachrichten verwenden bis zu fünf Minuten lang die definierten grünen Altersstufen. Eigene Nachrichten behalten ihre separate Darstellung; leere oder wiederverwendete Tabellenzeilen kehren zum normalen Design zurück und behalten keine veraltete Hervorhebung.
### Behoben
- **Worked-Status nach dem Login:** Persistierte SQLite-Informationen werden vor der Veröffentlichung jeder initialen ON4KST-Benutzerliste geladen und angewendet. Erneute Verbindungen, beide Kategorien und alle aktiven Varianten eines Basisrufzeichens starten dadurch mit dem richtigen Status. Damit ist [Issue #85](https://github.com/praktimarc/kst4contest/issues/85) behoben.
- **Fehlerhafte Trennung bei ruhigem Server:** Eine gültige ON4KST-Verbindung wird nicht mehr beendet, nur weil der Server gerade keine Aktivitätszeilen überträgt.
### Dokumentation und Auslieferung
- Das deutsche und englische Handbuch wurden mit der Implementierung abgeglichen und überarbeitet. Ein neues Kapitel zum Contest-Workflow verbindet die einzelnen Funktionen zu einem praktischen Betriebsablauf; außerdem wurden die Abschnitte zu Dual Chat, Privatnachrichten, QRG-Synchronisation, Simplelogfile-Auswertung und Konfiguration präzisiert.
- Die Website beschreibt Band- und Richtungsgelegenheiten, Stationskarte, QRG-Verarbeitung, Filter, globale Nachrichtenansichten, Privatnachrichten und Log-Synchronisation jetzt ausführlicher.
- Die AUR-Paketdefinitionen wurden auf v1.43.0 aktualisiert.
Die vollständige Funktionalität von v1.43.0 steht im [Release v1.43.0](https://github.com/praktimarc/kst4contest/releases/tag/v1.43.0) bereit. Wegen der inkonsistenten eingebetteten Versionsmetadaten ist v1.43.1 der empfohlene Paketstand.
---
## v1.42.0 (2026-08-22)
**Gemeinsamer Bandkontext, sitzungsbasierte ON4KST-Verbindung und signierte macOS-Pakete**
@@ -34,6 +100,8 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
- **Manuelle QTF-Eingabe:** Die aktuelle Antennenrichtung kann auch ohne PSTRotator direkt in KST4Contest geändert werden.
- **Nachrichtenvariable `MYQTF`:** Die aktuelle Antennenrichtung kann als numerischer Winkel in Grad in Shortcuts, Snippets und andere unterstützte Nachrichtentexte eingesetzt werden.
- **Filter zurücksetzen:** Ein eigener Reset-Button entfernt die aktiven Filterprädikate der Benutzerliste zuverlässig.
- **Kartencluster:** Räumlich dicht beieinanderliegende Stationen werden bei niedrigen Zoomstufen zusammengefasst. Die ausgewählte Station und relevante Richtungsgelegenheiten bleiben einzeln sichtbar.
@@ -70,7 +138,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.
@@ -80,6 +148,8 @@ v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen,
- **DXLog-Gesamtlog übernommen:** Der UCXLog-kompatible UDP-Listener verarbeitet neben `contactinfo` auch `contactreplace`. Dadurch kann ein von DXLog.net als vollständiges Log ausgesendeter Datenbestand eingelesen werden.
- **Automatische QRG-Übernahme abgesichert:** `MYQRG` wird nur von einer aktivierten Schnittstelle aktualisiert, die tatsächlich gültige `RadioInfo`- beziehungsweise Win-Test-`STATUS`-Pakete liefert. Eine aktivierte, aber nicht liefernde Quelle ersetzt die notwendige Funktionsprüfung oder manuelle QRG-Pflege nicht.
- **Versionserkennung verbessert:** Versionsnummern werden semantisch verglichen, damit beispielsweise Patch-Versionen und Nightly-Stände nicht mehr durch eine einfache Fließkommazahl falsch eingeordnet werden.
### Behoben
@@ -411,7 +481,6 @@ Erste öffentlich veröffentlichte Version. Grundfunktionen:
## Geplante Features
- `MYQTF`-Variable (eigene Antennenrichtung als Text)
- ~~Lebensdauer für den Worked-Status (automatisches Zurücksetzen)~~ ✅ **Umgesetzt in v1.40** (3-Tage-Lebensdauer, kein manuelles Zurücksetzen mehr nötig)
- Filterung des „Cluster & QSO der anderen"-Fensters auf eigenes QTF
- Weitere Topografie-basierte Berechnungen für die Richtungswarnung
+16 -1
View File
@@ -67,6 +67,8 @@ Aktiviere nur die Schnittstellen, die tatsächlich verwendet und vorher getestet
Ein Contest ist ein ungünstiger Zeitpunkt, um gleichzeitig die Funkbedingungen und eine erstmals aktivierte Netzwerkschnittstelle zu untersuchen.
Beim Simplelogfile sollte vor jedem Contest geprüft werden, ob das Logprogramm die aktuelle Contestdatei an den in KST4Contest ausgewählten Pfad schreibt. Ein Test-QSO muss innerhalb einer Minute als global gearbeitet erscheinen. KST4Contest setzt die aus dieser Datei abgeleiteten Worked-Markierungen beim Contestwechsel nicht automatisch zurück.
---
## Grundablauf während des Contests
@@ -89,7 +91,7 @@ KST4Contest hält die benötigten Informationen zwischen diesen Schritten zusamm
## CQ-Betrieb
Bei einer weitgehend festen CQ-QRG sollten `MYQRG` beziehungsweise `SECONDQRG` den tatsächlich verwendeten Frequenzen entsprechen. Eine aktivierte TRX-Synchronisation kann `MYQRG` automatisch aktualisieren. Ohne automatische Quelle muss der Wert von Hand gepflegt werden.
Bei einer weitgehend festen CQ-QRG sollten `MYQRG` beziehungsweise `SECONDQRG` den tatsächlich verwendeten Frequenzen entsprechen. Eine aktivierte und tatsächlich liefernde TRX-Synchronisation kann `MYQRG` automatisch aktualisieren. Die Aktivierung allein reicht nicht: Vor dem Contest sollte mit einer realen Frequenzänderung geprüft werden, ob gültige Pakete ankommen. Ohne funktionierende automatische Quelle muss der Wert von Hand gepflegt werden.
Der Beacon kann die aktuelle QRG, den Locator und die Antennenrichtung regelmäßig im Chat veröffentlichen. Seine Variablen werden bei jedem Sendedurchlauf erneut ausgewertet.
@@ -246,6 +248,19 @@ G1YBB verwendet die Richtungsanzeige besonders konsequent. Grün markierte Stati
KST4Contest automatisiert dabei nicht das QSO. Der Vorteil besteht darin, dass QRG, Richtung, Aircraft-Scatter-Informationen und weitere Bewertungsdaten bereits vorliegen, wenn die Gelegenheit entsteht. Die verbleibende Aufgabe ist eine schnelle betriebliche Entscheidung.
Eine weitere, besonders konsequente Betriebsweise nutzt die Stationskarte als geografische Arbeitsliste:
1. Der **wkd**-Filter blendet bereits gearbeitete Rufzeichen aus der Benutzerliste und damit auch aus der Karte aus.
2. G1YBB wählt eine interessante Station auf der Karte aus.
3. **Trigger cluster spot** übergibt die bekannte QRG an Minos.
4. Ein Klick auf den Spot wechselt in Minos auf diese QRG.
5. Nach dem QSO wird der Kontakt geloggt.
6. Die Log-Synchronisation aktualisiert den Worked-Status; der Filter entfernt die Station anschließend aus Benutzerliste und Karte.
Die Karte wird so zu einer räumlichen Liste der noch abzuarbeitenden Stationen. Dieser Ablauf ist optional. Er setzt eine zuverlässig eingerichtete Log-Synchronisation und DX-Cluster-Verbindung voraus und ist vor allem für Operatoren interessant, die den Contest bewusst auf diese geografische Weise strukturieren möchten.
[G1YBB zeigt diesen Workflow im Video.](https://www.youtube.com/watch?v=lMQZMiSHlUI)
---
## Optionale Schnittstellen im Workflow
+36 -6
View File
@@ -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.
@@ -147,7 +163,7 @@ Die Schaltfläche **Send test spot** erzeugt folgenden Testeintrag:
```text
Spotted callsign: DO5AMF
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: You donated $100!
Frequency: .300 des konfigurierten Fallback-Bandes
```
@@ -171,12 +187,21 @@ 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.
Die Nutzzeile folgt einem festen, DXSpider-kompatiblen 75-Zeichen-Format. Das DX-Rufzeichen beginnt in Spalte 27, das Kommentarfeld umfasst genau 30 Zeichen und die UTC-Zeit beginnt in Spalte 71. Kurze Kommentare werden mit Leerzeichen aufgefüllt, längere kontrolliert auf 30 Zeichen begrenzt. Unterschiedlich lange Spotter-Rufzeichen und Frequenzen bis 24 GHz verschieben die nachfolgenden Felder nicht.
Das vollständige DX-Rufzeichen wird nicht abgeschnitten. Ist es länger als zwölf Zeichen, verwirft KST4Contest den Spot stattdessen kontrolliert und protokolliert den Grund.
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.
Ein kompakter Kommentar mit AirScout-Information sieht beispielsweise so aus:
```text
JO51HK AP 1m/100%;4m/75%
```
---
@@ -193,7 +218,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 +230,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.
@@ -223,5 +252,6 @@ Die Schnittstelle wurde mit folgenden Logprogrammen verwendet:
- UCXLog
- N1MM+
- Minos
Weitere Logger können funktionieren, wenn sie eine normale TCP-Verbindung zu einem DX-Cluster-Server unterstützen.
+54 -32
View File
@@ -23,6 +23,10 @@ Angenommen, Station A schreibt eine gerichtete Nachricht an Station B:
Ein eingetragener Öffnungswinkel von `70°` ergibt damit einen angenommenen Korridor von jeweils `35°` links und rechts der Richtung von Station A zu Station B.
![Geometrie der angenommenen Antennenkorridore](directional_opportunity_geometry.png)
Im dargestellten Beispiel verläuft der grüne Korridor von F5FEN in Richtung DM5M. Sendet F5FEN eine gerichtete Nachricht an DM5M, prüft KST4Contest, ob die eigene Station innerhalb dieses Korridors liegt. Bei einer Antwort von DM5M an F5FEN wird die Richtung neu berechnet: Dann beginnt der blaue Korridor bei DM5M und zeigt in Gegenrichtung auf F5FEN. Bewertet wird also bei jeder Nachricht die angenommene Antennenrichtung des jeweiligen Absenders.
| Beispiel | Ergebnis |
|---|---|
| Richtung A → B: `120°`, Richtung A → eigene Station: `145°` | Winkeldifferenz `25°`: Richtungsgelegenheit erkannt |
@@ -80,13 +84,15 @@ KST4Contest wertet deshalb den Text jeder öffentlichen und gerichteten Chat-Nac
| Schreibweise | Beispiel | Verarbeitung |
|---|---|---|
| Vollständige Frequenz | `144.210`, `432,088`, `10368.100` | Das Band ergibt sich direkt aus der Frequenz. |
| Vollständige Frequenz | `144.210`, `432,088`, `144307`, `10368100` | Das Band ergibt sich direkt aus der Frequenz. |
| Relative Frequenz mit Punkt oder Komma | `.210`, `,088` | Das Band wird aus dem Stationskontext oder dem konfigurierten Fallback ergänzt. |
| Dreistellige Frequenz mit Textkontext | `qrg 210`, `freq is 210`, `on 210`, `210 MHz` | Die Zahl wird als relative Frequenz behandelt. |
| Dreistellige Zahl ohne Frequenzkontext | `210`, `599`, `144` | Die Zahl wird absichtlich nicht als QRG übernommen. |
Die letzte Einschränkung verhindert plausible, aber falsche Ergebnisse. Mit einem Fallback von `144 MHz` ließe sich ein Signalrapport `599` technisch problemlos zu `144.599 MHz` zusammensetzen. Das Ergebnis wäre formal gültig und fachlich trotzdem Unsinn.
Eine vollständige Frequenz kann auch ohne Punkt oder Komma geschrieben sein. KST4Contest behandelt dabei die letzten drei Ziffern als kHz-Anteil: `144307` im Namensfeld wird zu `144.307 MHz`, `10368100` in einer öffentlichen oder gerichteten Chatnachricht zu `10368.100 MHz`. Der Wert wird nur übernommen, wenn die daraus entstehende Frequenz innerhalb eines unterstützten Bandbereichs liegt.
### Wie wird das Band einer relativen QRG bestimmt?
KST4Contest verwendet folgende Reihenfolge:
@@ -132,7 +138,7 @@ Diese Trennung ist notwendig. Ein bereits gearbeitetes Rufzeichen kann auf einem
Die [Log-Synchronisation](de-Log-Synchronisation) übernimmt neue QSOs aus dem Logprogramm. Welche Informationen dabei zur Verfügung stehen, hängt von der verwendeten Schnittstelle ab:
- Der dateibasierte Simplelogfile-Interpreter erkennt nur das Rufzeichen. Er kann deshalb lediglich den globalen Worked-Status setzen.
- Der dateibasierte Simplelogfile-Interpreter liest die ausgewählte Textdatei einmal pro Minute mit einem festen Rufzeichenmuster. Treffer werden auf das Basisrufzeichen normalisiert und setzen für alle aktiven Varianten lediglich den globalen Worked-Status.
- Die QSO-UDP-Schnittstellen und der Win-Test-Netzwerk-Listener können zusätzlich das Band übernehmen.
- Enthält das Logpaket einen gültigen Locator, speichert KST4Contest außerdem das gearbeitete vierstellige Großfeld für dieses Band.
@@ -201,24 +207,12 @@ Im Klartext: Ein erkannter Hinweis bedeutet „wahrscheinlich auf diesem Band ak
### Speicherung und Lebensdauer
Worked-, NOT-QRV- und Großfeldinformationen werden in der internen SQLite-Datenbank gespeichert und beim nächsten Start wieder geladen. Die Einträge laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist deshalb normalerweise nicht erforderlich.
Worked-, NOT-QRV- und Großfeldinformationen aus den Netzwerkschnittstellen sowie manuelle Markierungen werden in der internen SQLite-Datenbank gespeichert und beim nächsten Start wieder geladen. Die Einträge laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist deshalb normalerweise nicht erforderlich.
Simplelogfile-Treffer sind davon ausgenommen. Sie werden nicht in SQLite persistiert, sondern bei jedem Programmlauf aus der ausgewählten Datei abgeleitet. Die Datei ist damit die dauerhafte Quelle. Der Interpreter entfernt während der laufenden Sitzung keine bereits gesetzte Worked-Markierung und setzt den Zustand beim Wechsel zu einem neuen Contest nicht automatisch zurück. Ein manueller Datenbank-Reset verändert oder leert die Datei ebenfalls nicht. Darin enthaltene Rufzeichen werden bei der nächsten Auswertung innerhalb einer Minute erneut als gearbeitet markiert.
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)
@@ -247,19 +241,33 @@ Die Filter oberhalb der Benutzerliste greifen auf dieselben Informationen zurüc
Mehrere aktivierte Filter werden gemeinsam angewendet. Eine Station bleibt nur sichtbar, wenn sie alle gewählten Bedingungen erfüllt. Die Filter reagieren unmittelbar auf neue Logeinträge und geänderte NOT-QRV-Markierungen.
Bedienung und Aufbau der Filterleiste: [Benutzeroberfläche Filter](de-Benutzeroberflaeche#filter).
Bedienung und Aufbau der Filterleiste: [Benutzeroberfläche Filter](de-Benutzeroberflaeche#filter-und-reachability-steuerung).
---
## 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!)*
---
## PM-Abfang (Catching Personal Messages)
## PM-Abfang (Catching Personal Messages, ab v1.1)
Manche Nutzer senden Direktnachrichten versehentlich öffentlich, z. B.:
@@ -267,7 +275,17 @@ Manche Nutzer senden Direktnachrichten versehentlich öffentlich, z. B.:
(DM5M) pse ur qrg
```
KST4Contest erkennt solche Nachrichten, die das eigene Rufzeichen enthalten, und sortiert sie automatisch in die **Privatnachrichten-Tabelle** ein. So gehen keine Nachrichten verloren.
KST4Contest sucht im Text aller nicht selbst gesendeten Nachrichten ohne Beachtung der Groß- und Kleinschreibung nach dem konfigurierten eigenen Login-Rufzeichen. Ein Treffer erscheint zusätzlich in der **Privatnachrichten-Tabelle**. Das gilt sowohl für öffentliche Nachrichten an `ALL` als auch für gerichtete Nachrichten zwischen anderen Chatteilnehmern.
Gerade bei einer fremden gerichteten Nachricht wirkt das etwas wie **„Lästererkennung“**. Das ist eine flapsige Nebenbezeichnung, kein formaler Funktionsname.
Die ursprüngliche Nachricht bleibt unverändert: Der tatsächliche Empfänger bleibt `ALL` beziehungsweise der andere Chatteilnehmer; auch Text, Chat-Kategorie und Routing bleiben erhalten. PM-Catching ergänzt nur eine weitere Ansicht derselben Nachricht.
Die Textsuche versteht keine Absicht. Ein Schreibfehler oder ein verkürztes Rufzeichen wird nicht erkannt. Umgekehrt kann eine bloße Erwähnung des vollständig geschriebenen Rufzeichens einen Treffer erzeugen, obwohl keine Antwort erwartet wird.
Wird eine eingehende Zeile in der PM-Tabelle ausgewählt, bereitet KST4Contest eine Antwort an ihren Absender vor. Bei einer eigenen ausgehenden Nachricht wird stattdessen der ursprüngliche Empfänger als Ziel wiederhergestellt. Die Auswahl versendet noch keine Nachricht.
Auch abgefangene Zeilen verwenden die normale Altersanzeige der PM-Tabelle. Sie lösen jedoch weder den einfachen PM-Hinweiston noch die CW- oder Sprachausgabe für ein eingehendes Rufzeichen aus. Dasselbe gilt für Nachrichten, die durch das QSO-Monitoring zusätzlich in der PM-Tabelle erscheinen.
---
@@ -294,21 +312,21 @@ Konfiguration, erkannte QRG-Anfragen und genaue Kategorienzuordnung: [Konfigurat
## Multi-Channel-Login (ab v1.26)
Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Beide Chats werden parallel überwacht.
KST4Contest meldet sich mit einem lokalen Login-Rufzeichen und Passwort einmal bei ON4KST an. Ein gemeinsamer Locator gilt für die gesamte TCP-Sitzung. Eine zweite Chat-Kategorie (z. B. 144 MHz und 432 MHz) wird per Single Sign-on innerhalb derselben Sitzung ergänzt. Beide Kategorien werden parallel überwacht; das sichtbare kategoriebasierte Namensfeld, der Nachrichtenkontext, die QRG und der Beacon bleiben je Kategorie getrennt.
---
## 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.
---
## Opposite Station Multi-Callsign Login-Tagging (ab v1.26)
Unterstützung für Stationen, die mit mehreren Rufzeichen gleichzeitig im Chat aktiv sind (z. B. Expedition-Setups).
Unterstützung für Gegenstationen, die mit mehreren vollständigen sichtbaren Rufzeichenvarianten gleichzeitig im Chat aktiv sind (z. B. Expedition-Setups). Vollständiges Rufzeichen und Chat-Kategorie bleiben getrennte Teilnehmeridentitäten; Worked-, Band- und Prioritätsdaten werden über das Basisrufzeichen gemeinsam ausgewertet.
---
@@ -424,7 +442,7 @@ Sniffed: (DN9APW-70 > 9A0BB-23) pse sked 19:30
Das ist ein wichtiger Unterschied. Das Monitoring fasst die Varianten zusammen, damit keine Nachricht übersehen wird. Das Nachrichtenrouting fasst sie nicht zusammen, weil andernfalls nicht mehr eindeutig wäre, welcher Login tatsächlich angesprochen wurde.
Das Basisrufzeichen wird außerdem unabhängig von der Chat-Kategorie ausgewertet. Ist KST4Contest gleichzeitig mit zwei Kategorien verbunden, gilt derselbe Monitoring-Eintrag für beide Nachrichtenströme. Die Kategorie der einzelnen Nachricht bleibt dabei erhalten.
Das Basisrufzeichen wird außerdem unabhängig von der Chat-Kategorie ausgewertet. Sind in derselben ON4KST-Sitzung zwei Kategorien aktiv, gilt derselbe Monitoring-Eintrag für beide Nachrichtenströme. Die Kategorie der einzelnen Nachricht bleibt dabei erhalten.
Erfasst werden:
@@ -507,7 +525,7 @@ UDP selbst bestätigt außerdem keine Paketzustellung. Bleibt die QTF unverände
Im Klartext: KST4Contest liefert die Zielrichtung und verarbeitet die gemeldete Position. Die mechanische Realität bleibt Aufgabe des Rotators und gelegentlich der Blick aus dem Fenster.
Konfiguration und Portbelegung: [Konfiguration PSTRotator-Einstellungen](de-Konfiguration#pstrotator-einstellungen-ab-v131-vollstaendig-konfigurierbar-ab-v140).
Konfiguration und Portbelegung: [Konfiguration PSTRotator-Einstellungen](de-Konfiguration#pstrotator-einstellungen-ab-v131-vollständig-konfigurierbar-ab-v140).
---
@@ -590,7 +608,7 @@ Die Bewertung wird für das normalisierte Basisrufzeichen vorgenommen. Ein Sked
### Wie werden mehrere SSIDs und Chat-Kategorien behandelt?
Aktive Rufzeichen wie `9A0BB-2`, `9A0BB-70`, `9A0BB-23` und `9A0BB-13` bleiben getrennte Chatmember. Dadurch können Nachrichten weiterhin an das vollständige Rufzeichen und die richtige Chat-Kategorie adressiert werden.
Aktive vollständige sichtbare Rufzeichenvarianten wie `9A0BB-2`, `9A0BB-70`, `9A0BB-23` und `9A0BB-13` bleiben getrennte Chatmember. Dadurch können Nachrichten weiterhin an das vollständige Rufzeichen und die richtige Chat-Kategorie adressiert werden.
Worked-, Band-, NOT-QRV- und Score-Informationen beziehen sich dagegen auf das gemeinsame Basisrufzeichen `9A0BB`. Der Score wird deshalb einmal berechnet und auf alle aktiven Varianten übertragen. Die Benutzerliste kann mehrere getrennte Zeilen mit demselben Score enthalten; in der Prioritätsliste erscheint das Basisrufzeichen nur einmal.
@@ -689,7 +707,7 @@ Die Timeline ist eine Vorschau. AirScout-Daten können sich ändern, und ein ein
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht: Die eigene QRG wird regelmäßig veröffentlicht, ohne dass derselbe Text von Hand wiederholt werden muss.
Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Der zweite Beacon wird nur gesendet, wenn auch der zweite Chat aktiviert und verbunden ist.
Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Der zweite Beacon wird nur gesendet, wenn die zweite Kategorie in derselben ON4KST-Sitzung aktiviert und vollständig synchronisiert ist.
Globale Variablen wie `MYQRG`, `SECONDQRG`, `MYLOCATOR` oder `MYQTF` werden unmittelbar vor jeder Aussendung neu aufgelöst. Eine zwischenzeitlich vom Logprogramm aktualisierte QRG kann dadurch bereits im nächsten Beacon erscheinen.
@@ -704,7 +722,7 @@ Konfiguration, Timer-Verhalten und verfügbare Variablen: [Konfiguration Bea
## Simplelogfile
Details: [Log-Synchronisation](de-Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
Der Simplelogfile-Interpreter liest einmal pro Minute Rufzeichen aus einer ausgewählten Textdatei. Fehlt die Datei, legt KST4Contest sie an und zeigt einen nicht blockierenden Hinweis mit Pfad, Funktionstest und Contestprüfung. Details: [Log-Synchronisation](de-Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
---
@@ -813,7 +831,11 @@ Die Bandangaben verwenden dieselbe Herleitung wie die Bandspalten, der Filter **
Treffen mehrere Zustände gleichzeitig zu, hat die für den Betrieb wichtigere Markierung Vorrang. Eine ausgewählte Station bleibt deshalb orange; eine Richtungsgelegenheit wird grün dargestellt, auch wenn das Rufzeichen bereits gearbeitet wurde.
Bei niedrigen Zoomstufen werden räumlich dicht beieinanderliegende Stationen zu einem Cluster zusammengefasst. Die Zahl im Cluster gibt die Anzahl der enthaltenen Stationen an. Ein Klick zoomt weiter hinein, wählt aber noch keine einzelne Station aus. Die aktuell ausgewählte Station und grün markierte Richtungsgelegenheiten bleiben auch bei niedriger Zoomstufe als einzelne Marker sichtbar.
Mit der Checkbox **Group nearby stations** lässt sich die räumliche Gruppierung steuern. Ist sie ausgewählt, werden bei niedrigen Zoomstufen dicht beieinanderliegende Marker zu einem Bildschirm-Cluster zusammengefasst. Die Zahl im Cluster gibt die Anzahl der enthaltenen Stationen an. Ein Klick zoomt weiter hinein, wählt aber noch keine einzelne Station aus. Die aktuell ausgewählte Station und grün markierte Richtungsgelegenheiten bleiben auch bei niedriger Zoomstufe möglichst als einzelne Marker sichtbar.
Ist **Group nearby stations** nicht ausgewählt, erscheinen alle positionierbaren Stationen unabhängig von der Zoomstufe als einzelne Marker. Die Änderung wirkt sofort, ohne Stationsdaten neu zu laden oder Zoom, Kartenausschnitt und Auswahl zu verändern. KST4Contest speichert die Einstellung automatisch und stellt sie beim nächsten Start wieder her. Bestehende Installationen beginnen mit aktivierter Gruppierung und behalten damit zunächst das bisherige Verhalten.
Diese Bildschirm-Cluster sind nicht mit der Zusammenfassung von Chatvarianten zu verwechseln. Mehrere aktive Varianten desselben normalisierten Basisrufzeichens werden weiterhin zu einem geografischen Marker zusammengeführt. Das Ausschalten von **Group nearby stations** erzeugt daraus keine zusätzlichen Marker und verändert weder Chatidentitäten noch Filter oder Stationsdaten.
### Auswahl und geografische Hilfen
@@ -994,4 +1016,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.
+26 -14
View File
@@ -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**: Fachliche Einstellungen mit **„Save Settings"** sichern, wenn sie beim nächsten Start wieder gelten sollen. Layoutänderungen wie Fenstergrößen, Divider-Positionen und Tabellenbreiten speichert KST4Contest automatisch. Die gemeinsame Datei liegt unter Linux und macOS in `~/.praktiKST/preferences.xml` und unter Windows in `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`).
---
@@ -14,14 +14,15 @@ Nach dem ersten Start öffnet sich das **Einstellungsfenster** dieses ist de
### Login und Chat-Kategorien
Hier werden die Zugangsdaten für den ON4KST-Chat eingetragen (Rufzeichen und Passwort).
Zudem wird die **primäre Chat-Kategorie** (z. B. IARU Region 1 VHF/Microwave) ausgewählt.
Hier werden das lokale Login-Rufzeichen und das Passwort für den ON4KST-Chat eingetragen. Zudem wird die **primäre Chat-Kategorie** (z. B. IARU Region 1 VHF/Microwave) ausgewählt.
Mit der Option für einen **zweiten Chat** (Multi-Channel-Login) kann man sich gleichzeitig in eine weitere Kategorie (z. B. UHF/SHF) einloggen. Beide Chats werden dann parallel überwacht. Hier kann optional auch ein abweichender Login-Name für den zweiten Chat vergeben werden (nützlich für Opposite Station Multi-Callsign Logging).
Mit der Option für einen **zweiten Chat** (Multi-Channel-Login) wird eine weitere Kategorie (z. B. UHF/SHF) per Single Sign-on in derselben ON4KST-TCP-Sitzung aktiviert. KST4Contest öffnet dafür keine zweite TCP-Verbindung und verwendet weder ein zweites lokales Login-Rufzeichen noch ein zweites Passwort.
**Name in Chat 2** konfiguriert ausschließlich das sichtbare, kategoriebasierte Namensfeld der zweiten Kategorie. Es ist weder ein zweites Login noch ein Nachrichtenempfänger. Das sichtbare Namensfeld, der Nachrichtenkontext, die QRG und der Beacon bleiben für beide Kategorien getrennt. **Opposite Station Multi-Callsign Login-Tagging** bezieht sich dagegen ausschließlich auf Gegenstationen, die mit mehreren vollständigen sichtbaren Rufzeichenvarianten im Chat erscheinen.
### Rufzeichen und Locator
Eigenes Rufzeichen und Maidenhead-Locator (6-stellig, z. B. `JN49IJ`) eintragen. Diese Werte werden für Distanz- und Richtungsberechnungen benötigt.
Eigenes Rufzeichen und Maidenhead-Locator (6-stellig, z. B. `JN49IJ`) eintragen. Der Locator ist kein Bestandteil der Authentifizierung. ON4KST verwendet ihn gemeinsam für beide Kategorien der einen TCP-Sitzung; KST4Contest benötigt ihn außerdem für Distanz- und Richtungsberechnungen.
### Aktivierte Bänder
@@ -108,7 +109,9 @@ Drei Methoden stehen zur Verfügung, um gearbeitete Stationen automatisch zu mar
### Universal File Based Callsign Interpreter (Simplelogfile)
Interpretiert beliebige Log-Dateien per Regex nach Rufzeichen-Mustern. Keine Bandinformation möglich. Geeignet als Fallback oder für nicht direkt unterstützte Logprogramme.
Liest die ausgewählte Textdatei einmal pro Minute mit einem fest eingebauten Rufzeichenmuster. Die Treffer werden auf Basisrufzeichen normalisiert und setzen für alle aktiven Varianten nur den globalen Worked-Status. Band- und Locatorinformationen können daraus nicht abgeleitet werden. Die Funktion eignet sich als Fallback für Logprogramme ohne unterstützte Netzwerkschnittstelle. Vor der Verwendung sollte deshalb geprüft werden, ob das eingesetzte Logprogramm bereits eine der unterstützten Netzwerkschnittstellen bereitstellt.
Existiert die ausgewählte Datei noch nicht, legt KST4Contest sie an und zeigt einmalig den vollständigen Pfad sowie Hinweise zum Funktions- und Contesttest. Die Datei ist selbst die dauerhafte Quelle; aus ihr abgeleitete Worked-Markierungen werden nicht in SQLite gespeichert und nicht automatisch für einen neuen Contest zurückgesetzt.
### Netzwerk-Listener für QSO-UDP-Broadcast
@@ -134,7 +137,7 @@ Die TRX-Synchronisation übernimmt die aktuelle Frequenz aus dem Logprogramm und
| **Win-Test STATUS** | `Win-Test STATUS QRG Sync` | Verarbeitet die Haupt- oder Pass-Frequenz aus nativen Win-Test-`STATUS`-Paketen. Der Win-Test-Listener verwendet seinen separat konfigurierten Port, standardmäßig `9871`. |
| **Manuelle Eingabe** | Beide automatischen QRG-Quellen deaktivieren | Die eigene QRG kann im Hauptfenster von Hand eingetragen werden. |
Der allgemeine Listener ist für Logprogramme vorgesehen, die kompatible `RadioInfo`-Pakete senden. Dazu gehören abhängig von deren jeweiliger Konfiguration UCXLog, N1MM+, QARTest und DXLog.net. QSO- und `RadioInfo`-Pakete verwenden denselben unter **Log sync** konfigurierten Port. Dort wird jedoch getrennt festgelegt, ob KST4Contest QSO-Informationen, TRX-Informationen oder beide Paketarten verarbeitet.
Der allgemeine Listener ist für Logprogramme vorgesehen, die kompatible `RadioInfo`-Pakete senden. Dazu gehören abhängig von deren jeweiliger Konfiguration UCXLog, N1MM+, QARTest und DXLog.net. QSO- und `RadioInfo`-Pakete verwenden denselben unter **Log sync** konfigurierten Port. Dort wird jedoch getrennt festgelegt, ob KST4Contest QSO-Informationen, TRX-Informationen oder beide Paketarten verarbeitet. Eine automatische QRG-Quelle muss aktiviert sein und tatsächlich gültige Pakete liefern; die Aktivierung allein aktualisiert `MYQRG` nicht.
Wird der gemeinsame UDP-Port geändert, muss KST4Contest neu gestartet werden. Eine reine Änderung der Checkboxen wird dagegen sofort berücksichtigt.
@@ -144,7 +147,7 @@ Beide automatischen Quellen aktualisieren ausschließlich `MYQRG`. Das ist die e
Bei aktiviertem zweiten Chat bleibt dessen QRG davon unabhängig. Sie wird nicht aus den empfangenen TRX-Paketen abgeleitet und steht als `SECONDQRG` zur Verfügung. Dadurch kann beispielsweise die erste Kategorie automatisch der Frequenz des Logprogramms folgen, während für die zweite Kategorie eine eigene QRG von Hand eingetragen wird.
Sobald mindestens eine automatische QRG-Quelle aktiviert ist, wird das QRG-Feld der ersten Kategorie im Hauptfenster an den empfangenen Wert gebunden. Eine manuelle Eingabe in dieses Feld ist wieder möglich, wenn sowohl der allgemeine RadioInfo-Listener als auch die Win-Test-STATUS-Synchronisation deaktiviert sind.
Sobald mindestens eine automatische QRG-Quelle aktiviert ist und einen gültigen Wert liefert, folgt das QRG-Feld der ersten Kategorie im Hauptfenster der empfangenen Frequenz. Bleiben die erwarteten Pakete aus, sollte die Schnittstelle vor dem Contest geprüft werden. Eine manuelle Eingabe in dieses Feld ist möglich, wenn sowohl der allgemeine RadioInfo-Listener als auch die Win-Test-STATUS-Synchronisation deaktiviert sind.
### Haupt- oder Pass-Frequenz aus Win-Test
@@ -332,6 +335,8 @@ Die drei Audiofunktionen arbeiten unabhängig voneinander:
CW- und Sprachausgabe können gleichzeitig aktiviert werden. Das ist technisch möglich, im Contest aber nicht zwingend hilfreich. In der Praxis sollte nur die Ausgabe eingeschaltet werden, die im eigenen Stationsbetrieb tatsächlich wahrgenommen werden kann, ohne den Operator dauerhaft zu beschäftigen.
Die PM-bezogenen Audioausgaben reagieren nur auf Nachrichten, die tatsächlich an das eigene Login-Rufzeichen gerichtet sind. Öffentliche Nachrichten, die durch PM-Catching zusätzlich in der PM-Tabelle erscheinen, und dort eingeblendete Monitoring-Nachrichten bleiben akustisch still.
### Fallback-Band für relative QRG-Erkennung
Das Dropdown **Fallback band for relative QRG detection** legt fest, welches Band KST4Contest verwendet, wenn eine relative QRG keinem aktuellen Stationskontext zugeordnet werden kann.
@@ -374,12 +379,14 @@ Folgende Einstellungen und Schaltflächen gehören zur lokalen DX-Cluster-Ausgab
```text
Spotted callsign: DO5AMF
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: You donated $100!
Frequency: .300 des ausgewählten Fallback-Bandes
```
Bei einem Fallback-Band von `144 MHz` wird daraus beispielsweise eine Frequenz von ungefähr `144.300 MHz`.
Alle Spots verwenden eine feste, DXSpider-kompatible 75-Zeichen-Nutzzeile mit einem 30 Zeichen breiten Kommentarfeld. Längere Kommentare werden an dieser Protokollgrenze kontrolliert gekürzt; das DX-Rufzeichen wird dagegen nicht abgeschnitten. Rufzeichen mit mehr als zwölf Zeichen führen dazu, dass der betreffende Spot verworfen und protokolliert wird.
Der Kommentar des Testspots ist ein bewusst beibehaltenes Easteregg. Er hat keine technische Bedeutung und löst trotz seiner erfreulich konkreten Formulierung keine Zahlung aus. Entscheidend ist, dass der Spot im verbundenen Logprogramm erscheint.
Der Test funktioniert nur, wenn
@@ -574,7 +581,7 @@ KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivi
- **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.
Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in ihren jeweiligen Kategorien gesendet. Der zweite Beacon wird nur berücksichtigt, wenn die zweite Kategorie in derselben ON4KST-Sitzung aktiviert und vollständig synchronisiert ist.
### Intervall und Timer-Verhalten
@@ -857,6 +864,8 @@ Die interne SQLite-Datenbank speichert die contestbezogenen Zustände unabhängi
- manuell gesetzte NOT-QRV-Tags pro Band und
- gearbeitete vierstellige Großfelder pro Band.
Eine Ausnahme bilden Worked-Markierungen des Simplelogfile-Interpreters. Sie werden nur aus der ausgewählten Datei in den laufenden Zustand übernommen und nicht in SQLite persistiert. Die Datei ist die dauerhafte Quelle und besitzt keinen automatischen Contest-Reset. Ein Datenbank-Reset verändert oder leert sie nicht; enthaltene Rufzeichen werden bei der nächsten periodischen Auswertung erneut als gearbeitet markiert.
Als Schlüssel wird das normalisierte Rufzeichen ohne sichtbare Chat-Klammern oder Kategorieformatierung verwendet. Dadurch können aktive Varianten desselben Rufzeichens konsistent ausgewertet werden.
Worked- und NOT-QRV-Informationen laufen drei Tage nach ihrer letzten Änderung automatisch ab. Gespeicherte Großfelder laufen drei Tage nach dem zugehörigen Logeintrag ab. Ein manuelles Zurücksetzen vor jedem Contest ist deshalb normalerweise nicht erforderlich.
@@ -871,14 +880,17 @@ 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.
---
## Einstellungen speichern
Nach **jeder** Änderung **„Save Settings"** klicken! Ohne Speichern gehen alle Änderungen beim nächsten Start verloren.
**Save Settings** speichert die fachlichen Einstellungen und den vollständigen aktuellen Layoutstand. Änderungen an Fenstergrößen und -positionen, relevanten Dividern, verwalteten Tabellenbreiten sowie der Karteneinstellung **Group nearby stations** werden zusätzlich automatisch mit kurzer Verzögerung gespeichert. Ein ausstehender Layoutstand wird beim Programmende noch geschrieben.
- Speicherort: unter Linux `~/.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.
- Speicherort: unter Linux und macOS `~/.praktiKST/preferences.xml` und unter Windows `%USERPROFILE%\.praktiKST\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktiKST\preferences.xml`)
- Der automatische Layout-Writer übernimmt keine noch nicht mit **Save Settings** bestätigten fachlichen Änderungen.
- Die Konfigurationsversion 6 ergänzt optionale Spaltenbreiten unter `guiOptions`. Ältere `preferences.xml`-Dateien bleiben lesbar; fehlen Breiten oder sind Einträge ungültig, ermittelt KST4Contest wieder brauchbare Anfangsbreiten.
- Die Konfigurationsversion 7 ergänzt `GUIstationMapClusteringEnabled` unter `guiOptions`. Fehlt der Eintrag oder ist sein Wert unbrauchbar, bleibt die räumliche Kartengruppierung aktiviert.
- Ältere Programmversionen ignorieren die zusätzlichen XML-Einträge. Wenn eine ältere Version die Datei vollständig neu speichert, können die Spaltenbreiten und die gespeicherte Auswahl für **Group nearby stations** verloren gehen.
- Bei Problemen: Konfigurationsdatei löschen → KST4Contest erstellt eine neue mit Standardwerten.
+8 -6
View File
@@ -10,13 +10,15 @@ KST4Contest übernimmt gearbeitete Stationen aus dem Logprogramm und stellt dara
## Methode 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest liest eine Logdatei und sucht mit einem konfigurierbaren regulären Ausdruck nach Rufzeichen. Die Datei wird ausschließlich gelesen und nicht verändert. Auch binäre Logdateien können verwendet werden; nicht als Text interpretierbare Inhalte werden übersprungen.
KST4Contest liest die ausgewählte Textdatei einmal pro Minute und sucht darin mit einem fest eingebauten regulären Ausdruck nach Rufzeichen. Jedes gefundene Rufzeichen wird auf sein Basisrufzeichen normalisiert. Der globale Worked-Status gilt dadurch für alle derzeit aktiven Chat-Varianten dieses Rufzeichens.
Der Vorteil liegt in der breiten Kompatibilität: Die Funktion benötigt keine besondere Netzwerkschnittstelle des Logprogramms.
Die Grenze ist ebenso eindeutig: Aus einem reinen Rufzeichentreffer lassen sich weder Band noch Locator zuverlässig ableiten. Der Simplelogfile-Interpreter kann deshalb nur den globalen Worked-Status setzen. Er erzeugt keine bandbezogene `X`-Markierung, kein Worked-Großfeld und keine belastbare Grundlage für den Band-Upgrade-Hinweis nach einem Logeintrag.
Den Pfad der Logdatei und den regulären Ausdruck im Reiter **Log sync** eintragen. Für bandbezogene Auswertungen sollte nach Möglichkeit eine der Netzwerkschnittstellen verwendet werden.
Den Pfad der Textdatei im Reiter **Log sync** auswählen. Fehlt die Datei, legt KST4Contest sie an und zeigt einmalig einen nicht blockierenden Hinweis mit dem Pfad und den nächsten Prüfschritten an. Lese- oder Erstellungsfehler werden protokolliert; die minütliche Auswertung läuft beim nächsten Termin weiter. Für bandbezogene Auswertungen sollte nach Möglichkeit eine der Netzwerkschnittstellen verwendet werden.
Der aus dem Simplelogfile abgeleitete Worked-Status wird nicht in der internen SQLite-Datenbank gespeichert. Die ausgewählte Datei ist die dauerhafte Quelle und wird auch nach einem Neustart erneut eingelesen. Der Interpreter setzt nur positive Worked-Markierungen; er entfernt während der laufenden Programmsitzung keine bereits gesetzte Markierung und führt beim Wechsel zu einem neuen Contest keinen automatischen Reset durch. Auch ein manueller Datenbank-Reset ändert oder leert das Simplelogfile nicht. Darin enthaltene Rufzeichen werden bei der nächsten Auswertung innerhalb einer Minute erneut als gearbeitet markiert. Vor jedem Contest sollte deshalb geprüft werden, ob das Logprogramm genau die aktuelle Contestdatei beschreibt.
---
@@ -163,7 +165,7 @@ Neben der QSO-Synchronisation übertragen UCXLog und andere Programme auch die *
![FrequenzButtons](qrg_buttons.png)
**Ergebnis**: Die eigene QRG muss im Chat nie mehr manuell eingegeben werden ein Klick auf den MYQRG-Button oder die Verwendung der Variable im Beacon genügt.
**Ergebnis**: Eine aktivierte Schnittstelle aktualisiert `MYQRG`, sobald sie tatsächlich gültige Frequenzpakete liefert. Das Aktivieren allein erzeugt noch keine QRG. Kommen keine passenden Pakete an, muss die Schnittstelle geprüft oder die QRG nach dem Deaktivieren beider automatischen Quellen manuell gepflegt werden.
**Quellen für die eigene QRG (MYQRG):**
- UCXLog, N1MM+, DXLog.net, QARTest via UDP-Port 12060
@@ -191,7 +193,7 @@ Für DM5M-typische Setups (2 Radios, 2 Computer, eine KST4Contest-Instanz oder z
## Interne Datenbank
KST4Contest speichert Worked-, NOT-QRV- und Großfeldinformationen in einer eigenen SQLite-Datenbank. Sie ist von der Datenbank des Logprogramms unabhängig.
KST4Contest speichert Worked-, NOT-QRV- und Großfeldinformationen aus den Netzwerkschnittstellen sowie manuelle Markierungen in einer eigenen SQLite-Datenbank. Sie ist von der Datenbank des Logprogramms unabhängig. Simplelogfile-Treffer sind davon ausgenommen und werden bei jeder Programmsitzung aus der ausgewählten Datei neu abgeleitet.
Die Datenquellen liefern unterschiedlich genaue Informationen:
@@ -201,6 +203,6 @@ Die Datenquellen liefern unterschiedlich genaue Informationen:
| QSO-UDP-Listener | ja | ja, wenn im Paket enthalten | ja, wenn Band und Locator enthalten sind |
| Win-Test-Netzwerk-Listener | ja | ja | ja, wenn ein Locator enthalten ist |
Die Daten werden beim Programmstart wieder geladen und bei neuen Logeinträgen während des Betriebs aktualisiert. Sie laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist daher normalerweise nicht erforderlich.
Die in SQLite gespeicherten Daten werden beim Programmstart wieder geladen und bei neuen Logeinträgen während des Betriebs aktualisiert. Sie laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist daher normalerweise nicht erforderlich. Für den Simplelogfile-Interpreter gilt diese Lebensdauer nicht: Seine Datei bleibt die dauerhafte Quelle und wird durch einen Datenbank-Reset weder geändert noch geleert. Enthaltene Rufzeichen setzt die nächste periodische Auswertung erneut auf den globalen Worked-Status.
Ein vollständiger manueller Reset entfernt Worked-Markierungen, NOT-QRV-Tags und Worked-Großfelder gemeinsam. Weitere Einzelheiten: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
Ein vollständiger manueller Reset entfernt Worked-Markierungen, NOT-QRV-Tags und Worked-Großfelder gemeinsam. Weitere Einzelheiten: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

+18 -10
View File
@@ -2,7 +2,7 @@
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-AirScout-Integration)
AirScout (by DL2ALF) is a program for detecting aircraft for aircraft scatter operation. KST4Contest is tightly integrated with AirScout and shows reflectable aircraft directly in the user list.
AirScout (by DL2ALF) calculates aircraft-scatter opportunities from current aircraft positions. KST4Contest receives these results and displays aircraft suitable for the path to each remote station directly in the user list.
> **Aircraft Scatter** enables very long-distance communication on VHF and higher even for stations with low altitude above sea level or unfavourable topographic conditions.
@@ -15,9 +15,9 @@ Download AirScout from:
---
## Aircraft Data Feeds (ADSB)
## Aircraft Data Feeds (ADS-B)
Public aircraft data feeds on the internet are often unreliable and limited in use. A recommended alternative is the dedicated ADSB feed service provided by **OV3T (Thomas)**:
Public aircraft data feeds on the internet are often unreliable and of limited use. A recommended alternative is the dedicated ADS-B feed service provided by **OV3T (Thomas)**:
- https://airscatter.dk/
- https://www.facebook.com/groups/825093981868542
@@ -28,7 +28,7 @@ An account is required for this service. Please consider donating to Thomas
## Setting Up AirScout
### Step 1: Configure the ADSB Feed in AirScout
### Step 1: Configure the ADS-B Feed in AirScout
1. Start AirScout.
2. Enter your OV3T feed account details (username, password, URL) in the AirScout settings.
@@ -85,32 +85,40 @@ Otherwise, it may result in incorrect AP data.
## AP Column in the User List
After setup, an **AP column** appears in the user list showing up to two reflectable aircraft per station.
After setup, an **AP column** appears in the user list. For each station, it shows the arrival time and the AirScout reflection potential of the first two suitable aircraft.
Example display:
| Station | AP Info |
|---|---|
| DF9QX | 2 Planes: 0 min / 0 min, 100% each |
| F5DYD | 2 Planes: 14 min / 31 min, 50% each |
| DF9QX | 0 (100%) / 0 (100%) |
| F5DYD | 14 (50%) / 31 (50%) |
The number before the brackets is the number of minutes remaining until the calculated opportunity. The percentage is the reflection potential reported by AirScout. It is not a QSO probability.
AP information is also available in the **private messages window**.
The percentage indicates the reflection potential (aircraft size, altitude, distance).
## Effect on the Priority Score and Timeline
At least one aircraft reported as reachable by AirScout raises the station's Priority Score. An opportunity expected in zero, one or two minutes receives additional time-dependent weighting. AirScout is only one factor alongside Worked status, available bands, QRB, antenna direction, chat activity and skeds.
The AP and sked timeline uses the Priority Score to select interesting stations and places the next suitable aircraft-scatter opportunity on the time axis. The reflection potential also controls how the AP marker is displayed. The timeline remains a preview and does not guarantee a QSO.
---
## AP Variables in Messages
Aircraft data can be inserted directly into messages:
Aircraft data for the selected station can be inserted directly into shortcuts, snippets and other station-specific messages:
- `FIRSTAP` → e.g. `a very big AP in 1 min`
- `SECONDAP` → e.g. `Next big AP in 9 min`
Details: [Macros and Variables](en-Macros-and-Variables#variables)
Because these values require a selected remote station, `FIRSTAP` and `SECONDAP` are not available as global beacon variables.
---
## "Show Path in AirScout" Button
In the user list there is a button with an arrow showing the direction (QTF) to the selected station. Clicking it maximises AirScout and shows the path with reflectable aircraft to the selected contact.
In the user list there is a button with an arrow showing the direction (QTF) to the selected station. Clicking it maximises the external AirScout window and displays the path to the selected remote station together with the calculated aircraft-scatter opportunities. The button does not start a separate terrain or propagation calculation in KST4Contest.
+73 -2
View File
@@ -8,6 +8,72 @@ Published Stable versions and their application packages are available under [Gi
---
## v1.43.1 (2026-09-03)
**Corrected version metadata**
v1.43.1 contains the same functional changes as v1.43.0. It corrects the application and build metadata used for display, ON4KST identification and update comparison. Some metadata in the first v1.43.0 package set still identified the build as version 1.42.
### Fixed
- **Consistent semantic version:** The user-visible application version now uses the complete `1.43.1` form. The compact value `1.431` remains only in the deprecated numeric field required for compatibility with older update feeds.
- **Tagged-release update feed:** The website build, version-feed validation and artifact upload now run as actual workflow steps after the GitHub Release has been published. They were previously indented into the release action's artifact list and therefore skipped.
Users of v1.43.0 should install v1.43.1. The functionality is unchanged; the update only corrects the version metadata and release workflow.
The corrected version is available as [Release v1.43.1](https://github.com/praktimarc/kst4contest/releases/tag/v1.43.1).
---
## v1.43.0 (2026-09-03)
**More reliable log synchronisation, persistent layouts and better DX Cluster compatibility**
v1.43 concentrates on reliability around external log data and long-running contest operation. It also adds practical control over table layouts and station grouping on the map.
### Added
- **Optional map clustering:** **Group nearby stations** immediately enables or disables spatial grouping at lower zoom levels. The stored setting changes neither the viewport nor the selected station; aggregation of active variants sharing one base callsign remains independent. This implements [Issue #79](https://github.com/praktimarc/kst4contest/issues/79).
- **Automatic table-layout persistence:** Tables receive useful widths when their first meaningful contents arrive. Manually changed column widths, window sizes and relevant dividers are written to `preferences.xml` after a short delay. The main and monitor windows keep separate DXCluster and QSO-of-the-other layouts.
- **Tooltips for truncated table values:** Normal table cells expose their complete text when the visible column is too narrow, without replacing functional tooltips or clickable links.
- **Simplelogfile creation notice:** When the selected file does not exist, KST4Contest creates it and displays a notice with the file path, a concrete test procedure and a link to the relevant manual section.
### Changed
- **Robust Simplelogfile evaluation:** The selected file is evaluated once per minute and closed after every pass so that the logging application can replace or rotate it. Detected callsigns apply the global Worked state to every active suffix variant of the base callsign. Disabling the function prevents any file access, while read or creation errors no longer terminate the periodic task.
- **Consistent external logger bands:** UCXLog-compatible packets and Win-Test events use one shared band normalisation. Numeric values, metre and centimetre designators and the existing Win-Test IDs now set the same Worked flags and worked grid squares. Values including `2320`, `5760` and `10368` are handled reliably; a missing or unknown band sets only the global Worked state.
- **Compact full-frequency recognition:** Complete frequencies without a decimal separator are accepted across the supported bands, with the final three digits interpreted as the kHz part. Bare three-digit numbers still require recognisable frequency context so that signal reports and unrelated numbers are not treated as QRGs.
- **DXSpider-compatible spot format:** Local DX Cluster spots use a fixed 75-character payload line with the DX callsign in column 27, a 30-character comment field and UTC time in column 71. The format remains stable up to 24 GHz. Overlong DX callsigns are rejected and logged instead of being silently truncated. This resolves [Issue #86](https://github.com/praktimarc/kst4contest/issues/86).
- **Active ON4KST connection probe:** A quiet chat server is checked with an explicit session-wide probe before the connection is treated as dead. Heartbeats and probe frames retain the required CR/LF framing.
- **Reliable private-message age highlighting:** Incoming private messages use the defined green age levels for up to five minutes. Locally sent messages retain their separate style, and empty or reused table rows return to the normal design instead of keeping an obsolete highlight.
### Fixed
- **Worked state after login:** Persisted SQLite Worked information is loaded and applied before each initial ON4KST user list is published. Reconnects, both categories and all active variants of a base callsign therefore start with the correct state. This resolves [Issue #85](https://github.com/praktimarc/kst4contest/issues/85).
- **False disconnect during quiet periods:** A valid ON4KST connection is no longer closed merely because the server currently has no activity lines to send.
### Documentation and packaging
- The German and English manuals were revised against the implementation. A new contest-workflow chapter connects the individual functions into a practical operating sequence, while the sections on dual chat, private-message handling, QRG synchronisation, Simplelogfile evaluation and configuration were clarified.
- The website feature pages now describe band and direction opportunities, the station map, QRG handling, filters, global message views, private-message handling and logger synchronisation in more detail.
- The AUR package definitions were updated for v1.43.0.
The complete v1.43.0 functionality is available in [Release v1.43.0](https://github.com/praktimarc/kst4contest/releases/tag/v1.43.0). Because its embedded version metadata is inconsistent, v1.43.1 is the recommended package set.
---
## v1.42.0 (2026-08-22)
**Shared band context, session-based ON4KST connection and signed macOS packages**
@@ -34,6 +100,8 @@ v1.42 brings several previously separate calculations together. Band information
- **Manual QTF input:** The current antenna direction can be changed directly in KST4Contest when PSTRotator is not being used.
- **`MYQTF` message variable:** The current antenna direction can be inserted as a numeric angle in degrees into shortcuts, snippets and other supported message texts.
- **Filter reset:** A dedicated reset button reliably removes the active user-list filter predicates.
- **Map clustering:** Stations close to each other are grouped at lower zoom levels. The selected station and relevant direction opportunities remain individually visible.
@@ -70,7 +138,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.
@@ -80,6 +148,8 @@ v1.42 brings several previously separate calculations together. Band information
- **DXLog full-log import:** In addition to `contactinfo`, the UCXLog-compatible UDP listener processes `contactreplace`. This allows a complete log broadcast by DXLog.net to be imported.
- **Guarded automatic QRG updates:** `MYQRG` is updated only by an enabled interface which actually supplies valid `RadioInfo` or Win-Test `STATUS` packets. An enabled source which provides no data does not remove the need for a functional check or manual QRG maintenance.
- **Improved version comparison:** Versions are compared semantically so that patch releases and Nightly versions are not misclassified by conversion to a floating-point number.
### Fixed
@@ -92,6 +162,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.
@@ -408,7 +480,6 @@ First publicly released version. Core features:
## Planned Features
- `MYQTF` variable (own antenna direction as text)
- ~~Lifetime for worked status (automatic reset)~~**Implemented in v1.40** (3-day lifetime, no manual reset needed anymore)
- Filtering the "Cluster & QSO of others" window to own QTF
- Further topography-based calculations for direction warnings
+46 -22
View File
@@ -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**: Use **Save Settings** for functional settings which should remain in effect after the next start. KST4Contest saves layout changes such as window sizes, divider positions and table-column widths automatically. The shared file is `~/.praktiKST/preferences.xml` on Linux and macOS and `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows.
---
@@ -14,14 +14,15 @@ After the first start, the **settings window** opens this is the central sta
### Login and Chat Categories
Enter your ON4KST chat credentials here (callsign and password).
Also, select the **primary chat category** (e.g., IARU Region 1 VHF/Microwave).
Enter the one local login callsign and password used for the ON4KST chat. Also select the **primary chat category** (e.g. IARU Region 1 VHF/Microwave).
With the option for a **second chat** (Multi-Channel Login), you can log in to another category simultaneously (e.g., UHF/SHF). Both chats will then be monitored in parallel. You can optionally specify a different login name for the second chat (useful for Opposite Station Multi-Callsign Logging).
The **second chat** option (Multi-Channel Login) adds another category (e.g. UHF/SHF) to the same ON4KST TCP session through Single Sign-on. KST4Contest does not open a second TCP connection and does not use another local login callsign or password.
**Name in Chat 2** configures only the visible, category-specific name field for the second category. It is neither another login nor a message destination. The visible name field, message context, QRG and beacon remain separate for each category. **Opposite Station Multi-Callsign Login Tagging**, by contrast, refers exclusively to remote stations which appear in the chat under several complete visible callsign variants.
### Callsign and Locator
Enter your own callsign and Maidenhead locator (6 characters, e.g., `JN49IJ`). These values are needed for distance and direction calculations.
Enter your own callsign and Maidenhead locator (6 characters, e.g. `JN49IJ`). The locator is not part of authentication. ON4KST shares it between both categories in the one TCP session, and KST4Contest also uses it for distance and direction calculations.
### Active Bands
@@ -40,13 +41,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
@@ -117,13 +128,15 @@ The **Log sync** tab selects the sources from which KST4Contest imports worked s
| **General QSO UDP listener** | QSO packets from UCXLog, QARTest, N1MM+ and DXLog.net | global and per-band Worked status plus worked grid square where both band and locator are transmitted |
| **Win-Test network listener** | native Win-Test network packets | global and per-band Worked status, locator information and, depending on the settings, QRG synchronisation and sked handover |
The file-based interpreter is mainly useful when the logging application provides no supported network interface. A callsign match alone, however, contains neither a reliable band nor a locator. Use one of the network listeners wherever possible if per-band information is required.
The file-based interpreter reads the selected text file once per minute using a fixed callsign pattern. Matches are normalised to base callsigns and set only the global Worked status for all active variants. It is mainly useful when the logging application provides no supported network interface. A callsign match alone contains neither a reliable band nor a locator, so use one of the network listeners wherever possible if per-band information is required.
If the selected file does not exist, KST4Contest creates it and displays its full path together with checks for initial setup and the next contest. The file itself is the durable source; Worked marks derived from it are not stored in SQLite and are not reset automatically for a new contest.
The general QSO UDP listener is the recommended interface for UCXLog, QARTest, N1MM+ and DXLog.net. QSO and `RadioInfo` packets use the same configurable UDP port; the default is `12060`. Separate options in **Log sync** and **TRX sync** determine whether the received QSO and frequency information is processed.
Win-Test uses its own network protocol and therefore has a separate listener. Its default port is `9871`. If this port is changed while the listener is enabled, KST4Contest restarts the Win-Test listener on the new port. After changing the shared UDP port `12060`, KST4Contest must instead be restarted completely.
All enabled input paths may be used in parallel. Their Worked information is merged into the same internal database; identical reports do not create separate Worked states. KST4Contest must be running when a QSO is saved unless the logging application can resend the existing log.
All enabled input paths may be used in parallel and identical reports do not create separate Worked states. Network-derived Worked information is stored in the internal database. Simplelogfile marks remain runtime state derived from the selected file. Before using Simplelogfile, check whether the logging application already provides one of the supported network interfaces. KST4Contest must be running when a network QSO is transmitted unless the logging application can resend the existing log.
Configuration of the individual logging applications, band and locator handling, and the Win-Test sked handover are described under [Log Synchronisation](en-Log-Sync).
@@ -143,7 +156,7 @@ TRX synchronisation imports the current frequency from the logging application a
| **Win-Test STATUS** | `Win-Test STATUS QRG Sync` | Processes the main or pass frequency from native Win-Test `STATUS` packets. The Win-Test listener uses its separately configured port, which defaults to `9871`. |
| **Manual entry** | Disable both automatic QRG sources | The local QRG can be entered manually in the main window. |
The general listener is intended for logging applications which transmit compatible `RadioInfo` packets. Depending on their individual configuration, this includes UCXLog, N1MM+, QARTest and DXLog.net. QSO and `RadioInfo` packets use the same port configured under **Log sync**, but separate options determine whether KST4Contest processes QSO information, TRX information or both packet types.
The general listener is intended for logging applications which transmit compatible `RadioInfo` packets. Depending on their individual configuration, this includes UCXLog, N1MM+, QARTest and DXLog.net. QSO and `RadioInfo` packets use the same port configured under **Log sync**, but separate options determine whether KST4Contest processes QSO information, TRX information or both packet types. An automatic QRG source must be enabled and actually supply valid packets; enabling it alone does not update `MYQRG`.
Restart KST4Contest after changing the shared UDP port. Changes to the two QRG-sync checkboxes take effect immediately.
@@ -153,7 +166,7 @@ Both automatic sources update `MYQRG` only. This is the local QRG of the first o
If a second chat is enabled, its QRG remains independent. It is not derived from incoming TRX packets and is available through `SECONDQRG`. The first category can therefore follow the logging application's frequency automatically while a separate QRG is entered manually for the second category.
As soon as at least one automatic QRG source is enabled, the first category's QRG field in the main window is bound to the received value. Manual entry becomes available again when both the general RadioInfo listener and Win-Test STATUS synchronisation are disabled.
When at least one automatic QRG source is enabled and supplies a valid value, the first category's QRG field in the main window follows the received frequency. If the expected packets do not arrive, verify the interface before the contest. Manual entry is available when both the general RadioInfo listener and Win-Test STATUS synchronisation are disabled.
### Main or Pass Frequency from Win-Test
@@ -363,11 +376,15 @@ AirScout setup, aircraft display and the meaning of the returned AP information
![Notifications, DX cluster output and QSO monitoring](client_settings_window_notification.png)
Three notification types are available:
The three audio functions work independently:
1. **Simple sounds**: TADA sound for incoming messages, tick for sked direction detection, etc.
2. **CW announcement**: The callsign of a station sending a private message is output as a CW signal.
3. **Phonetic announcement**: The callsign is pronounced phonetically.
1. **Simple sounds**: Plays short cues for new private messages, detected directional opportunities, sked reminders and `BAND+` hints.
2. **CW announcement**: Spells the sender's callsign in CW for a new private message.
3. **Phonetic announcement**: Speaks the sender's callsign phonetically for a new private message.
Each function can be enabled separately. CW and phonetic output can also be active at the same time.
PM-related audio output is triggered only by messages actually directed to the local login callsign. Messages shown additionally through PM Catching and messages added through QSO Monitoring remain silent.
### Fallback Band for Relative QRG Detection
@@ -411,12 +428,14 @@ The following settings and controls belong to the local DX cluster output:
```text
Spotted callsign: DO5AMF
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: You donated $100!
Frequency: .300 on the selected fallback band
```
With `144 MHz` selected as the fallback band, the resulting frequency is approximately `144.300 MHz`.
All spots use a fixed, DXSpider-compatible 75-character payload line with a 30-character comment field. Longer comments are deliberately truncated at this protocol boundary; the DX callsign is not. A callsign longer than twelve characters causes the affected spot to be rejected and logged.
The comment is a deliberately retained Easter egg. It has no technical meaning and, despite being remarkably specific, does not initiate a payment. Its practical purpose is to make the test spot easy to identify in the logging software.
The test works only if
@@ -562,7 +581,7 @@ KST4Contest uses one shared timer for both chat categories. Each category nevert
- **Beacon message** contains the public message for that category.
- **Shared beacon interval** sets the common interval used by both categories.
When both beacons are enabled, they are sent one after the other in their respective categories during the same timer run. The second beacon is only considered while the second chat is enabled and connected.
When both beacons are enabled, they are sent one after the other in their respective categories during the same timer run. The second beacon is only considered while the second category is enabled and fully synchronised in the same ON4KST session.
### Interval and timer behaviour
@@ -902,6 +921,8 @@ The internal SQLite database stores contest-related state independently of the l
- manually assigned NOT-QRV marks per band, and
- worked four-character grid squares per band.
Worked marks from the Simplelogfile interpreter are the exception. They are copied from the selected file into runtime state only and are not persisted in SQLite. The file is the durable source and has no automatic contest reset. A database reset does not change or empty it; callsigns contained in the file are marked as worked again during the next periodic evaluation.
The normalised callsign, without visible chat brackets or category formatting, is used as the key. This allows active variants of the same callsign to be evaluated consistently.
Worked and NOT-QRV information expires automatically three days after its most recent change. Stored grid squares expire three days after the corresponding log entry. A manual reset before every contest is therefore normally unnecessary.
@@ -916,14 +937,17 @@ 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.
---
## Saving Settings
Click **"Save Settings"** after **every** change! Without saving, all changes will be lost on the next start.
**Save Settings** stores functional settings and the complete current layout. Changes to window sizes and positions, relevant dividers, managed table-column widths and the **Group nearby stations** map setting are also saved automatically after a short delay. Any pending layout update is written when the programme exits.
- Storage location: `~/.praktikst/preferences.xml` on Linux 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.
- Storage location: `~/.praktiKST/preferences.xml` on Linux and macOS and `%USERPROFILE%\.praktiKST\preferences.xml` (or `C:\Users\<Username>\.praktiKST\preferences.xml`) on Windows
- The automatic layout writer does not copy functional changes which have not yet been confirmed with **Save Settings**.
- Configuration version 6 adds optional column-width entries below `guiOptions`. Older `preferences.xml` files remain readable. Missing or invalid widths simply cause KST4Contest to calculate useful initial widths again.
- Configuration version 7 adds `GUIstationMapClusteringEnabled` below `guiOptions`. If the entry is missing or unusable, spatial map clustering remains enabled.
- Older programme versions ignore the additional XML entries. If an older version rewrites the complete file, column widths and the stored **Group nearby stations** choice may be lost.
- If you encounter problems: delete the configuration file → KST4Contest will create a new one with default values.
+16 -1
View File
@@ -67,6 +67,8 @@ Enable only interfaces which are actually required and have already been tested:
A contest is not an ideal time to investigate radio conditions and a newly enabled network interface at the same time.
Before each contest, verify that the logging application writes the current contest file to the path selected for the Simplelogfile. A test QSO should appear as globally worked within one minute. KST4Contest does not reset Worked marks derived from this file automatically when the contest changes.
---
## Basic Contest Cycle
@@ -89,7 +91,7 @@ KST4Contest keeps the required information together between these steps. Changin
## CQ Operation
During operation on a mainly fixed CQ frequency, `MYQRG` and `SECONDQRG` should match the frequencies actually in use. Enabled TRX synchronisation can update `MYQRG` automatically. Without an automatic source, the value must be maintained manually.
During operation on a mainly fixed CQ frequency, `MYQRG` and `SECONDQRG` should match the frequencies actually in use. TRX synchronisation can update `MYQRG` automatically only when it is enabled and actually supplies valid packets. Before the contest, verify this with a real frequency change. Without a working automatic source, the value must be maintained manually.
The beacon can publish the current QRG, locator and antenna direction in the chat at regular intervals. Its variables are evaluated again for every transmission.
@@ -246,6 +248,19 @@ G1YBB uses the directional indication particularly consistently. Stations highli
KST4Contest does not automate the QSO. Its advantage is that QRG, direction, aircraft scatter information and other evaluation data are already available when the opportunity appears. The remaining task is a quick operating decision.
Another particularly consistent operating method uses the station map as a geographical worklist:
1. The **wkd** filter removes already worked callsigns from the user list and therefore from the map.
2. G1YBB selects an interesting station on the map.
3. **Trigger cluster spot** passes the known QRG to Minos.
4. Selecting the spot moves Minos to that QRG.
5. The completed QSO is logged.
6. Log synchronisation updates the Worked state, and the filter removes the station from both the user list and the map.
The map consequently becomes a spatial list of the stations still to be worked. This workflow is optional. It requires reliable log synchronisation and a working DX Cluster connection and is mainly useful for operators who deliberately want to organise the contest in this geographical way.
[Watch G1YBB demonstrate this workflow.](https://www.youtube.com/watch?v=lMQZMiSHlUI)
---
## Optional Interfaces in the Workflow
+208 -45
View File
@@ -2,99 +2,262 @@
> 🇬🇧 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.
![Notification settings and local DX Cluster server](client_settings_window_notification.png)
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
Comment: Testing DXC-Spot: Congrats, you donated $100!
Comment: DXC test: 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
The payload line uses a fixed, DXSpider-compatible 75-character format. The DX callsign starts in column 27, the comment field is exactly 30 characters wide and the UTC time starts in column 71. Short comments are padded with spaces; longer ones are deliberately limited to 30 characters. Different spotter-callsign lengths and frequencies up to 24 GHz do not move the following fields.
The logging software can then display the spot in the band map and tune the TRX to that frequency with a mouse click.
KST4Contest does not truncate the complete DX callsign. If it exceeds twelve characters, the spot is rejected in a controlled manner and the reason is logged.
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.
An automatic comment with AirScout information may look like this:
```text
JO51HK AP 1m/100%;4m/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+
- Minos
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)
+144 -37
View File
@@ -6,45 +6,122 @@ 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.
![Geometry of the assumed antenna corridors](directional_opportunity_geometry.png)
In the example, the green corridor runs from F5FEN towards DM5M. When F5FEN sends a directed message to DM5M, KST4Contest checks whether the local station lies inside this corridor. A reply from DM5M to F5FEN is calculated again in the opposite direction: the blue corridor then starts at DM5M and points towards F5FEN. Each message therefore evaluates the assumed antenna direction of its sender.
| 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.
![Detected directional opportunity in the user list](direction_opportunity_highlight.png)
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`, `144307`, `10368100` | 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.
A complete frequency may also be written without a dot or comma. KST4Contest treats the final three digits as the kHz part: `144307` in a station name becomes `144.307 MHz`, while `10368100` in a public or directed chat message becomes `10368.100 MHz`. The value is accepted only if the resulting frequency falls within a supported band range.
### 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).
---
@@ -62,7 +139,7 @@ This distinction matters. A callsign already worked on one band may still be use
[Log Synchronisation](en-Log-Sync) imports new QSOs from the logging application. The amount of information available depends on the interface being used:
- The file-based Simplelogfile interpreter detects callsigns only. It can therefore set only the global Worked status.
- The file-based Simplelogfile interpreter reads the selected text file once per minute using a fixed callsign pattern. Matches are normalised to the base callsign and set only the global Worked status for all active variants.
- The QSO UDP interfaces and the Win-Test network listener can also provide the band.
- If the log packet contains a valid locator, KST4Contest additionally stores the worked four-character grid square for that band.
@@ -131,7 +208,9 @@ In plain terms: an automatically detected hint means "probably active on this ba
### Storage and lifetime
Worked, NOT-QRV and worked-grid information is stored in the internal SQLite database and restored on the next start. Entries expire automatically after three days, so a reset before every contest is normally unnecessary.
Worked, NOT-QRV and worked-grid information received from network interfaces, together with manual marks, is stored in the internal SQLite database and restored on the next start. Entries expire automatically after three days, so a reset before every contest is normally unnecessary.
Simplelogfile matches are excluded. They are not persisted in SQLite but are derived from the selected file during each application session. The file is therefore the durable source. The interpreter does not remove an existing Worked mark during the current session and does not reset the state automatically for a new contest. A manual database reset does not change or empty the file. Callsigns contained in it are marked as worked again when the file is next evaluated within one minute.
A manual reset under **Workedstn database** removes all Worked marks, NOT-QRV marks and stored worked grid squares. The known callsign rows remain in the database. See [Worked Station Database Settings](en-Configuration#worked-station-database-settings) for details.
@@ -161,19 +240,33 @@ The filters above the user list use the same information as the Worked columns:
Several active filters are applied together. A station remains visible only if it satisfies every selected condition. The filters react immediately to new log entries and changed NOT-QRV marks.
Operation and layout of the filter bar: [User Interface Filters](en-User-Interface#filters).
Operation and layout of the filter bar: [User Interface Filters](en-User-Interface#filters-and-reachability-controls).
---
## 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!)*
---
## PM Catching
## PM Catching (from v1.1)
Some users accidentally post direct messages publicly, e.g.:
@@ -181,7 +274,17 @@ Some users accidentally post direct messages publicly, e.g.:
(DM5M) pse ur qrg
```
KST4Contest detects such messages that contain your own callsign and automatically sorts them into the **private messages table**. No messages are missed this way.
KST4Contest searches the text of every message not sent by the local station for the configured local login callsign without distinguishing upper- and lower-case letters. A match also appears in the **private messages table**. This applies to public messages addressed to `ALL` and to directed messages between other chat participants.
A directed message between two other stations is where the slightly flippant description **“gossip detection”** fits best. It is a nickname, not the formal name of the function.
The original message remains unchanged: its actual receiver remains `ALL` or the other chat participant; its text, chat category and routing are retained as well. PM Catching merely adds another view of the same message.
The text search cannot infer intention. A typing error or shortened callsign is not recognised. Conversely, a simple mention of the complete callsign can produce a match even when no reply was expected.
Selecting an incoming row in the PM table prepares a reply to its sender. For an outgoing message from the local station, the original receiver is restored as the target instead. Selecting the row does not send the message.
Caught rows use the normal PM-table age display. They do not trigger the simple PM sound, CW callsign output or phonetic callsign output. The same applies to messages shown additionally through QSO Monitoring.
---
@@ -208,21 +311,21 @@ Configuration, recognised QRG requests and category handling: [Configuration
## Multi-Channel Login (from v1.26)
Simultaneous login to **two chat categories** (e.g. 144 MHz and 432 MHz). Both chats are monitored in parallel.
KST4Contest signs in to ON4KST once with one local login callsign and password. One locator is shared by the entire TCP session. A second chat category (e.g. 144 MHz and 432 MHz) is added through Single Sign-on within the same session. Both categories are monitored in parallel, while the visible category-specific name field, message context, QRG and beacon remain separate for each category.
---
## 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.
---
## Opposite Station Multi-Callsign Login Tagging (from v1.26)
Support for stations that are active in the chat with multiple callsigns simultaneously (e.g. expedition setups).
Support for remote stations which are active in the chat under several complete visible callsign variants at the same time (e.g. expedition setups). The complete callsign and chat category remain separate participant identities, while Worked, band and priority data are shared through the base callsign.
---
@@ -338,7 +441,7 @@ Sniffed: (DN9APW-70 > 9A0BB-23) pse sked 19:30
This distinction is intentional. Monitoring aggregates the variants so that their communication remains visible. Message routing does not aggregate them because the intended login would otherwise become ambiguous.
The base callsign is evaluated across both connected chat categories. The category of each individual message remains unchanged.
The base callsign is evaluated across both chat categories in the shared ON4KST session. The category of each individual message remains unchanged.
The monitoring view includes:
@@ -503,7 +606,7 @@ The score is calculated for the normalised base callsign. A sked entered for an
### How are multiple suffixes and chat categories handled?
Active callsigns such as `9A0BB-2`, `9A0BB-70`, `9A0BB-23` and `9A0BB-13` remain separate chat members. Messages can therefore still be addressed to the complete callsign in the correct chat category.
Active complete visible callsign variants such as `9A0BB-2`, `9A0BB-70`, `9A0BB-23` and `9A0BB-13` remain separate chat members. Messages can therefore still be addressed to the complete callsign in the correct chat category.
Worked, band, NOT-QRV and score information belongs to the common base callsign `9A0BB`. The score is calculated once and projected to all active variants. The user list may consequently contain several separate rows with the same score, while the priority list contains only one entry for the base station.
@@ -604,7 +707,7 @@ The timeline is a preview. AirScout data can change, and a stored sked guarantee
KST4Contest can send recurring CQ messages to the public chat. The beacon is intended for longer periods of calling CQ on a fixed frequency: it publishes the local QRG regularly without requiring the operator to enter the same text again.
Both chat categories use one shared interval, but each category has its own enable setting and message template. The second beacon is only sent while the second chat is enabled and connected.
Both chat categories use one shared interval, but each category has its own enable setting and message template. The second beacon is only sent while the second category is enabled and fully synchronised in the shared ON4KST session.
Global variables such as `MYQRG`, `SECONDQRG`, `MYLOCATOR` and `MYQTF` are resolved immediately before every transmission. A QRG updated by the logging software can therefore appear in the next beacon.
@@ -619,7 +722,7 @@ Configuration, timer behaviour and available variables: [Configuration Beaco
## Simplelogfile
File-based log evaluation using regex. Details: [Log Synchronisation](en-Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
The Simplelogfile interpreter reads callsigns from a selected text file once per minute. If the file is missing, KST4Contest creates it and displays a non-blocking notice with its path and the initial setup and contest checks. Details: [Log Synchronisation](en-Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Global Message Views
@@ -717,12 +820,16 @@ Marker colours provide a compact status indication:
|---|---|
| Blue | Normal station marker |
| Yellow | The callsign has already been worked on at least one band |
| Green | The station is inside the current antenna sector and is relevant as a directional candidate |
| Green | A directional opportunity has been derived for the station from directed chat messages |
| Orange | Currently selected station |
The selected state has the highest display priority, followed by the directional warning and Worked state. A selected station therefore remains orange even if it also meets one of the other conditions.
At lower zoom levels, nearby markers are combined into screen-based clusters. This is a display function and does not merge the underlying chat members. Selected stations and important directional candidates remain individually visible where possible.
The **Group nearby stations** checkbox controls spatial grouping. When selected, nearby markers are combined into screen-based clusters at lower zoom levels. The number inside a cluster shows how many stations it contains. Clicking a cluster zooms in but does not select an individual station. Selected stations and important directional candidates remain individually visible where possible.
Clearing **Group nearby stations** displays every positionable station as an individual marker at every zoom level. The change takes effect immediately without reloading station data or changing the current zoom, viewport or selection. KST4Contest saves the setting automatically and restores it at the next start. Existing installations initially keep clustering enabled and therefore retain the previous behaviour.
Screen-based clustering is separate from base-callsign aggregation. Active variants of the same normalised base callsign continue to share one geographical marker where applicable. Disabling **Group nearby stations** does not split that marker and does not change chat identities, filters or station data.
Clicking a station marker selects the corresponding active chat member in the main window. KST4Contest scrolls to the entry in the user list, updates the **Further Info** panel and prepares the complete visible callsign as the message target. The chat suffix and category therefore remain relevant even though several variants may share one map marker.
@@ -890,4 +997,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.
+2
View File
@@ -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/)
+9 -7
View File
@@ -10,17 +10,19 @@ KST4Contest imports worked stations from the logging application and derives the
## Method 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest reads a log file and searches it for callsigns using a configurable regular expression. The file is read only and is never modified. Binary log files can also be used; content which cannot be interpreted as text is skipped.
KST4Contest reads the selected text file once per minute and searches it for callsigns using a fixed built-in regular expression. Each match is normalised to its base callsign, so the global Worked status applies to every currently active chat variant of that callsign.
The advantage is broad compatibility: no dedicated network interface is required from the logging application.
The limitation is equally clear. A callsign match alone provides neither a reliable band nor a locator. The Simplelogfile interpreter can therefore set only the global Worked status. It does not create a per-band `X`, a worked-grid record or a reliable basis for the band-upgrade hint after a log entry.
Configure the log-file path and regular expression in the **Log sync** tab. Use one of the network interfaces where possible if band-specific information is required.
Select the text-file path in the **Log sync** tab. If the file is missing, KST4Contest creates it and displays a one-time, non-blocking notice with the path and the checks to perform next. Read or creation errors are logged; the scheduled task continues with its next one-minute pass. Use one of the network interfaces where possible if band-specific information is required.
Worked status derived from the Simplelogfile is not stored in the internal SQLite database. The selected file is the durable source and is read again after every restart. The interpreter only adds positive Worked marks; it does not remove an existing mark during the current application session and does not reset automatically for a new contest. A manual database reset does not change or empty the Simplelogfile either. Callsigns contained in it are marked as worked again when the file is next evaluated within one minute. Before each contest, verify that the logging application is writing the current contest log to this exact file.
---
# Method 2: Network Listener for QSO UDP Packets Recommended
## Method 2: Network Listener for QSO UDP Packets Recommended
UCXLog, QARTest, N1MM+ and DXLog.net can transmit a UDP packet when a QSO is saved. KST4Contest receives these packets on port `12060` by default and imports the callsign together with any band and locator information they contain.
@@ -164,7 +166,7 @@ In addition to QSO synchronisation, UCXLog and other programs also transmit the
![Frequency Buttons](qrg_buttons.png)
**Result**: Your own QRG never needs to be typed manually in the chat clicking the MYQRG button or using the variable in the beacon is sufficient.
**Result**: An enabled interface updates `MYQRG` when it actually supplies valid frequency packets. Enabling an interface does not create a QRG on its own. If no suitable packets arrive, check the interface or disable both automatic sources and maintain the QRG manually.
**Sources for your own QRG (MYQRG):**
- UCXLog, N1MM+, DXLog.net, QARTest via UDP port 12060
@@ -192,7 +194,7 @@ For DM5M-style setups (2 radios, 2 computers, one KST4Contest instance or two se
## Internal Database
KST4Contest stores Worked, NOT-QRV and worked-grid information in its own SQLite database. This database is independent of the logging application's database.
KST4Contest stores Worked, NOT-QRV and worked-grid information received from network interfaces, together with manual marks, in its own SQLite database. This database is independent of the logging application's database. Simplelogfile matches are excluded and are derived again from the selected file in each application session.
The input sources provide different levels of detail:
@@ -202,6 +204,6 @@ The input sources provide different levels of detail:
| QSO UDP listener | yes | yes, if included in the packet | yes, if both band and locator are available |
| Win-Test network listener | yes | yes | yes, if a locator is available |
The information is restored when KST4Contest starts and updated during operation when new log entries arrive. It expires automatically after three days, so a reset before every contest is normally unnecessary.
The information stored in SQLite is restored when KST4Contest starts and updated during operation when new log entries arrive. It expires automatically after three days, so a reset before every contest is normally unnecessary. This lifetime does not apply to the Simplelogfile interpreter: its file remains the durable source and is neither changed nor emptied by a database reset. Callsigns contained in it set the global Worked status again during the next periodic evaluation.
A complete manual reset removes Worked marks, NOT-QRV marks and worked grid squares together. See [Worked Station Database Settings](en-Configuration#worked-station-database-settings) for details.
A complete manual reset removes Worked marks, NOT-QRV marks and worked grid squares together. See [Worked Station Database Settings](en-Configuration#worked-station-database-settings) for details.
+30 -10
View File
@@ -4,7 +4,7 @@
## Connecting to the Chat
Before connecting for the first time, configure at least the callsign, password, locator and primary chat category in the settings window. If a second category is required, its login must also be enabled and configured completely.
Before connecting for the first time, configure the local login callsign and password, the locator shared by the TCP session, and the primary chat category in the settings window. If a second category is required, enable and select it there. KST4Contest signs in once and adds the second category through Single Sign-on within the same TCP session.
The connection can be started in two ways:
@@ -54,11 +54,19 @@ Both indicators flash for approximately twelve seconds and then disappear. Their
### PM Window (top left)
The PM window shows private messages addressed to the local chat logins and the corresponding outgoing replies.
The PM window shows private messages addressed to the local login callsign and the corresponding outgoing replies.
Messages not sent by the local station are also shown when their text contains the configured local login callsign, ignoring letter case. This applies to public messages addressed to `ALL` and to directed messages between other chat participants. The informal description **“gossip detection”** is particularly apt for the latter case.
The actual receiver, message text, chat category and routing remain unchanged. The message merely gains an additional representation in the PM window.
If [QSO Monitoring](en-Features#qso-sniffer-from-v131) is enabled, it additionally shows captured messages involving the monitored base callsigns. These entries receive a `Sniffed:` prefix containing the complete visible sender and receiver callsigns.
New messages are initially highlighted and then gradually return to the normal table colour. This highlighting only indicates the age of the message; it does not change its content or routing.
New rows not sent by the local station pass through six green age levels and return to the normal table colour after five minutes. Messages sent by the local station retain their separate highlight. The colour only indicates message age; it does not change content or routing.
Selecting an incoming row prepares a reply to the sender. For an outgoing message from the local station, the original receiver is restored as the message target instead. Caught and monitored rows do not trigger PM audio output.
Age levels: [Coloured PM Rows](en-Features#coloured-pm-rows-from-v125). Recognition and limitations: [PM Catching](en-Features#pm-catching-from-v11).
### User List (Chat Members)
@@ -82,7 +90,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
@@ -146,7 +154,15 @@ The value affects, among other things:
---
## Message Tables
## Table Widths and Truncated Cell Values
When useful data first becomes available, KST4Contest sizes the columns of the user list, message views, DXCluster and QSO tables, and Worked database once from their headings and existing contents. Stored widths take precedence. The initial widths of **Name**, **AP** and **NOT QRV @** are capped so that one long value cannot displace the rest of the table. **Message** and similar free-text columns remain flexible and do not follow the longest message.
Manually changed column widths are saved automatically and restored at the next start. Later messages or station updates do not overwrite that choice.
If a normal text value does not fit in its cell, a tooltip shows the complete value. It appears only when the displayed text is actually truncated. Functional tooltips for QRA, Worked, band and similar states remain available. If such a cell is also truncated, the tooltip contains both the full value and the functional explanation.
### Message Text and Links
KST4Contest deliberately displays message text on a single line. This keeps a larger number of entries visible when chat activity is high. The disadvantage is obvious: if the **Message** column is narrow, not every message fits completely into its cell.
@@ -323,7 +339,11 @@ A single station marker can be selected directly. KST4Contest then:
Chat logins with the same normalised base callsign and position may share one marker. They nevertheless remain separate message targets inside KST4Contest.
Markers which are too close together at the current zoom level are displayed as a cluster containing the number of stations. Clicking the cluster zooms into that area. A concrete station is selected only after an individual marker becomes visible and is clicked.
When **Group nearby stations** is selected, markers which are too close together at lower zoom levels are displayed as a cluster containing the number of stations. Clicking the cluster zooms into that area. A concrete station is selected only after an individual marker becomes visible and is clicked. Clearing the checkbox displays every positionable station as an individual marker at every zoom level.
The change takes effect immediately without changing the current zoom, viewport or station selection. It is saved automatically and restored at the next programme start. If no value has been stored yet, **Group nearby stations** remains selected so existing installations retain the previous behaviour.
This switch controls only the spatial clusters on screen. Active chat variants of the same normalised base callsign may still share one geographical marker and remain separate message targets regardless of this setting.
For a selected station, the header additionally shows:
@@ -428,7 +448,7 @@ KST4Contest additionally opens the **Cluster & QSO of the other** window. It sho
![Separate monitor window for DX cluster traffic and directed messages between other stations](cluster_qso_monitor.png)
The vertical divider position and window size are stored together with the other UI settings. Use **Save Settings** after changing them.
The vertical divider position and window size are saved automatically. The DXCluster and QSO tables use their own column widths in this window, so changing the monitor layout does not alter the corresponding main-window tabs.
The window can be hidden and restored through:
@@ -474,13 +494,13 @@ Functions which communicate with the server are available only after the ON4KST
## Window Sizes and Dividers
When **Save Settings** is clicked, KST4Contest stores the programme-window sizes and the positions of the relevant dividers in the configuration file. These values are reused at the next start.
KST4Contest automatically stores programme-window sizes and positions, relevant dividers and manually changed table-column widths in the configuration file after a short delay. Any pending layout update is written when the programme exits. **Save Settings** is not required for these changes, but still stores the complete current state, including the layout.
The main window is additionally checked against the visible area of the primary screen during startup. If the stored size is too large, KST4Contest reduces and moves the window so that it remains accessible. The complete process is described under [Screen-Aware Main Window Sizing](en-Features#screen-aware-main-window-sizing-from-v141).
The other programme windows do not currently use this additional size restriction. If, for example, the separate monitor window appears too large after moving to a smaller screen, its size must be corrected manually and stored again using **Save Settings**.
The other programme windows do not currently use this additional size restriction. If, for example, the separate monitor window appears too large after moving to a smaller screen, correcting it manually is sufficient; the new size is saved automatically.
If the layout has become inconvenient, first move the dividers back to usable positions and save the settings again. Deleting the configuration file also resets the UI values, but it removes the other stored programme settings as well. It should therefore be used only when the interface cannot be restored in another way.
If the layout has become inconvenient, first move the dividers and column widths back to usable positions. Deleting the configuration file also resets the UI values, but it removes the other stored programme settings as well. It should therefore be used only when the interface cannot be restored in another way.
---
+3 -3
View File
@@ -1,6 +1,6 @@
pkgbase = kst4contest-bin
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation (pre-built)
pkgver = 1.42.0
pkgver = 1.43.0
pkgrel = 1
url = https://github.com/praktimarc/kst4contest
arch = x86_64
@@ -10,7 +10,7 @@ pkgbase = kst4contest-bin
provides = kst4contest
conflicts = kst4contest
conflicts = kst4contest-git
source = KST4Contest-v1.42.0-archlinux-x86_64.pkg.tar.zst::https://github.com/praktimarc/kst4contest/releases/download/v1.42.0/KST4Contest-v1.42.0-archlinux-x86_64.pkg.tar.zst
sha256sums = 3d8ac19c9f9d3ab0bdaf0ea621de0aa18c64f442c8776d28bfad607522aaf02b
source = KST4Contest-v1.43.0-archlinux-x86_64.pkg.tar.zst::https://github.com/praktimarc/kst4contest/releases/download/v1.43.0/KST4Contest-v1.43.0-archlinux-x86_64.pkg.tar.zst
sha256sums = 5116d1ec8dac7dfe96976bdf9bc23b767e4ff53832a91c4af26482db7a32dab5
pkgname = kst4contest-bin
+2 -2
View File
@@ -1,6 +1,6 @@
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
pkgname=kst4contest-bin
pkgver=1.42.0
pkgver=1.43.0
pkgrel=1
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation (pre-built)"
arch=('x86_64')
@@ -10,7 +10,7 @@ depends=('gst-plugins-base' 'gst-plugins-good')
provides=('kst4contest')
conflicts=('kst4contest' 'kst4contest-git')
source=("KST4Contest-v${pkgver}-archlinux-${CARCH}.pkg.tar.zst::https://github.com/praktimarc/kst4contest/releases/download/v${pkgver}/KST4Contest-v${pkgver}-archlinux-${CARCH}.pkg.tar.zst")
sha256sums=('3d8ac19c9f9d3ab0bdaf0ea621de0aa18c64f442c8776d28bfad607522aaf02b')
sha256sums=('5116d1ec8dac7dfe96976bdf9bc23b767e4ff53832a91c4af26482db7a32dab5')
package() {
cp -a "${srcdir}/usr" "${pkgdir}/"
+1 -1
View File
@@ -1,6 +1,6 @@
pkgbase = kst4contest-git
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation (git)
pkgver = 1.42.0.r256.g8aadbb9
pkgver = 1.43.0.r291.g3ed1cad5
pkgrel = 1
url = https://github.com/praktimarc/kst4contest
arch = x86_64
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
pkgname=kst4contest-git
pkgver=1.42.0.r256.g8aadbb9
pkgver=1.43.0.r291.g3ed1cad5
pkgrel=1
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation (git)"
arch=('x86_64')
+3 -3
View File
@@ -1,6 +1,6 @@
pkgbase = kst4contest
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation
pkgver = 1.42.0
pkgver = 1.43.0
pkgrel = 1
url = https://github.com/praktimarc/kst4contest
arch = x86_64
@@ -12,7 +12,7 @@ pkgbase = kst4contest
provides = kst4contest
conflicts = kst4contest-bin
conflicts = kst4contest-git
source = kst4contest-1.42.0.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v1.42.0.tar.gz
sha256sums = bd396387b8de41aac706458d5ebf64140ab3e83b8a7c710b66aa802bd48e804e
source = kst4contest-1.43.0.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v1.43.0.tar.gz
sha256sums = 75d0a872d126f46a9e64497a9bae247f741b31183be9b12079e1c57bf12d5c71
pkgname = kst4contest
+2 -2
View File
@@ -1,6 +1,6 @@
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
pkgname=kst4contest
pkgver=1.42.0
pkgver=1.43.0
pkgrel=1
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation"
arch=('x86_64')
@@ -11,7 +11,7 @@ makedepends=('java-environment=21' 'maven')
provides=('kst4contest')
conflicts=('kst4contest-bin' 'kst4contest-git')
source=("${pkgname}-${pkgver}.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v${pkgver}.tar.gz")
sha256sums=('bd396387b8de41aac706458d5ebf64140ab3e83b8a7c710b66aa802bd48e804e')
sha256sums=('75d0a872d126f46a9e64497a9bae247f741b31183be9b12079e1c57bf12d5c71')
build() {
cd "${srcdir}/kst4contest-${pkgver}"
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>de.x08</groupId>
<artifactId>praktiKST</artifactId>
<version>1.42.0-nightly</version>
<version>1.43.1-nightly</version>
<name>praktiKST</name>
@@ -20,14 +20,14 @@ public class ApplicationConstants {
/**
* Version shown to the user and used for semantic version comparison.
*/
public static final String APPLICATION_CURRENT_VERSION = "1.42";
public static final String APPLICATION_CURRENT_VERSION = "1.43.1";
/**
* Legacy numeric representation used only while older update feeds and
* application versions still exist.
*/
@Deprecated
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.42;
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.431;
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://kst4contest.hamradioonline.de/kst4ContestVersionInfo.xml";
public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
@@ -3,6 +3,7 @@ package kst4contest.controller;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.file.Path;
import java.sql.SQLException;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@@ -1847,7 +1848,7 @@ private ObservableList<String>
* assumption which one is the current run frequency. Therefore the legacy
* frequency property is initialized only when exactly one explicit QRG exists.</p>
*/
private void initializeFrequencyFromStationNameIfUnambiguous(
/* package */ void initializeFrequencyFromStationNameIfUnambiguous(
ChatMember member
) {
if (member == null) {
@@ -1941,6 +1942,8 @@ private ObservableList<String>
int categoryNumber = category.getCategoryNumber();
List<ChatMember> safeMembers = completeMembers == null
? List.of() : new ArrayList<>(completeMembers);
Map<String, ChatMember> workedDataFromDatabase =
loadWorkedStateForInitialUserList(safeMembers);
for (ChatMember member : safeMembers) {
initializeFrequencyFromStationNameIfUnambiguous(member);
}
@@ -1965,10 +1968,48 @@ private ObservableList<String>
&& member.getChatCategory() != null
&& member.getChatCategory().getCategoryNumber() == categoryNumber);
lst_chatMemberList.addAll(safeMembers);
if (workedDataFromDatabase != null) {
getLst_DBBasedWkdCallSignList().setAll(
workedDataFromDatabase.values());
}
fireUserListUpdate("Complete ON4KST user list received");
});
}
/**
* Loads one database snapshot for a completed initial ON4KST user list and
* applies it before those members are published to the active model or UI.
* Every callsign variant receives the state stored for its normalized base
* callsign.
*
* @param initialMembers completed members of one chat category
* @return loaded snapshot, or {@code null} when the database read failed
*/
/* package */ Map<String, ChatMember> loadWorkedStateForInitialUserList(
Collection<ChatMember> initialMembers
) {
if (dbHandler == null) {
LOGGER.warning(
"Cannot load Worked state for initial ON4KST user list: "
+ "database is not initialized");
return null;
}
try {
Map<String, ChatMember> workedDataFromDatabase =
dbHandler.fetchChatMemberWkdDataFromDB();
applyWorkedAndQrvStateFromDatabase(
initialMembers, workedDataFromDatabase);
return workedDataFromDatabase;
} catch (SQLException | RuntimeException exception) {
LOGGER.log(
Level.WARNING,
"Could not load Worked state for completed initial ON4KST user list",
exception);
return null;
}
}
/**
* Resolves a member from the thread-safe active model. This avoids reading the
* TableView backing list from MessageBusManagementThread.
@@ -2016,6 +2057,177 @@ private ObservableList<String>
return matchingMembers;
}
/**
* Applies a QSO received from an external logger to all active callsign
* variants. Active member objects back JavaFX views, so their state is changed
* only on the JavaFX Application Thread. The band-upgrade hint is evaluated
* afterwards and therefore sees the new Worked state.
*
* @param loggedQso validated external QSO
*/
public void applyExternalLoggedQso(ExternalLoggedQso loggedQso) {
if (loggedQso == null) {
return;
}
runOnFxThread(() -> {
int updatedMembers = markExternalLoggedQsoMembers(
activeChatMembersByCallAndCategory.values(), loggedQso);
if (updatedMembers == 0) {
return;
}
fireUserListUpdate("External logger Worked state updated");
try {
onExternalLogEntryReceived(loggedQso.getCallSign());
} catch (Exception exception) {
LOGGER.log(Level.WARNING,
"Band-upgrade hint failed after external logger update for "
+ loggedQso.getCallSign(), exception);
}
});
}
static int markExternalLoggedQsoMembers(
Collection<ChatMember> members,
ExternalLoggedQso loggedQso
) {
if (members == null || loggedQso == null) {
return 0;
}
String workedBaseCall = ChatMember.normalizeCallSignToBaseCallSign(
loggedQso.getCallSign());
if (workedBaseCall == null || workedBaseCall.isBlank()) {
return 0;
}
int updatedMembers = 0;
for (ChatMember member : members) {
if (member == null) {
continue;
}
String memberCall = member.getCallSignRaw() != null
? member.getCallSignRaw() : member.getCallSign();
String memberBaseCall = ChatMember.normalizeCallSignToBaseCallSign(memberCall);
if (memberBaseCall != null && memberBaseCall.equalsIgnoreCase(workedBaseCall)) {
loggedQso.applyToActiveMember(member);
updatedMembers++;
}
}
return updatedMembers;
}
/**
* Applies the global Worked state from the Simplelogfile to every active
* callsign variant with the same base callsign. UI-backed state is changed only
* on the JavaFX Application Thread.
*
* @param workedBaseCalls normalized callsigns detected in the selected file
*/
public void applySimpleLogWorkedBaseCalls(Set<String> workedBaseCalls) {
Set<String> normalizedWorkedBaseCalls = normalizeWorkedBaseCalls(workedBaseCalls);
if (normalizedWorkedBaseCalls.isEmpty()) {
return;
}
runOnFxThread(() -> {
int changedMembers = markSimpleLogWorkedMembers(
activeChatMembersByCallAndCategory.values(), normalizedWorkedBaseCalls);
int changedClusterMessages = markSimpleLogWorkedClusterMessages(
lst_clusterMemberList, normalizedWorkedBaseCalls);
if (changedMembers > 0) {
fireUserListUpdate("Simplelogfile Worked status updated");
}
LOGGER.log(Level.FINE,
"Simplelogfile marked {0} active members and {1} cluster messages as worked.",
new Object[] { changedMembers, changedClusterMessages });
});
}
static int markSimpleLogWorkedMembers(
Collection<ChatMember> members,
Set<String> workedBaseCalls
) {
if (members == null || workedBaseCalls == null || workedBaseCalls.isEmpty()) {
return 0;
}
int changed = 0;
for (ChatMember member : members) {
if (member == null || member.isWorked()) {
continue;
}
String memberCall = member.getCallSignRaw() != null
? member.getCallSignRaw() : member.getCallSign();
String baseCall = ChatMember.normalizeCallSignToBaseCallSign(memberCall);
if (baseCall != null && workedBaseCalls.contains(baseCall.toUpperCase(Locale.ROOT))) {
member.setWorked(true);
changed++;
}
}
return changed;
}
static int markSimpleLogWorkedClusterMessages(
Collection<ClusterMessage> clusterMessages,
Set<String> workedBaseCalls
) {
if (clusterMessages == null || workedBaseCalls == null || workedBaseCalls.isEmpty()) {
return 0;
}
int changed = 0;
for (ClusterMessage clusterMessage : clusterMessages) {
if (clusterMessage == null || clusterMessage.isReceiverWkd()
|| clusterMessage.getReceiver() == null) {
continue;
}
ChatMember receiver = clusterMessage.getReceiver();
String receiverCall = receiver.getCallSignRaw() != null
? receiver.getCallSignRaw() : receiver.getCallSign();
String baseCall = ChatMember.normalizeCallSignToBaseCallSign(receiverCall);
if (baseCall != null && workedBaseCalls.contains(baseCall.toUpperCase(Locale.ROOT))) {
clusterMessage.setReceiverWkd(true);
changed++;
}
}
return changed;
}
private static Set<String> normalizeWorkedBaseCalls(Set<String> workedBaseCalls) {
if (workedBaseCalls == null || workedBaseCalls.isEmpty()) {
return Set.of();
}
Set<String> normalizedCalls = new HashSet<>();
for (String callSign : workedBaseCalls) {
String baseCall = ChatMember.normalizeCallSignToBaseCallSign(callSign);
if (baseCall != null && !baseCall.isBlank()) {
normalizedCalls.add(baseCall.toUpperCase(Locale.ROOT));
}
}
return Set.copyOf(normalizedCalls);
}
/**
* Notifies the UI after a missing Simplelogfile was created successfully.
*
* @param filePath absolute path of the new file
*/
public void notifySimpleLogFileCreated(Path filePath) {
if (filePath == null || statusListener == null) {
return;
}
runOnFxThread(() -> statusListener.onSimpleLogFileCreated(filePath));
}
/**
* Copies the band-specific NOT-QRV state to every active category variant of the
* same base callsign. The database already uses callSignRaw as its key; applying
@@ -3682,15 +3894,6 @@ private ObservableList<String>
}
new Timer().schedule(new TimerTask() {
@Override
public void run() {
Thread.currentThread().setName("fetchWorkedFromDBTimer");
refreshWorkedStateAndDatabaseListFromDatabase();
}
}, 10000);
// new Timer().schedule(new TimerTask() {
// HashMap<String, ChatMember> getWorkedDataFromDb;
//
@@ -3794,23 +3997,34 @@ private ObservableList<String>
HashMap<String, ChatMember> finalWorkedDataFromDatabase = workedDataFromDatabase;
Platform.runLater(() -> {
helper_applyWorkedAndQrvStateFromDatabase(finalWorkedDataFromDatabase);
applyWorkedAndQrvStateFromDatabase(
activeChatMembersByCallAndCategory.values(),
finalWorkedDataFromDatabase);
getLst_DBBasedWkdCallSignList().setAll(finalWorkedDataFromDatabase.values());
fireUserListUpdate("Worked database state refreshed");
});
}
/**
* Applies the worked and not-QRV state from the database snapshot to all active
* chatmember objects that are currently visible in the live chat list.
* Applies the worked and not-QRV state from a database snapshot to chat members.
* Database rows are keyed by normalized base callsign, so all active category
* and suffix variants receive the same persisted state.
*
* @param chatMembers members that should receive persisted state
* @param workedDataFromDatabase map keyed by normalized raw callsign
*/
private void helper_applyWorkedAndQrvStateFromDatabase(HashMap<String, ChatMember> workedDataFromDatabase) {
/* package */ static void applyWorkedAndQrvStateFromDatabase(
Collection<ChatMember> chatMembers,
Map<String, ChatMember> workedDataFromDatabase
) {
if (chatMembers == null || workedDataFromDatabase == null) {
return;
}
for (Iterator iterator = getLst_chatMemberList().iterator(); iterator.hasNext();) {
ChatMember activeChatMember = (ChatMember) iterator.next();
for (ChatMember activeChatMember : chatMembers) {
if (activeChatMember == null) {
continue;
}
ChatMember storedChatMemberState = workedDataFromDatabase.get(activeChatMember.getCallSignRaw());
if (storedChatMemberState == null) {
@@ -4098,4 +4312,4 @@ private ObservableList<String>
return "Sniffed: (" + senderCall + " > " + receiverCall + ") " + msgText;
}
}
}
@@ -0,0 +1,165 @@
package kst4contest.controller;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
/**
* Formats local DX Cluster spots using the fixed-column layout emitted by
* DXSpider and accepted by common logging programs.
*/
final class DXClusterSpotFormatter {
/** Length of the DX Cluster line before BEL and CRLF framing. */
/* package */
static final int LINE_LENGTH = 75;
/** One-based column in which the spotted callsign starts. */
/* package */
static final int DX_CALL_COLUMN = 27;
/** Width of the fixed comment field. */
/* package */
static final int COMMENT_LENGTH = 30;
/** One-based column in which the UTC time starts. */
/* package */
static final int TIME_COLUMN = 71;
/** Zero-based exclusive end position of the frequency field. */
private static final int FREQUENCY_END = 24;
/** Maximum width of the spotted callsign field. */
private static final int DX_CALL_LENGTH = 12;
/** Required width of the HHMMZ time field. */
private static final int TIME_LENGTH = 5;
/** Minimum separator width between spotter and frequency. */
private static final int MIN_FREQUENCY_GAP = 1;
/** Wire framing appended to every formatted line. */
private static final String PAYLOAD_SUFFIX = "\u0007\u0007\r\n";
private DXClusterSpotFormatter() {
}
/** Builds the fixed 75-character payload line without wire framing. */
/* package */
static String formatLine(
final String spotterCallSign,
final String frequency,
final String dxCallSign,
final String comment,
final String time
) {
final String spotter = requireValue(
spotterCallSign,
"spotter callsign"
)
.toUpperCase(Locale.ROOT);
final String frequencyValue = requireValue(frequency, "frequency");
final String dxCall = requireValue(dxCallSign, "DX callsign")
.toUpperCase(Locale.ROOT);
final String timeValue = requireValue(time, "time");
validateDxCall(dxCall);
validateTime(timeValue);
final String prefix = "DX de " + spotter + ":";
final int frequencyPadding = calculateFrequencyPadding(
prefix,
frequencyValue
);
final String normalizedComment = normalizeComment(comment);
final String line = prefix
+ " ".repeat(frequencyPadding)
+ frequencyValue
+ " "
+ padRight(dxCall, DX_CALL_LENGTH)
+ " "
+ padRight(normalizedComment, COMMENT_LENGTH)
+ " "
+ timeValue;
if (line.length() != LINE_LENGTH) {
throw new IllegalStateException(
"DX Cluster formatter produced "
+ line.length()
+ " characters instead of "
+ LINE_LENGTH
);
}
return line;
}
/** Builds one complete ASCII spot payload including BEL and CRLF framing. */
/* package */
static byte[] formatPayload(
final String spotterCallSign,
final String frequency,
final String dxCallSign,
final String comment,
final String time
) {
return (formatLine(
spotterCallSign,
frequency,
dxCallSign,
comment,
time
) + PAYLOAD_SUFFIX).getBytes(StandardCharsets.US_ASCII);
}
private static void validateDxCall(final String dxCall) {
if (dxCall.length() > DX_CALL_LENGTH) {
throw new IllegalArgumentException(
"DX callsign exceeds 12 characters: " + dxCall
);
}
}
private static void validateTime(final String time) {
if (time.length() != TIME_LENGTH) {
throw new IllegalArgumentException(
"DX Cluster time must contain exactly five characters"
);
}
}
private static int calculateFrequencyPadding(
final String prefix,
final String frequency
) {
final int padding = FREQUENCY_END
- prefix.length()
- frequency.length();
if (padding < MIN_FREQUENCY_GAP) {
throw new IllegalArgumentException(
"Spotter callsign and frequency do not fit the DX Cluster prefix"
);
}
return padding;
}
private static String normalizeComment(final String comment) {
final String normalized = comment == null ? "" : comment.trim();
return normalized.length() > COMMENT_LENGTH
? normalized.substring(0, COMMENT_LENGTH)
: normalized;
}
private static String requireValue(
final String value,
final String fieldName
) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(
"DX Cluster " + fieldName + " is missing"
);
}
return value.trim();
}
private static String padRight(final String value, final int length) {
return value + " ".repeat(length - value.length());
}
}
@@ -152,6 +152,7 @@ public class DXClusterThreadPooledServer implements Runnable {
public boolean broadcastSingleDXClusterEntryToLoggers(
ChatMember chatMember
) {
final byte[] clusterPayload;
final String clusterMessage;
try {
@@ -162,24 +163,27 @@ public class DXClusterThreadPooledServer implements Runnable {
.getNotify_optionalFrequencyPrefix()
);
clusterMessage =
"DX de "
+ chatController
clusterPayload = DXClusterSpotFormatter.formatPayload(
chatController
.getChatPreferences()
.getNotify_DXCSrv_SpottersCallSign()
.getValue()
+ ": "
+ frequency
+ " "
+ chatMember.getCallSign().toUpperCase()
+ " "
+ chatMember.getQra().toUpperCase()
+ " "
+ new Utils4KST()
.getValue(),
frequency,
chatMember.getCallSign(),
chatMember.getQra(),
new Utils4KST()
.time_generateCurrenthhmmZTimeStringForClusterMessage()
+ ((char) 7)
+ ((char) 7)
+ "\r\n";
);
clusterMessage = new String(
clusterPayload,
StandardCharsets.US_ASCII
);
} catch (IllegalArgumentException exception) {
LOGGER.log(
Level.WARNING,
"DX Cluster spot rejected: " + exception.getMessage()
);
return false;
} catch (Exception exception) {
LOGGER.log(
Level.SEVERE,
@@ -204,11 +208,7 @@ public class DXClusterThreadPooledServer implements Runnable {
try {
OutputStream output = socket.getOutputStream();
output.write(
clusterMessage.getBytes(
StandardCharsets.US_ASCII
)
);
output.write(clusterPayload);
output.flush();
deliveredClients++;
} catch (IOException exception) {
@@ -360,4 +360,4 @@ class DXClusterServerWorkerRunnable implements Runnable {
}
}
}
}
}
@@ -1,44 +0,0 @@
package kst4contest.controller;
import javafx.beans.property.SimpleStringProperty;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
public class DXClusterThreadPooledServerTest {
public static void main(String[] args) {
ChatController client = new ChatController();
ChatPreferences testPreferences = new ChatPreferences();
testPreferences.setStn_loginCallSign("DM5M");
client.setChatPreferences(testPreferences);
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client, client);
new Thread(dxClusterServer).start();
try {
Thread.sleep(10 * 1000);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>ready.....go!");
} catch (InterruptedException e) {
e.printStackTrace();
}
ChatMember test = new ChatMember();
test.setCallSign("DL5ASG");
test.setQra("JO51HK");
test.setFrequency(new SimpleStringProperty("144776.0"));
dxClusterServer.broadcastSingleDXClusterEntryToLoggers(test);
// try {
// Thread.sleep(20 * 3333);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println("Stopping Server");
// server.stop();
}
}
@@ -0,0 +1,116 @@
package kst4contest.controller;
import java.util.Optional;
import kst4contest.model.ChatMember;
/**
* Validated QSO state shared by external logger listeners and the controller.
*/
public final class ExternalLoggedQso {
private final String callSign;
private final LoggedQsoBand band;
private final String locator;
private final String source;
private ExternalLoggedQso(
String callSign,
LoggedQsoBand band,
String locator,
String source
) {
this.callSign = callSign;
this.band = band;
this.locator = locator;
this.source = source;
}
/**
* Creates a QSO only when the logger supplied a usable callsign. Missing band
* and locator values remain unavailable and never receive guessed defaults.
*
* @param rawCallSign callsign received from the logger
* @param band normalized band or {@code null}
* @param rawLocator locator received from the logger or {@code null}
* @param source logger name used for persistence diagnostics
* @return validated QSO, or an empty value for a missing callsign
*/
public static Optional<ExternalLoggedQso> create(
String rawCallSign,
LoggedQsoBand band,
String rawLocator,
String source
) {
if (rawCallSign == null) {
return Optional.empty();
}
String normalizedCallSign = rawCallSign.trim();
if (normalizedCallSign.isEmpty()) {
return Optional.empty();
}
String normalizedLocator = WorkedGrossFieldCache.extractLocator6(rawLocator);
return Optional.of(new ExternalLoggedQso(
normalizedCallSign,
band,
normalizedLocator,
source
));
}
public String getCallSign() {
return callSign;
}
public LoggedQsoBand getBand() {
return band;
}
public String getLocator() {
return locator;
}
public String getSource() {
return source;
}
/**
* Builds the database representation of this QSO.
*
* @return chat-member state with global and optional band Worked flags
*/
public ChatMember toWorkedChatMember() {
ChatMember workedCall = new ChatMember();
workedCall.setCallSign(callSign);
workedCall.setWorked(true);
if (band != null) {
band.applyWorkedFlag(workedCall);
}
if (locator != null) {
workedCall.setQra(locator);
}
return workedCall;
}
/**
* Applies this QSO to one active callsign variant.
*
* @param member active variant with the same base callsign
*/
public void applyToActiveMember(ChatMember member) {
if (member == null) {
return;
}
member.setWorked(true);
if (band != null) {
band.applyWorkedFlag(member);
}
if (locator != null
&& (member.getQra() == null
|| member.getQra().isBlank()
|| "unknown".equalsIgnoreCase(member.getQra()))) {
member.setQra(locator);
}
}
}
@@ -0,0 +1,129 @@
package kst4contest.controller;
import java.util.Locale;
import kst4contest.model.Band;
import kst4contest.model.ChatMember;
/**
* Normalized band of a QSO received from an external logger.
*
* <p>Logger protocols use different representations for the same amateur-radio
* band. UCXLog-compatible XML may contain a frequency-like number or a metre /
* centimetre label, while Win-Test uses its own numeric band IDs. Resolving the
* raw value once prevents the Worked flag and gross-field update from interpreting
* the same packet differently.</p>
*/
public enum LoggedQsoBand {
BAND_50(Band.B_50),
BAND_70(Band.B_70),
BAND_144(Band.B_144),
BAND_432(Band.B_432),
BAND_1296(Band.B_1296),
BAND_2320(Band.B_2320),
BAND_3400(Band.B_3400),
BAND_5760(Band.B_5760),
BAND_10G(Band.B_10G),
BAND_24G(Band.B_24G),
BAND_47G(null),
BAND_76G(null);
private final Band projectBand;
LoggedQsoBand(Band projectBand) {
this.projectBand = projectBand;
}
/**
* Resolves aliases used by UCXLog, N1MM+, DXLog.net, QARTest and compatible
* loggers. The input is trimmed and case-normalized exactly once here.
*
* @param rawBand band value received in the XML packet
* @return normalized band, or {@code null} for a missing or unknown value
*/
public static LoggedQsoBand fromLoggerValue(String rawBand) {
if (rawBand == null) {
return null;
}
String normalizedBand = rawBand.trim().toLowerCase(Locale.ROOT);
return switch (normalizedBand) {
case "50", "6m" -> BAND_50;
case "70", "4m" -> BAND_70;
case "144", "2m" -> BAND_144;
case "432", "70cm" -> BAND_432;
case "1240", "1296", "23cm" -> BAND_1296;
case "2300", "2320", "13cm" -> BAND_2320;
case "3400", "9cm" -> BAND_3400;
case "5600", "5760", "6cm" -> BAND_5760;
case "10g", "10368", "3cm" -> BAND_10G;
default -> null;
};
}
/**
* Resolves the established Win-Test ADDQSO band IDs. IDs 22 and 23 retain
* their existing Worked flags even though the project Band enum has no 47 GHz
* or 76 GHz value for gross-field storage.
*
* @param rawBandId Win-Test band ID
* @return normalized band, or {@code null} for a missing or unknown ID
*/
public static LoggedQsoBand fromWinTestBandId(String rawBandId) {
if (rawBandId == null) {
return null;
}
String normalizedBandId = rawBandId.trim();
return switch (normalizedBandId) {
case "10" -> BAND_50;
case "11" -> BAND_70;
case "12" -> BAND_144;
case "14" -> BAND_432;
case "16" -> BAND_1296;
case "17" -> BAND_2320;
case "18" -> BAND_3400;
case "19" -> BAND_5760;
case "20" -> BAND_10G;
case "21" -> BAND_24G;
case "22" -> BAND_47G;
case "23" -> BAND_76G;
default -> null;
};
}
/**
* Returns the common Band value used by worked-grid persistence.
*
* @return project band, or {@code null} where no matching Band value exists
*/
public Band getProjectBand() {
return projectBand;
}
/**
* Applies the established per-band Worked flag to a chat member.
*
* @param member worked station or active callsign variant
*/
public void applyWorkedFlag(ChatMember member) {
if (member == null) {
return;
}
switch (this) {
case BAND_50 -> member.setWorked50(true);
case BAND_70 -> member.setWorked70(true);
case BAND_144 -> member.setWorked144(true);
case BAND_432 -> member.setWorked432(true);
case BAND_1296 -> member.setWorked1240(true);
case BAND_2320 -> member.setWorked2300(true);
case BAND_3400 -> member.setWorked3400(true);
case BAND_5760 -> member.setWorked5600(true);
case BAND_10G -> member.setWorked10G(true);
case BAND_24G -> member.setWorked24G(true);
case BAND_47G -> member.setWorked47G(true);
case BAND_76G -> member.setWorked76G(true);
}
}
}
@@ -57,7 +57,7 @@ public class MessageBusManagementThread extends Thread {
/*
* Frequency formats handled by the smart parser:
*
* Group 1: full frequencies, for example 144.210 or 10368.100
* Group 1: full frequencies, for example 144.210, 144210 or 10368100
* Group 2: relative frequencies with a separator, for example .210 or ,210
* Group 3: bare three-digit values, for example 210
*
@@ -66,7 +66,7 @@ public class MessageBusManagementThread extends Thread {
* would be converted into plausible but incorrect frequencies.
*/
private static final Pattern SMART_FREQUENCY_PATTERN = Pattern.compile(
"(?<![\\d])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
"(?<![A-Z0-9])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?|\\d{5,8})(?![A-Z0-9])"
+ "|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])"
+ "|(?<=\\s|^)(\\d{3})(?=\\s|$)"
);
@@ -279,7 +279,7 @@ public class MessageBusManagementThread extends Thread {
* @param message message whose text is inspected
* @param prefs preferences containing the global fallback band
*/
private void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
/* package */ void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
if (message == null || message.getMessageText() == null) {
return;
}
@@ -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 =
@@ -1841,14 +1847,14 @@ public class MessageBusManagementThread extends Thread {
* @param sender station for which the DX Cluster spot is generated
* @return locator with up to two optional AP entries
*/
private String buildDxClusterSpotComment(ChatMember sender) {
static String buildDxClusterSpotComment(ChatMember sender) {
if (sender == null) {
return "";
}
String locator = sender.getQra() == null
? ""
: sender.getQra().trim();
: sender.getQra().trim().toUpperCase(Locale.ROOT);
AirPlaneReflectionInfo reflectionInfo =
sender.getAirPlaneReflectInfo();
@@ -1876,7 +1882,7 @@ public class MessageBusManagementThread extends Thread {
aircraftComments.add(
aircraft.getArrivingDurationMinutes()
+ "min, "
+ "m/"
+ aircraft.getPotential()
+ "%"
);
@@ -1887,11 +1893,11 @@ public class MessageBusManagementThread extends Thread {
}
String apComment =
"AP: " + String.join("; ", aircraftComments);
"AP " + String.join(";", aircraftComments);
return locator.isEmpty()
? apComment
: locator + " , " + apComment;
: locator + " " + apComment;
}
/**
@@ -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;
}
}
}
@@ -2,10 +2,8 @@ package kst4contest.controller;
import javafx.application.Platform;
import kst4contest.ApplicationConstants;
import kst4contest.model.Band;
import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
import java.io.BufferedWriter;
import java.io.File;
@@ -98,7 +96,7 @@ public class ReadUDPByWintestThread extends Thread {
}
}
private void processWinTestMessage(String msg) {
void processWinTestMessage(String msg) {
// System.out.println("Wintest-Message received: " + msg);
lastPacketTime = System.currentTimeMillis();
@@ -368,33 +366,28 @@ public class ReadUDPByWintestThread extends Thread {
/**
* Resolves the project Band enum from Win-Test band IDs.
* Extracts the unchanged Win-Test band ID from the unquoted ADDQSO fields.
*
* <p>Only bands that exist in the current Band enum are returned. 50/70 MHz are
* still represented as worked flags in ChatMember, but they are not part of the
* current Reachability/New-Locator band enum.</p>
*
* @param bandId Win-Test band id from ADDQSO
* @return matching Band or null
* @param message complete ADDQSO packet
* @return raw band ID or an empty value when the field is missing
*/
private Band helper_resolveBandFromWinTestBandId(String bandId) {
if (bandId == null) {
return null;
static String extractBandIdFromWinTestAddQso(String message) {
if (message == null) {
return "";
}
return switch (bandId.trim()) {
case "10" -> Band.B_50;
case "11" -> Band.B_70;
case "12" -> Band.B_144;
case "14" -> Band.B_432;
case "16" -> Band.B_1296;
case "17" -> Band.B_2320;
case "18" -> Band.B_3400;
case "19" -> Band.B_5760;
case "20" -> Band.B_10G;
case "21" -> Band.B_24G;
default -> null;
};
String[] quotedParts = message.split("\"");
if (quotedParts.length <= 6) {
return "";
}
String unquotedFields = quotedParts[6].trim();
if (unquotedFields.isEmpty()) {
return "";
}
String[] packetFields = unquotedFields.split("\\s+");
return packetFields.length > 3 ? packetFields[3] : "";
}
/**
@@ -449,112 +442,32 @@ public class ReadUDPByWintestThread extends Thread {
* @param msg
*/
private void parseAddQso(String msg) {
ChatMember modifyThat = null;
try {
// int qsoNumber = extractQsoNumber(msg);
// receivedQsos.put(qsoNumber, msg);
// lastKnownQso = Math.max(lastKnownQso, qsoNumber);
String callSignCatched = msg.split("\"") [7];
String[] quotedParts = msg == null ? new String[0] : msg.split("\"");
String callSign = quotedParts.length > 7 ? quotedParts[7] : "";
String rawBandId = extractBandIdFromWinTestAddQso(msg);
String locatorFromLogger = helper_resolveLocatorFromWinTestAddQso(msg);
ChatMember workedCall = new ChatMember();
workedCall.setCallSign(callSignCatched);
workedCall.setWorked(true); //its worked at this place, for sure!
if (locatorFromLogger != null) {
workedCall.setQra(locatorFromLogger);
LoggedQsoBand loggedBand = LoggedQsoBand.fromWinTestBandId(rawBandId);
ExternalLoggedQso loggedQso = ExternalLoggedQso.create(
callSign, loggedBand, locatorFromLogger, "WINTEST").orElse(null);
if (loggedQso == null) {
System.out.println("[WinTestUDPRcvr: warning] ADDQSO without usable callsign ignored");
return;
}
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
String bandId;
bandId = msg.split("\"")[6].split(" ")[4].trim();
Band workedBand = helper_resolveBandFromWinTestBandId(bandId);
switch (bandId) {
case "10" -> workedCall.setWorked50(true);
case "11" -> workedCall.setWorked70(true);
case "12" -> workedCall.setWorked144(true);
case "14" -> workedCall.setWorked432(true);
case "16" -> workedCall.setWorked1240(true);
case "17" -> workedCall.setWorked2300(true);
case "18" -> workedCall.setWorked3400(true);
case "19" -> workedCall.setWorked5600(true);
case "20" -> workedCall.setWorked10G(true);
case "21" -> workedCall.setWorked24G(true);
case "22" -> workedCall.setWorked47G(true);
case "23" -> workedCall.setWorked76G(true);
default -> System.out.println("[WinTestUDPRcvr: warning] Unbekannte Band-ID: " + bandId);
if (loggedBand == null && !rawBandId.isEmpty()) {
System.out.println("[WinTestUDPRcvr: warning] Unknown band ID: " + rawBandId);
}
if (workedBand != null && locatorFromLogger != null) {
this.client.registerWorkedGrossField(workedBand, locatorFromLogger, workedCall, "WINTEST");
}
if (!markTheseChattersAsWorked.isEmpty()) {
//Worked call is part of the current chatmember list
for (int index : markTheseChattersAsWorked) {
//iterate through the logged in chatmembers callsigns and set the worked markers
modifyThat = client.getLst_chatMemberList().get(index);
modifyThat.setWorked(true); //worked its for sure
if (locatorFromLogger != null
&& (modifyThat.getQra() == null
|| modifyThat.getQra().isBlank()
|| "unknown".equalsIgnoreCase(modifyThat.getQra()))) {
modifyThat.setQra(locatorFromLogger);
}
if (workedCall.isWorked50()) {
modifyThat.setWorked50(true);
} else if (workedCall.isWorked70()) {
modifyThat.setWorked70(true);
} else if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
} else if (workedCall.isWorked24G()) {
modifyThat.setWorked24G(true);
} else if (workedCall.isWorked47G()) {
modifyThat.setWorked47G(true);
} else if (workedCall.isWorked76G()) {
modifyThat.setWorked76G(true);
} else {
System.out.println("[WinTestUDPRcvr: warning] found no new worked-flag for this band: " + workedCall.getCallSignRaw() + bandId);
}
}
try {
GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
// trigger band-upgrade hint after log entry (Win-Test)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[WinTestUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
} catch (Exception IllegalStateException) {
//do nothing, as it works...
}
ChatMember workedCall = loggedQso.toWorkedChatMember();
if (loggedBand != null
&& loggedBand.getProjectBand() != null
&& locatorFromLogger != null) {
this.client.registerWorkedGrossField(
loggedBand.getProjectBand(), locatorFromLogger, workedCall, loggedQso.getSource());
}
client.applyExternalLoggedQso(loggedQso);
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
// This will update the worked info on a worked chatmember. DBHandler will
@@ -587,17 +500,9 @@ public class ReadUDPByWintestThread extends Thread {
bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile);
if (modifyThat != null) {
bufwrtrRawMSGOut.write("\n" + modifyThat.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
} else {
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
}
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
System.out.println("[WinTest, Info: Marking Chatmember as worked: " + workedCall.toString());
@@ -3,7 +3,6 @@ package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import javax.xml.XMLConstants;
@@ -13,7 +12,6 @@ import javax.xml.parsers.ParserConfigurationException;
import kst4contest.ApplicationConstants;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -132,54 +130,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
return WorkedGrossFieldCache.extractLocator6(helper_getOptionalElementText(element, "rcvnr"));
}
/**
* Resolves the project Band enum from logger band values.
*
* @param band logger band text
* @return matching Band or null
*/
private Band helper_resolveBandFromLoggerBand(String band) {
if (band == null) {
return null;
}
switch (band.trim()) {
case "50":
case "6m":
return Band.B_50;
case "70":
case "4m":
return Band.B_70;
case "144":
case "2m":
return Band.B_144;
case "432":
case "70cm":
return Band.B_432;
case "1240":
case "1296":
case "23cm":
return Band.B_1296;
case "2300":
case "2320":
case "13cm":
return Band.B_2320;
case "3400":
case "9cm":
return Band.B_3400;
case "5600":
case "5760":
case "6cm":
return Band.B_5760;
case "10G":
case "10368":
case "3cm":
return Band.B_10G;
default:
return null;
}
}
public void run() {
System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
@@ -295,8 +245,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
String udpMsg = helper_extractXmlPayload(udpPacketToProcess);
ChatMember modifyThat = null;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
@@ -342,95 +290,27 @@ public class ReadUDPbyUCXMessageThread extends Thread {
Element element = (Element) node;
String call = element.getElementsByTagName("call").item(0).getTextContent();
String band = helper_getOptionalElementText(element, "band");
String call = helper_getOptionalElementText(element, "call");
String rawBand = helper_getOptionalElementText(element, "band");
String gridSquare = helper_resolveLocatorFromContactInfo(element);
String points = helper_getOptionalElementText(element, "points");
LoggedQsoBand loggedBand = LoggedQsoBand.fromLoggerValue(rawBand);
ExternalLoggedQso loggedQso = ExternalLoggedQso.create(
call, loggedBand, gridSquare, "UCXLOG").orElse(null);
if (loggedQso == null) {
System.out.println("[ReadUDPFromUCX, warning]: QSO packet without usable callsign ignored");
continue;
}
System.out.println("[Readudp, info ]: received Current Element :" + node.getNodeName()
+ "call: " + call + " / " + band + " ----> " + points + " POINTS");
+ "call: " + call + " / " + rawBand + " ----> " + points + " POINTS");
// client.getChatPreferences().setBcn_contestScoreSum(Long.parseLong(points));
ChatMember workedCall = new ChatMember();
workedCall.setCallSign(call);
workedCall.setWorked(true);
if (gridSquare != null) {
workedCall.setQra(gridSquare);
}
Band workedBand = helper_resolveBandFromLoggerBand(band);
switch (band) {
case "50":
case "6m":
{
workedCall.setWorked50(true);
break;
}
case "70":
case "4m":
{
workedCall.setWorked70(true);
break;
}
case "144":
case "2m": //minos contest logger
{
workedCall.setWorked144(true);
break;
}
case "432":
case "70cm":
{
workedCall.setWorked432(true);
break;
}
case "1240": //ucxlog style
case "1296": //used for n1mm / Dxlog
case "23cm": //minos contest logger
{
workedCall.setWorked1240(true);
break;
}
case "2300":
case "13cm":
{
workedCall.setWorked2300(true);
break;
}
case "3400":
case "9cm":
{
workedCall.setWorked3400(true);
break;
}
case "5600":
case "6cm":
{
workedCall.setWorked5600(true);
break;
}
case "10G":
case "3cm":
{
workedCall.setWorked10G(true);
break;
}
default:
System.out.println("[ReadUDPFromUCX, Error:] unexpected band value: \"" + band + "\"");
break;
ChatMember workedCall = loggedQso.toWorkedChatMember();
Band workedBand = loggedBand == null ? null : loggedBand.getProjectBand();
if (loggedBand == null && !rawBand.isEmpty()) {
System.out.println("[ReadUDPFromUCX, warning]: unexpected band value: \"" + rawBand + "\"");
}
{
@@ -444,60 +324,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
// asd //TODO: Check if callsign and callsignraw is similar, then mark first and further via new checklistforchatmembermultiplemethod with array of indize
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
if (markTheseChattersAsWorked.isEmpty()) {
//Worked call is not part of the chatmember list
} else {
for (int index : markTheseChattersAsWorked) {
modifyThat = client.getLst_chatMemberList().get(index);
modifyThat.setWorked(true);
if (workedCall.isWorked50()) {
modifyThat.setWorked50(true);
} else if (workedCall.isWorked70()) {
modifyThat.setWorked70(true);
} else if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
}
}
try {
GuiUtils.triggerGUIFilteredChatMemberListChange(this.client);
// BEGIN PATCH: trigger band-upgrade hint after log entry (UCXLog)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[UCXUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
} catch (Exception IllegalStateException) {
//do nothing, as it works...
}
}
client.applyExternalLoggedQso(loggedQso);
/**
* old mechanic to markup worked stations in the chatmember table
@@ -574,7 +401,8 @@ public class ReadUDPbyUCXMessageThread extends Thread {
}
if (workedBand != null && gridSquare != null) {
this.client.registerWorkedGrossField(workedBand, gridSquare, workedCall, "UCXLOG");
this.client.registerWorkedGrossField(
workedBand, gridSquare, workedCall, loggedQso.getSource());
}
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
@@ -609,17 +437,9 @@ public class ReadUDPbyUCXMessageThread extends Thread {
bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile);
if (modifyThat != null) {
bufwrtrRawMSGOut.write("\n" + modifyThat.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
} else {
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
}
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
}
}
@@ -771,4 +591,4 @@ public class ReadUDPbyUCXMessageThread extends Thread {
}
}
}
}
@@ -1,5 +1,7 @@
package kst4contest.controller;
import java.nio.file.Path;
import kst4contest.model.ThreadStateMessage;
public interface StatusUpdateListener {
@@ -33,4 +35,13 @@ public interface StatusUpdateListener {
// Optional for non-UI listeners.
}
}
/**
* Called after KST4Contest successfully creates a missing Simplelogfile.
*
* @param filePath absolute path of the newly created file
*/
default void onSimpleLogFileCreated(Path filePath) {
// Optional for non-UI listeners.
}
}
@@ -1,12 +1,13 @@
package kst4contest.controller;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -14,73 +15,50 @@ import kst4contest.model.ChatMember;
public class UCXLogFileToHashsetParser {
public BufferedReader fileReader;
// private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"; //OLD, S51AR for example will not work
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)|([A-Z]\\d{2}[A-Z]{1,3})";
private static final Pattern CALL_SIGN_PATTERN = Pattern.compile(
"(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)"
+ "|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"
+ "|([A-Z]\\d{2}[A-Z]{1,3})");
private final Path logFile;
public UCXLogFileToHashsetParser(String filePathAndName) {
try {
fileReader = new BufferedReader(new InputStreamReader(new FileInputStream(new File(filePathAndName))));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.logFile = Path.of(filePathAndName);
}
/**
* check if a line of the ucxlog-Logfile inhibits a Callsign<br/>
* <b>returns ChatMember = null, if no frequency found</b>
*
* @param chatMessage
*/
private ChatMember checkIfLineInhibitsCallSign(String line) {
Pattern pattern = Pattern.compile(PTRN_CallSign);
Matcher matcher = pattern.matcher(line);
String matchedString = "";
private String findLastCallSign(String line) {
Matcher matcher = CALL_SIGN_PATTERN.matcher(line);
String matchedCallSign = "";
while (matcher.find()) {
matchedString = matcher.group();
// System.out.println("[UCXLogFile:] Processed worked Callsign from file: " + matchedString);
matchedCallSign = matcher.group();
}
ChatMember newChatMember = new ChatMember();
newChatMember.setCallSign(matchedString.toUpperCase());
return newChatMember;
return matchedCallSign.toUpperCase(Locale.ROOT);
}
/**
* Parses an ucxlog-live-file (full qualified path given by constructor
* argument), looks by regex for callsigns and builds a hashmap with only one
* entry by callsign
* Parses the selected log file and returns every detected station as a
* normalized base callsign. The reader is closed after each pass so the logging
* application can continue replacing or rotating the file.
*
* @return unique normalized base callsigns found in the file
* @throws IOException if the file cannot be read
*/
public HashMap<String, String> parse() throws IOException {
public Set<String> parse() throws IOException {
Set<String> workedBaseCalls = new HashSet<>();
HashMap<String, String> chatMemberMap = new HashMap();
String line;
while ((line = fileReader.readLine()) != null) {
// System.out.println("raw: " + line);
ChatMember temp = checkIfLineInhibitsCallSign(line);
if (temp.getCallSign() != "") {
chatMemberMap.put(temp.getCallSign(), temp.getCallSign());
try (BufferedReader fileReader = Files.newBufferedReader(logFile, Charset.defaultCharset())) {
String line;
while ((line = fileReader.readLine()) != null) {
String matchedCallSign = findLastCallSign(line);
String baseCallSign = ChatMember.normalizeCallSignToBaseCallSign(matchedCallSign);
if (baseCallSign != null && !baseCallSign.isBlank()) {
workedBaseCalls.add(baseCallSign.toUpperCase(Locale.ROOT));
}
}
}
// System.out.println(chatMemberMap.size());
return chatMemberMap;
return workedBaseCalls;
}
}
@@ -1,325 +1,100 @@
package kst4contest.controller;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Set;
import java.util.TimerTask;
import javafx.collections.ObservableList;
import kst4contest.model.ChatMember;
import kst4contest.model.ClusterMessage;
import kst4contest.view.GuiUtils;
import java.util.logging.Level;
import java.util.logging.Logger;
public class UserActualizationTask extends TimerTask {
private ChatController client;
private static final Logger LOGGER = Logger.getLogger(UserActualizationTask.class.getName());
private final ChatController client;
public UserActualizationTask(ChatController client) {
this.client = client;
}
@Override
public void run() {
Thread.currentThread().setName("UserActualizationTask");
/*
* File-based log synchronization is optional. Do not create, open or parse the
* configured file while the feature is disabled.
*/
try {
updateWorkedCallSignsFromFile();
} catch (RuntimeException exception) {
LOGGER.log(Level.WARNING,
"Unexpected failure while updating Worked callsigns from the Simplelogfile; "
+ "the periodic task will continue.",
exception);
}
}
private void updateWorkedCallSignsFromFile() {
if (!client.getChatPreferences().isLogsynch_fileBasedWkdCallInterpreterEnabled()) {
return;
}
// System.out.println("[Useract: ] Thread runned now");
// System.out.println("***********************Useract started");
/**
* ******************************************since here: old mechanic for
* marking worked stations by .ucx-file
*/
HashMap<String, String> fetchedWorkedSet = new HashMap<>();
// HashMap<String, String> fetchedWorkedSetUdpBckup = new HashMap<>();
File f = new File(this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly());
if (!f.exists() && !f.isDirectory()) {
try {
f.createNewFile();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String configuredFileName = client.getChatPreferences()
.getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly();
if (configuredFileName == null || configuredFileName.isBlank()) {
LOGGER.warning("Cannot read the Simplelogfile because no file is selected.");
return;
}
UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser(
this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly());
final Path logFile;
try {
logFile = Path.of(configuredFileName).toAbsolutePath().normalize();
} catch (InvalidPathException exception) {
LOGGER.log(Level.WARNING,
"Cannot use the configured Simplelogfile path: " + configuredFileName,
exception);
return;
}
boolean created = createMissingLogFile(logFile);
if (!Files.isRegularFile(logFile)) {
return;
}
try {
fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse();
System.out.println("USERACT: fetchedWorkedSet size: " + fetchedWorkedSet.size());
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Set<String> workedBaseCalls = new UCXLogFileToHashsetParser(logFile.toString()).parse();
client.applySimpleLogWorkedBaseCalls(workedBaseCalls);
LOGGER.log(Level.FINE,
"Read {0} unique base callsigns from Simplelogfile {1}.",
new Object[] { workedBaseCalls.size(), logFile });
} catch (IOException exception) {
LOGGER.log(Level.WARNING, "Cannot read Simplelogfile " + logFile, exception);
}
ObservableList<ChatMember> praktiKSTActiveUserList = this.client.getLst_chatMemberList();
for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) {
ChatMember chatMember = (ChatMember) iterator.next();
// System.out.println(chatMember.getCallSign());
// System.out.println("USERACT active user list entries " + praktiKSTActiveUserList.size());
if (fetchedWorkedSet.containsKey(chatMember.getCallSign())) {
chatMember.setWorked(true);
System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign()
+ " as worked, based on READONLY-Logfile.");
}
// if (fetchedWorkedSetUdpBckup.containsKey(chatMember.getCallSign())) {
// chatMember.setWorked(true);
// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked, based on UDPLsnBackup-Logfile.");
// }
// GuiUtils.triggerGUIFilteredChatMemberListChange(this.client); //todo: quick and dirty gui fix
if (created) {
client.notifySimpleLogFileCreated(logFile);
}
ObservableList<ClusterMessage> praktiKSTClusterList = this.client.getLst_clusterMemberList();
for (Iterator iterator = praktiKSTClusterList.iterator(); iterator.hasNext();) {
ClusterMessage clusterMessage = (ClusterMessage) iterator.next();
if (fetchedWorkedSet.containsKey(clusterMessage.getReceiver().getCallSign())) {
clusterMessage.setReceiverWkd(true);
System.out.println("[USERACT, info:] marking Clusterspotted "
+ clusterMessage.getReceiver().getCallSign() + " as worked.");
}
// if (fetchedWorkedSetUdpBckup.containsKey(clusterMessage.getReceiver().getCallSign())) {
// clusterMessage.setReceiverWkd(true);
// System.out.println("[USERACT, info:] marking Clusterspotted "
// + clusterMessage.getReceiver().getCallSign() + " as worked.");
// }
}
/**
* ******************************************end here: old mechanic for marking
* worked stations by .ucx-file
*
*/
/**
* ******************************************since here: new mechanic for
* marking worked stations by udp/adif based information
*/
// HashMap<String, String> fetchedWorkedMap = new HashMap<>();
//
// fetchedWorkedMap = this.client.getMap_ucxLogInfoWorkedCalls();
// ObservableList<ChatMember> praktiKSTActiveUserList1 = this.client.getLst_chatMemberList();
//
// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) {
// ChatMember chatMember = (ChatMember) iterator.next();
//
// if (fetchedWorkedMap.containsKey(chatMember.getCallSign())) {
// chatMember.setWorked(true);
// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked based on UDP Log Info Collector.");
// }
// }
// ObservableList<ClusterMessage> praktiKSTClusterList1 = this.client.getLst_clusterMemberList();
//
// for (Iterator iterator = praktiKSTClusterList.iterator(); iterator.hasNext();) {
// ClusterMessage clusterMessage = (ClusterMessage) iterator.next();
//
// if (fetchedWorkedMap.containsKey(clusterMessage.getReceiver().getCallSign())) {
// clusterMessage.setReceiverWkd(true);
// System.out.println("[USERACT, info:] marking Clusterspotted "
// + clusterMessage.getReceiver().getCallSign() + " as worked based on UDP Log Info Collector.");
// }
//
// }
//
// UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser(
// "C:\\UcxLog\\Logs\\DO5AMF\\DVU322_I.UCX");
// try {
// fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse();
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
//
// ObservableList<ChatMember> praktiKSTActiveUserList = this.client.getLst_chatMemberList();
//
// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) {
// ChatMember chatMember = (ChatMember) iterator.next();
//
// if (fetchedWorkedSet.containsKey(chatMember.getCallSign())) {
// chatMember.setWorked(true);
// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked.");
// }
// }
//
//
// ObservableList<ClusterMessage> praktiKSTClusterList = this.client.getLst_clusterMemberList();
//
// for (Iterator iterator = praktiKSTClusterList.iterator(); iterator.hasNext();) {
// ClusterMessage clusterMessage = (ClusterMessage) iterator.next();
//
// if (fetchedWorkedSet.containsKey(clusterMessage.getReceiver().getCallSign())) {
// clusterMessage.setReceiverWkd(true);
// System.out.println("[USERACT, info:] marking Clusterspotted "
// + clusterMessage.getReceiver().getCallSign() + " as worked.");
// }
//
// }
/**
* ******************************************end here: new mechanic for marking
* worked stations by udp/adif based information
*
*/
// System.out.println("[UserActualizationtask:] Userlist actualization will be performed now. "
// + LocalDateTime.ofInstant(Instant.ofEpochMilli(scheduledExecutionTime()),
// ZoneId.systemDefault()));
// ChatMessage actualizeUserMsg = new ChatMessage();
// actualizeUserMsg.setDirectedToServer(true);
// actualizeUserMsg.setMessage("/show users");
// client.getMessageTXBus().add(actualizeUserMsg);
// Enumeration<String> e = this.client.getChatMemberTable().keys();
//
// while (e.hasMoreElements()) {
// String key = e.nextElement();
//
// System.out.println(this.client.getChatMemberTable().get(key).getCallSign() + ", "
// + this.client.getChatMemberTable().get(key).getQra() + ": "
// + this.client.getChatMemberTable().get(key).getFrequency());
//
// }
// System.out.println("[UserAct]: Show the Cluster with known frequencies now: ");
//
// Enumeration<String> e2 = this.client.getdXClusterMemberTable().keys();
//
// while (e2.hasMoreElements()) {
// String key = e2.nextElement();
//
// System.out.println(this.client.getdXClusterMemberTable().get(key).getCallSign() + ", "
// + this.client.getdXClusterMemberTable().get(key).getQra() + ": "
// + this.client.getdXClusterMemberTable().get(key).getFrequency());
//
// }
// for (int i = 0; i < 100; i++) {
//
// System.out.print("\n");
// }
/**
* keeepalive start
*/
// ChatMessage keepAliveMSG = new ChatMessage();
// keepAliveMSG.setMessageText("\r");
// keepAliveMSG.setMessageDirectedToServer(true);
//
// System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " [UserAct]: Sending keepalive: "
// + keepAliveMSG.getMessageText());
// /**
// * Sending keepalive
// */
// this.client.getMessageTXBus().add(keepAliveMSG);
/**
* keeepalive end
*/
// System.out.println("[UserAct]: Show the Userlist with known frequencies sorted now: ");
// ObservableList<ChatMember> userlist = this.client.getLst_chatMemberList();
// for (Iterator iterator = userlist.iterator(); iterator.hasNext();) {
// ChatMember chatMember = (ChatMember) iterator.next();
// System.out.println("[Useract] Entry " + this.client.getLst_chatMemberList().indexOf(chatMember) + ": " + chatMember.getCallSign());
// }
//
// String chatMembers ="";
// SortedSet<String> keys = new TreeSet<>(this.client.getChatMemberTable().keySet());
// for (String key : keys) {
//
// chatMembers += this.client.getChatMemberTable().get(key).getCallSign() + ", "
// + this.client.getChatMemberTable().get(key).getName() + " in "
// + this.client.getChatMemberTable().get(key).getQra() + " @ QRG: "
// + this.client.getChatMemberTable().get(key).getFrequency() + "\n";
//
// System.out.println(this.client.getChatMemberTable().get(key).getCallSign() + ", "
// + this.client.getChatMemberTable().get(key).getName() + " in "
// + this.client.getChatMemberTable().get(key).getQra() + " @ QRG: "
// + this.client.getChatMemberTable().get(key).getFrequency());
// }
// System.out.println("\n[UserAct]: Show the Clusterlist with known frequencies sorted now: ");
// String dxcMembers ="";
// SortedSet<String> keys2 = new TreeSet<>(this.client.getdXClusterMemberTable().keySet());
// for (String key : keys2) {
// System.out.println(this.client.getdXClusterMemberTable().get(key).getCallSign() + " in "
// + this.client.getdXClusterMemberTable().get(key).getQra() + " @ QRG: "
// + this.client.getdXClusterMemberTable().get(key).getFrequency());
//
// dxcMembers += this.client.getdXClusterMemberTable().get(key).getCallSign() + " in "
// + this.client.getdXClusterMemberTable().get(key).getQra() + " @ QRG: "
// + this.client.getdXClusterMemberTable().get(key).getFrequency();
//
// }
// File userListLogger = new File(new Utils4KST().time_generateCurrentMMddString() + "_praktiKST_userlist.txt");
//
// FileWriter fileWriterRAWChatMSGOut = null;
//
// try {
// fileWriterRAWChatMSGOut = new FileWriter(userListLogger, true);
// } catch (IOException e1) {
// // TODO Auto-generated catch block
// e1.printStackTrace();
// }
//
// BufferedWriter bufwrtrRawMSGOut;
//
// bufwrtrRawMSGOut = new BufferedWriter(fileWriterRAWChatMSGOut);
// System.out.println("#######################################" + chatMembers);
// try {
// bufwrtrRawMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " " +this.client.getChatMemberTable().size() + " Chatmembers:\n" + chatMembers+ "\n");
// bufwrtrRawMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " " + this.client.getdXClusterMemberTable().size() + " Clusterentries:\n" + dxcMembers + "\n");
// bufwrtrRawMSGOut.flush();
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// try {
// bufwrtrRawMSGOut.close();
// } catch (IOException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
// }
}
private boolean createMissingLogFile(Path logFile) {
if (Files.exists(logFile)) {
if (!Files.isRegularFile(logFile)) {
LOGGER.log(Level.WARNING,
"The selected Simplelogfile path is not a regular file: {0}",
logFile);
}
return false;
}
try {
Files.createFile(logFile);
LOGGER.log(Level.INFO, "Created missing Simplelogfile {0}.", logFile);
return true;
} catch (FileAlreadyExistsException exception) {
return false;
} catch (IOException | SecurityException exception) {
LOGGER.log(Level.WARNING, "Cannot create Simplelogfile " + logFile, exception);
return false;
}
}
}
@@ -13,9 +13,10 @@ import java.util.regex.Pattern;
* Common parser for explicit amateur-radio frequencies embedded in text.
*
* <p>This parser deliberately handles only complete frequencies such as
* 144.300, 432.357 or 10368.100. Relative forms such as ".210" or ambiguous
* bare values such as "210" require additional message context and remain the
* responsibility of the chat-message parser.</p>
* 144.300, 432.357, 10368.100 or their compact digit-only forms. Relative
* forms such as ".210" or ambiguous bare values such as "210" require
* additional message context and remain the responsibility of the
* chat-message parser.</p>
*/
public final class FrequencyTextParser {
@@ -26,14 +27,18 @@ public final class FrequencyTextParser {
* 432,357
* 10368.100
* 144.300.03
* 144300
* 10368100
*
* At least two digits are required before the decimal separator. This
* intentionally prevents "1.2" from being interpreted as a frequency.
* At least two digits are required before a decimal separator. Compact
* values need at least five digits because their final three digits form
* the kHz part. This intentionally prevents "1.2" and bare values such as
* "210" from being interpreted as complete frequencies.
*/
private static final Pattern EXPLICIT_FREQUENCY_PATTERN = Pattern.compile(
"(?<![A-Z0-9])"
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)"
+ "(?!\\d)",
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?|\\d{5,8})"
+ "(?![A-Z0-9])",
Pattern.CASE_INSENSITIVE
);
@@ -93,12 +98,12 @@ public final class FrequencyTextParser {
return null;
}
String normalized =
normalizeFrequencyString(
rawFrequency
.trim()
.replace(',', '.')
);
final String trimmedFrequency = rawFrequency.trim();
final String normalized = trimmedFrequency.matches("\\d{5,8}")
? trimmedFrequency.substring(0, trimmedFrequency.length() - 3)
+ "."
+ trimmedFrequency.substring(trimmedFrequency.length() - 3)
: normalizeFrequencyString(trimmedFrequency.replace(',', '.'));
try {
double frequencyMHz =
@@ -188,4 +193,4 @@ public final class FrequencyTextParser {
return sourceText;
}
}
}
}
@@ -1,11 +1,16 @@
package kst4contest.model;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.OptionalDouble;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
@@ -50,7 +55,7 @@ public class ChatPreferences {
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
*/
// private static final int CONFIG_VERSION = 2;
public static final int CONFIG_VERSION = 5;
public static final int CONFIG_VERSION = 7;
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
private static final String TAG_CONFIG_VERSION = "configVersion";
@@ -341,6 +346,12 @@ public class ChatPreferences {
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
private boolean GUIstationMapPathAnalysisVisible = true;
private boolean GUIstationMapClusteringEnabled = true;
private final Map<String, Double> tableColumnWidths = new LinkedHashMap<>();
private static final String TAG_TABLE_COLUMN_WIDTH = "tableColumnWidth";
private static final double MIN_TABLE_COLUMN_WIDTH = 16.0;
private static final double MAX_TABLE_COLUMN_WIDTH = 10_000.0;
/*********************************************************************************
@@ -645,6 +656,39 @@ public class ChatPreferences {
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
}
public boolean isGUIstationMapClusteringEnabled() {
return GUIstationMapClusteringEnabled;
}
public void setGUIstationMapClusteringEnabled(boolean GUIstationMapClusteringEnabled) {
this.GUIstationMapClusteringEnabled = GUIstationMapClusteringEnabled;
}
/**
* Returns a stored width for one stable table/leaf-column identity.
*
* @param tableId stable table layout identifier
* @param columnId stable leaf-column identifier
* @return stored pixel width, or empty when no usable value exists
*/
public synchronized OptionalDouble getTableColumnWidth(String tableId, String columnId) {
if (!isValidTableColumnIdentity(tableId, columnId)) {
return OptionalDouble.empty();
}
Double width = tableColumnWidths.get(tableColumnWidthKey(tableId, columnId));
return isValidTableColumnWidth(width) ? OptionalDouble.of(width) : OptionalDouble.empty();
}
/**
* Updates one table leaf-column width in memory. Persistence is coordinated by
* the layout autosave layer.
*/
public synchronized void setTableColumnWidth(String tableId, String columnId, double width) {
if (isValidTableColumnIdentity(tableId, columnId) && isValidTableColumnWidth(width)) {
tableColumnWidths.put(tableColumnWidthKey(tableId, columnId), width);
}
}
public boolean isGuiOptions_defaultFilterNothing() {
return guiOptions_defaultFilterNothing;
}
@@ -1389,7 +1433,7 @@ public class ChatPreferences {
*
* @return true if the file writing was successful, else false
*/
public boolean writePreferencesToXmlFile() {
public synchronized boolean writePreferencesToXmlFile() {
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
try {
@@ -2085,27 +2129,25 @@ public class ChatPreferences {
);
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
Element GUIstationMapClusteringEnabled = doc.createElement("GUIstationMapClusteringEnabled");
GUIstationMapClusteringEnabled.setTextContent(
String.valueOf(this.isGUIstationMapClusteringEnabled())
);
guiOptions.appendChild(GUIstationMapClusteringEnabled);
appendTableColumnWidths(doc, guiOptions);
/****************************************************************************************
****************************** now write this XML! *************************************
****************************************************************************************/
writeXml(doc, System.out);
// write dom document to a file
try (FileOutputStream output =
new FileOutputStream(storeAndRestorePreferencesFileName)) {
writeXml(doc, output);
} catch (IOException e) {
e.printStackTrace();
} catch (TransformerException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
writeDocumentAtomically(doc);
} catch (ParserConfigurationException | TransformerException e1) {
} catch (ParserConfigurationException | TransformerException | IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
return false;
}
return true;
@@ -2117,6 +2159,113 @@ public class ChatPreferences {
}
/**
* Writes only layout values into the existing preferences document. Functional
* settings are deliberately read from disk and left untouched.
*/
public synchronized boolean writeLayoutPreferencesToXmlFile() {
Path preferencesPath = Path.of(storeAndRestorePreferencesFileName).toAbsolutePath();
if (!Files.isRegularFile(preferencesPath)) {
System.out.println("[ChatPreferences, Warning]: Cannot autosave layout because preferences.xml does not exist.");
return false;
}
try {
DocumentBuilder documentBuilder = createSecureDocumentBuilderFactory().newDocumentBuilder();
Document document = documentBuilder.parse(preferencesPath.toFile());
Element root = document.getDocumentElement();
if (root == null) {
return false;
}
upsertDirectChildText(document, root, TAG_CONFIG_VERSION, String.valueOf(CONFIG_VERSION));
Element guiOptions = getDirectChildElement(root, "guiOptions");
if (guiOptions == null) {
guiOptions = document.createElement("guiOptions");
root.appendChild(guiOptions);
}
updateLayoutElements(document, guiOptions);
removeDirectChildren(guiOptions, TAG_TABLE_COLUMN_WIDTH);
appendTableColumnWidths(document, guiOptions);
writeDocumentAtomically(document);
return true;
} catch (ParserConfigurationException | SAXException | IOException | TransformerException exception) {
exception.printStackTrace();
return false;
}
}
private void updateLayoutElements(Document document, Element guiOptions) {
upsertDirectChildText(document, guiOptions, "GUIscn_ChatwindowMainSceneSizeHW",
getGUIscn_ChatwindowMainSceneSizeHW()[0] + ";" + getGUIscn_ChatwindowMainSceneSizeHW()[1]);
upsertDirectChildText(document, guiOptions, "GUIclusterAndQSOMonStage_SceneSizeHW",
getGUIclusterAndQSOMonStage_SceneSizeHW()[0] + ";" + getGUIclusterAndQSOMonStage_SceneSizeHW()[1]);
upsertDirectChildText(document, guiOptions, "GUIstage_updateStage_SceneSizeHW",
getGUIstage_updateStage_SceneSizeHW()[0] + ";" + getGUIstage_updateStage_SceneSizeHW()[1]);
upsertDirectChildText(document, guiOptions, "GUIsettingsStageSceneSizeHW",
getGUIsettingsStageSceneSizeHW()[0] + ";" + getGUIsettingsStageSceneSizeHW()[1]);
upsertDirectChildText(document, guiOptions, "GUIselectedCallSignSplitPane_dividerposition",
doubleArrayToCSVString(getGUIselectedCallSignSplitPane_dividerposition()));
upsertDirectChildText(document, guiOptions, "GUImainWindowLeftSplitPane_dividerposition",
doubleArrayToCSVString(getGUImainWindowLeftSplitPane_dividerposition()));
upsertDirectChildText(document, guiOptions, "GUImessageSectionSplitpane_dividerposition",
doubleArrayToCSVString(getGUImessageSectionSplitpane_dividerposition()));
upsertDirectChildText(document, guiOptions, "GUImainWindowRightSplitPane_dividerposition",
doubleArrayToCSVString(getGUImainWindowRightSplitPane_dividerposition()));
upsertDirectChildText(document, guiOptions, "GUIpnl_directedMSGWin_dividerpositionDefault",
doubleArrayToCSVString(getGUIpnl_directedMSGWin_dividerpositionDefault()));
upsertDirectChildText(document, guiOptions, "GUIstationMapStageSceneSizeHW",
getGUIstationMapStageSceneSizeHW()[0] + ";" + getGUIstationMapStageSceneSizeHW()[1]);
upsertDirectChildText(document, guiOptions, "GUIstationMapStagePositionXY",
getGUIstationMapStagePositionXY()[0] + ";" + getGUIstationMapStagePositionXY()[1]);
upsertDirectChildText(document, guiOptions, "GUIstationMapClusteringEnabled",
String.valueOf(isGUIstationMapClusteringEnabled()));
}
private void appendTableColumnWidths(Document document, Element guiOptions) {
for (Map.Entry<String, Double> entry : tableColumnWidths.entrySet()) {
if (!isValidTableColumnWidth(entry.getValue())) {
continue;
}
int separatorIndex = entry.getKey().indexOf('\u0000');
if (separatorIndex <= 0 || separatorIndex >= entry.getKey().length() - 1) {
continue;
}
Element widthElement = document.createElement(TAG_TABLE_COLUMN_WIDTH);
widthElement.setAttribute("tableId", entry.getKey().substring(0, separatorIndex));
widthElement.setAttribute("columnId", entry.getKey().substring(separatorIndex + 1));
widthElement.setAttribute("pixels", String.valueOf(entry.getValue()));
guiOptions.appendChild(widthElement);
}
}
private void writeDocumentAtomically(Document document) throws IOException, TransformerException {
Path target = Path.of(storeAndRestorePreferencesFileName).toAbsolutePath();
Path parent = target.getParent();
Path fileName = target.getFileName();
if (parent == null || fileName == null) {
throw new IOException("Preferences path has no parent directory: " + target);
}
Files.createDirectories(parent);
Path temporary = Files.createTempFile(parent, fileName.toString(), ".tmp");
try {
try (OutputStream output = Files.newOutputStream(temporary)) {
writeXml(document, output);
}
try {
Files.move(temporary, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
} catch (AtomicMoveNotSupportedException exception) {
Files.move(temporary, target, StandardCopyOption.REPLACE_EXISTING);
}
} finally {
Files.deleteIfExists(temporary);
}
}
// write doc to output stream
private static void writeXml(Document doc, OutputStream output) throws TransformerException {
@@ -2824,6 +2973,7 @@ public class ChatPreferences {
* case read GUI options
*
***********************************************/
this.setGUIstationMapClusteringEnabled(true);
list = doc.getElementsByTagName("guiOptions");
if (list.getLength() != 0) {
@@ -2862,6 +3012,17 @@ public class ChatPreferences {
"GUIstationMapPathAnalysisVisible"
));
/*
* Files written before config version 7 do not contain this value.
* Missing or malformed values keep clustering enabled so existing
* installations retain the established map behaviour.
*/
this.setGUIstationMapClusteringEnabled(getBooleanOrDefault(
element,
true,
"GUIstationMapClusteringEnabled"
));
// Splitpane divider positions
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
if (s1 != null) {
@@ -2920,6 +3081,22 @@ public class ChatPreferences {
if (s5 != null) {
this.setGUIpnl_directedMSGWin_dividerpositionDefault(csvStringToDoubleArray(s5));
}
tableColumnWidths.clear();
NodeList widthElements = element.getElementsByTagName(TAG_TABLE_COLUMN_WIDTH);
for (int widthIndex = 0; widthIndex < widthElements.getLength(); widthIndex++) {
Node widthNode = widthElements.item(widthIndex);
if (!(widthNode instanceof Element widthElement)) {
continue;
}
String tableId = widthElement.getAttribute("tableId");
String columnId = widthElement.getAttribute("columnId");
double width = parseDoubleOrDefault(widthElement.getAttribute("pixels"), Double.NaN);
if (isValidTableColumnIdentity(tableId, columnId) && isValidTableColumnWidth(width)) {
tableColumnWidths.put(tableColumnWidthKey(tableId, columnId), width);
}
}
}
}
}
@@ -3100,6 +3277,63 @@ public class ChatPreferences {
return (n instanceof Element) ? (Element) n : null;
}
private static DocumentBuilderFactory createSecureDocumentBuilderFactory()
throws ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
return factory;
}
private static Element getDirectChildElement(Element parent, String tagName) {
for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) {
if (child instanceof Element element && tagName.equals(element.getTagName())) {
return element;
}
}
return null;
}
private static void upsertDirectChildText(
Document document,
Element parent,
String tagName,
String value
) {
Element element = getDirectChildElement(parent, tagName);
if (element == null) {
element = document.createElement(tagName);
parent.appendChild(element);
}
element.setTextContent(value);
}
private static void removeDirectChildren(Element parent, String tagName) {
for (Node child = parent.getFirstChild(); child != null; ) {
Node next = child.getNextSibling();
if (child instanceof Element element && tagName.equals(element.getTagName())) {
parent.removeChild(child);
}
child = next;
}
}
private static String tableColumnWidthKey(String tableId, String columnId) {
if (!isValidTableColumnIdentity(tableId, columnId)) {
return "";
}
return tableId + '\u0000' + columnId;
}
private static boolean isValidTableColumnIdentity(String tableId, String columnId) {
return tableId != null && !tableId.isBlank() && tableId.indexOf('\u0000') < 0
&& columnId != null && !columnId.isBlank() && columnId.indexOf('\u0000') < 0;
}
private static boolean isValidTableColumnWidth(Double width) {
return width != null && Double.isFinite(width)
&& width >= MIN_TABLE_COLUMN_WIDTH && width <= MAX_TABLE_COLUMN_WIDTH;
}
/**
* Returns the text content of the first matching child tag (directly under {@code parent})
* or {@code defaultValue} if the tag does not exist or is empty.
@@ -3139,6 +3373,20 @@ public class ChatPreferences {
return "true".equalsIgnoreCase(v) || "1".equals(v) || "yes".equalsIgnoreCase(v);
}
private static boolean getBooleanOrDefault(Element parent, boolean defaultValue, String... tagNames) {
String value = getText(parent, null, tagNames);
if (value == null) {
return defaultValue;
}
if ("true".equalsIgnoreCase(value) || "1".equals(value) || "yes".equalsIgnoreCase(value)) {
return true;
}
if ("false".equalsIgnoreCase(value) || "0".equals(value) || "no".equalsIgnoreCase(value)) {
return false;
}
return defaultValue;
}
private static int getInt(Element parent, int defaultValue, String... tagNames) {
String v = getText(parent, null, tagNames);
if (v == null) {
@@ -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.*;
@@ -85,6 +84,9 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
private static final Logger LOGGER = Logger.getLogger(
Kst4ContestApplication.class.getName());
private static final String SIMPLE_LOG_MANUAL_URL =
"https://kst4contest.hamradioonline.de/manual/en/log-sync/"
+ "#method-1-universal-file-based-callsign-interpreter-simplelogfile";
private boolean gridSquareHighlightEnabled = false;
@@ -117,6 +119,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
private StationMapView stationMapView; //view class for the avl stn map
private StationMapBridge stationMapBridge; //bridge for mapping actions between map and view
private LayoutAutosave layoutAutosave;
private final Button btnConnectionStateIndicator = new Button("LINK");
private final Tooltip tipConnectionStateIndicator = new Tooltip();
@@ -184,7 +187,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
return;
}
stationMapView = new StationMapView(chatcontroller.getChatPreferences());
stationMapView = new StationMapView(
chatcontroller.getChatPreferences(),
this::requestLayoutSave
);
stationMapBridge = new StationMapBridge(
chatcontroller,
tbl_chatMember,
@@ -535,7 +541,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
* Builds the tooltip shown on a band-status cell: a fixed legend plus this row's
* resolved status for the given band.
*/
private Tooltip buildBandCellStatusTooltip(ChatMember chatMember, Band band, String status) {
private String buildBandCellStatusTooltipText(ChatMember chatMember, Band band, String status) {
StringBuilder tooltip = new StringBuilder("Band status:\n")
.append("X = worked on this band\n")
.append("B+ = band available, not worked on this band yet (call already worked on another band)\n")
@@ -549,30 +555,26 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
.append("Status: ").append(status == null || status.isBlank() ? "-" : status);
}
return new Tooltip(tooltip.toString());
return tooltip.toString();
}
/**
* Creates a shared cell factory for one band-status column, attaching the
* {@link #buildBandCellStatusTooltip(ChatMember, Band, String)} tooltip.
* {@link #buildBandCellStatusTooltipText(ChatMember, Band, String)} tooltip.
*/
private Callback<TableColumn<ChatMember, String>, TableCell<ChatMember, String>> createBandStatusCellFactory(Band band) {
return column -> new TableCell<ChatMember, String>() {
return column -> new TruncatedTextTableCell<ChatMember>(
java.util.function.Function.identity(),
(member, status) -> buildBandCellStatusTooltipText(member, band, status)
) {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setTooltip(null);
setStyle("");
return;
}
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
setText(item);
setTooltip(buildBandCellStatusTooltip(member, band, item));
setAlignment(Pos.CENTER);
setStyle("-fx-font-weight: bold;");
}
@@ -1372,6 +1374,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
// System.out.println("<<<<<<<<<<<<<<<<<<< devider " + selectedCallSignSplitPane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + selectedCallSignSplitPane.getDividers().size());
chatcontroller.getChatPreferences().getGUIselectedCallSignSplitPane_dividerposition()[selectedCallSignSplitPane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
requestLayoutSave();
}
});
@@ -1647,9 +1650,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}
});
tbl_chatMemberTable.setTooltip(new Tooltip(
"Stations available \n\nUse right click to a station to select predefined texts\nor hit <strg> + <1> ... <9> to write textsnippet to selected station\n\nHit <enter> to send"));
TableColumn<ChatMember, String> callSignCol =
new TableColumn<ChatMember, String>("Callsign");
@@ -1667,7 +1667,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
return new SimpleStringProperty(displayedCallsign);
});
callSignCol.setCellFactory(column -> new TableCell<ChatMember, String>() {
callSignCol.setCellFactory(column -> new TruncatedTextTableCell<ChatMember>() {
@Override
protected void updateItem(String item, boolean empty) {
@@ -1755,14 +1755,24 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
* <p>No font weight is changed here. The compact worked/grid status such as
* {@code xo} is emphasized only in the worked-any column.</p>
*/
qraCol.setCellFactory(column -> new TableCell<ChatMember, String>() {
qraCol.setCellFactory(column -> new TruncatedTextTableCell<ChatMember>(
java.util.function.Function.identity(),
(member, value) -> {
String grossField = WorkedGrossFieldCache.extractGrossField(value);
boolean gridWorked = member != null
&& chatcontroller != null
&& chatcontroller.isGridSquareWorkedAny(member);
return "Grid status: "
+ (grossField == null ? "unknown" : grossField)
+ "\nGrid worked any: "
+ (gridWorked ? "yes" : "no");
}
) {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setTooltip(null);
setStyle("");
return;
}
@@ -1772,16 +1782,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
&& chatcontroller != null
&& chatcontroller.isGridSquareWorkedAny(member);
String grossField = WorkedGrossFieldCache.extractGrossField(item);
setText(item);
setTooltip(new Tooltip(
"Grid status: "
+ (grossField == null ? "unknown" : grossField)
+ "\nGrid worked any: "
+ (gridWorked ? "yes" : "no")
));
/*
* Important:
* Do not style the cell unless the Grid color button is active AND the
@@ -1927,7 +1927,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
*/
airScoutCol.setCellFactory(new Callback<TableColumn<ChatMember, String>, TableCell<ChatMember, String>>() {
public TableCell call(TableColumn param) {
return new TableCell<ChatMember, String>() {
return new TruncatedTextTableCell<ChatMember>() {
@Override
public void updateItem(String item, boolean empty) {
@@ -1951,7 +1951,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
this.getStyleClass().add("table-cell-50PercentAP");
}
setText(item);
}
}
};
@@ -2036,22 +2035,18 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
/**
* Shows the compact worked/grid status and explains it by tooltip.
*/
wkdAny_subcol.setCellFactory(column -> new TableCell<ChatMember, String>() {
wkdAny_subcol.setCellFactory(column -> new TruncatedTextTableCell<ChatMember>(
java.util.function.Function.identity(),
(member, value) -> buildWorkedAnyGridStatusTooltip(member)
) {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setTooltip(null);
setStyle("");
return;
}
ChatMember member = getTableRow() == null ? null : getTableRow().getItem();
setText(item);
setTooltip(new Tooltip(buildWorkedAnyGridStatusTooltip(member)));
setAlignment(Pos.CENTER);
setStyle("-fx-font-weight: bold;");
}
@@ -2387,7 +2382,38 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
// }, new Date(), 5000);
tbl_chatMemberTable.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
tbl_chatMemberTable.autosize();
applyTruncatedTextCells(
nameCol, qrBCol, qtfCol, tropoCol, priorityScoreCol,
lastActCol, notQRVCol, chatCategoryCol
);
TableLayoutManager.install(
tbl_chatMemberTable,
"chat-members",
chatcontroller.getChatPreferences(),
layoutAutosave,
TableLayoutManager.column("callsign", callSignCol),
TableLayoutManager.column("name", nameCol).maximumInitialWidth(220),
TableLayoutManager.column("qra", qraCol),
TableLayoutManager.column("qrb", qrBCol),
TableLayoutManager.column("qtf", qtfCol),
TableLayoutManager.column("qrg", qrgCol),
TableLayoutManager.column("tropo", tropoCol),
TableLayoutManager.column("score", priorityScoreCol),
TableLayoutManager.column("activity", lastActCol),
TableLayoutManager.column("airscout", airScoutCol).maximumInitialWidth(190),
TableLayoutManager.column("worked-any", wkdAny_subcol),
TableLayoutManager.column("band-50", sixMCol_subcol),
TableLayoutManager.column("band-70", fourMCol_subcol),
TableLayoutManager.column("band-144", vhfCol_subcol),
TableLayoutManager.column("band-432", uhfCol_subcol),
TableLayoutManager.column("band-1296", shf23_subcol),
TableLayoutManager.column("band-2320", shf13_subcol),
TableLayoutManager.column("band-3400", shf9_subcol),
TableLayoutManager.column("band-5760", shf6_subcol),
TableLayoutManager.column("band-10g", shf3_subcol),
TableLayoutManager.column("not-qrv", notQRVCol).maximumInitialWidth(180),
TableLayoutManager.column("category", chatCategoryCol)
);
/**
@@ -3118,6 +3144,24 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ObservableList<ChatMessage> toOtherMSGList = chatcontroller.getLst_toOtherMessageList();
tbl_furtherInfoAbtCallsignMSGTable.setItems(chatcontroller.getLst_selectedCallSignInfofilteredMessageList());
applyTruncatedTextCells(
timeCol, callSignTRCVCol, callSignRCVRCol, qrgTXerCol,
qrgRXerCol, workedRXCol, workedTXCol
);
TableLayoutManager.install(
tbl_furtherInfoAbtCallsignMSGTable,
"selected-station-messages",
chatcontroller.getChatPreferences(),
layoutAutosave,
TableLayoutManager.column("time", timeCol),
TableLayoutManager.column("call-tx", callSignTRCVCol),
TableLayoutManager.column("call-rx", callSignRCVRCol),
TableLayoutManager.column("last-qrg-tx", qrgTXerCol),
TableLayoutManager.column("last-qrg-rx", qrgRXerCol),
TableLayoutManager.column("message", msgCol).flexible(360),
TableLayoutManager.column("worked-rx", workedRXCol),
TableLayoutManager.column("worked-tx", workedTXCol)
);
return tbl_furtherInfoAbtCallsignMSGTable;
}
@@ -3279,11 +3323,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
Tab dxClusterMessagesTab = new Tab("DXCluster messages");
dxClusterMessagesTab.setTooltip(new Tooltip("DXCluster spots."));
dxClusterMessagesTab.setContent(initDXClusterTable());
dxClusterMessagesTab.setContent(initDXClusterTable("dx-cluster-main"));
Tab qsoOfTheOtherTab = new Tab("QSO of the other");
qsoOfTheOtherTab.setTooltip(new Tooltip("Messages between other stations. This view is not tied to the selected ChatMember."));
qsoOfTheOtherTab.setContent(initChatToOtherMSGTable());
qsoOfTheOtherTab.setContent(initChatToOtherMSGTable("qso-other-main"));
bottomMessageTabs.getTabs().addAll(publicMessagesTab, dxClusterMessagesTab, qsoOfTheOtherTab);
bottomMessageTabs.getSelectionModel().select(publicMessagesTab);
@@ -3435,6 +3479,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ObservableList<ChatMessage> generalMSGList = chatcontroller.getLst_toAllMessageList();
tbl_generalMSGTable.setItems(generalMSGList);
applyTruncatedTextCells(timeCol, callSignCol, nameCol, categoryCol);
TableLayoutManager.install(
tbl_generalMSGTable,
"public-messages",
chatcontroller.getChatPreferences(),
layoutAutosave,
TableLayoutManager.column("time", timeCol),
TableLayoutManager.column("callsign", callSignCol),
TableLayoutManager.column("name", nameCol).maximumInitialWidth(220),
TableLayoutManager.column("message", msgCol).flexible(360),
TableLayoutManager.column("last-qrg", qrgCol),
TableLayoutManager.column("category", categoryCol)
);
tbl_generalMSGTable.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
@@ -3658,7 +3715,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
*/
airScoutCol.setCellFactory(new Callback<TableColumn<ChatMessage, String>, TableCell<ChatMessage, String>>() {
public TableCell call(TableColumn param) {
return new TableCell<ChatMessage, String>() {
return new TruncatedTextTableCell<ChatMessage>() {
@Override
public void updateItem(String item, boolean empty) {
@@ -3680,7 +3737,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}
setText(item);
}
}
};
@@ -3739,6 +3795,22 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ObservableList<ChatMessage> privateMSGList = chatcontroller.getLst_toMeMessageList();
tbl_privateMSGTable.setItems(privateMSGList);
applyTruncatedTextCells(timeCol, callSignCol, nameCol, qraCol, qrbCol, categoryCol);
TableLayoutManager.install(
tbl_privateMSGTable,
"private-messages",
chatcontroller.getChatPreferences(),
layoutAutosave,
TableLayoutManager.column("time", timeCol),
TableLayoutManager.column("callsign", callSignCol),
TableLayoutManager.column("name", nameCol).maximumInitialWidth(220),
TableLayoutManager.column("qra", qraCol),
TableLayoutManager.column("qrb", qrbCol),
TableLayoutManager.column("message", msgCol).flexible(360),
TableLayoutManager.column("last-qrg", qrgCol),
TableLayoutManager.column("airscout", airScoutCol).maximumInitialWidth(190),
TableLayoutManager.column("category", categoryCol)
);
tbl_privateMSGTable.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
@Override
@@ -3754,67 +3826,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);
}
}
@@ -3823,7 +3882,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
return tbl_privateMSGTable;
}
private TableView<ClusterMessage> initDXClusterTable() {
private TableView<ClusterMessage> initDXClusterTable(String layoutId) {
TableView<ClusterMessage> tbl_DXCTable = new TableView<ClusterMessage>();
// tbl_DXCTable.setTooltip(new Tooltip("Cluster Messages are shown here"));
@@ -3984,11 +4043,29 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ObservableList<ClusterMessage> clusterMSGList = chatcontroller.getLst_clusterMemberList();
tbl_DXCTable.setItems(clusterMSGList);
applyTruncatedTextCells(
timeCol, callSignCol, locTXCol, callSignRXCol,
locRXCol, workedCol
);
TableLayoutManager.install(
tbl_DXCTable,
layoutId,
chatcontroller.getChatPreferences(),
layoutAutosave,
TableLayoutManager.column("time", timeCol),
TableLayoutManager.column("call-tx", callSignCol),
TableLayoutManager.column("locator-tx", locTXCol),
TableLayoutManager.column("call-rx", callSignRXCol),
TableLayoutManager.column("locator-rx", locRXCol),
TableLayoutManager.column("qrg", qrgCol),
TableLayoutManager.column("message", msgCol).flexible(360),
TableLayoutManager.column("worked", workedCol)
);
return tbl_DXCTable;
}
private TableView<ChatMessage> initChatToOtherMSGTable() {
private TableView<ChatMessage> initChatToOtherMSGTable(String layoutId) {
TableView<ChatMessage> tbl_toOtherMSGTable = new TableView<ChatMessage>();
// tbl_toOtherMSGTable.setTooltip(new Tooltip("Messages between other member are shown here"));
@@ -4186,6 +4263,25 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ObservableList<ChatMessage> toOtherMSGList = chatcontroller.getLst_toOtherMessageList();
tbl_toOtherMSGTable.setItems(toOtherMSGList);
applyTruncatedTextCells(
timeCol, callSignTRCVCol, qrgTXerCol, workedTXCol,
callSignRCVRCol, qrgRXerCol, workedRXCol, categoryCol
);
TableLayoutManager.install(
tbl_toOtherMSGTable,
layoutId,
chatcontroller.getChatPreferences(),
layoutAutosave,
TableLayoutManager.column("time", timeCol),
TableLayoutManager.column("call-tx", callSignTRCVCol),
TableLayoutManager.column("last-qrg-tx", qrgTXerCol),
TableLayoutManager.column("worked-tx", workedTXCol),
TableLayoutManager.column("call-rx", callSignRCVRCol),
TableLayoutManager.column("last-qrg-rx", qrgRXerCol),
TableLayoutManager.column("worked-rx", workedRXCol),
TableLayoutManager.column("message", msgCol).flexible(360),
TableLayoutManager.column("category", categoryCol)
);
return tbl_toOtherMSGTable;
}
@@ -5272,6 +5368,28 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
tbl_chatMemberWkdDBTable.getColumns().addAll(callSignCol, workedCol);
tbl_chatMemberWkdDBTable.setItems(chatcontroller.getLst_DBBasedWkdCallSignList());
applyTruncatedTextCells(
callSignCol, wkdAny_subcol, sixMCol_subcol, fourMCol_subcol,
vhfCol_subcol, uhfCol_subcol, shf23_subcol, shf13_subcol,
shf9_subcol, shf6_subcol, shf3_subcol
);
TableLayoutManager.install(
tbl_chatMemberWkdDBTable,
"worked-database",
chatcontroller.getChatPreferences(),
layoutAutosave,
TableLayoutManager.column("callsign", callSignCol),
TableLayoutManager.column("worked-any", wkdAny_subcol),
TableLayoutManager.column("band-50", sixMCol_subcol),
TableLayoutManager.column("band-70", fourMCol_subcol),
TableLayoutManager.column("band-144", vhfCol_subcol),
TableLayoutManager.column("band-432", uhfCol_subcol),
TableLayoutManager.column("band-1296", shf23_subcol),
TableLayoutManager.column("band-2320", shf13_subcol),
TableLayoutManager.column("band-3400", shf9_subcol),
TableLayoutManager.column("band-5760", shf6_subcol),
TableLayoutManager.column("band-10g", shf3_subcol)
);
// TODO: https://www.youtube.com/watch?v=M_kp20qrtLw = tutorial dafuer
@@ -6111,6 +6229,9 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
@Override
public void stop() {
System.out.println("[Main.java, Info:] Stage is closing, killing all resources");
if (layoutAutosave != null) {
layoutAutosave.flushPending();
}
timer_buildWindowTitle.purge();
timer_buildWindowTitle.cancel();
@@ -6130,6 +6251,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
System.exit(0);
}
private void requestLayoutSave() {
if (layoutAutosave != null) {
layoutAutosave.requestSave();
}
}
private Queue<Media> musicList = new LinkedList<Media>();
private MediaPlayer mediaPlayer ;
@@ -6555,6 +6682,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
ChatMember ownChatMemberObject = new ChatMember();
chatcontroller = new ChatController(ownChatMemberObject, this); // instantiate the Chatcontroller with the user object
layoutAutosave = new LayoutAutosave(chatcontroller.getChatPreferences());
messageVariableResolver = new MessageVariableResolver(chatcontroller.getChatPreferences());
chatcontroller.setStatusListener(this); //callback interface for updating Thread events in visual
@@ -6698,6 +6826,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
chatcontroller.getChatPreferences().getGUIscn_ChatwindowMainSceneSizeHW()[1] = newWidthValue.doubleValue();
requestLayoutSave();
}
});
@@ -6705,6 +6834,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
chatcontroller.getChatPreferences().getGUIscn_ChatwindowMainSceneSizeHW()[0] = newHeightValue.doubleValue();
requestLayoutSave();
}
});
@@ -7410,6 +7540,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<< devider>>>>>> " + messageSectionSplitpane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + messageSectionSplitpane.getDividers().size());
chatcontroller.getChatPreferences().getGUImessageSectionSplitpane_dividerposition()[messageSectionSplitpane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
requestLayoutSave();
}
});
@@ -8593,6 +8724,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
System.out.println("<<<<<<<<<<<<<<<<<<< mainWindowLeftSplitPanedevider " + mainWindowLeftSplitPane.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + mainWindowLeftSplitPane.getDividers().size());
chatcontroller.getChatPreferences().getGUImainWindowLeftSplitPane_dividerposition()[mainWindowLeftSplitPane.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
requestLayoutSave();
}
});
@@ -8626,6 +8758,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
if (dividerIndex >= 0 && dividerIndex < storedPositions.length) {
storedPositions[dividerIndex] = newDividerPosition.doubleValue();
requestLayoutSave();
} else {
// Avoid crashes if preferences are older than the current UI layout.
System.out.println("WARN: cannot store mainWindowRightSplitPane divider position: index="
@@ -8674,7 +8807,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
SplitPane pnl_directedMSGWin = new SplitPane();
pnl_directedMSGWin.setOrientation(Orientation.VERTICAL);
pnl_directedMSGWin.setDividerPositions(chatcontroller.getChatPreferences().getGUIpnl_directedMSGWin_dividerpositionDefault());
pnl_directedMSGWin.getItems().addAll(initDXClusterTable(), initChatToOtherMSGTable());
pnl_directedMSGWin.getItems().addAll(
initDXClusterTable("dx-cluster-monitor"),
initChatToOtherMSGTable("qso-other-monitor")
);
/**
@@ -8687,6 +8823,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
public void changed(ObservableValue<? extends Number> observableValue, Number oldDividerPos, Number newDividerPosition) {
System.out.println("<<<<<<<<<<<<<<<<<<<|||||||||||||||||||| devider " + pnl_directedMSGWin.getDividers().indexOf(divider) + " position change, new position: " + newDividerPosition + " // size dev: " + pnl_directedMSGWin.getDividers().size());
chatcontroller.getChatPreferences().getGUIpnl_directedMSGWin_dividerpositionDefault()[pnl_directedMSGWin.getDividers().indexOf(divider)] = newDividerPosition.doubleValue();
requestLayoutSave();
}
});
@@ -8700,6 +8837,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newHeightValue) {
chatcontroller.getChatPreferences().getGUIclusterAndQSOMonStage_SceneSizeHW()[1] = newHeightValue.doubleValue();
requestLayoutSave();
}
});
@@ -8707,6 +8845,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number number, Number newWidthValue) {
chatcontroller.getChatPreferences().getGUIclusterAndQSOMonStage_SceneSizeHW()[0] = newWidthValue.doubleValue();
requestLayoutSave();
}
});
@@ -8820,6 +8959,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
System.out.println("SRVR Version: " + chatcontroller.getUpdateInformation().getLatestVersionNumberOnServer() + " // installed version " + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER);
stage_updateStage.setScene(new Scene(vbxUpdateWindow, chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[0], chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[1]));
stage_updateStage.getScene().widthProperty().addListener((observable, oldValue, newValue) -> {
chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[0] = newValue.doubleValue();
requestLayoutSave();
});
stage_updateStage.getScene().heightProperty().addListener((observable, oldValue, newValue) -> {
chatcontroller.getChatPreferences().getGUIstage_updateStage_SceneSizeHW()[1] = newValue.doubleValue();
requestLayoutSave();
});
// if (chatcontroller.getUpdateInformation().getLatestVersionNumberOnServer() > ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER) {
@@ -10747,7 +10894,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
testSpot.setFrequency(
new SimpleStringProperty("300")
);
testSpot.setQra("Testing DXC-Spot: Congrats, you donated $100!");
testSpot.setQra("DXC test: You donated $100!");
testSpot.setCallSign("DO5AMF");
if (!dxClusterServer
@@ -11388,6 +11535,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
confirmation.setContentText(
"This removes all worked markings, manually assigned NOT-QRV tags "
+ "and stored worked-grid information. The operation cannot be undone.\n\n"
+ "The selected Simplelogfile is not changed. Callsigns contained in it will be "
+ "marked as worked again when the file is read within the next minute.\n\n"
+ "A reset before every contest is normally not required because these data "
+ "expire automatically after three days."
);
@@ -11430,7 +11579,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
information.setContentText(
affectedLines
+ " callsign database entries were updated. "
+ "Worked-grid data were cleared as well."
+ "Worked-grid data were cleared as well. The selected Simplelogfile was not changed; "
+ "callsigns contained in it will be marked as worked again when the file is next read."
);
information.show();
});
@@ -11770,7 +11920,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
System.out.println("saved");
chatcontroller.getChatPreferences().writePreferencesToXmlFile();
if (chatcontroller.getChatPreferences().writePreferencesToXmlFile()
&& layoutAutosave != null) {
layoutAutosave.cancelPending();
}
Alert a = new Alert(AlertType.INFORMATION);
a.setTitle("Info");
@@ -11808,6 +11961,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
// VBox vBox = new VBox(tabPaneOptions);
settingsScene = new Scene(optionsPanel, chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[0], chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[1]);
settingsScene.getStylesheets().add(ApplicationConstants.STYLECSSFILE_DEFAULT_DAYLIGHT);
settingsScene.widthProperty().addListener((observable, oldValue, newValue) -> {
chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[0] = newValue.doubleValue();
requestLayoutSave();
});
settingsScene.heightProperty().addListener((observable, oldValue, newValue) -> {
chatcontroller.getChatPreferences().getGUIsettingsStageSceneSizeHW()[1] = newValue.doubleValue();
requestLayoutSave();
});
settingsStage.setScene(settingsScene);
@@ -12143,6 +12304,34 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
});
}
@Override
public void onSimpleLogFileCreated(Path filePath) {
Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Simplelogfile created");
alert.setHeaderText("The selected Simplelogfile did not exist and has been created");
Label explanation = new Label(
"File: " + filePath + "\n\n"
+ "First check whether you need the Simplelogfile integration. If your logging "
+ "application provides a supported network interface, use that interface for "
+ "band and locator information.\n\n"
+ "If you use Simplelogfile, configure your logging application to write its live log to this file. "
+ "Then log a test QSO and verify that the callsign is marked as worked "
+ "in KST4Contest within one minute.\n\n"
+ "Before each contest, verify that the logging application writes the current "
+ "contest log to this exact file. KST4Contest does not reset Simplelogfile-derived "
+ "Worked marks automatically when a new contest starts.");
explanation.setWrapText(true);
Hyperlink manualLink = new Hyperlink("Open the Simplelogfile manual");
manualLink.setOnAction(event -> getHostServices().showDocument(SIMPLE_LOG_MANUAL_URL));
VBox content = new VBox(10, explanation, manualLink);
content.setPrefWidth(560);
alert.getDialogPane().setContent(content);
alert.show();
}
/**
* Forces the station FilteredList to evaluate all active predicates again.
@@ -12495,13 +12684,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}
private static <T> void applyQrgUiFormatting(TableColumn<T, String> col) {
col.setCellFactory(tc -> new TableCell<T, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? "" : formatQrgForUi(item));
}
});
col.setCellFactory(tc -> new TruncatedTextTableCell<>(Kst4ContestApplication::formatQrgForUi));
}
@SafeVarargs
private static <T> void applyTruncatedTextCells(TableColumn<T, String>... columns) {
for (TableColumn<T, String> column : columns) {
column.setCellFactory(ignored -> new TruncatedTextTableCell<>());
}
}
/**
@@ -12952,4 +13142,4 @@ class CheckBoxTableCell<S, T> extends TableCell<S, T> {
}
}
@@ -0,0 +1,50 @@
package kst4contest.view;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.util.Duration;
import kst4contest.model.ChatPreferences;
import java.util.Objects;
/**
* Coalesces JavaFX layout changes into selective preferences writes.
*/
public final class LayoutAutosave {
private static final Duration SAVE_DELAY = Duration.millis(750);
private final ChatPreferences preferences;
private final PauseTransition saveDelay = new PauseTransition(SAVE_DELAY);
private boolean pending;
public LayoutAutosave(ChatPreferences preferences) {
this.preferences = Objects.requireNonNull(preferences, "preferences");
saveDelay.setOnFinished(event -> flushPending());
}
public void requestSave() {
if (!Platform.isFxApplicationThread()) {
Platform.runLater(this::requestSave);
return;
}
pending = true;
saveDelay.playFromStart();
}
public void flushPending() {
if (!pending) {
return;
}
saveDelay.stop();
pending = false;
preferences.writeLayoutPreferencesToXmlFile();
}
public void cancelPending() {
saveDelay.stop();
pending = false;
}
}
@@ -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,280 @@
package kst4contest.view;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.scene.Node;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.MouseEvent;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import kst4contest.model.ChatPreferences;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalDouble;
/**
* Applies persisted leaf-column widths and performs one content-based initial
* sizing pass when no width has been stored yet.
*/
public final class TableLayoutManager {
private static final double CELL_HORIZONTAL_PADDING = 16.0;
private static final double DEFAULT_MINIMUM_WIDTH = 24.0;
private TableLayoutManager() {
}
public static ColumnSpec column(String id, TableColumn<?, String> column) {
return new ColumnSpec(id, column);
}
public static <S> void install(
TableView<S> table,
String tableId,
ChatPreferences preferences,
LayoutAutosave autosave,
ColumnSpec... columnSpecs
) {
Objects.requireNonNull(table, "table");
Objects.requireNonNull(tableId, "tableId");
Objects.requireNonNull(preferences, "preferences");
Objects.requireNonNull(autosave, "autosave");
Map<TableColumn<?, String>, ColumnState> states = new IdentityHashMap<>();
for (ColumnSpec spec : columnSpecs) {
if (spec == null || !spec.column.getColumns().isEmpty()) {
continue;
}
if (spec.column.prefWidthProperty().isBound()) {
spec.column.prefWidthProperty().unbind();
}
spec.column.setId(tableId + "." + spec.id);
OptionalDouble storedWidth = preferences.getTableColumnWidth(tableId, spec.id);
ColumnState state = new ColumnState(spec, storedWidth.isEmpty());
states.put(spec.column, state);
if (storedWidth.isPresent()) {
setWidth(state, storedWidth.getAsDouble());
state.initialized = true;
}
spec.column.widthProperty().addListener((observable, oldWidth, newWidth) -> {
if (state.adjusting || !state.initialized || newWidth == null) {
return;
}
storeWidth(preferences, autosave, tableId, state.spec.id, newWidth.doubleValue());
});
}
installEarlyManualResizeDetection(table, tableId, preferences, autosave, states);
scheduleInitialSizingWhenUsable(table, tableId, preferences, autosave, states);
}
private static <S> void scheduleInitialSizingWhenUsable(
TableView<S> table,
String tableId,
ChatPreferences preferences,
LayoutAutosave autosave,
Map<TableColumn<?, String>, ColumnState> states
) {
if (states.values().stream().noneMatch(state -> !state.initialized)) {
return;
}
if (table.getItems() != null && !table.getItems().isEmpty()) {
Platform.runLater(() -> sizePendingColumns(table, tableId, preferences, autosave, states));
return;
}
@SuppressWarnings("unchecked")
final ListChangeListener<S>[] holder = new ListChangeListener[1];
holder[0] = change -> {
if (table.getItems() == null || table.getItems().isEmpty()) {
return;
}
table.getItems().removeListener(holder[0]);
Platform.runLater(() -> sizePendingColumns(table, tableId, preferences, autosave, states));
};
table.getItems().addListener(holder[0]);
}
private static <S> void sizePendingColumns(
TableView<S> table,
String tableId,
ChatPreferences preferences,
LayoutAutosave autosave,
Map<TableColumn<?, String>, ColumnState> states
) {
for (ColumnState state : states.values()) {
if (state.initialized) {
continue;
}
double width = state.spec.flexible
? flexibleInitialWidth(table, state.spec)
: contentInitialWidth(table, state.spec);
setWidth(state, width);
state.initialized = true;
storeWidth(preferences, autosave, tableId, state.spec.id, width);
}
}
private static <S> double contentInitialWidth(TableView<S> table, ColumnSpec spec) {
Text measurement = new Text();
measurement.setFont(Font.getDefault());
double requiredWidth = measure(spec.column.getText(), measurement);
for (int rowIndex = 0; rowIndex < table.getItems().size(); rowIndex++) {
ObservableValue<?> value = spec.column.getCellObservableValue(rowIndex);
if (value == null || value.getValue() == null) {
continue;
}
requiredWidth = Math.max(
requiredWidth,
measure(String.valueOf(value.getValue()), measurement)
);
}
return calculateInitialContentWidth(
requiredWidth,
spec.minimumWidth,
spec.maximumInitialWidth
);
}
private static <S> double flexibleInitialWidth(TableView<S> table, ColumnSpec spec) {
double tableShare = table.getWidth() > 1.0 ? table.getWidth() * 0.42 : spec.flexibleFallbackWidth;
Text measurement = new Text();
measurement.setFont(Font.getDefault());
double headerWidth = measure(spec.column.getText(), measurement) + CELL_HORIZONTAL_PADDING;
return clamp(Math.max(headerWidth, tableShare), spec.minimumWidth, spec.maximumInitialWidth);
}
private static <S> void installEarlyManualResizeDetection(
TableView<S> table,
String tableId,
ChatPreferences preferences,
LayoutAutosave autosave,
Map<TableColumn<?, String>, ColumnState> states
) {
Map<TableColumn<?, String>, Double> widthsAtHeaderPress = new IdentityHashMap<>();
table.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (!isColumnHeaderEvent(event)) {
return;
}
widthsAtHeaderPress.clear();
states.forEach((column, state) -> widthsAtHeaderPress.put(column, column.getWidth()));
});
table.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> {
if (widthsAtHeaderPress.isEmpty()) {
return;
}
states.forEach((column, state) -> {
Double oldWidth = widthsAtHeaderPress.get(column);
if (oldWidth == null || Math.abs(oldWidth - column.getWidth()) <= 0.5) {
return;
}
state.initialized = true;
storeWidth(preferences, autosave, tableId, state.spec.id, column.getWidth());
});
widthsAtHeaderPress.clear();
});
}
private static boolean isColumnHeaderEvent(MouseEvent event) {
Object target = event.getTarget();
Node node = target instanceof Node ? (Node) target : null;
while (node != null && node.getParent() != null) {
if (node.getStyleClass().contains("column-header")
|| node.getStyleClass().contains("nested-column-header")) {
return true;
}
node = node.getParent();
}
return false;
}
private static void setWidth(ColumnState state, double width) {
state.adjusting = true;
try {
state.spec.column.setPrefWidth(width);
} finally {
state.adjusting = false;
}
}
private static void storeWidth(
ChatPreferences preferences,
LayoutAutosave autosave,
String tableId,
String columnId,
double width
) {
preferences.setTableColumnWidth(tableId, columnId, width);
autosave.requestSave();
}
private static double measure(String value, Text measurement) {
measurement.setText(value == null ? "" : value);
return measurement.getLayoutBounds().getWidth();
}
private static double clamp(double value, double minimum, double maximum) {
return Math.max(minimum, Math.min(value, maximum));
}
/**
* Calculates a compact content width. Package-private for focused sizing tests.
*/
@SuppressWarnings("PMD.CommentDefaultAccessModifier")
static double calculateInitialContentWidth(
final double measuredWidth,
final double minimum,
final double maximum
) {
return clamp(measuredWidth + CELL_HORIZONTAL_PADDING, minimum, maximum);
}
public static final class ColumnSpec {
private final String id;
private final TableColumn<?, String> column;
private double minimumWidth = DEFAULT_MINIMUM_WIDTH;
private double maximumInitialWidth = Double.MAX_VALUE;
private double flexibleFallbackWidth = 320.0;
private boolean flexible;
private ColumnSpec(String id, TableColumn<?, String> column) {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("Column id must not be blank");
}
this.id = id;
this.column = Objects.requireNonNull(column, "column");
}
public ColumnSpec maximumInitialWidth(double maximumInitialWidth) {
this.maximumInitialWidth = maximumInitialWidth;
return this;
}
public ColumnSpec flexible(double fallbackWidth) {
flexible = true;
flexibleFallbackWidth = fallbackWidth;
return this;
}
}
private static final class ColumnState {
private final ColumnSpec spec;
private boolean adjusting;
private boolean initialized;
private ColumnState(ColumnSpec spec, boolean awaitingInitialSizing) {
this.spec = spec;
initialized = !awaitingInitialSizing;
}
}
}
@@ -0,0 +1,109 @@
package kst4contest.view;
import javafx.scene.control.TableCell;
import javafx.scene.control.Tooltip;
import javafx.scene.text.Text;
import javafx.util.Duration;
import java.util.function.Function;
import java.util.function.BiFunction;
/**
* Displays text normally and exposes the full value only when it is clipped.
* An optional functional explanation remains available and is combined with
* the full value when both are needed.
*/
public class TruncatedTextTableCell<S> extends TableCell<S, String> {
private final Function<String, String> formatter;
private final BiFunction<S, String, String> functionalTooltipProvider;
private final Tooltip tooltip = new Tooltip();
private final Text textMeasurement = new Text();
private String fullText = "";
public TruncatedTextTableCell() {
this(Function.identity(), null);
}
public TruncatedTextTableCell(Function<String, String> formatter) {
this(formatter, null);
}
public TruncatedTextTableCell(
Function<String, String> formatter,
BiFunction<S, String, String> functionalTooltipProvider
) {
this.formatter = formatter == null ? Function.identity() : formatter;
this.functionalTooltipProvider = functionalTooltipProvider;
tooltip.setWrapText(true);
tooltip.setMaxWidth(800);
tooltip.setShowDelay(Duration.millis(250));
tooltip.setShowDuration(Duration.seconds(30));
}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
fullText = "";
setText(null);
setGraphic(null);
setTooltip(null);
return;
}
String formatted = formatter.apply(item);
fullText = formatted == null ? "" : formatted;
setText(fullText);
setGraphic(null);
updateTooltip();
}
@Override
protected void layoutChildren() {
super.layoutChildren();
updateTooltip();
}
private void updateTooltip() {
if (isEmpty()) {
setTooltip(null);
return;
}
String functionalText = resolveFunctionalTooltip();
boolean clipped = isTextClipped();
String tooltipText = TruncatedTextTooltipSupport.buildTooltipText(
fullText,
clipped,
functionalText
);
if (tooltipText == null) {
setTooltip(null);
return;
}
tooltip.setText(tooltipText);
setTooltip(tooltip);
}
private String resolveFunctionalTooltip() {
if (functionalTooltipProvider == null || getTableRow() == null) {
return null;
}
return functionalTooltipProvider.apply(getTableRow().getItem(), fullText);
}
private boolean isTextClipped() {
if (fullText.isEmpty()) {
return false;
}
textMeasurement.setText(fullText);
textMeasurement.setFont(getFont());
double requiredWidth = textMeasurement.getLayoutBounds().getWidth();
double availableWidth = Math.max(0.0,
getWidth() - snappedLeftInset() - snappedRightInset() - 2.0);
return TruncatedTextTooltipSupport.isTextClipped(requiredWidth, availableWidth);
}
}
@@ -0,0 +1,28 @@
package kst4contest.view;
/**
* Pure tooltip decisions kept separate from JavaFX controls for unit testing.
*/
final class TruncatedTextTooltipSupport {
private TruncatedTextTooltipSupport() {
}
static boolean isTextClipped(double requiredWidth, double availableWidth) {
return requiredWidth > availableWidth + 1.0;
}
static String buildTooltipText(String fullText, boolean clipped, String functionalText) {
boolean hasFunctionalText = functionalText != null && !functionalText.isBlank();
if (!clipped && !hasFunctionalText) {
return null;
}
if (!clipped) {
return functionalText;
}
if (!hasFunctionalText) {
return fullText;
}
return fullText + "\n\n" + functionalText;
}
}
@@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets;
* - grid / beam / connection use non-interactive panes
* - JavaScript errors are forwarded to Java through javaMapBridge
* - setTheme(light|dark) aligns the map with the JavaFX application theme
* - setStationClusteringEnabled(boolean) re-renders the existing station data
*
* Important:
* This version intentionally uses integer Leaflet zoom levels again.
@@ -356,6 +357,7 @@ public final class MapHtmlResources {
*/
let stationData = [];
let stationsByCallsignRaw = {};
let stationClusteringEnabled = true;
let clustersById = {};
let clusterSequence = 0;
@@ -810,13 +812,19 @@ public final class MapHtmlResources {
return;
}
if (Number(map.getZoom()) >= KST_CLUSTER_DISABLE_ZOOM) {
if (!stationClusteringEnabled
|| Number(map.getZoom()) >= KST_CLUSTER_DISABLE_ZOOM) {
renderAllStationsIndividually();
} else {
renderClusteredStations();
}
}
function setStationClusteringEnabled(enabled) {
stationClusteringEnabled = Boolean(enabled);
renderStationMarkers();
}
/**
* Zooms into a cluster.
*
@@ -1271,6 +1279,7 @@ public final class MapHtmlResources {
getViewportState: getViewportState,
setHome: setHome,
setStations: setStations,
setStationClusteringEnabled: setStationClusteringEnabled,
setBeam: setBeam,
setConnection: setConnection,
setProfileHoverPoint: setProfileHoverPoint,
@@ -1284,4 +1293,4 @@ public final class MapHtmlResources {
</html>
""".replace("__TILE_PROXY_PORT__", String.valueOf(tileProxyPort));
}
}
}
@@ -14,16 +14,17 @@ import java.util.Locale;
/**
* Small helper service for preparing and filling the local offline DEM directory.
*
* <p>This is intentionally the first practical step before adding a real network
* downloader:
* <p>This service only prepares local data for a possible future offline terrain
* provider:
* <ul>
* <li>create a known default Copernicus DEM root directory below .praktiKST</li>
* <li>copy manually selected local *_DEM.tif files into that directory</li>
* </ul>
*
* <p>The active terrain provider already scans the configured root directory
* recursively. Therefore it is sufficient to import valid Copernicus DGED
* GeoTIFF files into one dedicated folder tree.</p>
* <p>Importing files does not activate an offline provider or change the terrain
* calculation chain. The active provider remains Open-Meteo with Copernicus GLO-90
* data. Valid Copernicus GLO-30 DGED GeoTIFF files are only copied into the prepared
* directory tree.</p>
*/
public final class OfflineDemImportService {
@@ -229,4 +230,4 @@ public final class OfflineDemImportService {
String message
) {
}
}
}
@@ -59,6 +59,7 @@ public final class StationMapView {
private final Label detailPathModeValue = new Label("-");
private final ChatPreferences chatPreferences;
private final Runnable layoutSaveRequester;
private final Stage stage = new Stage();
private final WebView webView = new WebView();
@@ -89,6 +90,9 @@ public final class StationMapView {
private final Button resetViewButton = new Button("Reset view");
private final Tooltip statusTooltip = new Tooltip();
private final CheckBox stationClusteringCheckBox = new CheckBox("Group nearby stations");
private final Tooltip stationClusteringTooltip = new Tooltip(
"Group nearby stations into clusters at lower zoom levels.");
private Runnable onResetView;
@@ -180,7 +184,12 @@ public final class StationMapView {
public StationMapView(ChatPreferences chatPreferences) {
this(chatPreferences, () -> { });
}
public StationMapView(ChatPreferences chatPreferences, Runnable layoutSaveRequester) {
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
this.layoutSaveRequester = Objects.requireNonNull(layoutSaveRequester, "layoutSaveRequester");
GuiUtils.applyApplicationIcon(stage);
try {
@@ -327,6 +336,21 @@ public final class StationMapView {
}
});
stationClusteringCheckBox.setMinWidth(Region.USE_PREF_SIZE);
stationClusteringCheckBox.setTooltip(stationClusteringTooltip);
stationClusteringCheckBox.setAccessibleText("Group nearby stations");
stationClusteringCheckBox.setAccessibleHelp(stationClusteringTooltip.getText());
stationClusteringCheckBox.setSelected(chatPreferences.isGUIstationMapClusteringEnabled());
stationClusteringCheckBox.selectedProperty().addListener((obs, oldValue, newValue) -> {
boolean enabled = newValue;
chatPreferences.setGUIstationMapClusteringEnabled(enabled);
if (mapReady) {
executeMapScriptSafely(
"window.kstMapApi.setStationClusteringEnabled(" + enabled + ");");
}
layoutSaveRequester.run();
});
pathAnalysisVisibilityButton.setMinWidth(Region.USE_PREF_SIZE);
pathAnalysisVisibilityButton.setTooltip(pathAnalysisVisibilityTooltip);
pathAnalysisVisibilityButton.setOnAction(event ->
@@ -461,17 +485,25 @@ public final class StationMapView {
stage.setY(pos[1]);
}
stage.widthProperty().addListener((obs, oldValue, newValue) ->
chatPreferences.getGUIstationMapStageSceneSizeHW()[0] = newValue.doubleValue());
stage.widthProperty().addListener((obs, oldValue, newValue) -> {
chatPreferences.getGUIstationMapStageSceneSizeHW()[0] = newValue.doubleValue();
layoutSaveRequester.run();
});
stage.heightProperty().addListener((obs, oldValue, newValue) ->
chatPreferences.getGUIstationMapStageSceneSizeHW()[1] = newValue.doubleValue());
stage.heightProperty().addListener((obs, oldValue, newValue) -> {
chatPreferences.getGUIstationMapStageSceneSizeHW()[1] = newValue.doubleValue();
layoutSaveRequester.run();
});
stage.xProperty().addListener((obs, oldValue, newValue) ->
chatPreferences.getGUIstationMapStagePositionXY()[0] = newValue.doubleValue());
stage.xProperty().addListener((obs, oldValue, newValue) -> {
chatPreferences.getGUIstationMapStagePositionXY()[0] = newValue.doubleValue();
layoutSaveRequester.run();
});
stage.yProperty().addListener((obs, oldValue, newValue) ->
chatPreferences.getGUIstationMapStagePositionXY()[1] = newValue.doubleValue());
stage.yProperty().addListener((obs, oldValue, newValue) -> {
chatPreferences.getGUIstationMapStagePositionXY()[1] = newValue.doubleValue();
layoutSaveRequester.run();
});
stage.setOnShown(event -> Platform.runLater(() -> {
webView.requestFocus();
@@ -521,6 +553,7 @@ public final class StationMapView {
statusLabel,
triggerClusterSpotButton,
resetViewButton,
stationClusteringCheckBox,
pathAnalysisHiddenHintLabel,
pathAnalysisVisibilityButton
);
@@ -794,6 +827,9 @@ public final class StationMapView {
window.setMember("javaMapBridge", javaMapBridge);
executeMapScriptSafely("window.kstMapApi.init();");
executeMapScriptSafely(
"window.kstMapApi.setStationClusteringEnabled("
+ chatPreferences.isGUIstationMapClusteringEnabled() + ");");
mapReady = true;
applyMapThemeToWebView(chatPreferences.isGUI_darkModeActive());
@@ -0,0 +1,85 @@
package kst4contest.controller;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class ChatControllerInitialWorkedStateTest {
@Test
void loadsEachCompletedInitialListOnceAndAppliesStateToEveryVariant()
throws SQLException {
DBController database = mock(DBController.class);
ChatMember stored = member("9A0BB", 2);
stored.setWorked(true);
stored.setWorked144(true);
stored.setWorked10G(true);
stored.setQrv432(false);
HashMap<String, ChatMember> databaseSnapshot = new HashMap<>();
databaseSnapshot.put(stored.getCallSignRaw(), stored);
when(database.fetchChatMemberWkdDataFromDB())
.thenReturn(databaseSnapshot);
ChatController controller = new ChatController();
controller.setDbHandler(database);
ChatMember mainVariant = member("9A0BB-2", 2);
ChatMember secondVariant = member("9A0BB-70", 3);
ChatMember reconnectMainVariant = member("9A0BB-144", 2);
ChatMember reconnectSecondVariant = member("9A0BB-432", 3);
controller.loadWorkedStateForInitialUserList(List.of(mainVariant));
controller.loadWorkedStateForInitialUserList(List.of(secondVariant));
controller.loadWorkedStateForInitialUserList(
List.of(reconnectMainVariant));
controller.loadWorkedStateForInitialUserList(
List.of(reconnectSecondVariant));
verify(database, times(4)).fetchChatMemberWkdDataFromDB();
for (ChatMember variant : List.of(
mainVariant,
secondVariant,
reconnectMainVariant,
reconnectSecondVariant
)) {
assertTrue(variant.isWorked());
assertTrue(variant.isWorked144());
assertTrue(variant.isWorked10G());
assertFalse(variant.isQrv432());
}
}
@Test
void keepsAmbiguousStationNameFromReplacingCompatibilityFrequency() {
ChatController controller = new ChatController();
ChatMember member = member("DL1ABC", 2);
member.setName("144307 and 432100");
controller.initializeFrequencyFromStationNameIfUnambiguous(member);
assertTrue(
member.getFrequency() == null
|| member.getFrequency().get() == null
|| member.getFrequency().get().isBlank()
);
}
private static ChatMember member(String callSign, int categoryNumber) {
ChatMember member = new ChatMember();
member.setCallSign(callSign);
member.setChatCategory(new ChatCategory(categoryNumber));
return member;
}
}
@@ -0,0 +1,51 @@
package kst4contest.controller;
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.util.List;
import java.util.Set;
import kst4contest.model.ChatMember;
import kst4contest.model.ClusterMessage;
import org.junit.jupiter.api.Test;
class ChatControllerSimpleLogTest {
@Test
void marksEveryActiveVariantOfWorkedBaseCallsign() {
ChatMember categoryTwoVariant = member("9A0BB-2");
ChatMember categoryThreeVariant = member("9A0BB-70");
ChatMember unrelated = member("DL1ABC");
int changed = ChatController.markSimpleLogWorkedMembers(
List.of(categoryTwoVariant, categoryThreeVariant, unrelated),
Set.of("9A0BB"));
assertEquals(2, changed);
assertTrue(categoryTwoVariant.isWorked());
assertTrue(categoryThreeVariant.isWorked());
assertFalse(unrelated.isWorked());
}
@Test
void marksClusterReceiverByBaseCallsignAndHandlesIncompleteMessages() {
ClusterMessage matching = new ClusterMessage();
matching.setReceiver(member("9A0BB-70"));
ClusterMessage incomplete = new ClusterMessage();
int changed = ChatController.markSimpleLogWorkedClusterMessages(
List.of(matching, incomplete), Set.of("9A0BB"));
assertEquals(1, changed);
assertTrue(matching.isReceiverWkd());
assertFalse(incomplete.isReceiverWkd());
}
private static ChatMember member(String callSign) {
ChatMember member = new ChatMember();
member.setCallSign(callSign);
return member;
}
}
@@ -0,0 +1,138 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class DXClusterSpotFormatterTest {
@ParameterizedTest
@MethodSource("supportedFrequencies")
void keepsFixedColumnsAcrossSupportedFrequencies(
String spotter,
String frequency
) {
String line = DXClusterSpotFormatter.formatLine(
spotter,
frequency,
"DL5ASG",
"JO51HK",
"1234Z"
);
assertEquals(DXClusterSpotFormatter.LINE_LENGTH, line.length());
assertEquals(
"DL5ASG",
line.substring(
DXClusterSpotFormatter.DX_CALL_COLUMN - 1,
DXClusterSpotFormatter.DX_CALL_COLUMN - 1 + 6
)
);
assertEquals(
"JO51HK",
line.substring(39, 45)
);
assertEquals(
"1234Z",
line.substring(DXClusterSpotFormatter.TIME_COLUMN - 1)
);
assertEquals(
frequency,
line.substring(0, 24).trim().replaceFirst("^DX de .+?:\\s*", "")
);
}
@Test
void padsShortCommentsAndTruncatesLongCommentsToThirtyCharacters() {
String shortLine = DXClusterSpotFormatter.formatLine(
"DM5M",
"144205.0",
"DL5ASG",
"JO51HK",
"1234Z"
);
String longLine = DXClusterSpotFormatter.formatLine(
"DM5M",
"144205.0",
"DL5ASG",
"123456789012345678901234567890EXTRA",
"1234Z"
);
assertEquals(
"JO51HK" + " ".repeat(24),
shortLine.substring(39, 69)
);
assertEquals(
"123456789012345678901234567890",
longLine.substring(39, 69)
);
}
@Test
void keepsVariableDxCallsignsWithoutMovingTheComment() {
String twelveCharacterLine = DXClusterSpotFormatter.formatLine(
"DO5AMF",
"24048100.0",
"ABCDEFGHIJKL",
"JO51HK AP 1m/100%;4m/75%",
"2359Z"
);
assertEquals("ABCDEFGHIJKL", twelveCharacterLine.substring(26, 38));
assertEquals(
"JO51HK AP 1m/100%;4m/75%" + " ".repeat(6),
twelveCharacterLine.substring(39, 69)
);
assertThrows(
IllegalArgumentException.class,
() -> DXClusterSpotFormatter.formatLine(
"DO5AMF",
"144205.0",
"ABCDEFGHIJKLM",
"JO51HK",
"2359Z"
)
);
}
@Test
void appendsExactlyTwoBellCharactersAndCrlf() {
byte[] payload = DXClusterSpotFormatter.formatPayload(
"DM5M",
"50200.0",
"DL5ASG",
"JO51HK",
"0000Z"
);
assertEquals(DXClusterSpotFormatter.LINE_LENGTH + 4, payload.length);
assertEquals(7, payload[75]);
assertEquals(7, payload[76]);
assertEquals('\r', payload[77]);
assertEquals('\n', payload[78]);
assertEquals(
75,
new String(payload, 0, 75, StandardCharsets.US_ASCII).length()
);
}
private static Stream<Arguments> supportedFrequencies() {
return Stream.of(
Arguments.of("DM5M", "50200.0"),
Arguments.of("DO5AMF", "70250.0"),
Arguments.of("DM5M", "144205.0"),
Arguments.of("DO5AMF", "432088.0"),
Arguments.of("DM5M", "1296338.0"),
Arguments.of("DO5AMF", "10368100.0"),
Arguments.of("DO5AMF", "24048100.0")
);
}
}
@@ -0,0 +1,173 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.stream.Stream;
import kst4contest.model.ChatMember;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
class ExternalLoggedQsoTest {
@ParameterizedTest
@MethodSource("loggerBandAliases")
void resolvesEveryLoggerBandAlias(String rawValue, LoggedQsoBand expectedBand) {
assertEquals(expectedBand, LoggedQsoBand.fromLoggerValue(rawValue));
}
@ParameterizedTest
@MethodSource("winTestBandIds")
void resolvesEveryExistingWinTestBandId(String rawValue, LoggedQsoBand expectedBand) {
assertEquals(expectedBand, LoggedQsoBand.fromWinTestBandId(rawValue));
}
@Test
void unknownOrMissingBandRemainsUnavailable() {
assertNull(LoggedQsoBand.fromLoggerValue(null));
assertNull(LoggedQsoBand.fromLoggerValue(""));
assertNull(LoggedQsoBand.fromLoggerValue("unknown"));
assertNull(LoggedQsoBand.fromWinTestBandId(null));
assertNull(LoggedQsoBand.fromWinTestBandId("99"));
}
@Test
void missingCallsignIsRejectedBeforeAnyStateIsBuilt() {
assertTrue(ExternalLoggedQso.create(null, LoggedQsoBand.BAND_144, "JO50AA", "TEST").isEmpty());
assertTrue(ExternalLoggedQso.create(" ", LoggedQsoBand.BAND_144, "JO50AA", "TEST").isEmpty());
}
@Test
void missingBandSetsOnlyGlobalWorkedState() {
ExternalLoggedQso qso = qso("DL1ABC", null, null);
ChatMember workedCall = qso.toWorkedChatMember();
assertTrue(workedCall.isWorked());
assertFalse(workedCall.isWorked50());
assertFalse(workedCall.isWorked70());
assertFalse(workedCall.isWorked144());
assertFalse(workedCall.isWorked2300());
assertNull(workedCall.getQra());
}
@Test
void winTest50And70MhzIdsSetTheirExistingFlags() {
String qso50Packet = winTestAddQsoPacket("10", "DL1ABC");
String qso70Packet = winTestAddQsoPacket("11", "DL1ABC");
String band50Id = ReadUDPByWintestThread.extractBandIdFromWinTestAddQso(qso50Packet);
String band70Id = ReadUDPByWintestThread.extractBandIdFromWinTestAddQso(qso70Packet);
ChatMember worked50 = qso("DL1ABC", LoggedQsoBand.fromWinTestBandId(band50Id), null)
.toWorkedChatMember();
ChatMember worked70 = qso("DL1ABC", LoggedQsoBand.fromWinTestBandId(band70Id), null)
.toWorkedChatMember();
assertEquals("10", band50Id);
assertEquals("11", band70Id);
assertTrue(worked50.isWorked50());
assertTrue(worked70.isWorked70());
}
@Test
void winTest47And76GhzFlagsRemainAvailableWithoutInventingProjectBands() {
LoggedQsoBand band47 = LoggedQsoBand.fromWinTestBandId("22");
LoggedQsoBand band76 = LoggedQsoBand.fromWinTestBandId("23");
ChatMember worked47 = qso("DL1ABC", band47, "JO50AA").toWorkedChatMember();
ChatMember worked76 = qso("DL1ABC", band76, "JO50AA").toWorkedChatMember();
assertTrue(worked47.isWorked47G());
assertTrue(worked76.isWorked76G());
assertNull(band47.getProjectBand());
assertNull(band76.getProjectBand());
}
@Test
void normalized2320AliasUpdatesEveryActiveCallsignVariant() {
ChatMember firstVariant = member("9A0BB-2");
ChatMember secondVariant = member("9A0BB-70");
ChatMember unrelated = member("DL1ABC");
ExternalLoggedQso qso = qso(
"9A0BB", LoggedQsoBand.fromLoggerValue(" 2320 "), "JO50AA");
int updated = ChatController.markExternalLoggedQsoMembers(
List.of(firstVariant, secondVariant, unrelated), qso);
assertEquals(2, updated);
assertTrue(firstVariant.isWorked());
assertTrue(firstVariant.isWorked2300());
assertEquals("JO50AA", firstVariant.getQra());
assertTrue(secondVariant.isWorked());
assertTrue(secondVariant.isWorked2300());
assertEquals("JO50AA", secondVariant.getQra());
assertFalse(unrelated.isWorked());
}
private static Stream<Arguments> loggerBandAliases() {
return Stream.of(
Arguments.of("50", LoggedQsoBand.BAND_50),
Arguments.of("6m", LoggedQsoBand.BAND_50),
Arguments.of("70", LoggedQsoBand.BAND_70),
Arguments.of("4m", LoggedQsoBand.BAND_70),
Arguments.of("144", LoggedQsoBand.BAND_144),
Arguments.of("2m", LoggedQsoBand.BAND_144),
Arguments.of("432", LoggedQsoBand.BAND_432),
Arguments.of("70cm", LoggedQsoBand.BAND_432),
Arguments.of("1240", LoggedQsoBand.BAND_1296),
Arguments.of("1296", LoggedQsoBand.BAND_1296),
Arguments.of("23cm", LoggedQsoBand.BAND_1296),
Arguments.of("2300", LoggedQsoBand.BAND_2320),
Arguments.of(" 2320 ", LoggedQsoBand.BAND_2320),
Arguments.of("13cm", LoggedQsoBand.BAND_2320),
Arguments.of("3400", LoggedQsoBand.BAND_3400),
Arguments.of("9cm", LoggedQsoBand.BAND_3400),
Arguments.of("5600", LoggedQsoBand.BAND_5760),
Arguments.of(" 5760 ", LoggedQsoBand.BAND_5760),
Arguments.of("6cm", LoggedQsoBand.BAND_5760),
Arguments.of("10G", LoggedQsoBand.BAND_10G),
Arguments.of(" 10368 ", LoggedQsoBand.BAND_10G),
Arguments.of("3cm", LoggedQsoBand.BAND_10G)
);
}
private static Stream<Arguments> winTestBandIds() {
return Stream.of(
Arguments.of(" 10 ", LoggedQsoBand.BAND_50),
Arguments.of("11", LoggedQsoBand.BAND_70),
Arguments.of("12", LoggedQsoBand.BAND_144),
Arguments.of("14", LoggedQsoBand.BAND_432),
Arguments.of("16", LoggedQsoBand.BAND_1296),
Arguments.of("17", LoggedQsoBand.BAND_2320),
Arguments.of("18", LoggedQsoBand.BAND_3400),
Arguments.of("19", LoggedQsoBand.BAND_5760),
Arguments.of("20", LoggedQsoBand.BAND_10G),
Arguments.of("21", LoggedQsoBand.BAND_24G),
Arguments.of("22", LoggedQsoBand.BAND_47G),
Arguments.of("23", LoggedQsoBand.BAND_76G)
);
}
private static ExternalLoggedQso qso(
String callSign,
LoggedQsoBand band,
String locator
) {
return ExternalLoggedQso.create(callSign, band, locator, "TEST").orElseThrow();
}
private static ChatMember member(String callSign) {
ChatMember member = new ChatMember();
member.setCallSign(callSign);
return member;
}
private static String winTestAddQsoPacket(String bandId, String callSign) {
return "ADDQSO: \"STN1\" \"\" \"STN1\" 1762202297 1440000 0 "
+ bandId + " 0 0 0 2 2 \"" + callSign
+ "\" \"599\" \"599001\" \"JO51UM\" \"\" \"\" 0 \"\" \"\" \"\" 44510";
}
}
@@ -0,0 +1,61 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import kst4contest.model.ChatPreferences;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class ExternalLoggerListenerValidationTest {
@Mock
ChatController controller;
@Mock
ChatPreferences preferences;
@Mock
ThreadStatusCallback statusCallback;
@BeforeEach
void configurePreferences() {
when(controller.getChatPreferences()).thenReturn(preferences);
}
@Test
void ucxLogPacketWithoutCallsignIsDiscardedWithoutStoppingProcessing() {
when(preferences.isLogsynch_ucxUDPWkdCallListenerEnabled()).thenReturn(true);
ReadUDPbyUCXMessageThread listener = new ReadUDPbyUCXMessageThread(
12060, controller, statusCallback);
String packet = "<?xml version=\"1.0\"?><contactinfo>"
+ "<band>144</band><call> </call><gridsquare>JO50AA</gridsquare>"
+ "</contactinfo>";
assertDoesNotThrow(() -> listener.processUCXUDPMessage(packet));
verify(controller, never()).applyExternalLoggedQso(any());
verify(controller, never()).getDbHandler();
}
@Test
void winTestPacketWithoutCallsignIsDiscardedWithoutStoppingProcessing() {
when(preferences.getStn_loginCallSignRaw()).thenReturn("DL0TEST");
when(preferences.getLogsynch_wintestNetworkPort()).thenReturn(9871);
ReadUDPByWintestThread listener = new ReadUDPByWintestThread(controller, statusCallback);
String packet = "ADDQSO: \"STN1\" \"\" \"STN1\" 1762202297 1440000 0 12 0 0 0 2 2 "
+ "\" \" \"599\" \"599001\" \"JO51UM\" \"\" \"\" 0 \"\" \"\" \"\" 44510";
assertDoesNotThrow(() -> listener.processWinTestMessage(packet));
verify(controller, never()).applyExternalLoggedQso(any());
verify(controller, never()).getDbHandler();
}
}
@@ -0,0 +1,51 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import javafx.collections.FXCollections;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import org.junit.jupiter.api.Test;
class MessageBusManagementThreadDxClusterCommentTest {
@Test
void keepsLocatorAndAddsCompactAirScoutInformation() {
ChatMember sender = new ChatMember();
sender.setQra("jo51hk");
AirPlane firstAircraft = new AirPlane();
firstAircraft.setArrivingDurationMinutes(1);
firstAircraft.setPotential(100);
AirPlane secondAircraft = new AirPlane();
secondAircraft.setArrivingDurationMinutes(4);
secondAircraft.setPotential(75);
AirPlaneReflectionInfo reflectionInfo = new AirPlaneReflectionInfo();
reflectionInfo.setRisingAirplanes(
FXCollections.observableArrayList(
firstAircraft,
secondAircraft
)
);
sender.setAirPlaneReflectInfo(reflectionInfo);
assertEquals(
"JO51HK AP 1m/100%;4m/75%",
MessageBusManagementThread.buildDxClusterSpotComment(sender)
);
}
@Test
void returnsLocatorWhenAirScoutInformationIsMissing() {
ChatMember sender = new ChatMember();
sender.setQra("JO51HK");
assertEquals(
"JO51HK",
MessageBusManagementThread.buildDxClusterSpotComment(sender)
);
}
}
@@ -0,0 +1,52 @@
package kst4contest.controller;
import kst4contest.model.Band;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatMessage;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import java.util.concurrent.LinkedBlockingQueue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class MessageBusManagementThreadFrequencyTest {
@ParameterizedTest
@ValueSource(booleans = {false, true})
void detectsCompactMicrowaveFrequencyInPublicAndDirectedMessages(
boolean directedMessage
) {
ChatController controller = mock(ChatController.class);
ThreadStatusCallback callback = mock(ThreadStatusCallback.class);
MessageBusManagementThread messageBus =
new MessageBusManagementThread(
controller,
callback,
1L,
new LinkedBlockingQueue<>(),
ignored -> true
);
ChatMember sender = member("DL1ABC");
ChatMessage message = new ChatMessage();
message.setSender(sender);
message.setReceiver(member(directedMessage ? "DL2XYZ" : "ALL"));
message.setMessageText("pse try 10368100");
messageBus.smartFrequencyExtraction(message, null);
verify(controller).applyDetectedFrequencyToActiveMembers(
sender,
Band.B_10G,
10368.100
);
}
private static ChatMember member(String callSign) {
ChatMember member = new ChatMember();
member.setCallSign(callSign);
return member;
}
}
@@ -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,76 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.nio.file.Files;
import java.nio.file.Path;
import kst4contest.model.ChatPreferences;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class UserActualizationTaskTest {
@TempDir
Path temporaryDirectory;
@Mock
ChatController controller;
@Mock
ChatPreferences preferences;
@BeforeEach
void configureControllerPreferences() {
when(controller.getChatPreferences()).thenReturn(preferences);
}
@Test
void disabledInterpreterDoesNotCreateOrReadFile() {
when(preferences.isLogsynch_fileBasedWkdCallInterpreterEnabled()).thenReturn(false);
new UserActualizationTask(controller).run();
verify(controller, never()).applySimpleLogWorkedBaseCalls(anySet());
verify(controller, never()).notifySimpleLogFileCreated(org.mockito.ArgumentMatchers.any());
}
@Test
void createsMissingFileAndNotifiesOnlyOnce() {
Path logFile = temporaryDirectory.resolve("created.log").toAbsolutePath().normalize();
when(preferences.isLogsynch_fileBasedWkdCallInterpreterEnabled()).thenReturn(true);
when(preferences.getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly())
.thenReturn(logFile.toString());
UserActualizationTask task = new UserActualizationTask(controller);
task.run();
task.run();
assertTrue(Files.isRegularFile(logFile));
verify(controller, times(2)).applySimpleLogWorkedBaseCalls(anySet());
verify(controller, times(1)).notifySimpleLogFileCreated(logFile);
}
@Test
void readFailureIsContainedAndDoesNotUpdateUiState() {
when(preferences.isLogsynch_fileBasedWkdCallInterpreterEnabled()).thenReturn(true);
when(preferences.getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly())
.thenReturn(temporaryDirectory.toString());
assertDoesNotThrow(() -> new UserActualizationTask(controller).run());
verify(controller, never()).applySimpleLogWorkedBaseCalls(anySet());
verify(controller, never()).notifySimpleLogFileCreated(org.mockito.ArgumentMatchers.any());
}
}
@@ -0,0 +1,134 @@
package kst4contest.test;
import kst4contest.model.ChatPreferences;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ChatPreferencesLayoutPersistenceTest {
@TempDir
Path temporaryDirectory;
@Test
void columnWidthsSurviveFullXmlRoundTripAndLayoutsStayIndependent() {
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
ChatPreferences written = preferencesAt(preferencesFile);
written.setTableColumnWidth("dx-cluster-main", "message", 410.5);
written.setTableColumnWidth("dx-cluster-monitor", "message", 275.25);
assertTrue(written.writePreferencesToXmlFile());
ChatPreferences restored = preferencesAt(preferencesFile);
assertTrue(restored.readPreferencesFromXmlFile());
assertEquals(410.5,
restored.getTableColumnWidth("dx-cluster-main", "message").orElseThrow());
assertEquals(275.25,
restored.getTableColumnWidth("dx-cluster-monitor", "message").orElseThrow());
assertFalse(restored.getTableColumnWidth("qso-other-main", "message").isPresent());
}
@Test
void legacyXmlWithoutColumnWidthsKeepsWidthsAbsent() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("legacy.xml");
Files.writeString(preferencesFile, """
<?xml version="1.0" encoding="UTF-8"?>
<praktiKST>
<configVersion>5</configVersion>
<guiOptions>
<GUIscn_ChatwindowMainSceneSizeHW>768;1234</GUIscn_ChatwindowMainSceneSizeHW>
</guiOptions>
</praktiKST>
""");
ChatPreferences restored = preferencesAt(preferencesFile);
assertTrue(restored.readPreferencesFromXmlFile());
assertFalse(restored.getTableColumnWidth("public-messages", "time").isPresent());
}
@Test
void invalidColumnWidthEntriesAreIgnored() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("invalid.xml");
Files.writeString(preferencesFile, """
<?xml version="1.0" encoding="UTF-8"?>
<praktiKST>
<configVersion>6</configVersion>
<guiOptions>
<tableColumnWidth tableId="public-messages" columnId="callsign" pixels="NaN"/>
<tableColumnWidth tableId="public-messages" columnId="name" pixels="-20"/>
<tableColumnWidth tableId="public-messages" columnId="category" pixels="999999"/>
<tableColumnWidth tableId="public-messages" columnId="time" pixels="88.5"/>
</guiOptions>
</praktiKST>
""");
ChatPreferences restored = preferencesAt(preferencesFile);
assertTrue(restored.readPreferencesFromXmlFile());
assertFalse(restored.getTableColumnWidth("public-messages", "callsign").isPresent());
assertFalse(restored.getTableColumnWidth("public-messages", "name").isPresent());
assertFalse(restored.getTableColumnWidth("public-messages", "category").isPresent());
assertEquals(88.5,
restored.getTableColumnWidth("public-messages", "time").orElseThrow());
}
@Test
void selectiveLayoutWritePreservesDiskSettingsAndUnknownXml() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("selective.xml");
Files.writeString(preferencesFile, """
<?xml version="1.0" encoding="UTF-8"?>
<praktiKST>
<configVersion>5</configVersion>
<station>
<LoginCallSign>SAVED-CALL</LoginCallSign>
</station>
<futureExtension mode="keep-me"><value>42</value></futureExtension>
<guiOptions>
<GUIscn_ChatwindowMainSceneSizeHW>700;1100</GUIscn_ChatwindowMainSceneSizeHW>
<futureLayoutValue>untouched</futureLayoutValue>
</guiOptions>
</praktiKST>
""");
ChatPreferences preferences = preferencesAt(preferencesFile);
assertTrue(preferences.readPreferencesFromXmlFile());
preferences.setStn_loginCallSign("UNSAVED-CALL");
preferences.getGUIscn_ChatwindowMainSceneSizeHW()[0] = 812;
preferences.getGUIscn_ChatwindowMainSceneSizeHW()[1] = 1340;
preferences.setGUIstationMapClusteringEnabled(false);
preferences.setTableColumnWidth("qso-other-monitor", "call-tx", 123.75);
assertTrue(preferences.writeLayoutPreferencesToXmlFile());
String writtenXml = Files.readString(preferencesFile);
assertTrue(writtenXml.contains("<LoginCallSign>SAVED-CALL</LoginCallSign>"));
assertFalse(writtenXml.contains("UNSAVED-CALL"));
assertTrue(writtenXml.contains("<futureExtension mode=\"keep-me\">"));
assertTrue(writtenXml.contains("<futureLayoutValue>untouched</futureLayoutValue>"));
assertTrue(writtenXml.contains("<configVersion>7</configVersion>"));
assertTrue(writtenXml.contains("<GUIscn_ChatwindowMainSceneSizeHW>812.0;1340.0"));
assertTrue(writtenXml.contains("<GUIstationMapClusteringEnabled>false"
+ "</GUIstationMapClusteringEnabled>"));
ChatPreferences restored = preferencesAt(preferencesFile);
assertTrue(restored.readPreferencesFromXmlFile());
assertEquals("SAVED-CALL", restored.getStn_loginCallSign());
assertFalse(restored.isGUIstationMapClusteringEnabled());
assertEquals(123.75,
restored.getTableColumnWidth("qso-other-monitor", "call-tx").orElseThrow());
}
private ChatPreferences preferencesAt(Path preferencesFile) {
ChatPreferences preferences = new ChatPreferences();
preferences.setStoreAndRestorePreferencesFileName(preferencesFile.toString());
return preferences;
}
}
@@ -0,0 +1,101 @@
package kst4contest.test;
import kst4contest.model.ChatPreferences;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ChatPreferencesStationMapClusteringTest {
@TempDir
Path temporaryDirectory;
@Test
void clusteringIsEnabledByDefault() {
assertTrue(new ChatPreferences().isGUIstationMapClusteringEnabled());
}
@Test
void disabledClusteringSurvivesFullXmlRoundTrip() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
ChatPreferences written = preferencesAt(preferencesFile);
written.setGUIstationMapClusteringEnabled(false);
assertTrue(written.writePreferencesToXmlFile());
String writtenXml = Files.readString(preferencesFile);
assertTrue(writtenXml.contains("<configVersion>7</configVersion>"));
assertTrue(writtenXml.contains("<GUIstationMapClusteringEnabled>false"
+ "</GUIstationMapClusteringEnabled>"));
ChatPreferences restored = preferencesAt(preferencesFile);
assertTrue(restored.readPreferencesFromXmlFile());
assertFalse(restored.isGUIstationMapClusteringEnabled());
}
@Test
void versionSixWithoutClusteringSettingKeepsClusteringEnabled() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("version-six.xml");
Files.writeString(preferencesFile, """
<?xml version="1.0" encoding="UTF-8"?>
<praktiKST>
<configVersion>6</configVersion>
<guiOptions>
<GUIstationMapStageSceneSizeHW>1000.0;800.0</GUIstationMapStageSceneSizeHW>
</guiOptions>
</praktiKST>
""");
ChatPreferences restored = preferencesAt(preferencesFile);
restored.setGUIstationMapClusteringEnabled(false);
assertTrue(restored.readPreferencesFromXmlFile());
assertTrue(restored.isGUIstationMapClusteringEnabled());
}
@Test
void missingGuiOptionsKeepsClusteringEnabled() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("missing.xml");
Files.writeString(preferencesFile, """
<?xml version="1.0" encoding="UTF-8"?>
<praktiKST>
<configVersion>6</configVersion>
</praktiKST>
""");
ChatPreferences restored = preferencesAt(preferencesFile);
restored.setGUIstationMapClusteringEnabled(false);
assertTrue(restored.readPreferencesFromXmlFile());
assertTrue(restored.isGUIstationMapClusteringEnabled());
}
@Test
void invalidClusteringValueKeepsClusteringEnabled() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("invalid.xml");
Files.writeString(preferencesFile, """
<?xml version="1.0" encoding="UTF-8"?>
<praktiKST>
<configVersion>7</configVersion>
<guiOptions>
<GUIstationMapClusteringEnabled>sometimes</GUIstationMapClusteringEnabled>
</guiOptions>
</praktiKST>
""");
ChatPreferences restored = preferencesAt(preferencesFile);
restored.setGUIstationMapClusteringEnabled(false);
assertTrue(restored.readPreferencesFromXmlFile());
assertTrue(restored.isGUIstationMapClusteringEnabled());
}
private ChatPreferences preferencesAt(Path preferencesFile) {
ChatPreferences preferences = new ChatPreferences();
preferences.setStoreAndRestorePreferencesFileName(preferencesFile.toString());
return preferences;
}
}
@@ -0,0 +1,86 @@
package kst4contest.test;
import kst4contest.logic.FrequencyTextParser;
import kst4contest.model.Band;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class FrequencyTextParserRegressionTest {
@ParameterizedTest
@MethodSource("compactFrequenciesAcrossSupportedBands")
void detectsCompactFrequenciesAcrossSupportedBands(
String compactFrequency,
Band expectedBand,
double expectedFrequencyMHz
) {
FrequencyTextParser.DetectedFrequency detected =
FrequencyTextParser.findExplicitFrequencies(
"QRV " + compactFrequency
).get(0);
assertEquals(expectedBand, detected.getBand());
assertEquals(
expectedFrequencyMHz,
detected.getFrequencyMHz(),
0.000_001
);
}
@Test
void detectsReferenceFrequencyInStationName() {
List<FrequencyTextParser.DetectedFrequency> detected =
FrequencyTextParser.findExplicitFrequencies(
"Operator 144307"
);
assertEquals(1, detected.size());
assertEquals(Band.B_144, detected.get(0).getBand());
assertEquals(
144.307,
detected.get(0).getFrequencyMHz(),
0.000_001
);
}
@Test
void rejectsCompactValuesOutsideSupportedBandRanges() {
assertTrue(
FrequencyTextParser.findExplicitFrequencies(
"146100 434100 99999"
).isEmpty()
);
}
@Test
void keepsBareThreeDigitValuesOutOfCompleteFrequencyDetection() {
assertTrue(
FrequencyTextParser.findExplicitFrequencies(
"210 599 144"
).isEmpty()
);
}
private static Stream<Arguments> compactFrequenciesAcrossSupportedBands() {
return Stream.of(
Arguments.of("50278", Band.B_50, 50.278),
Arguments.of("70200", Band.B_70, 70.200),
Arguments.of("145500", Band.B_144, 145.500),
Arguments.of("432100", Band.B_432, 432.100),
Arguments.of("1296100", Band.B_1296, 1296.100),
Arguments.of("2320100", Band.B_2320, 2320.100),
Arguments.of("3400100", Band.B_3400, 3400.100),
Arguments.of("5760100", Band.B_5760, 5760.100),
Arguments.of("10368100", Band.B_10G, 10368.100),
Arguments.of("24048100", Band.B_24G, 24048.100)
);
}
}
@@ -0,0 +1,31 @@
package kst4contest.test;
import kst4contest.view.map.MapHtmlResources;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class MapHtmlResourcesContractTest {
@Test
void stationClusteringCanBeToggledWithoutReplacingStationData() {
String html = MapHtmlResources.createStationMapHtml(12345);
assertTrue(html.contains("let stationClusteringEnabled = true;"));
assertTrue(html.contains("if (!stationClusteringEnabled"));
assertTrue(html.contains("|| Number(map.getZoom()) >= KST_CLUSTER_DISABLE_ZOOM)"));
assertTrue(html.contains("function setStationClusteringEnabled(enabled)"));
int setterStart = html.indexOf("function setStationClusteringEnabled(enabled)");
int setterEnd = html.indexOf('}', setterStart);
String setterBody = html.substring(setterStart, setterEnd);
int stateUpdate = setterBody.indexOf("stationClusteringEnabled = Boolean(enabled);");
int markerRender = setterBody.indexOf("renderStationMarkers();");
assertTrue(stateUpdate >= 0);
assertTrue(markerRender > stateUpdate);
assertFalse(setterBody.contains("stationData ="));
assertTrue(html.contains("setStationClusteringEnabled: setStationClusteringEnabled"));
}
}
@@ -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
)
);
}
}
@@ -1,24 +1,48 @@
package kst4contest.test;
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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import kst4contest.controller.UCXLogFileToHashsetParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
public class TestUCXLogFileToHashsetParser {
class TestUCXLogFileToHashsetParser {
@TempDir
Path temporaryDirectory;
@Test
public static void main(String[] args) {
// TODO Auto-generated method stub
void parsesUniqueWorkedBaseCallsignsWithFixedPattern() throws IOException {
Path logFile = temporaryDirectory.resolve("contest.log");
Files.writeString(logFile, String.join(System.lineSeparator(),
"QSO 001 S53CC 144 MHz",
"QSO 002 9A0BB-70 432 MHz",
"QSO 003 s53cc repeated",
"no station here"));
UCXLogFileToHashsetParser testTheParser = new UCXLogFileToHashsetParser("C:\\UcxLog\\Logs\\DO5AMF\\DVU322_I.UCX");
try {
testTheParser.parse();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Set<String> result = new UCXLogFileToHashsetParser(logFile.toString()).parse();
assertEquals(Set.of("S53CC", "9A0BB"), result);
}
@Test
void closesFileAfterEveryParsePass() throws IOException {
Path logFile = temporaryDirectory.resolve("replaceable.log");
Files.writeString(logFile, "QSO DL1ABC");
UCXLogFileToHashsetParser parser = new UCXLogFileToHashsetParser(logFile.toString());
assertEquals(Set.of("DL1ABC"), parser.parse());
Path replacement = temporaryDirectory.resolve("replacement.log");
Files.move(logFile, replacement);
assertFalse(Files.exists(logFile));
assertTrue(Files.exists(replacement));
}
}
@@ -0,0 +1,19 @@
package kst4contest.view;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TableLayoutManagerTest {
@Test
void contentWidthUsesCompactPadding() {
assertEquals(116.0, TableLayoutManager.calculateInitialContentWidth(100.0, 24.0, 200.0));
}
@Test
void contentWidthStillHonorsMinimumAndMaximum() {
assertEquals(24.0, TableLayoutManager.calculateInitialContentWidth(0.0, 24.0, 200.0));
assertEquals(200.0, TableLayoutManager.calculateInitialContentWidth(250.0, 24.0, 200.0));
}
}
@@ -0,0 +1,32 @@
package kst4contest.view;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TruncatedTextTableCellTest {
@Test
void plainFullTextTooltipIsShownOnlyForClippedText() {
assertNull(TruncatedTextTooltipSupport.buildTooltipText("complete", false, null));
assertEquals("complete",
TruncatedTextTooltipSupport.buildTooltipText("complete", true, null));
}
@Test
void functionalTooltipRemainsAndCombinesWithClippedValue() {
assertEquals("Worked status",
TruncatedTextTooltipSupport.buildTooltipText("X", false, "Worked status"));
assertEquals("Long value\n\nWorked status",
TruncatedTextTooltipSupport.buildTooltipText("Long value", true, "Worked status"));
}
@Test
void clippingComparisonUsesAvailableRenderedWidth() {
assertFalse(TruncatedTextTooltipSupport.isTextClipped(100, 100));
assertTrue(TruncatedTextTooltipSupport.isTextClipped(102, 100));
}
}
+2 -1
View File
@@ -2390,4 +2390,5 @@ DK0MM;Jens/Alex;JN49IU;StringProperty [value: 432.305]; wkd true; wkd144 false;
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 144.135]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 432.135]; wkd true; wkd144 true; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
LA0BY;Stefan @ hilltop;JO59IX;StringProperty [value: 144.062]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
LA0BY;null;JO49ML;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
LA0BY;null;JO49ML;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
OV3T;null;JO46CM;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
+114 -114
View File
@@ -7,93 +7,93 @@
<date>2026-07-01</date>
<description>Map view and path calculations</description>
<added>
Band and Frequency Detection per Chatmember, Band alert, Win-Test SKED Push via UDP,
Win-Test Broadcast Defaults and SKed Workflow, Chat History at Startup, Station Map / Map View
Local Leaflet /-TileProxy Backend, Maidenhead /-Grid Overlay, Path calculation, path profile,
Reachability filters, NAC filters (grid sqaure marking)
Band and frequency detection per chat member, band alert, Win-Test sked push via UDP,
Win-Test broadcast defaults and sked workflow, chat history at startup, station map / map view,
local Leaflet / tile-proxy backend, Maidenhead / grid overlay, path calculation, path profile,
reachability filters, NAC filters (grid-square marking)
</added>
<changed>
button housing, it works now on smaller screens / plus: detecting maximum screen size
Button layout now works on smaller screens; the maximum screen size is also detected
</changed>
<fixed>
9A2HM reported that 23cm qsos with N1MM are not marked correctly, ist fixed
9A2HM reported that 23 cm QSOs with N1MM were not marked correctly; this is fixed
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.40</changedVersionNumber>
<date>2026-02-16</date>
<description>Much changes and fixes</description>
<description>Many changes and fixes</description>
<added>
- win-test support (not deactivable yet, beta)
- QSO sniffer which filters changable callsigns lists messages to your pm window
- Chatmember Score system. Each chatmember gets a score depending to antenna direction, activity time,
activity msgcount, positive signals, active bands, active frequencies, his sked direction (degrees),
self handed rate, sked-made-rate and some others
- Chatmember Band and frequency recognition: now we list all active bands of each other chatmember
- Win-Test support (cannot be disabled yet, beta)
- QSO sniffer which filters messages from configurable callsign lists into the private-message window
- Chat-member score system. Each chat member receives a score based on antenna direction, activity time,
activity message count, positive signals, active bands, active frequencies, sked direction (degrees),
self-handed rate, sked-made rate and other factors
- Chat-member band and frequency recognition: all active bands of each other chat member are now listed
- Band alert for logged stations
- Sked reminder ALERT with automatic messages in 2+1 / 5+2+1 / 10+5+2+1 minutes
- Sked reminder alert with automatic messages at 2+1 / 5+2+1 / 10+5+2+1 minutes
- PSTRotator interface
- AirPlane Timeline
- Skedfail button in furtherinfo panel
- Wintest logsynch interface can now be activated and deactivated in the preferences
- load chat history on startup
- Airplane timeline
- Sked-fail button in the further-information panel
- Win-Test log-synchronisation interface can now be enabled and disabled in Preferences
- Load chat history at startup
</added>
<changed>
- added save option: ServerDNS / Port
- added save option: activate/deactivate pstRotator interface
- added save option: activate/deactivate wintest interface
- added save option: callsign sniffer
- added save option: boolean GUI_darkModeActiveByDefault = false
- added AP notes at the internal DX cluster spots
- Generic Autoanswer fires maximum once in 45 seconds per rx callsign
- Added saved options for server DNS and port
- Added saved option to enable or disable the PSTRotator interface
- Added saved option to enable or disable the Win-Test interface
- Added saved option for the callsign sniffer
- Added saved option: boolean GUI_darkModeActiveByDefault = false
- Added AP notes to internal DX cluster spots
- Generic auto-answer fires at most once every 45 seconds per receiving callsign
</changed>
<fixed>
- Userlist sort automatically on new member signon
- Posonpill-messages now kills only exactly one Client instance (bugreport 9A2HM)
- Crash of wtkst by disconnection of kst4Contest
- wtKST crashes when kst4contest is running (many reporters)
- wintest: logged per band excludes pre worked band (bugreport SM6VTZ)
- PSTRotator SOCKET Close on DISCONNECT BUTTON works now
- AirScout Map had not been updated (bugreport SM0RJV)
- QTFDefault had been not saved correctly (bugreport SM0RJV)
- Show path in AS button worked only with fixed server Strings (bugreport 9A2HM)
- Version number had been displayed wrong
- suffixes like /p, -2, etc. problem now fixed everywhere
- Dark mode: passing for qrg fields fix
- User list is sorted automatically when a new member signs on
- Poison-pill messages now terminate exactly one client instance (bug report 9A2HM)
- wtKST crash when KST4Contest disconnects
- wtKST crash while KST4Contest is running (many reporters)
- Win-Test: logging per band excludes previously worked bands (bug report SM6VTZ)
- The PSTRotator socket now closes when the disconnect button is used
- The AirScout map was not updated (bug report SM0RJV)
- QTFDefault was not saved correctly (bug report SM0RJV)
- The Show Path in AirScout button worked only with fixed server strings (bug report 9A2HM)
- The version number was displayed incorrectly
- Problems with suffixes such as /P and -2 are now fixed throughout the application
- Dark mode: padding of QRG fields fixed
</fixed>
<removed>
- No more date display. Not needed. Saving space, only time is important
- Date display removed to save space; only the time is relevant
</removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.31</changedVersionNumber>
<date>2025-12-13</date>
<description>Much changes and a hotfix</description>
<description>Many changes and a hotfix</description>
<added>
- win-test support (not deactivable yet, beta)
- pstRotator support (not deactivable yet, beta)
- airScout fix (calculations depends to cpu load now)
- Win-Test support (cannot be disabled yet, beta)
- PSTRotator support (cannot be disabled yet, beta)
- AirScout fix (calculations now depend on CPU load)
- QSO sniffer
</added>
<changed>
- DNS from www.on4kst.info to www.on4kst.org (hotfix, now configurable in preferences)
</changed>
<fixed>
- Endless loop in error case lets freeze the client
- PSTRotator SOCKET Close on DISCONNECT BUTTON
- Dark mode: passing for qrg fields fix
- An endless loop in an error case froze the client
- PSTRotator socket closes when the disconnect button is used
- Dark mode: padding of QRG fields fixed
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.266</changedVersionNumber>
<date>2025-10-03</date>
<description>Airscout interface did not work with multi-signons</description>
<description>AirScout interface did not work with multiple sign-ons</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>
Fixed by using raw callsign (without -2 suffix etc.) when communicating with AirScout.
Fixed by using the raw callsign (without a -2 suffix, etc.) when communicating with AirScout.
Thank you very much for testing this, Kreso 9A2HM!
</fixed>
<removed></removed>
@@ -101,36 +101,36 @@ Thank you very much for testing this, Kreso 9A2HM!
<changeLog>
<changedVersionNumber>1.265</changedVersionNumber>
<date>2025-09-28</date>
<description>Direction buttons stay now colored, if activated</description>
<description>Direction buttons now stay coloured when activated</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>The buttons stay now colored, if activated</fixed>
<fixed>The buttons now stay coloured when activated</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.264</changedVersionNumber>
<date>2025-08-02</date>
<description>Changed the callsign recognition pattern for Simplelogfile</description>
<description>Changed the callsign-recognition pattern for Simplelogfile</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>
Callsigns like S53CC, S51A etc. are now correctly marked as worked in SimpleLogFile.txt.
(bugreport Boris, S53CC)
Callsigns such as S53CC and S51A are now correctly marked as worked in SimpleLogFile.txt.
(bug report by Boris, S53CC)
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.263</changedVersionNumber>
<date>2025-06-08</date>
<description>Airscout communication and Loginname</description>
<description>AirScout communication and login name</description>
<added>nothing</added>
<changed>
Only entries with QRB lower than max-QRB are sent to AirScout in 60s intervals (was 12s for all).
Only entries with a QRB below the maximum QRB are sent to AirScout at 60-second intervals (previously 12 seconds for all entries).
</changed>
<fixed>
- AS communication message count hugely reduced
- Name in chat is now saveable
- Fixed AS client name (was hard-wired to "KST")
- AirScout communication message count greatly reduced
- Name in chat can now be saved
- AirScout client name fixed (was hard-coded as "KST")
73 / DO5AMF
</fixed>
<removed></removed>
@@ -138,39 +138,39 @@ Only entries with QRB lower than max-QRB are sent to AirScout in 60s intervals (
<changeLog>
<changedVersionNumber>1.262</changedVersionNumber>
<date>2025-05-21</date>
<description>Freezes caused by getting messages before user login should be fixed now</description>
<description>Freezes caused by receiving messages before user login should now be fixed</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>
ON4KST delivers messages of stations not yet logged in. That caused a freeze at the message
processing engine which is now fixed.
ON4KST delivers messages from stations which are not yet logged in. This caused the message-processing
engine to freeze and is now fixed.
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.26</changedVersionNumber>
<date>2025-05</date>
<description>Login to multiple Channels via single signon / spend some colors</description>
<description>Login to multiple channels via single sign-on / added some colour</description>
<added>
1. UI: Dark mode. Switch in "Window -> use dark mode"
2. Usage of two Chatcategories at the same time
3. opposite station multi-callsign login-tagging
1. UI: Dark mode. Switch under "Window -> use dark mode"
2. Use of two chat categories at the same time
3. Multi-callsign login tagging for remote stations
73 / DO5AMF
</added>
<changed>
- Coloring mechanic of the software. Modify colors via css by yourself...
- Colour handling in the application. Colours can be modified through CSS.
</changed>
<fixed>
- Station tagging fixed completely
- Station tagging completely fixed
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.251</changedVersionNumber>
<date>2025-02</date>
<description>BUGFIX of 1.25, tnx Steve Clements!</description>
<description>Bug fix for 1.25, thanks to Steve Clements!</description>
<added>
- Steve spotted a problem in udp broadcast spot info reading, its now fixed!
- Steve found a problem when reading UDP broadcast spot information; it is now fixed.
73 / DO5AMF
</added>
<changed></changed>
@@ -182,13 +182,13 @@ processing engine which is now fixed.
<date>2025-02</date>
<description>Wishlist-time</description>
<added>
- New configuration Tab: Messagehandling (auto-answering, CQ qrg auto-answer)
- Coloured lines: new pm rows appear in red, fade rainbow-like to white over 30s (tnx Gianluca)
- New configuration tab: Message handling (automatic replies, CQ QRG automatic reply)
- Coloured lines: new private-message rows appear in red and fade through a rainbow to white over 30 seconds (thanks to Gianluca)
73 / DO5AMF
</added>
<changed></changed>
<fixed>
- Users with suffixes like "-2 and -70" are now correctly marked as worked
- Users with suffixes such as -2 and -70 are now correctly marked as worked
</fixed>
<removed></removed>
</changeLog>
@@ -197,8 +197,8 @@ processing engine which is now fixed.
<date>2024-11</date>
<description>Wishlist-time</description>
<added>
- Button to show qrz.com profile of a selected station
- Button to show qrzcq.com profile of a selected station
- Button to show the qrz.com profile of a selected station
- Button to show the qrzcq.com profile of a selected station
</added>
<changed></changed>
<fixed></fixed>
@@ -207,10 +207,10 @@ processing engine which is now fixed.
<changeLog>
<changedVersionNumber>1.23</changedVersionNumber>
<date>2024-10</date>
<description>DXCluster Server is now implemented</description>
<description>DX cluster server is now implemented</description>
<added>
- DXCluster Server (tnx OMAAO): KST4Contest now provides a DXCluster server to feed
your log client with station-reachable warnings.
- DX cluster server (thanks to OMAAO): KST4Contest now provides a DX cluster server which sends
reachable-station warnings to the logging client.
</added>
<changed></changed>
<fixed></fixed>
@@ -219,13 +219,13 @@ processing engine which is now fixed.
<changeLog>
<changedVersionNumber>1.22</changedVersionNumber>
<date>2024-05</date>
<description>Increase usability, fixed AS button</description>
<description>Improved usability and fixed AirScout button</description>
<added>- Variables (tnx OMAAO): MYLOCATORSHORT, MYQRGSHORT, QRZNAME</added>
<changed>- Sendtext-field focus on callsign click</changed>
<changed>- Message-input focus when clicking a callsign</changed>
<fixed>
- Worked-station-filter (tnx Gianluca): filter is now live
- Chatters list sorting by QRB (tnx Alessandro): fixed to numeric sort
- Airscout showpath button now maximizes AS and shows the path
- Worked-station filter (thanks to Gianluca): the filter is now live
- User-list sorting by QRB (thanks to Alessandro): changed to numeric sorting
- AirScout Show Path button now maximises AirScout and displays the path
</fixed>
<removed></removed>
</changeLog>
@@ -235,8 +235,8 @@ processing engine which is now fixed.
<description>Increase usability</description>
<added></added>
<changed>
- Window sizes and divider positions are now stored and restored on next startup
- Filters section is now a flowpane for lower resolutions
- Window sizes and divider positions are now stored and restored at the next startup
- The filters section now uses a FlowPane for lower resolutions
</changed>
<fixed></fixed>
<removed></removed>
@@ -248,7 +248,7 @@ processing engine which is now fixed.
<added>
- Selectable bands
- Unworkable tags for each callsign per band
- QTF-Arrow on the "show path in AS" button
- QTF arrow on the "Show Path in AirScout" button
</added>
<changed></changed>
<fixed></fixed>
@@ -259,15 +259,15 @@ processing engine which is now fixed.
<date>2024-03</date>
<description>Increased workflow</description>
<added>
- Reachable warning function
- Reachable-station warning function
- Variables FIRSTAP and SECONDAP for predefined texts
- Marking new connected stations (bold) and away-state (brackets)
- Activity-timer since last message of a chatter
- Activity timer since a user's last message
</added>
<changed>- QRB/QTF info in pm-window</changed>
<changed>- QRB/QTF information in the private-message window</changed>
<fixed>
- Behaviour of messagefilter-radiobutton corrected (tnx Gian)
- Error sending macros on selected stations in pm window (tnx Gian)
- Behaviour of the message-filter radio button corrected (thanks to Gian)
- Error when sending macros to selected stations in the private-message window fixed (thanks to Gian)
</fixed>
<removed>- boot sound</removed>
</changeLog>
@@ -276,13 +276,13 @@ processing engine which is now fixed.
<date>2024-02</date>
<description>First stable release</description>
<added>
- audio notification support
- update-available warning
- changelog displaying
- known-bugs displaying
- message filters for selected station
- stationlist-filters (show by qtf / textstring, hide by qrb, qtf, workedstate)
- 10 macros (textsnippets available by hitting strg+numberkey)
- Audio-notification support
- Update-available warning
- Changelog display
- Known-bugs display
- Message filters for the selected station
- Station-list filters (show by QTF / text string; hide by QRB, QTF or Worked state)
- 10 macros (text snippets available with Ctrl+number key)
</added>
<changed></changed>
<fixed></fixed>
@@ -291,27 +291,27 @@ processing engine which is now fixed.
<changeLog>
<changedVersionNumber>0.9</changedVersionNumber>
<date>2024-02</date>
<description>Now the chat is disconnectable without closing it</description>
<description>The chat can now be disconnected without closing the application</description>
<added></added>
<changed></changed>
<fixed>disconnect-button, Textsnippets in CQ Window, "/CQ" at click on own sendtexts,
setname "qrg" works now</fixed>
<fixed>Disconnect button, text snippets in the CQ window, "/CQ" when clicking own sent texts;
setname "qrg" now works</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.8</changedVersionNumber>
<date>2023-10</date>
<description>savesettings function and disconnect function added. MYCALL-Highlighting fixed</description>
<added>savesettings function, disconnect function</added>
<description>Save-settings and disconnect functions added. MYCALL highlighting fixed</description>
<added>Save-settings function, disconnect function</added>
<changed></changed>
<fixed>MYCALL-Highlighting</fixed>
<fixed>MYCALL highlighting</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.7</changedVersionNumber>
<date>2023-07</date>
<description>Internal database for worked stations, shortcut and textsnippet editor</description>
<added>database, shortcut-editor, textsnipped-editor</added>
<description>Internal database for worked stations, shortcut editor and text-snippet editor</description>
<added>Database, shortcut editor, text-snippet editor</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
@@ -319,8 +319,8 @@ setname "qrg" works now</fixed>
<changeLog>
<changedVersionNumber>0.6</changedVersionNumber>
<date>2023-07</date>
<description>Added periodically sent beacon function</description>
<added>beacon to public channel</added>
<description>Added a periodically sent beacon function</description>
<added>Beacon to the public channel</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
@@ -328,8 +328,8 @@ setname "qrg" works now</fixed>
<changeLog>
<changedVersionNumber>0.5</changedVersionNumber>
<date>2023-02</date>
<description>Airscout interface, UDP interfaces for worked-station data and TRX info, shortcuts</description>
<added>Airscout-interface, packetreceiver for ucxlog, n1mm, qartest</added>
<description>AirScout interface, UDP interfaces for Worked-station data and TRX information, shortcuts</description>
<added>AirScout interface, packet receivers for UCXLog, N1MM and QARTest</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
@@ -337,8 +337,8 @@ setname "qrg" works now</fixed>
<changeLog>
<changedVersionNumber>0.4</changedVersionNumber>
<date>2022-12</date>
<description>Worked info by logfile via universal pattern matching logfile interpreter</description>
<added>simple-logfile</added>
<description>Worked information from a log file through a universal pattern-matching log-file interpreter</description>
<added>Simplelogfile</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
@@ -346,7 +346,7 @@ setname "qrg" works now</fixed>
<changeLog>
<changedVersionNumber>0.3</changedVersionNumber>
<date>2022-10</date>
<description>Added a GUI, chat was text based only</description>
<description>Added a GUI; the chat had previously been text-based only</description>
<added>Graphical user interface</added>
<changed></changed>
<fixed></fixed>
@@ -355,8 +355,8 @@ setname "qrg" works now</fixed>
<changeLog>
<changedVersionNumber>0.2</changedVersionNumber>
<date>2022-10</date>
<description>Pattern matching system for frequency information, frequency collection per chatmember</description>
<added>frequency-extraction for every callsign</added>
<description>Pattern-matching system for frequency information, frequency collection per chat member</description>
<added>Frequency extraction for every callsign</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
@@ -364,7 +364,7 @@ setname "qrg" works now</fixed>
<changeLog>
<changedVersionNumber>0.15</changedVersionNumber>
<date>2022-09</date>
<description>Researched the Chatprotocol and developed the base communication with the chatserver</description>
<description>Researched the chat protocol and developed the basic communication with the chat server</description>
<added></added>
<changed></changed>
<fixed></fixed>
+2 -1
View File
@@ -15,6 +15,7 @@ tagsList:
- SHF
- contest
related:
- station-filters
- timeline
- priority-score
- sked-reminder
@@ -172,4 +173,4 @@ Conversely, a reported aircraft does not guarantee sufficient signal strength or
[Read how the AP information affects the timeline.](/features/timeline/)
[Open the AirScout project on GitHub.](https://github.com/dl2alf/AirScout)
[Open the AirScout project on GitHub.](https://github.com/dl2alf/AirScout)
+3 -1
View File
@@ -13,6 +13,8 @@ tagsList:
- sked request
- dual chat
related:
- private-message-handling
- trx-qrg-synchronisation
- dual-chat
- macros
- sked-reminder
@@ -94,4 +96,4 @@ In plain terms: automatic replies remove repetitive typing. They do not take ove
[Read the complete configuration and recognised QRG requests in the manual.](/manual/en/configuration/#messagehandling-settings-from-v125)
[Read how two chat categories and suffixed callsigns are kept separate.](/features/dual-chat/)
[Read how two chat categories and suffixed callsigns are kept separate.](/features/dual-chat/)
+93
View File
@@ -0,0 +1,93 @@
---
title: Band Recognition and Opportunities
icon: 📶
category: Contest Workflow
since: "1.40"
summary: Automatically recognise active bands and alert the operator immediately after logging when the same station can still be worked on another common band.
description: KST4Contest combines QRGs, name fields, active callsign variants, local bands, Worked information and NOT-QRV marks to identify open band opportunities.
tagsList:
- band recognition
- band opportunity
- BAND+
- Worked
- NOT QRV
- ON4KST
- contest workflow
related:
- station-filters
- qrg-detection
- station-map
- directional-opportunities
- priority-score
- dual-chat
- log-sync
---
## Why does the moment after a QSO matter?
Finding a station, turning the antenna, agreeing on a frequency and completing the QSO takes time. Once that contact is in the log, much of the difficult work has already been done.
The first QSO has already demonstrated a usable path, correct antenna direction and an available operator. `BAND+` therefore appears immediately after logging when another common, unworked band is known, so the station can be moved before that operating context is lost. It does not guarantee the next band, but it identifies the best moment to try it.
This sequence is the useful part: KST4Contest first recognises possible bands, compares them with the local station and the log, and then presents the remaining opportunity while both operators are still in contact.
## How are active bands recognised?
KST4Contest combines information which may be distributed across several chat entries:
- QRGs detected for the remote station;
- explicit band designators in its name fields; and
- active callsign variants belonging to the same normalised base callsign.
A recent QRG is stronger evidence than a general chat category. Active variants are evaluated together because a station may use separate suffixes or logins for different bands. The individual variants nevertheless remain separate message targets in their respective chat categories.
## When does a detected band become an opportunity?
Recognition alone is not enough. KST4Contest compares the detected bands with:
1. the bands enabled for the local station;
2. stored Worked information for each band; and
3. manually assigned NOT-QRV marks.
Only a locally enabled, common and unworked band remains an open opportunity. A manual NOT-QRV mark takes precedence over a detected QRG, name field or callsign variant.
![Worked and band status in the user list](/manual/assets/not_qrv_controls.png)
The comparison remains band-specific. Working a station on 144 MHz does not mark it as worked on 432 MHz, and a NOT-QRV mark for one band does not remove a different band opportunity.
## Where is the result used?
The same derived information supports several parts of the contest workflow:
- `a` distinguishes an offered, unworked band for a callsign which has not yet been worked;
- `B+` marks another offered, unworked band after the callsign has already been worked elsewhere; if the separate `a` display is disabled, it also represents this opportunity for a completely new callsign;
- **New bands** filters the user list for stations with at least one detected opportunity;
- the Priority Score uses known band compatibility and open band-upgrade cases;
- map markers include derived bands and open `B+` opportunities; and
- `BAND+` reports the remaining bands immediately after a suitable QSO is logged.
These are not separate guesses. They use the same band, Worked and NOT-QRV context for different operating decisions.
## The immediate `BAND+` hint
When UCXLog or Win-Test reports a new QSO with band information, KST4Contest checks the station again. If another common and unworked band remains, a blinking hint appears for approximately twelve seconds, for example:
```text
BAND+ DL0ABC 432, 1296
```
The tooltip shows the enabled, worked and NOT-QRV bands behind the decision. If general notification sounds are enabled, a short sound accompanies the hint.
The file-based Simplelogfile interpreter cannot trigger this hint reliably because it supplies the callsign but not the band of the newly logged QSO.
## Recognition is evidence, not a promise
A detected QRG, name-field entry or active callsign variant indicates possible activity. It is not proof that the station is currently ready on that band, that propagation will hold or that the next QSO will succeed.
The reverse is equally important: unknown does not mean impossible. If KST4Contest has no band information for a station, it cannot present a known opportunity, but the missing information does not prove that no common band exists.
In practical terms: band recognition reduces the time between noticing an opportunity and asking for the next band. The decision to try it still belongs to the operator.
[Read how Worked, band and NOT-QRV information is derived in the manual.](/manual/en/features/#worked-callsigns-new-bands-and-new-grid-squares)
[Open the `BAND+` and Priority Boost settings.](/manual/en/configuration/#band-upgrade-hint-after-a-log-entry)
@@ -0,0 +1,76 @@
---
title: Directional Opportunities
icon: 📐
category: Contest Workflow
since: "1.1"
summary: Recognise when a station in a directed ON4KST exchange may be pointing towards the local station and highlight the short-lived opportunity.
description: KST4Contest derives a possible antenna direction from directed ON4KST messages and marks matching senders without requiring the local DX Cluster server.
tagsList:
- ON4KST
- directional opportunity
- antenna direction
- beamwidth
- QRB
- contest workflow
related:
- global-message-views
- station-map
- band-recognition
- dx-cluster
- rotator-control
---
## Why watch messages between other stations?
A directed ON4KST message shows who is talking to whom. It does not include an antenna direction. During a contest, however, a station requesting, answering or preparing a sked will often point at least approximately towards the station being addressed.
That creates a brief opportunity for stations near the same direction. KST4Contest has recognised this situation since version 1.1. The detection and user-list highlight work independently of the local DX Cluster server.
## How is the opportunity derived?
Assume that station A sends a directed message to station B. KST4Contest:
1. calculates the direction from A to B and treats it as the likely antenna direction of A;
2. calculates the direction from A to the local station;
3. compares the angular difference with half of the configured antenna beamwidth; and
4. checks that A is inside the configured maximum QRB.
If the local station lies inside this assumed antenna corridor, A becomes a directional opportunity. A reply from B is a new case: the likely direction is then calculated from B back towards A.
The configured beamwidth is the complete angle. With `70°`, KST4Contest therefore assumes a corridor of `35°` on either side of the direction between the two stations.
## What does the operator see?
The sender's callsign appears green and bold in the user list for five minutes. A later matching message restarts this period. If the station sends another directed message which no longer matches the geometry, the mark is removed immediately.
![Detected directional opportunity in the user list](/manual/assets/direction_opportunity_highlight.png)
When simple sound notifications are enabled, the first detection also produces a short acoustic indication. Further matching messages do not repeat the sound while the station is already marked.
## What practical value does it have?
The indication draws attention to a station at the moment when its antenna may already cover the local direction. It can be enough to notice a callsign which would otherwise remain one line among many in the chat.
DM5M reports that around 3540% of the directional opportunities used at that station have resulted in a QSO. This is station-specific operating experience, not a general success rate. Location, antenna patterns, propagation, contest activity and operating practice can produce very different results elsewhere.
## Optional use in the logger bandmap
Since version 1.23, KST4Contest can forward a detected opportunity and a known QRG through its local DX Cluster server. This is an optional second step. The direction calculation, green user-list mark and sound do not depend on a DX-Cluster connection.
The local DX Cluster output reuses the result; it does not create the directional opportunity. If the server is disabled or no suitable QRG is known, the visual indication still works normally.
## What does the indication not prove?
ON4KST does not transmit the actual antenna direction or beamwidth of the remote station. KST4Contest therefore uses the locally configured antenna beamwidth as a practical approximation.
The calculation also does not know the real antenna pattern, rotator position, terrain or current propagation. A matching corridor is not a propagation forecast and does not guarantee a QSO.
In practical terms: the green callsign identifies a geometrically plausible moment to pay attention. Whether calling makes sense remains an operator decision.
[Read the complete derivation and numerical examples in the manual.](/manual/en/features/#directional-opportunities-from-directed-messages)
[Open the antenna beamwidth setting.](/manual/en/configuration/#antenna-beamwidth)
[Open the maximum QRB setting.](/manual/en/configuration/#default-maximum-qrb)
[Read how the optional local DX Cluster output works.](/features/dx-cluster/)
+32 -25
View File
@@ -4,7 +4,7 @@ icon: 💬
category: ON4KST Chat
since: "1.26"
summary: Monitor two ON4KST categories in one interface while preserving the complete callsign and category required for correct message routing.
description: KST4Contest combines two ON4KST chat sessions, keeps individual logins separate and shares station-related Worked, band and priority information through the normalised base callsign.
description: KST4Contest uses one authenticated ON4KST TCP session for two chat categories, keeps their message contexts separate and shares station-related Worked, band and priority information through the normalised base callsign.
tagsList:
- ON4KST
- dual chat
@@ -15,6 +15,9 @@ tagsList:
- SHF
- microwave contest
related:
- private-message-handling
- trx-qrg-synchronisation
- band-recognition
- priority-score
- log-sync
- airscout
@@ -26,43 +29,47 @@ VHF, UHF and microwave activity is not always concentrated in one ON4KST categor
Opening two unrelated chat clients would show both message streams, but every comparison between them would remain manual. Worked status, band information, active stations and sked context would still be distributed across separate windows.
KST4Contest therefore opens up to two ON4KST chat sessions and processes both in one operating context.
KST4Contest therefore signs in once and adds a second chat category through ON4KST Single Sign-on when required. Both categories use the same TCP session and are processed in one operating context.
![Primary and secondary chat settings](/manual/assets/client_settings_window_station.png)
## Two connections remain two connections
## One connection, two category contexts
The primary and secondary chat sessions retain their own:
KST4Contest authenticates the ON4KST TCP session with one local login callsign and one password. The session uses one common locator for both categories; the locator is not an authentication credential. The primary category is part of the initial login. If a second category is enabled, the client adds it to the same session through Single Sign-on. It does not open another TCP connection or perform a second local login.
The primary and secondary categories still retain their own:
- ON4KST chat category;
- login callsign;
- visible category-specific **Name in Chat** field;
- public message stream;
- destination category for outgoing messages; and
- active login entries.
- message context and destination category for outgoing messages;
- local QRG;
- beacon enable setting and message template; and
- active chat-member entries.
The second session can use the same local callsign as the primary session or a separately configured callsign. A different login may be useful when the station uses category- or band-specific suffixes.
The **Name in Chat 2** field configures only the visible name field for the second category. It is neither a second login callsign nor a message destination. Both categories continue to use the one local login callsign and the common locator of the TCP session.
Combining the display does not turn both sessions into one ON4KST connection. Messages must still be sent through the category in which the intended destination is active.
One connection therefore does not mean one combined message stream. Messages must still be sent in the category in which the intended destination is active.
## What identifies an active chat member?
An active login is identified by:
For remote chat participants, an active member is identified by:
1. the complete visible callsign, including its suffix; and
1. the complete visible callsign variant, including its suffix; and
2. the ON4KST chat category.
Both parts are required.
Consider the following active entries:
| Complete callsign | Chat category | Active entity |
| Complete visible callsign variant | Chat category | Active entity |
|---|---:|---|
| `9A0BB-2` | 1 | separate |
| `9A0BB-70` | 1 | separate |
| `9A0BB-23` | 2 | separate |
| `9A0BB-13` | 2 | separate |
All four entries belong to the same base callsign, but they are four different chat logins. Joining, updating or leaving the chat affects only the corresponding complete callsign in the corresponding category.
All four entries belong to the same base callsign, but they are four different chat-member identities. Joining, updating or leaving the chat affects only the corresponding complete callsign in the corresponding category.
This is particularly important for `9A0BB-2` and `9A0BB-70`: because both use the same category, the category alone cannot distinguish them.
@@ -70,7 +77,7 @@ This is particularly important for `9A0BB-2` and `9A0BB-70`: because both use th
## How are messages routed?
When a row is selected, KST4Contest retains both the complete callsign and its category. A private message is addressed to that complete callsign and sent through the corresponding chat session.
When a row is selected, KST4Contest retains both the complete callsign and its category. A private message is addressed to that complete callsign and sent in the corresponding category within the shared session. Incoming private messages for the local station are addressed to the local login callsign, not to a category-specific **Name in Chat** field.
Selecting `9A0BB-70` in category 1 therefore creates a message for `9A0BB-70` in category 1. It is not silently reduced to `9A0BB`, and it is not sent through category 2 merely because another variant is active there.
@@ -83,11 +90,11 @@ The same distinction is used for:
- sked reminder messages; and
- leaving or updating an active chat entry.
The different variants do not have to be able to send messages to each other. That is not the purpose of the function. They must remain correctly reachable by other stations and receive the messages addressed to their respective login.
The different variants do not have to be able to send messages to each other. That is not the purpose of the function. They must remain correctly reachable by other stations and receive the messages addressed to their respective chat-member identity.
## What is shared through the base callsign?
Some information describes the radio station rather than one temporary chat login. This information is aggregated through the normalised base callsign.
Some information describes a remote radio station rather than one temporary chat-member identity. This information is aggregated through the normalised base callsign.
For the examples above, the common base callsign is `9A0BB`.
@@ -106,7 +113,7 @@ Per-band information is still retained. Working the station on 70 MHz does not m
## Why are band hints combined?
A station may use its visible login names to describe the available bands:
A remote station may use complete visible callsign variants to describe the available bands:
- `9A0BB-2`
- `9A0BB-70`
@@ -121,13 +128,13 @@ A manual NOT-QRV mark still takes precedence. An automatically detected suffix o
## Why is the Priority Score not duplicated?
The Priority Score represents the operating priority of the station, not the number of chat logins it happens to use.
The Priority Score represents the operating priority of the remote station, not the number of chat-member identities it happens to use.
KST4Contest therefore calculates the score for the normalised base callsign and projects it to the active variants. Several suffixes do not create several independent priority candidates merely by being logged in more than once.
The individual rows remain selectable because the correct message destination still matters.
In plain terms: one station should not occupy half the priority list, but the operator must still be able to contact the correct login.
In plain terms: one station should not occupy half the priority list, but the operator must still be able to contact the correct chat-member identity.
## Categories are context, not proof of a band
@@ -147,7 +154,7 @@ Categories unrelated to the supported VHF, UHF, microwave or EME workflows are i
Sharing the station context does not merge everything associated with the base callsign.
The following remain tied to the individual login or message:
The following remain tied to the individual remote chat member or message:
- complete message destination;
- chat category;
@@ -155,18 +162,18 @@ The following remain tied to the individual login or message:
- visible callsign suffix;
- join and leave state;
- the specific row selected by the operator; and
- message history associated with that login and category.
- message history associated with that chat member and category.
This distinction is the basis of the dual-chat implementation: operational information may be shared where it describes the same radio station, while communication remains attached to the actual ON4KST login.
This distinction is the basis of the dual-chat implementation: operational information may be shared where it describes the same remote radio station, while communication remains attached to the complete ON4KST callsign and category.
## Practical limitations
The shared base-callsign model assumes that suffix variants separated by `-` belong to the same underlying amateur-radio station. That is appropriate for common logins such as `-2`, `-70`, `-144` or `-432`.
The shared base-callsign model assumes that complete visible callsign variants separated by `-` belong to the same underlying amateur-radio station. That is appropriate for variants such as `-2`, `-70`, `-144` or `-432`.
KST4Contest cannot determine whether two operators behind those logins are using one station, several independently operated stations or a distributed contest setup. Worked and band information is therefore shared at callsign level, while the operator remains responsible for selecting the correct chat destination.
KST4Contest cannot determine whether two operators behind those chat entries are using one station, several independently operated stations or a distributed contest setup. Worked and band information is therefore shared at callsign level, while the operator remains responsible for selecting the correct chat destination.
[Read how both chat categories are configured.](/manual/en/configuration/#login-and-chat-categories)
[Read how Worked and band information is derived.](/manual/en/features/#worked-callsigns-new-bands-and-new-grid-squares)
[Read how suffixed callsigns are handled in skeds.](/features/sked-reminder/)
[Read how suffixed callsigns are handled in skeds.](/features/sked-reminder/)
+28 -13
View File
@@ -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
@@ -12,8 +12,13 @@ tagsList:
- QRG detection
- UCXLog
- N1MM+
- Minos
- contest logger
related:
- global-message-views
- qrg-detection
- station-map
- directional-opportunities
- log-sync
- priority-score
- dual-chat
@@ -29,9 +34,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 +44,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 +131,25 @@ 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.
The payload is a fixed, DXSpider-compatible 75-character line. The DX callsign begins in column 27, the 30-character comment begins in column 40 and the UTC time begins in column 71. Short comments are padded; longer comments are limited to the available field. A DX callsign longer than twelve characters is rejected rather than silently truncated.
An example comment with AirScout information may look like this:
```text
JN49GL , AP: 1min, 100%; 4min, 75%
JO51HK AP 1m/100%;4m/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.
@@ -191,6 +201,8 @@ A missing password is acceptable inside the intended station network. It is not
Use **Send test spot** after the logger has connected.
The test uses `DO5AMF` with the comment `DXC test: You donated $100!` and `.300` on the configured fallback band.
A successful test confirms that at least one client received the generated spot. If the test works but real spots do not appear, the TCP connection is probably not the problem. In that case, check the conditions used for the actual directional opportunity:
- Were valid locators available?
@@ -199,6 +211,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.
@@ -221,11 +235,12 @@ The local DX Cluster interface has been used with:
- UCXLog
- N1MM+
- Minos
Other loggers may work if they can open a normal TCP connection to a DX Cluster server and accept conventional `DX de ...` spot lines.
[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)
@@ -0,0 +1,90 @@
---
title: Global Message Views
icon: 📋
category: ON4KST Chat
since: "1.42"
summary: Follow public chat, ON4KST DX cluster traffic and directed messages between other stations without tying the overview to the currently selected station.
description: KST4Contest provides three global message views for current chat activity and coordination, backed by shared, bounded in-memory message stores.
tagsList:
- ON4KST
- public messages
- DXCluster messages
- QSO of the other
- chat activity
- contest coordination
related:
- private-message-handling
- qso-monitoring
- directional-opportunities
- dx-cluster
- qrg-detection
---
## Why use a global message view?
Selecting a station is useful when the next action concerns that station. It is less useful when the operator needs to keep an eye on the wider chat while moving through the user list.
KST4Contest therefore provides three message tabs whose contents do not depend on the currently selected station. They keep public activity, cluster traffic and visible coordination between other stations available as a compact contest overview.
Global Message Views are included from version 1.42 onwards.
![Global message tabs below the main user list](/manual/assets/global_message_tabs.png)
## Three views, three purposes
| View | What it shows |
|---|---|
| **Public messages** | Public chat messages, including CQ calls and beacons |
| **DXCluster messages** | DX cluster messages received from the ON4KST server |
| **QSO of the other** | Directed chat messages between two chat logins other than the local station |
**Public messages** is selected by default. Changing the selected user-list row does not filter or replace the contents of any of these tabs.
## Public messages
The public view is the continuous chat channel: CQ calls, beacons and other messages addressed to `ALL`. It provides the quickest indication of general activity without requiring a particular station to be selected first.
This makes it useful as a running contest work surface. The operator can follow new calls and general announcements while the selection remains on the station currently being handled.
## DXCluster messages
The cluster view displays DX cluster traffic delivered through the ON4KST connection. Depending on the received data, it can show the reporting and reported stations, their locators, QRG, message text and the global Worked state of the reported station.
This is an incoming message view. It is not the [Local DX Cluster Server](/features/dx-cluster/), which sends derived KST4Contest spots to connected logging software.
## QSO of the other
This view contains directed chat messages for which neither sender nor receiver is the local station. Messages addressed to `ALL` remain in **Public messages** instead.
Sender and receiver are shown separately, together with their most recently known QRGs, global Worked states, message text and chat category. That can reveal current sked arrangements, frequency exchanges and other coordination which would otherwise be distributed across the station-related views.
The label is deliberately compact. A directed chat message does not prove that an actual radio QSO has taken place. It may be a sked request, a frequency question or simply a private conversation between two chat logins.
Two other limits matter:
- **Last QRG TX** and **Last QRG RX** show the latest values currently known for the stations. They are not a historical record of the frequencies used when the displayed message was sent.
- **wkd TX?** and **wkd RX?** show global Worked state. They do not say that the station was worked on the QRG or band displayed next to it.
## The separate monitor uses the same data
The existing **Cluster & QSO of the other** window remains available. It places received DX cluster messages above the directed messages between other stations.
![Separate monitor window for DX cluster traffic and directed messages between other stations](/manual/assets/cluster_qso_monitor.png)
The main-window tabs and monitor window use the same underlying message stores. Opening the additional window does not create another ON4KST connection, receive a second copy of each message or maintain a separate history.
## Shared, bounded and session-only
**Public messages**, the private-message table, the messages in **Further Info** and **QSO of the other** are different views of the same global chat-message store. The cluster tab and the cluster table in the separate monitor likewise share one DX-cluster-message store.
Both stores are bounded. The chat-message store is reduced from more than 30,000 entries to 25,000; the cluster-message store is reduced from more than 10,000 entries to 8,000. The oldest entries disappear from every view which uses the respective store.
The stores exist in memory only. After restarting KST4Contest, the views start empty and are rebuilt from newly received messages. They are tools for the current operating session, not a permanent chat archive.
In practical terms: Global Message Views keep the wider contest conversation visible while the operator continues working station by station. They show observed chat activity and coordination, not a logbook of what happened on the radio.
[Read the detailed derivation and limitations in the manual.](/manual/en/features/#global-message-views)
[Open the user-interface description of the tabs and monitor window.](/manual/en/user-interface/#global-message-tabs-and-monitor-window)
[Read how the bounded message stores are shared.](/manual/en/features/#bounded-message-stores-from-v141)
+11 -4
View File
@@ -13,6 +13,9 @@ tagsList:
- DXLog.net
- contest logger
related:
- trx-qrg-synchronisation
- station-filters
- band-recognition
- priority-score
- dual-chat
- sked-reminder
@@ -34,7 +37,9 @@ The available information depends on the interface:
| General QSO UDP listener | yes | when included | when included |
| Win-Test network listener | yes | yes | when included |
The Simplelogfile interpreter is broadly compatible but can only identify worked callsigns. A callsign match in a file does not provide enough information to infer a reliable band or grid square.
The Simplelogfile interpreter reads the selected text file once per minute using a fixed callsign pattern. Matches are normalised to the base callsign and set the global Worked status for all active variants. A callsign match does not provide enough information to infer a reliable band or grid square.
If the file does not exist, KST4Contest creates it and first asks you to check whether Simplelogfile is needed or whether the logging application provides a supported network interface. The notice then displays the path together with the initial setup and contest checks. The file itself is the durable source. Its Worked marks are not stored in SQLite, are not removed automatically during the running session and are not reset automatically for a new contest.
The general UDP listener processes packets from UCXLog, N1MM+, QARTest and DXLog.net. Where the packet contains band and locator data, KST4Contest also updates the per-band Worked state and worked grid square.
@@ -44,7 +49,7 @@ The general UDP listener processes packets from UCXLog, N1MM+, QARTest and DXLog
Win-Test uses a separate listener for its native network protocol. KST4Contest resolves the Win-Test band ID, including 50 and 70 MHz, and stores the resulting Worked information in the same internal database.
STATUS packets can also update the local QRG. In multi-operator networks, a station-name filter prevents STATUS packets from another operating position from replacing the frequency of the intended radio.
STATUS packets can also update the local QRG when QRG synchronisation is enabled and valid packets actually arrive. Enabling the source alone does not supply a frequency. In multi-operator networks, a station-name filter prevents STATUS packets from another operating position from replacing the frequency of the intended radio.
Win-Test can additionally receive skeds created in KST4Contest. The handover only takes place when a QRG matching the selected band can be determined. No fixed fallback frequency is inserted merely to make the packet technically valid.
@@ -52,8 +57,10 @@ Win-Test can additionally receive skeds created in KST4Contest. The handover onl
## Stored state and limitations
Worked, NOT-QRV and worked-grid information is stored in the internal SQLite database and restored after a restart. Contest-related records expire automatically after three days.
Worked, NOT-QRV and worked-grid information received through the network interfaces, together with manual marks, is stored in the internal SQLite database and restored after a restart. Contest-related records expire automatically after three days.
Simplelogfile marks follow a different data flow: they remain runtime state derived from the selected file. The file is read again after a restart and has no automatic contest reset. A database reset does not change or empty it. Callsigns contained in the file are marked as worked again during the next evaluation within one minute.
KST4Contest can only use the fields supplied by the selected interface. Missing band or locator data is not reconstructed from guesswork. This makes the result less complete in some cases, but also avoids turning an incomplete log packet into incorrect Worked information.
[Read the complete log synchronisation setup in the manual.](/manual/en/log-sync/)
[Read the complete log synchronisation setup in the manual.](/manual/en/log-sync/)
+3 -2
View File
@@ -13,6 +13,7 @@ tagsList:
- ON4KST
- operator workflow
related:
- trx-qrg-synchronisation
- airscout
- dual-chat
- sked-reminder
@@ -169,7 +170,7 @@ A public beacon has no selected remote station. It can therefore use only global
![Beacon settings for both chat categories](/manual/assets/client_settings_window_beacon.png)
Each chat category has its own enable setting and message template. Both categories use the same timer interval. The second beacon is sent only when the second chat login and its beacon are enabled.
Each chat category has its own enable setting and message template. Both categories use the same timer interval. The second beacon is sent only when the second category and its beacon are enabled in the shared ON4KST session.
Global variables are evaluated again on every timer run. A QRG changed by the logger can therefore be included in the next beacon without editing its template.
@@ -227,4 +228,4 @@ In plain terms: the variables remove repeated typing. The operator still checks
[Open the shortcut and snippet configuration.](/manual/en/configuration/#shortcut-settings)
[Read how the AirScout values are obtained.](/features/airscout/)
[Read how the AirScout values are obtained.](/features/airscout/)
+4 -1
View File
@@ -15,6 +15,9 @@ tagsList:
- SHF
- contest workflow
related:
- station-filters
- qrg-detection
- band-recognition
- timeline
- airscout
- sked-reminder
@@ -345,4 +348,4 @@ In practical terms: the score keeps the available information from having to be
[Read how active suffix variants remain separate message targets.](/features/dual-chat/)
[Read how Priority Candidates are used on the timeline.](/features/timeline/)
[Read how Priority Candidates are used on the timeline.](/features/timeline/)
@@ -0,0 +1,94 @@
---
title: Private Message Handling
icon: ✉️
category: ON4KST Chat
since: "1.1"
summary: Catch mentions of the configured login callsign from public or directed chat traffic and use six green age levels without changing the original message routing.
description: KST4Contest combines PM Catching, age-based row highlighting and reply preparation for direct messages and callsign mentions across the visible chat traffic.
tagsList:
- ON4KST
- private messages
- PM Catching
- message age
- callsign mention
- contest workflow
related:
- automatic-replies
- qso-monitoring
- global-message-views
- dual-chat
---
## Why catch more than direct PMs?
Not every message which concerns the local station is addressed to it directly. An operator may accidentally post something like this publicly:
```text
(DM5M) pse ur qrg
```
The callsign may also appear in a directed message between two other chat participants. KST4Contest therefore also shows every message not sent by the local station in the PM table when its text contains the configured local login callsign. This covers public messages addressed to `ALL` and directed traffic between other stations.
PM Catching has existed since version 1.1. For directed messages between other stations, a slightly flippant description is **“gossip detection”**. It is a nickname, not the formal name of the function.
## How is a mention recognised?
The check searches the message text for the complete configured login callsign without distinguishing upper- and lower-case letters. A login of `DM5M` therefore matches both:
```text
(DM5M) pse ur qrg
dm5m are you qrv?
```
This is a text search, not a linguistic interpretation. A spelling error or shortened callsign does not match. Conversely, a sentence which merely mentions the callsign can appear in the PM table even when no reply was expected.
In plain terms: PM Catching finds visible callsign text. It cannot know what the author meant.
## The original message remains unchanged
Catching adds the message to the PM view. It does not turn it into a private message to the local station. The actual receiver remains `ALL` or the other chat participant; message text, chat category and routing remain unchanged as well.
The same principle applies to [QSO Monitoring](/features/qso-monitoring/): a monitored message may be shown additionally in the PM table, but its original routing remains intact. The two mechanisms use different criteria:
- PM Catching looks for the local login callsign in the message text.
- QSO Monitoring checks whether a configured station is actually the sender or receiver.
## Selecting a row prepares the reply
Selecting an incoming PM-table row prepares a `/cq` reply to its sender. If the selected row contains an outgoing message from the local station, KST4Contest instead restores the original receiver as the reply target.
The selection prepares the target and input context. It does not send a message automatically.
## Six green age levels
Age-based row highlighting is included from version 1.25 onwards. New non-local rows in the PM table pass through six green stages:
| 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. A colour boundary may therefore become visible during the next refresh rather than at the exact second.
After five minutes, the row returns to the normal table colour. Messages sent by the local station retain their separate highlight and do not use the green age scale.
## What produces a PM notification sound?
PM audio is reserved for messages actually directed to the local login. A message shown through PM Catching does not trigger the simple PM sound, CW callsign output or phonetic callsign output.
Messages added through QSO Monitoring likewise remain silent. Their appearance in the PM table is a visual aid, not evidence that the local station received a new private message.
In practical terms: the PM table brings the relevant lines together, the age colours show what is fresh, and selecting a row prepares the likely reply target. The operator still decides whether the message was really intended for the station and whether it needs an answer.
[Read the PM Catching recognition and its limits in the manual.](/manual/en/features/#pm-catching-from-v11)
[Read the exact six age levels.](/manual/en/features/#coloured-pm-rows-from-v125)
[Open the PM-window controls and selection behaviour.](/manual/en/user-interface/#pm-window-top-left)
[Read which incoming messages produce audio notifications.](/manual/en/configuration/#notification-settings)
+108
View File
@@ -0,0 +1,108 @@
---
title: Automatic QRG Detection
icon: 📻
category: ON4KST Chat
since: "1.1"
summary: Recognise complete and relative QRG information in chat messages and retain the sender's recent band context before using a global fallback.
description: KST4Contest extracts usable frequencies from public and directed ON4KST messages and makes the result available to band, priority, sked, path and logger workflows.
tagsList:
- QRG detection
- frequency
- relative QRG
- band context
- ON4KST
- contest workflow
related:
- global-message-views
- trx-qrg-synchronisation
- band-recognition
- station-map
- priority-score
- sked-reminder
- dx-cluster
---
## Why detect a frequency from chat text?
Frequencies in the ON4KST chat are rarely written in one consistent form. One station may send `432.088`, then shorten the next message to `.100`. Another may write `qrg 210` while a bare `210` in a different message means something else entirely.
KST4Contest evaluates both public and directed chat messages so that useful QRG information does not have to be copied manually into every later operating step.
Automatic QRG Detection has existed since version 1.1. The more precise sender-specific context evaluation described below is included from version 1.42 onwards.
## Which forms are recognised?
Complete frequencies provide their band directly. Examples include:
```text
144.210
432,088
144307
10368.100
10368100
```
The separator is optional for a complete frequency. For a digit-only value, KST4Contest treats the final three digits as the kHz part. This turns `144307` in a station name into `144.307 MHz` and `10368100` in a public or directed chat message into `10368.100 MHz`. The result still has to fall within one of the supported band ranges.
Relative forms omit the band and need additional context:
```text
.210
,088
qrg 210
freq is 210
on 210
210 MHz
```
A dot or comma marks a relative frequency explicitly. A three-digit value is accepted only when nearby text identifies it as a frequency.
## Which band is used for a relative QRG?
KST4Contest resolves the missing band in this order:
1. It first looks for a suitable band context 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 configured **Fallback band for relative QRG detection**.
For example, assume that the global fallback is 144 MHz. A station first sends `432.088` and then `.100` a few minutes later. The recent context belongs to the same sender, so the result is `432.100 MHz`, not `144.100 MHz`.
Another station without suitable recent context would use the global fallback for the same `.100` message.
## Why are bare three-digit numbers ignored?
Values such as:
```text
210
599
144
```
are deliberately not treated as QRGs without recognisable frequency context. Otherwise, a signal report of `599`, a band name or a QSO count could become a formally valid but operationally useless frequency.
Rejecting an ambiguous number is less convenient than guessing correctly. It is considerably more convenient than tuning to a frequency which existed only in the parser's imagination.
## Where is the detected QRG used?
The most recently detected frequency appears in the **QRG** column. The same information can also contribute to:
- [Band Recognition](/features/band-recognition/) and open band opportunities;
- the [Priority Score](/features/priority-score/);
- frequency selection for [skeds](/features/sked-reminder/);
- the selected frequency for [path analysis](/features/station-map/); and
- automatic or manually triggered [DX Cluster spots](/features/dx-cluster/).
If a QRG appears in the same message which creates a directional opportunity, it is processed before the optional spot check. The new frequency can therefore already be used for that spot.
## Text evidence is not current operating proof
The result is derived from message text. KST4Contest cannot prove that the sender is still operating on the detected frequency, that the value was not superseded outside the chat or that an apparently clear statement belongs to the intended operating context.
A detected QRG is useful working information. It is not a live measurement and not a guarantee that calling on that frequency will reach the station.
In practical terms: the function preserves frequency context which would otherwise be easy to lose. The operator still checks whether that context remains plausible before using it.
[Read the complete QRG Detection description in the manual.](/manual/en/features/#qrg-detection)
[Open the fallback-band configuration.](/manual/en/configuration/#fallback-band-for-relative-qrg-detection)
+6 -4
View File
@@ -14,6 +14,8 @@ tagsList:
- contest team
- dual chat
related:
- private-message-handling
- global-message-views
- dual-chat
- sked-reminder
- automatic-replies
@@ -70,13 +72,13 @@ The base callsign is used to decide whether a message should be shown in the mon
A message for `DN9APW-70` therefore remains a message for `DN9APW-70`. It is not redirected to `DN9APW`, `DN9APW-2` or another suffix.
This is the same distinction used elsewhere in KST4Contest: station-related information may be shared through the base callsign, while communication remains attached to the actual ON4KST login.
This is the same distinction used elsewhere in KST4Contest: station-related information may be shared through the base callsign, while communication remains attached to the remote participant's complete callsign and category.
## Both chat categories are included
When two ON4KST categories are connected, the same monitoring list applies to both. There is no need to configure the station separately for each category.
When two ON4KST categories are active in the same session, the same monitoring list applies to both. There is no need to configure the station separately for each category.
The category of the individual message remains part of its context. Combining the monitoring view does not merge the underlying chat connections.
The category of the individual message remains part of its context. Combining the monitoring view does not merge the category-specific message streams or create separate chat connections.
## What the function does not do
@@ -94,4 +96,4 @@ The list can be edited directly and is stored with **Save Settings**.
[Read the complete QSO monitoring configuration in the manual.](/manual/en/configuration/#sniffer-settings-from-v131)
[Read why complete callsigns and chat categories remain separate.](/features/dual-chat/)
[Read why complete callsigns and chat categories remain separate.](/features/dual-chat/)
+3 -1
View File
@@ -13,6 +13,8 @@ tagsList:
- UDP
- SPID
related:
- station-map
- directional-opportunities
- priority-score
- timeline
- airscout
@@ -74,4 +76,4 @@ In plain terms: KST4Contest removes the repeated transfer of a direction from on
[Read the complete setup and port assignment in the manual.](/manual/en/configuration/#pstrotator-settings-from-v131-fully-configurable-from-v140)
[Open the PSTRotator website.](https://www.pstrotator.com/)
[Open the PSTRotator website.](https://www.pstrotator.com/)
+3 -1
View File
@@ -11,6 +11,8 @@ tagsList:
- contest reminder
- Win-Test
related:
- trx-qrg-synchronisation
- qrg-detection
- priority-score
- airscout
- timeline
@@ -69,4 +71,4 @@ The proposed band is derived from recent chat and station-name information. This
In plain terms: the function makes a scheduled contact considerably harder to overlook. It cannot prevent every missed sked.
[Read the complete sked handling and its limitations in the manual.](/manual/en/features/#skeds-and-sked-reminders)
[Read the complete sked handling and its limitations in the manual.](/manual/en/features/#skeds-and-sked-reminders)
+103
View File
@@ -0,0 +1,103 @@
---
title: Combinable Station Filters
icon: 🔎
category: Contest Awareness
since: "1.0"
summary: Combine spatial, operating and propagation filters to focus the user list and station map without removing hidden stations from the active chat context.
description: KST4Contest applies several station filters together while keeping the canonical station model, message processing and Priority List independent of the filtered table view.
tagsList:
- station filters
- QTF
- QRB
- New bands
- AirScout
- Tropo
- Worked
- contest workflow
related:
- station-map
- band-recognition
- priority-score
- airscout
- log-sync
---
## Why combine station filters?
A busy ON4KST category can contain far more stations than are relevant to the current antenna direction, band or operating moment. One filter rarely describes the complete situation.
KST4Contest therefore allows spatial, operating and propagation-related conditions to be active at the same time. The purpose is not to discard stations. It is to reduce the visible worklist to the contacts which currently deserve attention.
![Station filters wrapped into two rows](/manual/assets/filter_bar_wrapped.png)
## Three kinds of filter
### Spatial filters
- **Show only QTF** keeps stations inside the selected antenna direction and configured beamwidth.
- **Show only QRB [km] <=** limits the list to the entered distance.
### Operating filters
- **Find** matches a complete or partial callsign.
- **wkd** hides base callsigns already worked on at least one supported band.
- The individual band buttons hide stations already worked or marked NOT QRV on that band.
- **Inactive stations** removes stations whose latest chat activity is too old.
- **Only new grids** retains stations in four-character grid squares not yet worked.
- **New bands** retains stations with at least one known, locally enabled and unworked band opportunity.
### Propagation-related filters
- **Tropo >=0dB** uses the available path-assessment result.
- **AS next 5m** retains stations with a current AirScout opportunity or one expected within the next five minutes.
These groups describe the operator's question, not separate processing stages. Every active filter is applied to the same station view.
## Active filters use AND logic
Several enabled filters are combined with AND. A station remains visible only when it satisfies every active condition.
For example, the operator can combine:
1. a QTF sector;
2. a maximum QRB;
3. **New bands**; and
4. **AS next 5m**.
The result contains only stations which lie in the chosen direction and distance, still offer a common unworked band and have a suitable AirScout window within the next five minutes.
This is deliberately strict. Adding another filter narrows the result; it does not add a second independent group of candidates.
## User list and map share the result
The user list and [Station Map](/features/station-map/) use the same filtered station set. If a station disappears from the table because of QTF, QRB, Worked, band or propagation criteria, it also disappears from the geographical view.
This makes the map useful as a spatial worklist rather than a second list with different rules.
## Hidden does not mean deleted
A filtered-out station remains in the active station model and continues to participate in chat processing. Incoming messages, QRG and band updates, Worked changes and other state can therefore make it relevant again.
The [Priority List](/features/priority-score/) is intentionally independent of the table filters. A high-priority candidate may still appear there while its row is hidden in the user list. Selecting that candidate updates the current station and **Further Info**, but does not silently disable the active filters.
## `Grid color` and `Reachability` are not filters
**Grid color** changes only the appearance of the QRA cell for an already worked four-character grid square. It does not remove a station and remains enabled when **Reset filters** is used.
The **Reachability** selector determines the band used for the Tropo column, the Tropo filter and an explicitly requested path calculation. Selecting a band does not itself filter the list. The choice is therefore also retained by **Reset filters**.
## Unknown Tropo results remain visible
**Tropo >=0dB** removes only stations for which a completed calculation returned a negative SSB margin.
Stations with a pending, missing or failed result remain visible. Missing data is not evidence that a path is unsuitable. Treating every absent online result as a negative path assessment would make the filter look decisive while merely hiding uncertainty.
## Filters react to new information
The visible result changes when relevant station data changes. A new log entry can remove a station through the Worked filter. A detected QRG may create a [Band Recognition](/features/band-recognition/) result for **New bands**. New [AirScout](/features/airscout/) information can make a station pass **AS next 5m**.
In practical terms: the filter bar turns the current station model into a temporary operating view. It does not create a smaller, disconnected copy of the contest.
[Read the complete filter and Reachability controls in the manual.](/manual/en/user-interface/#filters-and-reachability-controls)
[Read how Worked and band information changes through log synchronisation.](/features/log-sync/)
+101
View File
@@ -0,0 +1,101 @@
---
title: Station Map and Path Analysis
icon: 🗺️
category: Contest Awareness
since: "1.41"
summary: Use the filtered chat-member list as a geographical contest worklist and examine the selected radio path with terrain, Fresnel and link-budget estimates.
description: KST4Contest projects the current filtered station context onto a map and provides an engineering estimate of the selected path.
tagsList:
- station map
- path analysis
- terrain profile
- Fresnel zone
- link budget
- Open-Meteo
- Copernicus GLO-90
- contest workflow
related:
- station-filters
- qrg-detection
- band-recognition
- directional-opportunities
- dx-cluster
- rotator-control
---
## Why use a map during a contest?
A table is efficient when callsigns, scores or QRBs need to be sorted. It is less immediate when the operator wants to see which unworked stations remain in one direction or how they relate to the current antenna sector.
Since version 1.41, the KST4Contest station map provides this geographical view of the same operating context. It is not an independent list. The filters applied to the chat-member table also determine which stations appear on the map.
![Station map with path analysis](/manual/assets/station_map_path_analysis.png)
## A geographical contest worklist
With the **wkd** filter enabled, worked callsigns disappear from the user list. They also disappear from the map. What remains is a spatial worklist of stations which may still be relevant to the contest.
The map retains the context already known by KST4Contest:
- normal, Worked, directional-opportunity and selected marker states;
- known bands and open `B+` opportunities;
- the configured antenna direction, beamwidth and maximum QRB as a visible sector; and
- a Maidenhead locator grid for geographical orientation.
Only stations with a usable six-character locator can be positioned. Active chat variants of the same normalised base callsign are combined into one marker, while their actual message destinations remain separate.
At lower zoom levels, **Group nearby stations** keeps dense map areas readable by combining close markers into screen-based clusters. The grouping can be disabled immediately without changing the current viewport or selection, and KST4Contest remembers the choice. This setting does not split the geographical marker shared by active variants of the same base callsign.
Green has a specific meaning: it marks a directional opportunity derived from directed ON4KST messages. It does not merely mean that the marker happens to lie inside the local antenna sector.
## Selection remains connected to the chat
Selecting a marker selects the corresponding active chat member in the main window. KST4Contest scrolls to the entry in the user list, updates **Further Info** and prepares the complete visible callsign as the message target.
The suffix and chat category are retained. Combining several active variants into one geographical marker therefore does not turn them into one interchangeable ON4KST login.
The selected path is shown as a connection line. The map can then remain a quick selection surface or be used together with the lower path-analysis panel.
## G1YBB's map-driven workflow
G1YBB uses the map particularly consistently as a geographical contest worklist:
1. Enable the Worked filter so that already worked stations are removed.
2. Select an interesting station on the map.
3. Use **Trigger cluster spot** to send its known QRG to the connected logger.
4. Select the spot in Minos and move the radio to that QRG.
5. Complete and log the QSO.
6. Log synchronisation updates the Worked state, and the filter removes the station from both the user list and the map.
This creates a direct visual progression through the remaining stations. It is an optional and deliberately consistent operating method, not a prerequisite for using the map or KST4Contest.
[Watch G1YBB demonstrate this workflow.](https://www.youtube.com/watch?v=lMQZMiSHlUI)
## What does the path analysis calculate?
For the selected station, KST4Contest currently requests terrain elevations from the Open-Meteo elevation service. The provider uses Copernicus GLO-90 data, and one path is sampled with no more than 100 evenly distributed elevation points.
The resulting profile combines information including:
- terrain and the geometrical line between both antennas;
- radio and terrain horizons;
- the first Fresnel zone and its minimum clearance;
- possible Fresnel-zone intrusion and a rough diffraction estimate;
- a frequency selected from current QRG or band context; and
- an estimated link budget using power, antenna gain, feeder loss and path loss.
The **Frequency** shown in the panel is the value actually used for the calculation. It should be checked before interpreting the Fresnel zone or link budget, especially on the microwave bands.
## Engineering estimates, not measured conditions
The nominal resolution of the terrain model is not the distance between requested points. On a long path, 100 samples can leave substantial gaps, and narrow obstacles may remain undetected.
The calculation also cannot know the exact remote antenna height and pattern, buildings, vegetation, current atmospheric refractivity, interference or whether a detected QRG is still in use. Line of sight, Fresnel clearance, horizons and the link budget therefore remain technical estimates.
In practical terms: the map keeps the remaining contest stations geographically visible, while the path analysis exposes useful assumptions about one selected route. Neither view predicts propagation or guarantees a QSO.
[Read the complete map and path-analysis description in the manual.](/manual/en/features/#station-map-and-path-analysis-from-v141)
[Open the map controls in the user-interface manual.](/manual/en/user-interface/#station-map)
[Open the path-analysis and link-budget settings.](/manual/en/configuration/#path-analysis-and-link-budget)
@@ -0,0 +1,90 @@
---
title: TRX/QRG Synchronisation
icon: 🎛️
category: Station Control
since: "1.0"
summary: Keep the primary operating frequency in MYQRG current from RadioInfo or Win-Test STATUS packets while leaving logging and SECONDQRG independent.
description: KST4Contest can take the local primary QRG from compatible logger packets and make it available immediately to messages, beacons, QRG replies and Win-Test sked handover.
tagsList:
- TRX synchronisation
- QRG
- MYQRG
- RadioInfo
- Win-Test
- contest logger
related:
- log-sync
- macros
- automatic-replies
- dual-chat
- sked-reminder
- qrg-detection
---
## Why synchronise the local QRG?
Changing frequency in the logger and then copying the same value into the chat client is easy to forget. The result may be a perfectly valid beacon or QRG reply which points to yesterday's frequency.
KST4Contest can therefore update the primary local QRG, `MYQRG`, from the logger. The current value is then available immediately wherever the application's own QRG is required.
TRX/QRG Synchronisation has existed since version 1.0. Support for native Win-Test `STATUS` packets is included from version 1.31 onwards.
![TRX/QRG synchronisation settings](/manual/assets/client_settings_window_trxsync.png)
## Which sources can update MYQRG?
Two automatic sources are available:
- compatible `RadioInfo` packets received through the shared log-sync UDP listener; and
- native Win-Test `STATUS` packets received through the Win-Test network listener.
If neither source is enabled, `MYQRG` can be entered manually in the main window. Enabling an automatic source makes the received value the primary QRG instead.
## Frequency and QSO synchronisation are separate
The general UDP listener may receive QSO data and `RadioInfo` packets on the same port, but KST4Contest treats them as different functions.
A `RadioInfo` packet can update `MYQRG`; it does not mark a station as worked. Conversely, QSO or Worked synchronisation updates the log-derived station state without automatically changing the local QRG.
This separation is deliberate. A radio frequency says where the local station is operating. It says nothing about which remote station has just been worked.
## MYQRG does not mean SECONDQRG
Both automatic sources update `MYQRG` only. This is the local QRG of the first or primary chat category.
`SECONDQRG` remains independent and is not derived from incoming TRX packets. A dual-chat setup can therefore follow the logger automatically for the primary category while retaining a separately entered QRG for the second category.
## Main or pass frequency from Win-Test
By default, KST4Contest uses the main frequency from a Win-Test `STATUS` packet. It can instead be configured to use the pass frequency.
If the pass value is missing or invalid, KST4Contest falls back to the main frequency. An unusable pass value therefore does not clear `MYQRG` or replace it with an invalid frequency.
Several Win-Test operating positions may transmit `STATUS` packets on the same network. The configured station-name filter accepts QRG updates only from the intended position. This prevents another operating position from taking over the local frequency merely because its packet arrived later.
## What happens with several frequency sources?
All enabled sources write to the same `MYQRG` value. KST4Contest does not assign a fixed priority to `RadioInfo` and Win-Test `STATUS` packets.
If several sources are active, the most recently processed packet determines the current primary QRG. That is useful when the sources describe the same radio. Independent radios should not compete for the same value unless that last-packet behaviour is actually intended.
## Where is the synchronised QRG used?
The current primary QRG is immediately available for:
- [macros and variables](/features/macros/), including `MYQRG` and `MYQRGSHORT`;
- automatic beacons;
- [automatic QRG replies](/features/automatic-replies/) in the primary chat category; and
- a matching primary local-frequency fallback when a [sked is handed to Win-Test](/features/sked-reminder/).
The sked handover still checks whether the local QRG belongs to the selected band. Synchronisation supplies the value; it does not bypass that validation.
## What the function does not do
TRX/QRG Synchronisation tracks the local station's primary frequency. It does not detect the QRG of a remote station from chat text; that is the separate [Automatic QRG Detection](/features/qrg-detection/) function.
It also does not turn a `RadioInfo` packet into a logged QSO. Worked information remains the responsibility of [Log Synchronisation](/features/log-sync/).
In practical terms: one current local QRG can feed several operating workflows, but it remains one value. `SECONDQRG`, remote-station frequencies and Worked state keep their own meanings.
[Read the complete TRX Sync configuration in the manual.](/manual/en/configuration/#trx-sync-settings)
@@ -1,9 +1,9 @@
---
title: Hotfix Version 1.41.1
summary: Hotfix of a bug in Version 1.41
summary: Hotfix for a bug in version 1.41
date: 2026-07-08
---
## Version 1.41.1
We had a bug, where KST4Contest would clear out the text field, where you would send your messages after a period. It also happend to set your Cursor back to the Sending Field, when you were filtering for a station. We fixed this bug today. The new release is [here](https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1). You can also download the artifacts as always via the [Downloads-Page](/download/).
KST4Contest cleared the message input field after a while. It could also move the cursor back to that field while you were filtering for a station. This bug is fixed in version 1.41.1. The new release is available [here](https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1). You can also download the artifacts as usual from the [downloads page](/download/).
@@ -0,0 +1,29 @@
---
title: Version 1.43.1 released
summary: Reliable log synchronisation, persistent layouts and better DX Cluster compatibility
date: 2026-09-03
---
## Version 1.43.1
Version 1.43 focuses on the parts of KST4Contest that have to keep working during a long contest: ON4KST connection monitoring, log synchronisation, Worked information and the data sent to external logging software.
The first v1.43.0 packages already contained all functional changes described below, but some embedded version metadata still identified the build as version 1.42. v1.43.1 corrects this. If you already downloaded v1.43.0, use v1.43.1 instead.
### What changed
- **More reliable log synchronisation:** The Simplelogfile parser now closes the selected file after every pass, handles suffix variants by their base callsign and reports when a missing file has been created. UCXLog-compatible packets and Win-Test events share the same band normalisation, including the existing microwave bands.
- **Correct Worked state after login:** Persisted Worked information is applied before the initial ON4KST user list becomes visible. Reconnects and both chat categories therefore start with the correct state.
- **No false disconnect on a quiet server:** KST4Contest actively checks a quiet ON4KST session before treating it as dead. A period without new activity lines no longer causes an unnecessary reconnect.
- **DX Cluster compatibility:** Local spots now use a fixed, DXSpider-compatible 75-character line format that is also accepted reliably by DXLog. Frequencies up to 24 GHz keep the required column positions.
- **Layouts that stay where you put them:** Table column widths, window sizes and relevant dividers are saved automatically. Truncated table values expose their full text in a tooltip.
- **Controllable map grouping:** **Group nearby stations** switches map clustering on or off without changing the current viewport or selected station.
- **Smaller corrections:** Complete QRGs without a decimal separator are recognised across the supported bands, and private-message age highlighting no longer remains attached to reused table rows.
The complete technical list is available in the [changelog](/manual/en/changelog/) and the [GitHub release notes](https://github.com/praktimarc/kst4contest/releases/tag/v1.43.1).
### Getting it
Packages for Windows, Linux and macOS are available on the [download page](/download/) and in the [GitHub release](https://github.com/praktimarc/kst4contest/releases/tag/v1.43.1). The AUR packages are updated through their normal release workflow.
The German and English manuals have also been revised. The new contest-workflow chapter connects the individual functions into a practical operating sequence instead of describing them only in isolation. 73