Author SHA1 Message Date
Marc Froehlich 9037adf6eb on4kst: replace the legacy connection handling with a session-scoped supervisor using bounded connect, login and synchronisation timeouts, heartbeat and stale-link detection, controlled reconnect backoff and session-safe reader/writer queues; validate outgoing protocol context and malformed inbound user frames, enforce one locator per TCP session, publish complete user lists atomically and ignore repeated UE markers, prevent failed initial connections from entering a busy loop, add a compact high-visibility LINK state indicator, and remove false unhandled-frame reports. Solves #71 2026-08-14 00:36:53 +02:00
Marc Froehlich a4475e6d12 manual update: configuration, AS, functions 2026-08-13 22:49:19 +02:00
Marc Froehlich e3b2ea725a manual links updates 2026-08-13 00:56:22 +02:00
Marc Froehlich 6966edbfff manual update: map 2026-08-12 01:39:40 +02:00
Marc Froehlich 42d4b72dd6 website: prevent incomplete version-info deployments and regenerate validated update feeds after releases 2026-08-12 01:11:34 +02:00
Marc Froehlich 15f585c938 frequency-recognition: explicit frequencdies out of the name field will be copied to the qrg field and trigger the used band spot of a station to fit for this qrg. Airscout will then use this band explicitely for the calculation 2026-08-12 00:23:17 +02:00
Marc Froehlich 663f724c98 mapview: saved some space by moving some topics to the head bar 2026-08-11 23:38:47 +02:00
Marc Froehlich 9cccdb73e9 website - updated priority score md file 2026-08-10 03:13:18 +02:00
Marc Froehlich 401f271d56 fixed prio score penalty on sked fail 2026-08-10 03:06:06 +02:00
Marc Froehlich 00aefdacd5 website - updated macros md file 2026-08-10 02:53:03 +02:00
Marc Froehlich ea4fc9c008 website - updated dxcluster md file 2026-08-10 02:44:04 +02:00
Marc Froehlich caaeebd00c Added 6+4m and 10G and up frequency detection patterns 2026-08-10 02:42:29 +02:00
Marc Froehlich 852f76b05a fixed DXCluster Server where a spot will not be sent if no AP is between the station and me 2026-08-10 02:26:37 +02:00
Marc Froehlich d6c1ffbb34 website features and airscout information updated 2026-08-10 02:22:08 +02:00
Marc Froehlich cfbb978aee updated manual link and nav structure at the website 2026-08-10 02:08:11 +02:00
Marc Froehlich 8c3ff5f07c added favicon and finished the audit of the manual 2026-08-10 02:04:54 +02:00
Marc Froehlich 926bb9daee added manual (settings) and favicon for the website 2026-08-10 01:16:34 +02:00
Marc Froehlich 2828e7ef80 fix: make AirScout and map propagation band-aware by resolving realistic per-station QRGs to canonical AirScout bands, using one shared watchlist, honoring the operator-selected band for Calc selected and map path analysis, and replacing the obsolete 430 MHz fallback with 432 MHz (fixes #67) and parts of #74 2026-08-08 01:53:04 +02:00
Marc Froehlich b0cc5ee9ba updated en-manuals for skeds, user interface and features 2026-08-07 23:59:59 +02:00
Marc Froehlich b00e4c1e3d fixed worked-flag in the qso-of-the-other-table 2026-08-07 23:43:04 +02:00
Marc Froehlich b3ca684f04 updated website for sked and timeline features 2026-08-07 23:38:13 +02:00
Marc Froehlich 1de9673d12 uploaded 2 manual pictures for sked and timeline 2026-08-07 23:34:57 +02:00
Marc Froehlich c119f28b30 Uploaded sked, timeline, priority candidates and wintest connection manuals 2026-08-07 23:13:40 +02:00
Marc Froehlich 6a7f07c62e fix(wintest): replace unreliable band-limited AUTO sked mode detection with explicit SSB/CW selection and show exact KST callsigns in the timeline and fixed (#50 ensured Filter-Reset) 2026-08-07 22:56:24 +02:00
Marc Froehlich 4f574ebec6 fix(wintest): add band-aware sked QRG resolution, preserve portable callsigns, strip KST suffixes and correct ADDSKED timestamps 2026-08-07 22:43:17 +02:00
Marc Froehlich 7ce31e110b fix(wintest): add band-aware sked QRG resolution, preserve portable callsigns, strip KST suffixes and correct ADDSKED timestamps 2026-08-07 22:28:51 +02:00
Marc Froehlich 92804a622a Corrected typo error in MessagebusmanagementThread. Solves (#48) 2026-08-07 22:08:19 +02:00
Marc Froehlich 8bd5d8877d Add persistent map analysis toggle and improve compact layout for smaller screens. Solves (#69) 2026-08-06 01:52:59 +02:00
Marc Froehlich 97a93f726b Added en-manual for priority score and preferences + website feature text changed 2026-08-06 00:53:17 +02:00
Marc Froehlich c682ecf3f9 Changed website manual navigation 2026-08-06 00:46:00 +02:00
Marc Froehlich 259d0a4916 added priority score manual 2026-08-06 00:42:44 +02:00
Marc Froehlich 6d65bc365c fixed scoreservice. Not-QRV stations and such which dont provide needed bands now never could appear at the prirotity list 2026-08-06 00:26:29 +02:00
Marc Froehlich 084923366f fix(chat): grouped calculation of priority score for equal raw callsigns with different suffixes (also fixes #73) 2026-08-06 00:18:36 +02:00
Marc Froehlich eb38268be5 fix(chat): separate active members by full callsign and category 2026-08-06 00:01:29 +02:00
Marc Froehlich 36d2bd512d updated english manuals for changes from 1.40 due to 1.42 2026-08-05 23:20:11 +02:00
Marc Froehlich ffe7343671 inserted screenshots for manual pages for wkd status infos, band settings, functions and settings 2026-08-05 22:58:34 +02:00
97 changed files with 11395 additions and 1773 deletions
+18 -2
View File
@@ -6,6 +6,7 @@ on:
- main
paths:
- "src/**"
- "packaging/icons/**"
- "pom.xml"
- "mvnw"
- "mvnw.cmd"
@@ -62,7 +63,16 @@ jobs:
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec --dest dist
jpackage `
--type app-image `
--name praktiKST `
--icon packaging/icons/kst4contest.ico `
--input target/dist-libs `
--main-jar app.jar `
--main-class kst4contest.view.Kst4ContestApplication `
--module-path target/dist-libs `
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec `
--dest dist
- name: Create Windows ZIP
shell: pwsh
@@ -113,6 +123,7 @@ jobs:
jpackage \
--type app-image \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -198,6 +209,7 @@ jobs:
jpackage \
--type deb \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -257,6 +269,7 @@ jobs:
jpackage \
--type rpm \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -318,6 +331,7 @@ jobs:
jpackage \
--type app-image \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -436,6 +450,7 @@ jobs:
jpackage \
--type app-image \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -623,6 +638,7 @@ jobs:
jpackage \
--type dmg \
--name KST4Contest \
--icon packaging/icons/kst4contest.icns \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -646,4 +662,4 @@ jobs:
with:
name: macos-dmg-${{ matrix.os }}
path: dist/KST4Contest-*-macos-*.dmg
retention-days: 14
retention-days: 14
+63 -1
View File
@@ -7,7 +7,9 @@ on:
workflow_dispatch:
permissions:
actions: read
contents: write
issues: read
packages: write
env:
@@ -46,7 +48,16 @@ jobs:
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec --dest dist
jpackage `
--type app-image `
--name praktiKST `
--icon packaging/icons/kst4contest.ico `
--input target/dist-libs `
--main-jar app.jar `
--main-class kst4contest.view.Kst4ContestApplication `
--module-path target/dist-libs `
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec `
--dest dist
- name: Create Windows ZIP
shell: pwsh
@@ -90,6 +101,7 @@ jobs:
jpackage \
--type app-image \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -168,6 +180,7 @@ jobs:
jpackage \
--type deb \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -220,6 +233,7 @@ jobs:
jpackage \
--type rpm \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -272,6 +286,7 @@ jobs:
jpackage \
--type app-image \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -381,6 +396,7 @@ jobs:
jpackage \
--type app-image \
--name KST4Contest \
--icon src/main/resources/icons/kst4contest.png \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -510,6 +526,7 @@ jobs:
jpackage \
--type dmg \
--name KST4Contest \
--icon packaging/icons/kst4contest.icns \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
@@ -683,6 +700,9 @@ jobs:
- publish-flatpak-repo
steps:
- name: Checkout release source
uses: actions/checkout@v4.1.7
- name: Download Windows artifact
uses: actions/download-artifact@v4.1.3
with:
@@ -753,3 +773,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 }}
- 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: 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
+72 -16
View File
@@ -1,42 +1,98 @@
# KST4Contest
KST4Contest (also known as pratiKST) is a Java-based chat client for [ON4KST](http://www.on4kst.com/chat), focused on VHF/UHF/SHF contest operation.
KST4Contest is a Java-based client for the [ON4KST chat](https://www.on4kst.org/chat/login.php), developed for coordinated VHF, UHF and microwave contest operation.
## Website
The application is developed by Marc Fröhlich (DO5AMF) and since 2026 Philipp Wagner (DN9APW).
The offical Website of KST4Contest is now instead of [do5amf.funkerportal.de](https://do5amf.funkerportal.de) the new website [here](https://kst4contest.hamradioonline.de) [https://kst4contest.hamradioonline.de](https://kst4contest.hamradioonline.de)
## Start here
- [Project website](https://kst4contest.hamradioonline.de/)
- [Download Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
- [Online manual](https://kst4contest.hamradioonline.de/manual/)
- [GitHub wiki](https://github.com/praktimarc/kst4contest/wiki)
- [Issues and bug reports](https://github.com/praktimarc/kst4contest/issues)
- [Development roadmap](https://kst4contest.hamradioonline.de/roadmap/)
## What KST4Contest does
KST4Contest combines the ON4KST chat with information that is useful when coordinating contacts during a contest.
Among other things, it can:
- display and filter stations from the supported ON4KST chat categories;
- derive band and frequency information from chat messages and station names;
- maintain Worked and NOT QRV information for the available bands;
- calculate station priorities from distance, activity and other available information;
- manage internal skeds and received Win-Test skeds;
- use AirScout information when evaluating possible aircraft-scatter contacts;
- display stations, paths and additional propagation information on maps;
- exchange information with supported logging programs, Win-Test, PSTRotator and a local DX Cluster interface;
- provide configurable automatic replies for recurring chat requests.
Calculated scores, aircraft-scatter information and path assessments are operating aids. They depend on the available data and should not be treated as guarantees that a contact is possible.
## Installation
Ready-to-use packages are available for Windows, Linux and macOS. These packages include the required Java runtime, so a separate Java installation is normally not necessary.
Use the central download page to select the appropriate build:
- **Stable** is intended for normal contest operation.
- **Beta** contains changes that are being prepared for a stable release.
- **Nightly** contains the latest automated development build and is mainly intended for testing.
[Open the download page](https://kst4contest.hamradioonline.de/download/)
## Documentation
The full user documentation is maintained in the project wiki:
The documentation is available in German and English:
- https://github.com/praktimarc/kst4contest/wiki
- [German manual](https://github.com/praktimarc/kst4contest/wiki/de-Home)
- [English manual](https://github.com/praktimarc/kst4contest/wiki/en-Home)
- [Online manual](https://kst4contest.hamradioonline.de/manual/)
Direct entry points:
The Markdown sources used for the wiki and the generated PDF manuals are stored in [`github_docs`](github_docs/).
- German start page: https://github.com/praktimarc/kst4contest/wiki/de-Home
- English start page: https://github.com/praktimarc/kst4contest/wiki/en-Home
Changes to operating behaviour should be documented together with their purpose and limitations. This is especially important for functions whose result depends on external data, heuristics or information derived from chat messages.
## Build
## Building from source
Compile locally with Maven Wrapper:
Building KST4Contest requires JDK 21. The Maven Wrapper included in the repository should be used, so a separate Maven installation is not required.
Linux and macOS:
```bash
./mvnw clean test
./mvnw -B -DskipTests compile
```
## Notes
Windows:
- Source code is under `src/`.
- Documentation markdown pages for wiki/PDF are under `github_docs/`.
```powershell
mvnw.cmd clean test
mvnw.cmd -B -DskipTests compile
```
## Status of the latest CI:
Wiki Publishing:
## Repository structure
- `src/main/java/` application source code
- `src/test/` automated tests
- `github_docs/` German and English manual sources
- `website/` project website sources
- `packaging/` platform-specific packaging files
## CI status
### Documentation
[![Publish wiki](https://github.com/praktimarc/kst4contest/actions/workflows/github-wiki.yml/badge.svg)](https://github.com/praktimarc/kst4contest/actions/workflows/github-wiki.yml)
[![Docs PDF](https://github.com/praktimarc/kst4contest/actions/workflows/docs-pdf.yml/badge.svg)](https://github.com/praktimarc/kst4contest/actions/workflows/docs-pdf.yml)
Builds:
### Builds
[![Nightly Runtime Artifacts](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml/badge.svg)](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml)
## License
KST4Contest is distributed under the [GNU General Public License v3.0](LICENSE).
+26 -6
View File
@@ -1,10 +1,13 @@
# KST4Contest
# KST4Contest Manual / Handbuch
KST4Contest is a desktop client for the [ON4KST Chat](https://www.on4kst.org/chat/login.php), developed for VHF, UHF and SHF contest operation. It combines chat, candidate selection, sked planning, aircraft scatter information and connections to logging and station software.
KST4Contest is a desktop client for the [ON4KST Chat](https://www.on4kst.org/chat/login.php), developed for VHF, UHF and SHF contest operation. It combines chat, candidate selection, sked planning, aircraft-scatter information, station mapping and connections to logging and station software.
KST4Contest ist ein Desktop-Client für den [ON4KST-Chat](https://www.on4kst.org/chat/login.php), der für den Contest-Betrieb auf den VHF-, UHF- und SHF-Bändern entwickelt wurde. Er verbindet Chat, Stationsauswahl, Sked-Planung, Aircraft-Scatter-Daten sowie die Anbindung an Log- und Stationssoftware.
KST4Contest ist ein Desktop-Client für den [ON4KST-Chat](https://www.on4kst.org/chat/login.php), der für den Contest-Betrieb auf den VHF-, UHF- und SHF-Bändern entwickelt wurde. Er verbindet Chat, Stationsauswahl, Sked-Planung, Aircraft-Scatter-Daten, Stationskarte sowie die Anbindung an Log- und Stationssoftware.
Developed by / Entwickelt von **DO5AMF (Marc Fröhlich)**, operator at / Operator bei DM5M.
Developed by / Entwickelt von:
- **DO5AMF (Marc Fröhlich)**, operator at / Operator bei **DM5M**
- **DN9APW (Philipp Wagner)**, developer since / Entwickler seit Mai 2025
---
@@ -14,12 +17,29 @@ Developed by / Entwickelt von **DO5AMF (Marc Fröhlich)**, operator at / Operato
|---|---|
| [Deutsches Handbuch](de-Home) | [English manual](en-Home) |
Both versions follow the same structure and describe the same application state.
Beide Sprachfassungen verwenden dieselbe Struktur und beschreiben denselben Programmstand.
---
## Start here / Hier beginnen
- [Download Stable, Beta or Nightly / Stable, Beta oder Nightly herunterladen](https://kst4contest.hamradioonline.de/download/)
- [Online manual / Online-Handbuch](https://kst4contest.hamradioonline.de/manual/)
- [Installation](de-Installation) / [Installation](en-Installation)
- [Configuration](en-Configuration) / [Konfiguration](de-Konfiguration)
The Stable version is the normal choice for contest operation. Beta and Nightly builds are intended for testing particular changes and may contain functions which have not yet been released as Stable.
Für den normalen Contestbetrieb ist die Stable-Version vorgesehen. Beta- und Nightly-Builds dienen dem gezielten Test neuer Änderungen und können Funktionen enthalten, die noch nicht als Stable veröffentlicht wurden.
---
## Project links / Projektlinks
- [Project website / Projektwebseite](https://kst4contest.hamradioonline.de/)
- [Download the current stable release / Aktuelle stabile Version herunterladen](https://github.com/praktimarc/kst4contest/releases/latest)
- [Downloads](https://kst4contest.hamradioonline.de/download/)
- [Source code / Quellcode](https://github.com/praktimarc/kst4contest)
- [GitHub Releases](https://github.com/praktimarc/kst4contest/releases)
- [Bug reports and feature requests / Fehler und Funktionswünsche](https://github.com/praktimarc/kst4contest/issues)
- [Online manual / Online-Handbuch](https://kst4contest.hamradioonline.de/manual/)
- [Development roadmap / Entwicklungsstand](https://kst4contest.hamradioonline.de/roadmap/)
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+1 -1
View File
@@ -107,7 +107,7 @@ Die Flugzeugdaten können direkt in 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](Makros-und-Variablen#variablen)
Details: [Makros und Variablen](de-Makros-und-Variablen#variablen)
---
+142 -9
View File
@@ -33,7 +33,7 @@ Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfigu
| QTF | Richtung in Grad |
| QRG | Zuletzt aus einer Chat-Nachricht erkannte Frequenz |
| Tropo | Ergebnis der bandbezogenen Tropo- beziehungsweise Streckenbewertung |
| Score | Aktueller Prioritätswert |
| Score | Aktueller, numerisch sortierbarer Prioritätswert des normalisierten Basisrufzeichens |
| Act | Minuten seit der letzten Aktivität |
| AP | AirScout-Flugzeugdaten, sofern aktiviert |
| worked | Bandbezogener Worked-, Bandmöglichkeits- und Großfeldstatus sowie `wkdany` |
@@ -128,34 +128,167 @@ Die Änderung wirkt sofort auf die Spalte **NOT QRV @**, die Bandmöglichkeiten
![Bandbezogene NOT-QRV-Markierungen im Further-Info-Bereich](not_qrv_controls.png)
Hier können auch **Sked-Erinnerungen / Wecker** für beide Skkedpartner aktiviert werden.
Im selben Bereich wird der aktuelle **Priority score** der ausgewählten Station angezeigt.
Mit **Sked fail** lässt sich ein fehlgeschlagener Versuch markieren. Der Score des normalisierten Basisrufzeichens wird dadurch stark reduziert. **Reset fail** entfernt diese Markierung wieder. Die Markierung gilt für alle aktiven Suffix- und Kategorievarianten der Station und bleibt innerhalb der laufenden Programmsitzung erhalten.
Darunter befinden sich die Bedienelemente zum Anlegen eines Skeds:
| Bedienelement | Bedeutung |
|---|---|
| **Sked in** | Zeit bis zum Sked |
| **Band** | vereinbartes Band aus den eigenen aktivierten Bändern |
| **Mode** | `SSB` oder `CW` für eine mögliche Win-Test-Übergabe |
| **Create sked** | internen Sked anlegen |
| **Remind-PM in** | automatische Reminder-PMs aktivieren |
| **2+1**, **5+2+1**, **10+5+2+1** | Zeitpunkte der Reminder-PMs vor dem Termin |
![Sked-Steuerung im Further-Info-Bereich](sked_controls.png)
Das vorgeschlagene Band wird aus aktuellen QRG- und Namensinformationen der Station hergeleitet. Vor dem Anlegen kann es ausdrücklich geändert werden. Die Mode-Auswahl betrifft nur die Übergabe an Win-Test; der interne Sked und die Reminder-PMs funktionieren unabhängig davon.
**Create sked** legt den Termin immer zuerst in KST4Contest an. Ist der Win-Test-Netzwerk-Listener aktiv, wird anschließend zusätzlich eine Übergabe an Win-Test versucht. Kann keine zum ausgewählten Band passende QRG ermittelt werden oder ist Win-Test nicht erreichbar, bleiben der interne Sked, seine Priorisierung und gegebenenfalls angelegte Reminder erhalten.
Die vollständige Herleitung und die Grenzen der Funktion sind unter [Skeds und Sked-Erinnerungen](de-Funktionen#skeds-und-sked-erinnerungen) beschrieben.
---
## Prioritätsliste
Zeigt die vom Score-Service berechneten Top-Kandidaten. Aktualisiert sich automatisch im Hintergrund basierend auf Richtung, Entfernung und AP-Verfügbarkeit.
Die kompakte Prioritätsleiste befindet sich rechts zwischen Benutzerliste und Further-Info-Bereich. Sie zeigt die beiden derzeit höchstbewerteten Kandidaten unmittelbar im Hauptfenster:
```text
Priority: 1 RUFZEICHEN SCORE 2 RUFZEICHEN SCORE more
```
Ein Klick auf einen der beiden Kandidaten wählt den dazugehörigen aktiven Chatmember aus. Dabei werden das vollständige Rufzeichen einschließlich Suffix und die zugehörige Chat-Kategorie verwendet.
Die Schaltfläche **more** öffnet ein separates Fenster mit bis zu 15 Kandidaten. Die Liste ist nach absteigendem Score sortiert. Ein Doppelklick wählt den betreffenden Kandidaten aus und schließt das Fenster.
![Priority Score, kompakte Kandidatenliste und Further-Info-Steuerung](priority_score_overview.png)
Stationen mit einem Score von `0` werden nicht in die Prioritätsliste aufgenommen. In der Benutzerliste bleiben sie sichtbar, sodass der Ausschluss nachvollzogen und beispielsweise durch eine geänderte NOT-QRV-Markierung korrigiert werden kann.
Der Score wird für das normalisierte Basisrufzeichen berechnet. Mehrere aktive Varianten wie `9A0BB-2` und `9A0BB-70` können daher in der Benutzerliste denselben Wert anzeigen. Die Chatmember bleiben trotzdem getrennte Nachrichtenziele.
Neue Nachrichten, AirScout-Daten, Skeds und Statusänderungen lösen eine Neuberechnung aus. Zusätzlich erfolgt eine regelmäßige Aktualisierung im Hintergrund. Eine kurzzeitig noch nicht angepasste Reihenfolge ist deshalb kein Fehler.
Herleitung und Grenzen: [Prioritätsscore und Prioritätsliste](de-Funktionen#prioritätsscore-und-prioritätsliste-ab-v140).
---
## Cluster & QSO der anderen
## Stationskarte
Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen interessant in ruhigeren Phasen.
Die Stationskarte kann auf zwei Wegen geöffnet werden:
- **Windows → Show / hide station map** öffnet oder schließt das Kartenfenster.
- **Show on map** im **Further Info**-Bereich öffnet die Karte und zentriert sie auf die ausgewählte Station.
Die Karte zeigt die Stationen, die auch nach Anwendung der aktuellen Benutzerlistenfilter noch sichtbar sind. Ein Hinweis in der Kopfzeile zeigt an, wenn eine gefilterte Ansicht aktiv ist.
![Stationskarte mit ausgewählter Station und eingeblendeter Streckenanalyse](station_map_path_analysis.png)
Ein einzelner Stationsmarker kann direkt angeklickt werden. KST4Contest übernimmt die Station daraufhin als aktuelle Auswahl, scrollt die Benutzerliste zum passenden Chatmember und aktualisiert den **Further Info**-Bereich.
Marker, die bei der aktuellen Zoomstufe zu dicht beieinanderliegen, werden als Cluster mit einer Stationsanzahl angezeigt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus.
Für die ausgewählte Station erscheinen rechts unter **Selected station**:
- Rufzeichen,
- Locator,
- QRB und QTF,
- erkannte aktive Bänder,
- gegebenenfalls `B+` für eine offene Bandmöglichkeit und
- die zuletzt bekannten QRGs.
**Trigger cluster spot** sendet für die ausgewählte Station einen einzelnen Spot an die mit dem integrierten DX-Cluster-Server verbundenen Logprogramme. Die Schaltfläche setzt deshalb einen aktivierten Cluster-Server und mindestens einen verbundenen Client voraus.
Unterhalb der Karte befindet sich das Höhenprofil. Rechts werden die dazugehörigen Detailwerte angezeigt, unter anderem:
- verwendete Datenquelle und Anzahl der Höhenpunkte,
- Analysefrequenz,
- Erdkrümmungs- beziehungsweise Refraktionsmodell,
- Radio- und Geländehorizont,
- Fresnel-Freiheit,
- erkannte Hindernisse,
- Link-Budget,
- geschätzter Empfangspegel und
- eine zusammenfassende Pfadbewertung.
Mit **Hide path analysis** werden das Profil unterhalb der Karte und die ausführlichen Analysewerte rechts gemeinsam ausgeblendet. Der Kartenbereich erhält dadurch mehr Platz.
![Stationskarte mit ausgeblendeter Pfadanalyse](station_map_compact.png)
Der Hinweis **Path analysis is hidden** bleibt zusammen mit **Show path analysis** sichtbar. Die Funktion kann daher ohne Umweg wieder eingeschaltet werden. Der Zustand wird gespeichert.
Der Divider zwischen Karte und Detailbereich lässt sich horizontal verschieben. Bei schmalem Detailbereich werden längere Angaben umgebrochen; falls die Höhe nicht ausreicht, erscheint dort eine vertikale Scrollleiste.
Ausführliche Herleitung und Grenzen: [Stationskarte und Streckenanalyse](de-Funktionen#stationskarte-und-streckenanalyse-ab-v141)
---
## Globale Nachrichtentabs und Monitorfenster
Der untere Bereich des Hauptfensters enthält drei globale Nachrichtentabs. Ihr Inhalt ist nicht von der aktuell in der Benutzerliste ausgewählten Station abhängig.
| Tab | Inhalt |
|---|---|
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
| **DXCluster messages** | Über ON4KST empfangene DX-Cluster-Meldungen |
| **QSO of the other** | Gerichtete Nachrichten zwischen zwei anderen Stationen |
![Globale Nachrichtentabs im Hauptfenster](global_message_tabs.png)
Im Tab **QSO of the other** werden Absender und Empfänger getrennt dargestellt. Die Spalten **Last QRG TX** und **Last QRG RX** enthalten die zuletzt für beide Stationen bekannten Frequenzen. Sie geben nicht zwingend die QRG der angezeigten Unterhaltung wieder.
**wkd TX?** und **wkd RX?** zeigen den globalen Worked-Status der beiden Basisrufzeichen. Die Angaben sind nicht bandbezogen.
Der Tab **DXCluster messages** zeigt den meldenden und den gemeldeten Teilnehmer, deren Locator, die QRG, den Meldungstext und den globalen Worked-Status der gemeldeten Station. Welche Felder tatsächlich gefüllt sind, hängt von der vom ON4KST-Server übertragenen Meldung ab.
Nachrichtentexte bleiben einzeilig. Ist eine Zelle zu schmal, erscheint der vollständige Inhalt als Tooltip. Webadressen im Meldungstext lassen sich anklicken.
### Separates Monitorfenster
Zusätzlich öffnet KST4Contest das Fenster **Cluster & QSO of the other**. Es zeigt oben die DX-Cluster-Meldungen und darunter die gerichteten Nachrichten zwischen anderen Stationen.
![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.
Das Fenster lässt sich über das Menü aus- und wieder einblenden:
```text
Windows → Hide cluster / stranger QSOs
Windows → Show cluster / stranger QSOs
```
Die Tabellen im Hauptfenster und im Monitorfenster greifen auf dieselben Daten zu. Das Ausblenden des Monitorfensters beendet daher weder den Empfang noch die Darstellung in den unteren Tabs.
Herleitung und Grenzen: [Globale Nachrichtenansichten](de-Funktionen#globale-nachrichtenansichten).
---
## Menü
### Window
- **Use Dark Mode** (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.
### Windows
- **Hide cluster / stranger QSOs** beziehungsweise **Show cluster / stranger QSOs**: Blendet das zusätzliche Cluster- und QSO-Monitorfenster aus oder wieder ein.
- **hide options** beziehungsweise **show options**: Blendet das Einstellungsfenster aus oder wieder ein.
- **Use dark mode design**: Aktiviert das dunkle Farbschema.
- **Use default mode design**: Aktiviert das normale helle Farbschema.
- **Show / hide station map** öffnet beziehungsweise schließt das separate Fenster mit Stationskarte und Streckenanalyse.
---
## Fenstergrößen und Divider
Ab **v1.21** werden beim Klick auf **Save Settings"** auch Fenstergrößen und Divider-Positionen aller Panels in der Konfigurationsdatei gespeichert und beim nächsten Start wiederhergestellt.
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.
Bei Problemen mit der Darstellung: Konfigurationsdatei löschen → KST4Contest erstellt neue Standardwerte.
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.
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.
---
+156 -13
View File
@@ -4,25 +4,168 @@
Versionsverlauf von KST4Contest / PraktiKST.
Die veröffentlichten Stable-Versionen und ihre Programmpakete stehen unter [GitHub Releases](https://github.com/praktimarc/kst4contest/releases). Zusätzlich enthält diese Seite die Änderungen des aktuellen Entwicklungsstands, soweit sie bereits implementiert und geprüft wurden.
---
letzter Changelog bitte aus GitHub entnehmen. Der bisherige Changelog
## v1.42 Nightly / in Entwicklung
## v1.42 (2026-08)
- **QRG-Erkennung präzisiert**: Vollständige und relative Frequenzangaben werden weiterhin erkannt. Nackte dreistellige Zahlen werden nur noch mit erkennbarem Frequenzkontext ausgewertet, damit Signalrapporte, Bandangaben und andere Zahlen keine falsche QRG erzeugen.
- **Stationsbezogener Bandkontext**: Bei relativen Frequenzen verwendet KST4Contest zuerst einen höchstens 30 Minuten alten Bandkontext desselben Absenders. Erst danach greift das globale Fallback-Band.
- **Fallback-Band als Dropdown**: Das globale Fallback kann nur noch aus den tatsächlich unterstützten Bändern ausgewählt werden und gilt für die gesamte QRG-Erkennung, nicht nur für DX-Cluster-Spots.
- **QRG-Anzeige vereinheitlicht**: Frequenzen werden in der Benutzerliste und den Nachrichtentabellen mit mindestens drei Nachkommastellen dargestellt.
> Stand dieses Abschnitts: 10. August 2026.
> v1.42 ist noch kein veröffentlichtes Stable-Release. Bis zur Freigabe können weitere Änderungen hinzukommen.
## v1.41
**Stationskarte, Performance, Reaktionsfähiges UI**
v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen, Worked-Status, NOT-QRV-Markierungen, Rufzeichensuffixe und Frequenzen werden dadurch konsistenter in der Benutzerliste, der Stationskarte, der Prioritätsberechnung und den externen Schnittstellen verwendet.
**Neu:**
- **Stationskarte**: Interaktive OpenStreetMap-Karte zeigt die geografische Position aller aktiven Chatmember. Enthält Stationsmarker, Antennen-Kegel, Verbindungslinie zur ausgewählten Station, Maidenhead-Raster-Overlay und ein Wegprofil-Diagramm mit Geländehöhen-Analyse (Fresnel-Zonen, Horizonterkennung). Geländedaten aus Copernicus GLO-30, Open-Meteo API oder Offline-DEM-Import. Aircraft-Scatter-Weganalyse integriert. Funktioniert in AppImage und Flatpak ohne externe CDN-Verbindung (lokaler Tile-Proxy, eingebettetes Leaflet.js).
### Neu
**Geändert:**
- **Nachrichten-Tabellen-Limit auf 30.000 erhöht**: Chat- und Nachrichtentabellen sind auf 30.000 Einträge begrenzt. Ältere Nachrichten werden automatisch verworfen, was die Performance bei mehrtägigem Contest-Betrieb stabil hält.
- **Bildschirmgerechte Fenstergröße**: Beim Start wird das Hauptfenster auf den aktuellen Bildschirm angepasst. Wenn KST4Contest zuletzt auf einem größeren Monitor betrieben wurde, wird das Fenster automatisch verkleinert. Das UI-Layout ist kompakter und reaktionsfähiger auf kleineren Bildschirmen.
- **Gemeinsame Herleitung verfügbarer Bänder:** Ein zentraler `BandOpportunityResolver` wertet aktuelle QRGs, Bandangaben im Namensfeld, aktive Rufzeichenvarianten, Worked-Informationen und NOT-QRV-Markierungen gemeinsam aus. Benutzerliste, **New bands**, Band-Upgrade-Hinweis, Priority Score, Stationskarte und automatische Bandauswahl verwenden damit dieselbe Grundlage.
- **Erweiterte Bandanzeige:** Die Bandspalten unterscheiden jetzt:
- `X` für auf diesem Band gearbeitet,
- `a` für ein angebotenes, noch nicht gearbeitetes Band einer insgesamt neuen Station,
- `B+` für ein angebotenes, noch nicht gearbeitetes Band einer bereits auf einem anderen Band gearbeiteten Station und
- `o` für ein auf diesem Band bereits gearbeitetes Locator-Großfeld.
Die Anzeigen können kombiniert werden, beispielsweise als `ao` oder `B+o`. Die zusätzlichen Kennzeichnungen `a` und `o` lassen sich in den GUI-Einstellungen separat ausblenden.
- **Unterstützung für 50 und 70 MHz:** Beide Bänder stehen in der Stationskonfiguration, den Worked- und NOT-QRV-Funktionen, der Benutzerliste, den Filtern, der internen Datenbank, der UCXLog-Auswertung und dem Win-Test-Listener zur Verfügung. Bereits gespeicherte Datenbanken werden um die benötigten Spalten ergänzt.
- **Globale Nachrichtentabs:** Öffentliche Nachrichten, ON4KST-DX-Cluster-Meldungen und gerichtete Nachrichten zwischen anderen Stationen können direkt im Hauptfenster angezeigt werden. Das bisherige separate Monitorfenster bleibt zusätzlich verfügbar und verwendet dieselben Nachrichtenspeicher.
- **Manuelle QTF-Eingabe:** Die aktuelle Antennenrichtung kann auch ohne PSTRotator direkt in KST4Contest geändert 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.
- **Ausblendbare Streckenanalyse:** Geländeprofil und Analysebereich der Stationskarte können vollständig ausgeblendet werden. Die Auswahl wird gespeichert und beim nächsten Programmstart wiederhergestellt.
### Geändert
- **QRG-Erkennung präzisiert:** Vollständige und relative Frequenzangaben werden weiterhin erkannt. Nackte dreistellige Zahlen gelten nur noch bei erkennbarem Frequenzkontext als QRG. Signalrapporte, Bandangaben und andere Zahlen erzeugen dadurch seltener falsche Frequenzen.
- **Stationsbezogener Frequenzkontext:** Bei relativen QRGs verwendet KST4Contest zuerst einen höchstens 30 Minuten alten Bandkontext derselben Station. Erst wenn dieser fehlt, wird das global konfigurierte Fallback-Band verwendet.
- **Fallback-Band als Dropdown:** Das globale Fallback kann nur noch aus unterstützten Bandwerten ausgewählt werden. Es betrifft die gesamte QRG-Erkennung und nicht nur DX-Cluster-Spots.
- **Einheitliche QRG-Darstellung:** Frequenzen werden in Benutzer- und Nachrichtentabellen mit mindestens drei Nachkommastellen dargestellt.
- **Bandabhängige AirScout- und Streckenberechnung:** KST4Contest leitet für jede Station eine möglichst realistische Frequenz aus der aktuellen QRG und den bekannten Bandinformationen ab. AirScout erhält kanonische Bandwerte. Die frühere Zwischenlösung mit 430 MHz wurde durch 432 MHz ersetzt.
- **Gemeinsame Frequenzherleitung:** AirScout, **Calc selected** und die Pfadanalyse der Stationskarte verwenden denselben `PropagationFrequencyResolver`. Ein im Reachability-Dropdown ausdrücklich gewähltes Band wird bei manuellen Berechnungen berücksichtigt.
- **Rufzeichenvarianten getrennt verarbeitet:** Aktive Chatmember werden durch das vollständige Rufzeichen einschließlich Suffix und die Chat-Kategorie unterschieden. `DN9APW`, `DN9APW-2` oder vergleichbare Logins bleiben dadurch getrennte Nachrichtenziele.
- **Gemeinsame Basisinformationen:** Worked-Flags, NOT-QRV-Informationen und der Priority Score werden weiterhin für Varianten desselben Basisrufzeichens gemeinsam ausgewertet. Getrennte Nachrichtenziele führen damit nicht zu widersprüchlichen Worked-Daten.
- **Priority Score korrigiert:** Stationen ohne gemeinsames verfügbares Band oder mit übersteuernder NOT-QRV-Markierung werden nicht mehr als Prioritätskandidaten angeboten. Bandgelegenheiten bereits gearbeiteter Stationen können einen eigenen Priority Boost erhalten.
- **Sked-Erstellung erweitert:** Das Band wird aus den lokal aktivierten Bändern gewählt. Für die Win-Test-Übergabe wird `SSB` oder `CW` ausdrücklich ausgewählt, statt den Mode unzuverlässig aus dem Band abzuleiten.
- **Win-Test-Sked-Übergabe präzisiert:** Die QRG muss zum gewählten Band passen. Sichtbare KST-Suffixe werden für das Logziel entfernt, portable Bestandteile bleiben erhalten und die Zeitangaben der `ADDSKED`-Pakete werden korrekt erzeugt. Ein Fehler bei der Übergabe entfernt den internen KST4Contest-Sked nicht.
- **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.
- **Variablen zentral aufgelöst:** Nachrichtenvariablen für Beacons, Shortcuts, Snippets und andere automatisch erzeugte Texte werden über einen gemeinsamen Resolver verarbeitet.
- **Nachrichtentabellen verbessert:** Abgeschnittene Nachrichtentexte erhalten einen Tooltip mit dem vollständigen Inhalt. Erkannte Webadressen können im Systembrowser geöffnet werden.
- **Kompaktere Filterleiste:** Die Filter bleiben bei normaler Breite in einer kompakten Anordnung und werden erst dann umgebrochen, wenn der tatsächlich verfügbare Platz nicht mehr ausreicht. Der mittlere Divider kann dadurch weiter verschoben werden.
- **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.
- **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
- **Langzeitfehler der Stationsauswahl:** Die vom Message-Thread verwalteten Chatmember wurden von der JavaFX-Ansicht entkoppelt. Gleichzeitige Änderungen der Daten und Tabellenansicht führen dadurch nicht mehr nach längerer Laufzeit zu fehlerhaften Auswahlmodellen oder Concurrent-Modification-Problemen.
- **Keine Phantom-Chatmember durch UM3:** Historische oder zusätzliche Servermeldungen erzeugen keine Benutzerlisteneinträge für Stationen, die nicht tatsächlich im Chat angemeldet sind.
- **Nachrichten an Rufzeichen mit Suffix:** Mehrere gleichzeitig angemeldete Varianten desselben Basisrufzeichens überschreiben sich nicht mehr gegenseitig. Damit wurde [Issue #73](https://github.com/praktimarc/kst4contest/issues/73) behoben.
- **DX-Cluster-Locatoren:** Sender und gemeldete Station erhalten nicht mehr versehentlich denselben Locator. Damit wurde [Issue #48](https://github.com/praktimarc/kst4contest/issues/48) behoben.
- **Worked-Anzeige in „QSO of the other“:** Die Worked-Spalten verwenden wieder die jeweils richtige sendende beziehungsweise empfangende Station.
- **Fehlende SECONDAP-Daten:** Eine nicht vorhandene zweite Aircraft-Scatter-Gelegenheit führt beim Bearbeiten der Anzeige nicht mehr zu einer ungültigen Textauswahl und JavaFX-Exception.
- **Historische Rufzeichen:** Das Hervorheben oder Anklicken eines Rufzeichens, das nicht mehr in der aktuellen Benutzerliste vorhanden ist, läuft nicht mehr in eine Exception.
- **Win-Test-Sked-Zeit und Rufzeichen:** Zeitstempel, Band-QRG-Zuordnung sowie die Behandlung von KST-Suffixen und portablen Rufzeichen wurden korrigiert.
- **Filter-Reset:** Alle Filterprädikate werden tatsächlich entfernt; der sichtbare Zustand des Buttons entspricht wieder dem wirksamen Filterzustand.
### Dokumentation und Auslieferung
- Das deutsche und englische Handbuch wurde systematisch mit dem Quellcode abgeglichen, erweitert und mit aktuellen Screenshots versehen. Die Dokumentation erklärt nicht nur die Bedienelemente, sondern auch Herleitung, Datenquellen und Grenzen der Funktionen.
- Für Arch Linux stehen `kst4contest-bin`, `kst4contest` und `kst4contest-git` im AUR zur Verfügung.
- Die Downloadseite unterscheidet Stable, Beta und Nightly und bietet die jeweils tatsächlich vorhandenen Pakete für Windows, Linux und macOS an.
- Nightly-Pakete werden automatisiert aus dem aktuellen `main`-Branch gebaut. Stable- und Beta-Releases verwenden reproduzierbare Paketnamen für die unterstützten Plattformen.
### Bekannte Grenzen
- Die aktive Geländedatenquelle verwendet Open-Meteo mit Copernicus-GLO-90-Daten und höchstens 100 Höhenpunkten pro Strecke.
- Der atmosphärische K-Faktor ist derzeit fest auf `4/3` eingestellt.
- Für die Gegenstation wird eine Antennenhöhe von 10 Metern über Grund angenommen.
- Aircraft-Scatter-Daten aus AirScout und die Geländeanalyse der Stationskarte sind weiterhin getrennte Bewertungen.
- Eine genauere Konfiguration von Stationshöhe, Frequenz und K-Faktor wird in [Issue #74](https://github.com/praktimarc/kst4contest/issues/74) weiterverfolgt.
---
## v1.41.1 (2026-07-08)
**Hotfix für Texteingabe und Fokusverhalten**
### Behoben
- Das Nachrichteneingabefeld wurde nach einiger Zeit beziehungsweise bei bestimmten UI-Aktualisierungen unerwartet geleert.
- Beim Filtern oder Auswählen einer Station wurde der Eingabefokus unbeabsichtigt wieder in das Sendefeld verschoben.
Die korrigierte Version ist als [Release v1.41.1](https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1) verfügbar.
---
## v1.41.0 (2026-07-01)
**Stationskarte, begrenzte Nachrichtenspeicher und bildschirmgerechtes Hauptfenster**
### Neu
- **Stationskarte:** Eine interaktive OpenStreetMap-Karte stellt aktive Chatmember mit brauchbarem Locator geografisch dar.
- **Antennensektor und Verbindungslinie:** Die Karte zeigt den aktuellen eigenen QTF, den konfigurierten Antennen-Öffnungswinkel, das maximale QRB und die Verbindung zur ausgewählten Station.
- **Maidenhead-Raster:** Ein an die Zoomstufe angepasstes Locator-Raster erleichtert die geografische Einordnung.
- **Geländeprofil:** Für ausgewählte Stationen kann ein Höhenprofil über die Open-Meteo Elevation API berechnet werden. Die aktive Datenquelle verwendet Copernicus GLO-90 und höchstens 100 gleichmäßig verteilte Abfragepunkte.
- **Geometrische Streckenanalyse:** Die Auswertung berücksichtigt Sichtlinie, Erdkrümmung mit `k = 4/3`, Radio- und Geländehorizont, erste Fresnel-Zone und eine grobe Hindernisabschätzung.
- **Lokaler Karten-Proxy:** Leaflet wird mit der Anwendung ausgeliefert. Kartenkacheln werden über einen lokalen Proxy geladen, damit keine externe JavaScript-Bibliothek zur Laufzeit nachgeladen werden muss. Für die OpenStreetMap-Kacheln und die Online-Höhendaten ist weiterhin eine Internetverbindung erforderlich.
### Geändert
- **Begrenzte Nachrichtenspeicher:** Die globale Chatnachrichtenliste wird oberhalb von 30.000 Einträgen auf 25.000 verkleinert. Der getrennte DX-Cluster-Speicher wird oberhalb von 10.000 Einträgen auf 8.000 verkleinert.
- **Bildschirmgerechte Startgröße:** Das Hauptfenster wird beim Start gegen den sichtbaren Bereich des primären Bildschirms geprüft und bei Bedarf verkleinert oder verschoben.
- **Kompaktere Benutzeroberfläche:** Mehrere Bereiche wurden für kleinere Bildschirme und verstellbare Divider angepasst.
### Einordnung der Kartenfunktion
Die Stationskarte und AirScout können dieselbe Gegenstation betreffen, führen aber getrennte Berechnungen durch. Aircraft-Scatter-Flugzeuge werden nicht in das Geländeprofil eingerechnet.
Im Quellcode vorhandene Klassen für Copernicus GLO-30, Offline-DEM-Import und weitere Terrain-Provider waren in v1.41 nicht Bestandteil der aktiv verwendeten Berechnungskette. Die tatsächlich verwendete Online-Höhenquelle ist Open-Meteo auf Basis von Copernicus GLO-90.
---
+505 -55
View File
@@ -313,19 +313,86 @@ Für ausgewählte Stationen in der Benutzerliste gibt es direkte Buttons, um das
---
## Sked-Erinnerungen mit ALERT (ab v1.40)
## Skeds und Sked-Erinnerungen
Für jeden Chatmember kann ein Sked-Erinnerungsdienst mit automatischen Nachrichten aktiviert werden. Konfigurierbare Intervallmuster:
> Verfügbar ab v1.40; Band-, Rufzeichen- und Win-Test-Behandlung erweitert in Nightly / v1.42.
- **2+1 Minuten**: Nachrichten bei 2 min und 1 min vor dem Sked.
- **5+2+1 Minuten**: Nachrichten bei 5, 2 und 1 min vor dem Sked.
- **10+5+2+1 Minuten**: Nachrichten bei 10, 5, 2 und 1 min vor dem Sked.
Ein Sked ist mehr als eine Erinnerung an eine Uhrzeit. Er muss während des laufenden Contestbetriebs rechtzeitig sichtbar werden, die vereinbarte Station priorisieren und sofern gewünscht die Gegenstation noch einmal an den Termin erinnern.
Zusätzlich zu den Nachrichten an die Gegenstation gibt es eine **akustische und optische Benachrichtigung** für den eigenen Operator, sodass kein Sked vergessen wird.
KST4Contest behandelt deshalb drei voneinander unabhängige Aufgaben:
Aktivierung: FurtherInfo-Panel der entsprechenden Station.
1. Der Sked wird intern gespeichert und in die Prioritätsberechnung einbezogen.
2. Der Termin erscheint in der AP- und Sked-Timeline.
3. Optional werden vor dem Termin automatische Privatnachrichten gesendet.
Ist der Win-Test-Netzwerk-Listener aktiviert, versucht KST4Contest zusätzlich, den Sked an Win-Test zu übergeben. Ein Problem bei dieser Übergabe löscht oder verhindert den internen Sked nicht.
### Sked anlegen
Zuerst die gewünschte Station in der Benutzerliste auswählen. Die Bedienelemente befinden sich anschließend unten im Bereich **Further Info**.
| Bedienelement | Funktion |
|---|---|
| **Sked in** | Legt fest, in wie vielen Minuten der Sked stattfinden soll. Verfügbar sind 2 bis 15 sowie 20 Minuten. |
| **Band** | Wählt das Band des Skeds. Angeboten werden die unter **Station → my station uses …** aktivierten eigenen Bänder. |
| **Mode** | Legt den an Win-Test zu übertragenden Mode fest. Verfügbar sind `SSB` und `CW`. Die Auswahl hat keinen Einfluss auf den internen Sked oder die Reminder-PMs. |
| **Create sked** | Legt den internen Sked an und versucht bei aktiviertem Win-Test-Netzwerk-Listener zusätzlich die Übergabe an Win-Test. |
| **Remind-PM in** | Aktiviert die automatischen Privatnachrichten vor dem Termin. |
| **2+1**, **5+2+1**, **10+5+2+1** | Legt fest, wie viele Minuten vor dem Sked die Reminder-PMs gesendet werden. |
![Sked-Steuerung im Further-Info-Bereich](sked_controls.png)
KST4Contest versucht, ein sinnvolles Band vorzuwählen. Dafür werden nacheinander folgende Informationen verwendet:
1. eine höchstens 30 Minuten alte QRG der ausgewählten Station auf einem eigenen aktivierten Band,
2. eine eindeutige Bandangabe im Namensfeld der Station und
3. das erste aktivierte eigene Band.
Aktive Rufzeichenvarianten desselben Basisrufzeichens werden bei der Suche nach einer aktuellen Bandinformation gemeinsam betrachtet. Eine manuelle NOT-QRV-Markierung wird bei der automatischen Vorauswahl berücksichtigt. Das Band kann trotzdem ausdrücklich geändert werden, wenn der Operator bewusst eine andere Vereinbarung getroffen hat.
### Auswirkung auf den Priority Score
Ein eingetragener Sked erhöht den Score des normalisierten Basisrufzeichens:
| Zeitraum | Sked-Anteil am Score |
|---|---:|
| mehr als 15 Minuten vor dem Termin | `+40` |
| 15 bis 3 Minuten vor dem Termin | kontinuierlicher Anstieg von `+300` bis in Richtung `+1200` |
| weniger als 3 Minuten vor bis 1 Minute nach dem Termin | `+5000` |
| später als 1 Minute nach dem Termin | kein Sked-Boost mehr |
Die starke Gewichtung unmittelbar vor dem Termin ist beabsichtigt. Ein vereinbarter Sked soll dann nicht durch eine gerade sehr aktive, aber nicht fest eingeplante Station aus der Prioritätsliste verdrängt werden.
Der Score wird für das Basisrufzeichen berechnet. Ein Sked mit `DN9APW-2` beeinflusst daher auch den gemeinsamen Score weiterer aktiver Varianten von `DN9APW`. Das konkrete Nachrichtenziel bleibt trotzdem `DN9APW-2` in der beim Anlegen ausgewählten Chat-Kategorie.
Fünf Minuten nach dem Termin wird der Sked aus der internen Liste entfernt.
### Reminder-PMs
Reminder-PMs werden nur angelegt, wenn **Remind-PM in** aktiviert ist. Je nach ausgewähltem Muster sendet KST4Contest beispielsweise zwei und eine Minute vor dem Sked folgende Privatnachricht:
```text
[KST4C Autoreminder] sked in 2 min
```
Die Nachricht geht an das vollständige sichtbare KST-Rufzeichen und in die Chat-Kategorie, in der der Sked angelegt wurde. Ein Sked für `DN9APW-2` wird daher nicht versehentlich an `DN9APW`, `DN9APW-70` oder eine gleichnamige Station in einer anderen Kategorie gesendet.
Beim tatsächlichen Reminder zeigt KST4Contest zusätzlich den optischen **SKED**-Hinweis an. Ist die einfache Soundausgabe aktiviert, wird außerdem ein Hinweiston abgespielt. Das bloße Aktivieren des Reminders löst noch kein Blinken aus.
Wird für dasselbe vollständige Rufzeichen ein neuer Satz Reminder aktiviert, ersetzt dieser die zuvor geplanten Reminder dieses Rufzeichens.
### Speicherung und Grenzen
Skeds und Reminder-Zeitpläne werden nur im Arbeitsspeicher geführt. Nach einem Neustart von KST4Contest müssen noch benötigte Termine erneut angelegt werden.
Die automatische Bandvorauswahl ist eine Herleitung aus vorhandenen Chatinformationen. Sie beweist nicht, dass die Station noch auf der zuletzt genannten QRG arbeitet. Band, Uhrzeit und Mode sollten deshalb vor **Create sked** kontrolliert werden.
Bedienung: [Stationsinfo-Panel](de-Benutzeroberflaeche#stationsinfo-panel-further-info)
Darstellung: [AP- und Sked-Timeline](#ap--und-sked-timeline)
Win-Test-Übergabe: [Log-Synchronisation Win-Test](de-Log-Synchronisation#win-test)
---
## QSO-Monitoring (ab v1.31)
@@ -337,17 +404,23 @@ Konfiguration: [Konfiguration Sniffer-Einstellungen](de-Konfiguration#sniffe
---
## Win-Test-Integration (ab v1.31, vollständig ab v1.40)
## Win-Test-Integration
KST4Contest unterstützt [Win-Test](https://www.win-test.com/) vollständig als Logprogramm:
KST4Contest verwendet für Win-Test einen eigenen Listener für das native Win-Test-Netzwerkprotokoll. Darüber werden drei voneinander getrennte Funktionen bereitgestellt:
- **Log-Synchronisation**: Gearbeitete Stationen werden automatisch aus Win-Test übernommen und in der Benutzerliste markiert.
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz wird aus Win-Test-UDP-Paketen ausgewertet und befüllt die `MYQRG`-Variable.
- **Sked-Übergabe (SKED Push via UDP)**: Vereinbarte Skeds aus KST4Contest können direkt an Win-Test übertragen werden, sodass das Rufzeichen der Gegenstation im Win-Test-Sked-Fenster erscheint.
- neue QSOs einschließlich Band- und gegebenenfalls Locatorinformation übernehmen,
- die aktuelle QRG aus Win-Test-STATUS-Paketen auswerten und
- intern angelegte Skeds als `ADDSKED` an das Win-Test-Netzwerk übergeben.
Bei der Sked-Übergabe wird die QRG nicht durch eine feste Standardfrequenz ersetzt. KST4Contest sendet nur dann einen Win-Test-Sked, wenn eine zum ausgewählten Band passende QRG ermittelt werden konnte. Der interne Sked, die Timeline und die Reminder-PMs funktionieren unabhängig davon weiter.
Ein sichtbarer KST-Suffix wie `-2`, `-70` oder `-144` bleibt innerhalb von KST4Contest erhalten, wird für das Win-Test-Logrufzeichen jedoch entfernt. Portable Bestandteile wie `/P`, `/M` oder ein Länderpräfix bleiben bestehen.
Einrichtung und genaue Datenbehandlung: [Log-Synchronisation Win-Test](de-Log-Synchronisation#win-test)
Einstellungen: [Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener-ab-v131)
Details zur Konfiguration: [Konfiguration Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener)
---
## PSTRotator-Interface (ab v1.31, vollständig ab v1.40)
@@ -372,86 +445,463 @@ Konfiguration: [Band-Upgrade-Hinweis nach einem Logeintrag](de-Konfiguration#ban
Worked-, NOT-QRV- und Großfelddaten laufen nach drei Tagen automatisch ab. Einzelheiten und manueller Reset: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
---
## Chatmember Score-System / Prioritätsliste (ab v1.40)
## Prioritätsscore und Prioritätsliste (ab v1.40)
KST4Contest berechnet automatisch eine **Prioritätsbewertung** für jeden aktiven Chatmember. Der Score setzt sich zusammen aus:
### Warum wird überhaupt ein Score benötigt?
- Antennenrichtung der Gegenstation (zeigt sie auf mich?)
- QRB (Entfernung)
- Aktivitätszeit und Nachrichtenanzahl
- Aktive Bänder und Frequenzen
- AP-Verfügbarkeit (AirScout)
- Sked-Richtung
- Sked-Erfolgsrate und Skedfail-Markierungen
Eine klassische Chat-Benutzerliste zeigt zunächst nur, welche Stationen gerade eingeloggt sind. Im Contestbetrieb reicht diese Information nicht aus. Der Operator muss zusätzlich abschätzen, welche Station noch nicht gearbeitet wurde, auf welchem Band ein QSO möglich sein könnte, wohin die Antenne zeigt, ob ein passendes Flugzeug verfügbar ist und ob ein vereinbarter Sked unmittelbar bevorsteht.
Die Top-Kandidaten werden in einer eigenen Prioritätsliste hervorgehoben und helfen, im Contest-Stress die wichtigsten Stationen nicht zu übersehen.
Bei einer kurzen Liste lässt sich das noch im Kopf erledigen. Mit zunehmender Contestdauer, mehreren Bändern und zwei gleichzeitig verwendeten Chat-Kategorien wird daraus jedoch eine ständig wiederholte Entscheidung.
Stationen, bei denen ein Sked gescheitert ist, können über den **Skedfail-Button** im FurtherInfo-Panel markiert werden das senkt ihren Score vorübergehend.
KST4Contest führt die bereits vorhandenen Informationen deshalb in einem Prioritätsscore zusammen. Der Score beantwortet nicht die Frage, ob ein QSO sicher möglich ist. Er hilft bei der praktisch wichtigeren Frage:
> Welche der aktuell sichtbaren Stationen sollte ich mir als Nächstes ansehen?
### Wann wird eine Station ausgeschlossen?
Vor der eigentlichen Gewichtung prüft KST4Contest, ob überhaupt eine bekannte Bandmöglichkeit besteht. Dafür werden alle aktiven Chat-Einträge desselben normalisierten Basisrufzeichens gemeinsam ausgewertet.
Berücksichtigt werden:
1. die in den Stationseinstellungen aktivierten eigenen Bänder,
2. höchstens 30 Minuten alte QRG-Erkennungen der Gegenstation,
3. eindeutige Bandangaben im Namensfeld ihrer aktiven Chat-Einträge,
4. die pro Band gespeicherten Worked-Markierungen und
5. manuell gesetzte NOT-QRV-Tags.
NOT QRV hat dabei Vorrang vor automatisch erkannten Frequenzen oder Bandangaben.
Sind Bänder der Gegenstation bekannt, aber keines davon ist lokal aktiviert und noch verfügbar, erhält die Station einen Score von `0`. Dasselbe gilt, wenn alle gemeinsam möglichen Bänder bereits gearbeitet wurden.
Fehlen dagegen sämtliche Bandinformationen, wird die Station nicht allein deshalb ausgeschlossen. Eine unbekannte Bandmöglichkeit ist nicht dasselbe wie eine nachweislich unmögliche Bandmöglichkeit. Erst wenn alle eigenen aktivierten Bänder für die Station manuell als NOT QRV markiert wurden, ist auch in diesem Fall keine aktuelle Contestmöglichkeit mehr vorhanden.
Stationen mit einem Score von `0` bleiben in der Benutzerliste sichtbar, erscheinen aber nicht in der Prioritätsliste.
### Welche Informationen erhöhen oder verringern den Score?
Der Score entsteht aus mehreren voneinander unabhängigen Hinweisen. Ein einzelnes Kriterium entscheidet daher normalerweise nicht über den endgültigen Listenplatz.
| Faktor | Wirkung auf die Priorisierung |
|---|---|
| Worked-Status | Ein noch auf keinem unterstützten Band gearbeitetes Rufzeichen erhält eine höhere Ausgangspriorität. Bereits gearbeitete Stationen werden niedriger bewertet, bleiben bei offenen Bandmöglichkeiten aber Kandidaten. |
| Verfügbare Bänder | Mehrere gemeinsam nutzbare und noch nicht gearbeitete Bänder erhöhen die Priorität. Optional kann für Band-Upgrades ein zusätzlicher Boost aktiviert werden. |
| Entfernung | Entfernungen unter 200 km werden niedriger gewichtet. Der Bereich zwischen 200 km und dem konfigurierten maximalen QRB wird bevorzugt. Stationen jenseits des maximalen QRB werden deutlich herabgestuft. Fehlt der QRB, entfällt dieser Faktor. |
| Antennenrichtung | Liegt der QTF zur Gegenstation innerhalb der Hälfte des konfigurierten Antennen-Öffnungswinkels um den aktuellen eigenen QTF, steigt der Score. Je näher die Richtungen zusammenliegen, desto stärker wirkt der Hinweis. |
| AirScout | Mindestens ein aktuell erreichbares Flugzeug erhöht den Score. Eine erwartete AP-Gelegenheit in null, einer oder zwei Minuten wird zusätzlich zeitlich gewichtet. |
| Aktuelle Chat-Aktivität | Eine Nachricht innerhalb der letzten Minute wirkt stärker als eine Nachricht innerhalb der letzten drei Minuten. Mehrere eingehende Zeilen innerhalb des Aktivitätsfensters erhöhen den Score zusätzlich. |
| Positive Signale | Erkannte Angaben wie `QRV`, `READY`, `RGR`, `OK`, `TNX` oder vergleichbare konfigurierte Textmuster werden für einige Minuten als positiver Hinweis berücksichtigt. |
| Antwortverhalten | Reagiert eine Station nach einer eigenen `/cq`-Nachricht schnell mit einer weiteren sichtbaren Chat-Zeile, wirkt sich die gemittelte Reaktionszeit positiv aus. Bleibt eine solche Zeile aus, entsteht nach dem konfigurierten Timeout eine negative Bewertung. |
| Skeds | Ein eingetragener Sked erhöht die Priorität zunächst leicht. Innerhalb der letzten 15 Minuten vor dem Termin steigt der Einfluss kontinuierlich an. Zwischen drei Minuten vor und einer Minute nach dem Termin erhält der Sked eine sehr hohe Priorität. |
| Fehlgeschlagener Versuch | **Sked fail** reduziert den Score der Station stark, bis die Markierung mit **Reset fail** zurückgesetzt oder KST4Contest neu gestartet wird. |
Das standardmäßige Aktivitätsfenster für die Anzahl eingehender Nachrichten beträgt 180 Sekunden. Eine aktuelle Nachricht innerhalb der letzten 60 Sekunden wird noch einmal gesondert bewertet. Der standardmäßige No-Reply-Timeout beträgt 13 Minuten.
Beim Antwortverhalten kann KST4Contest nicht sicher feststellen, ob eine nachfolgende öffentliche oder private Nachricht tatsächlich die Antwort auf die eigene Anfrage war. Jede anschließend empfangene Zeile derselben Station beendet deshalb den laufenden Antwortzeitversuch. Der Wert ist eine praktische Näherung und keine statistisch belastbare Antwortquote.
### Was bedeutet ein eingetragener Sked?
Ein Sked ist eine zeitlich vereinbarte Arbeitsaufgabe. Deshalb übersteuert ein unmittelbar bevorstehender Sked die meisten normalen Aktivitäts- und Entfernungshinweise. Ohne diese Gewichtung könnte eine gerade sehr aktive Station einen vereinbarten Termin aus der Prioritätsliste verdrängen.
Der starke Sked-Boost ist absichtlich auf den Zeitraum von drei Minuten vor bis eine Minute nach dem eingetragenen Termin begrenzt. Ein weiter in der Zukunft liegender Sked bleibt sichtbar, soll den laufenden Betrieb aber noch nicht dominieren.
Die Bewertung wird für das normalisierte Basisrufzeichen vorgenommen. Ein Sked für eine aktive Variante wie `9A0BB-23` beeinflusst daher den gemeinsamen Score der zu `9A0BB` gehörenden Chat-Einträge.
### 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.
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.
Als konkretes Nachrichtenziel verwendet KST4Contest den zuletzt passenden aktiven Login in der zuletzt verwendeten Chat-Kategorie. Beim Anklicken eines Kandidaten wird anschließend das vollständige Rufzeichen einschließlich Suffix und Kategorie ausgewählt.
### Aktualisierung und Anzeige
Änderungen durch neue Nachrichten, AirScout-Daten, Skeds, Worked-Informationen oder manuelle NOT-QRV- und Sked-fail-Markierungen fordern unmittelbar eine Neuberechnung an. Zusätzlich wird der Score regelmäßig im Hintergrund aktualisiert, weil Aktivitäts-, AP- und Sked-Informationen auch ohne neues Ereignis altern.
Eine kurze Verzögerung von einigen Sekunden zwischen einem Ereignis und der sichtbaren neuen Reihenfolge ist daher normal.
Die Benutzeroberfläche zeigt den Score an drei Stellen:
- als numerisch sortierbare Spalte **Score** in der Benutzerliste,
- für die ausgewählte Station im Bereich **Further Info** und
- als kompakte Liste der beiden derzeit höchstbewerteten Kandidaten mit einem zusätzlichen Fenster für bis zu 15 Kandidaten.
Bedienung: [Prioritätsliste in der Benutzeroberfläche](de-Benutzeroberflaeche#prioritätsliste).
### Was sagt der Score nicht aus?
Der Zahlenwert ist weder eine Erfolgswahrscheinlichkeit noch eine Signalprognose. Ein doppelt so hoher Score bedeutet nicht, dass ein QSO doppelt so wahrscheinlich ist.
Die Berechnung kennt unter anderem nicht:
- die tatsächliche Antennenrichtung der Gegenstation,
- deren momentane Betriebssituation,
- lokale Störungen,
- kurzfristige Ausbreitungsänderungen,
- Geländeabschattungen außerhalb der jeweils angebundenen Funktionen oder
- die Frage, ob eine im Chat aktive Station tatsächlich gerade am Funkgerät sitzt.
Auch bekannte Eingabedaten können veraltet oder missverständlich sein. Eine erkannte Frequenz belegt beispielsweise nur, dass diese QRG kürzlich im Zusammenhang mit der Station aufgetreten ist.
Im Klartext: Der Score ersetzt nicht die Entscheidung des Operators. Er sorgt dafür, dass die dafür bereits vorhandenen Informationen nicht bei jedem Kandidaten erneut im Kopf zusammengesucht werden müssen.
Zugehörige Einstellungen:
- [Aktivierte Bänder](de-Konfiguration#aktivierte-bänder)
- [Antennen-Öffnungswinkel](de-Konfiguration#antennen-öffnungswinkel-antenna-beamwidth)
- [Standard-Maximum-QRB](de-Konfiguration#standard-maximum-qrb)
- [AirScout-Einstellungen](de-Konfiguration#airscout-einstellungen)
- [Band-Upgrade-Hinweis und Priority Boost](de-Konfiguration#band-upgrade-hinweis-nach-einem-logeintrag)
---
## AP-Timeline (ab v1.40)
## AP- und Sked-Timeline
Eine visuelle Zeitleiste zeigt für jeden möglichen AP-Ankunftsminuten-Slot bis zu 4 hochbewertete Stationen, die per Aircraft Scatter erreichbar wären. Priorisierungskriterien:
Die Timeline stellt bevorstehende Aircraft-Scatter-Gelegenheiten und eingetragene Skeds für die nächsten 30 Minuten gemeinsam dar. Sie beantwortet damit zwei Fragen auf einen Blick:
- Bevorzugt werden APs mit dem **höchsten Reflexionspotenzial** (nicht unbedingt die schnellste Ankunft).
- Stationen, auf die die eigene Antenne nicht zeigt, werden **transparent** dargestellt.
- Wann entsteht voraussichtlich eine interessante AP-Gelegenheit?
- Welcher bereits vereinbarte Sked nähert sich unabhängig davon?
So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und über welche Flugzeuge erreichbar sein werden.
Weiter in der Zukunft liegende Ereignisse erscheinen rechts. Mit ablaufender Zeit wandern sie nach links in Richtung des aktuellen Zeitpunkts.
---
![AP-Kandidaten und Skeds in der Timeline](sked_timeline.png)
### AP-Kandidaten
AP-Kandidaten erscheinen in den oberen Spuren. Pro Ankunftsminute können bis zu vier ausgewählte Kandidaten dargestellt werden. Die Auswahl berücksichtigt den Priority Score und das von AirScout gemeldete Reflexionspotenzial.
Die Farbe des AP-Symbols kennzeichnet das Reflexionspotenzial:
| Farbe | Reflexionspotenzial |
|---|---:|
| Magenta | mindestens 95 % |
| Rot | mindestens 75 % |
| Gelb | mindestens 50 % |
| Blau | unter 50 % |
Die Farbe ist keine QSO-Wahrscheinlichkeit. Sie gibt den von AirScout übernommenen Wert für die berechnete Reflexionsgeometrie wieder.
Ein Klick auf einen AP-Kandidaten wählt den dazugehörigen aktiven Chatmember einschließlich Rufzeichensuffix und Chat-Kategorie aus. Dadurch kann unmittelbar eine passende Nachricht vorbereitet werden.
### Skeds
Skeds erscheinen als Rauten in der unteren Spur. Die Beschriftung verwendet das vollständige KST-Rufzeichen, beispielsweise `SKED: DN9APW-2`. Dadurch bleibt erkennbar, welcher konkrete Login für den Termin ausgewählt wurde.
Der Tooltip eines Skeds zeigt mindestens:
- das vollständige KST-Rufzeichen,
- das vereinbarte Band und
- den QTF zur Gegenstation.
Sind passende AirScout-Daten vorhanden, werden zusätzlich die aktuelle AP-Erreichbarkeit und die nächste berechnete AP-Gelegenheit angezeigt.
### Berücksichtigung der Antennenrichtung
Liegt der QTF eines Ereignisses deutlich außerhalb der aktuellen Antennenrichtung, wird dessen Symbol transparenter dargestellt. Die Beschriftung bleibt lesbar. Liegt das Ziel nahe der Mitte des konfigurierten Antennenbereichs, wird das Symbol zusätzlich hervorgehoben.
Diese Darstellung verändert weder den Sked noch den Priority Score. Sie ist eine optische Hilfe, um Kandidaten in der aktuellen Antennenrichtung schneller zu erkennen.
Die Timeline ist eine Vorschau. AirScout-Daten können sich ändern, und ein eingetragener Sked garantiert weder eine freie Frequenz noch eine tatsächlich vorhandene Ausbreitungsverbindung.
## Intervall-Beacon
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Globale Variablen wie `MYQRG`, `SECONDQRG` oder `MYLOCATOR` werden unmittelbar vor jeder Aussendung aktualisiert.
Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim Absuchen oder häufigen Wechseln der QRG sollte er ausgeschaltet werden, damit keine inzwischen falsche Frequenz verbreitet wird. Details: [Konfiguration Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
---
## Simplelogfile
Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
Details: [Log-Synchronisation](de-Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Cluster & QSO der anderen
## Globale Nachrichtenansichten
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
Das Stationsinfo-Panel und die PM-Tabelle beantworten Fragen zu einer bestimmten Station oder zur eigenen Kommunikation. Daneben gibt es Nachrichtenströme, die unabhängig von der aktuell ausgewählten Station betrachtet werden müssen.
Dieses Fenster kann miniaturisiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
KST4Contest fasst diese globalen Informationen in drei Ansichten zusammen:
| Ansicht | Inhalt |
|---|---|
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
| **DXCluster messages** | Vom ON4KST-Server gelieferte DX-Cluster-Meldungen |
| **QSO of the other** | Gerichtete Chatnachrichten zwischen zwei anderen Stationen |
Die Ansichten befinden sich als Tabs im unteren Bereich des Hauptfensters. **Public messages** ist nach dem Programmstart vorausgewählt.
![Globale Nachrichtentabs im Hauptfenster](global_message_tabs.png)
### DXCluster messages
Der Tab **DXCluster messages** zeigt DX-Cluster-Meldungen, die über die bestehende ON4KST-Verbindung empfangen werden. Je nach Inhalt der Meldung stehen folgende Informationen zur Verfügung:
- Zeitpunkt,
- sendende beziehungsweise meldende Station,
- deren Locator,
- gemeldete Station,
- deren Locator,
- QRG,
- Meldungstext und
- globaler Worked-Status der gemeldeten Station.
Nicht jede vom Server übertragene Meldung enthält alle Felder. Ein leeres Locator- oder Nachrichtenfeld bedeutet deshalb nicht zwangsläufig einen Verarbeitungsfehler.
Diese Anzeige darf nicht mit dem [integrierten lokalen DX-Cluster-Server](de-DX-Cluster-Server) verwechselt werden. Der Tab zeigt empfangene ON4KST-Clusterinformationen. Der lokale Server erzeugt dagegen aus einer erkannten Richtungsgelegenheit einen Spot und gibt ihn an ein verbundenes Logprogramm weiter.
### QSO of the other
Der Tab **QSO of the other** zeigt gerichtete Chatnachrichten, bei denen weder Absender noch Empfänger die eigene Station sind. Öffentliche Nachrichten an `ALL` werden nicht aufgenommen.
Die Tabelle enthält:
| Spalte | Bedeutung |
|---|---|
| **Time** | Zeitpunkt der Chatnachricht |
| **Call TX** | Absender der Nachricht |
| **Last QRG TX** | zuletzt für den Absender bekannte QRG |
| **wkd TX?** | globaler Worked-Status des Absenders |
| **Call RX** | Empfänger der Nachricht |
| **Last QRG RX** | zuletzt für den Empfänger bekannte QRG |
| **wkd RX?** | globaler Worked-Status des Empfängers |
| **Message** | Inhalt der gerichteten Nachricht |
| **Category** | Chat-Kategorie der Nachricht |
Die beiden QRG-Spalten zeigen den zuletzt in KST4Contest bekannten Wert der jeweiligen Station. Das ist nicht zwangsläufig die Frequenz, auf der sich die beiden Stationen gerade verabreden. Die QRG kann aus einer früheren Nachricht stammen und sich inzwischen geändert haben.
Auch die Worked-Spalten sind bewusst bandunabhängig. Ein `X` bedeutet, dass das betreffende Basisrufzeichen auf mindestens einem Band gearbeitet wurde. Daraus folgt nicht, dass es auf der in der Tabelle sichtbaren oder vermuteten QRG bereits gearbeitet wurde.
Die Bezeichnung **QSO of the other** ist eine praktische Kurzform. Eine gerichtete Nachricht beweist weder, dass anschließend ein Funkkontakt zustande kam, noch dass beide Stationen tatsächlich auf derselben Frequenz arbeiten. Die Ansicht zeigt beobachtbare Koordination im Chat nicht das Logbuch der anderen Stationen.
### Zusätzliches Monitorfenster
Dieselben DX-Cluster-Meldungen und gerichteten Nachrichten stehen weiterhin im separaten Fenster **Cluster & QSO of the other** zur Verfügung. Dort erscheinen die DX-Cluster-Tabelle oben und die Nachrichten zwischen anderen Stationen darunter.
![Separates Cluster- und QSO-Monitorfenster](cluster_qso_monitor.png)
Das separate Fenster und die Tabs verwenden dieselben zugrunde liegenden Listen. Eine Meldung wird dadurch nicht doppelt empfangen oder doppelt gespeichert. Es handelt sich lediglich um zwei Darstellungen derselben Daten.
Das Monitorfenster kann über **Windows → Hide cluster / stranger QSOs** ausgeblendet und mit **Show cluster / stranger QSOs** wieder eingeblendet werden. Wer den Platz nicht benötigt, kann das Fenster daher schließen oder minimieren, ohne auf die entsprechenden Tabs im Hauptfenster verzichten zu müssen.
Die vollständigen Texte abgeschnittener Nachrichten erscheinen als Tooltip. Erkannte Webadressen können wie in den übrigen Nachrichtentabellen angeklickt und im Standardbrowser geöffnet werden.
Die Ansichten helfen dabei, Aktivität und Koordination anderer Stationen zu erkennen. Bei hohem Chat-Aufkommen entsteht daraus allerdings schnell mehr Information als Erkenntnis. Das separate Fenster ist deshalb vor allem dann nützlich, wenn ein bestimmter Kommunikationsfluss gezielt beobachtet werden soll.
---
## Stationskarte (ab v1.41)
## Stationskarte und Streckenanalyse (ab v1.41)
Eine interaktive OpenStreetMap-Karte zeigt die geografische Position aller aktiven Chatmember.
Eine lange Benutzerliste beantwortet zwei geografische Fragen nur unzureichend: Wo befinden sich die eingeloggten Stationen, und welche davon liegen ungefähr in der aktuellen Antennenrichtung? Die Stationskarte überträgt deshalb die bereits bekannten Locator-, Richtungs-, Band- und Worked-Informationen in eine interaktive Kartenansicht.
**Funktionen:**
Die Karte ist keine zweite, unabhängig verwaltete Stationsliste. Sie verwendet die aktuell durch die Filter der Benutzerliste sichtbaren Chatmember. Wird beispielsweise nach Entfernung, Richtung, Worked-Status oder einem bestimmten Band gefiltert, wirkt sich dies auch auf die dargestellten Stationen aus. In der Kopfzeile der Karte wird angezeigt, wie viele Stationen sichtbar sind und ob eine gefilterte Ansicht aktiv ist.
- Stationsmarker mit Rufzeichen-Labels, farblich nach Aktivität und Sked-Status
- **Antennen-Kegel** für die eigene Station
- **Verbindungslinie** zur aktuell ausgewählten Station
- **Maidenhead-Raster** (QRA-Locator-Gitter als Overlay)
- **Wegprofil-Diagramm**: Geländehöhen-Querschnitt zwischen eigener und ausgewählter Station, inklusive Fresnel-Zonen-Analyse und Horizonterkennung
- Mehrere Terrainquellen: **Copernicus GLO-30** (hochauflösendes DEM), **Open-Meteo API**, synthetischer Fallback und **Offline-DEM-Import** für den Betrieb ohne Internetverbindung
- Aircraft-Scatter-Weganalyse verknüpft mit den Geländedaten
![Stationskarte mit ausgewählter Station und eingeblendeter Streckenanalyse](station_map_path_analysis.png)
Die Karte funktioniert in gepackten Umgebungen (AppImage, Flatpak) ohne Zugriff auf externe CDNs: Die Kartenkacheln werden über einen lokalen Tile-Proxy abgerufen, die Leaflet.js-Bibliothek ist in der Anwendung eingebettet.
### Welche Stationen werden dargestellt?
Für einen Kartenmarker benötigt KST4Contest einen brauchbaren sechsstelligen Locator. Chatmember ohne einen solchen Locator können in der Benutzerliste vorhanden sein, erscheinen aber nicht auf der Karte.
Mehrere aktive Chat-Einträge desselben Basisrufzeichens werden für die Kartenansicht zusammengefasst. Das verhindert, dass beispielsweise getrennte Logins in mehreren Chat-Kategorien mehrere Marker an derselben geografischen Position erzeugen. Als sichtbares Rufzeichen und für die Detailinformationen wird die zuletzt geeignete aktive Variante verwendet.
Die Beschriftung eines Markers kann zusätzlich enthalten:
- die für die Station erkannten aktiven Bänder,
- `B+`, wenn mindestens ein eigenes aktiviertes und noch nicht gearbeitetes Band angeboten wird.
Die Bandangaben verwenden dieselbe Herleitung wie die Bandspalten, der Filter **New bands** und der Priority Score. Aktuelle QRG-Erkennungen, Bandangaben im Namensfeld, Worked-Informationen und manuelle NOT-QRV-Markierungen werden daher auch in der Kartenansicht konsistent berücksichtigt.
### Bedeutung der Markerfarben
| Darstellung | Bedeutung |
|---|---|
| Blauer Rand | Station ohne eine der nachfolgenden besonderen Markierungen |
| Gelber Rand | Das Basisrufzeichen wurde bereits auf mindestens einem Band gearbeitet |
| Grün | Für die Station besteht eine aus gerichteten Chatnachrichten hergeleitete Richtungsgelegenheit |
| Orange | Aktuell ausgewählte Station |
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.
### Auswahl und geografische Hilfen
Ein Klick auf einen einzelnen Stationsmarker:
1. wählt den dazugehörigen aktiven Chatmember aus,
2. scrollt die Benutzerliste zu diesem Eintrag,
3. aktualisiert den **Further Info**-Bereich und
4. bereitet das Rufzeichen wie bei einer Auswahl in der Benutzerliste als Nachrichtenziel vor.
Für die ausgewählte Station zeichnet KST4Contest eine Verbindungslinie von der eigenen Station zum Ziel. Der eingezeichnete Antennensektor verwendet:
- den aktuellen eigenen QTF,
- den konfigurierten Antennen-Öffnungswinkel und
- das konfigurierte Standard-Maximum-QRB.
Das Maidenhead-Raster passt seine Genauigkeit an die Zoomstufe und den sichtbaren Kartenausschnitt an. Es dient der räumlichen Orientierung; die Position eines Stationsmarkers wird aus dem sechsstelligen Locator abgeleitet und ist deshalb keine exakte GPS-Position.
### Strecken- und Geländeprofil
Nach Auswahl einer Station fordert KST4Contest ein Höhenprofil zwischen dem eigenen und dem fremden Locator an. Die aktive Online-Datenquelle ist die Open-Meteo Elevation API mit Geländedaten auf Basis von **Copernicus GLO-90**.
Die Online-Abfrage ist auf höchstens 100 gleichmäßig über die Strecke verteilte Höhenpunkte begrenzt. Eine 100 Kilometer lange Strecke wird damit grob im Abstand von etwa einem Kilometer abgetastet. Bei kürzeren Strecken wird der Abstand entsprechend kleiner, schmale Hindernisse können trotzdem zwischen zwei Abfragepunkten liegen.
Aus den Höhenpunkten berechnet KST4Contest unter anderem:
- das Geländeprofil,
- die geometrische Sichtlinie,
- die Erdkrümmung mit einem festen effektiven Erdradiusfaktor von `k = 4/3`,
- den geometrischen Radiohorizont beider Stationen,
- relevante Geländehorizonte,
- die erste Fresnel-Zone,
- die geringste Fresnel-Freiheit,
- den stärksten erkannten Eingriff in die Fresnel-Zone und
- eine grobe Einzelhindernis- beziehungsweise Knife-Edge-Abschätzung.
Die eingestellte eigene Antennenhöhe wird zur lokalen Geländehöhe addiert. Für die Gegenstation wird derzeit eine feste angenommene Antennenhöhe von 10 Metern über Grund verwendet.
Bewegst du die Maus über das Profil, wird der dazugehörige Abfragepunkt zusätzlich auf der Karte markiert. Dadurch lässt sich ein auffälliger Berg oder Geländeeinschnitt leichter einer geografischen Position zuordnen.
### Welche Frequenz wird für die Berechnung verwendet?
Die Frequenz beeinflusst insbesondere die Größe der Fresnel-Zone, die Freiraumdämpfung und das Link-Budget. KST4Contest versucht deshalb, für die ausgewählte Station eine passende Analysefrequenz zu bestimmen.
Vorrangig wird eine aktuell bekannte QRG auf einem eigenen aktivierten und für die Gegenstation nutzbaren Band verwendet. Fehlt eine geeignete QRG, wird aus den vorhandenen Bandinformationen ein automatisches Analyseband hergeleitet und dessen Standardfrequenz verwendet. Manuelle NOT-QRV-Markierungen werden dabei berücksichtigt.
Die tatsächlich verwendete Frequenz steht im Feld **Frequency** der Streckenanalyse. Sie sollte kontrolliert werden, wenn die automatische Zuordnung nicht zur vorgesehenen Funkverbindung passt. Eine Berechnung auf 144 MHz ist für eine geplante Verbindung auf 1296 MHz keine gleichwertige Näherung.
### Link-Budget und Tropo-Spalte
Zusätzlich zur geometrischen Bewertung erstellt KST4Contest eine vereinfachte Link-Budget-Abschätzung. Verwendet werden:
- die konfigurierte eigene Sendeleistung,
- der eigene Antennengewinn,
- die angenommene Sendeleistung der Gegenstation,
- der angenommene Antennengewinn der Gegenstation,
- eine frequenzabhängig geschätzte Speiseleitungsdämpfung,
- die Freiraumdämpfung und
- gegebenenfalls eine grobe zusätzliche Hindernisdämpfung.
Antennengewinne werden in `dBi` eingegeben. Ein in `dBd` bekannter Wert muss deshalb vor der Eingabe um `2,15 dB` erhöht werden.
Die Berechnung betrachtet beide Übertragungsrichtungen. Der daraus abgeleitete ungünstigere SSB-Wert wird als Tropo-Marge für die Station gespeichert und kann anschließend in der **Tropo**-Spalte, beim Sortieren und durch den Filter **Tropo >=0dB** verwendet werden.
Karte und Benutzerliste führen keine voneinander unabhängigen Berechnungen durch. Sie verwenden denselben Reachability-Service und denselben Ergebnisspeicher. Eine über die Karte oder mit **Calc selected** angestoßene Berechnung kann deshalb anschließend auch in der Benutzerliste erscheinen.
Es wird absichtlich keine automatische Online-Geländeabfrage für jeden sichtbaren Chatmember gestartet. Eine Berechnung erfolgt durch eine ausdrückliche Auswahl in der Karte oder über **Calc selected**. Das begrenzt API-Anfragen und verhindert, dass jede Tabellenaktualisierung eine neue Serie von Höhenabfragen auslöst.
### Pfadanalyse ausblenden
Das Geländeprofil und die ausführliche Analyse benötigen einen erheblichen Teil der Fensterhöhe. Werden sie gerade nicht gebraucht, können beide Bereiche gemeinsam mit **Hide path analysis** ausgeblendet werden. Die Karte nutzt den frei werdenden Platz unmittelbar.
![Stationskarte mit ausgeblendeter Pfadanalyse](station_map_compact.png)
Mit **Show path analysis** werden Profil und Detailwerte wieder eingeblendet. Das zuletzt berechnete Ergebnis bleibt erhalten. Die gewählte Sichtbarkeit wird in den Einstellungen gespeichert und beim nächsten Programmstart wiederhergestellt.
Der rechte Detailbereich ist über einen Divider in der Breite verstellbar. Er kann so weit verkleinert werden, dass mehr Platz für die Karte entsteht, ohne ein Rufzeichen mit üblicher Länge vollständig zu verdecken.
### Was sagt die Streckenanalyse nicht aus?
Die Auswertung ist ein geometrisches und rechnerisches Modell. Sie misst weder die tatsächliche Feldstärke noch die momentanen Ausbreitungsbedingungen.
Insbesondere kennt KST4Contest nicht:
- die wirkliche Antennenhöhe und den tatsächlichen Antennengewinn der Gegenstation,
- deren Sendeleistung und Speiseleitungsverluste,
- Gebäude, Bewuchs und andere Hindernisse, die im Höhenmodell nicht enthalten sind,
- lokale Störungen und Empfängereigenschaften,
- den aktuellen atmosphärischen K-Faktor,
- Inversionsschichten oder Ducting und
- die tatsächliche Antennenrichtung der Gegenstation.
Die unter **Mechanisms** genannten Ausbreitungswege sind eine allgemeine Einordnung anhand der Geländegeometrie. Eine Nennung von Aircraft Scatter bedeutet nicht, dass aktuelle Flugzeuge aus AirScout in die Geländeanalyse eingerechnet wurden.
Im Klartext: Ein freier Weg ist ein nützlicher positiver Hinweis. Ein geometrisch versperrter Weg bedeutet im VHF-, UHF- und Mikrowellenbereich aber nicht automatisch „unmöglich“. Ebenso garantiert ein positives Link-Budget kein QSO.
Bedienung: [Stationskarte in der Benutzeroberfläche](de-Benutzeroberflaeche#stationskarte)
Konfiguration: [Streckenanalyse und Link-Budget](de-Konfiguration#streckenanalyse-und-link-budget)
---
## Optimierte Nachrichtenverarbeitung / 30.000-Nachrichten-Limit (ab v1.41)
## Begrenzte Nachrichtenspeicher (ab v1.41)
Die internen Chat- und Nachrichtentabellen sind auf **30.000 Einträge** begrenzt. Ältere Nachrichten werden automatisch verworfen, sobald das Limit erreicht wird. Damit bleiben Speicherverbrauch und Darstellungsperformance auch bei mehrtägigen Contest-Betrieb stabil.
Während eines längeren Contests können mehrere zehntausend Chat- und DX-Cluster-Meldungen eintreffen. Würden diese Listen während der gesamten Programmlaufzeit unbegrenzt wachsen, stiege nicht nur der Speicherverbrauch. Auch das Filtern, Sortieren und Aktualisieren der darauf aufbauenden Tabellen würde zunehmend aufwendiger.
KST4Contest verwendet deshalb zwei getrennte, begrenzte Nachrichtenspeicher:
| Nachrichtenspeicher | Aufräumen ab | Größe nach dem Aufräumen |
|---|---:|---:|
| Chatnachrichten | mehr als 30.000 Einträge | 25.000 Einträge |
| DX-Cluster-Meldungen | mehr als 10.000 Einträge | 8.000 Einträge |
Neue Nachrichten werden am Anfang der jeweiligen Liste eingefügt. Wird der obere Grenzwert überschritten, entfernt KST4Contest die ältesten Einträge am Ende der Liste, bis die angegebene Zielgröße erreicht ist.
### Warum gibt es zwei Grenzwerte?
Der Speicher wird nicht nach jeder einzelnen Nachricht wieder exakt auf seine Maximalgröße verkleinert. Nach dem Aufräumen bleiben bei den Chatnachrichten 5.000 und bei den DX-Cluster-Meldungen 2.000 freie Plätze.
Dadurch muss KST4Contest nicht für jede anschließend eintreffende Nachricht erneut Listeneinträge entfernen. Das Aufräumen erfolgt blockweise und damit deutlich seltener.
### Welche Tabellen teilen sich einen Speicher?
Die folgenden Ansichten sind gefilterte Darstellungen derselben globalen Chatnachrichtenliste:
- **Public messages**,
- die PM-Tabelle,
- die Nachrichten im Bereich **Further Info** und
- **QSO of the other**.
Diese Tabellen speichern nicht jeweils zusätzlich bis zu 30.000 Nachrichten. Wird eine alte Chatnachricht aus dem gemeinsamen Speicher entfernt, verschwindet sie gleichzeitig aus allen darauf basierenden Ansichten.
Ebenso verwenden der Tab **DXCluster messages** und die DX-Cluster-Tabelle im separaten Monitorfenster denselben Cluster-Speicher. Das zusätzliche Fenster erzeugt weder eine zweite Nachrichtenverbindung noch eine Kopie der empfangenen Meldungen.
Chatnachrichten und DX-Cluster-Meldungen besitzen dagegen voneinander unabhängige Speicher und Grenzwerte. Ein hohes Aufkommen an öffentlichen Chatnachrichten verkleinert deshalb nicht den DX-Cluster-Speicher und umgekehrt.
### Keine dauerhafte Historie
Beide Nachrichtenspeicher liegen ausschließlich im Arbeitsspeicher. Sie werden weder in die interne Worked-Datenbank noch in eine andere lokale Nachrichtendatei geschrieben.
Nach einem Neustart beginnen die Tabellen wieder mit leeren Listen und werden ausschließlich aus den neu empfangenen Meldungen aufgebaut. Die Ansichten sind damit ein Arbeitsmittel für die laufende Sitzung und kein dauerhaftes Chatarchiv.
---
## Bildschirmgerechte Fenstergröße (ab v1.41)
## Bildschirmgerechte Größe des Hauptfensters (ab v1.41)
Beim Programmstart berechnet KST4Contest eine bildschirmgerechte Startgröße für das Hauptfenster:
KST4Contest speichert die zuletzt verwendete Größe des Hauptfensters. Das ist praktisch, solange das Programm beim nächsten Start auf einem vergleichbaren Bildschirm läuft. Wurde die Anwendung zuvor auf einem größeren Monitor verwendet, kann die gespeicherte Größe auf einem kleineren Bildschirm jedoch außerhalb des sichtbaren Bereichs liegen.
- Die gespeicherte Fenstergröße aus der letzten Session wird verwendet aber **niemals größer als der aktuelle Bildschirm**.
- Wenn KST4Contest zuletzt auf einem größeren Monitor betrieben wurde, wird das Fenster automatisch auf die aktuelle Anzeige verkleinert.
- Das UI-Layout ist **kompakter und reaktionsfähiger auf kleineren Bildschirmen**.
KST4Contest prüft die gespeicherte Größe deshalb beim Programmstart gegen den nutzbaren Bereich des primären Bildschirms.
Damit werden unbrauchbare, abgeschnittene Fenster beim Wechsel zwischen Geräten oder Monitoren verhindert.
### Wie wird die Startgröße bestimmt?
Sofern die gespeicherten Werte gültig sind, verwendet KST4Contest zunächst die zuletzt gespeicherte Höhe und Breite. Fehlen brauchbare Werte, gilt eine Standardgröße von:
- 1.234 Pixel Breite und
- 768 Pixel Höhe.
Als verfügbare Fläche verwendet KST4Contest nicht die vollständige Bildschirmauflösung, sondern den von JavaFX gemeldeten sichtbaren Bereich des primären Bildschirms. Taskleiste, Dock und vergleichbare Bereiche des Betriebssystems sind darin bereits ausgenommen.
Von dieser Fläche wird zusätzlich ein Sicherheitsabstand von 40 Pixeln abgezogen. Überschreitet die gespeicherte Breite oder Höhe den verbleibenden Platz, wird nur der betreffende Wert verkleinert.
Nachdem die Oberfläche mit dieser Scene-Größe aufgebaut wurde, prüft KST4Contest zusätzlich das tatsächliche native Fenster einschließlich seiner vom Betriebssystem erzeugten Rahmen und Titelleiste. Das Fenster wird bei Bedarf noch einmal verkleinert oder in den sichtbaren Bereich verschoben.
Damit werden zwei unterschiedliche Fälle abgefangen:
1. Die gespeicherte Inhaltsfläche ist größer als der aktuelle Bildschirm.
2. Die Inhaltsfläche passt, das vollständige native Fenster ragt durch Rahmen oder Position trotzdem über den sichtbaren Bereich hinaus.
### Was passiert mit dem Layout?
Die Oberfläche wird nicht als Ganzes proportional skaliert. Stattdessen erhält das Hauptfenster weniger Platz, und die dafür vorgesehenen UI-Bereiche reagieren auf die verfügbare Breite.
Die Filterleiste bleibt bei normaler Fensterbreite kompakt. Erst wenn der tatsächlich benötigte Platz nicht mehr ausreicht, werden Bedienelemente in zusätzliche Zeilen umgebrochen. Divider können weiterhin verwendet werden, um den Platz zwischen den Nachrichten- und Stationsbereichen aufzuteilen.
### Grenzen der automatischen Korrektur
Die Prüfung verwendet immer den **primären Bildschirm**. Sie stellt nicht die frühere Position auf einem bestimmten sekundären Monitor wieder her.
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.
+20 -9
View File
@@ -50,15 +50,15 @@ Download, unterstützte Betriebssysteme und Installationswege sind im Kapitel [I
## Versionsstand dieses Handbuchs
Dieses Handbuch beschreibt die stabile Version **v1.41.1**.
Dieses Handbuch unterscheidet zwischen der veröffentlichten Stable-Version und dem aktuellen Entwicklungsstand.
Funktionen oder Änderungen, die nur im aktuellen Entwicklungsstand enthalten sind, werden ausdrücklich als **Nightly / v1.42** gekennzeichnet. Fehlt eine solche Kennzeichnung, bezieht sich die Beschreibung auf die stabile Version.
Die derzeit veröffentlichte Stable-Version ist **v1.41.1**. Funktionen oder Änderungen, die erst im Entwicklungsstand für v1.42 enthalten sind, werden ausdrücklich als **Nightly / v1.42** gekennzeichnet. Fehlt eine solche Kennzeichnung, bezieht sich die Beschreibung auf die Stable-Version.
- [Aktuelle stabile Version herunterladen](https://github.com/praktimarc/kst4contest/releases/latest)
- [Nightly-Builds und automatisierte Builds](https://github.com/praktimarc/kst4contest/actions)
- [Versionsgeschichte](de-Changelog)
- [Stable, Beta und Nightly herunterladen](https://kst4contest.hamradioonline.de/download/)
- [Veröffentlichte GitHub Releases](https://github.com/praktimarc/kst4contest/releases)
- [Versionsgeschichte und aktueller Nightly-Stand](de-Changelog)
Für einen Contest ist grundsätzlich die stabile Version zu empfehlen. Nightly-Builds enthalten neuere Korrekturen und Funktionen, können sich aber zwischen zwei Builds verändern. Sie sind sinnvoll, wenn eine bestimmte Änderung getestet werden soll weniger sinnvoll ist der erste Versuch zehn Minuten vor Contestbeginn.
Für einen Contest ist grundsätzlich die Stable-Version zu empfehlen. Nightly-Builds enthalten neuere Korrekturen und Funktionen, können sich aber zwischen zwei Builds verändern. Sie sind sinnvoll, wenn eine konkrete Änderung getestet werden soll. Der erste Versuch zehn Minuten vor Contestbeginn ist dagegen meistens eine recht effiziente Methode, gleichzeitig Software und Operator zu testen.
---
@@ -80,7 +80,7 @@ Für einen Contest ist grundsätzlich die stabile Version zu empfehlen. Nightly-
## Kontakt und Support
- **Download:** [Aktuelle stabile Version](https://github.com/praktimarc/kst4contest/releases/latest)
- **Download:** [Stable, Beta und Nightly](https://kst4contest.hamradioonline.de/download/)
- **Quellcode:** [praktimarc/kst4contest](https://github.com/praktimarc/kst4contest)
- **Fehler und Funktionswünsche:** [GitHub Issues](https://github.com/praktimarc/kst4contest/issues)
- **E-Mail:** praktimarc+kst4contest@gmail.com
@@ -112,6 +112,17 @@ In Datei- und Verzeichnisnamen wird teilweise noch der technische Name `praktiKS
## Danksagungen
KST4Contest wurde durch Rückmeldungen aus dem praktischen Contest-Betrieb wesentlich verbessert.
KST4Contest wurde durch Rückmeldungen aus dem praktischen Contest-Betrieb wesentlich verbessert. Viele Funktionen entstanden nicht aus einer theoretischen Anforderungsliste, sondern aus Situationen, in denen während eines Contests eine Information fehlte, zu spät sichtbar wurde oder schlicht an der falschen Stelle stand.
Besonderer Dank gilt Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), Viliam Petrik (OM0AAO, Idee zum DX-Cluster), Konrad Neitzel (DC9DJ, Projektstruktur), Andreas (DO5ALF, Webmaster von funkerportal.de), Franz van Velzen (PE0WGA, Tester), DN9APW als neuem Entwickler im Team und Master über die CI/CD pipelines sowie allen weiteren Testern und Ideengebern.
Besonderer Dank gilt:
- Gianluca Costantino (IU3OAR),
- Alessandro Murador (IZ3VTH),
- Reczetár István (HA1FV),
- Viliam Petrik (OM0AAO) für die Idee zum integrierten DX-Cluster,
- Konrad Neitzel (DC9DJ) für Unterstützung bei der Projektstruktur,
- Andreas (DO5ALF), Webmaster von funkerportal.de,
- Franz van Velzen (PE0WGA) für Tests sowie
- DN9APW (Philipp Wagner), der seit Mai 2025 an der Entwicklung und den CI/CD-Pipelines mitarbeitet.
Ebenso wichtig sind die Fehlermeldungen, Tests und konkreten Betriebserfahrungen aller weiteren Nutzer. Eine genaue Beschreibung eines reproduzierbaren Problems hilft dem Projekt meistens mehr als ein allgemeines „läuft gut“ auch wenn Letzteres natürlich angenehmer zu lesen ist.
+77 -17
View File
@@ -62,17 +62,46 @@ Der Wert sollte zum eigenen Stationsaufbau und zum vorgesehenen Contestbetrieb p
---
## Server-Einstellungen (ab v1.31)
### Streckenanalyse und Link-Budget
Der Chat-Server-DNS und -Port sind in den Preferences konfigurierbar:
Die Stationskarte verwendet mehrere Werte aus dem Reiter **Station**, um das Geländeprofil und das Link-Budget zur ausgewählten Gegenstation zu berechnen. Die Angaben zum eigenen Stationsaufbau sollten deshalb möglichst realistisch sein. Bei der Gegenstation handelt es sich dagegen um globale Annahmen, solange keine genaueren Daten vorliegen.
- **Server-DNS**: Standard `www.on4kst.org` (ab v1.31 geändert von `www.on4kst.info`).
- **Port**: Standardport des ON4KST-Servers.
| Einstellung | Verwendung |
|---|---|
| **Own antenna height AGL** | Höhe der eigenen Antenne über dem lokalen Gelände in Metern |
| **Own TX power W** | Eigene Sendeleistung in Watt |
| **Own ant. gain dBi** | Gewinn der eigenen Antenne in dBi |
| **DX OM TX power W** | Angenommene Sendeleistung der Gegenstation in Watt |
| **DX OM ant. gain dBi** | Angenommener Antennengewinn der Gegenstation in dBi |
Eine Änderung ist nur notwendig, wenn der Server umzieht oder ein alternativer Endpunkt genutzt wird.
**AGL** bedeutet *Above Ground Level*. Trage hier nicht die Höhe über dem Meeresspiegel ein. Die Geländehöhe am eigenen Standort stammt bereits aus dem Höhenprofil; KST4Contest addiert die konfigurierte Antennenhöhe zu diesem Wert.
Für die Gegenstation verwendet KST4Contest derzeit eine feste Antennenhöhe von 10 Metern über dem lokalen Gelände. Sendeleistung und Antennengewinn der Gegenstation stammen aus den beiden **DX OM**-Feldern. Diese Werte sind bewusst nur Annahmen: Der ON4KST-Chat überträgt weder die tatsächliche Antennenhöhe noch die vollständigen Stationsdaten der Gegenstation.
Antennengewinne müssen in `dBi` eingetragen werden. Liegt ein Wert in `dBd` vor, muss vor der Eingabe `2.15 dB` addiert werden:
```text
dBi = dBd + 2.15 dB
```
Die aktuelle QTF, der konfigurierte Antennen-Öffnungswinkel und das Standard-Maximum-QRB beeinflussen die Darstellung der Stationskarte. Die für die eigene Station aktivierten Bänder und die für die Gegenstation hergeleitete Frequenz wirken sich zusätzlich auf die Auswahl der Analysefrequenz aus.
Für das Geländeprofil berücksichtigt KST4Contest die Antennenhöhen, das Höhenmodell und die Erdkrümmung mit einem festen effektiven Erdradiusfaktor von `k = 4/3`. Das Link-Budget verwendet außerdem:
- die Entfernung und die aktuelle Analysefrequenz,
- die Sendeleistungen und Antennengewinne beider Stationen,
- frequenzabhängig geschätzte Speiseleitungsverluste,
- die Freiraumdämpfung und
- eine grobe zusätzliche Dämpfung durch das maßgebliche Hindernis im Geländeprofil.
Die Berechnung erfolgt für beide Übertragungsrichtungen. Für die gemeinsame SSB- beziehungsweise CW-Marge ist die ungünstigere Richtung maßgeblich. Zu optimistische Leistungs- oder Antennenwerte verbessern daher zwar das angezeigte Ergebnis, nicht aber den realen Funkweg.
Die Werte bleiben technische Abschätzungen. Aktuelle Ausbreitungsbedingungen, lokale Abschattungen, Bewuchs, Gebäude, Störungen und nicht bekannte Stationsparameter können das tatsächliche Ergebnis deutlich verändern. Bedienung, Frequenzauswahl und Grenzen der Berechnung sind unter [Stationskarte und Streckenanalyse](de-Funktionen#stationskarte-und-streckenanalyse-ab-v141) beschrieben.
---
## Log-Sync-Einstellungen
Drei Methoden stehen zur Verfügung, um gearbeitete Stationen automatisch zu markieren. Details: [Log-Synchronisation](de-Log-Synchronisation).
@@ -186,7 +215,13 @@ Bleibt mindestens ein gemeinsames, noch nicht gearbeitetes Band übrig, erschein
Die beiden Optionen haben unterschiedliche Aufgaben:
- **Blink + sound …** aktiviert den Hinweis nach einem passenden Logeintrag.
- **Priority boost …** erhöht zusätzlich die Priorität von Stationen mit einer offenen Bandmöglichkeit. Der Boost ist ein Faktor innerhalb der gesamten Prioritätsberechnung und garantiert keinen bestimmten Listenplatz.
- **Priority boost …** erhöht zusätzlich den Score von Stationen, die bereits auf mindestens einem Band gearbeitet wurden, aber noch ein weiteres gemeinsames und nicht gearbeitetes Band anbieten.
Der Priority Boost ist nur ein Faktor innerhalb der gesamten Berechnung. Entfernung, Antennenrichtung, aktuelle Aktivität, AirScout-Daten, Skeds und negative Hinweise können den endgültigen Listenplatz weiterhin verändern. Die aktivierte Option garantiert deshalb weder einen bestimmten Score noch einen bestimmten Platz in der Prioritätsliste.
Die übrigen Score-Gewichte besitzen derzeit keine eigenen Bedienelemente. Mehrere vorhandene Einstellungen liefern jedoch Eingangsdaten für die Berechnung, insbesondere die [aktivierten Bänder](#aktivierte-bänder), der [Antennen-Öffnungswinkel](#antennen-öffnungswinkel-antenna-beamwidth), der [Standard-Maximum-QRB](#standard-maximum-qrb) und die [AirScout-Einstellungen](#airscout-einstellungen).
Die vollständige Herleitung ist unter [Prioritätsscore und Prioritätsliste](de-Funktionen#prioritätsscore-und-prioritätsliste-ab-v140) beschrieben.
Der Hinweis setzt eine Log-Synchronisation mit Bandinformation voraus. Der einfache dateibasierte Callsign-Interpreter erkennt lediglich Rufzeichen und liefert deshalb keine sichere Information über das Band des gerade geloggten QSOs.
@@ -218,7 +253,7 @@ Doppelte oder syntaktisch ungültige Rufzeichen werden nicht übernommen. Die Li
## Shortcut Settings (Schnellzugriff-Schaltflächen)
Konfiguration von Schnellzugriff-Schaltflächen, die direkt im Hauptfenster erscheinen. Ein Klick auf eine Schaltfläche fügt den konfigurierten Text in das Sendfeld ein. Alle [Variablen](Makros-und-Variablen#variablen) können verwendet werden.
Konfiguration von Schnellzugriff-Schaltflächen, die direkt im Hauptfenster erscheinen. Ein Klick auf eine Schaltfläche fügt den konfigurierten Text in das Sendfeld ein. Alle [Variablen](de-Makros-und-Variablen#variablen) können verwendet werden.
---
@@ -348,20 +383,45 @@ Weitere Hintergründe: [Automatische Antworten auf Privatnachrichten](de-Funktio
## Win-Test-Netzwerk-Listener (ab v1.31)
Dedizierter Empfänger für Win-Test-spezifische UDP-Pakete. Ermöglicht:
Der Win-Test-Netzwerk-Listener verarbeitet das native Win-Test-UDP-Protokoll. Er ist vom allgemeinen QSO-UDP-Listener auf Port `12060` unabhängig und übernimmt drei Aufgaben:
- **Log-Synchronisation**: Gearbeitete Stationen werden aus Win-Test übernommen und in der Benutzerliste markiert.
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz aus Win-Test befüllt die `MYQRG`-Variable.
- **Sked-Übergabe (SKED Push)**: Skeds aus KST4Contest werden via UDP direkt an Win-Test übergeben. Der UDP-Broadcast-Standardport von Win-Test (9871) wird verwendet.
- QSOs einschließlich Band- und Locatorinformation auswerten,
- STATUS-Pakete für die eigene QRG verarbeiten und
- Skeds an das Win-Test-Netzwerk übergeben.
Einstellungen:
- **Aktivieren/Deaktivieren**: Checkbox in den Preferences (ab v1.40).
- **Port**: Konfigurierbarer UDP-Port für den Win-Test-Listener.
- **Sked-UDP-Adresse und Port**: Zieladresse und Port für die SKED-Übergabe an Win-Test.
### Einstellungen unter Log sync
> **Hinweis**: Der Win-Test-Listener ist ein **zusätzlicher** Listener der Standard-QSO-UDP-Broadcast-Listener auf Port 12060 bleibt davon unabhängig.
| Einstellung | Funktion |
|---|---|
| **Receive Win-Test network based UDP log messages** | Aktiviert den Win-Test-Netzwerk-Listener. Bei aktiviertem Listener wird nach **Create sked** auch die Sked-Übergabe versucht. |
| **UDP-Port for Win-Test listener** | Port des Win-Test-Netzwerks. Standard ist `9871`. Der Port wird auch für die Sked-Übergabe verwendet. |
| **KST station name in Win-Test network (src of SKED packets)** | Stationsname, unter dem KST4Contest die Sked-Pakete sendet. In einem Netzwerk mit mehreren Clients sollte ein eindeutiger Name verwendet werden. |
| **Win-Test network broadcast address** | Zieladresse für ausgehende Win-Test-Netzwerkpakete. Bei lokalem Netzwerkbetrieb muss hier eine vom Win-Test-Rechner erreichbare Broadcast-Adresse eingetragen sein. |
Die Broadcast-Adresse ist konfigurierbar, weil `255.255.255.255` nicht in jedem Stationsnetz und nicht über jede Netzwerkschnittstelle zuverlässig weitergeleitet wird. Bei mehreren Rechnern kann stattdessen die zum Stationsnetz gehörende gerichtete Broadcast-Adresse erforderlich sein.
### Einstellungen unter TRX sync
| Einstellung | Funktion |
|---|---|
| **Win-Test STATUS QRG Sync** | Übernimmt die aktuelle Frequenz aus Win-Test-STATUS-Paketen als eigene QRG. |
| **Use pass frequency from Win-Test STATUS** | Verwendet die übertragene Pass-Frequenz anstelle der normalen TRX-QRG. |
| **Win-Test station name filter** | Verarbeitet nur STATUS-Pakete der angegebenen Win-Test-Station. Ein leeres Feld akzeptiert alle Stationsnamen. |
Der Stationsfilter ist insbesondere bei mehreren Win-Test-Clients sinnvoll. Ohne Filter kann die zuletzt eingegangene STATUS-Meldung eines anderen Arbeitsplatzes die eigene QRG in KST4Contest überschreiben.
### Sked-Übergabe
Für die Sked-Übergabe gibt es keinen davon getrennten internen Sked-Modus. Ist der Listener aktiviert, versucht **Create sked** zusätzlich zur internen Anlage die Übertragung an Win-Test.
KST4Contest sendet nur dann ein `ADDSKED`-Paket, wenn eine QRG ermittelt wurde, die zum ausdrücklich ausgewählten Band gehört. Kann keine passende QRG gefunden werden, bleibt der interne Sked bestehen und die Win-Test-Übergabe wird ausgelassen.
Die Auswahl `SSB` oder `CW` erfolgt direkt im Further-Info-Bereich beim Anlegen des Skeds. Eine automatische Mode-Ableitung wird nicht verwendet.
Nach Änderungen **Save Settings** verwenden, damit Port, Stationsname, Broadcast-Adresse und TRX-Optionen beim nächsten Programmstart wiederhergestellt werden.
Datenbehandlung und QRG-Auswahl: [Log-Synchronisation Win-Test](de-Log-Synchronisation#win-test)
---
## PSTRotator-Einstellungen (ab v1.31)
+64 -23
View File
@@ -86,35 +86,76 @@ Beim Broadcast des vollständigen Logbuchs verwendet DXLog.net `contactreplace`
### Win-Test
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
Win-Test wird über einen eigenen UDP-Listener für das native Win-Test-Netzwerkprotokoll angebunden. Dieser Listener ist vom allgemeinen QSO-UDP-Listener auf Port `12060` unabhängig.
Bei einem neuen QSO übernimmt KST4Contest das Rufzeichen und löst die native Win-Test-Band-ID auf. Dabei werden auch 50 und 70 MHz verarbeitet. Ist im Paket ein gültiger Locator enthalten, wird zusätzlich das gearbeitete Großfeld für das erkannte Band gespeichert.
#### QSO- und Worked-Synchronisation
**Vorteile der Win-Test Integration:**
- **Bandbezogene Worked-Daten:** Neue QSOs setzen die Worked-Markierung des von Win-Test gemeldeten Bandes und aktualisieren sofern vorhanden den Großfeldstatus.
- Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen.
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieser auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet* automatisch, sobald der Listener aktiv ist.
- Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden.
- **Automatische QRG-Auflösung für SKEDs:** KST4Contest wählt die Sked-Frequenz intelligent:
1. Hat die Gegenstation in einer Chat-Nachricht ihre QRG genannt, wird diese verwendet.
2. Sonst wird die eigene aktuelle QRG verwendet (aus Win-Test STATUS oder manueller Eingabe).
Bei einem neuen QSO übernimmt KST4Contest:
**Einstellungen im Reiter „Log-Synchronisation":**
- `Receive Win-Test network based UDP log messages` aktivieren.
- `UDP-Port for Win-Test listener` (Standard: 9871).
- `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST").
- `Win-Test network broadcast address`: Wird i.d.R. automatisch erkannt; erforderlich für das Senden von Sked-Paketen.
- das geloggte Rufzeichen,
- die native Win-Test-Band-ID und
- einen gültigen Locator, sofern er im Paket enthalten ist.
**Einstellungen im Reiter „TRX-Synchronisation":**
- `Win-Test STATUS QRG Sync`: Wenn aktiviert, übernimmt KST4Contest die aktuelle Transceiverfrequenz aus dem Win-Test STATUS-Paket als eigene QRG (MYQRG).
- `Use pass frequency from Win-Test STATUS`: Statt der eigenen TRX-QRG wird die im STATUS-Paket enthaltene Pass-Frequenz als MYQRG verwendet (für Multi-Op-Setups, bei denen mit einer Pass-QRG gearbeitet wird).
- `Win-Test station name filter`: Wird hier ein Name eingetragen (z.B. "STN1"), verarbeitet KST4Contest nur Pakete dieser Win-Test-Instanz. Leer lassen, um alle zu akzeptieren.
Die Band-IDs für 50 und 70 MHz werden ebenso verarbeitet wie die VHF-, UHF- und SHF-Bänder. Das Rufzeichen wird global und auf dem erkannten Band als gearbeitet markiert. Liegt zusätzlich ein Locator vor, wird dessen vierstelliges Großfeld für dieses Band gespeichert.
**Einstellungen in Win-Test:**
- Das Netzwerk in Win-Test muss aktiv sein.
- Win-Test muss so konfiguriert sein, dass es seine Broadcasts an den entsprechenden Port (Standard 9871) sendet bzw. empfängt.
Die Daten werden in derselben internen Datenbank abgelegt wie Worked-Informationen aus den übrigen QSO-UDP-Schnittstellen und nach einem Neustart wiederhergestellt.
#### Skeds an Win-Test übergeben
Mit **Create sked** wird zunächst ein interner KST4Contest-Sked angelegt. Ist der Win-Test-Netzwerk-Listener aktiviert, versucht KST4Contest anschließend automatisch, den Sked als `ADDSKED` an das Win-Test-Netzwerk zu übertragen.
Die QRG wird in folgender Reihenfolge bestimmt:
1. KST4Contest sucht die neueste, höchstens 30 Minuten alte QRG der Gegenstation auf dem ausdrücklich ausgewählten Band. Dabei werden aktive Varianten desselben Basisrufzeichens gemeinsam ausgewertet.
2. Fehlt eine solche QRG, wird die eigene QRG der Chat-Kategorie geprüft, in der der Sked angelegt wurde. Sie wird nur verwendet, wenn sie sich auswerten lässt und tatsächlich zum ausgewählten Band gehört.
3. Kann auf keinem dieser Wege eine passende QRG ermittelt werden, wird kein `ADDSKED` gesendet.
Eine feste Ersatzfrequenz wie `144.300` wird bewusst nicht verwendet. Eine technisch erfolgreiche Übergabe mit falschem Band oder falscher QRG wäre im Contestbetrieb schlechter als eine sichtbar ausgelassene Übergabe.
Der interne Sked bleibt in jedem Fall erhalten. Das gilt auch bei einer ungültigen Broadcast-Adresse, einem Netzwerkfehler oder einem nicht erreichbaren Win-Test-Client.
#### Behandlung von KST-Rufzeichensuffixen
KST-Suffixe kennzeichnen häufig den verwendeten Chat-Login oder ein Band. Sie gehören nicht in jedem Fall zum Logrufzeichen. Für Win-Test entfernt KST4Contest deshalb einen mit `-` abgetrennten KST-Suffix, erhält aber portable und internationale Rufzeichenbestandteile:
| Rufzeichen im KST-Chat | Übergabe an Win-Test |
|---|---|
| `DN9APW-2` | `DN9APW` |
| `9A0BB-70` | `9A0BB` |
| `EA5/G8MBI/P-70` | `EA5/G8MBI/P` |
| `DN9APW-2/P` | `DN9APW/P` |
Innerhalb von KST4Contest bleibt das vollständige Rufzeichen erhalten. Timeline, Reminder-PMs und Chat-Kategorie beziehen sich weiterhin auf den konkret ausgewählten Login.
#### Mode, Zeitpunkt und Notizen
Der Mode wird beim Anlegen des Skeds ausdrücklich als `SSB` oder `CW` gewählt. Eine automatische Ableitung aus der QRG findet nicht statt, weil eine begrenzte Liste angenommener Bandsegmente nicht alle unterstützten VHF-, UHF- und SHF-Bänder zuverlässig abbilden kann.
KST4Contest überträgt den tatsächlichen Sked-Zeitpunkt ohne einen zusätzlichen Minutenversatz. Die Notizen enthalten soweit bekannt Locator und QTF sowie den Hinweis, dass der Sked über KST4Contest angelegt wurde.
Für die Übergabe sendet KST4Contest die Win-Test-Pakete `LOCKSKED`, `ADDSKED` und `UNLOCKSKED`.
![Von KST4Contest an Win-Test übergebener Sked](wintest_sked_handover.png)
#### Einstellungen
Im Reiter **Log sync**:
- `Receive Win-Test network based UDP log messages`
- `UDP-Port for Win-Test listener`, standardmäßig `9871`
- `KST station name in Win-Test network (src of SKED packets)`
- `Win-Test network broadcast address`
Im Reiter **TRX sync**:
- `Win-Test STATUS QRG Sync`
- `Use pass frequency from Win-Test STATUS`
- `Win-Test station name filter`
Das Win-Test-Netzwerk muss in Win-Test aktiviert sein. Bei mehreren Computern muss die Broadcast-Adresse das betreffende lokale Netzwerk erreichen. Der Stationsname sollte die sendende KST4Contest-Instanz innerhalb des Win-Test-Netzwerks eindeutig erkennen lassen.
Ausführliche Beschreibung der Einstellungen: [Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener-ab-v131)
---
## TRX-Frequenz-Synchronisation
+1 -1
View File
@@ -107,7 +107,7 @@ Aircraft data can be inserted directly into messages:
- `FIRSTAP` → e.g. `a very big AP in 1 min`
- `SECONDAP` → e.g. `Next big AP in 9 min`
Details: [Macros and Variables](Macros-and-Variables#variables)
Details: [Macros and Variables](en-Macros-and-Variables#variables)
---
+161 -13
View File
@@ -4,22 +4,170 @@
Version history of KST4Contest / PraktiKST.
---
For the latest changelog, please refer to GitHub. The previous changelog is below.
## v1.41
**Station Map, Performance, Responsive UI**
**New:**
- **Station Map**: Interactive OpenStreetMap-based map showing the geographic position of all active chat members. Includes station markers, antenna beam cone, connection line to the selected station, Maidenhead grid overlay, and a path profile chart with terrain elevation analysis (Fresnel zones, horizon detection). Terrain data from Copernicus GLO-30, Open-Meteo API, or offline DEM import. Aircraft scatter path analysis integrated. Works in AppImage and Flatpak without external CDN access (local tile proxy, bundled Leaflet.js).
**Changed:**
- **Message table limit raised to 30,000**: Chat and message tables are capped at 30,000 entries. Older messages are automatically discarded, keeping performance stable during multi-day contest operations.
- **Screen-aware window sizing**: On startup, the main window is sized to fit the current screen. If KST4Contest was last used on a larger monitor, the window is automatically scaled down. The UI layout is more compact and responsive on smaller screens.
Published Stable versions and their application packages are available under [GitHub Releases](https://github.com/praktimarc/kst4contest/releases). This page also lists changes from the current development version where they have already been implemented and tested.
---
## v1.42 Nightly / in development
> Status of this section: 10 August 2026.
> v1.42 is not a published Stable release yet. Further changes may be added before release.
v1.42 brings several previously separate calculations together. Band information, Worked status, NOT-QRV marks, callsign suffixes and frequencies are now used more consistently by the user list, station map, priority calculation and external interfaces.
### Added
- **Shared band-opportunity calculation:** A central `BandOpportunityResolver` evaluates recent QRGs, band designators in station names, active callsign variants, Worked information and NOT-QRV marks. The user list, **New bands**, band-upgrade hint, Priority Score, station map and automatic band selection now use the same basis.
- **Extended band status display:** The band columns now distinguish:
- `X` for worked on this band,
- `a` for an offered and unworked band of a completely new callsign,
- `B+` for an offered and unworked band of a callsign already worked elsewhere, and
- `o` for a locator square already worked on this band.
The codes can be combined, for example as `ao` or `B+o`. The additional `a` and `o` indicators can be hidden separately in the GUI settings.
- **50 and 70 MHz support:** Both bands are available in station settings, Worked and NOT-QRV handling, the user list, filters, the internal database, UCXLog processing and the Win-Test listener. Existing databases are extended with the required columns.
- **Global message tabs:** Public messages, ON4KST DX cluster messages and directed messages between other stations can be displayed directly in the main window. The existing separate monitor window remains available and uses the same underlying message stores.
- **Manual QTF input:** The current antenna direction can be changed directly in KST4Contest when PSTRotator is not being used.
- **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.
- **Hideable path analysis:** The terrain profile and analysis section of the station map can be hidden completely. The selected state is stored and restored at the next application start.
### Changed
- **More precise QRG recognition:** Complete and relative frequency references continue to be recognised. Bare three-digit numbers are treated as QRGs only when a frequency context is available. Signal reports, band designators and unrelated numbers therefore produce fewer false frequencies.
- **Station-specific frequency context:** For relative QRGs, KST4Contest first uses a band context for the same station which is no more than 30 minutes old. The globally configured fallback band is used only when this context is unavailable.
- **Fallback band dropdown:** The global fallback can only be selected from supported band values. It applies to the complete QRG parser, not merely to DX cluster spots.
- **Consistent QRG formatting:** Frequencies are displayed with at least three decimal places in the user list and message tables.
- **Band-aware AirScout and path calculations:** KST4Contest derives a realistic frequency for each station from its current QRG and known band information. AirScout receives canonical band values. The temporary 430 MHz fallback has been replaced with 432 MHz.
- **Shared propagation-frequency selection:** AirScout, **Calc selected** and the station-map path analysis use the same `PropagationFrequencyResolver`. A band selected explicitly in the Reachability dropdown is respected for manual calculations.
- **Separate callsign variants:** Active chat members are distinguished by their complete callsign, including suffix, and by chat category. `DN9APW`, `DN9APW-2` and similar logins therefore remain separate message targets.
- **Shared base-callsign information:** Worked flags, NOT-QRV information and Priority Scores continue to be evaluated across variants of the same base callsign. Separate message targets no longer result in contradictory Worked information.
- **Corrected Priority Score eligibility:** Stations without a common available band, or with an overriding NOT-QRV mark, are no longer offered as priority candidates. An open band at a station already worked elsewhere can receive a separate Priority Boost.
- **Extended sked creation:** The band is selected from those enabled locally. `SSB` or `CW` is selected explicitly for the Win-Test handover instead of attempting to infer the mode unreliably from the band.
- **More precise Win-Test sked handover:** The QRG must belong to the selected band. Visible KST suffixes are removed for the logging target while portable callsign components are preserved. `ADDSKED` timestamps are generated correctly. A failed handover does not remove the internal KST4Contest sked.
- **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.
- **Central variable resolution:** Message variables used by beacons, shortcuts, snippets and other generated text are processed by one shared resolver.
- **Improved message tables:** Truncated messages show their complete text in a tooltip. Recognised web addresses can be opened in the system browser.
- **More compact filter bar:** Filters retain their compact arrangement at normal widths and wrap only when the available space is genuinely insufficient. This allows the centre divider to be moved further to the right.
- **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.
- **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
- **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.
- **Messages to callsigns with suffixes:** Several active variants of the same base callsign no longer overwrite each other. This resolves [Issue #73](https://github.com/praktimarc/kst4contest/issues/73).
- **DX cluster locators:** The reporting and reported station no longer receive the same locator accidentally. This resolves [Issue #48](https://github.com/praktimarc/kst4contest/issues/48).
- **Worked state in “QSO of the other”:** The Worked columns again use the correct transmitting and receiving station.
- **Missing SECONDAP data:** A missing second aircraft-scatter opportunity no longer produces an invalid text selection and JavaFX exception when the field is edited.
- **Historical callsigns:** Highlighting or selecting a callsign which is no longer present in the active user list no longer causes an exception.
- **Win-Test sked time and callsign handling:** Timestamps, band-to-QRG assignment, KST suffixes and portable callsigns are handled correctly.
- **Filter reset:** All filter predicates are actually removed, and the visible button state again matches the effective filter state.
### Documentation and packaging
- The German and English manuals have been systematically checked against the source code, extended and supplied with current screenshots. The documentation now describes not only the controls, but also how information is derived and where the limits of a result lie.
- The AUR provides `kst4contest-bin`, `kst4contest` and `kst4contest-git` for Arch Linux.
- The download page distinguishes between Stable, Beta and Nightly and offers only packages which actually belong to the corresponding channel.
- Nightly packages are built automatically from the current `main` branch. Stable and Beta releases use predictable asset names for the supported platforms.
### Known limitations
- The active terrain provider uses Open-Meteo with Copernicus GLO-90 data and no more than 100 elevation samples per path.
- The atmospheric K factor is currently fixed at `4/3`.
- An antenna height of 10 metres above ground is assumed for the remote station.
- Aircraft-scatter information from AirScout and the terrain analysis in the station map remain separate assessments.
- More detailed configuration of station height, frequency and K factor is being tracked in [Issue #74](https://github.com/praktimarc/kst4contest/issues/74).
---
## v1.41.1 (2026-07-08)
**Hotfix for text input and focus handling**
### Fixed
- The message input field was unexpectedly cleared after some time or during particular UI updates.
- Filtering or selecting a station could move the input focus back to the send field unintentionally.
The corrected version is available as [Release v1.41.1](https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1).
---
## v1.41.0 (2026-07-01)
**Station map, bounded message stores and screen-aware main window**
### Added
- **Station map:** An interactive OpenStreetMap-based map displays active chat members for which a usable locator is available.
- **Antenna sector and connection line:** The map shows the current local QTF, configured antenna beamwidth, maximum QRB and the path to the selected station.
- **Maidenhead grid:** A locator grid adapted to the current zoom level provides geographical orientation.
- **Terrain profile:** An elevation profile for a selected station can be requested from the Open-Meteo Elevation API. The active source uses Copernicus GLO-90 data and no more than 100 evenly distributed samples.
- **Geometrical path analysis:** The calculation includes line of sight, Earth curvature using `k = 4/3`, radio and terrain horizons, the first Fresnel zone and a rough obstruction estimate.
- **Local map proxy:** Leaflet is bundled with the application. Map tiles are loaded through a local proxy so that no external JavaScript library is required at runtime. An Internet connection is still required for OpenStreetMap tiles and online elevation data.
### Changed
- **Bounded message stores:** The global chat-message list is reduced from more than 30,000 entries to 25,000. The separate DX cluster store is reduced from more than 10,000 entries to 8,000.
- **Screen-aware startup size:** The main window is checked against the visible area of the primary screen at startup and reduced or repositioned when necessary.
- **More compact user interface:** Several UI sections were adapted for smaller displays and movable dividers.
### Scope of the map function
The station map and AirScout may refer to the same remote station, but their calculations remain separate. Aircraft used by AirScout are not included in the terrain profile.
Classes for Copernicus GLO-30, offline DEM imports and additional terrain providers existed in the source tree but were not part of the active calculation chain in v1.41. The elevation source actually used was Open-Meteo based on Copernicus GLO-90.
---
## v1.40 (2026-02-16)
**Major Feature Release: Score System, AP Timeline, Win-Test, PSTRotator**
+196 -27
View File
@@ -25,11 +25,22 @@ Enter your own callsign and Maidenhead locator (6 characters, e.g., `JN49IJ`). T
### Active Bands
Use the **"my station uses band"** checkboxes to select the active bands. Buttons and table rows will only appear in the user interface for selected bands. The software must be restarted after making changes.
The **My station uses ** checkboxes define the bands available in the current local station setup. Supported choices are 50 MHz, 70 MHz, 144 MHz, 432 MHz, 1296 MHz, 2320 MHz, 3400 MHz, 5760 MHz and 10 GHz.
This selection controls more than the visible band columns. It is also used for:
- the per-band Worked and NOT-QRV filters,
- the NOT-QRV controls visible in the **Further Info** panel,
- the `a` and `B+` band-opportunity calculation,
- the **New bands** filter,
- the band-upgrade hint after a log entry, and
- band-specific priority and Reachability functions.
After a change, click **Save Settings** and restart KST4Contest. Band columns and several related controls are created while the user interface is being built and are therefore not added or removed completely during the current session.
### Antenna Beamwidth
Enter a realistic value for your antenna's beamwidth (in degrees). This value is used for the [Sked Direction Highlighting](Features#sked-direction-highlighting). A test value of 50° has proven effective; DM5M uses quads with 69°.
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°.
> **Do not** enter fantasy values the direction calculations will become useless.
@@ -37,34 +48,84 @@ Enter a realistic value for your antenna's beamwidth (in degrees). This value is
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.
### Path Analysis and Link Budget
The station map uses several values from the **Station** tab to calculate the terrain profile and link budget for the selected remote station. Enter the local station data as realistically as possible. The remote-station values remain global assumptions unless more accurate information is available.
| Setting | Use |
|---|---|
| **Own antenna height AGL** | Height of the local antenna above the surrounding terrain in metres |
| **Own TX power W** | Local transmit power in watts |
| **Own ant. gain dBi** | Gain of the local antenna in dBi |
| **DX OM TX power W** | Assumed transmit power of the remote station in watts |
| **DX OM ant. gain dBi** | Assumed antenna gain of the remote station in dBi |
**AGL** means *Above Ground Level*. Do not enter the height above sea level. The terrain elevation at the local station already comes from the elevation profile; KST4Contest adds the configured antenna height to this value.
For the remote station, KST4Contest currently uses a fixed antenna height of 10 metres above the local terrain. Its transmit power and antenna gain come from the two **DX OM** fields. These values are deliberately assumptions: ON4KST provides neither the actual antenna height nor the complete station data of the remote operator.
Antenna gains must be entered in `dBi`. Add `2.15 dB` before entering a value specified in `dBd`:
```text
dBi = dBd + 2.15 dB
```
The current QTF, configured antenna beamwidth and default maximum QRB affect the map display. The bands enabled for the local station and the frequency derived for the remote station also affect the selection of the analysis frequency.
For the terrain profile, KST4Contest uses the antenna heights, elevation model and Earth curvature with a fixed effective Earth-radius factor of `k = 4/3`. The link budget additionally includes:
- distance and the current analysis frequency,
- transmit powers and antenna gains in both directions,
- frequency-dependent estimated feeder losses,
- free-space path loss, and
- a rough additional loss caused by the relevant obstruction in the terrain profile.
The calculation is performed in both directions. The weaker direction determines the common SSB or CW margin. Optimistic power or antenna values therefore improve the displayed result, but they do not improve the real radio path.
The results remain engineering estimates. Current propagation conditions, local obstructions, vegetation, buildings, interference and unknown station parameters can change the actual result considerably. Operation, frequency selection and the limits of the calculation are described under [Station Map and Path Analysis](en-Features#station-map-and-path-analysis-from-v141).
---
## Server Settings (from v1.31)
The chat server DNS and port are configurable in the Preferences:
The connection details for the ON4KST chat server are shown at the top of the **Station** tab. They normally do not need to be changed.
- **Server DNS**: Default `www.on4kst.org` (changed from `www.on4kst.info` in v1.31 hotfix).
- **Port**: Default port of the ON4KST server.
| Setting | Meaning |
|---|---|
| **ON4KST server** | DNS name or IP address of the chat server; the software default is `www.on4kst.org` |
| **Port** | TCP port of the chat server; the default is `23001` |
A change is only needed if the server moves or an alternative endpoint is used.
`www.on4kst.info` may also remain in an existing, working configuration. There is no reason to change it merely because the displayed name differs from the software default, provided that the chat connection remains reliable.
These values apply only to the outgoing ON4KST chat connection. They do not change the local DX cluster server or any connection to AirScout, PSTRotator or logging software.
The port must be between `1` and `65535`. An invalid entry is rejected and replaced with the last valid value. An incorrect server name or port, on the other hand, prevents KST4Contest from connecting to the chat.
Changing either value does not alter an existing TCP connection. Disconnect and reconnect the chat, or restart KST4Contest. Then use **Save Settings** so that the values are retained for the next program start.
---
## Log Sync Settings
Three methods are available for automatically marking worked stations. Details: [Log Synchronisation](en-Log-Sync).
The **Log sync** tab selects the sources from which KST4Contest imports worked stations. The three input paths provide different levels of detail:
### Universal File Based Callsign Interpreter (Simplelogfile)
![Log synchronisation settings](client_settings_window_logsync.png)
Interprets any log file using regex for callsign patterns. No band information is available. Suitable as a fallback or for log programs that are not directly supported.
| Input path | Data used | Result in KST4Contest |
|---|---|---|
| **Simplelogfile** | Callsigns read from a selected file | global Worked status, but no band or locator information |
| **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 |
### Network Listener for Logger's QSO UDP Broadcast
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.
**Recommended method.** KST4Contest listens for UDP packets sent by the logging software to the broadcast address when a QSO is saved. Stations are marked with band information. UDP port: default **12060**. (Used by UCXLog, N1MM+, QARTest, DXLog.net, etc.).
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 Network Listener (Additional UDP Listener)
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.
A dedicated network listener for Win-Test. KST4Contest receives and processes Win-Test-specific UDP packets (including sked handovers) on the configured port.
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.
Configuration of the individual logging applications, band and locator handling, and the Win-Test sked handover are described under [Log Synchronisation](en-Log-Sync).
---
@@ -87,17 +148,76 @@ Configuration of the interface to AirScout for aircraft scatter detection. Detai
## Notification Settings
![Notifications, DX cluster output and QSO monitoring](client_settings_window_notification.png)
Three notification types are available:
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.
### Fallback Band for Relative QRG Detection
The **Fallback band for relative QRG detection** dropdown selects the band used when a relative QRG cannot be assigned to a recent station-specific band context.
Only band prefixes supported by the frequency parser are available:
```text
50 MHz
70 MHz
144 MHz
432 MHz
1296 MHz
2320 MHz
3400 MHz
5760 MHz
10368 MHz (10G)
24048 MHz (24G)
```
The dropdown is neither a filter nor an override for complete frequencies. `432.088` is recognised as a frequency in the 432 MHz band regardless of the selection. The fallback is needed for relative values such as `.205`, `,205` or `qrg 205`.
Before using the fallback, KST4Contest checks the sender's recent band context. If a complete frequency has been detected for the same station during the previous 30 minutes, that band takes precedence. A fallback setting of `144 MHz` therefore still turns `.100` into `432.100 MHz` if the station mentioned `432.088` shortly before.
Although the setting is located in the Notification tab, it affects the general QRG parser. It therefore influences the QRG column, detected active bands, priority calculations, band-upgrade hints and other functions which use a known station frequency not only DX cluster spots.
### Band Upgrade Hint after a Log Entry
After receiving a log entry from UCXLog or Win-Test, KST4Contest can check whether the station which has just been worked still offers another common and unworked band.
The check uses the same derivation as the `a` and `B+` display:
1. the bands enabled in the local station settings,
2. QRGs detected for the remote station during the previous 30 minutes,
3. explicit band designators in the name fields of its active chat entries,
4. stored per-band Worked marks, and
5. manually assigned NOT-QRV marks.
Active chat variants of the same normalised callsign are evaluated together. NOT-QRV takes precedence over an automatically detected QRG or band designator.
If at least one common and unworked band remains, the main window displays a blinking **BAND+** hint for approximately twelve seconds. The callsign and remaining bands are included in the button text; the tooltip contains the complete derivation. If general notification sounds are enabled, KST4Contest also plays a short sound.
The two options serve different purposes:
- **Blink + sound …** enables the hint after a matching log entry.
- **Priority boost …** additionally raises the score of stations which have already been worked on at least one band but still offer another common and unworked band.
The Priority Boost is only one factor in the complete calculation. Distance, antenna direction, recent activity, AirScout data, skeds and negative hints may still change the final order. Enabling the option therefore guarantees neither a particular score nor a particular position in the priority list.
The other score weights currently have no separate user-interface controls. Several existing settings nevertheless provide input data for the calculation, particularly the [active bands](#active-bands), [antenna beamwidth](#antenna-beamwidth), [default maximum QRB](#default-maximum-qrb) and [AirScout settings](#airscout-settings).
The complete calculation is described under [Priority Score and Priority List](en-Features#priority-score-and-priority-list-from-v140).
The hint requires a log-synchronisation source which provides band information. The file-based callsign interpreter sees callsigns only and cannot reliably identify the band of the QSO which has just been logged.
Further explanation: [Band Upgrade Hint after a Log Entry](en-Features#band-upgrade-hint-after-a-log-entry).
---
## Shortcut Settings
Configuration of quick-access buttons that appear directly in the main window. Clicking a button inserts the configured text into the send field. All [variables](Macros-and-Variables#variables) can be used.
Configuration of quick-access buttons that appear directly in the main window. Clicking a button inserts the configured text into the send field. All [variables](en-Macros-and-Variables#variables) can be used.
---
@@ -135,18 +255,44 @@ New settings section with the following options:
## Win-Test Network Listener (from v1.31)
A dedicated listener for Win-Test-specific UDP packets. Enables:
The Win-Test network listener processes the native Win-Test UDP protocol. It is independent of the general QSO UDP listener on port `12060` and has three separate tasks:
- **Log synchronisation**: Worked stations are retrieved from Win-Test and marked in the user list.
- **Frequency parsing**: The current TRX frequency from Win-Test populates the `MYQRG` variable.
- **Sked handover (SKED push)**: Skeds from KST4Contest are passed directly to Win-Test via UDP. Win-Test's default UDP broadcast port (9871) is used.
- processing QSOs including band and locator information,
- processing STATUS packets for the local QRG, and
- handing skeds over to the Win-Test network.
Settings:
- **Enable/Disable**: Checkbox in Preferences (from v1.40).
- **Port**: Configurable UDP port for the Win-Test listener.
- **Sked UDP address and port**: Target address and port for SKED handover to Win-Test.
### Log sync settings
> **Note**: The Win-Test listener is an **additional** listener the standard QSO UDP broadcast listener on port 12060 remains independent.
| Setting | Function |
|---|---|
| **Receive Win-Test network based UDP log messages** | Enables the Win-Test network listener. When the listener is enabled, pressing **Create sked** also attempts the Win-Test handover. |
| **UDP-Port for Win-Test listener** | Port used by the Win-Test network. The default is `9871`. The same port is used for the sked handover. |
| **KST station name in Win-Test network (src of SKED packets)** | Station name used by KST4Contest when sending sked packets. A unique name should be used in a network containing several clients. |
| **Win-Test network broadcast address** | Destination address for outgoing Win-Test network packets. In a local network, the address must be reachable by the Win-Test computer. |
The broadcast address is configurable because `255.255.255.255` is not forwarded reliably through every station network or network interface. In a multi-computer setup, the directed broadcast address belonging to the station network may be required instead.
### TRX sync settings
| Setting | Function |
|---|---|
| **Win-Test STATUS QRG Sync** | Takes the current frequency from Win-Test STATUS packets and uses it as the local QRG. |
| **Use pass frequency from Win-Test STATUS** | Uses the transmitted pass frequency instead of the normal TRX QRG. |
| **Win-Test station name filter** | Only processes STATUS packets from the specified Win-Test station. An empty field accepts every station name. |
The station filter is particularly useful when several Win-Test clients are active. Without a filter, the most recently received STATUS packet from another operating position can overwrite the local QRG in KST4Contest.
### Sked handover
There is no separate internal sked mode for the Win-Test handover. When the listener is enabled, **Create sked** attempts the Win-Test transfer in addition to creating the internal sked.
KST4Contest only sends an `ADDSKED` packet when it can determine a QRG which belongs to the explicitly selected band. If no matching QRG is available, the internal sked remains intact and the Win-Test handover is omitted.
`SSB` or `CW` is selected directly in the Further Info section when the sked is created. No automatic mode inference is used.
Click **Save Settings** after making changes so that the port, station name, broadcast address and TRX options are restored at the next start.
Data handling and QRG selection: [Log Synchronisation Win-Test](en-Log-Sync#win-test)
---
@@ -174,14 +320,37 @@ Use case: Keep track of important stations (e.g. DX expeditions or trusted conte
---
## GUI Settings: Band-Column Hints
Two optional additions to the band columns can be enabled or disabled in the **GUI** tab:
- **Show "o" in band columns …** displays an `o` if the four-character grid square has already been worked on the relevant band. Disabling the option does not delete any database records; it only hides the additional indicator in the band columns. `wkdany` is unaffected.
- **Show "a" in band columns …** distinguishes a completely new callsign from a band opportunity involving a callsign already worked elsewhere. When disabled, both cases are displayed as `B+`. The band-opportunity calculation itself remains unchanged.
Changes are reflected in the current user interface immediately. Click **Save Settings** afterwards if they should persist after the next start.
![GUI settings for band-column hints](client_settings_window_gui.png)
---
## Worked Station Database Settings
The internal worked database contains:
The internal SQLite database stores contest-related state independently of the logging application's database:
- Worked status of all stations (per band)
- NOT-QRV tags (since v1.2)
- the global Worked status of a callsign,
- Worked status per band,
- manually assigned NOT-QRV marks per band, and
- worked four-character grid squares per band.
**From v1.40**: Entries have an automatic lifetime of **3 days** manually resetting before each contest is no longer strictly necessary. For a full reset, the **"Reinitialize"** button is still available.
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.
The **Reset worked, NOT-QRV and grid data...** button removes every Worked mark, NOT-QRV mark and stored worked grid square. A confirmation dialog is displayed first. Known callsign rows remain in the database; only the contest-related state is reset.
A reset is useful when you deliberately want to start with an empty contest state or have imported test data. It is not intended as a daily maintenance step.
Display and derivation: [Worked Callsigns, New Bands and New Grid Squares](en-Features#worked-callsigns-new-bands-and-new-grid-squares).
---
+587 -75
View File
@@ -28,7 +28,7 @@ The calculation is based on the following logic:
The calculation does not include topographic path calculations this is a deliberate simplification. It may be added in a future version.
> Configuration: [Configuration Antenna Beamwidth](Configuration#antenna-beamwidth)
> Configuration: [Configuration Antenna Beamwidth](en-Configuration#antenna-beamwidth)
---
@@ -48,26 +48,92 @@ Recognised formats: `144.205`, `432.088`, `.205` (with configured band assumptio
---
## Worked Marking
## Worked Callsigns, New Bands and New Grid Squares
Worked stations are visually marked in the user list per band. Based on [Log Synchronisation](en-Log-Sync) via UDP or Simplelogfile.
KST4Contest distinguishes between three pieces of information which may look similar during a contest but answer different questions:
Reset the database before each contest: [Configuration Worked Station Database Settings](Configuration#worked-station-database-settings).
1. Has this callsign been worked before?
2. Has this callsign been worked on a particular band?
3. Has the four-character Maidenhead grid square already been worked, possibly with a different station?
---
This distinction matters. A callsign already worked on one band may still be useful on another. Conversely, a new callsign may be located in a grid square which is already in the log.
## NOT-QRV Tags (from v1.2)
### Worked information from the log
When a station indicates it is not QRV on a specific band, this can be manually marked:
[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 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.
Missing information is not guessed. A QSO without a locator does not create a worked-grid record, and a Simplelogfile match does not create a band-specific Worked mark.
### Meaning of the band columns
The shared **worked** column contains only the bands enabled under **Station → My station uses …**. Its cells deliberately use short status codes because a full description would leave very little room for the actual user list.
| Display | Meaning |
|---|---|
| `X` | The callsign has been worked on this band. |
| `a` | The station offers this band, the band has not been worked yet, and the callsign has not been worked on any band. |
| `B+` | The station offers this band and it has not been worked yet. The callsign has already been worked on another band. If the separate `a` display is disabled, a completely new callsign is also shown as `B+`. |
| `o` | The station's four-character grid square has already been worked on this band, regardless of callsign. |
| empty | No matching information is available for this band. This does not mean that the station is not QRV. |
The `o` is an independent overlay and can therefore be combined with the other codes. Examples include `Xo`, `ao` and `B+o`. A single `o` means that the grid square has been worked on this band, while the displayed callsign has neither a Worked mark nor a current band opportunity.
![Band-specific Worked status and worked grid squares](worked_band_status.png)
### How is a band opportunity derived?
KST4Contest displays `a` or `B+` only if it can derive an open band opportunity. The calculation combines:
1. the bands enabled for the local station,
2. QRGs detected for the remote station during the previous 30 minutes,
3. explicit band designators in the remote station's name field,
4. stored per-band Worked marks, and
5. manually assigned NOT-QRV marks.
Active chat entries with the same normalised callsign are evaluated together. This is particularly relevant when the station appears in several chat categories or with different visible callsign variants. An explicit band designator in the name field remains useful while the corresponding chat entry is active. A band derived from a detected QRG expires after 30 minutes.
The remaining set contains only bands which are enabled locally, known for the remote station and not yet worked. A manual NOT-QRV mark overrides automatically detected evidence. The chat category alone is not sufficient evidence that an individual station is QRV on a particular band.
The global Worked mark does not decide whether a band opportunity exists. It merely selects `a` or `B+` for the display. The actual opportunity calculation uses per-band Worked information.
### Meaning of `wkdany`
The **wkdany** subcolumn combines the global callsign and grid-square status:
| Display | Meaning |
|---|---|
| empty | Neither the callsign nor the four-character grid square has been worked. |
| `x` | The callsign has been worked on at least one band. |
| `o` | The four-character grid square has been worked on at least one band. |
| `xo` | Both the callsign and the grid square have been worked. |
`wkdany` is deliberately band-independent. The lower-case `x` must therefore not be confused with the upper-case `X` in a band column. The global status is used for the overview and the global **wkd** filter; it is not a substitute for per-band Worked information.
### NOT-QRV marks
If a station reports that it is not QRV on a particular band, mark this in the selected station's **Further Info** panel:
1. Select the station in the user list.
2. Right-click → Set NOT-QRV for the appropriate band.
2. Enable the relevant band under **Not QRV**.
3. Use **tag not qrv all** only if the station should not be requested on any supported band.
These tags are stored in the internal database and persist after a KST4Contest restart. Can be reset via the settings.
The individual NOT-QRV controls are shown for the bands enabled at the local station. **tag not qrv all**, however, marks every supported band, including bands which are not currently visible in the user interface. The state is stored per band under the normalised callsign and propagated to its active chat variants.
**Benefit**: Prevents repeated sked requests on bands where the station is not active saves time for both sides.
![Per-band NOT-QRV marks in the Further Info panel](not_qrv_controls.png)
---
NOT-QRV is a manual correction and therefore takes precedence over detected QRGs and band designators in the name field. The affected band is no longer offered as `a` or `B+`, is not counted as an opportunity by the **New bands** filter and is excluded by the corresponding band filter.
In plain terms: an automatically detected hint means "probably active on this band". A manual NOT-QRV mark means "do not request this station on this band". The next detected number must not silently reverse that decision.
### 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.
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.
## Direction Filter
@@ -83,9 +149,19 @@ Hide stations beyond a maximum distance. The **"Show only QRB [km] <="** button
---
## Worked and NOT-QRV Filter
## Filters for Worked Status, New Bands and New Grid Squares
Toggle buttons (one per band) to hide already-worked stations and/or NOT-QRV-tagged stations. The filter takes effect **immediately** without manually reactivating (live since v1.22).
The filters above the user list use the same information as the Worked columns:
- **wkd** hides callsigns which have been worked on at least one band.
- The individual band buttons hide a station if the callsign has already been worked on that band or has manually been marked NOT QRV there.
- **New bands** shows only stations for which at least one locally enabled and unworked band is known. It evaluates recent QRG detections and band designators in the name field; NOT-QRV takes precedence.
- **Only new grids** shows only stations whose four-character grid square has not been worked on any band. Stations without a valid locator do not pass this filter.
- **Grid color** does not filter the list. When enabled, it gives the QRA cell of an already worked grid square a slightly darker background. New grid squares retain the normal table colour.
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).
---
@@ -135,19 +211,86 @@ For selected stations in the user list, there are direct buttons to open the **Q
---
## Sked Reminders with ALERT (from v1.40)
## Skeds and Sked Reminders
A sked reminder service with automatic messages can be activated for each chat member. Configurable interval patterns:
> Available from v1.40; band, callsign and Win-Test handling extended in Nightly / v1.42.
- **2+1 minutes**: Messages at 2 min and 1 min before the sked.
- **5+2+1 minutes**: Messages at 5, 2 and 1 min before the sked.
- **10+5+2+1 minutes**: Messages at 10, 5, 2 and 1 min before the sked.
A sked is more than a reminder tied to a particular time. During a contest, it must become visible early enough, move the agreed station up the priority list and if required remind the remote station as well.
In addition to the automated messages to the remote station, there is an **acoustic and visual notification** for your own operator so no sked is ever missed.
KST4Contest therefore treats three tasks separately:
Activate from the FurtherInfo panel of the corresponding station.
1. The sked is stored internally and included in the priority calculation.
2. The scheduled contact appears in the AP and sked timeline.
3. Automatic private reminder messages can optionally be sent before the agreed time.
When the Win-Test network listener is enabled, KST4Contest also attempts to hand the sked over to Win-Test. A failed handover neither removes nor prevents the internal sked.
### Creating a sked
First select the required station in the user list. The sked controls then appear at the bottom of the **Further Info** section.
| Control | Function |
|---|---|
| **Sked in** | Sets the number of minutes until the sked. Available values are 2 through 15 and 20 minutes. |
| **Band** | Selects the sked band. The dropdown contains the local bands enabled under **Station → my station uses …**. |
| **Mode** | Sets the mode passed to Win-Test. Available values are `SSB` and `CW`. This selection does not affect the internal sked or reminder PMs. |
| **Create sked** | Creates the internal sked and, if the Win-Test network listener is enabled, also attempts the Win-Test handover. |
| **Remind-PM in** | Enables automatic private reminder messages before the sked. |
| **2+1**, **5+2+1**, **10+5+2+1** | Selects how many minutes before the sked the reminder PMs are sent. |
![Sked controls in the Further Info section](sked_controls.png)
KST4Contest attempts to preselect a useful band. It checks the following information in this order:
1. a QRG of the selected station which is no more than 30 minutes old and belongs to a locally enabled band,
2. an unambiguous band designator in the station's name field, and
3. the first locally enabled band.
Active callsign variants belonging to the same base callsign are evaluated together when looking for recent band information. A manual NOT-QRV mark is taken into account by the automatic selection. The operator can still select another band explicitly when a different arrangement has been made.
### Effect on the Priority Score
A stored sked raises the score of the normalised base callsign:
| Time relative to the sked | Contribution to the score |
|---|---:|
| more than 15 minutes before the sked | `+40` |
| 15 to 3 minutes before the sked | continuous increase from `+300` towards `+1200` |
| less than 3 minutes before until 1 minute after the sked | `+5000` |
| more than 1 minute after the sked | no remaining sked boost |
The strong weighting immediately around the scheduled time is intentional. An agreed sked should not disappear from the priority list merely because another station is currently very active but has no fixed appointment.
The score is calculated for the base callsign. A sked created for `DN9APW-2` therefore also affects the shared score of other active `DN9APW` variants. The actual message target nevertheless remains `DN9APW-2` in the chat category selected when the sked was created.
The sked is removed from the internal list five minutes after its scheduled time.
### Reminder PMs
Reminder PMs are only scheduled when **Remind-PM in** is enabled. Depending on the selected pattern, KST4Contest sends a private message such as the following two and one minute before the sked:
```text
[KST4C Autoreminder] sked in 2 min
```
The message is sent to the complete visible KST callsign in the chat category in which the sked was created. A sked for `DN9APW-2` is therefore not accidentally sent to `DN9APW`, `DN9APW-70` or a similarly named station in another category.
When a reminder is actually triggered, KST4Contest also displays the visual **SKED** indication. If simple notification sounds are enabled, a short sound is played as well. Merely arming a reminder does not start the blinking indication.
Creating a new set of reminders for the same complete callsign replaces the previously scheduled reminders for that callsign.
### Storage and limitations
Skeds and reminder schedules are kept in memory only. Any skeds which are still required must be recreated after restarting KST4Contest.
The automatic band selection is derived from available chat information. It cannot prove that the station is still operating on the most recently mentioned QRG. Check the band, time and mode before pressing **Create sked**.
Operation: [Station Info Panel](en-User-Interface#station-info-panel-further-info)
Display: [AP and Sked Timeline](#ap-and-sked-timeline)
Win-Test handover: [Log Synchronisation Win-Test](en-Log-Sync#win-test)
---
## QSO Sniffer (from v1.31)
@@ -157,17 +300,22 @@ Configuration: [Configuration Sniffer Settings](en-Configuration#sniffer-set
---
## Win-Test Integration (from v1.31, fully configurable from v1.40)
## Win-Test Integration
KST4Contest fully supports [Win-Test](https://www.win-test.com/) as a logging programme:
KST4Contest uses a dedicated listener for the native Win-Test network protocol. It provides three separate functions:
- **Log synchronisation**: Worked stations are automatically retrieved from Win-Test and marked in the user list.
- **Frequency parsing**: The current TRX frequency is read from Win-Test UDP packets and populates the `MYQRG` variable.
- **Sked handover (SKED push via UDP)**: Agreed skeds from KST4Contest can be pushed directly to Win-Test, so the remote callsign appears in Win-Test's sked window.
- importing new QSOs including band and, where available, locator information,
- reading the current QRG from Win-Test STATUS packets, and
- handing internally created skeds over to the Win-Test network as `ADDSKED` packets.
Details: [Configuration Win-Test Network Listener](en-Configuration#win-test-network-listener)
The sked handover does not replace a missing QRG with a fixed default frequency. KST4Contest only sends a Win-Test sked when it can determine a QRG which belongs to the selected band. The internal sked, timeline and reminder PMs continue to work independently.
A visible KST suffix such as `-2`, `-70` or `-144` is retained inside KST4Contest but removed from the callsign passed to the Win-Test log. Portable components such as `/P`, `/M` and country prefixes are preserved.
Setup and data handling: [Log Synchronisation Win-Test](en-Log-Sync#win-test)
Settings: [Win-Test Network Listener](en-Configuration#win-test-network-listener-from-v131)
---
## PSTRotator Interface (from v1.31, fully configurable from v1.40)
@@ -177,95 +325,459 @@ Configuration: [Configuration PSTRotator Settings](en-Configuration#pstrotat
---
## Band Alert for New QSOs (from v1.40)
## Band Upgrade Hint after a Log Entry
When a station is logged, KST4Contest automatically checks whether that station has shown any other active bands in the chat that you are also QRV on. If so, a **hint alert** appears so no multi-band opportunity is missed.
When UCXLog or Win-Test reports a new log entry with band information, KST4Contest checks whether the worked station still offers another common band.
The calculation follows the same rules as `a`, `B+` and the **New bands** filter: locally enabled bands, recent QRG detections, band designators in the name field, per-band Worked marks and NOT-QRV marks. Active chat variants of the same normalised callsign are evaluated together.
If at least one common and unworked band remains, a blinking hint appears for approximately twelve seconds. It includes the callsign and the remaining bands, for example `BAND+ DL0ABC 432, 1296`. Its tooltip also lists the enabled, worked and NOT-QRV bands used for the decision. If general notification sounds are enabled, KST4Contest also plays a short sound.
The Simplelogfile interpreter cannot trigger this hint reliably because it provides no band information for the QSO which has just been logged.
Configuration: [Band Upgrade Hint after a Log Entry](en-Configuration#band-upgrade-hint-after-a-log-entry).
Worked, NOT-QRV and worked-grid data expire automatically after three days. See [Worked Station Database Settings](en-Configuration#worked-station-database-settings) for the lifetime and manual reset behaviour.
---
## Worked Tag Lifetime (from v1.40)
## Priority Score and Priority List (from v1.40)
Worked stations are automatically removed from the database after **3 days**. Manually resetting the worked database before each contest is therefore no longer strictly necessary the database keeps itself up to date.
### Why is a score needed at all?
A conventional chat user list initially tells the operator only which stations are logged in. That is not enough during a contest. The operator must also consider which stations have not yet been worked, which bands may still be available, where the antenna is pointing, whether a suitable aircraft is approaching and whether an agreed sked is about to begin.
With a short list, much of this can still be handled mentally. As the contest continues, several bands are used and two chat categories are monitored at the same time, the same decision has to be reconstructed over and over again.
KST4Contest therefore combines the available information into a priority score. The score does not answer whether a QSO will definitely be possible. It supports the more practical question:
> Which of the currently visible stations should I examine next?
### When is a station excluded?
Before applying the weighted factors, KST4Contest checks whether a known band opportunity exists. All active chat entries belonging to the same normalised base callsign are evaluated together.
The calculation uses:
1. the bands enabled in the local station settings,
2. QRGs detected for the remote station during the previous 30 minutes,
3. explicit band designators in the name fields of its active chat entries,
4. stored per-band Worked marks, and
5. manually assigned NOT-QRV marks.
NOT QRV takes precedence over automatically detected frequencies and band designators.
If the remote stations bands are known but none of them is both enabled locally and still available, the station receives a score of `0`. The same applies when every common band opportunity has already been worked.
A station is not excluded merely because all band information is missing. An unknown band opportunity is not the same as a known incompatibility. In this case, the station is removed from consideration only if all locally enabled bands have been manually marked NOT QRV for that station.
Stations with a score of `0` remain visible in the user list but are not included in the priority list.
### Which information raises or lowers the score?
The score combines several independent hints. One factor will therefore not normally determine the final position on its own.
| Factor | Effect on priority |
|---|---|
| Worked status | A callsign which has not been worked on any supported band receives a higher initial priority. A station which has already been worked is ranked lower but remains a candidate when another band opportunity is available. |
| Available bands | Several common and unworked bands raise the priority. An additional boost for band-upgrade cases can be enabled separately. |
| Distance | Distances below 200 km are weighted lower. The range between 200 km and the configured maximum QRB is preferred. Stations beyond the maximum QRB are reduced substantially. If the QRB is unavailable, this factor is omitted. |
| Antenna direction | The score rises when the QTF to the station lies within half of the configured antenna beamwidth around the current local QTF. The closer both directions are, the stronger the effect. |
| AirScout | At least one currently reachable aircraft raises the score. An expected AP opportunity in zero, one or two minutes receives an additional time-dependent weighting. |
| Recent chat activity | A message received during the previous minute has a stronger effect than one received during the previous three minutes. Several incoming lines within the activity window raise the score further. |
| Positive signals | Detected terms such as `QRV`, `READY`, `RGR`, `OK`, `TNX` or comparable configured text patterns are treated as a positive hint for several minutes. |
| Reply behaviour | If another visible chat line from the station follows an outgoing `/cq` message quickly, the averaged reaction time raises the score. If no such line arrives before the configured timeout, a negative mark is added. |
| Skeds | A scheduled contact initially adds a small amount of priority. During the final 15 minutes, its influence increases continuously. From three minutes before until one minute after the scheduled time, the sked receives very high priority. |
| Failed attempt | **Sked fail** strongly reduces the stations score until the mark is removed with **Reset fail** or KST4Contest is restarted. |
The default activity window for counting incoming messages is 180 seconds. A message received during the previous 60 seconds is evaluated separately as current activity. The default no-reply timeout is 13 minutes.
For reply behaviour, KST4Contest cannot prove that a later public or private line is actually a reply to the operators request. Any subsequent line received from the same station therefore ends the pending response-time measurement. This is a practical approximation, not a statistically reliable response rate.
### What does a scheduled contact mean for the score?
A sked is a time-dependent operating commitment. An imminent sked must therefore take precedence over most normal activity and distance hints. Without this weighting, a station which happens to be very active in the chat could displace an agreed contact from the priority list.
The strongest sked boost is deliberately limited to the period from three minutes before until one minute after the scheduled time. A sked further in the future remains relevant but should not yet dominate current operation.
The score is calculated for the normalised base callsign. A sked entered for an active variant such as `9A0BB-23` therefore affects the common score of the chat entries belonging to `9A0BB`.
### 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.
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.
KST4Contest uses the most recently suitable active login in the last relevant chat category as the concrete message target. Selecting a priority candidate then resolves the complete callsign, including its suffix and chat category.
### Updating and displaying the score
New messages, AirScout data, skeds, Worked information and manual NOT-QRV or Sked-fail changes request a new calculation immediately. The score is also refreshed periodically because activity, AP and sked information changes with time even when no new event is received.
A delay of a few seconds between an event and the visible new order is therefore normal.
The user interface displays the score in three places:
- the numerically sortable **Score** column in the user list,
- the **Further Info** section for the selected station, and
- the compact list of the two highest-ranked candidates, with a separate window containing up to 15 candidates.
Operation: [Priority List in the User Interface](en-User-Interface#priority-list).
### What does the score not tell you?
The numerical value is neither a success probability nor a signal prediction. A score which is twice as high does not mean that the QSO is twice as likely.
Among other things, the calculation does not know:
- the actual antenna direction of the remote station,
- its current operating situation,
- local interference,
- short-term propagation changes,
- terrain obstruction outside the connected assessment functions, or
- whether a station which is active in the chat is currently sitting at the radio.
Known input data may also be outdated or ambiguous. A detected frequency, for example, proves only that the QRG recently appeared in connection with that station.
In practical terms, the score does not replace the operators decision. It prevents the information already available to KST4Contest from having to be reconstructed mentally for every candidate.
Related settings:
- [Active Bands](en-Configuration#active-bands)
- [Antenna Beamwidth](en-Configuration#antenna-beamwidth)
- [Default Maximum QRB](en-Configuration#default-maximum-qrb)
- [AirScout Settings](en-Configuration#airscout-settings)
- [Band Upgrade Hint and Priority Boost](en-Configuration#band-upgrade-hint-after-a-log-entry)
---
## Chatmember Score System / Priority List (from v1.40)
## AP and Sked Timeline
KST4Contest automatically calculates a **priority score** for each active chat member. The score is derived from:
The timeline combines upcoming aircraft-scatter opportunities and stored skeds for the next 30 minutes. It therefore answers two different questions in the same place:
- Antenna direction of the remote station (is it pointing towards me?)
- QRB (distance)
- Activity time and message count
- Active bands and frequencies
- AP availability (AirScout)
- Sked direction (degrees)
- Sked success rate and skedfail markings
- When is an interesting AP opportunity expected?
- Which previously agreed sked is approaching independently of that opportunity?
The top candidates are highlighted in a dedicated priority list, helping you not to miss the most important contacts during contest stress.
Events further in the future appear on the right. As time passes, they move left towards the current time.
Stations with a failed sked can be marked using the **Skedfail button** in the FurtherInfo panel this temporarily lowers their score.
![AP candidates and skeds in the timeline](sked_timeline.png)
---
### AP candidates
## AP Timeline (from v1.40)
AP candidates appear in the upper lanes. Up to four selected candidates can be displayed for each aircraft arrival minute. The selection takes the Priority Score and the reflection potential reported by AirScout into account.
A visual timeline shows up to 4 highly-scored stations per minute slot that should be workable via aircraft scatter. Prioritisation criteria:
The colour of an AP marker represents the reflection potential:
- **Highest reflection potential** is preferred (not necessarily the fastest arrival).
- Stations towards which your antenna is not pointing are shown **transparently**.
| Colour | Reflection potential |
|---|---:|
| Magenta | at least 95% |
| Red | at least 75% |
| Yellow | at least 50% |
| Blue | below 50% |
The colour is not a QSO probability. It represents the AirScout value for the calculated reflection geometry.
Clicking an AP candidate selects the corresponding active chat member, including its callsign suffix and chat category. A suitable message can then be prepared immediately.
### Skeds
Skeds appear as diamonds in the lower lane. Their labels use the complete KST callsign, for example `SKED: DN9APW-2`. This makes it clear which particular login was selected for the scheduled contact.
A sked tooltip shows at least:
- the complete KST callsign,
- the agreed band, and
- the QTF towards the remote station.
Where suitable AirScout data is available, the tooltip also includes current AP reachability and the next calculated AP opportunity.
### Antenna direction
When the QTF of an event is clearly outside the current antenna direction, its marker becomes more transparent. The label remains readable. A target close to the centre of the configured antenna beam is highlighted.
This visual effect changes neither the sked nor the Priority Score. It is simply a quick way of identifying candidates which fit the current antenna direction.
The timeline is a preview. AirScout data can change, and a stored sked guarantees neither a clear frequency nor an actual propagation path.
This gives the contest operator a quick overview of which stations will be reachable via which aircraft and at what time.
---
## Interval Beacon
Automatic CQ messages in the public channel at a configurable interval. Recommended: use the `MYQRG` variable so the current frequency is always accurate. Details: [Configuration Beacon Settings](Configuration#beacon-settings).
Automatic CQ messages in the public channel at a configurable interval. Recommended: use the `MYQRG` variable so the current frequency is always accurate. Details: [Configuration Beacon Settings](en-Configuration#beacon-settings).
---
## Simplelogfile
File-based log evaluation using regex. Details: [Log Synchronisation](Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
File-based log evaluation using regex. Details: [Log Synchronisation](en-Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Global Message Views
Most message tables in KST4Contest are deliberately tied either to the local station or to the station currently selected in the user list. Some message streams must, however, remain visible independently of that selection.
KST4Contest therefore provides three global message tabs below the main user list:
| Tab | Content |
|---|---|
| **Public messages** | Public chat messages, including CQ calls and beacon messages |
| **DXCluster messages** | DX cluster messages delivered by the ON4KST server |
| **QSO of the other** | Directed chat messages between chat logins other than the local station |
**Public messages** is selected by default. Changing the selected station does not affect any of these three views.
![Global message tabs below the main user list](global_message_tabs.png)
### DXCluster messages
The DX cluster table shows cluster messages received through the ON4KST connection. Depending on the information contained in the source message, the table displays:
- the time,
- the reporting station and its locator,
- the reported station and its locator,
- the QRG,
- the message text, and
- the global Worked state of the reported station.
An empty locator or another empty field does not necessarily indicate a processing error. The corresponding information may simply be absent from the source message.
This view must not be confused with the [built-in DX Cluster server](en-DX-Cluster-Server). The built-in server sends derived direction spots to connected logging software. The **DXCluster messages** tab displays cluster traffic received from ON4KST.
### QSO of the other
The **QSO of the other** table displays directed chat messages for which neither the sender nor the receiver is the local station. Messages addressed to `ALL` are not included.
The table contains the following columns:
| Column | Meaning |
|---|---|
| **Time** | Time of the chat message |
| **Call TX** | Complete callsign of the sender |
| **Last QRG TX** | Most recently detected QRG assigned to the sender |
| **wkd TX?** | Global Worked state of the sender |
| **Call RX** | Complete callsign of the receiver |
| **Last QRG RX** | Most recently detected QRG assigned to the receiver |
| **wkd RX?** | Global Worked state of the receiver |
| **Message** | Message text |
| **Category** | Chat category in which the message was received |
The QRG columns are not a historical record of the frequency used for the displayed message. They show the latest QRG currently known for the respective chat member. The value may originate from another message and may change when a newer QRG is detected.
The two Worked columns show the global callsign state. They do not indicate whether the station has already been worked on the QRG or band shown next to it.
The expression “QSO of the other” is used as a compact user-interface label. A directed chat message does not prove that an actual radio QSO has taken place. It may equally be a sked request, a frequency exchange or another private message between two chat logins.
### Separate monitor window
The DX cluster and QSO-of-the-other tables are additionally available in a separate monitor window. It places the DX cluster table above the directed messages between other stations.
![Separate monitor window for DX cluster traffic and directed messages between other stations](cluster_qso_monitor.png)
The tabs and the monitor window use the same underlying message stores. Opening the separate window does not create another connection, receive the messages a second time or maintain an independent history.
The window can be hidden or restored through:
**Windows → Hide cluster / stranger QSOs**
or:
**Windows → Show cluster / stranger QSOs**
The additional window is useful when these message streams should remain visible on a second monitor or while another part of the main window is being used. During periods with heavy chat traffic, the global tabs are usually more compact.
When a table cell cannot display its complete message, moving the mouse over the cell shows the full text in a tooltip. Web links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
---
## Cluster & QSO of Others
## Station Map and Path Analysis (from v1.41)
A separate window showing the QSO flow between other stations. Particularly interesting during quieter night-time hours of a contest. This window can be minimised when not needed. Future plan: filtering to stations in your selected QTF.
The station map shows the geographical relationship between the local station and the chat members which are currently relevant in the main window. It is not a second, independent user list: filters applied to the chat-member table also determine which stations are passed to the map.
![Station map with path analysis](station_map_path_analysis.png)
### Stations and markers
A station can be displayed only if a usable six-character Maidenhead locator is available. Chat entries without a sufficiently precise locator remain in the user list but cannot be positioned reliably on the map.
Active chat variants belonging to the same normalised base callsign are combined into one map marker. This avoids several markers being placed at exactly the same position when, for example, a station is logged in with separate suffixes for different bands. The marker information includes the currently derived bands and, where applicable, open `B+` opportunities.
Marker colours provide a compact status indication:
| Colour | Meaning |
|---|---|
| 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 |
| 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.
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.
### Antenna sector, connection line and locator grid
The map displays the local station together with the currently configured antenna direction, beamwidth and maximum QRB. These values form the visible antenna sector.
Selecting a remote station adds a connection line between both locations. The Maidenhead overlay provides a geographical reference without requiring the operator to translate every locator mentally.
The map does not know the actual radiation pattern, side lobes or elevation angle of the antenna. The displayed sector is therefore a geometrical representation of the configured horizontal beamwidth, not a complete antenna model.
### Terrain profile
For the selected path, KST4Contest requests terrain elevations from the Open-Meteo elevation service. The active provider uses Copernicus GLO-90 data and requests no more than 100 evenly distributed elevation coordinates for one path.
The terrain resolution and the sampling distance are not the same thing. On a long path, the distance between two requested points can be considerably larger than the nominal resolution of the elevation model. Small terrain features may therefore remain undetected.
The profile combines:
- terrain elevation,
- the geometrical line between both antennas,
- Earth-curvature correction using an effective Earth-radius factor of `k = 4/3`,
- the radio and terrain horizons,
- the first Fresnel zone,
- minimum Fresnel clearance,
- detected Fresnel-zone intrusion, and
- a rough knife-edge diffraction estimate for relevant obstructions.
The configured **Own antenna height AGL** is added to the terrain elevation at the local station. For the remote station, KST4Contest currently assumes an antenna height of 10 metres above the local terrain.
Moving the mouse over the path profile marks the corresponding position on the map. This makes it easier to identify which hill or terrain section causes a reported obstruction.
### Frequency selection
Fresnel clearance and link-budget results depend on frequency. KST4Contest therefore attempts to derive a usable analysis frequency from recent QRG or band information associated with the selected station.
The value displayed as **Frequency** in the analysis panel is the frequency actually used for the calculation. Check it before interpreting the result. A frequency which merely belongs to a possible band is still only an approximation if the station is expected to operate elsewhere.
This matters particularly on the microwave bands. The Fresnel zone becomes smaller as frequency increases, while free-space path loss and feeder loss increase. A calculation performed for the wrong band may therefore look plausible while describing a different radio path.
### Link budget and propagation assessment
The link-budget estimate uses:
- the configured local and remote transmit powers,
- the configured antenna gains,
- estimated feeder losses,
- free-space path loss, and
- a rough additional loss derived from the terrain obstruction.
Antenna gains must be entered in dBi. Values specified in dBd must first be converted.
The calculation produces an estimated received power and a bidirectional SSB margin. The result is also made available to the Reachability calculation used by the **Tropo** column and the corresponding filter in the main window.
The map and the table use the same `ReachabilityService` and calculation cache. A result calculated for the map can therefore also become available to the user list without repeating the complete request.
KST4Contest deliberately does not request an online terrain profile for every visible chat member whenever the list changes. That would create unnecessary API traffic and make normal chat processing dependent on a large number of external requests. Select the required station on the map or use **Calc selected** when a current calculation is needed.
### Compact view
The lower analysis panel can be hidden with **Hide path analysis** and restored with **Show path analysis**. Its visibility is stored in the preferences and restored at the next start.
The divider between the map and the analysis panel can be moved to allocate more space to either section. Hiding the analysis panel does not discard the selected station or close the map.
Operation of the map window is described under [Station Map](en-User-Interface#station-map).
Configuration of antenna height, power and gain is described under [Path Analysis and Link Budget](en-Configuration#path-analysis-and-link-budget).
### Limits of the result
The path analysis is an engineering estimate. Among other things, it does not know:
- the actual antenna height and station setup of the remote operator,
- vegetation, buildings and other clutter which is not represented in the elevation data,
- the current refractivity profile of the atmosphere,
- ducting, scattering or reflection conditions,
- local interference or receiver performance, or
- whether a detected QRG is still in use.
The **Mechanisms** indication lists propagation mechanisms which may be consistent with the calculated geometry. It does not prove that one of them is currently available.
Aircraft Scatter information is not currently coupled to the terrain-profile calculation. AirScout data and the path analysis may both describe the same remote station, but they remain separate assessments.
OpenStreetMap tiles and the active elevation provider require an Internet connection. Leaflet and the map application itself are bundled locally, and tile requests pass through a local proxy, but this proxy is not a permanent offline map store.
In plain terms: the analysis helps to identify plausible paths, obvious obstructions and incorrect assumptions. It does not replace propagation experience or a real signal.
---
## Bounded Message Stores (from v1.41)
During a long contest, KST4Contest may receive tens of thousands of chat and DX cluster messages. If these lists were allowed to grow without limit for the complete runtime, memory consumption would not be the only problem. Filtering, sorting and updating the tables built on top of them would also become increasingly expensive.
KST4Contest therefore uses two separate bounded message stores:
| Message store | Clean-up starts above | Size after clean-up |
|---|---:|---:|
| Chat messages | 30,000 entries | 25,000 entries |
| DX cluster messages | 10,000 entries | 8,000 entries |
New messages are inserted at the beginning of the respective list. When the upper limit is exceeded, KST4Contest removes the oldest entries from the end until the specified target size is reached.
### Why are there two thresholds?
The store is not reduced to its maximum size again after every single incoming message. After a clean-up, the chat-message store has room for another 5,000 entries and the DX cluster store for another 2,000.
KST4Contest therefore removes old entries in batches instead of modifying the end of the list again for every subsequent message. The clean-up runs much less frequently as a result.
### Which tables share a store?
The following views are filtered representations of the same global chat-message list:
- **Public messages**,
- the private-message table,
- the messages in the **Further Info** panel, and
- **QSO of the other**.
These tables do not each retain another 30,000 messages. When an old chat message is removed from the shared store, it disappears from all views based on that store at the same time.
The **DXCluster messages** tab and the DX cluster table in the separate monitor window likewise use the same cluster-message store. Opening the additional window neither creates a second message connection nor duplicates the received messages.
The chat and DX cluster stores are independent of each other. Heavy public-chat traffic therefore does not reduce the capacity available for DX cluster messages, and vice versa.
### No permanent history
Both message stores exist in memory only. They are written neither to the internal Worked database nor to another local message file.
After restarting KST4Contest, the tables begin with empty lists and are rebuilt exclusively from newly received messages. These views are working tools for the current session, not a permanent chat archive.
---
## Station Map (from v1.41)
## Screen-Aware Main Window Sizing (from v1.41)
An interactive OpenStreetMap-based map showing the geographic position of all active chat members.
KST4Contest stores the most recently used size of the main window. This is useful as long as the application is started on a comparable display the next time. If it was previously used on a larger monitor, however, the stored size may extend beyond the visible area of a smaller screen.
**Features:**
KST4Contest therefore checks the stored size against the usable area of the primary screen during startup.
- Station markers with callsign labels, coloured by activity and sked state
- Antenna **beam cone** visualisation for the own station
- **Connection line** to the currently selected station
- **Maidenhead grid** overlay (QRA locator grid)
- **Path profile chart**: Terrain elevation cross-section between own station and the selected station, including Fresnel zone analysis and obstruction/horizon detection
- Multiple terrain data sources: **Copernicus GLO-30** (high-resolution DEM), **Open-Meteo API**, synthetic fallback, and **offline DEM import** for air-gapped use
- Aircraft scatter path analysis integrated with the terrain data
### How is the startup size determined?
The map works in packaged environments (AppImage, Flatpak) without internet access to external CDNs: map tiles are fetched via a local tile proxy, and the Leaflet.js library is bundled inside the application.
If the stored values are valid, KST4Contest initially uses the last saved height and width. If no usable values are available, the following default size is used:
---
- 1,234 pixels wide and
- 768 pixels high.
## Optimised Message Handling / 30,000 Message Limit (from v1.41)
KST4Contest does not use the complete screen resolution as the available area. It uses the visual bounds reported by JavaFX for the primary screen. Taskbars, docks and similar operating-system areas are already excluded from these bounds.
The internal chat and message tables are capped at **30,000 entries**. Older messages are automatically discarded when the limit is reached. This keeps memory usage and rendering performance stable during multi-day contest operations.
An additional safety margin of 40 pixels is subtracted. If the stored width or height exceeds the remaining space, only the affected value is reduced.
---
After the user interface has been built with this content size, KST4Contest checks the complete native operating-system window, including its title bar and borders. The window is reduced or moved into the visible area again if necessary.
## Screen-Aware Window Sizing (from v1.41)
This catches two different cases:
On startup, KST4Contest calculates a screen-aware size for the main window:
1. The stored content area is larger than the current screen.
2. The content area fits, but the complete native window still extends beyond the visible area because of its borders or position.
- The stored window size from the previous session is used but **never larger than the current screen**.
- If KST4Contest was last used on a larger monitor, the window is automatically scaled down to fit the current display without clipping.
- The UI layout is more **compact and responsive on smaller screens**, showing the same information in less space.
### What happens to the layout?
This prevents unusable oversized windows when switching between machines or monitors.
The complete interface is not scaled proportionally. Instead, the main window receives less space and the UI areas designed for this situation react to the available width.
The filter bar remains compact at normal window sizes. Its controls wrap into additional rows only when their actual required width no longer fits. The dividers can still be used to distribute the available space between the message and station areas.
### Limits of the automatic correction
The check always uses the **primary screen**. It does not restore the previous position on a particular secondary monitor.
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.
+20 -8
View File
@@ -50,15 +50,15 @@ Downloads, supported operating systems and installation methods are described in
## Manual version
This manual describes the stable release **v1.41.1**.
This manual describes the current stable release of KST4Contest.
Features or changes that are only available in the current development version are explicitly marked as **Nightly / v1.42**. If no such label is present, the description refers to the stable release.
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 the current stable release](https://github.com/praktimarc/kst4contest/releases/latest)
- [Nightly and automated builds](https://github.com/praktimarc/kst4contest/actions)
- [Download Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
- [GitHub releases](https://github.com/praktimarc/kst4contest/releases)
- [Version history](en-Changelog)
For contest operation, the stable release is generally the appropriate choice. Nightly builds contain newer fixes and features, but may change between builds. They are useful when a particular change needs testing. Trying one for the first time ten minutes before a contest is less useful.
The stable release is normally the appropriate choice for contest operation. Beta and Nightly builds contain newer fixes and functions, but may still change between builds. They are intended for testing specific changes. Ten minutes before a contest is usually not the ideal time for a first test.
---
@@ -80,7 +80,7 @@ For contest operation, the stable release is generally the appropriate choice. N
## Contact and support
- **Download:** [Current stable release](https://github.com/praktimarc/kst4contest/releases/latest)
- **Download:** [Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
- **Source code:** [praktimarc/kst4contest](https://github.com/praktimarc/kst4contest)
- **Bug reports and feature requests:** [GitHub Issues](https://github.com/praktimarc/kst4contest/issues)
- **Email:** praktimarc+kst4contest@gmail.com
@@ -112,6 +112,18 @@ Some file and directory names still use the technical name `praktiKST`. They ref
## Acknowledgements
KST4Contest has benefited considerably from feedback gathered during actual contest operation.
Many functions and corrections in KST4Contest originate from observations made during actual contest operation. Reports that describe not only what happened, but also under which conditions it happened, are particularly useful.
Special thanks go to Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), Viliam Petrik (OM0AAO, DX Cluster idea), Konrad Neitzel (DC9DJ, project structure), Andreas (DO5ALF, webmaster of funkerportal.de), Franz van Velzen (PE0WGA, testing), and all other testers and contributors.
Special thanks go to:
- Gianluca Costantino (IU3OAR)
- Alessandro Murador (IZ3VTH)
- Reczetár István (HA1FV)
- Viliam Petrik (OM0AAO) for the DX Cluster idea
- Konrad Neitzel (DC9DJ) for his work on the project structure
- Andreas (DO5ALF), webmaster of funkerportal.de
- Franz van Velzen (PE0WGA) for testing
- Philipp (DN9APW) for further development of KST4Contest and the CI/CD infrastructure
- all other testers and contributors who supplied reproducible reports, ideas and corrections
Not every suggestion can be implemented unchanged. Nevertheless, reports from real operation remain an important basis for deciding which problems should be solved first.
+91 -33
View File
@@ -2,7 +2,7 @@
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Log-Synchronisation)
KST4Contest automatically marks worked stations in the chat user list. Two basic methods are available:
KST4Contest imports worked stations from the logging application and derives the global Worked status, per-band Worked marks and where a locator is available worked grid squares. Three input paths are available: the file-based Simplelogfile interpreter, the general QSO UDP listener and the dedicated Win-Test network listener.
---
@@ -10,24 +10,25 @@ KST4Contest automatically marks worked stations in the chat user list. Two basic
## Method 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest reads a log file and searches for callsign patterns using a regular expression. Binary log files are also supported unreadable binary content is simply ignored.
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.
**Advantage**: Works with almost any logging program that writes a file.
**Disadvantage**: No band information available stations are only marked as "worked", not on which band.
The advantage is broad compatibility: no dedicated network interface is required from the logging application.
Enter the path to the log file in the Preferences. The file is only read, never modified (read-only).
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.
> **Tip**: The Simplelogfile function can also be used to mark stations that are definitely unreachable (e.g. personal notes). This will be replaced in a later version by a better tagging system.
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.
---
## Method 2: Network Listener (UDP Broadcast) Recommended
# Method 2: Network Listener for QSO UDP Packets Recommended
When saving a QSO, the logging software sends a UDP packet to the broadcast address of the home network. KST4Contest receives this packet and marks the station including **band information** in its internal SQLite database.
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.
> **Important**: KST4Contest must be **running in parallel with the logging software**. QSOs logged while KST4Contest is not running will not be captured except with QARTest (which can send the complete log).
If a band is available, the callsign is marked as worked on that band. If the packet also contains a valid locator, KST4Contest stores its four-character grid square for that band. Missing information is not inferred from unrelated fields.
**Default UDP port**: 12060 (matches the default of most logging programs)
KST4Contest must be running when the packet is transmitted. Some logging applications can, however, resend an existing log: QARTest provides **Invia log completo**, while DXLog.net sends `contactreplace` packets when broadcasting the complete log. KST4Contest processes both mechanisms.
**Default port:** `12060`
---
@@ -81,34 +82,81 @@ For the built-in DX cluster server: configure N1MM+ as a DX cluster client (serv
- Enter the IP of the KST4Contest computer (green-highlighted fields)
- Port: 12060
When broadcasting the complete logbook, DXLog.net uses `contactreplace` instead of `contactinfo`. KST4Contest processes both packet types. Older QSOs can therefore be imported by starting a complete-log broadcast while KST4Contest is running.
### Win-Test
Win-Test is supported with a dedicated UDP network listener that understands the native Win-Test network protocol.
Win-Test is connected through a dedicated UDP listener for the native Win-Test network protocol. This listener is independent of the general QSO UDP listener on port `12060`.
**Advantages of Win-Test Integration:**
- Automatic QSO synchronization to mark worked stations.
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet* automatically, as soon as the listener is active. No separate toggle is needed.
- You can choose between "AUTO", "SSB", or "CW" sked modes.
- **Automatic QRG resolution for SKEDs:** KST4Contest selects the sked frequency intelligently:
1. If the other station mentioned their QRG in a recent chat message, that frequency is used.
2. Otherwise, your own current QRG is used (from Win-Test STATUS or manual entry).
#### QSO and Worked synchronisation
**Settings in the "Log Synchronisation" tab:**
- Enable `Receive Win-Test network based UDP log messages`.
- `UDP-Port for Win-Test listener` (default: 9871).
- `KST station name in Win-Test network (src of SKED packets)`: Defines the station name KST4Contest uses in the WT network (e.g. "KST").
- `Win-Test network broadcast address`: Usually detected automatically; required to send sked packets to the network.
For a new QSO, KST4Contest imports:
**Settings in the "TRX Synchronisation" tab:**
- `Win-Test STATUS QRG Sync`: When enabled, KST4Contest takes the current transceiver frequency from the Win-Test STATUS packet and uses it as your own QRG (MYQRG).
- `Use pass frequency from Win-Test STATUS`: Instead of the main TRX frequency, the pass frequency contained in the STATUS packet is used as MYQRG (useful for multi-op setups that operate with a dedicated pass QRG).
- `Win-Test station name filter`: If a name is entered here (e.g. "STN1"), KST4Contest only processes packets from that specific Win-Test instance. Leave empty to accept all.
- the logged callsign,
- the native Win-Test band ID, and
- a valid locator where one is included in the packet.
Band IDs for 50 and 70 MHz are processed in the same way as the VHF, UHF and SHF bands. The callsign is marked as worked globally and on the detected band. If a locator is also available, its four-character grid square is stored for that band.
The information is written to the same internal database as Worked data received through the other QSO UDP interfaces and is restored after a restart.
#### Handing skeds over to Win-Test
Pressing **Create sked** first creates an internal KST4Contest sked. If the Win-Test network listener is enabled, KST4Contest then automatically attempts to send the sked to the Win-Test network as an `ADDSKED` packet.
The QRG is selected in the following order:
1. KST4Contest looks for the most recent QRG of the remote station on the explicitly selected band. The QRG must be no more than 30 minutes old. Active variants of the same base callsign are evaluated together.
2. If no such QRG is available, KST4Contest checks the local QRG of the chat category in which the sked was created. It is only used if it can be parsed and actually belongs to the selected band.
3. If neither source provides a matching QRG, no `ADDSKED` packet is sent.
A fixed replacement frequency such as `144.300` is deliberately not used. During a contest, a technically successful handover containing the wrong band or QRG is worse than a visibly omitted handover.
The internal sked remains intact in every case. This also applies when the broadcast address is invalid, the network fails or no Win-Test client can be reached.
#### Handling KST callsign suffixes
KST suffixes often identify a particular chat login or band. They are not necessarily part of the log callsign. KST4Contest therefore removes a suffix separated by `-` before handing the callsign over to Win-Test, while preserving portable and international callsign components:
| Callsign in the KST chat | Callsign passed to Win-Test |
|---|---|
| `DN9APW-2` | `DN9APW` |
| `9A0BB-70` | `9A0BB` |
| `EA5/G8MBI/P-70` | `EA5/G8MBI/P` |
| `DN9APW-2/P` | `DN9APW/P` |
The complete callsign remains available inside KST4Contest. The timeline, reminder PMs and chat category continue to refer to the login which was actually selected.
#### Mode, time and notes
The mode is selected explicitly as `SSB` or `CW` when the sked is created. It is not inferred automatically from the QRG because a limited list of assumed band segments cannot represent every supported VHF, UHF and SHF band reliably.
KST4Contest sends the actual scheduled time without adding an extra minute. Where available, the notes include the locator and QTF together with an indication that the sked was created through KST4Contest.
The handover consists of the Win-Test packets `LOCKSKED`, `ADDSKED` and `UNLOCKSKED`.
![Sked handed over from KST4Contest to Win-Test](wintest_sked_handover.png)
#### Settings
In the **Log sync** tab:
- `Receive Win-Test network based UDP log messages`
- `UDP-Port for Win-Test listener`, default `9871`
- `KST station name in Win-Test network (src of SKED packets)`
- `Win-Test network broadcast address`
In the **TRX sync** tab:
- `Win-Test STATUS QRG Sync`
- `Use pass frequency from Win-Test STATUS`
- `Win-Test station name filter`
The Win-Test network must be enabled in Win-Test. When several computers are used, the broadcast address must reach the relevant local network. The station name should identify the sending KST4Contest instance unambiguously within the Win-Test network.
Detailed settings: [Win-Test Network Listener](en-Configuration#win-test-network-listener-from-v131)
**Settings in Win-Test:**
- The network in Win-Test must be active.
- Win-Test must be configured to send/receive its broadcasts on the corresponding port (default 9871).
---
## TRX Frequency Synchronisation
@@ -144,6 +192,16 @@ For DM5M-style setups (2 radios, 2 computers, one KST4Contest instance or two se
## Internal Database
KST4Contest stores worked information in an internal **SQLite database**. This is independent of the logging program's database and is only populated via the UDP broadcast.
KST4Contest stores Worked, NOT-QRV and worked-grid information in its own SQLite database. This database is independent of the logging application's database.
Before each new contest: reset the database! → [Configuration Worked Station Database Settings](Configuration#worked-station-database-settings)
The input sources provide different levels of detail:
| Source | Global callsign status | Per-band status | Grid square |
|---|---:|---:|---:|
| Simplelogfile | yes | no | no |
| 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.
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.
+193 -21
View File
@@ -26,14 +26,29 @@ The central table of all currently active chat users. Columns (depending on conf
| Column | Content |
|---|---|
| Call | Station's callsign |
| Name | Name from the chat name field |
| Loc | Maidenhead locator |
| Callsign | Station callsign |
| Name | Name and additional information from the chat name field |
| QRA | Maidenhead locator |
| QRB | Distance in km |
| QTF | Direction in degrees |
| QRG | Automatically detected frequency |
| AP | AirScout aircraft data (when active) |
| Band colours | Worked / NOT-QRV status per band |
| QRG | Most recent frequency detected in a chat message |
| Tropo | Result of the band-specific tropo or path assessment |
| Score | Current, numerically sortable priority score of the normalised base callsign |
| Act | Minutes since the most recent activity |
| AP | AirScout aircraft data, when enabled |
| worked | Per-band Worked, band-opportunity and grid-square status, plus `wkdany` |
| NOT QRV @ | Bands on which the station has manually been marked not QRV |
| Category | Chat category of this entry |
### Worked, band and grid-square status
The subcolumns under **worked** use compact codes because several enabled bands leave little room for full descriptions. `X` marks a callsign worked on that band. `a` and `B+` identify an offered band which has not yet been worked. An appended `o` means that the four-character grid square has already been worked on this band.
The **wkdany** subcolumn is band-independent: `x` means that the callsign has been worked, `o` means that the grid square has been worked on any band, and `xo` means both.
Each status cell has a tooltip containing the legend and the state derived for that station. For the complete calculation, including NOT-QRV precedence, see [Worked Callsigns, New Bands and New Grid Squares](en-Features#worked-callsigns-new-bands-and-new-grid-squares).
![Band-specific Worked status and worked grid squares](worked_band_status.png)
**Sorting**: Click column headers. QRB sorting is numerical (corrected in v1.22).
@@ -53,53 +68,210 @@ Input field for the current antenna direction. Used for the planned `MYQTF` vari
## Filters
The filter bar (from v1.21 as a flowpane for small screens):
The filter bar is located above the chat-member table and groups related controls:
- **Show only QTF**: Activate direction filter (N/NE/E/… buttons or degree input)
- **Show only QRB [km] <=**: Activate distance filter (toggle button)
- **Hide Worked [Band]**: Hide worked stations per band (one toggle per band)
- **Hide NOT-QRV [Band]**: Hide NOT-QRV-tagged stations per band
- **Show only QTF** limits the list to a selected antenna direction.
- **Show only QRB [km] <=** sets a maximum distance.
- **Find** searches for a callsign.
- **wkd** hides callsigns which have already been worked on at least one band.
- The individual band buttons hide a station if it has already been worked on that band or has been marked NOT QRV there. Only bands enabled for the local station are shown.
- **Only new grids** shows only stations in four-character grid squares which have not been worked on any band.
- **Grid color** is not a filter. It marks the QRA cell of an already worked grid square without hiding stations.
- **New bands** shows stations with at least one detected, locally enabled and unworked band opportunity. NOT-QRV marks take precedence.
- **Reachability**, **Tropo >=0dB** and **AS next 5m** limit the list according to the selected path or AirScout criteria.
The filter bar has no fixed width. QTF, Worked and Reachability controls initially use the available space in their respective rows. When the horizontal divider is moved to the right and the chat-member area becomes narrower, controls wrap only when their actual required width no longer fits.
![Wrapped filter bar in a narrow chat-member view](filter_bar_wrapped.png)
In plain terms: the filters determine the table contents, but no longer enforce the minimum width of the entire right-hand side. The bar remains compact in the normal layout and uses additional height only when the view becomes genuinely narrow. Moving the divider back to the left immediately returns the controls to the available rows.
---
## Station Info Panel (Further Info)
Bottom right: Shows all messages of a selected station (CQ messages and PMs in one panel). A message filter can be pre-configured via the default filter in the Preferences.
The lower-right panel combines the messages associated with the selected station. This includes public messages, private messages to the local station and, where visible in the chat, private messages addressed to other stations.
**Sked reminders** can also be activated here.
The selected filter controls which of these messages are displayed. Under **Settings → GUI**, the default filter can be set to:
- all messages,
- private messages to the local station,
- private messages to other stations, or
- public messages.
This setting changes the Further Info display only. Messages are neither discarded nor removed from the other message tables, and the filter can be changed at any time for the currently selected station.
The lower part of the panel contains per-band **Not QRV** marks for the selected station. Individual controls are shown for the bands enabled in the local station settings. **tag not qrv all** sets or removes the mark for every supported band, including bands which are not currently visible.
The change immediately affects the **NOT QRV @** column, band opportunities and the corresponding filters. It is stored in the internal database and restored after a restart.
![Per-band NOT-QRV marks in the Further Info panel](not_qrv_controls.png)
The current **Priority score** of the selected station is displayed in the same section.
**Sked fail** marks an unsuccessful attempt and strongly reduces the score of the normalised base callsign. **Reset fail** removes the mark. It applies to all active suffix and category variants of the station and remains active for the current program session.
The controls underneath are used to create a sked:
| Control | Meaning |
|---|---|
| **Sked in** | Time remaining until the sked |
| **Band** | Agreed band selected from the locally enabled bands |
| **Mode** | `SSB` or `CW` for a possible Win-Test handover |
| **Create sked** | Create the internal sked |
| **Remind-PM in** | Enable automatic reminder PMs |
| **2+1**, **5+2+1**, **10+5+2+1** | Times at which reminder PMs are sent before the sked |
![Sked controls in the Further Info section](sked_controls.png)
The proposed band is derived from recent QRG and name information for the station. It can be changed explicitly before creating the sked. The mode selection only affects the Win-Test handover; the internal sked and reminder PMs work independently.
**Create sked** always creates the appointment inside KST4Contest first. If the Win-Test network listener is active, KST4Contest then attempts an additional handover to Win-Test. If no QRG matching the selected band can be found or Win-Test cannot be reached, the internal sked, its priority contribution and any scheduled reminders remain intact.
The complete derivation and limitations are described under [Skeds and Sked Reminders](en-Features#skeds-and-sked-reminders).
---
## Priority List
Shows the top candidates calculated by the Score Service. Updates automatically in the background based on direction, distance and AP availability.
The compact priority bar is located on the right-hand side between the user list and the Further Info section. It displays the two currently highest-ranked candidates directly in the main window:
```text
Priority: 1 CALLSIGN SCORE 2 CALLSIGN SCORE more
```
Clicking either candidate selects the corresponding active chat member. The complete callsign, including its suffix and chat category, is used.
The **more** button opens a separate window containing up to 15 candidates. The list is sorted by descending score. Double-clicking an entry selects the candidate and closes the window.
![Priority Score, compact candidate list and Further Info controls](priority_score_overview.png)
Stations with a score of `0` are not included in the priority list. They remain visible in the user list so that the reason for their exclusion can be examined and, for example, an incorrect NOT-QRV mark can be changed.
The score is calculated for the normalised base callsign. Several active variants such as `9A0BB-2` and `9A0BB-70` may therefore display the same value in the user list. They nevertheless remain separate message targets.
New messages, AirScout data, skeds and status changes request a new calculation. A periodic refresh also runs in the background. A briefly outdated order is therefore not an error.
Calculation and limitations: [Priority Score and Priority List](en-Features#priority-score-and-priority-list-from-v140).
---
## Cluster & QSO of Others
## Station Map
Separate window (can be minimised). Shows the communication flow between other stations interesting during quieter contest periods.
The station map is opened or closed through:
**Windows → Show / hide station map**
The window uses the chat members currently visible in the filtered user list. Changing the QRB, QTF, Worked, band or Reachability filters can therefore also change the stations shown on the map.
A station can additionally be opened directly from the **Further Info** panel using **Show on map**. This selects the station on the map and requests the associated path analysis.
Stations with the same normalised base callsign and position are combined into one marker. At lower zoom levels, nearby markers may additionally be displayed as clusters. These are display groups only; the individual chat logins remain separate message targets inside KST4Contest.
Clicking a station marker:
1. selects the corresponding chat member,
2. scrolls the main user list to that entry,
3. updates the **Further Info** panel, and
4. prepares the complete visible callsign as the message target.
The map details for the selected station include its locator, QRB, QTF, detected bands and available band opportunities. **Trigger cluster spot** sends a spot through the built-in local DX Cluster server so that connected logging software can receive the selected station and QRG.
The path-analysis section shows the terrain profile and the calculated route between both stations. Depending on the available data, it includes:
- the analysis frequency,
- line-of-sight and horizon information,
- Fresnel-zone clearance,
- detected obstructions,
- an estimated link budget,
- received power and SSB margin, and
- a short assessment of the path.
Moving the mouse over the terrain profile highlights the corresponding geographical position on the map.
The analysis can be hidden using **Hide path analysis** when more space is required for the map. The compact state displays **Path analysis is hidden.** together with the **Show path analysis** button.
![Station map with hidden path analysis](station_map_compact.png)
The selected station and map contents remain available while the analysis panel is hidden. The setting is stored and restored at the next start.
Calculation method and limitations: [Station Map and Path Analysis](en-Features#station-map-and-path-analysis-from-v141).
---
## Global Message Tabs and Monitor Window
Three global message tabs are located below the main user list. Unlike the **Further Info** panel, their contents do not depend on the station currently selected.
| Tab | Displayed messages |
|---|---|
| **Public messages** | All public chat messages, including CQ calls and beacons |
| **DXCluster messages** | DX cluster messages received from the ON4KST server |
| **QSO of the other** | Directed messages between chat logins other than the local station |
The **Public messages** tab is selected by default.
![Global message tabs below the main user list](global_message_tabs.png)
The **DXCluster messages** table contains the time, reporting and reported stations, locators, QRG, message text and global Worked state where these values are available in the received message.
The **QSO of the other** table contains:
- the complete sender and receiver callsigns,
- the latest QRG currently known for each station,
- the global Worked state of each station,
- the message text, and
- the chat category.
The displayed QRG is not necessarily the frequency on which the stations intend to make a contact. It is the latest QRG currently associated with the respective chat member. The Worked state is global and not specific to the displayed QRG or band.
A directed chat message in this table does not prove that a radio QSO has taken place. The table also contains sked requests, frequency exchanges and other directed messages between third-party chat logins.
### Separate monitor window
The DX cluster and QSO-of-the-other tables can also be displayed together in a separate window.
![Separate monitor window for DX cluster traffic and directed messages between other stations](cluster_qso_monitor.png)
The separate window and the tabs use the same underlying messages. Hiding the window does not stop message processing or remove messages from the tabs.
Use **Windows → Hide cluster / stranger QSOs** to hide the window and **Windows → Show cluster / stranger QSOs** to restore it.
If a message is too long for its table cell, moving the mouse over the cell displays the complete text in a tooltip. Links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
---
## Menu
### Window
- **Use Dark Mode** (from v1.26): Toggle dark colour scheme on/off.
### Windows
- **Hide cluster / stranger QSOs** hides the separate monitor window for DX cluster messages and directed messages between other stations.
- **Show cluster / stranger QSOs** restores the monitor window.
- **hide options** hides the settings window.
- **show options** restores the settings window.
- **Use dark mode design** activates the dark colour scheme.
- **Use default mode design** restores the default colour scheme.
- **Show / hide station map** opens or closes the separate station-map and path-analysis window.
---
## Window Sizes and Dividers
From **v1.21**, clicking **"Save Settings"** also saves window sizes and divider positions of all panels in the configuration file, which are restored on the next start.
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.
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**.
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 you encounter display problems: delete the configuration file → KST4Contest creates new default values.
---
## Operating Tips
- **Keep the settings window open**: Quick access to enable/disable the beacon.
- **Right-click in the user list**: Opens the snippet menu and further actions (QRZ.com profile, set NOT-QRV tags).
- **Right-click in the user list**: Opens the snippet menu and other context actions.
- **Mark a station NOT QRV**: Select the station and use the per-band controls in the **Further Info** panel.
- **Enter from anywhere**: When text is in the send field, Enter sends directly even if the focus is elsewhere.
- **Stop the beacon**: Switch off the beacon while scanning frequencies to avoid flooding the chat with messages.
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+1013
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+1
View File
@@ -445,6 +445,7 @@
<addmodule>java.sql</addmodule>
<addmodule>java.net.http</addmodule>
<addmodule>jdk.crypto.ec</addmodule>
<addmodule>jdk.net</addmodule>
</addmodules>
<mainclass>${main.class}</mainclass>
<input>${project.build.directory}/modules</input>
@@ -5,13 +5,19 @@ import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import kst4contest.model.Band;
import kst4contest.model.ChatMember;
/**
* Sends periodical path requests and an AirScout watchlist for the currently
* active ON4KST stations.
@@ -24,8 +30,17 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
private static final String BROADCAST_ADDRESS = "255.255.255.255";
private final ChatController client;
/*
* ASWATCHLIST is sent as one common list. Remember one syntactically valid
* AirScout band value so an empty list can still be sent on a later cycle
* to remove stations which are no longer active.
*/
private String lastWatchListBandValue;
public AirScoutPeriodicalAPReflectionInquirerTask(
ChatController client
) {
@@ -50,9 +65,6 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
client.getChatPreferences().getAirScout_asClientNameString();
String serverIdentifier =
client.getChatPreferences().getAirScout_asServerNameString();
String bandValue =
client.getChatPreferences().getAirScout_asBandString();
String ownCallSign = normalizeOwnCallSign(
client.getChatPreferences().getStn_loginCallSign()
);
@@ -79,14 +91,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
+ "\" \"" + serverIdentifier + "\" ";
String ownStation = ownCallSign + "," + ownLocator;
StringBuilder watchListMessage = new StringBuilder(
watchListPrefix
+ bandValue
+ ","
+ ownStation
);
List<ChatMember> activeMembers = client.snapshotChatMembers();
List<String> watchListTargets = new ArrayList<>();
Set<String> processedCallsigns = new LinkedHashSet<>();
String watchListBandValue = null;
int port = client.getChatPreferences()
.getAirScout_asCommunicationPort();
@@ -102,11 +110,38 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
continue;
}
if (member.getQrb()
if (member.getQrb() == null
|| member.getQrb()
>= client.getChatPreferences().getStn_maxQRBDefault()) {
continue;
}
String callsignKey = member.getCallSignRaw();
if (callsignKey == null || callsignKey.isBlank()) {
callsignKey = member.getCallSign();
}
if (callsignKey == null
|| !processedCallsigns.add(
callsignKey.trim().toUpperCase(Locale.ROOT)
)) {
continue;
}
/*
* The resolver may deliberately return an exact QRG. AirScout must
* only see a canonical protocol band value such as 4320000.
*/
String bandValue = canonicalizeAirScoutBandValue(
client.resolveAirScoutBandValue(member)
);
if (bandValue == null) {
continue;
}
if (watchListBandValue == null) {
watchListBandValue = bandValue;
}
String targetStation =
member.getCallSign() + "," + member.getQra();
@@ -126,19 +161,47 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
pathQuery
);
watchListMessage
.append(",")
.append(targetStation);
watchListTargets.add(targetStation);
}
watchListMessage.append(" ");
/*
* AirScout keeps one watchlist per client/server pair. Do not send
* separate lists for the individual station bands because a later
* list would replace stations from an earlier one.
*
* If there are no targets in this cycle, reuse the last valid band
* token and send an empty list so AirScout can clear stale entries.
*/
if (watchListBandValue == null) {
watchListBandValue = lastWatchListBandValue;
}
if (watchListBandValue != null) {
StringBuilder watchListMessage = new StringBuilder(
watchListPrefix
+ watchListBandValue
+ ","
+ ownStation
);
for (String targetStation : watchListTargets) {
watchListMessage
.append(",")
.append(targetStation);
}
watchListMessage.append(" ");
sendPacket(
socket,
broadcastAddress,
port,
watchListMessage.toString()
);
lastWatchListBandValue = watchListBandValue;
}
sendPacket(
socket,
broadcastAddress,
port,
watchListMessage.toString()
);
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
@@ -148,6 +211,60 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
}
}
/**
* Converts a frequency-like value returned by the station resolver into the
* canonical band token expected by the AirScout UDP protocol.
*
* <p>The internal resolver may keep an exact working frequency for path
* analysis. This method removes that precision only at the AirScout protocol
* boundary. For example, {@code 4321740} is sent to AirScout as
* {@code 4320000}.</p>
*
* @param resolvedValue frequency-like AirScout value produced by the resolver
* @return canonical AirScout band value, or {@code null} if unsupported
*/
private String canonicalizeAirScoutBandValue(String resolvedValue) {
if (resolvedValue == null || resolvedValue.isBlank()) {
return null;
}
String normalizedValue = resolvedValue.trim();
if ("off".equalsIgnoreCase(normalizedValue)
|| "auto".equalsIgnoreCase(normalizedValue)) {
return null;
}
final long numericValue;
try {
numericValue = Long.parseLong(normalizedValue);
} catch (NumberFormatException exception) {
LOGGER.log(
Level.WARNING,
"Unsupported AirScout band value: " + resolvedValue,
exception
);
return null;
}
double frequencyMHz = numericValue / 10_000.0;
Band band = Band.fromFrequency(frequencyMHz);
if (band == null) {
LOGGER.warning(
"AirScout query skipped because frequency "
+ frequencyMHz
+ " MHz does not belong to a supported band."
);
return null;
}
return band.getPrefix() + "0000";
}
/**
* Removes the ON4KST login suffix because AirScout expects the actual
* station callsign, for example 9A1W instead of 9A1W-2.
File diff suppressed because it is too large Load Diff
@@ -18,6 +18,9 @@ import kst4contest.ApplicationConstants;
import kst4contest.locatorUtils.DirectionUtils;
import kst4contest.locatorUtils.Location;
import kst4contest.model.*;
import kst4contest.logic.FrequencyTextParser;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.LongPredicate;
/**
*
@@ -36,6 +39,11 @@ public class MessageBusManagementThread extends Thread {
private PrintWriter writer;
// private Socket socket;
private ChatController client;
private final long connectionSessionId;
private final LinkedBlockingQueue<ChatMessage> receiveQueue;
private final LongPredicate connectionSessionIsActive;
// private File fileLogRAW;
// private TimerTask userActualizationTask; // Is used as a temporary userout-print
// private TimerTask userActualizationTask; //kst4contest.test 4 23001
@@ -58,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{3,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
"(?<![\\d])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
+ "|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])"
+ "|(?<=\\s|^)(\\d{3})(?=\\s|$)"
);
@@ -117,9 +125,22 @@ public class MessageBusManagementThread extends Thread {
}
public MessageBusManagementThread(ChatController client, ThreadStatusCallback callBack) {
this(client, callBack, 0L, client.getMessageRXBus(), ignored -> true);
}
public MessageBusManagementThread(
ChatController client,
ThreadStatusCallback callBack,
long connectionSessionId,
LinkedBlockingQueue<ChatMessage> receiveQueue,
LongPredicate connectionSessionIsActive
) {
this.callBackToController = callBack;
this.client = client;
this.connectionSessionId = connectionSessionId;
this.receiveQueue = receiveQueue;
this.connectionSessionIsActive = connectionSessionIsActive;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
@@ -319,16 +340,28 @@ public class MessageBusManagementThread extends Thread {
try {
String reconstructed =
candidateBand.getPrefix() + "." + foundRaw;
double candidateFrequency = Double.parseDouble(
normalizeFrequencyString(reconstructed)
);
candidateBand.getPrefix()
+ "."
+ foundRaw;
if (candidateBand.isPlausible(candidateFrequency)
FrequencyTextParser.DetectedFrequency detectedFrequency =
FrequencyTextParser.parseExplicitFrequency(
reconstructed
);
if (detectedFrequency != null
&& detectedFrequency.getBand() == candidateBand
&& info.timestampEpoch > bestTimestamp) {
finalDetectedFrequency = candidateFrequency;
finalDetectedBand = candidateBand;
bestTimestamp = info.timestampEpoch;
finalDetectedFrequency =
detectedFrequency.getFrequencyMHz();
finalDetectedBand =
candidateBand;
bestTimestamp =
info.timestampEpoch;
}
} catch (NumberFormatException ignored) {
// Try the next known band.
@@ -357,27 +390,40 @@ public class MessageBusManagementThread extends Thread {
try {
String reconstructed =
fallbackBand.getPrefix() + "." + foundRaw;
double candidateFrequency = Double.parseDouble(
normalizeFrequencyString(reconstructed)
);
fallbackBand.getPrefix()
+ "."
+ foundRaw;
if (fallbackBand.isPlausible(candidateFrequency)) {
finalDetectedFrequency = candidateFrequency;
finalDetectedBand = fallbackBand;
FrequencyTextParser.DetectedFrequency detectedFrequency =
FrequencyTextParser.parseExplicitFrequency(
reconstructed
);
if (detectedFrequency != null
&& detectedFrequency.getBand() == fallbackBand) {
finalDetectedFrequency =
detectedFrequency.getFrequencyMHz();
finalDetectedBand =
fallbackBand;
}
} catch (NumberFormatException ignored) {
// The matched value cannot be converted into a frequency.
}
}
} else {
try {
finalDetectedFrequency = Double.parseDouble(
normalizeFrequencyString(foundRaw)
);
finalDetectedBand = Band.fromFrequency(finalDetectedFrequency);
} catch (NumberFormatException ignored) {
// Continue with the next possible match in the message.
FrequencyTextParser.DetectedFrequency detectedFrequency =
FrequencyTextParser.parseExplicitFrequency(
foundRaw
);
if (detectedFrequency != null) {
finalDetectedFrequency =
detectedFrequency.getFrequencyMHz();
finalDetectedBand =
detectedFrequency.getBand();
}
}
@@ -462,22 +508,22 @@ public class MessageBusManagementThread extends Thread {
* Example: "144.210.10" -> "144.21010"
* Example: "144.210" -> "144.210"
*/
private String normalizeFrequencyString(String rawInput) {
// Input is already guaranteed to have only dots as separators (commas replaced earlier)
int firstDotIndex = rawInput.indexOf(".");
if (firstDotIndex != -1) {
// Check if there are more dots after the first one
String decimalPart = rawInput.substring(firstDotIndex + 1);
if (decimalPart.contains(".")) {
// Remove all subsequent dots to make it a valid double
decimalPart = decimalPart.replace(".", "");
return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
}
}
return rawInput;
}
// private String normalizeFrequencyString(String rawInput) {
// // Input is already guaranteed to have only dots as separators (commas replaced earlier)
//
// int firstDotIndex = rawInput.indexOf(".");
//
// if (firstDotIndex != -1) {
// // Check if there are more dots after the first one
// String decimalPart = rawInput.substring(firstDotIndex + 1);
// if (decimalPart.contains(".")) {
// // Remove all subsequent dots to make it a valid double
// decimalPart = decimalPart.replace(".", "");
// return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
// }
// }
// return rawInput;
// }
/**
@@ -496,7 +542,8 @@ public class MessageBusManagementThread extends Thread {
messageToProcess.setMessageText(reduce);
if (messageToProcess.getMessageText().isEmpty()) {
if (messageToProcess.getMessageText() == null
|| messageToProcess.getMessageText().isEmpty()) {
// System.out.println("[MSGBUSMGTT:] ###################### no processable data");
} else {
@@ -602,79 +649,176 @@ public class MessageBusManagementThread extends Thread {
}
/**
* Resolves a message sender from the thread-safe active-member model. If a
* CH/CR message arrives before the matching user-enter message, a marked
* fallback sender is used. The returned sender is never null.
* Resolves a message sender from the thread-safe active-member model.
*
* <p>The local login is handled before the active-user lookup because the own
* ChatMember is intentionally not stored in the visible user list. This also
* prevents another active login with the same base callsign from replacing the
* identity of our own message echo.</p>
*/
private ChatMember resolveInboundSender(String senderCallSign, ChatCategory category, ChatMessage message) {
private ChatMember resolveInboundSender(
String senderCallSign,
ChatCategory category,
ChatMessage message
) {
String myCall =
this.client.getChatPreferences().getStn_loginCallSign();
if (senderCallSign != null
&& myCall != null
&& senderCallSign.equalsIgnoreCase(myCall)) {
ChatMember ownSender = new ChatMember();
ownSender.setCallSign(senderCallSign);
ownSender.setChatCategory(category);
ownSender.setAirPlaneReflectInfo(
new AirPlaneReflectionInfo()
);
return ownSender;
}
ChatMember lookup = new ChatMember();
lookup.setCallSign(senderCallSign);
lookup.setChatCategory(category);
ChatMember senderObj = this.client.findActiveChatMember(lookup);
ChatMember senderObj =
this.client.findActiveChatMember(lookup);
if (senderObj != null) {
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
senderObj.setActivityTimeLastInEpoch(
new Utils4KST().time_generateCurrentEpochTime()
);
// Remember the last active category so later outgoing replies can be routed correctly.
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
this.client.rememberLastInboundCategory(
senderObj.getCallSignRaw(),
senderObj.getChatCategory()
);
// Metrics influence priority scoring; process them after message text is known.
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
message == null ? null : message.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
myCall
);
this.client.getScoreService().requestRecompute(
"rx-chat-message"
);
this.client.getScoreService().requestRecompute("rx-chat-message");
return senderObj;
}
ChatMember fallbackSender = new ChatMember();
String myCall = this.client.getChatPreferences().getStn_loginCallSign();
if (senderCallSign != null && senderCallSign.equalsIgnoreCase(myCall)) {
fallbackSender.setCallSign(myCall);
} else {
fallbackSender.setCallSign("[n/a]" + senderCallSign);
}
fallbackSender.setCallSign("[n/a]" + senderCallSign);
fallbackSender.setChatCategory(category);
fallbackSender.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
fallbackSender.setAirPlaneReflectInfo(
new AirPlaneReflectionInfo()
);
return fallbackSender;
}
/**
* Resolves a message receiver from the active-member model. Unknown receivers
* are represented as explicit fallback objects instead of null. This keeps PM
* echo display and historic messages stable even if the target station already
* left the chat.
* Resolves a message receiver from the thread-safe active-member model.
*
* <p>The local login is handled before the active-user lookup. The receiver
* from the ON4KST packet therefore remains authoritative even if another
* station with the same base callsign is logged into the same category.</p>
*/
private ChatMember resolveInboundReceiver(String receiverCallSign, ChatCategory category) {
if (receiverCallSign == null || receiverCallSign.equals("0")) {
private ChatMember resolveInboundReceiver(
String receiverCallSign,
ChatCategory category
) {
if (receiverCallSign == null
|| receiverCallSign.equals("0")) {
return createAllReceiver();
}
String myCall =
this.client.getChatPreferences().getStn_loginCallSign();
if (myCall != null
&& receiverCallSign.equalsIgnoreCase(myCall)) {
ChatMember ownReceiver = new ChatMember();
ownReceiver.setCallSign(receiverCallSign);
ownReceiver.setChatCategory(category);
ownReceiver.setAirPlaneReflectInfo(
new AirPlaneReflectionInfo()
);
return ownReceiver;
}
ChatMember lookup = new ChatMember();
lookup.setCallSign(receiverCallSign);
lookup.setChatCategory(category);
ChatMember receiverObj = this.client.findActiveChatMember(lookup);
ChatMember receiverObj =
this.client.findActiveChatMember(lookup);
if (receiverObj != null) {
return receiverObj;
}
ChatMember fallbackReceiver = new ChatMember();
String myCall = this.client.getChatPreferences().getStn_loginCallSign();
if (receiverCallSign.equalsIgnoreCase(myCall)) {
fallbackReceiver.setCallSign(myCall);
} else {
fallbackReceiver.setCallSign(receiverCallSign + "(left)");
}
fallbackReceiver.setCallSign(receiverCallSign + "(left)");
fallbackReceiver.setChatCategory(category);
fallbackReceiver.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
fallbackReceiver.setAirPlaneReflectInfo(
new AirPlaneReflectionInfo()
);
return fallbackReceiver;
}
private boolean validateInboundUserFrame(String[] fields) {
if (fields == null || fields.length < 6) {
logRejectedInboundUserFrame(
"Ignoring truncated ON4KST user frame", fields);
return false;
}
if (fields[2] == null || fields[2].isBlank()) {
logRejectedInboundUserFrame(
"Ignoring ON4KST user frame with empty callsign", fields);
return false;
}
try {
On4KstProtocol.category(Integer.parseInt(fields[1]));
On4KstProtocol.locator(fields[4]);
Integer.parseInt(fields[5]);
return true;
} catch (IllegalArgumentException invalidUser) {
logRejectedInboundUserFrame(
"Ignoring malformed ON4KST user '" + fields[2]
+ "': " + invalidUser.getMessage(), fields);
return false;
}
}
/**
* Records a rejected user frame without writing the complete raw frame or user
* name to the diagnostic log.
*
* <p>The opcode, category and callsign are sufficient to identify the offending
* list position. Omitting the remaining fields avoids unnecessary disclosure of
* free-form profile text.</p>
*/
private void logRejectedInboundUserFrame(String reason, String[] fields) {
String opcode = fields != null && fields.length > 0 ? fields[0] : "UNKNOWN";
String category = fields != null && fields.length > 1 ? fields[1] : "UNKNOWN";
String callsign = fields != null && fields.length > 2 ? fields[2] : "UNKNOWN";
client.onOn4KstConnectionWarning(
reason + "; opcode=" + opcode
+ ", category=" + category
+ ", callsign=" + callsign
+ ", fieldCount=" + (fields == null ? 0 : fields.length));
}
/**
* Processes received messages via port 23001 (improved telnet Interface)
*
@@ -729,23 +873,33 @@ public class MessageBusManagementThread extends Thread {
* here we have a helper list for identifying questions for my qrg which can be autoanswered later
*/
if (messageToProcess.getMessageText().isEmpty()) {
// System.out.println("[MSGBUSMGTT:] no processable data");
if (messageToProcess.getMessageText() == null
|| messageToProcess.getMessageText().isEmpty()) {
// No processable data.
} else {
if (messageToProcess.getMessageText().contains(SRVR_LOGSTAT)) {
String logstatMessage[];
logstatMessage = messageToProcess.getMessageText().split("\\|");
if (logstatMessage[1].contains(SRVR_LOGINOK)) {
this.client.setConnectedAndLoggedIn(true);
} else {
this.client.setConnectedAndNOTLoggedIn(true);
this.client.setConnectedAndLoggedIn(false);
}
if (messageToProcess.getMessageText().startsWith(SRVR_LOGSTAT + "|")) {
String[] logstatMessage =
messageToProcess.getMessageText().split("\\|", -1);
this.client.onOn4KstLogstat(
connectionSessionId,
logstatMessage);
}
String splittedMessageLine[] = messageToProcess.getMessageText().split("\\|");
String[] splittedMessageLine =
messageToProcess.getMessageText().split("\\|");
String opcode = splittedMessageLine.length == 0
? ""
: splittedMessageLine[0];
if ((INITIALUSERLISTENTRY.equals(opcode)
|| USERENTEREDCHAT.equals(opcode)
|| USERENTEREDCHAT2.equals(opcode))
&& !validateInboundUserFrame(splittedMessageLine)) {
return;
}
// String splittedMessageLine[] = messageToProcess.getMessageText().split("\\|");
/**
* Initializes the Userlist if entry fits UA0
@@ -753,7 +907,7 @@ public class MessageBusManagementThread extends Thread {
*
*
*/
if (splittedMessageLine[0].contains(INITIALUSERLISTENTRY)) {
if (splittedMessageLine[0].equals(INITIALUSERLISTENTRY)) {
// System.out.println("MSGBUS: User detected");
ChatMember newMember = new ChatMember();
@@ -774,7 +928,9 @@ public class MessageBusManagementThread extends Thread {
if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) {
this.client.addOrUpdateActiveChatMember(newMember); // the own call will not be in the list
this.client.stageInitialOn4KstChatMember(
connectionSessionId,
newMember);
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
// Reachability is calculated on demand only: map click, selected station, or manual request.
}
@@ -798,7 +954,8 @@ public class MessageBusManagementThread extends Thread {
* UA2|2|W5ADD|Parker|EM40WL|2|
*
*/
if (splittedMessageLine[0].contains(USERENTEREDCHAT) || splittedMessageLine[0].contains(USERENTEREDCHAT2)) {
if (splittedMessageLine[0].equals(USERENTEREDCHAT)
|| splittedMessageLine[0].equals(USERENTEREDCHAT2)) {
// System.out.println("MSGBUS: User detected");
@@ -1114,27 +1271,31 @@ public class MessageBusManagementThread extends Thread {
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
try {
if (newMessageArrived.getSender().getFrequency() != null) {
//TODO: testing for next version 3.33: additional information will be displayed in cluster if there is such information
ChatMember sender = newMessageArrived.getSender();
String detectedFrequency = sender.getFrequency() == null
? null
: sender.getFrequency().getValue();
if (detectedFrequency != null && !detectedFrequency.isBlank()) {
/*
* The DX Cluster spot must not depend on AirScout data. A known
* frequency and a valid directional opportunity are sufficient.
* Available AP information is added only as an optional comment.
*/
ChatMember onlyForSpottingObject = new ChatMember();
onlyForSpottingObject.setCallSign(newMessageArrived.getSender().getCallSign());
onlyForSpottingObject.setFrequency(newMessageArrived.getSender().getFrequency());
onlyForSpottingObject.setCallSign(sender.getCallSign());
onlyForSpottingObject.setFrequency(sender.getFrequency());
onlyForSpottingObject.setQra(
buildDxClusterSpotComment(sender)
);
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 0) {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + " , AP: " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getArrivingDurationMinutes() + "min, " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getPotential() + "%");
this.client
.getDxClusterServer()
.broadcastSingleDXClusterEntryToLoggers(
onlyForSpottingObject
);
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 1) {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + "; " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getArrivingDurationMinutes() + "min, " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getPotential() + "%");
}
} else {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra());
}
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject);
}
} catch (Exception exception) {
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
@@ -1320,9 +1481,12 @@ public class MessageBusManagementThread extends Thread {
newDXCListSender3.setQra(splittedMessageLine[5]);
ChatMember newDXCListReceiver3 = new ChatMember();
// newDXCListReceiver3.setFrequency(splittedMessageLine[4]);
newDXCListReceiver3.setCallSign(splittedMessageLine[4]);
newDXCListReceiver3.setQra(splittedMessageLine[5]);
/*
* MA format:
* MA|0|epoch|sender|receiver|sender locator|receiver locator|
*/
newDXCListReceiver3.setQra(splittedMessageLine[6]);
dxcMsg3.setSender(newDXCListSender3);
dxcMsg3.setReceiver(newDXCListReceiver3);
@@ -1554,18 +1718,29 @@ public class MessageBusManagementThread extends Thread {
/**
* Userinfo-update: UE|2|22562|
*/
if (splittedMessageLine[0].contains(SRVR_USERLISTEND)) {
if (SRVR_USERLISTEND.equals(opcode)) {
if (splittedMessageLine.length < 2) {
System.out.println(
"[MSGBUSMGT, Warning:] Ignoring malformed UE frame: "
+ messageToProcess.getMessageText());
return;
}
// No worthy information, count of users
} else
this.client.onOn4KstInitialUserListCompleted(
connectionSessionId,
util_getChatCategoryByCategoryNrString(
splittedMessageLine[1]));
if (splittedMessageLine[0].contains(SRVR_DXCEND)) {
} else if (SRVR_DXCEND.equals(opcode)) {
// No worthy information, count of users
} else
// DF marks the end of the initial DX-cluster data.
// The frame contains no data that needs to be published.
} else if (SRVR_COMMUNICATIONK.equals(opcode)) {
// CK is a regular server delimiter/acknowledgement.
// It is intentionally accepted without further processing.
if (splittedMessageLine[0].contains(SRVR_COMMUNICATIONK)) {
// No worthy information, end of srvrmsgs
} else
//-> LOGSTAT|114|Wrong password!|
@@ -1638,6 +1813,70 @@ public class MessageBusManagementThread extends Thread {
}
}
/**
* Builds the comment transmitted with a local DX Cluster spot.
*
* <p>The station locator is always retained. AirScout information is optional:
* a missing response, an empty aircraft list or an incomplete aircraft entry
* must never prevent the spot itself from being sent.</p>
*
* @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) {
if (sender == null) {
return "";
}
String locator = sender.getQra() == null
? ""
: sender.getQra().trim();
AirPlaneReflectionInfo reflectionInfo =
sender.getAirPlaneReflectInfo();
if (reflectionInfo == null
|| reflectionInfo.getRisingAirplanes() == null
|| reflectionInfo.getRisingAirplanes().isEmpty()) {
return locator;
}
ArrayList<String> aircraftComments = new ArrayList<>();
int aircraftCount = Math.min(
2,
reflectionInfo.getRisingAirplanes().size()
);
for (int index = 0; index < aircraftCount; index++) {
AirPlane aircraft =
reflectionInfo.getRisingAirplanes().get(index);
if (aircraft == null) {
continue;
}
aircraftComments.add(
aircraft.getArrivingDurationMinutes()
+ "min, "
+ aircraft.getPotential()
+ "%"
);
}
if (aircraftComments.isEmpty()) {
return locator;
}
String apComment =
"AP: " + String.join("; ", aircraftComments);
return locator.isEmpty()
? apComment
: locator + " , " + apComment;
}
/**
* Method gets a String with a messagecategory-number and returns out of which of the existing categories
* (chat channels) this message/user had written from
@@ -1781,13 +2020,17 @@ public class MessageBusManagementThread extends Thread {
while (true) {
try {
messageTextRaw = client.getMessageRXBus().take();
messageTextRaw = receiveQueue.take();
if (messageTextRaw.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL) && messageTextRaw.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
client.getMessageRXBus().clear();
if (ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(messageTextRaw.getMessageText())
&& ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(messageTextRaw.getMessageSenderName())) {
receiveQueue.clear();
break;
}
else {
if (!connectionSessionIsActive.test(connectionSessionId)) {
break;
}
messageLine = messageTextRaw.getMessageText();
/***********************************************
@@ -0,0 +1,837 @@
package kst4contest.controller;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import jdk.net.ExtendedSocketOptions;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatMessage;
import kst4contest.model.ChatPreferences;
/**
* Owns the complete lifecycle of the single ON4KST TCP session.
*
* <p>Every reader, writer, queue and parser belongs to an immutable session id.
* A delayed failure from an old socket can therefore never close or consume data
* from its replacement.</p>
*/
final class On4KstConnectionManager {
private static final Logger LOGGER =
Logger.getLogger(On4KstConnectionManager.class.getName());
private static final DateTimeFormatter LIVE_MESSAGE_TIMESTAMP =
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
static final int CONNECT_TIMEOUT_MILLIS = 10_000; //TCP-Connect-Timeout
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
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
private final ChatController controller;
private final ScheduledExecutorService scheduler;
private final AtomicLong generation = new AtomicLong();
private final AtomicLong lastReceivedMessageTimestamp = new AtomicLong();
private volatile Session activeSession;
private volatile On4KstConnectionState state =
On4KstConnectionState.DISCONNECTED;
private volatile boolean stopRequested = true;
private int reconnectAttempt;
On4KstConnectionManager(ChatController controller) {
this.controller = controller;
this.scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "On4KstConnectionSupervisor");
thread.setDaemon(true);
return thread;
});
this.scheduler.scheduleAtFixedRate(
this::monitorActiveSession, 5L, 5L, TimeUnit.SECONDS);
LOGGER.fine("ON4KST connection supervisor initialized");
}
/**
* Returns the last lifecycle state published by the connection supervisor.
*
* @return current immutable connection-state value
*/
On4KstConnectionState getState() {
return state;
}
/**
* Verifies that a callback still belongs to the currently installed session.
*
* <p>Every reconnect receives a new id. Late EOF, write or parser callbacks from
* an obsolete socket therefore become harmless instead of closing the replacement
* connection.</p>
*
* @param sessionId id captured by the calling worker
* @return {@code true} only for the current, open and non-stopped session
*/
boolean isActiveSession(long sessionId) {
Session session = activeSession;
return session != null
&& session.id == sessionId
&& !session.closed
&& !stopRequested;
}
/**
* Starts a non-blocking connection attempt.
*
* <p>Configuration is validated before a socket is opened. A duplicate Connect
* action is ignored while another attempt or session is active. Connection work
* runs on the supervisor executor, so an unreachable server cannot block the
* JavaFX application thread.</p>
*/
void start() {
long token;
synchronized (this) {
if (!stopRequested && state.isConnectionAttemptActive()) {
return;
}
try {
validateConfiguration();
} catch (IllegalArgumentException invalidConfiguration) {
stopRequested = true;
transition(On4KstConnectionState.DISCONNECTED,
"Invalid ON4KST configuration: "
+ invalidConfiguration.getMessage(), true);
return;
}
stopRequested = false;
reconnectAttempt = 0;
token = generation.incrementAndGet();
transition(On4KstConnectionState.CONNECTING,
"Opening ON4KST connection", false);
}
scheduler.execute(() -> openConnection(token));
}
/**
* Stops the current session and invalidates every scheduled callback or reconnect
* belonging to it.
*/
void stopByUser() {
Session oldSession;
synchronized (this) {
stopRequested = true;
generation.incrementAndGet();
transition(On4KstConnectionState.STOPPING,
"Disconnecting from ON4KST", false);
oldSession = activeSession;
activeSession = null;
}
closeSession(oldSession);
controller.onOn4KstConnectionLost();
transition(On4KstConnectionState.DISCONNECTED,
"Disconnected by user", false);
}
/**
* Records one received protocol line as proof of application-level liveness.
*
* <p>TCP's {@code isConnected()} only states that a connection once succeeded.
* It does not prove that the peer is still reachable. Updating the inbound
* timestamp here gives the monitor a meaningful end-to-end signal.</p>
*
* @param sessionId immutable source-session id
* @param line complete protocol line received from ON4KST
*/
void onInboundActivity(long sessionId, String line) {
Session session = activeSession;
if (session == null || session.id != sessionId || session.closed) {
return;
}
long now = System.currentTimeMillis();
session.lastInboundMillis.set(now);
session.lastProgressMillis.set(now);
String opcode = opcode(line);
if ("CK".equals(opcode)) {
sendHeartbeat(session);
}
if (!session.loginSent
&& line != null
&& line.toLowerCase(Locale.ROOT).contains("login")) {
scheduler.execute(() -> sendLogin(sessionId));
}
if ("CH".equals(opcode) || "CR".equals(opcode)) {
recordHistoryTimestamp(line);
}
}
void onLogstat(long sessionId, String[] fields) {
String[] copy = fields == null ? new String[0] : fields.clone();
scheduler.execute(() -> handleLogstat(sessionId, copy));
}
void stageInitialChatMember(long sessionId, ChatMember member) {
Session session = activeSession;
if (session == null || session.id != sessionId || member == null
|| member.getChatCategory() == null || member.getCallSign() == null) {
return;
}
int category = member.getChatCategory().getCategoryNumber();
session.initialMembers
.computeIfAbsent(category, ignored -> new ConcurrentHashMap<>())
.put(member.getCallSign().trim().toUpperCase(Locale.ROOT), member);
session.lastProgressMillis.set(System.currentTimeMillis());
}
void onInitialUserListCompleted(long sessionId, ChatCategory category) {
if (category == null) {
return;
}
scheduler.execute(() -> completeInitialUserList(
sessionId, category.getCategoryNumber()));
}
private void openConnection(long token) {
if (!mayOpen(token)) {
return;
}
LOGGER.log(Level.INFO,
"Opening ON4KST TCP session {0}", token);
Socket socket = new Socket();
try {
ChatPreferences preferences = controller.getChatPreferences();
socket.connect(new InetSocketAddress(
preferences.getStn_on4kstServersDns(),
preferences.getStn_on4kstServersPort()),
CONNECT_TIMEOUT_MILLIS);
configureSocket(socket);
LOGGER.log(Level.INFO,
"ON4KST TCP session {0} connected to {1}",
new Object[] {token, socket.getRemoteSocketAddress()});
LinkedBlockingQueue<ChatMessage> receiveQueue =
new LinkedBlockingQueue<>();
LinkedBlockingQueue<ChatMessage> transmitQueue =
new LinkedBlockingQueue<>();
Session session = new Session(token, socket, receiveQueue, transmitQueue);
ReadThread readThread = new ReadThread(
token, socket, receiveQueue, this::isActiveSession,
line -> onInboundActivity(token, line),
failure -> onConnectionFailure(token, failure));
WriteThread writeThread = new WriteThread(
token, socket, transmitQueue,
controller.getChatPreferences().getLoginChatCategoryMain()
.getCategoryNumber(),
this::isActiveSession,
failure -> onConnectionFailure(token, failure),
controller::onOn4KstOutboundFrameRejected);
MessageBusManagementThread messageProcessor =
new MessageBusManagementThread(
controller, controller, token, receiveQueue,
this::isActiveSession);
session.readThread = readThread;
session.writeThread = writeThread;
session.messageProcessor = messageProcessor;
synchronized (this) {
if (!mayOpen(token)) {
closeSession(session);
return;
}
activeSession = session;
controller.installOn4KstSession(
token, socket, receiveQueue, transmitQueue,
readThread, writeThread, messageProcessor);
transition(On4KstConnectionState.WAITING_FOR_LOGIN_PROMPT,
"TCP connected; waiting for ON4KST login prompt", false);
}
messageProcessor.start();
writeThread.start();
readThread.start();
scheduler.schedule(
() -> sendLogin(token), LOGIN_FALLBACK_MILLIS,
TimeUnit.MILLISECONDS);
} catch (Exception exception) {
try {
socket.close();
} catch (IOException ignored) {
// The original connection exception is more useful.
}
scheduler.execute(() -> handleOpenFailure(token, exception));
}
}
private boolean mayOpen(long token) {
return !stopRequested && generation.get() == token;
}
private void sendLogin(long sessionId) {
Session session = activeSession;
if (session == null || session.id != sessionId || session.loginSent
|| session.closed || stopRequested) {
return;
}
try {
ChatPreferences preferences = controller.getChatPreferences();
int mainCategory = preferences.getLoginChatCategoryMain()
.getCategoryNumber();
long historyFrom = Math.max(
0L, lastReceivedMessageTimestamp.get() - 1L);
String login = On4KstProtocol.login(
preferences.getStn_loginCallSign(),
preferences.getStn_loginPassword(),
mainCategory,
"KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION,
historyFrom);
session.loginSent = true;
session.lastProgressMillis.set(System.currentTimeMillis());
LOGGER.log(Level.INFO,
"Sending ON4KST login for session {0}, main category {1}",
new Object[] {sessionId, mainCategory});
transition(On4KstConnectionState.AUTHENTICATING,
"ON4KST login sent", false);
sendControl(session, login);
} catch (IllegalArgumentException invalidConfiguration) {
failPermanently(session,
"Invalid ON4KST login configuration: "
+ invalidConfiguration.getMessage());
}
}
private void handleLogstat(long sessionId, String[] fields) {
Session session = activeSession;
if (session == null || session.id != sessionId || session.closed) {
return;
}
String code = fields.length > 1 ? fields[1] : "";
if (!"100".equals(code)) {
String serverText = fields.length > 2 ? fields[2] : "Login rejected";
failPermanently(session,
"ON4KST login rejected (" + code + "): " + serverText);
return;
}
if (session.authenticated) {
return;
}
session.authenticated = true;
session.lastProgressMillis.set(System.currentTimeMillis());
LOGGER.log(Level.INFO,
"ON4KST login accepted for session {0}", sessionId);
int mainCategory = controller.getChatPreferences()
.getLoginChatCategoryMain().getCategoryNumber();
transition(On4KstConnectionState.SYNCING_MAIN_CHAT,
"Login accepted; loading main chat", false);
sendControl(session, On4KstProtocol.settingsDone(mainCategory));
}
/**
* Publishes the initial user snapshot for one chat category exactly once.
*
* <p>ON4KST can send further {@code UE} frames after live user updates or
* after commands such as {@code SETNAME} and {@code BACK}. Those frames do
* not announce a new, empty snapshot. Treating them as another initial-list
* completion would remove the already published members because the staging
* map was consumed by the first {@code UE} frame.</p>
*
* <p>The completed-category set is updated before the staging map is removed.
* This makes the operation idempotent even if completion callbacks should
* later be invoked from more than one thread. A genuinely empty initial list
* remains valid: the first {@code UE} for a category is always processed,
* even when no preceding valid {@code UA0} frame was staged.</p>
*
* @param sessionId immutable id of the socket session that received the frame
* @param categoryNumber numeric ON4KST category terminated by {@code UE}
*/
private void completeInitialUserList(long sessionId, int categoryNumber) {
Session session = activeSession;
if (session == null || session.id != sessionId || session.closed) {
return;
}
if (!session.completedInitialUserLists.add(categoryNumber)) {
LOGGER.log(Level.FINE,
"ON4KST session {0}: ignoring duplicate user-list end "
+ "marker for category {1}; the initial snapshot "
+ "has already been published",
new Object[] {sessionId, categoryNumber});
return;
}
Map<String, ChatMember> staged =
session.initialMembers.remove(categoryNumber);
Collection<ChatMember> completeMembers = staged == null
? List.of()
: new ArrayList<>(staged.values());
LOGGER.log(Level.INFO,
"ON4KST session {0}: complete user list for category {1} "
+ "contains {2} valid users",
new Object[] {
sessionId,
categoryNumber,
completeMembers.size()
});
controller.replaceActiveChatMembersForCategory(
sessionId,
new ChatCategory(categoryNumber),
completeMembers);
ChatPreferences preferences = controller.getChatPreferences();
int mainCategory =
preferences.getLoginChatCategoryMain().getCategoryNumber();
if (categoryNumber == mainCategory && !session.mainListComplete) {
session.mainListComplete = true;
configureMainChat(session);
if (hasDistinctSecondChat(preferences)) {
int secondCategory =
preferences.getLoginChatCategorySecond()
.getCategoryNumber();
transition(
On4KstConnectionState.SYNCING_SECOND_CHAT,
"Main chat ready; loading second chat",
false);
sendControl(
session,
On4KstProtocol.addChat(
secondCategory,
Math.max(
0L,
lastReceivedMessageTimestamp.get() - 1L)));
} else {
markOnline(session);
}
return;
}
if (hasDistinctSecondChat(preferences)
&& categoryNumber
== preferences.getLoginChatCategorySecond()
.getCategoryNumber()
&& !session.secondListComplete) {
session.secondListComplete = true;
configureSecondChat(session);
markOnline(session);
}
}
private void configureMainChat(Session session) {
ChatPreferences preferences = controller.getChatPreferences();
int category = preferences.getLoginChatCategoryMain().getCategoryNumber();
sendControl(session, On4KstProtocol.setLocator(
category, preferences.getStn_loginLocatorMainCat()));
if (preferences.getStn_loginNameMainCat() != null
&& !preferences.getStn_loginNameMainCat().isBlank()) {
sendControl(session, On4KstProtocol.setName(
category, preferences.getStn_loginNameMainCat()));
}
sendControl(session, On4KstProtocol.back(category));
String secondLocator = preferences.getStn_loginLocatorSecondCat();
String mainLocator = preferences.getStn_loginLocatorMainCat();
if (preferences.isLoginToSecondChatEnabled()
&& secondLocator != null && !secondLocator.isBlank()
&& !secondLocator.equalsIgnoreCase(mainLocator)) {
controller.onOn4KstConnectionWarning(
"ON4KST uses one locator per TCP session. The second-chat locator '"
+ secondLocator + "' is ignored; using '" + mainLocator + "'.");
}
}
private void configureSecondChat(Session session) {
ChatPreferences preferences = controller.getChatPreferences();
int category = preferences.getLoginChatCategorySecond().getCategoryNumber();
if (preferences.getStn_loginNameSecondCat() != null
&& !preferences.getStn_loginNameSecondCat().isBlank()) {
sendControl(session, On4KstProtocol.setName(
category, preferences.getStn_loginNameSecondCat()));
}
sendControl(session, On4KstProtocol.back(category));
}
private void markOnline(Session session) {
if (!isActiveSession(session.id)) {
return;
}
reconnectAttempt = 0;
session.online = true;
session.lastProgressMillis.set(System.currentTimeMillis());
LOGGER.log(Level.INFO,
"ON4KST session {0} is authenticated and synchronized",
session.id);
transition(On4KstConnectionState.ONLINE,
"ON4KST session is authenticated and synchronized", false);
controller.onOn4KstConnectionOnline();
}
private void sendControl(Session session, String frame) {
if (session == null || !isActiveSession(session.id)) {
return;
}
ChatMessage message = new ChatMessage();
message.setMessageDirectedToServer(true);
message.setMessageText(frame);
session.transmitQueue.offer(message);
}
private void sendHeartbeat(Session session) {
if (session == null || !isActiveSession(session.id)) {
return;
}
long now = System.currentTimeMillis();
session.lastHeartbeatMillis.set(now);
LOGGER.log(Level.FINE,
"Sending application heartbeat for ON4KST session {0}",
session.id);
ChatMessage heartbeat = new ChatMessage();
heartbeat.setMessageDirectedToServer(true);
heartbeat.setMessageText("");
session.transmitQueue.offer(heartbeat);
}
private void onConnectionFailure(long sessionId, Throwable failure) {
scheduler.execute(() -> failSession(sessionId, failure));
}
private void failSession(long sessionId, Throwable failure) {
Session failedSession;
synchronized (this) {
failedSession = activeSession;
if (failedSession == null || failedSession.id != sessionId
|| failedSession.closed) {
return;
}
activeSession = null;
failedSession.closed = true;
}
closeSession(failedSession);
controller.onOn4KstConnectionLost();
if (stopRequested) {
transition(On4KstConnectionState.DISCONNECTED,
"ON4KST connection stopped", false);
return;
}
scheduleReconnect(failure);
}
private void handleOpenFailure(long token, Throwable failure) {
if (!mayOpen(token)) {
return;
}
controller.onOn4KstConnectionLost();
scheduleReconnect(failure);
}
private void scheduleReconnect(Throwable failure) {
if (stopRequested) {
transition(On4KstConnectionState.DISCONNECTED,
"ON4KST connection stopped", false);
return;
}
String reason = describeFailure(failure);
LOGGER.log(Level.WARNING,
"ON4KST connection lost; automatic reconnect scheduled", failure);
long delay = RECONNECT_DELAYS_MILLIS.get(Math.min(
reconnectAttempt, RECONNECT_DELAYS_MILLIS.size() - 1));
reconnectAttempt++;
transition(On4KstConnectionState.RECONNECT_WAIT,
"Connection lost (" + reason + "); reconnecting in "
+ Duration.ofMillis(delay).toSeconds() + " s", true);
long nextToken = generation.incrementAndGet();
scheduler.schedule(() -> {
if (!mayOpen(nextToken)) {
return;
}
transition(On4KstConnectionState.CONNECTING,
"Reconnecting to ON4KST", false);
openConnection(nextToken);
}, delay, TimeUnit.MILLISECONDS);
}
private void failPermanently(Session session, String reason) {
if (session == null || !isActiveSession(session.id)) {
return;
}
stopRequested = true;
generation.incrementAndGet();
activeSession = null;
session.closed = true;
closeSession(session);
controller.onOn4KstConnectionLost();
LOGGER.log(Level.WARNING, reason);
transition(On4KstConnectionState.DISCONNECTED, reason, true);
}
private void monitorActiveSession() {
try {
Session session = activeSession;
if (session == null || session.closed || stopRequested) {
return;
}
if (session.socket.isClosed()) {
failSession(session.id,
new SocketException("Socket is closed"));
return;
}
long now = System.currentTimeMillis();
if (!session.online
&& now - session.lastProgressMillis.get()
> HANDSHAKE_TIMEOUT_MILLIS) {
failSession(session.id,
new SocketException("ON4KST handshake timed out"));
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"));
return;
}
if (inboundIdle > APPLICATION_HEARTBEAT_AFTER_MILLIS
&& session.lastHeartbeatMillis.get()
< session.lastInboundMillis.get()) {
sendHeartbeat(session);
}
} catch (RuntimeException exception) {
LOGGER.log(Level.WARNING,
"ON4KST connection monitor failed", exception);
}
}
private void validateConfiguration() {
ChatPreferences preferences = controller.getChatPreferences();
On4KstProtocol.login(
preferences.getStn_loginCallSign(),
preferences.getStn_loginPassword(),
preferences.getLoginChatCategoryMain().getCategoryNumber(),
"KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION,
0L);
On4KstProtocol.locator(preferences.getStn_loginLocatorMainCat());
if (preferences.getStn_loginNameMainCat() != null
&& !preferences.getStn_loginNameMainCat().isBlank()) {
On4KstProtocol.field(
preferences.getStn_loginNameMainCat(), "main chat name");
}
if (preferences.isLoginToSecondChatEnabled()) {
if (preferences.getLoginChatCategorySecond() == null) {
throw new IllegalArgumentException("Second chat has no category");
}
On4KstProtocol.category(
preferences.getLoginChatCategorySecond().getCategoryNumber());
if (preferences.getLoginChatCategorySecond().getCategoryNumber()
== preferences.getLoginChatCategoryMain().getCategoryNumber()) {
controller.onOn4KstConnectionWarning(
"Second ON4KST chat equals the main chat and will not be added twice.");
}
if (preferences.getStn_loginNameSecondCat() != null
&& !preferences.getStn_loginNameSecondCat().isBlank()) {
On4KstProtocol.field(
preferences.getStn_loginNameSecondCat(), "second chat name");
}
}
}
private boolean hasDistinctSecondChat(ChatPreferences preferences) {
return preferences.isLoginToSecondChatEnabled()
&& preferences.getLoginChatCategorySecond() != null
&& preferences.getLoginChatCategorySecond().getCategoryNumber()
!= preferences.getLoginChatCategoryMain().getCategoryNumber();
}
private void configureSocket(Socket socket) throws IOException {
socket.setTcpNoDelay(true);
socket.setKeepAlive(true);
try {
socket.setOption(ExtendedSocketOptions.TCP_KEEPIDLE, 45);
socket.setOption(ExtendedSocketOptions.TCP_KEEPINTERVAL, 15);
socket.setOption(ExtendedSocketOptions.TCP_KEEPCOUNT, 3);
} catch (UnsupportedOperationException | IOException exception) {
LOGGER.log(Level.INFO,
"Platform does not support configurable TCP keepalive; "
+ "application heartbeat remains active", exception);
}
}
private void closeSession(Session session) {
if (session == null) {
return;
}
LOGGER.log(Level.FINE,
"Closing ON4KST session {0}", session.id);
session.closed = true;
if (session.readThread != null) {
session.readThread.interrupt();
}
if (session.writeThread != null) {
session.writeThread.interrupt();
}
if (session.messageProcessor != null) {
session.messageProcessor.interrupt();
}
try {
session.socket.close();
} catch (IOException exception) {
LOGGER.log(Level.FINE, "Error closing obsolete ON4KST socket", exception);
}
}
private void transition(
On4KstConnectionState newState,
String detail,
boolean critical
) {
On4KstConnectionState previousState = state;
state = newState;
Level level = critical
|| newState == On4KstConnectionState.DISCONNECTED
|| newState == On4KstConnectionState.RECONNECT_WAIT
? Level.WARNING : Level.INFO;
LOGGER.log(level,
"ON4KST state {0} -> {1}; detail: {2}",
new Object[] {previousState, newState, detail});
controller.updateOn4KstConnectionState(newState, detail, critical);
}
private void recordHistoryTimestamp(String line) {
long timestamp = parseMessageTimestamp(line);
if (timestamp > 0L) {
lastReceivedMessageTimestamp.accumulateAndGet(timestamp, Math::max);
}
}
static long parseMessageTimestamp(String line) {
String[] fields = line == null ? new String[0] : line.split("\\|", -1);
if (fields.length < 3) {
return 0L;
}
try {
long numeric = Long.parseLong(fields[2]);
long now = System.currentTimeMillis() / 1_000L;
if (numeric > 0L && numeric <= now + 86_400L) {
return numeric;
}
} catch (NumberFormatException ignored) {
return 0L;
}
try {
return LocalDateTime.parse(fields[2], LIVE_MESSAGE_TIMESTAMP)
.toEpochSecond(ZoneOffset.UTC);
} catch (DateTimeParseException ignored) {
return 0L;
}
}
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";
}
String message = failure.getMessage();
return message == null || message.isBlank()
? failure.getClass().getSimpleName() : message;
}
private static final class Session {
private final Set<Integer> completedInitialUserLists =
ConcurrentHashMap.newKeySet();
private final long id;
private final Socket socket;
private final LinkedBlockingQueue<ChatMessage> receiveQueue;
private final LinkedBlockingQueue<ChatMessage> transmitQueue;
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 Map<Integer, Map<String, ChatMember>> initialMembers =
new ConcurrentHashMap<>();
private volatile ReadThread readThread;
private volatile WriteThread writeThread;
private volatile MessageBusManagementThread messageProcessor;
private volatile boolean loginSent;
private volatile boolean authenticated;
private volatile boolean mainListComplete;
private volatile boolean secondListComplete;
private volatile boolean online;
private volatile boolean closed;
private Session(
long id,
Socket socket,
LinkedBlockingQueue<ChatMessage> receiveQueue,
LinkedBlockingQueue<ChatMessage> transmitQueue
) {
this.id = id;
this.socket = socket;
this.receiveQueue = receiveQueue;
this.transmitQueue = transmitQueue;
}
}
}
@@ -0,0 +1,50 @@
package kst4contest.controller;
/**
* Observable lifecycle of the ON4KST TCP session.
*
* <p>A connected TCP socket is deliberately not synonymous with an authenticated
* chat session. The intermediate states make that distinction visible to the UI
* and prevent application messages from being sent in the wrong protocol context.</p>
*/
public enum On4KstConnectionState {
DISCONNECTED,
CONNECTING,
WAITING_FOR_LOGIN_PROMPT,
AUTHENTICATING,
SYNCING_MAIN_CHAT,
SYNCING_SECOND_CHAT,
ONLINE,
RECONNECT_WAIT,
STOPPING;
/**
* Returns whether the complete application-level ON4KST handshake has finished.
*
* @return {@code true} only after authentication and all requested user lists
* have been synchronized
*/
public boolean isOnline() {
return this == ONLINE;
}
/**
* Returns whether a connection attempt or usable session is currently owned by
* the connection manager.
*
* <p>This is intentionally broader than {@link #isOnline()}. The UI uses it to
* prevent a second Connect action while authentication, synchronization or a
* scheduled reconnect is already in progress.</p>
*
* @return {@code true} while connecting, synchronizing, online or waiting for
* an automatic reconnect
*/
public boolean isConnectionAttemptActive() {
return switch (this) {
case CONNECTING, WAITING_FOR_LOGIN_PROMPT, AUTHENTICATING,
SYNCING_MAIN_CHAT, SYNCING_SECOND_CHAT, ONLINE,
RECONNECT_WAIT -> true;
default -> false;
};
}
}
@@ -0,0 +1,192 @@
package kst4contest.controller;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* Builds ON4KST port-23001 frames and rejects values that could break framing or
* put the server into an invalid chat context.
*
* <p>All outbound protocol construction is concentrated here. User-controlled
* values may therefore never introduce a field separator or a second line, and
* category and locator validation happens before the frame reaches the socket.</p>
*/
final class On4KstProtocol {
private static final Pattern LOCATOR_6 =
Pattern.compile("^[A-Ra-r]{2}[0-9]{2}[A-Xa-x]{2}$");
private On4KstProtocol() {
}
/**
* Builds the initial authenticated login frame.
*
* @param callsign login callsign
* @param password ON4KST password; never logged by this class
* @param category primary chat category
* @param clientName client identification sent to the server
* @param lastMessageTimestamp earliest history timestamp to request
* @return validated frame without CR/LF terminator
*/
static String login(
String callsign,
String password,
int category,
String clientName,
long lastMessageTimestamp
) {
return "LOGINC|" + field(callsign, "callsign")
+ "|" + password(password)
+ "|" + category(category)
+ "|" + field(clientName, "client name")
+ "|25|0|1|" + Math.max(0L, lastMessageTimestamp) + "|0|";
}
/** Builds the settings-complete frame for the supplied chat category. */
static String settingsDone(int category) {
return "SDONE|" + category(category) + "|";
}
/** Builds the frame used to add a distinct second chat to the same session. */
static String addChat(int category, long lastMessageTimestamp) {
return "ACHAT|" + category(category)
+ "|25|10|2|" + Math.max(0L, lastMessageTimestamp)
+ "|0|";
}
/** Builds a category-qualified locator command after validating Maidenhead syntax. */
static String setLocator(int category, String locator) {
return command(category, "/SETLOC " + locator(locator));
}
/** Builds a category-qualified chat-name command. */
static String setName(int category, String name) {
return command(category, "/SETNAME " + field(name, "chat name"));
}
/** Builds the command that changes the operator state back to available. */
static String back(int category) {
return command(category, "/BACK");
}
/**
* Wraps one validated slash command in an ON4KST message frame.
*
* @return frame without CR/LF terminator
*/
static String command(int category, String command) {
return "MSG|" + category(category) + "|0|"
+ messageText(command) + "|0|";
}
/**
* Wraps one operator chat message in a category-qualified ON4KST frame.
*
* @return frame without CR/LF terminator
*/
static String chatMessage(int category, String text) {
return "MSG|" + category(category) + "|0|"
+ messageText(text) + "|0|";
}
/**
* Removes trailing line terminators from a legacy raw frame while rejecting an
* embedded line break that could inject a second server command.
*
* @param frame legacy raw frame, possibly with trailing CR/LF
* @return exactly one normalized protocol line
* @throws IllegalArgumentException if the value is {@code null} or contains an
* embedded line break
*/
static String normalizeRawFrame(String frame) {
if (frame == null) {
throw new IllegalArgumentException("ON4KST frame must not be null");
}
int end = frame.length();
while (end > 0) {
char last = frame.charAt(end - 1);
if (last != '\r' && last != '\n') {
break;
}
end--;
}
String normalized = frame.substring(0, end);
if (normalized.indexOf('\r') >= 0 || normalized.indexOf('\n') >= 0) {
throw new IllegalArgumentException(
"ON4KST frame contains an embedded line break");
}
return normalized;
}
/**
* Validates and normalizes a six-character Maidenhead locator.
*
* @return upper-case locator
*/
static String locator(String locator) {
String normalized = field(locator, "locator").toUpperCase(Locale.ROOT);
if (!LOCATOR_6.matcher(normalized).matches()) {
throw new IllegalArgumentException(
"Locator must be a six-character Maidenhead locator: " + normalized);
}
return normalized;
}
/** Rejects message text containing an ON4KST field or line delimiter. */
static String messageText(String text) {
String value = field(text, "message text");
if (value.indexOf('|') >= 0) {
throw new IllegalArgumentException(
"Message text contains the ON4KST field separator '|'");
}
return value;
}
/**
* Validates one required, non-password protocol field.
*
* @param value field value
* @param label diagnostic label used in validation errors
* @return trimmed value
*/
static String field(String value, String label) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(label + " must not be empty");
}
if (value.indexOf('|') >= 0
|| value.indexOf('\r') >= 0
|| value.indexOf('\n') >= 0) {
throw new IllegalArgumentException(
label + " contains an ON4KST frame delimiter");
}
return value.trim();
}
private static String password(String value) {
if (value == null || value.isEmpty()) {
throw new IllegalArgumentException("password must not be empty");
}
if (value.indexOf('|') >= 0
|| value.indexOf('\r') >= 0
|| value.indexOf('\n') >= 0) {
throw new IllegalArgumentException(
"password contains an ON4KST frame delimiter");
}
return value;
}
/**
* Validates the category range supported by ON4KST.
*
* @return the unchanged category for convenient inline use
*/
static int category(int category) {
if (category < 1 || category > 12) {
throw new IllegalArgumentException(
"Unsupported ON4KST chat category: " + category);
}
return category;
}
}
@@ -0,0 +1,69 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
class On4KstProtocolTest {
@Test
void buildsLoginWithReplayOverlap() {
assertEquals(
"LOGINC|DL1ABC|secret|2|KST4Contest v1.2.3|25|0|1|12344|0|",
On4KstProtocol.login(
"DL1ABC", "secret", 2,
"KST4Contest v1.2.3", 12_344L));
}
@Test
void buildsContextSafeSecondChatFrames() {
assertEquals("SDONE|2|", On4KstProtocol.settingsDone(2));
assertEquals("ACHAT|3|25|10|2|100|0|",
On4KstProtocol.addChat(3, 100L));
assertEquals("MSG|2|0|/SETLOC JO31AA|0|",
On4KstProtocol.setLocator(2, "jo31aa"));
assertEquals("MSG|3|0|/SETNAME 10G 10368.200|0|",
On4KstProtocol.setName(3, "10G 10368.200"));
}
@Test
void stripsOnlyTrailingLineEndings() {
assertEquals("CK|", On4KstProtocol.normalizeRawFrame("CK|\r\n"));
assertThrows(IllegalArgumentException.class,
() -> On4KstProtocol.normalizeRawFrame("CK|\rBROKEN"));
}
@Test
void rejectsValuesThatCouldCreateASecondProtocolFrame() {
assertThrows(IllegalArgumentException.class,
() -> On4KstProtocol.chatMessage(2, "hello|0|"));
assertThrows(IllegalArgumentException.class,
() -> On4KstProtocol.chatMessage(2, "hello\r\nQUIT|"));
assertThrows(IllegalArgumentException.class,
() -> On4KstProtocol.login(
"DL1ABC", "bad|password", 2, "client", 0L));
}
@Test
void rejectsInvalidLocatorAndCategoryBeforeTheyReachTheServer() {
assertThrows(IllegalArgumentException.class,
() -> On4KstProtocol.setLocator(2, "JO31"));
assertThrows(IllegalArgumentException.class,
() -> On4KstProtocol.settingsDone(99));
}
@Test
void convertsBothHistoryAndLiveMessageTimestampsForReconnect() {
assertEquals(1_186_819_108L,
On4KstConnectionManager.parseMessageTimestamp(
"CR|2|1186819108|EA6VQ|Gabriel|0|msg|0|"));
assertEquals(
LocalDateTime.of(2026, 8, 13, 12, 34, 56)
.toEpochSecond(ZoneOffset.UTC),
On4KstConnectionManager.parseMessageTimestamp(
"CH|2|20260813123456|DL1ABC|Op|0|msg|0|"));
}
}
@@ -0,0 +1,100 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.BufferedReader;
import java.io.OutputStreamWriter;
import java.io.InputStreamReader;
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 On4KstSocketThreadTest {
@Test
@Timeout(5)
void eofIsReportedImmediately() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
CompletableFuture<Void> serverDone = CompletableFuture.runAsync(() -> {
try (Socket accepted = server.accept();
OutputStreamWriter out = new OutputStreamWriter(
accepted.getOutputStream(), StandardCharsets.UTF_8)) {
out.write("CK|\r\n");
out.flush();
} 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);
CompletableFuture<Throwable> failure = new CompletableFuture<>();
ReadThread reader = new ReadThread(
7L, client, queue, ignored -> active.get(), ignored -> { },
failure::complete);
reader.start();
assertEquals("CK|", queue.poll(2, TimeUnit.SECONDS).getMessageText());
failure.get(2, TimeUnit.SECONDS);
active.set(false);
reader.join(Duration.ofSeconds(2).toMillis());
}
serverDone.get(2, TimeUnit.SECONDS);
}
}
@Test
@Timeout(5)
void writerUsesOneExactCrLfPerFrameIncludingHeartbeat() throws Exception {
try (ServerSocket server = new ServerSocket(0)) {
CompletableFuture<String> firstLine = new CompletableFuture<>();
CompletableFuture<String> secondLine = new CompletableFuture<>();
CompletableFuture<Void> serverDone = CompletableFuture.runAsync(() -> {
try (Socket accepted = server.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(
accepted.getInputStream(), StandardCharsets.UTF_8))) {
firstLine.complete(in.readLine());
secondLine.complete(in.readLine());
} 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("SDONE|2|\r"));
assertEquals("", firstLine.get(2, TimeUnit.SECONDS));
assertEquals("SDONE|2|", secondLine.get(2, TimeUnit.SECONDS));
active.set(false);
writer.interrupt();
writer.join(Duration.ofSeconds(2).toMillis());
}
serverDone.get(2, TimeUnit.SECONDS);
}
}
private ChatMessage serverFrame(String text) {
ChatMessage message = new ChatMessage();
message.setMessageDirectedToServer(true);
message.setMessageText(text);
return message;
}
}
@@ -1,6 +1,7 @@
package kst4contest.controller;
import kst4contest.logic.BandOpportunityResolver;
import kst4contest.view.map.MapCallsignRawSnapshot;
import kst4contest.logic.PropagationFrequencyResolver;
import java.util.ArrayList;
import java.util.Collections;
@@ -12,9 +13,7 @@ import java.util.function.Consumer;
import javafx.application.Platform;
import kst4contest.locatorUtils.Location;
import kst4contest.model.Band;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.view.map.GeometryOnlyPathAnalysisService;
import kst4contest.view.map.OpenMeteoTerrainProfileProvider;
@@ -22,7 +21,6 @@ import kst4contest.view.map.PathAnalysisRequest;
import kst4contest.view.map.PathAnalysisResult;
import kst4contest.view.map.PathAnalysisService;
import kst4contest.view.map.PathGeometryUtils;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
@@ -139,9 +137,11 @@ public final class ReachabilityService {
* @param member best matching ChatMember, may be null when only a map snapshot exists
* @param selectedSnapshot selected map snapshot
* @param fxCallback callback executed on the JavaFX thread
* @param requestedBandOverride operator-selected band, or null for automatic resolution
*/
public void requestPathAnalysisForMap(ChatMember member,
MapCallsignRawSnapshot selectedSnapshot,
Band requestedBandOverride,
Consumer<PathAnalysisResult> fxCallback) {
String ownLocator6 = normalizeLocator6(chatController.getChatPreferences().getStn_loginLocatorMainCat());
@@ -165,22 +165,23 @@ public final class ReachabilityService {
return;
}
double analysisFrequencyMHz = PathGeometryUtils.resolveAnalysisFrequencyMHz(
selectedSnapshot.lastKnownFrequenciesByBand()
);
Band analysisBand;
double analysisFrequencyMHz;
Band analysisBand = Band.fromFrequency(analysisFrequencyMHz);
if (analysisBand != null && !isUsableAutomaticBand(member, analysisBand)) {
analysisFrequencyMHz = Double.NaN;
analysisBand = null;
}
if (requestedBandOverride != null) {
/*
* An explicit operator selection has priority over automatic propagation
* resolution. Exact recent QRG information on that band is still used
* when available; otherwise the band's default analysis frequency is used.
*/
analysisBand = requestedBandOverride;
analysisFrequencyMHz =
resolveAnalysisFrequencyForBand(member, analysisBand);
} else {
PropagationFrequencyResolver.Resolution frequencyResolution =
resolveAutomaticPropagationFrequency(member);
if (!Double.isFinite(analysisFrequencyMHz)
|| analysisFrequencyMHz <= 0.0
|| analysisBand == null) {
Band fallbackBand = resolveAutoBand(member);
if (fallbackBand == null) {
if (frequencyResolution == null) {
dispatchFxCallback(
fxCallback,
PathAnalysisResult.waitingForUsableBand(
@@ -192,8 +193,9 @@ public final class ReachabilityService {
return;
}
analysisBand = fallbackBand;
analysisFrequencyMHz = resolveAnalysisFrequencyForBand(member, fallbackBand);
analysisBand = frequencyResolution.getBand();
analysisFrequencyMHz =
frequencyResolution.getAnalysisFrequencyMHz();
}
PathAnalysisRequest request = buildRequest(
@@ -236,72 +238,32 @@ public final class ReachabilityService {
}
/**
* Resolves the auto reachability band.
*
* <ol>
* <li>Use the lowest band detected in this session.</li>
* <li>If no session band exists and the station is in the microwave category, use 1296 MHz.</li>
* <li>Otherwise use 144 MHz.</li>
* </ol>
* Resolves the auto reachability band through the shared propagation
* frequency selection used by AirScout and path analysis.
*
* @param member member to inspect
* @return resolved band
*/
public Band resolveAutoBand(ChatMember member) {
EnumSet<Band> enabledBands = getEnabledStationBands();
if (enabledBands.isEmpty()) {
return null;
}
PropagationFrequencyResolver.Resolution resolution =
resolveAutomaticPropagationFrequency(member);
return resolution == null ? null : resolution.getBand();
}
List<ChatMember> variants = resolveCallsignVariants(member);
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(variants, System.currentTimeMillis());
EnumSet<Band> availableOfferedBands = resolution.getAvailableBands();
availableOfferedBands.retainAll(enabledBands);
if (!availableOfferedBands.isEmpty()) {
return availableOfferedBands.stream()
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(null);
}
// Known evidence exists, but every matching band is disabled or NOT QRV.
if (resolution.hasBandEvidence()) {
return null;
}
EnumSet<Band> fallbackBands = EnumSet.copyOf(enabledBands);
fallbackBands.removeAll(resolution.getNotQrvBands());
if (fallbackBands.isEmpty()) {
return null;
}
if (member != null
&& member.getChatCategory() != null
&& member.getChatCategory().getCategoryNumber() == ChatCategory.MICROWAVE
&& fallbackBands.contains(Band.B_1296)) {
return Band.B_1296;
}
if (member != null
&& member.getChatCategory() != null
&& member.getChatCategory().getCategoryNumber() == ChatCategory.FIFTYSEVENTYMHz) {
if (fallbackBands.contains(Band.B_50)) {
return Band.B_50;
}
if (fallbackBands.contains(Band.B_70)) {
return Band.B_70;
}
}
if (fallbackBands.contains(Band.B_144)) {
return Band.B_144;
}
return fallbackBands.stream()
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(null);
/**
* Resolves one automatic band and exact analysis frequency for a station.
*
* @param member any active category variant of the target station
* @return shared propagation resolution, or {@code null} for unsupported data
*/
public PropagationFrequencyResolver.Resolution resolveAutomaticPropagationFrequency(
ChatMember member
) {
return PropagationFrequencyResolver.resolve(
resolveCallsignVariants(member),
getEnabledStationBands(),
System.currentTimeMillis()
);
}
/**
@@ -329,27 +291,7 @@ public final class ReachabilityService {
return variants.isEmpty() ? List.of(member) : variants;
}
/**
* Verifies that an automatically selected map/snapshot frequency belongs to a
* locally enabled band that is still available after NOT-QRV resolution.
* Manual UI band overrides are handled separately and are not changed here.
*/
private boolean isUsableAutomaticBand(ChatMember member, Band band) {
if (band == null || !getEnabledStationBands().contains(band)) {
return false;
}
if (member == null) {
return true;
}
BandOpportunityResolver.Resolution resolution = BandOpportunityResolver.resolve(
resolveCallsignVariants(member),
System.currentTimeMillis()
);
return resolution.getAvailableBands().contains(band);
}
/**
* Stops the background executor.
@@ -541,28 +483,7 @@ public final class ReachabilityService {
}
}
/**
* Resolves the analysis frequency from a map snapshot first, because the map
* aggregates all visible ChatMember variants and often knows the best current
* frequency per band.
*
* @param member fallback member
* @param selectedSnapshot selected map snapshot
* @return analysis frequency in MHz
*/
private double resolveAnalysisFrequencyForSnapshot(ChatMember member, MapCallsignRawSnapshot selectedSnapshot) {
if (selectedSnapshot != null) {
double snapshotFrequencyMHz =
PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
if (Double.isFinite(snapshotFrequencyMHz) && snapshotFrequencyMHz > 0.0) {
return snapshotFrequencyMHz;
}
}
Band fallbackBand = member == null ? Band.B_144 : resolveAutoBand(member);
return resolveAnalysisFrequencyForBand(member, fallbackBand);
}
/**
* Resolves the analysis frequency for one member/band pair.
@@ -575,15 +496,28 @@ public final class ReachabilityService {
* </ol>
*/
private double resolveAnalysisFrequencyForBand(ChatMember member, Band band) {
if (member != null && member.getKnownActiveBands() != null) {
ChatMember.ActiveFrequencyInfo activeFrequencyInfo = member.getKnownActiveBands().get(band);
if (band == null) {
return Double.NaN;
}
ChatMember.ActiveFrequencyInfo latestFrequencyInfo = null;
for (ChatMember variant : resolveCallsignVariants(member)) {
ChatMember.ActiveFrequencyInfo activeFrequencyInfo =
variant.getKnownActiveBands().get(band);
if (activeFrequencyInfo != null
&& Double.isFinite(activeFrequencyInfo.frequency)
&& activeFrequencyInfo.frequency > 0.0) {
return activeFrequencyInfo.frequency;
&& activeFrequencyInfo.frequency > 0.0
&& (latestFrequencyInfo == null
|| activeFrequencyInfo.timestampEpoch > latestFrequencyInfo.timestampEpoch)) {
latestFrequencyInfo = activeFrequencyInfo;
}
}
if (latestFrequencyInfo != null) {
return latestFrequencyInfo.frequency;
}
if (member != null && member.getFrequency() != null && member.getFrequency().getValue() != null) {
double parsedFrequencyMHz = PathGeometryUtils.tryParseFrequencyMHz(member.getFrequency().getValue());
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
@@ -1,119 +1,130 @@
package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.io.BufferedReader;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Consumer;
import java.util.function.LongPredicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import kst4contest.model.ChatMessage;
/**
* This thread is responsible for reading telnet servers input at port 23001 and printing it
* to the console.
* It runs in an infinite loop until the client disconnects from the server.
* Reads exactly one immutable ON4KST connection session.
*
* @author www.codejava.net
* <p>EOF is a connection-loss event, not an empty chat message. Every line is
* associated with the session id captured by this reader, so a delayed exception
* from an obsolete socket cannot affect a newer reconnect.</p>
*/
public class ReadThread extends Thread {
private static final Logger LOGGER = Logger.getLogger(ReadThread.class.getName());
private BufferedReader reader;
private Socket socket;
private ChatController client;
public boolean accidentalDisconnected;
public boolean isAccidentalDisconnected() {
return accidentalDisconnected;
}
public void setAccidentalDisconnected(boolean accidentalDisconnected) {
this.accidentalDisconnected = accidentalDisconnected;
}
private final long sessionId;
private final Socket socket;
private final LinkedBlockingQueue<ChatMessage> receiveQueue;
private final LongPredicate sessionIsActive;
private final Consumer<String> inboundActivity;
private final Consumer<Throwable> connectionFailure;
private final BufferedReader reader;
// private boolean readingFinished = true; //kst4contest.test 4 23001
private boolean readingFinished = true;
InputStream input;
public ReadThread(Socket socket, ChatController client) {
/**
* Compatibility constructor for the pre-session controller path.
*
* @deprecated new connections should be created by
* {@link On4KstConnectionManager}
*/
@Deprecated
public ReadThread(Socket socket, ChatController client) throws IOException {
this(0L, socket, client.getMessageRXBus(), ignored -> true,
ignored -> { }, ignored -> { });
}
/**
* Creates the reader for one connection generation.
*
* @param sessionId immutable id of the owning socket session
* @param socket connected ON4KST socket
* @param receiveQueue private receive queue belonging to this session
* @param sessionIsActive guard against callbacks from an obsolete session
* @param inboundActivity callback used for liveness and protocol progress
* @param connectionFailure callback for EOF, I/O and unexpected runtime errors
* @throws IOException if the socket input stream cannot be opened
*/
public ReadThread(
long sessionId,
Socket socket,
LinkedBlockingQueue<ChatMessage> receiveQueue,
LongPredicate sessionIsActive,
Consumer<String> inboundActivity,
Consumer<Throwable> connectionFailure
) throws IOException {
this.sessionId = sessionId;
this.socket = socket;
this.client = client;
try {
input = socket.getInputStream();
reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Error getting input stream", ex);
}
this.receiveQueue = receiveQueue;
this.sessionIsActive = sessionIsActive;
this.inboundActivity = inboundActivity;
this.connectionFailure = connectionFailure;
this.reader = new BufferedReader(new InputStreamReader(
socket.getInputStream(), StandardCharsets.UTF_8));
}
@Override
public void run() {
Thread.currentThread().setName("ReadFromTelnetThread");
ChatMessage message; //bugfix leak, moved out of while
while (true) {
// System.out.println("rdth");
try {
String response = reader.readLine();
message = new ChatMessage();
message.setMessageText(response);
// message.setDirectedToServer(false);
// message.setDirectedToServer(false);
// message.setDirectedToServer(false);
if (response != null) {
client.getMessageRXBus().put(message);
// System.out.println("[RT]: read message and added it to msgrxqueue --- " + response + " ---");
} else {
System.out.println("[RT]: read message responsed a nullstring, do nothing, buffersize = " + socket.getReceiveBufferSize() + ", reader ready? "
+ reader.ready());
// reader = new BufferedReader(new InputStreamReader(input));
// response = reader.readLine();
this.client.getSocket().close();
this.interrupt();
Thread.currentThread().setName("ReadFromOn4Kst-" + sessionId);
LOGGER.log(Level.FINE,
"ON4KST reader started for session {0}", sessionId);
try {
while (!isInterrupted() && sessionIsActive.test(sessionId)) {
String response = reader.readLine();
if (response == null) {
throw new EOFException("ON4KST closed the TCP connection");
}
}
catch (Exception sexc) {
LOGGER.log(Level.SEVERE, "[ReadThread] Socket closed unexpectedly", sexc);
try {
this.client.getSocket().close();
this.interrupt();
break;
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ReadThread] Error closing socket", e);
}
inboundActivity.accept(response);
if (!sessionIsActive.test(sessionId)) {
break;
}
ChatMessage message = new ChatMessage();
message.setMessageText(response);
receiveQueue.put(message);
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
} catch (IOException exception) {
if (sessionIsActive.test(sessionId)) {
LOGGER.log(Level.FINE,
"ON4KST read failed for session " + sessionId,
exception);
connectionFailure.accept(exception);
}
} catch (RuntimeException exception) {
if (sessionIsActive.test(sessionId)) {
LOGGER.log(Level.SEVERE, "Unexpected ON4KST reader failure", exception);
connectionFailure.accept(exception);
}
} finally {
LOGGER.log(Level.FINE,
"ON4KST reader stopped for session {0}", sessionId);
}
}
/**
* Interrupts the read loop and closes the session socket.
*
* @return always {@code true} after a successful close
* @throws IOException if closing the reader or socket fails
*/
public boolean terminateConnection() throws IOException {
this.reader.close();
this.input.close();
this.socket.close();
return true;
interrupt();
reader.close();
socket.close();
return true;
}
public boolean isReadingFinished() {
return readingFinished;
}
public void setReadingFinished(boolean readingReady) {
this.readingFinished = readingReady;
}
}
@@ -166,7 +166,15 @@ public final class ScoreService {
scoreByCallRaw.put(callRaw, score);
preferredCategoryByCallRaw.put(callRaw, representative.getChatCategory());
topAll.add(new TopCandidate(callRaw, representative.getCallSign(), representative.getChatCategory(), score));
if (Double.isFinite(score) && score > 0.0) {
topAll.add(new TopCandidate(
callRaw,
representative.getCallSign(),
representative.getChatCategory(),
score
));
}
}
// 3) Build Top-N
@@ -227,12 +235,11 @@ public final class ScoreService {
ChatMember chosen = null;
if (preferredCat != null) {
for (ChatMember v : variants) {
if (v != null && v.getChatCategory() == preferredCat) {
chosen = v;
break;
}
}
chosen = variants.stream()
.filter(Objects::nonNull)
.filter(v -> isSameChatCategory(v.getChatCategory(), preferredCat))
.max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
.orElse(null);
}
if (chosen == null) {
@@ -247,6 +254,14 @@ public final class ScoreService {
return representative;
}
private static boolean isSameChatCategory(ChatCategory left, ChatCategory right) {
return left != null
&& right != null
&& left.getCategoryNumber() == right.getCategoryNumber();
}
/**
* Projects the immutable score snapshot back into ChatMember display fields so
* the normal station table can sort/filter by score without knowing the score
@@ -2,6 +2,7 @@ package kst4contest.controller;
import kst4contest.logic.SignalDetector;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ChatMember;
import java.util.ArrayDeque;
import java.util.Deque;
@@ -21,7 +22,7 @@ import java.util.regex.Pattern;
public final class StationMetricsService {
/** /cq <CALL> ... */
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/]+)\\b.*");
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/-]+)\\b.*");
/** Rolling window timestamps for momentum scoring. */
private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32;
@@ -195,7 +196,7 @@ public final class StationMetricsService {
private static String normalizeCallRaw(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
return ChatMember.normalizeCallSignToBaseCallSign(s);
}
private static final class StationMetrics {
@@ -17,4 +17,20 @@ public interface StatusUpdateListener {
void onUserListUpdated(String reason);
// new: userlist-update
}
/**
* Called whenever the authoritative ON4KST session changes lifecycle state.
*
* <p>The callback may originate from a background connection supervisor. A UI
* implementation must marshal control changes onto its application thread.</p>
*
* @param state new connection, authentication or synchronization state
* @param detail human-readable progress or failure reason
*/
default void onConnectionStateChanged(
On4KstConnectionState state,
String detail
) {
// Optional for non-UI listeners.
}
}
@@ -101,102 +101,143 @@ public class Utils4KST {
}
/**
* Normalizes a chatmembers frequency-string for cluster usage<br/>
* <b>returns a frequency String in KHz like = "144300" or "144300.0" to match DXC protocol needs</b>
* Converts a frequency detected in a chat message into the kHz representation
* used by the DX Cluster protocol.
*
* @param optionalPrefix: if there is a value like ".300", it have to be decided, wich ".300": 144.300, 432.300, 1296.300 .... prefix means for example "144."
* <p>Complete frequencies with two to five MHz digits are supported, for
* example 50.200, 144.205, 1296.338, 10368.100 and 24048.100. An optional
* second fractional group is retained as sub-kHz precision, for example
* 144.205.2 becomes 144205.2 kHz.</p>
*
* <p>Relative values such as .205 or 205 use the configured fallback-band
* prefix. The method only performs this fallback conversion after the general
* message parser has already accepted the value as a frequency.</p>
*
* @param qrgString frequency as detected by the chat parser
* @param optionalPrefix fallback MHz prefix for relative frequencies
* @return frequency in kHz for a DX Cluster spot, or an empty string if the
* value cannot be converted safely
*/
public static String normalizeFrequencyString(String qrgString, SimpleStringProperty optionalPrefix) {
// final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
try {
qrgString = qrgString.replace(" ","");
} catch (Exception e) {
System.out.println("UTILS: QRG NULL, nothing to convert");
// e.printStackTrace();
public static String normalizeFrequencyString(
String qrgString,
SimpleStringProperty optionalPrefix
) {
if (qrgString == null || qrgString.isBlank()) {
return "";
}
/*
* A comma is accepted as a decimal separator. Spaces are removed so older
* stored formats such as "432 088" remain usable.
*/
String normalizedValue = qrgString
.trim()
.replace(" ", "")
.replace(',', '.');
final String PTRN_QRG_CAT2_wholeQRGMHz4Digits = "(([0-9]{4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)"; //1296.300.3 etc
final String PTRN_QRG_CAT2_wholeQRGMHz3Digits = "(([0-9]{3}[\\.|,| ]?[0-9]{3})([\\.][\\d]{1,2})?)"; //144.300.3 etc
final String PTRN_QRG_CAT2_QRGwithoutPrefix = "((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; //144.300.3 etc
/*
* Complete frequency with an explicit separator:
*
* 50.200
* 144.205
* 1296.338
* 10368.100
* 144.205.2
*/
Matcher completeFrequencyMatcher = Pattern.compile(
"^(\\d{2,5})\\.(\\d{1,3})(?:\\.(\\d{1,2}))?$"
).matcher(normalizedValue);
String stringAggregation = "";
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz4Digits)) {//case 1296.200 or 1296.200.2 etc.
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".","");
stringAggregation = stringAggregation.replace(",","");
stringAggregation = stringAggregation.replace(" ", "");
if (stringAggregation.length() == 8) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
stringAggregation = stringAggregationNew + ".0";
return stringAggregation;
} else if (stringAggregation.length() == 9) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
}
} else
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz3Digits)) { //case 144.300 or 144.300.2
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".","");
stringAggregation = stringAggregation.replace(",","");
stringAggregation = stringAggregation.replace(" ", "");
if (stringAggregation.length() == 6) {
stringAggregation = stringAggregation + ".0";
return stringAggregation;
}
if (stringAggregation.length() == 7) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
stringAggregation = stringAggregationNew + ".0";
return stringAggregation;
} else if (stringAggregation.length() == 8) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
}
}
else
if (testPattern(qrgString, PTRN_QRG_CAT2_QRGwithoutPrefix)) { //case ".050 or .300 or something like that"
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".", "");
stringAggregation = stringAggregation.replace(",", "");
stringAggregation = stringAggregation.replace(" ", "");
if (stringAggregation.length() == 3) { // like 050 or 300
String stringAggregationNew = optionalPrefix.getValue() + stringAggregation;
stringAggregation = stringAggregationNew + ".0";
return stringAggregation;
} else if (stringAggregation.length() == 4) { //like 050.2 --> 0502
stringAggregation = optionalPrefix.getValue() + stringAggregation;
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 1) + "." + stringAggregation.substring(stringAggregation.length() - 1, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
} else if (stringAggregation.length() == 5) { //like 050.20 --> 05020
stringAggregation = optionalPrefix.getValue() + stringAggregation;
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 2) + "." + stringAggregation.substring(stringAggregation.length() - 2, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
}
if (completeFrequencyMatcher.matches()) {
return formatDxClusterFrequency(
completeFrequencyMatcher.group(1),
completeFrequencyMatcher.group(2),
completeFrequencyMatcher.group(3)
);
}
return stringAggregation; //if nothing else helps
/*
* Compact complete frequency retained for compatibility:
*
* 144205
* 1296338
* 10368100
* 432088.2
*/
Matcher compactFrequencyMatcher = Pattern.compile(
"^(\\d{2,5})(\\d{3})(?:\\.(\\d{1,2}))?$"
).matcher(normalizedValue);
if (compactFrequencyMatcher.matches()) {
return formatDxClusterFrequency(
compactFrequencyMatcher.group(1),
compactFrequencyMatcher.group(2),
compactFrequencyMatcher.group(3)
);
}
/*
* Relative frequency. Values above 499 are deliberately rejected, matching
* the previous behaviour and preventing a report such as 599 from becoming
* a plausible-looking cluster frequency.
*/
Matcher relativeFrequencyMatcher = Pattern.compile(
"^\\.?([0-4]\\d{2})(?:\\.(\\d{1,2}))?$"
).matcher(normalizedValue);
if (!relativeFrequencyMatcher.matches()) {
return "";
}
String fallbackPrefix = optionalPrefix == null
? null
: optionalPrefix.getValue();
if (fallbackPrefix == null) {
return "";
}
fallbackPrefix = fallbackPrefix.trim();
if (!fallbackPrefix.matches("\\d{2,5}")) {
return "";
}
return formatDxClusterFrequency(
fallbackPrefix,
relativeFrequencyMatcher.group(1),
relativeFrequencyMatcher.group(2)
);
}
/**
* Formats an MHz part, a fractional MHz part and optional sub-kHz digits as a
* DX Cluster frequency in kHz.
*
* <p>The fractional MHz part is padded on the right because 144.2 means
* 144.200 MHz, while 144.21 means 144.210 MHz.</p>
*
* @param mhzPart complete MHz part
* @param fractionalMhzPart one to three digits following the first separator
* @param subKhzPart optional digits following a second separator
* @return DX Cluster frequency in kHz
*/
private static String formatDxClusterFrequency(
String mhzPart,
String fractionalMhzPart,
String subKhzPart
) {
String paddedFraction =
(fractionalMhzPart + "000").substring(0, 3);
String subKhz = subKhzPart == null
? "0"
: subKhzPart;
return mhzPart
+ paddedFraction
+ "."
+ subKhz;
}
}
@@ -39,26 +39,63 @@ public class WinTestSkedSender {
}
/**
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
* sequence via UDP broadcast.
* Pushes a ContestSked into Win-Test by sending the
* LOCKSKED / ADDSKED / UNLOCKSKED sequence.
*
* @param sked the sked to push
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0)
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST")
* @param sked sked to push
* @param targetCallsign callsign prepared for Win-Test
* @param frequencyKHz operating frequency in kHz
* @param notes optional notes
* @param mode Win-Test mode ID: 0 for CW, 1 for SSB
*/
public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) {
public void pushSkedToWinTest(ContestSked sked,
String targetCallsign,
double frequencyKHz,
String notes,
int mode) {
try {
sendLockSked();
sendAddSked(sked, frequencyKHz, notes, modeOverride);
sendAddSked(
sked,
targetCallsign,
frequencyKHz,
notes,
mode
);
sendUnlockSked();
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false);
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign()
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand());
} catch (Exception e) {
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage());
e.printStackTrace();
reportStatus(
"Sked pushed to WT: " + targetCallsign,
false
);
System.out.println(
"[WinTestSkedSender] Sked pushed: "
+ targetCallsign
+ " at "
+ frequencyKHz
+ " kHz, band="
+ sked.getBand()
+ ", mode="
+ mode
);
} catch (Exception exception) {
reportStatus(
"ERROR pushing sked: "
+ exception.getMessage(),
true
);
System.out.println(
"[WinTestSkedSender] Error pushing sked: "
+ exception.getMessage()
);
exception.printStackTrace();
}
}
@@ -86,46 +123,53 @@ public class WinTestSkedSender {
/**
* Sends an ADDSKED message with the sked details.
* <p>
* Win-Test ADDSKED data format (from wtKST):
* <pre>
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
* </pre>
* <p>
* Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch).
* The C# code adds 60 seconds to compensate.
*
* <p>The wtKST implementation subtracts a reference time of
* 1970-01-01 00:01:00 UTC and subsequently adds 60 seconds. Both
* operations cancel each other out. The transmitted value is therefore
* an ordinary Unix timestamp and must not receive another offset here.</p>
*/
private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception {
// Win-Test timestamp: epoch seconds with 60s offset
long epochSeconds = sked.getSkedTimeEpoch() / 1000;
long wtTimestamp = epochSeconds + 60;
private void sendAddSked(ContestSked sked,
String targetCallsign,
double frequencyKHz,
String notes,
int mode) throws Exception {
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
long wtTimestamp =
sked.getSkedTimeEpoch() / 1000L;
// Win-Test band ID
int bandId = toWinTestBandId(sked.getBand());
// Frequency in 0.1 kHz units.
long frequencyTenthKHz =
Math.round(frequencyKHz * 10.0);
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
int mode;
if (modeOverride >= 0) {
mode = modeOverride;
} else {
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
}
int bandId =
toWinTestBandId(sked.getBand());
String data = wtTimestamp
+ " " + freqTenthKHz
+ " " + bandId
+ " " + mode
+ " \"" + sked.getTargetCallsign() + "\""
+ " \"" + (notes != null ? notes : "") + "\"";
/*
* Accept only the mode IDs supported by this UI.
* Any unexpected value falls back to SSB.
*/
int winTestMode =
mode == 0
? 0
: 1;
WinTestMessage msg = new WinTestMessage(
String data =
wtTimestamp
+ " " + frequencyTenthKHz
+ " " + bandId
+ " " + winTestMode
+ " \"" + targetCallsign + "\""
+ " \"" + (notes != null ? notes : "") + "\"";
WinTestMessage message = new WinTestMessage(
WinTestMessage.MessageType.ADDSKED,
stationName, "",
data);
sendUdp(msg);
stationName,
"",
data
);
sendUdp(message);
}
/**
@@ -168,18 +212,6 @@ public class WinTestSkedSender {
};
}
/**
* Very simple SSB segment heuristic.
* A more complete implementation would check actual mode from Win-Test STATUS.
*/
private boolean isInSsbSegment(double frequencyKHz) {
// SSB segments (kHz ranges)
if (frequencyKHz >= 144300 && frequencyKHz <= 144399) return true; // 2m SSB
if (frequencyKHz >= 432200 && frequencyKHz <= 432399) return true; // 70cm SSB
if (frequencyKHz >= 1296200 && frequencyKHz <= 1296399) return true; // 23cm SSB
return false;
}
private void reportStatus(String text, boolean isError) {
if (callback != null) {
callback.onThreadStatus(THREAD_NICKNAME,
@@ -1,289 +1,173 @@
package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.function.Consumer;
import java.util.function.LongPredicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMessage;
/**
* This thread is responsible for sending content to the chat. As we only use
* the tx function, there is no content in run() method
*
* Serializes and writes exactly one immutable ON4KST connection session.
*
* <p>The writer owns one private queue and appends exactly one CR/LF terminator
* per frame. All category selection and delimiter validation happens before bytes
* reach the socket.</p>
*/
public class WriteThread extends Thread {
private PrintWriter writer;
private Socket socket;
private ChatController client;
private OutputStream output;
private ChatMessage messageToBeSend;
public WriteThread(Socket socket, ChatController client) throws InterruptedException {
this.socket = socket;
this.client = client;
try {
output = socket.getOutputStream();
writer = new PrintWriter(output, true, StandardCharsets.UTF_8);
} catch (IOException ex) {
System.out.println("Error getting output stream: " + ex.getMessage());
ex.printStackTrace();
}
}
private static final Logger LOGGER =
Logger.getLogger(WriteThread.class.getName());
private final long sessionId;
private final Socket socket;
private final LinkedBlockingQueue<ChatMessage> transmitQueue;
private final LongPredicate sessionIsActive;
private final Consumer<Throwable> connectionFailure;
private final Consumer<String> rejectedFrame;
private final BufferedWriter writer;
private final int defaultCategory;
/**
* This method is used to send a message to the server, raw formatted. E.g. for
* the keepalive message. This method sends only in the main message-Category. To send it in a category
* "defined by Chatmessage", use txByRxmsgCatOrigin(Chatmessage "toBeSend")
*
* @param messageToServer
* @throws InterruptedException
*/
public void tx(ChatMessage messageToServer) throws InterruptedException {
// writer.println(messageToServer.getMessage()); //kst4contest.test 4 23001
// writer.flush(); //kst4contest.test 4 23001
System.out.println(messageToServer.getMessageText() + "< sended to the writer");
writer.println(messageToServer.getMessageText());
}
/**
* This method is used to send a message directly to a receiver in a special chatcategory. The receivers category
* will be read out of the Chatmessage.getChatCategory method. <b> The message text will be modified to fit kst
* messageformat</b>
* Compatibility constructor for the pre-session controller path.
*
* @param messageToServer
* @throws InterruptedException
* @deprecated new connections should be created by
* {@link On4KstConnectionManager}
*/
public void txByRxmsgCatOrigin(ChatMessage messageToServer) throws InterruptedException {
// writer.println(messageToServer.getMessage()); //kst4contest.test 4 23001
// writer.flush(); //kst4contest.test 4 23001
String originalMessageText = messageToServer.getMessageText() + "";
String newMessageText = "";
newMessageText = ("MSG|" + messageToServer.getChatCategory().getCategoryNumber()
+ "|0|" + originalMessageText + "|0|"); //original before 1.26
System.out.println(newMessageText + "< sended to the writer (DIRECTED REPLY)");
writer.println(newMessageText);
@Deprecated
public WriteThread(Socket socket, ChatController client) throws IOException {
this(0L, socket, client.getMessageTXBus(),
client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber(),
ignored -> true,
ignored -> { }, System.out::println);
}
/**
* This method gets a textmessage to the chat and adds some characters to hit
* the neccessarry format to send a message in the on4kst chat either to another
* station or to the public.
*
* @param messageToServer
* @throws InterruptedException
* Creates the writer for one connection generation.
*
* @param sessionId immutable id of the owning socket session
* @param socket connected ON4KST socket
* @param transmitQueue private transmit queue belonging to this session
* @param defaultCategory fallback category for unqualified chat messages
* @param sessionIsActive guard against writes from an obsolete session
* @param connectionFailure callback for socket and unexpected runtime errors
* @param rejectedFrame callback for locally rejected protocol content
* @throws IOException if the socket output stream cannot be opened
*/
public void txKSTFormatted(ChatMessage messageToServer) throws InterruptedException {
public WriteThread(
long sessionId,
Socket socket,
LinkedBlockingQueue<ChatMessage> transmitQueue,
int defaultCategory,
LongPredicate sessionIsActive,
Consumer<Throwable> connectionFailure,
Consumer<String> rejectedFrame
) throws IOException {
this.sessionId = sessionId;
this.socket = socket;
this.transmitQueue = transmitQueue;
this.defaultCategory = defaultCategory;
this.sessionIsActive = sessionIsActive;
this.connectionFailure = connectionFailure;
this.rejectedFrame = rejectedFrame;
this.writer = new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream(), StandardCharsets.UTF_8));
}
// writer.println(messageToServer.getMessageText());
messageToBeSend = messageToServer;
@Override
public void run() {
Thread.currentThread().setName("WriteToOn4Kst-" + sessionId);
LOGGER.log(Level.FINE,
"ON4KST writer started for session {0}", sessionId);
try {
messageToBeSend = client.getMessageTXBus().take();
// this.client.getmesetChatsetServerready(true);
} catch (InterruptedException e) {
e.printStackTrace();
}
String messageLine = messageToBeSend.getMessageText();
if (messageToBeSend.isMessageDirectedToServer()) {
/**
* We have to check if we only commands the server (keepalive) or want do talk
* to the community
*/
try {
tx(messageToBeSend);
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
ChatMessage ownMSG = new ChatMessage();
// ownMSG.setMessageText(
// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
ownMSG.setMessageText("MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber()
+ "|0|" + messageLine + "|0|"); //original before 1.26
try {
tx(ownMSG);
System.out.println("BUS: tx: " + ownMSG.getMessageText());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if (messageToBeSend.equals("/QUIT")) {
try {
this.client.getReadThread().terminateConnection();
this.client.getReadThread().interrupt();
this.client.getWriteThread().terminateConnection();
this.client.getWriteThread().interrupt();
this.interrupt();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public boolean terminateConnection() throws IOException {
this.output.close();
this.socket.close();
return true;
}
public void run() {
Thread.currentThread().setName("WriteToTelnetThread");
while (true) {
try {
messageToBeSend = client.getMessageTXBus().take();
if (messageToBeSend.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)
&& messageToBeSend.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
client.getMessageRXBus().clear();
this.interrupt();
while (!isInterrupted() && sessionIsActive.test(sessionId)) {
ChatMessage message = transmitQueue.take();
if (isPoisonPill(message)) {
break;
}
if (!sessionIsActive.test(sessionId)) {
break;
} else {
String messageLine = messageToBeSend.getMessageText();
if (messageToBeSend.isMessageDirectedToServer()) {
/**
* We have to check if we only commands the server (keepalive) or want do talk
* to the community
*/
try {
tx(messageToBeSend);
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else { //message is not directed to the server, it´s directed to all or to a station
if (messageToBeSend.getChatCategory() == this.client.getChatCategoryMain() || messageToBeSend.getChatCategory() == this.client.getChatCategorySecondChat()) {
txByRxmsgCatOrigin(messageToBeSend);
} else { //default bhv if destination cat is not detectable
ChatMessage ownMSG = new ChatMessage();
ownMSG.setMessageText(
"MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|"
+ messageLine + "|0|");
try {
tx(ownMSG);
System.out.println("WT: tx (raw): " + ownMSG.getMessageText());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
System.out.println("WritheTh: got message out of the queue: " + messageToBeSend.getMessageText());
// this.client.getmesetChatsetServerready(true);
} catch (InterruptedException e) {
e.printStackTrace();
client.getMessageTXBus().clear();
}
// String messageLine = messageTextRaw.getMessageText();
//
// if (messageTextRaw.isMessageDirectedToServer()) {
// /**
// * We have to check if we only commands the server (keepalive) or want do talk
// * to the community
// */
//
// try {
// tx(messageTextRaw);
// System.out.println("BUS: tx: " + messageTextRaw.getMessageText());
//
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
//
// } else {
//
// ChatMessage ownMSG = new ChatMessage();
//
//// ownMSG.setMessageText(
//// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
//
// ownMSG.setMessageText(
// "MSG|" + this.client.getChatPreferences().getLoginChatCategory().getCategoryNumber() + "|0|"
// + messageLine + "|0|");
//
// try {
// tx(ownMSG);
// System.out.println("BUS: tx: " + ownMSG.getMessageText());
//
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// }
try {
writeFrame(formatFrame(message));
} catch (IllegalArgumentException invalidFrame) {
LOGGER.log(Level.FINE,
"Rejected outbound ON4KST frame in session {0}: {1}",
new Object[] {sessionId, invalidFrame.getMessage()});
rejectedFrame.accept(invalidFrame.getMessage());
}
}
// if (messageTextRaw.equals("/QUIT")) {
// try {
// this.client.getReadThread().terminateConnection();
// this.client.getReadThread().interrupt();
// this.client.getWriteThread().terminateConnection();
// this.client.getWriteThread().interrupt();
// this.interrupt();
//
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// }
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
} catch (IOException exception) {
if (sessionIsActive.test(sessionId)) {
LOGGER.log(Level.FINE,
"ON4KST write failed for session " + sessionId,
exception);
connectionFailure.accept(exception);
}
} catch (RuntimeException exception) {
if (sessionIsActive.test(sessionId)) {
LOGGER.log(Level.SEVERE,
"Unexpected ON4KST writer failure for session "
+ sessionId,
exception);
connectionFailure.accept(exception);
}
} finally {
LOGGER.log(Level.FINE,
"ON4KST writer stopped for session {0}", sessionId);
}
}
// while (true) {
//
// }
private String formatFrame(ChatMessage message) {
if (message == null) {
throw new IllegalArgumentException("Cannot send an empty ON4KST message");
}
if (message.isMessageDirectedToServer()) {
return On4KstProtocol.normalizeRawFrame(message.getMessageText());
}
ChatCategory category = message.getChatCategory();
return On4KstProtocol.chatMessage(
category == null ? defaultCategory : category.getCategoryNumber(),
message.getMessageText());
}
private void writeFrame(String frame) throws IOException {
writer.write(frame);
writer.write("\r\n");
writer.flush();
}
private boolean isPoisonPill(ChatMessage message) {
return message != null
&& ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(
message.getMessageText())
&& ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(
message.getMessageSenderName());
}
/**
* Interrupts the write loop and closes the session socket.
*
* @return always {@code true} after a successful close
* @throws IOException if closing the writer or socket fails
*/
public boolean terminateConnection() throws IOException {
interrupt();
writer.close();
socket.close();
return true;
}
}
@@ -4,6 +4,13 @@ import java.util.TimerTask;
import kst4contest.model.ChatMessage;
/**
* Enqueues the empty application-level reply expected by ON4KST keepalive
* handling.
*
* <p>The writer owns line termination and appends exactly one CR/LF sequence.
* Supplying an empty payload here avoids the historic double-terminator frame.</p>
*/
public class keepAliveMessageSenderTask extends TimerTask {
private ChatController client;
@@ -16,13 +23,14 @@ public class keepAliveMessageSenderTask extends TimerTask {
@Override
public void run() {
Thread.currentThread().setName("KeepAliveMessageSenderTask");
// System.out.println("[keepalive: ] Thread runned now");
ChatMessage keepAliveMSG = new ChatMessage();
keepAliveMSG.setMessageText("\r");
// WriteThread appends exactly one CRLF. An empty frame is the protocol reply.
keepAliveMSG.setMessageText("");
keepAliveMSG.setMessageDirectedToServer(true);
// System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " [keepaliveTask]: Sending keepalive: "
@@ -33,4 +41,4 @@ public class keepAliveMessageSenderTask extends TimerTask {
this.client.getMessageTXBus().add(keepAliveMSG);
}
}
}
@@ -113,12 +113,32 @@ public final class BandOpportunityResolver {
return detectedBands;
}
/*
* Explicit band descriptions:
* 2m, 70cm, 23cm, 432, 1296, ...
*/
for (Map.Entry<Band, Pattern> entry : STATION_NAME_BAND_PATTERNS.entrySet()) {
if (entry.getValue().matcher(stationName).find()) {
detectedBands.add(entry.getKey());
}
}
/*
* Explicit full QRGs in name field:
* 432.357 -> 432 MHz
* 1296.210 -> 1296 MHz
* etc.
*/
for (FrequencyTextParser.DetectedFrequency detectedFrequency
: FrequencyTextParser.findExplicitFrequencies(
stationName
)) {
detectedBands.add(
detectedFrequency.getBand()
);
}
return detectedBands;
}
@@ -0,0 +1,191 @@
package kst4contest.logic;
import kst4contest.model.Band;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
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>
*/
public final class FrequencyTextParser {
/*
* Examples:
* 50.150
* 144.300
* 432,357
* 10368.100
* 144.300.03
*
* At least two digits are required before the decimal separator. This
* intentionally prevents "1.2" from being interpreted as a frequency.
*/
private static final Pattern EXPLICIT_FREQUENCY_PATTERN = Pattern.compile(
"(?<![A-Z0-9])"
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)"
+ "(?!\\d)",
Pattern.CASE_INSENSITIVE
);
private FrequencyTextParser() {
}
/**
* Finds all distinct explicit frequencies that fall into one of the bands
* supported by {@link Band}.
*/
public static List<DetectedFrequency> findExplicitFrequencies(String text) {
if (text == null || text.isBlank()) {
return List.of();
}
Matcher matcher = EXPLICIT_FREQUENCY_PATTERN.matcher(text);
Map<String, DetectedFrequency> uniqueFrequencies =
new LinkedHashMap<>();
while (matcher.find()) {
DetectedFrequency detected =
parseExplicitFrequency(matcher.group(1));
if (detected == null) {
continue;
}
String uniqueKey =
detected.getBand().name()
+ "|"
+ Double.toString(
detected.getFrequencyMHz()
);
uniqueFrequencies.putIfAbsent(
uniqueKey,
detected
);
}
return List.copyOf(
new ArrayList<>(uniqueFrequencies.values())
);
}
/**
* Parses one complete frequency.
*
* @return detected band/frequency or {@code null} when the value is invalid
* or outside the supported amateur bands
*/
public static DetectedFrequency parseExplicitFrequency(
String rawFrequency
) {
if (rawFrequency == null || rawFrequency.isBlank()) {
return null;
}
String normalized =
normalizeFrequencyString(
rawFrequency
.trim()
.replace(',', '.')
);
try {
double frequencyMHz =
Double.parseDouble(normalized);
if (!Double.isFinite(frequencyMHz)) {
return null;
}
Band band =
Band.fromFrequency(frequencyMHz);
if (band == null) {
return null;
}
return new DetectedFrequency(
band,
frequencyMHz,
rawFrequency.trim()
);
} catch (NumberFormatException ignored) {
return null;
}
}
/**
* Normalizes KST-style frequency strings with an optional second decimal
* separator.
*
* Examples:
* 144.300.03 -> 144.30003
* 144.300 -> 144.300
*/
private static String normalizeFrequencyString(
String rawInput
) {
int firstDotIndex = rawInput.indexOf('.');
if (firstDotIndex < 0) {
return rawInput;
}
String decimalPart =
rawInput.substring(firstDotIndex + 1);
if (!decimalPart.contains(".")) {
return rawInput;
}
decimalPart =
decimalPart.replace(".", "");
return rawInput.substring(0, firstDotIndex + 1)
+ decimalPart;
}
/**
* Immutable result of one explicit-frequency detection.
*/
public static final class DetectedFrequency {
private final Band band;
private final double frequencyMHz;
private final String sourceText;
private DetectedFrequency(
Band band,
double frequencyMHz,
String sourceText
) {
this.band = band;
this.frequencyMHz = frequencyMHz;
this.sourceText = sourceText;
}
public Band getBand() {
return band;
}
public double getFrequencyMHz() {
return frequencyMHz;
}
public String getSourceText() {
return sourceText;
}
}
}
@@ -83,12 +83,13 @@ public class PriorityCalculator {
// --------------------------------------------------------------------
double score = 100.0;
// if (!member.isWorked()) {
// score += 200.0;
// }
//"worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
// EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
/*
* A manual Sked-fail mark represents an explicit operator decision. The
* penalty is therefore applied after all positive contributions, including
* an imminent sked. Applying it earlier would allow the later sked boost to
* almost completely cancel the failure mark.
*/
boolean manualSkedFailed = false;
if (workedBandsForScoring.isEmpty()) {
score += 200.0; // never worked on any supported band -> higher priority
@@ -190,9 +191,11 @@ public class PriorityCalculator {
}
// Manual sked fail (path likely bad) => strong, permanent penalty until reset
if (mx.manualSkedFailed) {
score *= 0.15;
}
/*
* Remember the explicit failure state here. The actual penalty is applied
* after the sked contribution has been calculated.
*/
manualSkedFailed = mx.manualSkedFailed;
}
}
@@ -226,10 +229,25 @@ public class PriorityCalculator {
// --------------------------------------------------------------------
// 8) Legacy penalty: failed attempts in ChatMember
// --------------------------------------------------------------------
// --------------------------------------------------------------------
// 8) Legacy penalty: failed attempts in ChatMember
// --------------------------------------------------------------------
if (member.getFailedQSOAttempts() > 0) {
score = score / (member.getFailedQSOAttempts() + 1);
}
// --------------------------------------------------------------------
// 9) Explicit operator override: manual Sked fail
// --------------------------------------------------------------------
if (manualSkedFailed) {
/*
* Apply the penalty to the complete result. This includes an imminent
* sked and prevents its strong time-dependent boost from cancelling the
* operator's explicit failure assessment.
*/
score *= 0.15;
}
return Math.max(0.0, score);
}
@@ -0,0 +1,381 @@
package kst4contest.logic;
import kst4contest.model.Band;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
/**
* Selects one realistic propagation frequency for a station.
*
* <p>The same resolution is used by AirScout and by the internal path analysis.
* Only the chat categories supported by these features participate. This keeps
* unrelated KST categories from silently falling back to 144 MHz.</p>
*/
public final class PropagationFrequencyResolver {
// private static final double DUAL_VUHF_MICROWAVE_FALLBACK_MHZ = 430.0;
private PropagationFrequencyResolver() {
}
/** Explains why a frequency was selected. */
public enum Source {
CURRENT_QRG,
STATION_NAME_QRG,
STATION_NAME,
DUAL_CATEGORY_FALLBACK,
CHAT_CATEGORY
}
/**
* Resolves the frequency from all active category variants of one base
* callsign.
*
* <ol>
* <li>Most recently detected QRG</li>
* <li>Lowest band explicitly named by the station</li>
* <li>432 MHz if the station is present in VUHF and Microwave</li>
* <li>Lowest usable fallback band of the supported chat category</li>
* </ol>
*
* <p>Locally disabled and manually excluded bands are never selected.</p>
*
* @param variants active category variants of one callsign
* @param enabledBands bands enabled for the local station
* @param nowEpochMs current time used for the QRG age check
* @return one resolution, or {@code null} if no safe choice exists
*/
public static Resolution resolve(Collection<ChatMember> variants,
EnumSet<Band> enabledBands,
long nowEpochMs) {
if (variants == null || variants.isEmpty()
|| enabledBands == null || enabledBands.isEmpty()) {
return null;
}
List<ChatMember> supportedVariants = variants.stream()
.filter(PropagationFrequencyResolver::isSupportedVariant)
.toList();
if (supportedVariants.isEmpty()) {
return null;
}
BandOpportunityResolver.Resolution opportunityResolution =
BandOpportunityResolver.resolve(supportedVariants, nowEpochMs);
EnumSet<Band> usableBands = EnumSet.copyOf(enabledBands);
usableBands.removeAll(opportunityResolution.getNotQrvBands());
if (usableBands.isEmpty()) {
return null;
}
FrequencyCandidate latestQrg = findLatestQrg(
supportedVariants,
usableBands,
nowEpochMs
);
if (latestQrg != null) {
return new Resolution(
latestQrg.band,
latestQrg.frequencyMHz,
Source.CURRENT_QRG
);
}
FrequencyCandidate stationNameQrg =
findUniqueStationNameQrg(
supportedVariants,
usableBands
);
if (stationNameQrg != null) {
return new Resolution(
stationNameQrg.band,
stationNameQrg.frequencyMHz,
Source.STATION_NAME_QRG
);
}
EnumSet<Band> nameBands = EnumSet.noneOf(Band.class);
for (ChatMember variant : supportedVariants) {
nameBands.addAll(
BandOpportunityResolver.detectBandsFromStationName(variant.getName())
);
}
nameBands.retainAll(usableBands);
Band nameBand = lowestBand(nameBands);
if (nameBand != null) {
return new Resolution(
nameBand,
nameBand.getDefaultAnalysisFrequencyMHz(),
Source.STATION_NAME
);
}
EnumSet<SupportedCategory> categories = collectSupportedCategories(supportedVariants);
if (categories.contains(SupportedCategory.VUHF)
&& categories.contains(SupportedCategory.MICROWAVE)
&& usableBands.contains(Band.B_432)) {
return new Resolution(
Band.B_432,
Band.B_432.getDefaultAnalysisFrequencyMHz(),
Source.DUAL_CATEGORY_FALLBACK
);
}
EnumSet<Band> categoryBands = EnumSet.noneOf(Band.class);
for (SupportedCategory category : categories) {
categoryBands.addAll(category.fallbackBands);
}
categoryBands.retainAll(usableBands);
Band categoryBand = lowestBand(categoryBands);
if (categoryBand == null) {
return null;
}
return new Resolution(
categoryBand,
categoryBand.getDefaultAnalysisFrequencyMHz(),
Source.CHAT_CATEGORY
);
}
private static FrequencyCandidate findLatestQrg(List<ChatMember> variants,
EnumSet<Band> usableBands,
long nowEpochMs) {
FrequencyCandidate latest = null;
for (ChatMember variant : variants) {
for (var entry : variant.getKnownActiveBands().entrySet()) {
Band band = entry.getKey();
ChatMember.ActiveFrequencyInfo info = entry.getValue();
if (band == null || info == null || !usableBands.contains(band)) {
continue;
}
long ageMs = nowEpochMs - info.timestampEpoch;
if (ageMs < 0L
|| ageMs > BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS
|| !Double.isFinite(info.frequency)
|| !band.isPlausible(info.frequency)) {
continue;
}
if (latest == null || info.timestampEpoch > latest.timestampEpochMs) {
latest = new FrequencyCandidate(
band,
info.frequency,
info.timestampEpoch
);
}
}
}
return latest;
}
/**
* Resolves one exact station-name QRG only when the evidence is unambiguous.
*
* <p>The same QRG repeated in several category variants counts only once.
* If different explicit QRGs are advertised, no run frequency is guessed and
* the caller continues with normal band-name/category resolution.</p>
*/
private static FrequencyCandidate findUniqueStationNameQrg(
List<ChatMember> variants,
EnumSet<Band> usableBands
) {
java.util.LinkedHashMap<String, FrequencyCandidate> uniqueCandidates =
new java.util.LinkedHashMap<>();
for (ChatMember variant : variants) {
if (variant == null) {
continue;
}
for (FrequencyTextParser.DetectedFrequency detectedFrequency
: FrequencyTextParser.findExplicitFrequencies(
variant.getName()
)) {
Band band =
detectedFrequency.getBand();
if (!usableBands.contains(band)) {
continue;
}
double frequencyMHz =
detectedFrequency.getFrequencyMHz();
String key =
band.name()
+ "|"
+ Double.toString(frequencyMHz);
uniqueCandidates.putIfAbsent(
key,
new FrequencyCandidate(
band,
frequencyMHz,
Long.MIN_VALUE
)
);
/*
* More than one different exact QRG means that we cannot safely
* identify one run frequency.
*/
if (uniqueCandidates.size() > 1) {
return null;
}
}
}
return uniqueCandidates.size() == 1
? uniqueCandidates.values()
.iterator()
.next()
: null;
}
private static boolean isSupportedVariant(ChatMember member) {
if (member == null || member.getChatCategory() == null) {
return false;
}
int categoryNumber = member.getChatCategory().getCategoryNumber();
return categoryNumber == ChatCategory.FIFTYSEVENTYMHz
|| categoryNumber == ChatCategory.VUHF
|| categoryNumber == ChatCategory.MICROWAVE
|| categoryNumber == ChatCategory.EMEJT65;
}
private static EnumSet<SupportedCategory> collectSupportedCategories(
List<ChatMember> variants
) {
EnumSet<SupportedCategory> categories = EnumSet.noneOf(SupportedCategory.class);
for (ChatMember variant : variants) {
int categoryNumber = variant.getChatCategory().getCategoryNumber();
if (categoryNumber == ChatCategory.FIFTYSEVENTYMHz) {
categories.add(SupportedCategory.FIFTY_SEVENTY);
} else if (categoryNumber == ChatCategory.VUHF) {
categories.add(SupportedCategory.VUHF);
} else if (categoryNumber == ChatCategory.MICROWAVE) {
categories.add(SupportedCategory.MICROWAVE);
} else if (categoryNumber == ChatCategory.EMEJT65) {
categories.add(SupportedCategory.EME);
}
}
return categories;
}
private static Band lowestBand(Collection<Band> bands) {
if (bands == null || bands.isEmpty()) {
return null;
}
return bands.stream()
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(null);
}
private enum SupportedCategory {
FIFTY_SEVENTY(EnumSet.of(Band.B_50, Band.B_70)),
VUHF(EnumSet.of(Band.B_144, Band.B_432)),
MICROWAVE(EnumSet.of(
Band.B_1296,
Band.B_2320,
Band.B_3400,
Band.B_5760,
Band.B_10G,
Band.B_24G
)),
EME(EnumSet.of(
Band.B_144,
Band.B_432,
Band.B_1296,
Band.B_2320,
Band.B_3400,
Band.B_5760,
Band.B_10G,
Band.B_24G
));
private final EnumSet<Band> fallbackBands;
SupportedCategory(EnumSet<Band> fallbackBands) {
this.fallbackBands = fallbackBands;
}
}
private static final class FrequencyCandidate {
private final Band band;
private final double frequencyMHz;
private final long timestampEpochMs;
private FrequencyCandidate(Band band,
double frequencyMHz,
long timestampEpochMs) {
this.band = band;
this.frequencyMHz = frequencyMHz;
this.timestampEpochMs = timestampEpochMs;
}
}
/** Immutable selected band/frequency pair. */
public static final class Resolution {
private final Band band;
private final double analysisFrequencyMHz;
private final Source source;
private Resolution(Band band,
double analysisFrequencyMHz,
Source source) {
this.band = band;
this.analysisFrequencyMHz = analysisFrequencyMHz;
this.source = source;
}
public Band getBand() {
return band;
}
public double getAnalysisFrequencyMHz() {
return analysisFrequencyMHz;
}
public Source getSource() {
return source;
}
/**
* Converts MHz to AirScout's 100-Hz protocol unit.
*
* @return integer protocol value, for example 1442100 for 144.210 MHz
*/
public String getAirScoutBandValue() {
return Long.toString(Math.round(analysisFrequencyMHz * 10_000.0));
}
}
}
@@ -518,6 +518,7 @@ public class ChatMember {
this.frequency = frequency;
}
public long getActivityTimeLastInEpoch() {
return activityTimeLastInEpoch;
}
@@ -595,6 +596,40 @@ public class ChatMember {
}
/**
* Initializes the compatibility frequency only when no frequency is known yet.
*
* <p>This is used for an explicit QRG contained in the ON4KST station name.
* A subsequently detected chat QRG may overwrite this initial value, but a
* station-name QRG never replaces an already known frequency.</p>
*
* @param frequencyMhz explicit station-name frequency
* @return true when the frequency was initialized
*/
public boolean initializeFrequencyIfEmpty(double frequencyMhz) {
if (!Double.isFinite(frequencyMhz)
|| frequencyMhz <= 0.0) {
return false;
}
if (frequency == null) {
frequency = new SimpleStringProperty();
}
String currentFrequency = frequency.get();
if (currentFrequency != null
&& !currentFrequency.isBlank()) {
return false;
}
frequency.set(
Double.toString(frequencyMhz)
);
return true;
}
/**
* Sets all worked information of this object to false. Scope: GUI, Reset Button
* for worked info, called by appcontroller
@@ -50,7 +50,7 @@ public class ChatPreferences {
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
*/
// private static final int CONFIG_VERSION = 2;
public static final int CONFIG_VERSION = 4;
public static final int CONFIG_VERSION = 5;
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
private static final String TAG_CONFIG_VERSION = "configVersion";
@@ -224,7 +224,7 @@ public class ChatPreferences {
boolean logsynch_wintestNetworkListenerEnabled = true; // default true = bisheriges Verhalten
String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test
boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP
String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO
String logsynch_wintestSkedMode = "SSB"; // Supported values: SSB or CW
boolean logsynch_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet
boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet
@@ -245,6 +245,7 @@ public class ChatPreferences {
boolean AirScout_asUDPListenerEnabled = true;
String AirScout_asServerNameString = "AS";
String AirScout_asClientNameString = "KST";
boolean AirScout_autoBandSelectionEnabled = true;
String AirScout_asBandString = "1440000";
int AirScout_asCommunicationPort = 9872;
@@ -339,6 +340,7 @@ public class ChatPreferences {
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
private boolean GUIstationMapPathAnalysisVisible = true;
/*********************************************************************************
@@ -635,6 +637,14 @@ public class ChatPreferences {
this.GUIstationMapStagePositionXY = GUIstationMapStagePositionXY;
}
public boolean isGUIstationMapPathAnalysisVisible() {
return GUIstationMapPathAnalysisVisible;
}
public void setGUIstationMapPathAnalysisVisible(boolean GUIstationMapPathAnalysisVisible) {
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
}
public boolean isGuiOptions_defaultFilterNothing() {
return guiOptions_defaultFilterNothing;
}
@@ -995,6 +1005,16 @@ public class ChatPreferences {
return AirScout_asBandString;
}
public boolean isAirScout_autoBandSelectionEnabled() {
return AirScout_autoBandSelectionEnabled;
}
public void setAirScout_autoBandSelectionEnabled(
boolean airScoutAutoBandSelectionEnabled
) {
AirScout_autoBandSelectionEnabled = airScoutAutoBandSelectionEnabled;
}
public void setAirScout_asBandString(String airScout_asBandString) {
if (airScout_asBandString == null) {
AirScout_asBandString = "1440000";
@@ -1686,6 +1706,13 @@ public class ChatPreferences {
asQry_airScoutUDPPort.setTextContent(this.getAirScout_asCommunicationPort()+"");
AirScoutQuerier.appendChild(asQry_airScoutUDPPort);
Element asQry_airScoutAutoBandSelectionEnabled =
doc.createElement("asQry_airScoutAutoBandSelectionEnabled");
asQry_airScoutAutoBandSelectionEnabled.setTextContent(
Boolean.toString(this.isAirScout_autoBandSelectionEnabled())
);
AirScoutQuerier.appendChild(asQry_airScoutAutoBandSelectionEnabled);
Element asQry_airScoutBandValue = doc.createElement("asQry_airScoutBandValue");
asQry_airScoutBandValue.setTextContent(this.getAirScout_asBandString());
AirScoutQuerier.appendChild(asQry_airScoutBandValue);
@@ -2048,6 +2075,12 @@ public class ChatPreferences {
);
guiOptions.appendChild(GUIstationMapStagePositionXY);
Element GUIstationMapPathAnalysisVisible = doc.createElement("GUIstationMapPathAnalysisVisible");
GUIstationMapPathAnalysisVisible.setTextContent(
String.valueOf(this.isGUIstationMapPathAnalysisVisible())
);
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
/****************************************************************************************
****************************** now write this XML! *************************************
****************************************************************************************/
@@ -2423,8 +2456,11 @@ public class ChatPreferences {
notify_noReplyPenaltyMinutes = noReply;
}
Integer momentum = getInt(notificationsEl, 666, "notify_momentumWindowSeconds");
if (momentum != null) {
Integer momentum = getInt(
notificationsEl,
notify_momentumWindowSeconds,
"notify_momentumWindowSeconds"
); if (momentum != null) {
notify_momentumWindowSeconds = momentum;
}
@@ -2490,6 +2526,14 @@ public class ChatPreferences {
)
);
setAirScout_autoBandSelectionEnabled(
getBoolean(
airScoutEl,
AirScout_autoBandSelectionEnabled,
"asQry_airScoutAutoBandSelectionEnabled"
)
);
setAirScout_asBandString(
getText(
airScoutEl,
@@ -2507,6 +2551,8 @@ public class ChatPreferences {
+ AirScout_asClientNameString
+ ", port="
+ AirScout_asCommunicationPort
+ ", automatic band selection="
+ AirScout_autoBandSelectionEnabled
+ ", band="
+ AirScout_asBandString
);
@@ -2780,6 +2826,17 @@ public class ChatPreferences {
this.getGUIstationMapStagePositionXY()
);
/*
* Files written before config version 5 do not contain this value.
* Keep the default true in that case so existing users discover the
* path-analysis feature before choosing to hide it themselves.
*/
this.setGUIstationMapPathAnalysisVisible(getBoolean(
element,
this.isGUIstationMapPathAnalysisVisible(),
"GUIstationMapPathAnalysisVisible"
));
// Splitpane divider positions
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
if (s1 != null) {
@@ -3,25 +3,58 @@ package kst4contest.model;
/**
* Represents a scheduled event or an AirScout opportunity in the future.
* Used for the Timeline View and Priority Calculation.
*
* <p>The base callsign remains the grouping key for scoring and worked-state
* handling. The exact KST login and its chat category are stored separately
* because reminders and external logger handover refer to the selected
* ChatMember entity.</p>
*/
public class ContestSked {
private String targetCallsign;
private double targetAzimuth; // Required for Antenna-Visuals
private long skedTimeEpoch; // The peak time (e.g., AP)
private String targetChatCallsign;
private ChatCategory targetChatCategory;
private double targetAzimuth;
private long skedTimeEpoch;
private Band band;
// Opportunity potential (0..100). -1 means "unknown".
int opportunityPotentialPercent = -1;
// Status flags to prevent spamming alarms
// Status flags to prevent spamming alarms.
private boolean warning3MinSent = false;
private boolean warningNowSent = false;
public ContestSked(String call, double azimuth, long time, Band b) {
this.targetCallsign = call;
/**
* Backward-compatible constructor.
*/
public ContestSked(String call, double azimuth, long time, Band band) {
this(call, call, null, azimuth, time, band);
}
/**
* Creates a sked for one exact KST login.
*
* @param callRaw base callsign used for scoring and worked states
* @param chatCallsign exact KST login, including an optional dash suffix
* @param chatCategory category in which the selected login is active
* @param azimuth target azimuth
* @param time sked time in epoch milliseconds
* @param band selected amateur-radio band
*/
public ContestSked(String callRaw,
String chatCallsign,
ChatCategory chatCategory,
double azimuth,
long time,
Band band) {
this.targetCallsign = callRaw;
this.targetChatCallsign = chatCallsign;
this.targetChatCategory = chatCategory;
this.targetAzimuth = azimuth;
this.skedTimeEpoch = time;
this.band = b;
this.band = band;
}
/**
@@ -32,15 +65,54 @@ public class ContestSked {
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
}
// Getters and Setters...
public String getTargetCallsign() { return targetCallsign; }
public double getTargetAzimuth() { return targetAzimuth; }
public long getSkedTimeEpoch() { return skedTimeEpoch; }
public Band getBand() { return band; }
public boolean isWarning3MinSent() { return warning3MinSent; }
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
public boolean isWarningNowSent() { return warningNowSent; }
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
/**
* Returns the base callsign used for scoring and worked-state grouping.
*/
public String getTargetCallsign() {
return targetCallsign;
}
/**
* Returns the exact KST login selected when the sked was created.
*/
public String getTargetChatCallsign() {
if (targetChatCallsign == null || targetChatCallsign.isBlank()) {
return targetCallsign;
}
return targetChatCallsign;
}
public ChatCategory getTargetChatCategory() {
return targetChatCategory;
}
public double getTargetAzimuth() {
return targetAzimuth;
}
public long getSkedTimeEpoch() {
return skedTimeEpoch;
}
public Band getBand() {
return band;
}
public boolean isWarning3MinSent() {
return warning3MinSent;
}
public void setWarning3MinSent(boolean warning3MinSent) {
this.warning3MinSent = warning3MinSent;
}
public boolean isWarningNowSent() {
return warningNowSent;
}
public void setWarningNowSent(boolean warningNowSent) {
this.warningNowSent = warningNowSent;
}
public int getOpportunityPotentialPercent() {
return opportunityPotentialPercent;
@@ -0,0 +1,73 @@
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 ChatPreferencesStationMapVisibilityTest {
@TempDir
Path temporaryDirectory;
@Test
void pathAnalysisIsVisibleByDefault() {
ChatPreferences preferences = new ChatPreferences();
assertTrue(preferences.isGUIstationMapPathAnalysisVisible());
}
@Test
void hiddenPathAnalysisStateSurvivesXmlRoundTrip() throws IOException {
Path preferencesFile = temporaryDirectory.resolve("preferences.xml");
ChatPreferences writtenPreferences = new ChatPreferences();
writtenPreferences.setStoreAndRestorePreferencesFileName(
preferencesFile.toString());
writtenPreferences.setGUIstationMapPathAnalysisVisible(false);
writtenPreferences.writePreferencesToXmlFile();
String writtenXml = Files.readString(preferencesFile);
assertTrue(writtenXml.contains(
"<GUIstationMapPathAnalysisVisible>false"
+ "</GUIstationMapPathAnalysisVisible>"));
ChatPreferences restoredPreferences = new ChatPreferences();
restoredPreferences.setStoreAndRestorePreferencesFileName(
preferencesFile.toString());
restoredPreferences.readPreferencesFromXmlFile();
assertFalse(restoredPreferences.isGUIstationMapPathAnalysisVisible());
}
@Test
void legacyXmlWithoutVisibilitySettingKeepsAnalysisDiscoverable()
throws IOException {
Path legacyPreferencesFile =
temporaryDirectory.resolve("legacy-preferences.xml");
Files.writeString(legacyPreferencesFile, """
<?xml version="1.0" encoding="UTF-8"?>
<praktiKST>
<configVersion>4</configVersion>
<guiOptions>
<GUIstationMapStageSceneSizeHW>1000.0;800.0</GUIstationMapStageSceneSizeHW>
</guiOptions>
</praktiKST>
""");
ChatPreferences restoredPreferences = new ChatPreferences();
restoredPreferences.setStoreAndRestorePreferencesFileName(
legacyPreferencesFile.toString());
restoredPreferences.readPreferencesFromXmlFile();
assertTrue(restoredPreferences.isGUIstationMapPathAnalysisVisible());
}
}
@@ -0,0 +1,123 @@
package kst4contest.test;
import kst4contest.logic.FrequencyTextParser;
import kst4contest.model.Band;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class FrequencyTextParserTest {
@Test
void detectsExplicitStationNameFrequencies() {
List<FrequencyTextParser.DetectedFrequency> detected =
FrequencyTextParser.findExplicitFrequencies(
"Phil 432.357"
);
assertEquals(1, detected.size());
assertEquals(
Band.B_432,
detected.get(0).getBand()
);
assertEquals(
432.357,
detected.get(0).getFrequencyMHz(),
0.000_001
);
}
@Test
void detectsCommaAndMicrowaveFrequencies() {
List<FrequencyTextParser.DetectedFrequency> detected =
FrequencyTextParser.findExplicitFrequencies(
"QRV 1296,210 / 10368.100"
);
assertEquals(2, detected.size());
assertEquals(
Band.B_1296,
detected.get(0).getBand()
);
assertEquals(
Band.B_10G,
detected.get(1).getBand()
);
}
@Test
void detectsFiftyAndSeventyMhzFrequencies() {
assertEquals(
Band.B_50,
FrequencyTextParser
.findExplicitFrequencies("50.150")
.get(0)
.getBand()
);
assertEquals(
Band.B_70,
FrequencyTextParser
.findExplicitFrequencies("70.200")
.get(0)
.getBand()
);
}
@Test
void ignoresRelativeAndAmbiguousValues() {
assertTrue(
FrequencyTextParser
.findExplicitFrequencies(
"Mike .180"
)
.isEmpty()
);
assertTrue(
FrequencyTextParser
.findExplicitFrequencies(
"Mike 180"
)
.isEmpty()
);
assertTrue(
FrequencyTextParser
.findExplicitFrequencies(
"David 1.2"
)
.isEmpty()
);
assertTrue(
FrequencyTextParser
.findExplicitFrequencies(
"RST 599"
)
.isEmpty()
);
}
@Test
void normalizesSubKhzNotation() {
FrequencyTextParser.DetectedFrequency detected =
FrequencyTextParser
.findExplicitFrequencies(
"144.300.03"
)
.get(0);
assertEquals(Band.B_144, detected.getBand());
assertEquals(
144.30003,
detected.getFrequencyMHz(),
0.000_001
);
}
}
@@ -0,0 +1,63 @@
package kst4contest.test;
import javafx.beans.property.SimpleStringProperty;
import kst4contest.controller.Utils4KST;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class Utils4KSTFrequencyTest {
@Test
void convertsCompleteFrequenciesAcrossAllSupportedPrefixLengths() {
assertEquals("50200.0", convert("50.200", "144"));
assertEquals("70250.0", convert("70,250", "144"));
assertEquals("144205.0", convert("144.205", "432"));
assertEquals("432088.0", convert("432.088", "144"));
assertEquals("1296338.0", convert("1296.338", "144"));
assertEquals("10368100.0", convert("10368.100", "144"));
assertEquals("24048100.0", convert("24048.100", "144"));
}
@Test
void retainsSubKhzPrecision() {
assertEquals("144205.2", convert("144.205.2", "432"));
assertEquals("1296338.25", convert("1296.338.25", "144"));
}
@Test
void convertsCompactLegacyFormats() {
assertEquals("432088.0", convert("432088", "144"));
assertEquals("10368100.0", convert("10368100", "144"));
assertEquals("432088.2", convert("432088.2", "144"));
assertEquals("432088.0", convert("432 088", "144"));
}
@Test
void appliesFallbackOnlyToRelativeFrequencies() {
assertEquals("432205.0", convert(".205", "432"));
assertEquals("144205.0", convert("205", "144"));
assertEquals("10368300.0", convert(".300", "10368"));
}
@Test
void rejectsMissingOrUnsupportedValues() {
assertEquals("", convert(null, "144"));
assertEquals("", convert("", "144"));
assertEquals("", convert("not-a-frequency", "144"));
assertEquals("", convert("599", "144"));
assertEquals("", convert(".205", null));
assertEquals("", convert(".205", "invalid"));
}
private String convert(String frequency, String fallbackPrefix) {
SimpleStringProperty fallback = fallbackPrefix == null
? null
: new SimpleStringProperty(fallbackPrefix);
return Utils4KST.normalizeFrequencyString(
frequency,
fallback
);
}
}
+55 -3
View File
@@ -1,14 +1,66 @@
package kst4contest.view;
import kst4contest.controller.ChatController;
import kst4contest.model.ChatMember;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import java.net.URL;
public class GuiUtils {
private static final String APPLICATION_ICON_RESOURCE = "/icons/kst4contest.png";
private static Image applicationIcon;
/**
* Applies the common KST4Contest application icon to a JavaFX stage.
*
* <p>The icon is loaded only once and reused for all application windows.
* A missing icon resource must never prevent a window from opening.</p>
*
* @param stage stage that should receive the application icon
*/
public static void applyApplicationIcon(Stage stage) {
if (stage == null) {
return;
}
Image icon = getApplicationIcon();
if (icon != null && !stage.getIcons().contains(icon)) {
stage.getIcons().add(icon);
}
}
/**
* Loads and caches the common KST4Contest application icon.
*
* @return application icon or null if the resource is unavailable
*/
private static Image getApplicationIcon() {
if (applicationIcon != null) {
return applicationIcon;
}
URL iconUrl = GuiUtils.class.getResource(APPLICATION_ICON_RESOURCE);
if (iconUrl == null) {
System.err.println(
"Application icon resource not found: "
+ APPLICATION_ICON_RESOURCE
);
return null;
}
applicationIcon = new Image(iconUrl.toExternalForm());
return applicationIcon;
}
private static final String PTRN_CALLSIGNSYNTAX = "^(?:[A-Z]{1,2}[0-9]|[0-9][A-Z])[0-9A-Z]{1,3}$";
/**
* Checks wheter the input value of the String is numeric or not, true if yes
File diff suppressed because it is too large Load Diff
@@ -208,7 +208,12 @@ public class TimelineView extends Pane {
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent()));
String baseToolTipFallBack = sked.getTargetCallsign() + " (" + sked.getBand() + ")\nAz: " + sked.getTargetAzimuth();
String baseToolTipFallBack =
sked.getTargetChatCallsign()
+ " ("
+ sked.getBand()
+ ")\nAz: "
+ sked.getTargetAzimuth();
if (skedTooltipExtraTextProvider != null) {
String extra = skedTooltipExtraTextProvider.apply(sked);
@@ -220,7 +225,9 @@ public class TimelineView extends Pane {
Tooltip t = new Tooltip(baseToolTipFallBack);
Tooltip.install(diamond, t);
Label lbl = new Label("SKED: " + sked.getTargetCallsign());
Label lbl = new Label(
"SKED: " + sked.getTargetChatCallsign()
);
// lbl.setFont(new Font(9));
// lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(14);
@@ -15,6 +15,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import kst4contest.logic.FrequencyTextParser;
/**
* Builds immutable map snapshots from the currently visible chat members.
@@ -204,6 +205,92 @@ public final class MapCallsignRawSnapshotBuilder {
));
}
}
}
/*
* A QRG explicitly contained in the current station name does not expire.
*
* Recent dynamic QRG evidence has priority. Therefore station-name QRGs fill
* only bands for which no recent dynamic frequency is available.
*
* Different explicit QRGs for the same band are considered ambiguous and are
* not reduced to one arbitrary run frequency.
*/
Map<Band, Map<String, FrequencyTextParser.DetectedFrequency>>
stationNameFrequenciesByBand =
new EnumMap<>(Band.class);
for (ChatMember variant : variants) {
if (variant == null) {
continue;
}
for (FrequencyTextParser.DetectedFrequency detectedFrequency
: FrequencyTextParser.findExplicitFrequencies(
variant.getName()
)) {
Band band =
detectedFrequency.getBand();
if (availableBands == null
|| !availableBands.contains(band)) {
continue;
}
stationNameFrequenciesByBand
.computeIfAbsent(
band,
ignored -> new LinkedHashMap<>()
)
.putIfAbsent(
Double.toString(
detectedFrequency.getFrequencyMHz()
),
detectedFrequency
);
}
}
for (Map.Entry<
Band,
Map<String, FrequencyTextParser.DetectedFrequency>>
entry : stationNameFrequenciesByBand.entrySet()) {
Band band = entry.getKey();
/*
* A recent QRG detected from chat always wins.
*/
if (latestByBand.containsKey(band)) {
continue;
}
/*
* Do not guess when several different QRGs were published for the
* same band.
*/
if (entry.getValue().size() != 1) {
continue;
}
FrequencyTextParser.DetectedFrequency detectedFrequency =
entry.getValue()
.values()
.iterator()
.next();
latestByBand.put(
band,
new FrequencyCandidate(
band,
formatFrequency(
detectedFrequency.getFrequencyMHz()
),
Long.MIN_VALUE
)
);
}
LinkedHashMap<String, String> ordered = new LinkedHashMap<>();
@@ -1,6 +1,5 @@
package kst4contest.view.map;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -496,37 +495,7 @@ public final class PathGeometryUtils {
}
}
/**
* Resolves one usable analysis frequency from aggregated marker frequency data.
*
* <p>Strategy:
* <ol>
* <li>Prefer 144 MHz data if available</li>
* <li>Otherwise use the first parsable known station frequency</li>
* <li>Otherwise use the central default frequency</li>
* </ol>
*
* @param frequenciesByBand known frequencies grouped by band
* @return resolved analysis frequency in MHz
*/
public static double resolveAnalysisFrequencyMHz(Map<String, String> frequenciesByBand) {
if (frequenciesByBand != null && !frequenciesByBand.isEmpty()) {
String band144Text = frequenciesByBand.get("144");
double parsed144 = tryParseFrequencyMHz(band144Text);
if (Double.isFinite(parsed144) && parsed144 > 0.0) {
return parsed144;
}
for (String value : frequenciesByBand.values()) {
double parsedFrequencyMHz = tryParseFrequencyMHz(value);
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
return parsedFrequencyMHz;
}
}
}
return DEFAULT_ANALYSIS_FREQUENCY_MHZ;
}
/**
* Small immutable geographic point used by great-circle interpolation.
@@ -18,6 +18,7 @@ import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
import kst4contest.model.Band;
import java.util.function.Predicate;
@@ -40,6 +41,7 @@ public final class StationMapBridge {
private final TableView<ChatMember> chatMemberTable;
private final StationMapView stationMapView;
private final Consumer<ChatMember> focusChatMemberConsumer;
private final Supplier<Band> reachabilityBandOverrideSupplier;
@@ -55,22 +57,30 @@ public final class StationMapBridge {
public StationMapBridge(ChatController chatController,
TableView<ChatMember> chatMemberTable,
StationMapView stationMapView,
Consumer<ChatMember> focusChatMemberConsumer) {
Consumer<ChatMember> focusChatMemberConsumer,
Supplier<Band> reachabilityBandOverrideSupplier) {
this.chatController = Objects.requireNonNull(chatController, "chatController");
this.chatMemberTable = Objects.requireNonNull(chatMemberTable, "chatMemberTable");
this.stationMapView = Objects.requireNonNull(stationMapView, "stationMapView");
this.focusChatMemberConsumer = Objects.requireNonNull(focusChatMemberConsumer, "focusChatMemberConsumer");
this.focusChatMemberConsumer = Objects.requireNonNull(
focusChatMemberConsumer,
"focusChatMemberConsumer"
);
this.reachabilityBandOverrideSupplier = Objects.requireNonNull(
reachabilityBandOverrideSupplier,
"reachabilityBandOverrideSupplier"
);
this.refreshCoalescer.setOnFinished(event -> refreshNow());
}
public void install() {
stationMapView.setOnCallsignRawSelected(this::handleMapCallsignSelection);
stationMapView.setOnTriggerClusterSpot(this::handleExplicitClusterSpot);
stationMapView.setOnResetView(this::handleMapReset);
chatController.getLst_chatMemberSortedFilteredList().addListener(
(ListChangeListener<ChatMember>) change -> scheduleRefresh()
);
@@ -90,6 +100,35 @@ public final class StationMapBridge {
requestImmediateRefresh();
}
private void handleMapReset() {
Runnable resetAction = () -> {
/*
* Ignore an analysis result that may still arrive for the
* previously selected station.
*/
pathAnalysisGeneration.incrementAndGet();
lastPathAnalysisRequestSignature = "";
/*
* Clear the application's central station selection.
*/
chatController.getScoreService().setSelectedChatMember(null);
/*
* Keep the main station table synchronized with the central selection.
*/
chatMemberTable.getSelectionModel().clearSelection();
requestImmediateRefresh();
};
if (Platform.isFxApplicationThread()) {
resetAction.run();
} else {
Platform.runLater(resetAction);
}
}
public void showWindow() {
stationMapView.showWindow();
requestImmediateRefresh();
@@ -115,6 +154,18 @@ public final class StationMapBridge {
}
}
/**
* Forces the currently selected map path to be requested again.
*
* <p>This is used by the explicit "Calc selected" action after the operator
* changed the reachability band. Merely changing the ComboBox still does not
* trigger terrain analysis.</p>
*/
public void requestSelectedPathAnalysisRefresh() {
lastPathAnalysisRequestSignature = "";
requestImmediateRefresh();
}
public void focusSelectedCallsign() {
showWindow();
@@ -222,9 +273,12 @@ public final class StationMapBridge {
ChatMember selectedMember = resolveBestChatMember(targetCallsignRaw);
Band requestedBandOverride = reachabilityBandOverrideSupplier.get();
chatController.getReachabilityService().requestPathAnalysisForMap(
selectedMember,
selectedSnapshot,
requestedBandOverride,
result -> {
if (generation != pathAnalysisGeneration.get()) {
return;
@@ -232,6 +286,7 @@ public final class StationMapBridge {
stationMapView.setPathAnalysisResult(result);
}
);
}
/**
@@ -259,6 +314,13 @@ public final class StationMapBridge {
chatMemberTable.scrollTo(resolved);
focusChatMemberConsumer.accept(resolved);
/*
* A map click is an explicit operator action. Clear the signature so a
* newly selected reachability band is honored even when the same station
* is clicked again.
*/
lastPathAnalysisRequestSignature = "";
requestImmediateRefresh();
});
}
@@ -315,10 +377,16 @@ public final class StationMapBridge {
private double resolveAnalysisFrequencyMHz(MapCallsignRawSnapshot selectedSnapshot) {
if (selectedSnapshot == null) {
return PathGeometryUtils.DEFAULT_ANALYSIS_FREQUENCY_MHZ;
return Double.NaN;
}
return PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
ChatMember selectedMember = resolveBestChatMember(selectedSnapshot.callSignRaw());
var resolution = chatController.getReachabilityService()
.resolveAutomaticPropagationFrequency(selectedMember);
return resolution == null
? Double.NaN
: resolution.getAnalysisFrequencyMHz();
}
@@ -13,6 +13,7 @@ import javafx.scene.layout.VBox;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import kst4contest.view.GuiUtils;
import netscape.javascript.JSObject;
import kst4contest.ApplicationConstants;
import kst4contest.locatorUtils.Location;
@@ -28,6 +29,8 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.ColumnConstraints;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
/**
* Standalone station map window.
@@ -49,6 +52,9 @@ public final class StationMapView {
*/
private static final boolean MAP_DEBUG_LOGGING = false;
private static final double MINIMUM_HEIGHT_WITH_PATH_ANALYSIS = 650.0;
private static final double MINIMUM_HEIGHT_WITHOUT_PATH_ANALYSIS = 420.0;
private final PathProfileChart detailPathProfileChart = new PathProfileChart();
private final Label detailPathModeValue = new Label("-");
@@ -76,13 +82,20 @@ public final class StationMapView {
private VBox detailPane;
private final Label statusLabel = new Label("Station map not initialized yet.");
private final Label detailCallsignValue = new Label("-");
private final Label detailLocatorValue = new Label("-");
private final Label detailQrbValue = new Label("-");
private final Label detailQtfValue = new Label("-");
private final Label detailBandsValue = new Label("-");
private final TextArea detailFrequenciesArea = new TextArea();
private final Label detailAirplanesValue = new Label("-");
private final Label pathAnalysisHiddenHintLabel = new Label("Path analysis is hidden.");
private final Button pathAnalysisVisibilityButton = new Button();
private final Tooltip pathAnalysisVisibilityTooltip = new Tooltip();
private final Button resetViewButton = new Button("Reset view");
private final Tooltip statusTooltip = new Tooltip();
private Runnable onResetView;
private double lastDetailDividerPosition = 0.65;
private final Button triggerClusterSpotButton = new Button("Trigger cluster spot");
private final Label detailPathFromLocatorValue = new Label("-");
@@ -138,8 +151,12 @@ public final class StationMapView {
private PathAnalysisResult lastPathAnalysisResult = PathAnalysisResult.waitingForSelection("");
private VBox mapAndProfilePane;
private VBox profileSection;
private VBox pathAnalysisSection;
private ScrollPane detailScrollPane;
private final Label detailPathLosValue = new Label("-");
private final Label detailPathWorstClearanceValue = new Label("-");
@@ -164,6 +181,8 @@ public final class StationMapView {
public StationMapView(ChatPreferences chatPreferences) {
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
GuiUtils.applyApplicationIcon(stage);
try {
tileProxyServer = new TileProxyServer();
} catch (IOException e) {
@@ -192,6 +211,11 @@ public final class StationMapView {
this.onTriggerClusterSpot = onTriggerClusterSpot;
}
public void setOnResetView(Runnable onResetView) {
this.onResetView = onResetView;
}
public void showWindow() {
applyThemeFromPreferences();
@@ -264,10 +288,6 @@ public final class StationMapView {
stage.setTitle("Station Map");
detailFrequenciesArea.setEditable(false);
detailFrequenciesArea.setWrapText(true);
detailFrequenciesArea.setPrefRowCount(4);
detailPathEndpointsValue.setWrapText(true);
detailPathEndpointsValue.setMaxWidth(Double.MAX_VALUE);
@@ -281,12 +301,31 @@ public final class StationMapView {
detailPathMechanismsValue.setMaxWidth(Double.MAX_VALUE);
triggerClusterSpotButton.setDisable(true);
triggerClusterSpotButton.setVisible(false);
triggerClusterSpotButton.setManaged(false);
triggerClusterSpotButton.setMinWidth(Region.USE_PREF_SIZE);
triggerClusterSpotButton.setOnAction(event -> {
if (detailCallsignRaw != null && onTriggerClusterSpot != null) {
onTriggerClusterSpot.accept(detailCallsignRaw);
}
});
resetViewButton.setMinWidth(Region.USE_PREF_SIZE);
resetViewButton.setOnAction(event -> {
if (onResetView != null) {
onResetView.run();
}
});
pathAnalysisVisibilityButton.setMinWidth(Region.USE_PREF_SIZE);
pathAnalysisVisibilityButton.setTooltip(pathAnalysisVisibilityTooltip);
pathAnalysisVisibilityButton.setOnAction(event ->
setPathAnalysisVisible(!profileSection.isVisible(), true));
pathAnalysisHiddenHintLabel.setMinWidth(Region.USE_PREF_SIZE);
pathAnalysisHiddenHintLabel.setStyle("-fx-font-style: italic; -fx-opacity: 0.85;");
webView.setFocusTraversable(true);
webView.setPickOnBounds(true);
@@ -323,7 +362,8 @@ public final class StationMapView {
webView.widthProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
webView.heightProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
VBox profileSection = createProfileSection();
profileSection = createProfileSection();
mapAndProfilePane = new VBox(6, webView, profileSection);
mapAndProfilePane.setPadding(new Insets(0));
@@ -349,10 +389,9 @@ public final class StationMapView {
mapAndProfilePane.widthProperty().subtract(20)
);
detailPane = new VBox(10,
createSelectedStationSection(),
createPathAnalysisSection()
);
pathAnalysisSection = createPathAnalysisSection();
detailPane = new VBox(10, pathAnalysisSection);
detailPane.setPadding(new Insets(10));
detailPane.setMinWidth(0);
@@ -364,8 +403,14 @@ public final class StationMapView {
detailScrollPane.setFitToWidth(true);
detailScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
detailScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
detailScrollPane.setMinWidth(300);
/*
* Allow the details pane to be reduced far enough to leave more room for the
* map. At its minimum width, a callsign with up to ten characters remains
* readable.
*/
detailScrollPane.setMinWidth(210);
detailScrollPane.setPrefWidth(350);
detailScrollPane.setMaxWidth(Double.MAX_VALUE);
detailScrollPane.setFitToWidth(true);
@@ -378,8 +423,7 @@ public final class StationMapView {
SplitPane.setResizableWithParent(detailScrollPane, true);
rootPane = new BorderPane();
rootPane.setTop(statusLabel);
BorderPane.setMargin(statusLabel, new Insets(8));
rootPane.setTop(createMapHeader());
rootPane.setCenter(mainSplitPane);
double[] size = chatPreferences.getGUIstationMapStageSceneSizeHW();
@@ -390,7 +434,12 @@ public final class StationMapView {
scene = new Scene(rootPane, initialWidth, initialHeight);
stage.setMinWidth(900);
stage.setMinHeight(650);
stage.setMinHeight(resolveMinimumStationMapHeight(
chatPreferences.isGUIstationMapPathAnalysisVisible()));
stage.setScene(scene);
setPathAnalysisVisible(chatPreferences.isGUIstationMapPathAnalysisVisible(), false);
applyThemeFromPreferences();
stage.setScene(scene);
applyThemeFromPreferences();
@@ -446,46 +495,98 @@ public final class StationMapView {
}
private VBox createSelectedStationSection() {
GridPane detailGrid = new GridPane();
detailGrid.setHgap(8);
detailGrid.setVgap(6);
/**
* Creates an always-visible header for the map status and analysis controls.
*
* Keeping the control outside the sections that it hides is important: users
* must always have an obvious way to restore a previously hidden analysis.
*/
private HBox createMapHeader() {
statusLabel.setMinWidth(0);
statusLabel.setMaxWidth(Double.MAX_VALUE);
statusLabel.setTextOverrun(OverrunStyle.ELLIPSIS);
statusLabel.setTooltip(statusTooltip);
configureCompactGrid(detailGrid);
int row = 0;
detailGrid.add(new Label("Station:"), 0, row);
detailGrid.add(detailCallsignValue, 1, row++);
detailGrid.add(new Label("Locator:"), 0, row);
detailGrid.add(detailLocatorValue, 1, row++);
detailGrid.add(new Label("Path:"), 0, row);
Label compactPathValue = new Label();
compactPathValue.textProperty().bind(
detailQrbValue.textProperty()
.concat(" / ")
.concat(detailQtfValue.textProperty())
HBox header = new HBox(
10,
statusLabel,
triggerClusterSpotButton,
resetViewButton,
pathAnalysisHiddenHintLabel,
pathAnalysisVisibilityButton
);
detailGrid.add(compactPathValue, 1, row++);
detailGrid.add(new Label("Bands:"), 0, row);
detailGrid.add(detailBandsValue, 1, row++);
header.setAlignment(Pos.CENTER_LEFT);
header.setPadding(new Insets(8));
// Frequencies are useful, but they consume vertical space. Keep them compact.
detailFrequenciesArea.setPrefRowCount(2);
detailGrid.add(new Label("QRG:"), 0, row);
detailGrid.add(detailFrequenciesArea, 1, row++);
HBox.setHgrow(statusLabel, Priority.ALWAYS);
return new VBox(8,
new Label("Selected station"),
new Separator(Orientation.HORIZONTAL),
detailGrid,
triggerClusterSpotButton
);
return header;
}
/**
* Shows or hides both parts of the path analysis as one logical feature.
*
* Both visible and managed must be changed. A node that is merely invisible
* would still reserve layout space and the map would not grow into that area.
* The current analysis result remains attached to the controls and is
* immediately available again when the user restores the sections.
*
* @param visible true to show the profile and detailed analysis
* @param persist true when the change was explicitly requested by the user
*/
private void setPathAnalysisVisible(boolean visible, boolean persist) {
profileSection.setVisible(visible);
profileSection.setManaged(visible);
pathAnalysisSection.setVisible(visible);
pathAnalysisSection.setManaged(visible);
updateDetailPanePresence(visible);
pathAnalysisHiddenHintLabel.setVisible(!visible);
pathAnalysisHiddenHintLabel.setManaged(!visible);
pathAnalysisVisibilityButton.setText(
visible ? "Hide path analysis" : "Show path analysis");
pathAnalysisVisibilityButton.setAccessibleText(
visible ? "Hide path analysis" : "Show path analysis");
pathAnalysisVisibilityButton.setStyle(
visible ? "" : "-fx-font-weight: bold;");
pathAnalysisVisibilityTooltip.setText(visible
? "Hide the path profile and detailed path analysis. You can show them again at any time."
: "Show the path profile and detailed path analysis.");
pathAnalysisVisibilityButton.setAccessibleHelp(
pathAnalysisVisibilityTooltip.getText());
if (!visible) {
/*
* Do not leave a profile hover marker on the map after its chart was
* hidden.
*/
showProfileHoverPointOnMap(null);
}
stage.setMinHeight(resolveMinimumStationMapHeight(visible));
if (persist) {
chatPreferences.setGUIstationMapPathAnalysisVisible(visible);
}
if (rootPane != null) {
rootPane.requestLayout();
Platform.runLater(this::requestMapInvalidateSize);
}
}
private double resolveMinimumStationMapHeight(boolean pathAnalysisVisible) {
return pathAnalysisVisible
? MINIMUM_HEIGHT_WITH_PATH_ANALYSIS
: MINIMUM_HEIGHT_WITHOUT_PATH_ANALYSIS;
}
/**
* ensures that the labels remains visible
* @param gridPane
@@ -494,13 +595,18 @@ public final class StationMapView {
gridPane.getColumnConstraints().clear();
ColumnConstraints labelColumn = new ColumnConstraints();
labelColumn.setMinWidth(105);
labelColumn.setMinWidth(70);
labelColumn.setPrefWidth(115);
labelColumn.setMaxWidth(130);
labelColumn.setHgrow(Priority.NEVER);
ColumnConstraints valueColumn = new ColumnConstraints();
valueColumn.setMinWidth(180);
/*
* Reserve enough space for a callsign with up to ten characters, while still
* allowing the details pane to become considerably narrower.
*/
valueColumn.setMinWidth(85);
valueColumn.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(labelColumn, valueColumn);
@@ -824,58 +930,107 @@ public final class StationMapView {
private void updateStatusLabel() {
StringBuilder text = new StringBuilder();
text.append("Showing ").append(lastSnapshots.size()).append(" visible stations");
text.append("Showing ")
.append(lastSnapshots.size())
.append(" visible stations");
if (filteredViewActive) {
text.append(" | filtered view active");
}
statusLabel.setText(text.toString());
MapCallsignRawSnapshot selectedSnapshot = lastSelectedSnapshot;
if (selectedSnapshot != null) {
text.append(" | Selected: ")
.append(selectedSnapshot.displayCallSign());
if (!selectedSnapshot.locator6().isBlank()) {
text.append(" | ")
.append(selectedSnapshot.locator6());
}
text.append(" | ")
.append(String.format(
Locale.US,
"%.0f km / %.0f°",
selectedSnapshot.qrbKm(),
selectedSnapshot.qtfDeg()
));
String bandText = selectedSnapshot.bandSummary().isBlank()
? "-"
: selectedSnapshot.bandSummary();
if (selectedSnapshot.offersSelectedBand()) {
bandText += " B+";
}
text.append(" | Bands: ")
.append(bandText);
String frequencies = selectedSnapshot.detailFrequencyText();
if (frequencies != null && !frequencies.isBlank()) {
frequencies = frequencies
.replace('\n', ' ')
.replace('\r', ' ')
.replaceAll("\\s+", " ")
.trim();
text.append(" | QRG: ")
.append(frequencies);
}
}
String statusText = text.toString();
statusLabel.setText(statusText);
statusTooltip.setText(statusText);
}
private void updateDetailPanel(MapCallsignRawSnapshot selectedSnapshot) {
if (selectedSnapshot == null) {
clearSelectedStationPanel();
detailCallsignRaw = null;
triggerClusterSpotButton.setDisable(true);
triggerClusterSpotButton.setVisible(false);
triggerClusterSpotButton.setManaged(false);
clearPathAnalysisPanel();
return;
}
updateSelectedStationPanel(selectedSnapshot);
if (selectedSnapshot == null) {
updatePathAnalysisPanel(PathAnalysisResult.waitingForSelection(homeLocator6));
} else {
updatePathAnalysisPanel(lastPathAnalysisResult);
}
}
private void clearSelectedStationPanel() {
detailCallsignRaw = null;
detailCallsignValue.setText("-");
detailLocatorValue.setText("-");
detailQrbValue.setText("-");
detailQtfValue.setText("-");
detailBandsValue.setText("-");
detailFrequenciesArea.setText("-");
detailAirplanesValue.setText("-");
triggerClusterSpotButton.setDisable(true);
}
private void updateSelectedStationPanel(MapCallsignRawSnapshot selectedSnapshot) {
detailCallsignRaw = selectedSnapshot.callSignRaw();
detailCallsignValue.setText(selectedSnapshot.displayCallSign());
detailLocatorValue.setText(selectedSnapshot.locator6());
detailQrbValue.setText(String.format(Locale.US, "%.0f km", selectedSnapshot.qrbKm()));
detailQtfValue.setText(String.format(Locale.US, "%.0f°", selectedSnapshot.qtfDeg()));
String bandText = selectedSnapshot.bandSummary().isBlank() ? "-" : selectedSnapshot.bandSummary();
if (selectedSnapshot.offersSelectedBand()) {
bandText += " B+";
}
detailBandsValue.setText(bandText);
detailFrequenciesArea.setText(selectedSnapshot.detailFrequencyText());
detailAirplanesValue.setText(String.valueOf(selectedSnapshot.reachableAirplanes()));
triggerClusterSpotButton.setDisable(false);
triggerClusterSpotButton.setVisible(true);
triggerClusterSpotButton.setManaged(true);
updatePathAnalysisPanel(lastPathAnalysisResult);
}
private void updateDetailPanePresence(boolean visible) {
if (mainSplitPane == null || detailScrollPane == null) {
return;
}
if (visible) {
if (!mainSplitPane.getItems().contains(detailScrollPane)) {
mainSplitPane.getItems().add(detailScrollPane);
Platform.runLater(() ->
mainSplitPane.setDividerPositions(lastDetailDividerPosition)
);
}
} else {
if (!mainSplitPane.getDividers().isEmpty()) {
lastDetailDividerPosition =
mainSplitPane.getDividers().get(0).getPosition();
}
mainSplitPane.getItems().remove(detailScrollPane);
}
}
private void clearPathAnalysisPanel() {
@@ -1205,13 +1360,13 @@ public final class StationMapView {
mainSplitPane.setStyle("-fx-background-color: #2b3035;");
detailPane.setStyle("-fx-background-color: #31373c; -fx-border-color: #4c565c; -fx-border-width: 0 0 0 1;");
statusLabel.setStyle("-fx-background-color: #373e43; -fx-text-fill: lightgray; -fx-padding: 8 10 8 10; -fx-background-radius: 4;");
detailFrequenciesArea.setStyle("-fx-control-inner-background: #444b50; -fx-text-fill: lightgray;");
// detailFrequenciesArea.setStyle("-fx-control-inner-background: #444b50; -fx-text-fill: lightgray;");
} else {
rootPane.setStyle("-fx-background-color: #f2f2f2;");
mainSplitPane.setStyle("-fx-background-color: #f2f2f2;");
detailPane.setStyle("-fx-background-color: #f7f7f7; -fx-border-color: #d0d0d0; -fx-border-width: 0 0 0 1;");
statusLabel.setStyle("-fx-background-color: #f7f7f7; -fx-text-fill: #333333; -fx-padding: 8 10 8 10; -fx-background-radius: 4;");
detailFrequenciesArea.setStyle("");
// detailFrequenciesArea.setStyle("");
}
}
@@ -1530,8 +1685,11 @@ public final class StationMapView {
return 768.0;
}
double minimumHeight = resolveMinimumStationMapHeight(
chatPreferences.isGUIstationMapPathAnalysisVisible());
// Avoid restoring very large old test sizes after the layout changed.
if (storedSize[1] < 650.0 || storedSize[1] > 1100.0) {
if (storedSize[1] < minimumHeight || storedSize[1] > 1100.0) {
return 768.0;
}
+1
View File
@@ -10,6 +10,7 @@ module praktiKST {
requires java.net.http;
requires java.desktop;
requires jdk.crypto.ec;
requires jdk.net;
requires org.junit.jupiter.api;
requires org.mockito;
exports kst4contest.controller.interfaces;
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -14,6 +14,39 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class BandOpportunityResolverTest {
@Test
void explicitStationNameFrequencyProvidesBandEvidence() {
ChatMember station = new ChatMember();
station.setName("Phil 432.357");
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(
List.of(station),
System.currentTimeMillis()
);
assertEquals(
EnumSet.of(Band.B_432),
resolution.getOfferedBands()
);
}
@Test
void relativeStationNameFrequencyDoesNotProvideBandEvidence() {
ChatMember station = new ChatMember();
station.setName("Mike .180");
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(
List.of(station),
System.currentTimeMillis()
);
assertTrue(
resolution.getOfferedBands().isEmpty()
);
}
@Test
void resolvesCommonShorthandBandsFromStationName() {
EnumSet<Band> bands = BandOpportunityResolver.detectBandsFromStationName(
@@ -15,6 +15,70 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class MapCallsignRawSnapshotBuilderTest {
@Test
void explicitStationNameQrgIsShownInMapSnapshot() {
ChatMember station =
buildStation(
"G0JSB",
"Phil 432.357",
"IO91AA",
1_000L
);
MapCallsignRawSnapshot snapshot =
new MapCallsignRawSnapshotBuilder()
.buildSnapshots(
List.of(station),
null,
EnumSet.of(Band.B_432)
)
.get(0);
assertEquals(
"432",
snapshot.bandSummary()
);
assertEquals(
"432.357",
snapshot
.lastKnownFrequenciesByBand()
.get("432")
);
}
@Test
void recentDynamicQrgWinsOverStationNameQrgInMap() {
ChatMember station =
buildStation(
"G0JSB",
"Phil 432.357",
"IO91AA",
1_000L
);
station.addKnownFrequency(
Band.B_432,
432.335
);
MapCallsignRawSnapshot snapshot =
new MapCallsignRawSnapshotBuilder()
.buildSnapshots(
List.of(station),
null,
EnumSet.of(Band.B_432)
)
.get(0);
assertEquals(
"432.335",
snapshot
.lastKnownFrequenciesByBand()
.get("432")
);
}
@Test
void marksSnapshotWhenNameAdvertisesSelectedBand() {
ChatMember station = buildStation("DL1ABC", "QRV 2-70-23", "JN58TD", 1_000L);
@@ -0,0 +1,291 @@
package kst4contest.test;
import kst4contest.logic.PropagationFrequencyResolver;
import kst4contest.model.Band;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import org.junit.jupiter.api.Test;
import java.util.EnumSet;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class PropagationFrequencyResolverTest {
private static final long NOW = 10_000_000L;
@Test
void exactStationNameQrgWinsOverBandAndCategoryFallback() {
ChatMember station =
station(
ChatCategory.VUHF,
"Phil 432.357"
);
PropagationFrequencyResolver.Resolution resolution =
resolve(
List.of(station),
EnumSet.of(
Band.B_144,
Band.B_432
)
);
assertEquals(
Band.B_432,
resolution.getBand()
);
assertEquals(
432.357,
resolution.getAnalysisFrequencyMHz(),
0.000_001
);
assertEquals(
PropagationFrequencyResolver.Source.STATION_NAME_QRG,
resolution.getSource()
);
}
@Test
void recentChatQrgWinsOverExactStationNameQrg() {
ChatMember station =
station(
ChatCategory.VUHF,
"Phil 432.357"
);
addCurrentQrg(
station,
Band.B_432,
432.335,
NOW - 1_000L
);
PropagationFrequencyResolver.Resolution resolution =
resolve(
List.of(station),
EnumSet.of(Band.B_432)
);
assertEquals(
432.335,
resolution.getAnalysisFrequencyMHz(),
0.000_001
);
assertEquals(
PropagationFrequencyResolver.Source.CURRENT_QRG,
resolution.getSource()
);
}
@Test
void multipleStationNameQrgsAreNotTreatedAsOneRunFrequency() {
ChatMember station =
station(
ChatCategory.EMEJT65,
"QRV 432.357 1296.210"
);
PropagationFrequencyResolver.Resolution resolution =
resolve(
List.of(station),
EnumSet.of(
Band.B_432,
Band.B_1296
)
);
/*
* Both bands are known, but no exact run QRG is guessed.
* Existing station-name band selection therefore chooses the
* lowest usable advertised band.
*/
assertEquals(
Band.B_432,
resolution.getBand()
);
assertEquals(
432.0,
resolution.getAnalysisFrequencyMHz(),
0.000_001
);
assertEquals(
PropagationFrequencyResolver.Source.STATION_NAME,
resolution.getSource()
);
}
@Test
void currentQrgWinsOverNameAndCategoryFallback() {
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
addCurrentQrg(station, Band.B_2320, 2320.175, NOW - 1_000L);
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_1296, Band.B_2320, Band.B_10G)
);
assertEquals(Band.B_2320, resolution.getBand());
assertEquals(2320.175, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(PropagationFrequencyResolver.Source.CURRENT_QRG, resolution.getSource());
assertEquals("23201750", resolution.getAirScoutBandValue());
}
@Test
void mostRecentlyDetectedQrgWinsAcrossCategoryVariants() {
ChatMember vhf = station(ChatCategory.VUHF, "");
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
addCurrentQrg(vhf, Band.B_144, 144.210, NOW - 10_000L);
addCurrentQrg(microwave, Band.B_1296, 1296.210, NOW - 1_000L);
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(vhf, microwave),
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
);
assertEquals(Band.B_1296, resolution.getBand());
assertEquals(1296.210, resolution.getAnalysisFrequencyMHz(), 0.000_001);
}
@Test
void microwaveFallsBackToLowestEnabledMicrowaveBand() {
ChatMember station = station(ChatCategory.MICROWAVE, "");
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_2320, Band.B_3400)
);
assertEquals(Band.B_2320, resolution.getBand());
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(PropagationFrequencyResolver.Source.CHAT_CATEGORY, resolution.getSource());
}
@Test
void supportedCategoriesUseTheirAgreedLowestFallbackBand() {
assertEquals(
Band.B_50,
resolve(
List.of(station(ChatCategory.FIFTYSEVENTYMHz, "")),
EnumSet.of(Band.B_50, Band.B_70)
).getBand()
);
assertEquals(
Band.B_144,
resolve(
List.of(station(ChatCategory.VUHF, "")),
EnumSet.of(Band.B_144, Band.B_432)
).getBand()
);
assertEquals(
Band.B_1296,
resolve(
List.of(station(ChatCategory.MICROWAVE, "")),
EnumSet.of(Band.B_1296, Band.B_2320)
).getBand()
);
assertEquals(
Band.B_144,
resolve(
List.of(station(ChatCategory.EMEJT65, "")),
EnumSet.of(Band.B_144, Band.B_1296)
).getBand()
);
}
@Test
void vhfAndMicrowaveUse432MhzFallback() {
ChatMember vhf = station(ChatCategory.VUHF, "");
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(vhf, microwave),
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
);
assertEquals(Band.B_432, resolution.getBand());
assertEquals(432.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(
PropagationFrequencyResolver.Source.DUAL_CATEGORY_FALLBACK,
resolution.getSource()
);
assertEquals("4320000", resolution.getAirScoutBandValue());
}
@Test
void unsupportedChatCategoriesDoNotFallBackTo144Mhz() {
for (int categoryNumber = ChatCategory.LOWBAND;
categoryNumber <= ChatCategory.TENMeter;
categoryNumber++) {
ChatMember unsupported = station(categoryNumber, "QRV 2m");
addCurrentQrg(unsupported, Band.B_144, 144.300, NOW - 1_000L);
assertNull(
resolve(
List.of(unsupported),
EnumSet.of(Band.B_144, Band.B_432)
),
"Category " + categoryNumber + " must be ignored"
);
}
}
@Test
void manualNotQrvExclusionForcesNextUsableMicrowaveBand() {
ChatMember station = station(ChatCategory.MICROWAVE, "");
station.setQrv1240(false);
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_1296, Band.B_2320)
);
assertEquals(Band.B_2320, resolution.getBand());
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
}
@Test
void stationNameBandHintWinsOverCategoryFallback() {
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_1296, Band.B_10G)
);
assertEquals(Band.B_10G, resolution.getBand());
assertEquals(10368.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(PropagationFrequencyResolver.Source.STATION_NAME, resolution.getSource());
}
private PropagationFrequencyResolver.Resolution resolve(
List<ChatMember> variants,
EnumSet<Band> enabledBands
) {
return PropagationFrequencyResolver.resolve(variants, enabledBands, NOW);
}
private ChatMember station(int categoryNumber, String name) {
ChatMember station = new ChatMember();
station.setCallSign("DL1ABC");
station.setChatCategory(new ChatCategory(categoryNumber));
station.setName(name);
return station;
}
private void addCurrentQrg(ChatMember station,
Band band,
double frequencyMHz,
long timestampEpochMs) {
station.addKnownFrequency(band, frequencyMHz);
station.getKnownActiveBands().get(band).timestampEpoch = timestampEpochMs;
}
}
+8 -1
View File
@@ -2379,4 +2379,11 @@ OZ1DLD/P;Bent;JO45SK;StringProperty [value: 144.285]; wkd true; wkd144 true; wkd
SM6VTZ;Chris 2/70/23/3;JO58UJ;StringProperty [value: 144.135]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ7KJ;Skive Club;JO46ML;StringProperty [value: 144.225]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OV3T;Thomas;JO46CM;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ6TY;Henning;JO55XE;StringProperty [value: 144.196]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ6TY;Henning;JO55XE;StringProperty [value: 144.196]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
DF7KF;Dithmar;JO30FK;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
DH1NFJ;Jochen;JO50QL;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 3: Microwave
PA2RU;René;JO32LT;StringProperty [value: null]; wkd true; wkd144 false; wkd432false; wkd1240true; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 144.135]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
SM7EYW;Torleif 432,205;JO65NK;StringProperty [value: 144.205]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
DJ8MS;Tor_70cm;JO54UC;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
DK0MM;Jens/Alex;JN49IU;StringProperty [value: 432.305]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
+19 -8
View File
@@ -71,6 +71,14 @@ function getManualPageOrder(lang, slug) {
return index >= 0 ? index : 999;
}
function githubCompatibleSlug(value) {
return (value || "")
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
.replace(/\s+/g, "-");
}
function rewriteManualLinks(content, lang) {
return (content || "")
@@ -97,15 +105,17 @@ function rewriteManualLinks(content, lang) {
return `[${label}](/manual/${linkLang}/${slug.toLowerCase()}/)`;
})
// Markdown-Links auf de-/en-Dateien ohne .md
.replace(/\]\((en|de)-([^)#]+)\)/g, (match, linkLang, slug) => {
return `](/manual/${linkLang}/${slug.toLowerCase()}/)`;
})
// Markdown links to German or English manual pages.
// The source may contain an optional ".md" suffix and an optional anchor.
// Both forms must be converted to the corresponding website manual URL.
.replace(
/\]\((en|de)-([^#)]+?)(?:\.md)?(?:#([^)]+))?\)/g,
(match, linkLang, slug, anchor) => {
const fragment = anchor ? `#${anchor}` : "";
// Markdown-Links auf de-/en-Dateien mit .md
.replace(/\]\((en|de)-([^)#]+)\.md\)/g, (match, linkLang, slug) => {
return `](/manual/${linkLang}/${slug.toLowerCase()}/)`;
});
return `](/manual/${linkLang}/${slug.toLowerCase()}/${fragment})`;
}
);
}
module.exports = function (eleventyConfig) {
@@ -155,6 +165,7 @@ module.exports = function (eleventyConfig) {
linkify: true,
typographer: true
}).use(markdownItAnchor, {
slugify: githubCompatibleSlug,
permalink: markdownItAnchor.permalink.headerLink()
});
+3 -1
View File
@@ -4,7 +4,9 @@
"private": true,
"scripts": {
"start": "eleventy --serve",
"build": "eleventy"
"build": "eleventy && npm run validate:version-info",
"validate:version-info": "node scripts/validate-version-info.js",
"test": "node --test test/*.test.js"
},
"devDependencies": {
"@11ty/eleventy": "^3.0.0"
+280
View File
@@ -0,0 +1,280 @@
const fs = require("fs");
const path = require("path");
const DEFAULT_FILE = path.join(
__dirname,
"..",
"_site",
"kst4ContestVersionInfo.xml"
);
function fail(message) {
throw new Error(`[versionInfo validation] ${message}`);
}
function normaliseVersion(value) {
return String(value || "")
.trim()
.replace(/^v/, "")
.split("-")[0]
.split("+")[0];
}
function extractTag(xml, tagName) {
const match = xml.match(
new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`)
);
return match ? match[1].trim() : null;
}
function assertWellFormedStructure(xml) {
if (/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-f]+;)/i.test(xml)) {
fail("the XML contains an unescaped ampersand");
}
const withoutCommentsAndDeclaration = xml
.replace(/<!--[\s\S]*?-->/g, "")
.replace(/<\?[\s\S]*?\?>/g, "");
const tagPattern =
/<\/?([A-Za-z_][\w:.-]*)(?:\s[^<>]*?)?\s*\/?>/g;
const stack = [];
let match;
while (
(match = tagPattern.exec(withoutCommentsAndDeclaration)) !== null
) {
const fullTag = match[0];
const tagName = match[1];
if (fullTag.startsWith("</")) {
const openTag = stack.pop();
if (openTag !== tagName) {
fail(
`closing tag </${tagName}> does not match `
+ `<${openTag || "none"}>`
);
}
} else if (!fullTag.endsWith("/>")) {
stack.push(tagName);
}
}
if (stack.length > 0) {
fail(`unclosed tag <${stack[stack.length - 1]}>`);
}
const remainingMarkup =
withoutCommentsAndDeclaration.replace(tagPattern, "");
// A plain ">" is legal character data and occurs in historical
// changelog notation such as "->". A remaining "<" cannot be legal
// after all valid tags have been removed.
if (/</.test(remainingMarkup)) {
fail("the XML contains malformed markup");
}
}
function assertContainsTag(block, tagName) {
const pattern =
new RegExp(`<${tagName}>[\\s\\S]*?<\\/${tagName}>`);
if (!pattern.test(block)) {
fail(`required element <${tagName}> is missing`);
}
}
function validateVersionInfo(xml, expectedStableVersion = "") {
if (!xml || Buffer.byteLength(xml, "utf8") < 500) {
fail("the generated file is empty or implausibly small");
}
assertWellFormedStructure(xml);
const completeDocumentPattern =
/^<\?xml[^>]*>\s*<praktiKST>[\s\S]*<\/praktiKST>\s*$/;
if (!completeDocumentPattern.test(xml)) {
fail(
"the document does not contain one complete "
+ "<praktiKST> root element"
);
}
const latestVersionBlock = extractTag(xml, "latestVersion");
if (latestVersionBlock === null) {
fail("<latestVersion> is missing");
}
for (const tagName of [
"versionNumber",
"semanticVersion",
"adminMessage",
"majorChanges",
"latestVersionPathOnWebserver"
]) {
assertContainsTag(latestVersionBlock, tagName);
}
const legacyVersion =
extractTag(latestVersionBlock, "versionNumber");
const semanticVersion =
extractTag(latestVersionBlock, "semanticVersion");
const releaseUrl =
extractTag(
latestVersionBlock,
"latestVersionPathOnWebserver"
);
if (
!legacyVersion
|| !/^\d+(?:\.\d+)?$/.test(legacyVersion)
) {
fail(
"<versionNumber> is not a valid legacy numeric version"
);
}
if (
!semanticVersion
|| !/^\d+\.\d+(?:\.\d+)?$/.test(semanticVersion)
) {
fail("<semanticVersion> is not a valid Stable version");
}
if (
!releaseUrl
|| !releaseUrl.startsWith(
"https://github.com/praktimarc/"
+ "kst4contest/releases/tag/"
)
) {
fail(
"<latestVersionPathOnWebserver> is not "
+ "a KST4Contest release URL"
);
}
const changeLogs = [
...xml.matchAll(
/<changeLog>([\s\S]*?)<\/changeLog>/g
)
].map((match) => match[1]);
if (changeLogs.length === 0) {
fail(
"the document does not contain any "
+ "<changeLog> entries"
);
}
for (const entry of changeLogs) {
for (const tagName of [
"changedVersionNumber",
"date",
"description",
"added",
"changed",
"fixed",
"removed"
]) {
assertContainsTag(entry, tagName);
}
}
const expected = normaliseVersion(expectedStableVersion);
if (expected) {
if (semanticVersion !== expected) {
fail(
`latest Stable version ${semanticVersion} `
+ `does not match expected release ${expected}`
);
}
const releaseEntryExists = changeLogs.some(
(entry) =>
extractTag(
entry,
"changedVersionNumber"
) === expected
);
if (!releaseEntryExists) {
fail(
"the changelog does not contain the expected "
+ `Stable release ${expected}`
);
}
}
return {
semanticVersion,
changeLogEntries: changeLogs.length
};
}
function parseArguments(argv) {
const result = {
file: DEFAULT_FILE,
expectedStableVersion:
process.env.EXPECTED_STABLE_VERSION || ""
};
for (let index = 0; index < argv.length; index++) {
if (
argv[index] === "--file"
&& argv[index + 1]
) {
result.file = path.resolve(argv[++index]);
} else if (
argv[index] === "--expected-stable"
&& argv[index + 1]
) {
result.expectedStableVersion = argv[++index];
} else {
fail(
`unknown or incomplete argument: ${argv[index]}`
);
}
}
return result;
}
if (require.main === module) {
try {
const options =
parseArguments(process.argv.slice(2));
const xml =
fs.readFileSync(options.file, "utf8");
const result =
validateVersionInfo(
xml,
options.expectedStableVersion
);
console.log(
`[versionInfo validation] OK: `
+ `Stable ${result.semanticVersion}, `
+ `${result.changeLogEntries} changelog entries, `
+ options.file
);
} catch (err) {
console.error(err.message);
process.exitCode = 1;
}
}
module.exports = {
normaliseVersion,
validateVersionInfo
};
+143 -20
View File
@@ -5,16 +5,93 @@ const REPO = "praktimarc/kst4contest";
const API = `https://api.github.com/repos/${REPO}`;
const LABEL_MAP = { enhancement: "added", bug: "fixed" };
const GITHUB_API_ATTEMPTS = 3;
function wait(milliseconds) {
return new Promise(
(resolve) => setTimeout(resolve, milliseconds)
);
}
async function githubGet(urlPath) {
const headers = { Accept: "application/vnd.github+json" };
const headers = {
Accept: "application/vnd.github+json"
};
if (process.env.GITHUB_TOKEN) {
headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
headers.Authorization =
`Bearer ${process.env.GITHUB_TOKEN}`;
}
const res = await fetch(`${API}${urlPath}`, { headers });
if (!res.ok) {
throw new Error(`GitHub API ${urlPath} failed: ${res.status}`);
let lastError = null;
for (
let attempt = 1;
attempt <= GITHUB_API_ATTEMPTS;
attempt++
) {
try {
const res = await fetch(
`${API}${urlPath}`,
{
headers,
signal: AbortSignal.timeout(15000)
}
);
if (res.ok) {
return res.json();
}
const rateLimitRemaining =
res.headers.get("x-ratelimit-remaining");
const rateLimitReset =
res.headers.get("x-ratelimit-reset");
const rateLimitInfo =
rateLimitRemaining === null
? ""
: `, rate limit remaining `
+ rateLimitRemaining
+ (
rateLimitReset
? `, reset ${rateLimitReset}`
: ""
);
lastError = new Error(
`GitHub API ${urlPath} failed with `
+ `HTTP ${res.status}${rateLimitInfo}`
);
// Authentication and permission errors do not
// become valid by retrying.
if (
![408, 429].includes(res.status)
&& res.status < 500
) {
throw lastError;
}
} catch (err) {
lastError = err;
// Do not hide an invalid or expired token behind
// repeated requests.
if (
/HTTP (401|403|404)/.test(err.message)
) {
throw err;
}
}
if (attempt < GITHUB_API_ATTEMPTS) {
await wait(attempt * 500);
}
}
return res.json();
throw lastError
|| new Error(`GitHub API ${urlPath} failed`);
}
// UpdateChecker.java parses <versionNumber> with Double.parseDouble() and
@@ -108,25 +185,64 @@ function loadLegacySections() {
// UpdateChecker.java) from GitHub releases + closed issues, falling back to
// version-history.xml for releases that predate GitHub Releases.
module.exports = async function () {
const fallback = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<praktiKST></praktiKST>';
try {
const historyEntries = loadHistoryEntries();
const rawReleases = await githubGet("/releases?per_page=100");
const rawReleases =
await githubGet("/releases?per_page=100");
if (!Array.isArray(rawReleases)) {
throw new Error(
"GitHub releases response was not an array"
);
}
const releases = rawReleases
.map((r) => ({
tagName: r.tag_name,
publishedAt: r.published_at || "",
name: r.name || r.tag_name,
body: r.body || "",
isPrerelease: r.prerelease
.map((release) => ({
tagName: release.tag_name,
publishedAt: release.published_at || "",
name:
release.name
|| release.tag_name,
body: release.body || "",
isPrerelease: release.prerelease,
isDraft: release.draft
}))
.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1));
.sort(
(first, second) =>
first.publishedAt
< second.publishedAt
? 1
: -1
);
const stableReleases = releases.filter(
(release) =>
!release.isPrerelease
&& !release.isDraft
);
const stableReleases = releases.filter((r) => !r.isPrerelease);
const stable = stableReleases[0];
const ghVersions = new Set(stableReleases.map((r) => toAppVersionNumber(r.tagName)));
if (
!stable
|| !stable.tagName
|| !stable.publishedAt
) {
throw new Error(
"GitHub did not return "
+ "a published Stable release"
);
}
const ghVersions = new Set(
stableReleases.map(
(release) =>
toAppVersionNumber(
release.tagName
)
)
);
const parts = [];
parts.push('<?xml version="1.0" encoding="UTF-8" standalone="no"?>');
@@ -203,7 +319,14 @@ module.exports = async function () {
parts.push("</praktiKST>");
return parts.join("\n");
} catch (err) {
console.warn(`[versionInfo] Could not generate version info XML, using empty fallback: ${err.message}`);
return fallback;
throw new Error(
"[versionInfo] Could not generate "
+ "a complete update feed. "
+ "The website build has been aborted "
+ "so that the existing production feed "
+ "remains untouched: "
+ err.message,
{ cause: err }
);
}
};
+3 -2
View File
@@ -1,10 +1,11 @@
<!doctype html>
<html lang="en">
<html lang="{{ lang or 'en' }}">
<head>
<meta charset="utf-8">
<title>{{ title or "KST4Contest" }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
<meta name="description" content="{{ description or 'KST4Contest connects ON4KST chat, candidate selection, sked planning and station software for VHF, UHF and SHF contest operation.' }}">
<link rel="canonical" href="https://kst4contest.hamradioonline.de{{ page.url }}">
@@ -13,7 +14,7 @@
<meta property="og:description" content="{{ description or 'ON4KST chat, candidate selection, sked planning and station software in one VHF, UHF and SHF contest workflow.' }}">
<meta property="og:url" content="https://kst4contest.hamradioonline.de{{ page.url }}">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="en_GB">
<meta property="og:locale" content="{{ 'de_DE' if lang == 'de' else 'en_GB' }}">
<meta property="og:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
+37 -19
View File
@@ -1,40 +1,44 @@
---
layout: base.njk
lang: en
---
<section class="hero">
<p class="badge">{{ category }} · since {{ since }}</p>
<p class="eyebrow">{{ category }} · available since {{ since }}</p>
<h1>{{ title }}</h1>
<p class="lead">{{ summary }}</p>
<div class="actions">
<a class="button secondary" href="/features/">← All functions</a>
<a class="button ghost" href="/manual/en/">Open the manual</a>
</div>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">{{ icon }}</div>
<div class="feature-icon large" aria-hidden="true">{{ icon }}</div>
{{ content | safe }}
{% if tagsList %}
<h2>Keywords</h2>
<div class="tag-list">
{% for tag in tagsList %}
<span>{{ tag }}</span>
{% endfor %}
</div>
{% endif %}
{% if related %}
<h2>Related features</h2>
<h2>Related functions</h2>
<div class="actions">
{% for slug in related %}
{% for item in collections.sortedFeatures %}
{% if item.fileSlug == slug %}
<a class="button secondary" href="{{ item.url }}">{{ item.data.title }}</a>
<a class="button secondary" href="{{ item.url }}">
{{ item.data.title }}
</a>
{% endif %}
{% endfor %}
{% endfor %}
</div>
{% endif %}
<p>
<a href="/features/">← Back to all functions</a>
</p>
</article>
</section>
@@ -44,15 +48,29 @@ layout: base.njk
"@type": "TechArticle",
"headline": "{{ title }}",
"description": "{{ summary }}",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de{{ page.url }}",
"author": {
"@type": "Person",
"name": "Praktimarc"
{% if tagsList %}
"keywords": "{{ tagsList | join(', ') }}",
{% endif %}
"about": {
"@type": "SoftwareApplication",
"name": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/"
},
"url": "https://kst4contest.hamradioonline.de{{ page.url }}",
"author": [
{
"@type": "Person",
"name": "Marc Fröhlich (DO5AMF)"
},
{
"@type": "Person",
"name": "Philipp (DN9APW)"
}
],
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
"name": "KST4Contest Project",
"url": "https://kst4contest.hamradioonline.de/"
}
}
</script>
+89
View File
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 48 48"
width="48"
height="48"
role="img"
version="1.1"
id="svg5"
sodipodi:docname="_raw_06-favicon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs5" />
<sodipodi:namedview
id="namedview5"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<title
id="title1">KST4Contest Favicon — Dark</title>
<rect
x="2"
y="2"
width="44"
height="44"
rx="10"
fill="#5FD63A"
id="rect1" />
<g
transform="translate(12.3 9.4) scale(0.325)"
id="g5">
<g
fill="none"
stroke="#0F150F"
stroke-width="4.2"
stroke-linecap="round"
id="g4">
<path
d="M20 2 C12.5 7.8 12.5 17.2 20 23"
id="path1" />
<path
d="M52 2 C59.5 7.8 59.5 17.2 52 23"
id="path2" />
<path
d="M27 6.5 C22.8 10 22.8 15 27 18.5"
id="path3" />
<path
d="M45 6.5 C49.2 10 49.2 15 45 18.5"
id="path4" />
</g>
<circle
cx="36"
cy="10.5"
r="5.2"
fill="#0F150F"
id="circle4" />
<rect
x="33.6"
y="14"
width="4.8"
height="11"
rx="2.4"
fill="#0F150F"
id="rect4" />
<rect
x="0"
y="23"
width="72"
height="35"
rx="10.5"
fill="#0F150F"
id="rect5" />
<path
d="M22 54 L22 64 L36 56 Z"
fill="#0F150F"
id="path5" />
<path
d="m 18.493652,47.099998 h 2.755127 v -3.848144 l 1.788575,-1.969238 3.821044,5.817382 h 3.260987 l -5.176026,-7.633056 5.031495,-5.826416 h -3.369385 l -3.297119,3.866211 c -0.894288,1.083984 -1.517579,1.933105 -2.104737,2.872558 l 0.04517,-3.062256 v -3.676513 h -2.755127 z m 18.057373,0.198731 c 3.27002,0 5.194092,-1.580811 5.194092,-4.028809 0,-2.2583 -1.752441,-3.41455 -4.019775,-3.938476 l -1.219483,-0.298096 c -1.138183,-0.270996 -2.149902,-0.695557 -2.149902,-1.698242 0,-0.90332 0.794922,-1.553711 2.204102,-1.553711 1.38208,0 2.240234,0.623291 2.339599,1.698242 h 2.664795 c -0.04517,-2.366699 -1.996338,-4.019775 -4.977295,-4.019775 -2.926758,0 -5.058594,1.625976 -5.058594,4.055908 0,1.960205 1.364014,3.107422 3.640381,3.658447 l 1.481446,0.370362 c 1.463379,0.361328 2.321533,0.785888 2.321533,1.707275 0,1.020752 -0.975586,1.716309 -2.447998,1.716309 -1.490479,0 -2.583496,-0.677491 -2.673828,-2.041504 h -2.682862 c 0.0813,2.863525 2.149903,4.37207 5.383789,4.37207 z m 6.503907,-11.372803 h 4.11914 v 11.174072 h 2.76416 V 35.925926 h 4.110108 v -2.2854 H 43.054932 Z"
id="text5"
style="font-weight:700;font-size:18.5px;font-family:Inter, Arial, sans-serif;text-anchor:middle;fill:#5fd63a"
aria-label="KST" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+156 -8
View File
@@ -3,25 +3,173 @@ title: AirScout Integration
icon: ✈️
category: Aircraft Scatter
since: "1.26"
summary: Use AirScout AP predictions in candidate evaluation, the timeline and sked planning.
description: KST4Contest receives aircraft scatter information from AirScout and relates AP timing to active ON4KST stations.
summary: Request station-specific aircraft-scatter information from AirScout and include the returned AP timing in the user list, timeline and candidate priority.
description: KST4Contest sends active station paths to AirScout, receives matching aircraft information and relates the result to the current ON4KST operating context.
tagsList:
- AirScout
- airplane scatter
- aircraft scatter
- VHF contest
- AP windows
- VHF
- UHF
- SHF
- contest
related:
- timeline
- priority-score
- sked-reminder
---
## Airplane scatter in the operator workflow
## Why connect AirScout to the chat client?
Aircraft reflections can create short but valuable contact opportunities.
An aircraft-scatter opportunity is useful for a limited time. AirScout can calculate the path and identify suitable aircraft, but it does not know which of the many stations in the ON4KST chat currently matters to the operator.
KST4Contest helps combine ON4KST chat activity with AirScout-based propagation awareness.
KST4Contest provides this missing context. It sends relevant active stations to AirScout and assigns the returned aircraft information to the corresponding chat members.
## From candidate to sked decision
The responsibilities remain separate:
AirScout information can support the decision whether a candidate station should be called immediately, scheduled later or monitored for a better window.
- AirScout obtains the aircraft data and evaluates the reflection geometry.
- KST4Contest selects the station paths to request.
- KST4Contest relates the returned timing to messages, candidate priorities and skeds.
KST4Contest does not download ADS-B data and does not calculate the aircraft geometry itself.
## Which stations are sent to AirScout?
KST4Contest updates the AirScout requests every 60 seconds. A station is included only if:
- AirScout communication is enabled;
- the station has a usable callsign and locator;
- its distance is known;
- its QRB is below the configured maximum QRB; and
- a usable propagation band can be determined.
Active chat variants of the same base callsign are combined for the frequency decision. The path itself is requested once instead of sending duplicate calculations for every visible suffix or chat-category entry.
KST4Contest also updates the AirScout watchlist. Stations which are no longer active or no longer meet the conditions are therefore not intended to remain permanent watchlist targets.
## How is the AirScout band selected?
> Automatic station-specific band selection is included in Nightly / v1.42. A fixed configured AirScout band remains available as a manual fallback.
In **Auto per station** mode, KST4Contest uses the same propagation-frequency resolver as the internal path analysis. The sources are evaluated in the following order:
1. the most recently detected QRG of the station;
2. a band explicitly stated in the name of one of its active chat entries;
3. 432 MHz when the station is present in both the VHF/UHF and microwave categories and 432 MHz is enabled locally;
4. the lowest locally enabled fallback band belonging to the supported chat category.
Only bands enabled for the local station are eligible. A manual NOT-QRV mark removes the corresponding band before the selection is made.
Recent detected frequencies are preferred because the path should normally be evaluated for the band on which the contact is actually intended. A category alone is weaker evidence: it describes a range of possible activity, not necessarily the current operating band of one station.
Unsupported ON4KST categories are ignored. They must not silently turn into a 144 MHz request merely because no better information is available.
If no usable result remains, KST4Contest omits the AirScout request for that station.
## What does AirScout return?
AirScout replies with the aircraft currently considered relevant for the requested path. For each aircraft, KST4Contest receives information including:
- the aircraft identifier;
- the AirScout size category;
- the distance to the expected reflection point;
- the potential reported by AirScout; and
- the remaining time until the expected arrival.
The aircraft are ordered by their reported potential and, where this is equal, by arrival time.
The result is assigned to every active chat variant of the corresponding base callsign. A station logged in as `CALLSIGN`, `CALLSIGN-2` or `CALLSIGN-432` therefore receives the same path information without merging the individual message targets.
![AirScout information in the station overview](/manual/assets/priority_score_overview.png)
## How does the result affect KST4Contest?
AirScout information is used in several places:
- The **AP** column shows the next available aircraft for a station.
- The **Further Info** section provides the aircraft information for the selected station.
- AP candidates can appear on the 30-minute timeline.
- A station with available aircraft receives an additional Priority Score contribution.
- An aircraft expected within the next few minutes raises the score further.
- The `FIRSTAP` and `SECONDAP` variables can insert the information into a message.
The percentage reported by AirScout is not interpreted as a contact probability. In particular, a displayed value of 100% does not mean that a QSO will succeed.
![AirScout candidates and skeds on the timeline](/manual/assets/sked_timeline.png)
## Priority and timing are separate questions
The Priority Score currently considers whether AirScout has reported usable aircraft and how soon the earliest aircraft is expected. The detailed potential value is used for the AP display and the timeline colour, but it is not converted directly into a percentage-based score.
This distinction is intentional. The AirScout value describes the aircraft geometry known to AirScout. The complete contest decision also depends on the band, antenna direction, activity of the remote station, Worked state and any existing sked.
In plain terms: an aircraft may make the path interesting. It does not make the remote station ready.
## Show the selected path in AirScout
The direction button of the selected station can send an `ASSHOWPATH` request to AirScout. AirScout then opens or updates the corresponding path display.
The request uses:
- the configured AirScout server identifier;
- the configured KST4Contest client identifier;
- the selected propagation frequency;
- the local callsign and locator; and
- the complete callsign and locator of the selected station.
The local ON4KST suffix is removed before the path request because AirScout expects the actual station callsign rather than a chat-specific login suffix.
If the selected station has no usable locator or no propagation frequency can be determined, the request is omitted. Opening an impressive empty AirScout window would not add much information.
## Several clients in the same network
The default identifiers are:
| Setting | Default |
|---|---|
| AirScout server identifier | `AS` |
| KST4Contest client identifier | `KST` |
| UDP port | `9872` |
The server and client identifiers are configurable. This matters when several KST4Contest or AirScout instances are operated in the same station network.
Each KST4Contest instance should use a distinct client identifier. Incoming AirScout replies are accepted only when the server and client identifiers exactly match the configured values. The comparison is case-sensitive.
This keeps a reply for one operating position from being assigned to another client merely because both listen on the same UDP network.
> Strict reply filtering by the configured client/server pair is included in Nightly / v1.42.
## AP variables in messages
The selected station can be referenced through two message variables:
| Variable | Result |
|---|---|
| `FIRSTAP` | Description and arrival time of the first aircraft |
| `SECONDAP` | Description and arrival time of the second aircraft |
If no aircraft is available, `FIRSTAP` returns `no ap available`. If no second aircraft exists, `SECONDAP` is replaced with an empty string.
The absence of a second aircraft is a normal result and does not prevent editing or sending the message.
## What the integration cannot guarantee
The result depends on several external and derived inputs:
- the aircraft data available to AirScout;
- the AirScout configuration and calculation;
- the locator assigned to the remote station;
- the band or frequency selected by KST4Contest;
- the age of detected QRG information; and
- the actual operating situation at the remote station.
A missing aircraft in KST4Contest does not prove that the path is unusable. It may also mean that AirScout returned no matching result, the station was outside the configured QRB range, the locator was missing or no suitable band could be derived.
Conversely, a reported aircraft does not guarantee sufficient signal strength or a completed contact.
[Read the complete AirScout setup in the manual.](/manual/en/airscout-integration/)
[Read how the AP information affects the timeline.](/features/timeline/)
[Open the AirScout project on GitHub.](https://github.com/dl2alf/AirScout)
+154 -7
View File
@@ -3,23 +3,170 @@ title: Dual Chat Categories
icon: 💬
category: ON4KST Chat
since: "1.26"
summary: Log in to two ON4KST categories and handle both message streams in one station list and interface.
description: KST4Contest combines two ON4KST chat categories while retaining the category required for messages and station context.
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.
tagsList:
- ON4KST
- dual chat
- multi-channel login
- callsign suffix
- VHF
- UHF
- SHF
- microwave contest
related:
- priority-score
- log-sync
- airscout
---
## Two categories, one workflow
## Why use two chat categories?
KST4Contest can operate with two ON4KST chat categories at once.
VHF, UHF and microwave activity is not always concentrated in one ON4KST category. An operator may need to follow one category for the lower bands and another for microwave operation.
This is useful when contest activity spans VHF/UHF and microwave operation.
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.
## Less window switching
KST4Contest therefore opens up to two ON4KST chat sessions and processes both in one operating context.
The operator can keep more information visible without constantly changing tools.
![Primary and secondary chat settings](/manual/assets/client_settings_window_station.png)
## Two connections remain two connections
The primary and secondary chat sessions retain their own:
- ON4KST chat category;
- login callsign;
- public message stream;
- destination category for outgoing messages; and
- active login 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.
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.
## What identifies an active chat member?
An active login is identified by:
1. the complete visible callsign, including its suffix; and
2. the ON4KST chat category.
Both parts are required.
Consider the following active entries:
| Complete callsign | 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.
This is particularly important for `9A0BB-2` and `9A0BB-70`: because both use the same category, the category alone cannot distinguish them.
> Correct separation of several suffix variants within the same category is included in Nightly / v1.42 and fixes [Issue #73](https://github.com/praktimarc/kst4contest/issues/73).
## 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.
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.
The same distinction is used for:
- incoming private messages;
- outgoing message echoes;
- public messages addressed to the local station;
- station selection from the user list;
- 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.
## 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.
For the examples above, the common base callsign is `9A0BB`.
The shared station context includes:
- global Worked status;
- Worked status per band;
- manually assigned NOT-QRV marks;
- known band opportunities derived from the active variants;
- the Priority Score; and
- stored contest state in the internal database.
A QSO with `9A0BB-70` therefore also marks the corresponding base callsign as worked for the other active suffix variants. It would be misleading to present `9A0BB-2` as a completely new station immediately afterwards.
Per-band information is still retained. Working the station on 70 MHz does not mark it as worked on 23 cm. The common base callsign links the variants; the band remains a separate dimension.
## Why are band hints combined?
A station may use its visible login names to describe the available bands:
- `9A0BB-2`
- `9A0BB-70`
- `9A0BB-23`
- `9A0BB-13`
KST4Contest can evaluate these active variants together when deriving possible bands. Recent QRG information and band designators in the name fields can contribute as well.
This allows the user list, the `a` and `B+` indicators, the **New bands** filter and the automatic propagation-frequency selection to use information which may be distributed across the two chat categories.
A manual NOT-QRV mark still takes precedence. An automatically detected suffix or name is evidence of possible activity, not permission to ignore an explicit correction.
## 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.
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.
## Categories are context, not proof of a band
A chat category describes the general operating area of that chat. It does not prove that every station in the category is currently QRV on every associated band.
KST4Contest therefore uses stronger station-specific information where available:
1. a recently detected QRG;
2. an explicit band designator in the station name or suffix;
3. the locally enabled bands;
4. stored Worked and NOT-QRV information; and
5. only then a supported category fallback where the function requires one.
Categories unrelated to the supported VHF, UHF, microwave or EME workflows are ignored by propagation functions which cannot derive a meaningful result from them. They must not cause a fallback to an arbitrary band.
## What remains separate?
Sharing the station context does not merge everything associated with the base callsign.
The following remain tied to the individual login or message:
- complete message destination;
- chat category;
- public message stream;
- visible callsign suffix;
- join and leave state;
- the specific row selected by the operator; and
- message history associated with that login 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.
## 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`.
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.
[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/)
+218 -11
View File
@@ -1,24 +1,231 @@
---
title: DXCluster Server
title: Local DX Cluster Server
icon: 📡
category: Radio Workflow
category: Logger Integration
since: "1.23"
summary: Send direction warnings as DX Cluster spots to compatible logging software when a usable QRG is known.
description: The built-in DX Cluster server forwards KST4Contest direction warnings and known frequency information to compatible contest loggers.
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.
tagsList:
- DXCluster
- ham radio
- contest
- DX Cluster
- bandmap
- ON4KST
- QRG detection
- UCXLog
- N1MM+
- contest logger
related:
- log-sync
- priority-score
- dual-chat
---
## What kind of DX Cluster is this?
## More situational awareness
KST4Contest does not connect to a public DX Cluster and does not import external spots.
DXCluster information can support contest operators by adding another source of activity information.
Instead, it provides a local TCP server to which a compatible contest logger can connect as a DX-Cluster client. KST4Contest uses that connection to forward selected information already detected in the ON4KST chat.
## Integrated workflow
The purpose is practical: when a station appears to be pointing in the local direction and its QRG is known, the information can appear directly in the logger bandmap. The operator no longer has to read the frequency in the chat, remember it and enter it again in the logger.
The goal is not to open another isolated tool, but to connect activity information with the wider KST4Contest workflow.
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?
A real 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.
3. The sender is within the configured maximum QRB.
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.
## 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.
It then compares:
- the direction from A to B; and
- the direction from A to the local station.
The configured antenna beamwidth is treated as the complete angle. Half of the value is applied to either side of the direction from A to B.
For a configured beamwidth of `70°`, the assumed corridor is therefore `±35°`.
If the local station lies inside this corridor, KST4Contest marks the sender as a directional opportunity. When a QRG is also available, the local DX Cluster spot can be generated immediately.
This is a geometric approximation. ON4KST does not transmit the actual antenna direction or beamwidth of the remote station, so KST4Contest uses the locally configured beamwidth as a practical substitute.
Terrain, propagation and the actual operating intention of the sender are not part of this calculation.
## Which frequency is used?
KST4Contest uses the same QRG detection that supplies the frequency column and other band-related functions.
Complete frequencies determine their band directly:
```text
50.200
70.250
144.205
432.088
1296.338
10368.100
24048.100
```
The value is converted into the kHz representation expected by the DX Cluster protocol:
| Chat frequency | DX Cluster frequency |
|---|---:|
| `50.200` | `50200.0` |
| `144.205` | `144205.0` |
| `1296.338` | `1296338.0` |
| `10368.100` | `10368100.0` |
| `24048.100` | `24048100.0` |
An additional sub-kHz group is retained. For example, `144.205.2` becomes `144205.2`.
## Relative QRG information
A relative frequency contains only the part inside a band:
```text
.205
,205
qrg 205
freq is 205
on 205
```
KST4Contest determines the missing band in this order:
1. a recent band context detected for the same station during the previous 30 minutes;
2. the most recently updated plausible context if several bands are known;
3. the configured **Fallback band for relative QRG detection**.
Example:
```text
Configured fallback: 144 MHz
Recent complete QRG of the station: 432.088 MHz
New message from the same station: .100
Result: 432.100 MHz
DX Cluster value: 432100.0 kHz
```
Without the recent station-specific context, the same `.100` would use the configured fallback and become `144.100 MHz`.
A bare three-digit value is accepted only when the surrounding text identifies it as a frequency. This prevents values such as `599`, a bare band name or the number of completed QSOs from becoming plausible-looking spots.
## What does the spot contain?
The 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 current UTC time.
An example comment with AirScout information may look like this:
```text
JN49GL , AP: 1min, 100%; 4min, 75%
```
AirScout information is optional. A missing AirScout response does not prevent the spot from being sent.
> AP-independent spot creation, corrected sender-locator handling and band-generic frequency conversion are included in Nightly / v1.42.
## Connecting a logger
Open the **Notification** tab in the KST4Contest settings.
![Notification and local DX Cluster settings](/manual/assets/client_settings_window_notification.png)
Configure:
1. **Enable the local DX Cluster server …**
2. A free TCP port. The default is `8000`.
3. The fallback band used for relative QRG information.
4. A spotter callsign.
The logger is then configured as the DX-Cluster client:
| Setting | Same computer | Separate logger computer |
|---|---|---|
| Host | `127.0.0.1` | IP address of the KST4Contest computer |
| Port | Configured KST4Contest TCP port | Configured KST4Contest TCP port |
| Login | Any callsign if required | Any callsign if required |
| Password | Not required | Not required |
The spotter callsign should preferably differ from the contest callsign. Some loggers suppress or specially handle spots which appear to originate from the local station. A spot can therefore be generated correctly and still remain invisible in the bandmap.
## Several connected clients
The local server can retain several DX-Cluster client connections. A generated spot is sent to every client currently connected.
KST4Contest sends an empty keep-alive line every 30 seconds so idle connections are less likely to be closed unnoticed. Clients which disconnect while a spot is being sent are removed from the active connection list.
Changing the configured port while connected restarts the local server. Existing logger connections must then reconnect to the new port.
## Network boundary
The server does not authenticate the login supplied by a DX-Cluster client. It is intended for the local computer or a trusted station network.
When the logger runs on another computer:
- use the IP address of the KST4Contest computer;
- allow the configured TCP port through the local firewall; and
- do not expose the server directly to the internet.
A missing password is acceptable inside the intended station network. It is not a security concept for a public service.
## Testing the connection
Use **Send test spot** after the logger has connected.
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?
- Was the sender within the maximum QRB?
- Was the direction inside the configured beamwidth?
- Was a valid frequency known?
- Did a station-specific band context change the relative QRG?
## 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.
It does not prove:
- the actual antenna direction of the station;
- that the station intends to work the local operator;
- that the path is currently open;
- that the frequency is still unchanged; or
- that a QSO will succeed.
The logger may tune the transceiver to the spot frequency, depending on its own configuration. The operator still decides whether calling the station makes sense.
In plain terms: KST4Contest can remove the typing. It cannot remove the judgement.
## Tested loggers
The local DX Cluster interface has been used with:
- UCXLog
- N1MM+
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 relative QRG information is configured.](/manual/en/configuration/#fallback-band-for-relative-qrg-detection)
+100 -9
View File
@@ -1,28 +1,119 @@
---
layout: base.njk
title: KST4Contest Features
description: Contest-optimized ON4KST features for VHF, UHF and SHF operation.
lang: en
title: KST4Contest Functions
description: Technical overview of the KST4Contest functions for ON4KST chat processing, candidate evaluation, sked planning, AirScout and logger integration.
---
<section class="hero">
<p class="badge">Features</p>
<h1>Contest tools, not just chat windows.</h1>
<p class="eyebrow">Functions</p>
<h1>How KST4Contest supports the contest workflow</h1>
<p class="lead">
KST4Contest combines ON4KST chat, candidate scoring, AirScout workflow,
sked handling, log synchronization and operator decision support.
KST4Contest combines ON4KST messages with station, band, Worked, sked,
AirScout and logger information. The individual functions use this shared
context instead of treating every message or interface as an isolated
event.
</p>
<p>
Some information is received directly from another program. Other
information must be derived from chat messages, station names or the
current operating state. The distinction matters: a detected band or a
calculated priority is useful context, but not automatically a confirmed
fact.
</p>
<div class="actions">
<a class="button" href="/manual/en/features/">Read the technical description</a>
<a class="button secondary" href="/manual/en/configuration/">Open configuration</a>
<a class="button ghost" href="/download/">Download KST4Contest</a>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Shared context</p>
<h2>Why the functions affect each other</h2>
<p>
A newly detected frequency can change the known bands of a station. This
can affect its Worked state, filters, priority, map path and a later sked
handover. The same principle applies to information received from a
logger or entered manually.
</p>
</div>
<div class="grid">
{% for feature in collections.features %}
<article class="card">
<h3>Received information</h3>
<p>
ON4KST messages, logger packets, Win-Test network data and AirScout
responses provide information at different levels of detail. Missing
fields are not automatically equivalent to negative information.
</p>
</article>
<article class="card">
<h3>Derived information</h3>
<p>
Bands, frequencies, activity and candidate priorities may be derived
from several sources. Explicit NOT-QRV information overrules a merely
inferred band opportunity.
</p>
</article>
<article class="card">
<h3>Operator decision</h3>
<p>
Scores, filters, AP windows and path assessments reduce the amount of
information that must be evaluated manually. They do not guarantee a
contact or replace checking the actual operating situation.
</p>
</article>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Function overview</p>
<h2>Functions in their intended order</h2>
<p>
The pages below explain the operating problem behind each function, the
information it uses and the limitations that remain.
</p>
</div>
<div class="grid">
{% for feature in collections.sortedFeatures %}
<article class="card feature-card">
<div class="feature-icon">{{ feature.data.icon }}</div>
<p class="eyebrow">{{ feature.data.category }}</p>
<h3><a href="{{ feature.url }}">{{ feature.data.title }}</a></h3>
<h3>
<a href="{{ feature.url }}">{{ feature.data.title }}</a>
</h3>
<p>{{ feature.data.summary }}</p>
<a href="{{ feature.url }}">Learn more →</a>
<a href="{{ feature.url }}">Read how it works →</a>
</article>
{% endfor %}
</div>
</section>
<section class="section">
<div class="cta-panel">
<p class="eyebrow">Technical details</p>
<h2>The manual remains the authoritative description</h2>
<p>
These pages provide a practical overview. Configuration parameters,
interface requirements, exact behaviour and version-specific limitations
are documented in the manual. Where the short description and the manual
differ, the manual should be corrected first. Two competing descriptions
of the same function would not improve matters.
</p>
<div class="actions">
<a class="button" href="/manual/en/">Open the English manual</a>
<a class="button secondary" href="/manual/de/">Deutsches Handbuch</a>
</div>
</div>
</section>
+42 -8
View File
@@ -1,25 +1,59 @@
---
title: Log Synchronization
title: Log Synchronisation
icon: 🔄
category: Logger Integration
since: "1.31"
summary: Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.
description: KST4Contest receives worked-station and, where supported, frequency data from UCXLog, N1MM+, QARTest, DXLog.net and Win-Test through file and UDP interfaces.
summary: Import callsign, band, locator and QRG information from supported contest loggers at the level provided by each interface.
description: KST4Contest connects chat activity with current log state through file-based evaluation, general QSO UDP packets and the native Win-Test network protocol.
tagsList:
- Win-Test
- UCXLog
- N1MM+
- QARTest
- DXLog.net
- contest logger
related:
- priority-score
- dual-chat
- sked-reminder
---
## Connect chat and logging
## Why connect the logger?
Contest operation is faster when chat information and log state are connected.
The chat may still show a station as an interesting candidate after the QSO has already been logged. Without synchronisation, Worked filters, band columns and priority calculations would continue to use an outdated contest state.
KST4Contest can use logger information to improve workflow awareness.
KST4Contest therefore imports the information provided by the logging application and applies it to the active chat entries of the corresponding base callsign.
## Worked and band awareness
## Three interfaces with different levels of detail
Log synchronization helps the client understand which stations and bands are relevant.
The available information depends on the interface:
| Interface | Callsign | Band | Locator |
|---|---:|---:|---:|
| Simplelogfile interpreter | yes | no | no |
| 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 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.
![Log synchronisation settings](/manual/assets/client_settings_window_logsync.png)
## Native Win-Test integration
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.
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.
> The band-aware sked handover and explicit `SSB`/`CW` selection are included in Nightly / v1.42.
## 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.
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/)
+209 -7
View File
@@ -3,21 +3,223 @@ title: Macros and Variables
icon: ⚡
category: Operator Speed
since: "1.0"
summary: Build recurring chat messages from configurable text blocks and variables such as the current QRG or locator.
description: KST4Contest macros combine reusable message text with values taken from the current station, frequency and operating context.
summary: Insert recurring text through shortcut buttons or snippets and add current QRG, locator, heading, station-name and AirScout information when the message is used.
description: KST4Contest provides configurable shortcut buttons, station-addressed snippets and message variables for information which would otherwise have to be typed repeatedly.
tagsList:
- macros
- message variables
- text snippets
- shortcuts
- ON4KST
- operator workflow
related:
- sked-reminder
- airscout
- dual-chat
- sked-reminder
---
## Faster messages
## Three different mechanisms
Macros reduce repetitive typing during active contest operation.
KST4Contest uses three related but distinct mechanisms:
## More consistent operation
| Mechanism | Purpose |
|---|---|
| Shortcut | Inserts a configured text through a button in the main window |
| Snippet | Prepares or extends a message for the selected station |
| Variable | Replaces a reserved placeholder with current operating information |
Variables help keep messages structured and reduce manual errors.
Shortcuts and snippets store text. Variables add values which may change while the program is running.
This distinction matters. A shortcut such as `pse sked?` always inserts the same text. A shortcut containing `MYQRG` inserts the QRG which is current when the shortcut is used.
## Shortcut buttons
Each entry configured under **Shortcut Settings** creates a button in the main window.
Pressing the button appends its text to the send field. Variables contained in the shortcut are resolved against the current station and operating context.
Typical shortcuts are:
```text
pse sked?
rrr
tnx
pse call me at MYQRGSHORT
/SETNAME MYQRG
```
Shortcuts are useful for short expressions which are required frequently and are not necessarily addressed to a particular station.
`MYQRG` and `SECONDQRG` are also recognised as frequency buttons and insert the corresponding current QRG.
## Text snippets
Snippets are longer text blocks intended primarily for communication with a selected station.
They are available through:
- the context menu of the user list;
- the context menus of the public and private message tables; and
- `Ctrl+1` through `Ctrl+0` for the first ten configured snippets.
When a station is selected, a keyboard snippet prepares a directed message using the complete visible callsign:
```text
/cq CALLSIGN snippet text
```
A suffixed callsign such as `9A0BB-70` remains `9A0BB-70`. Its chat category is retained for the later transmission, so the snippet is not accidentally sent through the other active category.
The prepared message is not sent automatically. It remains in the send field and can be checked or edited before pressing `Enter` or **TX**.
## When are variables resolved?
Variables inserted through a shortcut or snippet are resolved when that text is inserted.
Variables typed or pasted directly into the send field are resolved immediately before the message is placed in the transmission queue. This provides a second controlled resolution point and ensures that manually entered variables work as well.
Station-specific variables always refer to the currently selected station. They are not derived from a callsign which was typed manually somewhere inside the message.
Variable names are case-sensitive and must be written in uppercase.
## Global variables
Global variables depend only on the local station and its current configuration.
| Variable | Replacement |
|---|---|
| `MYQRG` | Current QRG of the primary chat category |
| `MYQRGSHORT` | First seven characters of the primary QRG |
| `SECONDQRG` | Current QRG of the second chat category |
| `MYLOCATOR` | Complete configured locator of the local station |
| `MYLOCATORSHORT` | First four characters of the local locator |
| `MYCALL` | Configured local login callsign |
| `MYQTF` | Current antenna heading in degrees |
Example:
```text
cq at MYQRGSHORT, qtf MYQTF, loc MYLOCATOR
```
may become:
```text
cq at 144.388, qtf 135, loc JO51IJ
```
`MYQRG` always refers to the primary category and `SECONDQRG` always refers to the second category. Their meaning does not change merely because a station from the other chat category is selected.
The QRG values may originate from logger synchronisation or from the corresponding manually editable QRG fields.
`MYQTF` is a numeric heading. It is not converted into words such as `north` or `south-west`.
## Variables requiring a selected station
The following variables use information belonging to the selected remote station:
| Variable | Replacement |
|---|---|
| `QRZNAME` | Name shown for the selected station, or its complete callsign if no name is available |
| `FIRSTAP` | Description and arrival time of the first AirScout aircraft |
| `SECONDAP` | Description and arrival time of the second AirScout aircraft |
Example:
```text
Hi QRZNAME, FIRSTAP, pse lsn at MYQRGSHORT
```
may become:
```text
Hi David, a very big AP in 2 min, pse lsn at 144.388
```
If AirScout reports no aircraft for the selected station, `FIRSTAP` becomes:
```text
no ap available
```
If no second aircraft is available, `SECONDAP` becomes an empty string.
When no station is selected, `QRZNAME`, `FIRSTAP` and `SECONDAP` remain visible in the text. They are deliberately not removed, because an unresolved placeholder is easier to notice than a plausible-looking but incomplete message.
Select the intended station before using these variables and check that no unresolved placeholder remains before sending.
## Variables in automatic beacons
A public beacon has no selected remote station. It can therefore use only global variables:
- `MYQRG`
- `MYQRGSHORT`
- `SECONDQRG`
- `MYLOCATOR`
- `MYLOCATORSHORT`
- `MYCALL`
- `MYQTF`
`QRZNAME`, `FIRSTAP` and `SECONDAP` must not be used in a beacon.
![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.
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.
The completely resolved beacon text must not exceed 120 characters. The configured minimum interval is one minute.
A suitable template is:
```text
cq at MYQRGSHORT, qtf MYQTF, loc MYLOCATOR
```
For the second category, use `SECONDQRG` if that category operates on a different QRG.
## Practical workflow
A typical sequence is:
1. Select the intended station.
2. Press `Ctrl+1` to prepare a directed snippet.
3. Check the complete callsign and the resolved values.
4. Adjust the message if the station proposed another QRG.
5. Press `Enter` or **TX**.
For example, the configured snippet:
```text
Hi QRZNAME, pse sked? I call at MYQRGSHORT
```
can produce:
```text
/cq DL1ABC-432 Hi Peter, pse sked? I call at 432.088
```
The complete callsign identifies the message destination. The current QRG and station name reduce repetitive typing but do not decide whether the information is still operationally correct.
## Operational limitations
Variables reproduce the information currently known to KST4Contest. They do not verify that it is still correct.
In particular:
- a logger-supplied QRG may already have changed;
- a manually entered QRG remains in use until it is changed again;
- the selected station may differ from a manually typed `/cq` target;
- AirScout may have no current aircraft information; and
- an unresolved station variable remains visible when no station is selected.
The send field therefore remains editable after inserting a shortcut or snippet.
In plain terms: the variables remove repeated typing. The operator still checks the message.
[Read the complete macro and variable reference in the manual.](/manual/en/macros-and-variables/)
[Open the shortcut and snippet configuration.](/manual/en/configuration/#shortcut-settings)
[Read how the AirScout values are obtained.](/features/airscout/)
+328 -9
View File
@@ -3,27 +3,346 @@ title: Priority Score System
icon: 🎯
category: Contest Workflow
since: "1.40"
summary: Rank active stations from known context such as direction, activity, band, QRG, AP and sked information.
description: The Priority Score System combines available contest information to populate a separate list of currently relevant candidates.
summary: Calculate one priority per base callsign, exclude known unusable band combinations and order the remaining active stations by band, distance, direction, activity, AirScout, reply and sked context.
description: KST4Contest combines information which would otherwise have to be evaluated separately and uses the resulting score to identify stations worth examining next.
tagsList:
- ON4KST
- Priority Score
- priority candidates
- band opportunity
- VHF
- UHF
- SHF
- contest
- priority candidates
- contest workflow
related:
- timeline
- airscout
- sked-reminder
- dual-chat
---
## Why it matters
## What question does the score answer?
During active VHF, UHF and microwave contests, relevant stations can disappear quickly in busy ON4KST chat traffic.
An ON4KST user list initially shows which stations are logged in. During a contest, that is only the beginning of the decision.
The Priority Score System helps operators focus on stations that are more likely to be useful during contest operation.
For every possible contact, the operator would otherwise have to combine questions such as:
## Built for contest decisions
- Has the station already been worked?
- Is another common band still available?
- Does the current antenna direction fit?
- Is the station active in the chat?
- Is an aircraft-scatter opportunity approaching?
- Has the station replied quickly before?
- Is there an agreed sked?
KST4Contest is not just a chat viewer. It evaluates contest-relevant context and helps turn chat activity into better operating decisions.
KST4Contest combines this existing context into one relative Priority Score.
The score answers:
> Which active station should I examine next?
It does not answer whether a QSO will succeed.
## Eligibility comes before weighting
Before adding activity, AirScout or sked points, KST4Contest checks whether a usable band opportunity is known.
All active callsign and category variants of the same base callsign are evaluated together.
The band calculation uses:
1. the bands enabled under **My station uses ...**;
2. QRGs detected for the remote station during the previous 30 minutes;
3. explicit band designators in the name fields of its active chat entries;
4. stored per-band Worked marks; and
5. manually assigned NOT-QRV marks.
A manual NOT-QRV mark takes precedence over an automatically detected QRG or band designator.
If bands are known for the remote station but none of them is both enabled locally and still available, the station receives a score of `0`.
The same applies when every known common band has already been worked.
## Unknown is not the same as impossible
A station is not excluded merely because no band information has been detected.
Missing information means that KST4Contest cannot currently prove which band is available. It does not prove that no common band exists.
Such a station remains eligible unless every locally enabled band has explicitly been marked NOT QRV.
Passing this eligibility check does not guarantee a place in the priority list. It only allows the normal weighting to continue. If the final calculation still results in `0`, the station remains outside the priority list.
This distinction is important. A band incompatibility is a hard reason for exclusion. A low final score is the result of the complete operating context.
## How is the score calculated?
The current calculation starts with a base value of `100`.
The following contributions are then applied in order.
### Worked and band information
| Condition | Current effect |
|---|---:|
| No supported band has been worked | `+200` |
| At least one supported band has already been worked | `150` |
| Each additional compatible offered band after the first | `+80` |
| Optional band-upgrade Priority Boost | `+180` |
The additional band count is based on offered bands which are enabled locally and not marked NOT QRV. Worked bands remain part of that general multi-band context, while at least one unworked common band is required for a known band-upgrade opportunity.
The optional Priority Boost applies only when:
- the station has already been worked on at least one band;
- another common and unworked band remains; and
- **Priority boost for band-upgrade cases** is enabled.
Without this option, an open band makes the station eligible but does not by itself guarantee a positive final score.
## Distance
When a valid QRB is available, the subtotal calculated so far is multiplied according to the distance:
| Distance | Multiplier |
|---|---:|
| Below `200 km` | `× 0.7` |
| Between `200 km` and the configured maximum QRB | `× 1.15` |
| Beyond the configured maximum QRB | `× 0.3` |
This does not mean that short or very distant contacts are impossible. It reflects the intended contest workflow: stations inside the configured working range, but not already in the very short-distance range, receive more attention.
If no QRB is available, the distance multiplier is omitted.
## AirScout information
When AirScout reports at least one reachable aircraft, the station receives `+200`.
The arrival time of the next aircraft adds:
| Expected arrival | Additional score |
|---|---:|
| `0 minutes` | `+120` |
| `1 minute` | `+60` |
| `2 minutes` | `+30` |
The AirScout potential percentage is used elsewhere for display and timeline selection. It is not converted directly into the Priority Score.
A displayed aircraft therefore indicates a time-dependent opportunity. It is not a success probability.
## Antenna direction
If the QTF to the remote station lies inside half of the configured antenna beamwidth around the current local QTF, the station receives between `+80` and `+200`.
The value rises towards the centre of the antenna direction:
- approximately `+80` at the edge of the configured beam;
- up to `+200` when both directions match.
The calculation uses the local antenna direction. It does not know the actual antenna direction of the remote station.
## Current chat activity
Recent incoming messages raise the priority:
| Activity | Score |
|---|---:|
| Last incoming line less than 60 seconds ago | `+120` |
| Last incoming line less than three minutes ago | `+60` |
Several incoming lines inside the configured momentum window add:
| Lines inside the window | Score |
|---|---:|
| At least 2 | `+60` |
| At least 4 | `+110` |
| At least 6 | `+160` |
The default momentum window is 180 seconds.
This factor identifies stations which are currently active in the chat. It does not prove that they are available for a contact with the local station.
## Positive text signals
Configured expressions such as `QRV`, `READY`, `RGR`, `OK`, `TNX` or `LSN` are treated as positive activity hints.
A detected expression remains effective for five minutes and adds `+120`.
The detector uses case-insensitive literal substring matching. It does not interpret sentence meaning or negation. A line containing `not QRV` still contains the configured substring `QRV`.
Positive text signals must therefore remain a comparatively weak hint inside the larger calculation. They are useful for recognising activity, but they are not a natural-language interpretation of the message.
## Reply behaviour
When the operator sends:
```text
/cq CALLSIGN ...
```
KST4Contest starts measuring the response time for the corresponding base callsign.
A smoothed average response time adds:
| Average response time | Score |
|---|---:|
| Below one minute | `+80` |
| Below three minutes | `+40` |
Any subsequently received public or private line from the same base callsign ends the pending measurement.
KST4Contest cannot determine whether that line was really an answer to the local operator. The value is therefore an operational approximation, not a statistically reliable reply rate.
If no line is received before the configured timeout, a No-Reply strike is added. The default timeout is 13 minutes.
Each strike divides the current subtotal by:
```text
1 + (number of strikes × 0.6)
```
No-Reply strikes accumulate during the current program run.
## Scheduled contacts
A stored sked is an operating commitment and receives a deliberately strong time-dependent contribution.
| Time relative to the sked | Effect |
|---|---:|
| More than 15 minutes before | `+40` |
| Final 15 minutes | Increasing contribution |
| Three minutes before until one minute after | `+5000` |
The strong contribution around the scheduled time is intentional. An agreed contact should not disappear from the list merely because another station happens to be very active in the chat.
The sked contribution belongs to the normalised base callsign. A sked created for `9A0BB-23` therefore also raises the common score shown for other active `9A0BB` variants.
The actual reminder destination remains the complete selected callsign in its original chat category.
## Manual Sked fail
**Sked fail** represents an explicit operator assessment that the attempted contact or path has failed.
The complete final score, including an imminent sked contribution, is multiplied by `0.15`.
Applying this penalty at the end is important. Otherwise the later `+5000` sked contribution would almost completely cancel the failure mark and leave the station at the top of the list.
The mark:
- applies to the normalised base callsign;
- affects all active suffix and category variants;
- remains active for the current program run; and
- can be removed with **Reset fail**.
The sked itself remains stored. Only its effect on the final operating priority is overruled.
## One station, several active logins
Calls such as:
```text
9A0BB-2
9A0BB-70
9A0BB-23
9A0BB-13
```
remain separate message targets.
For scoring, they are grouped through the base callsign:
```text
9A0BB
```
KST4Contest calculates one score for that base callsign and projects it to every active variant.
This prevents one physical station from occupying several positions in the priority list merely because it uses several suffixes or chat categories.
The individual rows remain separate because outgoing messages still require the complete callsign and the correct category.
## Which login is shown in the priority list?
KST4Contest selects one active login as the representative of the base callsign.
The selection order is:
1. a variant in the chat category from which the most recent inbound activity was received;
2. the most recently active variant inside that category;
3. otherwise, the most recently active variant across all categories.
The resulting Priority Candidate retains:
- the normalised base callsign;
- the complete displayed callsign; and
- the preferred chat category.
Selecting the candidate therefore leads back to an actual active login rather than an abstract database key.
## Display and operation
![Priority Score, compact candidate list and Further Info controls](/manual/assets/priority_score_overview.png)
The score appears in three places:
- the numerically sortable **Score** column in the user list;
- the **Further Info** section of the selected station; and
- the compact priority bar between the user list and Further Info.
The compact bar shows the two highest-ranked candidates.
The **more** button opens a separate list containing up to 15 candidates. Double-click a candidate to select it.
Only finite scores above `0` are included in the priority list. Stations with a score of `0` remain visible in the normal user list, where Worked, band and NOT-QRV information can still be examined.
The priority list is calculated from the active station model and is independent of the current table filters. A candidate may therefore appear in the priority list while its row is hidden by **New bands**, QRB, QTF or another user-list filter.
Selecting such a candidate still updates Further Info and prepares the directed message. The active filter remains unchanged.
> Correct band eligibility, base-callsign grouping, separate suffix routing, the final Sked-fail override and selection of filtered candidates are included in Nightly / v1.42.
## When is the score updated?
New calculations are requested after relevant events such as:
- incoming chat messages;
- AirScout updates;
- newly created skeds;
- log and Worked updates;
- changes to NOT-QRV marks;
- **Sked fail** and **Reset fail**; and
- outgoing `/cq` messages.
The background scheduler checks the score every three seconds. Time-dependent information is also recalculated periodically when no new event arrives.
A short delay between an event and the visible new order is therefore normal. The calculation is deliberately coalesced so that several closely spaced events do not create a separate JavaFX update for every station.
## What does the score not tell you?
The Priority Score is not:
- a probability of completing the QSO;
- a signal-strength estimate;
- a propagation forecast;
- a confirmed antenna direction of the remote station; or
- proof that the station is currently sitting at the radio.
A score of `800` is not twice as promising as a score of `400`. The values establish an order inside the current calculation; they do not form a physical scale.
Input data may also be incomplete or outdated:
- a detected QRG can be up to 30 minutes old;
- a name field may describe several bands without stating the current one;
- a chat line may be unrelated to the local station;
- an AirScout result depends on its external aircraft data and configuration; and
- an agreed sked may no longer reflect the actual situation.
In practical terms: the score keeps the available information from having to be reconstructed mentally for every station. The decision still belongs to the operator.
[Read the complete Priority Score description in the manual.](/manual/en/features/#priority-score-and-priority-list-from-v140)
[Open the related station and scoring settings.](/manual/en/configuration/#band-upgrade-hint-after-a-log-entry)
[Read how active suffix variants remain separate message targets.](/features/dual-chat/)
[Read how Priority Candidates are used on the timeline.](/features/timeline/)
+53 -7
View File
@@ -3,24 +3,70 @@ title: Sked Reminder
icon: 🔔
category: Sked Management
since: "1.40"
summary: Store planned contacts and issue configurable chat messages plus local alerts before the agreed time.
description: Sked Reminder keeps scheduled contacts visible and provides automatic advance messages together with acoustic and visual operator alerts.
summary: Create a timed contact, raise its priority, show it on the timeline and optionally send reminder PMs before the agreed time.
description: KST4Contest keeps scheduled contacts in the active workflow, increases their priority as the agreed time approaches and can remind both operators.
tagsList:
- sked
- ON4KST
- contest reminder
- Win-Test
related:
- priority-score
- airscout
- timeline
---
## Never lose important skeds
## Why store a sked inside the chat client?
During active contests, it is easy to miss a planned contact while handling chat traffic, logging and band changes.
A contact agreed for five or ten minutes later has to compete with incoming messages, logging, antenna changes and other stations asking for attention. Remembering the time is only part of the problem. The station must also become visible again when the appointment approaches.
Sked Reminder keeps scheduled contacts visible.
KST4Contest therefore treats a sked as an active operating task rather than a simple alarm.
## Built for real contest pressure
## What happens when a sked is created?
The workflow supports time-critical operation where missing a few minutes can mean missing a QSO.
Select the station, the remaining time and one of the locally enabled bands. The mode can be set to `SSB` or `CW` for a possible Win-Test handover.
After pressing **Create sked**, KST4Contest:
1. stores the sked internally,
2. raises the station's Priority Score as the scheduled time approaches,
3. adds the contact to the AP and sked timeline, and
4. optionally schedules private reminder messages.
![Sked controls in the Further Info section](/manual/assets/sked_controls.png)
The internal sked does not depend on Win-Test. If no logger is connected or the network handover fails, the sked remains available in KST4Contest.
## Reminding the remote station
Reminder PMs are optional. The available patterns are:
- two and one minute before the sked,
- five, two and one minute before the sked, or
- ten, five, two and one minute before the sked.
Messages are sent to the complete KST callsign in the selected chat category. A band-specific login such as `CALLSIGN-70` therefore remains a separate message target instead of being silently reduced to the base callsign.
The local operator receives a visual **SKED** indication and, if simple notification sounds are enabled, an acoustic reminder.
## Win-Test handover
When the Win-Test network listener is enabled, KST4Contest also attempts to send the sked to Win-Test.
The frequency is not guessed. KST4Contest first looks for a recent QRG of the remote station on the selected band. If none is available, it checks whether the local QRG of the selected chat category belongs to that band. Without a matching frequency, the Win-Test handover is omitted while the internal sked remains intact.
KST-specific suffixes such as `-2`, `-70` or `-144` are removed from the callsign passed to the log. Portable components such as `/P` and `/M` are preserved.
> Band-aware QRG validation, explicit `SSB`/`CW` selection and the corrected handling of KST suffixes are included in Nightly / v1.42.
![Sked handed over from KST4Contest to Win-Test](/manual/assets/wintest_sked_handover.png)
## What the reminder cannot guarantee
Skeds and reminder schedules are stored in memory. They must be recreated after restarting KST4Contest.
The proposed band is derived from recent chat and station-name information. This is useful context, not proof that the station is still operating on the same QRG. Check the band, time and mode before creating the sked.
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)
+38 -9
View File
@@ -1,25 +1,54 @@
---
title: Timeline View
title: AP and Sked Timeline
icon: ⏱️
category: Contest Awareness
since: "1.40"
summary: Show AP-based priority candidates and scheduled contacts on a shared timeline.
description: The Timeline View places upcoming aircraft scatter candidates and planned skeds into a common time-based overview.
summary: Show upcoming aircraft-scatter candidates and scheduled contacts together on a 30-minute timeline.
description: The timeline relates AirScout opportunities, priority candidates, antenna direction and internal skeds to their expected time.
tagsList:
- timeline
- AP windows
- airplane scatter
- aircraft scatter
- sked
related:
- airscout
- priority-score
- sked-reminder
---
## Timing matters
## The useful station may only be useful for a minute
Many VHF/UHF/SHF opportunities are short-lived.
Aircraft-scatter opportunities are time-dependent. A candidate which matters in two minutes may be irrelevant now, while an agreed sked must remain visible even when no aircraft is currently available.
The Timeline View helps operators see upcoming timing windows instead of keeping everything in mind manually.
The timeline places both kinds of event into the same 30-minute view.
## Designed for fast awareness
Events further in the future appear on the right. As their time approaches, they move left towards the present.
The timeline supports quick decisions during busy contest operation.
![AP candidates and skeds in the timeline](/manual/assets/sked_timeline.png)
## AP candidates and scheduled contacts remain distinct
AP candidates appear in the upper lanes. Up to four selected candidates can be shown for each arrival minute. Their colours represent the reflection potential reported by AirScout:
- magenta from 95%,
- red from 75%,
- yellow from 50%, and
- blue below 50%.
Skeds appear as diamonds in the lower lane. Their labels use the complete selected KST callsign so that band-specific or otherwise suffixed logins remain identifiable.
> Complete KST callsigns in sked labels are included in Nightly / v1.42.
## Antenna direction remains visible
A marker becomes more transparent when its QTF is clearly outside the current antenna direction. The callsign remains readable. Targets near the centre of the configured antenna beam receive an additional visual highlight.
Clicking an AP candidate selects the corresponding active chat member, including the callsign suffix and chat category.
## A timing aid, not a contact forecast
The timeline shows when an event is expected to matter. It does not guarantee that the frequency is clear, that the remote station is ready or that the calculated aircraft path will produce a workable signal.
AirScout data can change. So can the actual operating situation.
[Read the complete timeline behaviour in the manual.](/manual/en/features/#ap-and-sked-timeline)
+22 -3
View File
@@ -5,14 +5,33 @@ pagination:
alias: manual
permalink: "/manual/{{ manual.lang }}/{{ manual.slug }}/"
layout: base.njk
lang: "{{ manual.lang }}"
title: "{{ manual.title }} | KST4Contest Manual"
description: "KST4Contest manual page: {{ manual.title }}"
description: "{{ manual.summary }}"
---
<section class="section">
<div class="card manual-content">
<p><a href="/manual/{{ manual.lang }}/">← Back to manual overview</a></p>
<h1>{{ manual.title }}</h1>
{% if manual.lang == "de" %}
<p>
<a href="/manual/de/">← Zurück zur deutschen Handbuchübersicht</a>
</p>
{% else %}
<p>
<a href="/manual/en/">← Back to the English manual overview</a>
</p>
{% endif %}
{{ manual.content | markdown | safe }}
{% if manual.lang == "de" %}
<p>
<a href="/manual/de/">← Zurück zur deutschen Handbuchübersicht</a>
</p>
{% else %}
<p>
<a href="/manual/en/">← Back to the English manual overview</a>
</p>
{% endif %}
</div>
</section>
+89 -6
View File
@@ -1,24 +1,107 @@
---
layout: base.njk
title: KST4Contest Deutsches Handbuch
description: Deutsches Benutzerhandbuch für KST4Contest.
lang: de
title: KST4Contest Deutsches Handbuch
description: Technisches Benutzerhandbuch für Installation, Konfiguration, ON4KST-Betrieb, AirScout, Log-Synchronisation und die weiteren Funktionen von KST4Contest.
---
<section class="hero">
<p class="eyebrow">Dokumentation</p>
<h1>Deutsches Handbuch</h1>
<p>Das Handbuch wird zentral in GitHub gepflegt und als Wiki, PDF und Online-Dokumentation veröffentlicht.</p>
<p>
Dieses Handbuch beschreibt nicht nur die Bedienelemente von KST4Contest.
Es erklärt auch, wie Bandinformationen, Prioritäten, Erreichbarkeit und
weitere Betriebshilfen hergeleitet werden und an welchen Stellen die
verfügbaren Daten keine eindeutige Aussage erlauben.
</p>
<div class="actions">
<a class="button" href="/manual/de/installation/">Installation</a>
<a class="button secondary" href="/manual/de/konfiguration/">Konfiguration</a>
<a class="button ghost" href="/manual/de/funktionen/">Funktionen</a>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Einstieg nach Aufgabe</p>
<h2>Wo solltest du anfangen?</h2>
<p>
Die Kapitel können einzeln verwendet werden. Bei einer neuen Installation
ist die folgende Reihenfolge jedoch meist sinnvoller, als direkt in einer
Detailfunktion zu beginnen.
</p>
</div>
<div class="grid">
<article class="card">
<h3>KST4Contest neu installieren</h3>
<p>
Wähle den passenden Build, installiere die Anwendung und prüfe, ob sie
auf dem vorgesehenen Contest-Rechner korrekt startet.
</p>
<div class="actions">
<a class="button" href="/download/">Build auswählen</a>
<a class="button secondary" href="/manual/de/installation/">Installation öffnen</a>
</div>
</article>
<article class="card">
<h3>Die eigene Station einrichten</h3>
<p>
Trage Rufzeichen, Locator, verwendete Bänder und Chatkategorien ein.
Richte anschließend nur die Schnittstellen ein, die im Stationsaufbau
tatsächlich verwendet werden.
</p>
<div class="actions">
<a class="button secondary" href="/manual/de/konfiguration/">Konfiguration öffnen</a>
<a class="button ghost" href="/manual/de/log-synchronisation/">Log-Synchronisation</a>
</div>
</article>
<article class="card">
<h3>Die Anzeigen richtig einordnen</h3>
<p>
Prioritäten, Bandinformationen, AP-Bewertungen und Erreichbarkeit werden
aus mehreren Quellen abgeleitet. Die entsprechenden Kapitel erklären,
welche Informationen dafür verwendet werden und wo die Grenzen liegen.
</p>
<div class="actions">
<a class="button secondary" href="/manual/de/funktionen/">Funktionen öffnen</a>
<a class="button ghost" href="/manual/de/benutzeroberflaeche/">Benutzeroberfläche</a>
</div>
</article>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Alle Kapitel</p>
<h2>Vollständiges deutsches Handbuch</h2>
<p>
Die Liste wird automatisch aus den Markdown-Dateien im Verzeichnis
<code>github_docs</code> erzeugt. Änderungen am zentralen Handbuch werden
dadurch auch in die Online-Dokumentation übernommen.
</p>
</div>
<div class="grid">
{% for manual in collections.manualPages %}
{% if manual.lang == "de" %}
<div class="card">
<h3><a href="/manual/de/{{ manual.slug }}/">{{ manual.title }}</a></h3>
<article class="card">
<h3>
<a href="/manual/de/{{ manual.slug }}/">{{ manual.title }}</a>
</h3>
{% if manual.summary %}
<p>{{ manual.summary }}</p>
{% endif %}
</div>
<p>
<a href="/manual/de/{{ manual.slug }}/">Kapitel öffnen →</a>
</p>
</article>
{% endif %}
{% endfor %}
</div>
+89 -6
View File
@@ -1,24 +1,107 @@
---
layout: base.njk
title: KST4Contest English Manual
description: English user manual for KST4Contest.
lang: en
title: KST4Contest English Manual
description: Technical user manual covering installation, configuration, ON4KST operation, AirScout, log synchronisation and the other functions of KST4Contest.
---
<section class="hero">
<p class="eyebrow">Documentation</p>
<h1>English Manual</h1>
<p>The manual is maintained centrally in GitHub and published as Wiki, PDF and online documentation.</p>
<p>
This manual describes more than the controls of KST4Contest. It also
explains how band information, priorities, reachability and other operating
aids are derived, and where the available data does not support an
unambiguous result.
</p>
<div class="actions">
<a class="button" href="/manual/en/installation/">Installation</a>
<a class="button secondary" href="/manual/en/configuration/">Configuration</a>
<a class="button ghost" href="/manual/en/features/">Functions</a>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Start with the task</p>
<h2>Where should you begin?</h2>
<p>
Each chapter can be used separately. For a new installation, however,
the following order is usually more useful than starting with an isolated
detail.
</p>
</div>
<div class="grid">
<article class="card">
<h3>Install KST4Contest</h3>
<p>
Select the appropriate build, install the application and verify that it
starts correctly on the computer intended for contest operation.
</p>
<div class="actions">
<a class="button" href="/download/">Select a build</a>
<a class="button secondary" href="/manual/en/installation/">Open installation</a>
</div>
</article>
<article class="card">
<h3>Configure the station</h3>
<p>
Enter the callsign, locator, available bands and chat categories. Then
configure only the interfaces that are actually part of the station
setup.
</p>
<div class="actions">
<a class="button secondary" href="/manual/en/configuration/">Open configuration</a>
<a class="button ghost" href="/manual/en/log-sync/">Log synchronisation</a>
</div>
</article>
<article class="card">
<h3>Interpret the displayed information</h3>
<p>
Priorities, band information, AP assessments and reachability are
derived from several sources. The corresponding chapters explain which
information is used and where its limitations begin.
</p>
<div class="actions">
<a class="button secondary" href="/manual/en/features/">Open functions</a>
<a class="button ghost" href="/manual/en/user-interface/">User interface</a>
</div>
</article>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">All chapters</p>
<h2>Complete English manual</h2>
<p>
This list is generated automatically from the Markdown files in
<code>github_docs</code>. Changes to the central manual are therefore also
included in the online documentation.
</p>
</div>
<div class="grid">
{% for manual in collections.manualPages %}
{% if manual.lang == "en" %}
<div class="card">
<h3><a href="/manual/en/{{ manual.slug }}/">{{ manual.title }}</a></h3>
<article class="card">
<h3>
<a href="/manual/en/{{ manual.slug }}/">{{ manual.title }}</a>
</h3>
{% if manual.summary %}
<p>{{ manual.summary }}</p>
{% endif %}
</div>
<p>
<a href="/manual/en/{{ manual.slug }}/">Open chapter →</a>
</p>
</article>
{% endif %}
{% endfor %}
</div>
+76 -4
View File
@@ -1,18 +1,90 @@
---
layout: base.njk
title: KST4Contest Manual
description: Online manual for KST4Contest, the contest-optimized ON4KST chat client.
description: Technical user manual for KST4Contest, covering installation, station setup, ON4KST operation, AirScout, logger interfaces, path analysis and known limitations.
---
<section class="hero">
<span class="eyebrow">Documentation</span>
<h1>KST4Contest Manual</h1>
<p>
User documentation for installation, configuration, features,
AirScout integration, log synchronization and contest workflow.
The manual describes the controls and interfaces of KST4Contest as well as
the reasoning behind functions that derive information from chat messages,
station data or external programs. Where a result depends on incomplete
information, the corresponding limitations are described as well.
</p>
<div class="actions">
<a class="button" href="/manual/en/">English Manual</a>
<a class="button" href="/manual/en/">English manual</a>
<a class="button secondary" href="/manual/de/">Deutsches Handbuch</a>
<a class="button ghost" href="/download/">Download KST4Contest</a>
</div>
</section>
<section class="section">
<div class="section-heading">
<span class="eyebrow">Where to start</span>
<h2>Select the section that matches your current task</h2>
</div>
<div class="grid">
<article class="card">
<h3>Install KST4Contest</h3>
<p>
Select a Stable, Beta or Nightly build and install the application on
Windows, Linux or macOS.
</p>
<div class="actions">
<a class="button" href="/download/">Downloads</a>
<a class="button secondary" href="/manual/en/installation/">English instructions</a>
<a class="button ghost" href="/manual/de/installation/">Deutsche Anleitung</a>
</div>
</article>
<article class="card">
<h3>Configure a station</h3>
<p>
Set the callsign, locator, available bands, chat categories and the
interfaces used in the station setup.
</p>
<div class="actions">
<a class="button secondary" href="/manual/en/configuration/">English configuration</a>
<a class="button ghost" href="/manual/de/konfiguration/">Deutsche Konfiguration</a>
</div>
</article>
<article class="card">
<h3>Understand functions and limitations</h3>
<p>
Read how KST4Contest derives band information, priorities, reachability
and other operating aids, and where manual verification is still
required.
</p>
<div class="actions">
<a class="button secondary" href="/manual/en/features/">English functions</a>
<a class="button ghost" href="/manual/de/funktionen/">Deutsche Funktionen</a>
</div>
</article>
</div>
</section>
<section class="section">
<div class="cta-panel">
<div>
<span class="eyebrow">Choosing a build</span>
<h2>Stable for operation, Beta or Nightly for testing</h2>
<p>
The Stable build is normally the appropriate choice for contest
operation. Beta and Nightly builds are useful when a specific correction
or new function needs to be tested. Test them before the contest rather
than during its first minutes.
</p>
</div>
<div class="actions">
<a class="button" href="/download/">Open downloads</a>
<a class="button secondary" href="/roadmap/">View roadmap</a>
</div>
</div>
</section>
+93
View File
@@ -0,0 +1,93 @@
const assert = require("node:assert/strict");
const test = require("node:test");
const generateVersionInfo =
require("../src/_data/versionInfo");
const {
validateVersionInfo
} = require("../scripts/validate-version-info");
const VALID_XML =
`<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<praktiKST>
<latestVersion>
<versionNumber>1.411</versionNumber>
<semanticVersion>1.41.1</semanticVersion>
<adminMessage></adminMessage>
<majorChanges>Hotfix</majorChanges>
<latestVersionPathOnWebserver>https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1</latestVersionPathOnWebserver>
</latestVersion>
<needUpdateSinceLastVersion>
<filename>nothing</filename>
</needUpdateSinceLastVersion>
<changeLog>
<changedVersionNumber>1.41.1</changedVersionNumber>
<date>2026-07-08</date>
<description>Hotfix</description>
<added></added>
<changed></changed>
<fixed>Text input handling</fixed>
<removed></removed>
</changeLog>
</praktiKST>`;
test("accepts a complete update feed", () => {
const result =
validateVersionInfo(VALID_XML, "v1.41.1");
assert.equal(result.semanticVersion, "1.41.1");
assert.equal(result.changeLogEntries, 1);
});
test("rejects the former empty fallback", () => {
assert.throws(
() =>
validateVersionInfo(
'<?xml version="1.0" '
+ 'encoding="UTF-8"?>'
+ "<praktiKST></praktiKST>"
),
/empty or implausibly small/
);
});
test(
"rejects a feed which does not contain "
+ "the expected Stable release",
() => {
assert.throws(
() =>
validateVersionInfo(
VALID_XML,
"v1.42.0"
),
/does not match expected release/
);
}
);
test(
"aborts generation when GitHub rejects "
+ "the API request",
async () => {
const originalFetch = global.fetch;
global.fetch = async () => ({
ok: false,
status: 401,
headers: {
get: () => null
}
});
try {
await assert.rejects(
generateVersionInfo(),
/website build has been aborted.*HTTP 401/i
);
} finally {
global.fetch = originalFetch;
}
}
);