Author SHA1 Message Date
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
Marc Froehlich bdd10ee00b Created manual pages for wkd status infos, band settings, functions and settings 2026-08-05 01:55:10 +02:00
Marc Froehlich 6092ff882a added project Band enum from Win-Test band IDs 2026-08-05 01:18:41 +02:00
Rsclub2_2 e37cda8ff2 Add 50/70 MHz band support (table, NOT-QRV, station setup, loggers)
Issue #68

Adds Band.B_50/B_70 and wires them through the same band-opportunity
machinery as the other bands: X/a/B+/o table columns and filter button
in the User table and the Workedstn database table, NOT-QRV checkboxes
and propagation across callsign variants, "My station uses 6m/4m band"
toggles, station-name detection ("50", "6M", "70MHZ", "4M" - without
stealing the existing bare "70"/"6" cm-band shorthand), Win-Test sked
band IDs (10/50MHz, 11/70MHz), and UCX-logger worked-band recognition.

Persists worked50/70 and notQRV50/70 via an additive SQLite migration
(same ensureColumnExists pattern as the earlier v1.1->v1.2 migration),
verified against a real database file.

ReachabilityService.resolveAutoBand() now also falls back to 50 MHz
(then 70 MHz) for stations in the "50/70 MHz" chat category, mirroring
the existing Microwave-category fallback to 23cm.

Assisted by Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 21:10:15 +02:00
Rsclub2_2 fdec5220d1 Unify band-opportunity logic and add B+/a/o status to the station table
Issue #70, #66, #65

Introduces a shared BandOpportunityResolver so the station table's band
columns, the New bands filter, the band-upgrade hint, the priority
score, the map markers and the automatic reachability band selection
all derive band availability the same way: recent QRG detections (30
min window) plus station-name hints, evaluated across every active
callsignRaw variant, with manual NOT-QRV always taking precedence.

The per-band table cells now distinguish:
- X: worked on this band
- a: band available, call not worked on any band yet
- B+: band available, call already worked on another band
- o: grid square already worked on this band (any station) - combines
  with the others, e.g. "ao" or "B+o"

Both "a" and "o" can be toggled off in the GUI settings tab.

Assisted by Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 21:00:23 +02:00
Rsclub2_2 35f790f06b Fix stale Beta tab showing a superseded prerelease
GitHub never clears the prerelease flag once a beta ships as stable,
so the latest prerelease can be older than the latest stable release.
Compare publish dates and fall back to the empty state when stable is
newer.

Assisted by Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 20:49:06 +02:00
Rsclub2_2 c04117ad0a added Beta and Nightly Download Links to website using nightly.link 2026-08-03 22:31:14 +02:00
Marc Froehlich c7d1dfbe92 Refactored QRG recognition (added some band context to increase the quality of the values + changed cluster fallback-band-value to dropdown switch + normalizing qrg values) 2026-08-03 01:09:32 +02:00
Marc Froehlich e9f09b764d Refactored QRG recognition (added some band context to increase the quality of the values + changed cluster fallback-band-value to dropdown switch + normalizing qrg values) 2026-08-03 01:09:25 +02:00
Marc Froehlich b432129798 Added contactreplace packets for broadcast whole log - function of DXLog.net to the ucxlog packet listener 2026-07-28 19:30:26 +02:00
Marc Froehlich 344ca547fd Changed sked opportunity highlighting mechanism to more precision, updated the manual 2026-07-26 23:56:32 +02:00
Marc Froehlich 182de0526b Changed ui, used flowpanes to make center slider compatible to smaller screens. URLs are now clickable. Tooltips are shown in case of too small table drawings. Added documentation in the gethub manual. 2026-07-26 18:10:39 +02:00
Marc Froehlich 333063f350 Changed Autoanswer implementation (cooldown timer, message routing) and its documentation 2026-07-26 11:04:47 +02:00
Marc Froehlich ee974bbd73 Fixed beacon implementation, pstrotator settings, Variable handling, Shortcut and snipped preferences ui, beacon handling, AS Settings and all fitting manual texts 2026-07-26 09:49:50 +02:00
Marc Froehlich 00496b56e9 Fixed AS preferences implementation, DXcluster Server and the documentation 2026-07-25 03:01:49 +02:00
Rsclub2_2 5b7897c872 Remove unnecessary Website Build 2026-07-21 13:50:48 +02:00
138 changed files with 9425 additions and 15649 deletions
-76
View File
@@ -1,76 +0,0 @@
name: Build Website
on:
push:
branches:
- main
paths:
- website/**
- github_docs/**
- .github/workflows/website-build.yml
pull_request:
branches:
- main
paths:
- website/**
- github_docs/**
- .github/workflows/website-build.yml
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
concurrency:
group: website-build-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
build-website:
name: Build 11ty Website
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
with:
ssh-key: ${{ secrets.WEBSITE_DEPLOY_KEY }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: website/package-lock.json
- name: Install dependencies
working-directory: website
run: npm ci
- name: Build website
working-directory: website
run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload website artifact
uses: actions/upload-artifact@v4.3.4
with:
name: kst4contest-website
path: website/_site/
retention-days: 7
- name: Commit rebuilt site
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add website/_site
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: rebuild website [skip ci]"
git push
fi
@@ -1,59 +0,0 @@
name: Rebuild Roadmap
on:
issues:
types: [opened, edited, closed, reopened, labeled, unlabeled, milestoned, demilestoned]
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
concurrency:
group: website-build-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
jobs:
rebuild-roadmap:
name: Rebuild website roadmap
runs-on: ubuntu-latest
# Only worth a rebuild if the "enhancement" label is involved: either the
# issue currently carries it, or this event just added/removed it.
if: contains(github.event.issue.labels.*.name, 'enhancement') || github.event.label.name == 'enhancement'
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
with:
ref: main
ssh-key: ${{ secrets.WEBSITE_DEPLOY_KEY }}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "24"
cache: npm
cache-dependency-path: website/package-lock.json
- name: Install dependencies
working-directory: website
run: npm ci
- name: Build website
working-directory: website
run: npm run build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit rebuilt site
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add website/_site
if git diff --cached --quiet; then
echo "No changes to commit"
else
git commit -m "chore: rebuild website roadmap [skip ci]"
git push
fi
+3
View File
@@ -39,5 +39,8 @@ dist/
# node Modules in website # node Modules in website
website/node_modules/ website/node_modules/
# built website output (rebuilt on the server)
website/_site/
# Local secrets for act testing # Local secrets for act testing
.secrets .secrets
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+115 -14
View File
@@ -26,17 +26,38 @@ Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfigu
| Spalte | Inhalt | | Spalte | Inhalt |
|---|---| |---|---|
| Call | Rufzeichen der Station | | Callsign | Rufzeichen der Station |
| Name | Name aus dem Chat-Namenfeld | | Name | Name beziehungsweise Zusatzinformationen aus dem Chat-Namensfeld |
| Loc | Maidenhead-Locator | | QRA | Maidenhead-Locator |
| QRB | Entfernung in km | | QRB | Entfernung in km |
| QTF | Richtung in Grad | | QTF | Richtung in Grad |
| QRG | Automatisch erkannte Frequenz | | QRG | Zuletzt aus einer Chat-Nachricht erkannte Frequenz |
| AP | AirScout-Flugzeugdaten (wenn aktiv) | | Tropo | Ergebnis der bandbezogenen Tropo- beziehungsweise Streckenbewertung |
| Band-Farben | Worked/NOT-QRV-Status pro Band | | 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` |
| NOT QRV @ | Bänder, auf denen die Station manuell als nicht QRV markiert wurde |
| Category | Chat-Kategorie des Eintrags |
Die QRG-Spalte zeigt die zuletzt für eine Station erkannte Frequenz. Fehlende Nullen werden für die Anzeige ergänzt, sodass beispielsweise `144.21` als `144.210` erscheint. Erkennt KST4Contest nacheinander Frequenzen auf mehreren Bändern, zeigt die Spalte den letzten Treffer; die internen Bandinformationen können trotzdem mehrere aktuelle Bänder der Station enthalten.
Relative Angaben werden zunächst mit einem höchstens 30 Minuten alten Bandkontext desselben Absenders kombiniert. Nur wenn dieser fehlt, verwendet KST4Contest das globale Fallback-Band. Erkennungsregeln, Beispiele und Grenzen: [QRG-Erkennung](de-Funktionen#qrg-erkennung).
### Worked-, Band- und Großfeldstatus
Die Unterspalten unter **worked** sind kompakt, weil bei mehreren aktivierten Bändern kaum Platz für ausgeschriebene Zustände bleibt. `X` kennzeichnet ein auf diesem Band gearbeitetes Rufzeichen. `a` und `B+` weisen auf ein angebotenes, noch nicht gearbeitetes Band hin. Ein angehängtes `o` bedeutet, dass das vierstellige Großfeld auf diesem Band bereits gearbeitet wurde.
Die Unterspalte **wkdany** ist bandunabhängig: `x` steht für ein bereits gearbeitetes Rufzeichen, `o` für ein auf irgendeinem Band gearbeitetes Großfeld und `xo` für beides.
Jede Statuszelle besitzt einen Tooltip mit der Legende und dem für die betreffende Station ermittelten Zustand. Die vollständige Herleitung einschließlich NOT-QRV-Vorrang: [Gearbeitete Rufzeichen, neue Bänder und neue Großfelder](de-Funktionen#gearbeitete-rufzeichen-neue-bänder-und-neue-großfelder).
![Bandbezogener Worked-Status und Worked-Großfelder](worked_band_status.png)
**Sortierung**: Klick auf Spaltenköpfe. QRB-Sortierung arbeitet numerisch (ab v1.22 korrigiert). **Sortierung**: Klick auf Spaltenköpfe. QRB-Sortierung arbeitet numerisch (ab v1.22 korrigiert).
Ein grün und fett dargestelltes Rufzeichen kennzeichnet eine aus einer gerichteten Nachricht hergeleitete Richtungsgelegenheit. Die Markierung bezieht sich auf den Absender der Nachricht und bleibt höchstens fünf Minuten sichtbar. Herleitung und Grenzen: [Richtungsgelegenheiten aus gerichteten Nachrichten](de-Funktionen#richtungsgelegenheiten-aus-gerichteten-nachrichten).
### Sendfeld ### Sendfeld
Texteingabe für ausgehende Nachrichten. Nach Klick auf ein Rufzeichen in der Benutzerliste erhält das Sendfeld automatisch den Fokus sofort tippen ohne Doppelklick (ab v1.22). Texteingabe für ausgehende Nachrichten. Nach Klick auf ein Rufzeichen in der Benutzerliste erhält das Sendfeld automatisch den Fokus sofort tippen ohne Doppelklick (ab v1.22).
@@ -51,28 +72,108 @@ Eingabefeld für die aktuelle Antennenrichtung. Wird für die geplante `MYQTF`-V
--- ---
## Nachrichtentabellen
KST4Contest zeigt Nachrichtentexte bewusst einzeilig an. So bleiben auch bei hohem Chat-Aufkommen viele Einträge gleichzeitig sichtbar. Der Nachteil liegt auf der Hand: Bei einer schmalen **Message**-Spalte passt nicht jede Nachricht vollständig in die Zeile.
Ist ein Nachrichtentext breiter als die sichtbare Zelle, zeigt KST4Contest den vollständigen Inhalt als Tooltip an. Dazu die Maus kurz über die betreffende **Message**-Zelle halten. Passt der Text vollständig in die Spalte, wird kein zusätzlicher Volltext-Tooltip eingeblendet.
Webadressen mit `http://`, `https://` oder dem Präfix `www.` werden innerhalb des Nachrichtentextes als Links dargestellt. Ein Klick öffnet die Adresse im Standardbrowser des Betriebssystems. Andere Protokolle werden nicht als Link behandelt.
![Abgeschnittener Nachrichtentext mit Volltext-Tooltip und Link](message_tooltip_and_link.png)
Damit muss der Divider nicht allein deshalb verschoben werden, um eine einzelne längere Nachricht zu lesen. Für einen dauerhaft breiteren Nachrichtenbereich kann er selbstverständlich weiterhin angepasst werden.
---
## Filter ## Filter
Die Filter-Leiste (ab v1.21 als Flowpane für kleine Bildschirme): Die Filterleiste befindet sich oberhalb der Chatmember-Tabelle. Sie ist in mehrere logisch zusammengehörige Bereiche gegliedert:
- **Show only QTF**: Richtungsfilter aktivieren (Buttons N/NE/E/… oder Grad-Eingabe) - **Show only QTF** begrenzt die Liste auf eine gewählte Antennenrichtung.
- **Show only QRB [km] <=**: Entfernungsfilter aktivieren (Toggle-Button) - **Show only QRB [km] <=** setzt eine maximale Entfernung.
- **Hide Worked [Band]**: Gearbeitete Stationen pro Band ausblenden (je ein Toggle pro Band) - **Find** sucht nach einem Rufzeichen.
- **Hide NOT-QRV [Band]**: NOT-QRV-markierte Stationen pro Band ausblenden - **wkd** blendet Rufzeichen aus, die bereits auf mindestens einem Band gearbeitet wurden.
- Die einzelnen Band-Schaltflächen blenden eine Station aus, wenn sie auf dem betreffenden Band bereits gearbeitet oder dort als NOT QRV markiert wurde. Angezeigt werden nur die für die eigene Station aktivierten Bänder.
- **Only new grids** zeigt ausschließlich Stationen aus vierstelligen Großfeldern, die auf noch keinem Band gearbeitet wurden.
- **Grid color** ist kein Filter. Die Funktion markiert das QRA-Feld bereits gearbeiteter Großfelder, ohne Stationen auszublenden.
- **New bands** zeigt Stationen mit mindestens einer erkannten, an der eigenen Station aktivierten und noch nicht gearbeiteten Bandmöglichkeit. NOT-QRV-Markierungen haben Vorrang.
- **Reachability**, **Tropo >=0dB** und **AS next 5m** schränken die Liste anhand der gewählten Strecken- beziehungsweise AirScout-Bedingungen ein.
Die Filterleiste besitzt keine feste Breite. QTF sowie die Worked- und Reachability-Filter nutzen zunächst den gesamten Platz ihrer jeweiligen Zeile. Wird der horizontale Divider nach rechts verschoben und die Chatmember-Ansicht dadurch schmaler, wechseln die Controls erst dann in die nächste Zeile, wenn ihre tatsächlich benötigte Breite nicht mehr zur Verfügung steht.
![Umgebrochene Filterleiste bei schmaler Chatmember-Ansicht](filter_bar_wrapped.png)
Im Klartext: Die Filter bestimmen weiterhin den Inhalt der Tabelle, aber nicht mehr die Mindestbreite der gesamten rechten Programmseite. In der normalen Ansicht bleibt die Leiste kompakt. Erst bei einer tatsächlich schmalen Ansicht benötigt sie mehr Höhe. Der Divider kann anschließend wieder nach links verschoben werden; die Controls ordnen sich unmittelbar neu an.
---
--- ---
## Stationsinfo-Panel (Further Info) ## Stationsinfo-Panel (Further Info)
Rechts unten: Zeigt alle Nachrichten einer ausgewählten Station (CQ-Nachrichten und PMs in einem Panel). Ein Nachrichtenfilter lässt sich über den Standard-Filter in den Preferences vorbelegen. Rechts unten werden die Nachrichten der ausgewählten Station zusammengeführt. Dazu gehören öffentliche Nachrichten, Privatnachrichten an die eigene Station und soweit im Chat sichtbar Privatnachrichten an andere Stationen.
Hier können auch **Sked-Erinnerungen** aktiviert werden. Der im Panel gewählte Filter bestimmt, welche dieser Nachrichten angezeigt werden. Unter **Settings → GUI** lässt sich festlegen, welcher Filter beim Öffnen einer Stationsinformation vorausgewählt ist:
- alle Nachrichten,
- Privatnachrichten an die eigene Station,
- Privatnachrichten an andere Stationen oder
- öffentliche Nachrichten.
Die Einstellung verändert nur die Darstellung im Stationsinfo-Panel. Nachrichten werden dadurch weder verworfen noch aus den übrigen Nachrichtentabellen entfernt. Der Filter kann im Panel jederzeit für die aktuell betrachtete Station gewechselt werden.
Im unteren Bereich können für die ausgewählte Station bandbezogene **Not QRV**-Markierungen gesetzt werden. Sichtbar sind die Bänder, die in den Stationseinstellungen für die eigene Station aktiviert wurden. **tag not qrv all** setzt beziehungsweise entfernt die Markierung für alle unterstützten Bänder gemeinsam, einschließlich momentan nicht eingeblendeter Bänder.
Die Änderung wirkt sofort auf die Spalte **NOT QRV @**, die Bandmöglichkeiten und die zugehörigen Filter. Sie wird in der internen Datenbank gespeichert und nach einem Neustart wiederhergestellt.
![Bandbezogene NOT-QRV-Markierungen im Further-Info-Bereich](not_qrv_controls.png)
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 ## 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).
--- ---
+6
View File
@@ -8,6 +8,12 @@ Versionsverlauf von KST4Contest / PraktiKST.
letzter Changelog bitte aus GitHub entnehmen. Der bisherige Changelog letzter Changelog bitte aus GitHub entnehmen. Der bisherige Changelog
## 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.
## v1.41 ## v1.41
**Stationskarte, Performance, Reaktionsfähiges UI** **Stationskarte, Performance, Reaktionsfähiges UI**
+187 -38
View File
@@ -2,75 +2,224 @@
> 🇬🇧 [English version](en-DX-Cluster-Server) | 🇩🇪 Du liest gerade die deutsche Version > 🇬🇧 [English version](en-DX-Cluster-Server) | 🇩🇪 Du liest gerade die deutsche Version
Ab **Version 1.23** enthält KST4Contest einen integrierten DX-Cluster-Server. Dieser sendet Spots direkt an das Logprogramm, wenn eine Richtungs-Warnung ausgelöst wird. Seit Version 1.23 enthält KST4Contest einen lokalen DX-Cluster-Server. Er übergibt erkannte Richtungsgelegenheiten und die dazugehörige Frequenz an den DX-Cluster-Client eines Logprogramms.
*(Idee von OM0AAO, Viliam Petrik danke!)* Die Idee dazu stammt von OM0AAO, Viliam Petrik. Vielen Dank!
--- ---
## Wozu dient der integrierte DX-Cluster-Server? ## Warum überhaupt ein eigener DX-Cluster-Server?
Wenn KST4Contest erkennt, dass eine Station aus der eigenen Richtung ein Sked anfragt und gleichzeitig eine QRG bekannt ist, wird **automatisch ein DX-Cluster-Spot generiert** und an den Cluster-Client des Logprogramms gesendet. Eine interessante Frequenz im Chat zu erkennen ist nur der erste Schritt. Im Contest muss diese Information dort ankommen, wo sie unmittelbar verwendet werden kann: im Logprogramm und dessen Bandmap.
Das Logprogramm zeigt den Spot in der Bandkarte an. Ein Klick auf den Spot stellt Frequenz und Mode des Transceivers direkt ein ohne manuelles Eintippen. KST4Contest verbindet deshalb zwei bereits vorhandene Informationen:
1. Aus einer gerichteten Chat-Nachricht lässt sich abschätzen, in welche Richtung die sendende Station ihre Antenne wahrscheinlich ausgerichtet hat.
2. Aus derselben oder einer vorherigen Nachricht kann eine Frequenz der Station bekannt sein.
Treffen beide Informationen zusammen, erzeugt KST4Contest einen lokalen DX-Cluster-Spot. Der Logger kann diesen Spot in seiner Bandmap anzeigen und abhängig von seiner eigenen Konfiguration den Transceiver nach einem Klick auf die entsprechende Frequenz einstellen.
Im Klartext: Die Information muss nicht erst im Chat gefunden, gelesen, gemerkt und anschließend erneut in den Logger eingetragen werden. Genau diese kleinen Unterbrechungen kosten im Contest überraschend viel Aufmerksamkeit.
--- ---
## Einrichtung ## Wie wird eine Richtungsgelegenheit hergeleitet?
### In KST4Contest Angenommen, Station A schreibt eine gerichtete Nachricht an Station B. KST4Contest verwendet die Richtung von A zu B als Näherung für die aktuelle Antennenrichtung von Station A. Anschließend wird geprüft, ob die eigene Station aus Sicht von A innerhalb des angenommenen Antennenkorridors liegt.
In den Preferences → **DX-Cluster-Server-Einstellungen**: Dafür werden zwei Richtungen verglichen:
1. **Port** des internen Servers eintragen (z. B. 7300 oder 8000 muss mit dem Logprogramm übereinstimmen). - die Richtung von Station A zu Station B,
2. **Spotter-Rufzeichen** eintragen **unbedingt ein anderes Rufzeichen als das Contest-Rufzeichen verwenden!** - die Richtung von Station A zur eigenen Station.
- Grund: Logprogramme filtern Spots, die vom eigenen Rufzeichen stammen, als „gearbeitet" heraus. Wenn der Spotter dasselbe Rufzeichen hat, werden die Spots nicht angezeigt.
3. **Angenommene MHz** eintragen: Bei Frequenzangaben wie „.205" im Chat muss KST4Contest entscheiden, ob 144.205, 432.205 oder 1296.205 gemeint ist. Bei Einband-Contests einfach die entsprechende Bandmitte eintragen. Vollständige Frequenzangaben wie „144.205" oder „1296.338" im Chat werden immer korrekt erkannt.
### In UCXLog Der in den Station Settings konfigurierte Antennen-Öffnungswinkel ist der vollständige Winkel. Für die Prüfung wird jeweils die Hälfte links und rechts der Richtung A → B angesetzt. Bei `70°` sind das somit `±35°`.
- Verbindung zu einem DX-Cluster-Server konfigurieren: Da ON4KST keine Antennendaten der fremden Station überträgt, verwendet KST4Contest den Öffnungswinkel der eigenen Antenne zugleich als Näherung für Station A. Das ist keine Messung der tatsächlichen Antennenrichtung, sondern eine bewusst einfache geometrische Annahme.
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
- Port: Wie in KST4Contest konfiguriert
- Passwort: kann leer bleiben
- Über die Schaltfläche **„Send a test message to your log"** kann die Verbindung getestet werden.
### In N1MM+ Ein DX-Cluster-Spot wird nur erzeugt, wenn alle folgenden Bedingungen erfüllt sind:
Ähnliche Einstellungen: 1. Eine gerichtete Nachricht wurde zwischen zwei anderen Stationen erkannt.
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers) 2. Für Absender und Empfänger sind gültige Locator bekannt.
- Port: Wie in KST4Contest konfiguriert 3. Der Absender liegt innerhalb des konfigurierten maximalen QRB zur eigenen Station.
4. Die eigene Station liegt aus Sicht des Absenders innerhalb des angenommenen Antennenkorridors.
5. Für den Absender ist eine verwertbare Frequenz bekannt oder in der aktuellen Nachricht erkannt worden.
6. Der lokale DX-Cluster-Server ist aktiviert.
Treffen die Bedingungen zu, wird der Spot unmittelbar beim Verarbeiten der Nachricht erzeugt. Die parallel angezeigte grüne Richtungsmarkierung bleibt dagegen fünf Minuten sichtbar und kann durch spätere Nachrichten verlängert oder vorzeitig entfernt werden.
Das Verfahren berücksichtigt weder Gelände noch aktuelle Ausbreitungsbedingungen und beweist keine tatsächliche Antennenstellung. Es erkennt eine plausible Gelegenheit. Die ausführliche Herleitung und ein Zahlenbeispiel stehen unter [Richtungsgelegenheiten aus gerichteten Nachrichten](de-Funktionen#richtungsgelegenheiten-aus-gerichteten-nachrichten).
--- ---
## Funktionsweise ## Welche Frequenz wird verwendet?
Ein Spot wird generiert, wenn **beide** Bedingungen erfüllt sind: Ein DX-Cluster-Spot benötigt eine eindeutige Frequenz. KST4Contest verwendet dafür dieselbe QRG-Erkennung wie die Benutzerliste und die übrigen bandbezogenen Funktionen.
1. Eine **Richtungs-Warnung** wurde ausgelöst (Station macht ein Sked in die eigene Richtung). Vollständige Frequenzen bestimmen ihr Band direkt:
2. **QRG der Station ist bekannt** (aus dem Chat ausgelesen oder manuell eingetragen).
Der generierte Spot enthält: ```text
- Rufzeichen der Station 144.205
- Frequenz 432,088
- Spotterzeit 1296.338
10368.100
```
Das Logprogramm kann den Spot dann in der Bandkarte anzeigen und den TRX per Mausklick auf die Frequenz abstimmen. Relative Angaben enthalten dagegen nur den Frequenzanteil innerhalb eines Bandes:
```text
.205
,205
qrg 205
freq is 205
on 205
205 MHz
```
Eine nackte dreistellige Zahl wie `205` wird ohne Frequenzkontext nicht ausgewertet. Dasselbe gilt für `599`, `144` oder Formulierungen wie `worked 210 stations`. So verhindert KST4Contest, dass Signalrapporte, Bandnamen oder Zählwerte als scheinbar plausible QRG gespeichert und später an den Logger weitergegeben werden.
Bei einer relativen QRG wird das Band in dieser Reihenfolge bestimmt:
1. KST4Contest prüft, ob für denselben Absender innerhalb der letzten 30 Minuten bereits ein passender Bandkontext bekannt wurde.
2. Sind mehrere aktuelle Bänder bekannt, wird der zuletzt aktualisierte plausible Kontext verwendet.
3. Erst wenn kein geeigneter Stationskontext vorhanden ist, verwendet KST4Contest das unter **Fallback band for relative QRG detection** ausgewählte Band.
Beispiel:
```text
Globales Fallback: 144 MHz
Letzte vollständige QRG der Station: 432.088 MHz
Neue Chat-Angabe derselben Station: .100
Erkannte QRG: 432.100 MHz
DX-Cluster-Frequenz: 432100.0 kHz
```
Ohne den aktuellen 432-MHz-Kontext würde dieselbe Angabe mit dem globalen Fallback zu `144.100 MHz` ergänzt.
Die QRG-Erkennung läuft vor der Richtungs- und Spotprüfung. Nennt eine Station ihre Frequenz erstmals in der gerichteten Nachricht, die zugleich eine Richtungsgelegenheit auslöst, kann bereits der daraus erzeugte Spot diese Frequenz enthalten. War zuvor eine andere QRG der Station bekannt, wird sie durch die neu erkannte Angabe aktualisiert.
Das Fallback-Band ist eine globale Einstellung der QRG-Erkennung. Seine Wirkung ist nicht auf den DX-Cluster beschränkt. Konfiguration, unterstützte Bänder und weitere Folgen sind unter [Fallback-Band für relative QRG-Erkennung](de-Konfiguration#fallback-band-für-relative-qrg-erkennung) beschrieben.
--- ---
## Multi-Computer-Setup ## Einrichtung in KST4Contest
Wenn KST4Contest auf einem separaten Computer läuft (nicht auf dem Log-Computer): Öffne den Reiter **Notification** in den Preferences.
- Host im Logprogramm: IP des KST4Contest-Computers (nicht `127.0.0.1`) ![Benachrichtigungen und lokaler DX-Cluster-Server](client_settings_window_notification.png)
- Entspricht der Konfiguration der QSO-UDP-Broadcast-Pakete (siehe [Log-Synchronisation](de-Log-Synchronisation))
Konfiguriere anschließend:
1. **Enable the local DX Cluster server …** aktivieren.
2. Einen freien **TCP port** eintragen. Standard ist `8000`.
3. Unter **Fallback band for relative QRG detection** das passende Band auswählen.
4. Ein **Spotter callsign** festlegen.
Für das Spotter-Rufzeichen sollte nach Möglichkeit ein anderes Rufzeichen als das Contest-Rufzeichen verwendet werden. Einige Logger filtern Spots, die scheinbar von der eigenen Station stammen. Das Ergebnis wäre technisch korrekt erzeugt, aber in der Bandmap trotzdem unsichtbar eine besonders unproduktive Art von Erfolg.
Änderungen am Aktivierungsstatus und am TCP-Port werden während einer laufenden Chat-Verbindung sofort angewendet. Bei einem Portwechsel werden vorhandene DX-Cluster-Verbindungen getrennt und müssen vom Logger neu aufgebaut werden.
Die Einstellungen werden erst mit **Save Settings** dauerhaft in der `preferences.xml` gespeichert.
---
## Einrichtung im Logprogramm
Das Logprogramm wird als DX-Cluster-Client mit KST4Contest verbunden.
| Einstellung | KST4Contest und Logger auf demselben Computer | Logger auf einem anderen Computer |
|---|---|---|
| Host | `127.0.0.1` | IP-Adresse des KST4Contest-Computers |
| Port | In KST4Contest konfigurierter TCP-Port | In KST4Contest konfigurierter TCP-Port |
| Login | Beliebiges Rufzeichen, falls der Logger eines verlangt | Beliebiges Rufzeichen, falls der Logger eines verlangt |
| Passwort | Nicht erforderlich | Nicht erforderlich |
KST4Contest wertet den vom Logger gesendeten Login nicht zur Authentifizierung aus. Die Verbindung ist für ein lokales oder vertrauenswürdiges Stationsnetz vorgesehen.
Wenn der Logger auf einem anderen Computer läuft, muss dessen Verbindung durch die lokale Firewall des KST4Contest-Computers zugelassen werden. Der Port sollte nicht ohne weiteren Schutz aus dem Internet erreichbar sein.
Mehrere DX-Cluster-Clients können gleichzeitig verbunden werden. Ein erzeugter Spot wird an alle aktuell verbundenen Clients gesendet.
---
## Verbindung testen
Die Schaltfläche **Send test spot** erzeugt einen neutralen Testeintrag:
```text
Spotted callsign: DO5AMF
Comment: KST4CONTEST TEST
Frequency: .300 des konfigurierten Fallback-Bandes
```
Bei einem Fallback-Band von `144` erscheint der Spot daher auf ungefähr `144.300 MHz`.
Vor dem Test müssen drei Bedingungen erfüllt sein:
1. KST4Contest ist mit dem ON4KST-Chat verbunden.
2. Der lokale DX-Cluster-Server ist aktiviert.
3. Der DX-Cluster-Client des Logprogramms ist mit KST4Contest verbunden.
Fehlt die Client-Verbindung, zeigt KST4Contest eine entsprechende Meldung an. Ein erfolgreich ausgeführter Test bedeutet damit tatsächlich, dass mindestens ein Client den Spot erhalten hat.
---
## Inhalt eines erzeugten Spots
Ein Spot enthält:
- das konfigurierte Spotter-Rufzeichen,
- die normalisierte Frequenz,
- das Rufzeichen der erkannten Station,
- den Locator,
- Flugzeug-Scatter-Informationen, falls vorhanden,
- die aktuelle UTC-Zeit.
Wenn für die Station aktuelle Aircraft-Scatter-Informationen vorliegen, kann KST4Contest diese als zusätzliche AP-Information in den Kommentar des Spots aufnehmen.
---
## Wenn kein Spot erscheint
### Der Testspot kommt nicht im Logger an
Prüfe:
- Ist KST4Contest mit dem Chat verbunden?
- Ist der lokale DX-Cluster-Server aktiviert?
- Verwendet der Logger denselben TCP-Port?
- Verwendet der Logger bei lokalem Betrieb `127.0.0.1`?
- Blockiert eine Firewall die Verbindung?
- Ist im Logger das DX-Cluster-Fenster beziehungsweise die Bandmap aktiviert?
### Testspot funktioniert, aber reale Spots fehlen
Dann funktioniert die Verbindung grundsätzlich. Für die betreffende Chat-Situation war wahrscheinlich mindestens eine fachliche Bedingung nicht erfüllt:
- kein gerichteter Nachrichtenaustausch,
- fehlender Locator,
- Station außerhalb des maximalen QRB,
- Richtung außerhalb des konfigurierten Öffnungswinkels,
- keine erkannte Frequenz.
KST4Contest sendet absichtlich nicht jede gefundene Frequenz an den Logger. Andernfalls würde aus einer Arbeitserleichterung sehr schnell eine lokale Spot-Schleuder.
### Der Spot erscheint auf dem falschen Band
Prüfe zuerst, welche Frequenzen für die betreffende Station innerhalb der letzten 30 Minuten erkannt wurden. Bei einer relativen Angabe hat dieser Stationskontext Vorrang vor dem globalen Fallback.
Ist kein aktueller Stationskontext vorhanden, prüfe die Auswahl unter **Fallback band for relative QRG detection**. Das Fallback wird nur benötigt, wenn sich das Band weder aus einer vollständigen Frequenz noch aus dem aktuellen Kontext des Absenders ergibt.
### Der Spot wird vom Logger ausgeblendet
Verwende ein Spotter-Rufzeichen, das nicht mit dem eigenen Contest-Rufzeichen identisch ist. Abhängig vom Logger können eigene Spots gefiltert oder besonders behandelt werden.
--- ---
## Getestete Logprogramme ## Getestete Logprogramme
- **UCXLog** ✓ Die Schnittstelle wurde mit folgenden Logprogrammen verwendet:
- **N1MM+** ✓
Weitere Testergebnisse sind willkommen bitte per E-Mail an DO5AMF melden. - UCXLog
- N1MM+
Weitere Logger können funktionieren, wenn sie eine normale TCP-Verbindung zu einem DX-Cluster-Server unterstützen.
+456 -68
View File
@@ -2,57 +2,209 @@
> 🇬🇧 [English version](en-Features) | 🇩🇪 Du liest gerade die deutsche Version > 🇬🇧 [English version](en-Features) | 🇩🇪 Du liest gerade die deutsche Version
Übersicht aller Hauptfunktionen von KST4Contest. Dieses Kapitel beschreibt die wichtigsten Funktionen von KST4Contest, ihre Herleitung und die Grenzen der daraus gewonnenen Informationen.
--- ---
## Richtungsgelegenheiten aus gerichteten Nachrichten
## Sked-Richtungs-Hervorhebung Im ON4KST-Chat ist sichtbar, welche Station eine Nachricht an welche andere Station richtet. Eine tatsächliche Antennenrichtung wird dabei nicht übertragen. Für den Contestbetrieb lässt sich aus einer solchen Nachricht trotzdem eine brauchbare Annahme ableiten: Wer einen Sked anfragt, beantwortet oder vorbereitet, richtet seine Antenne normalerweise zumindest ungefähr auf die angesprochene Station.
Eine der Kernfunktionen: Wenn eine Station ein Sked in die **eigene Richtung** sendet, wird sie in der Benutzerliste **grün und fett** hervorgehoben. KST4Contest wertet deshalb gerichtete Nachrichten zwischen zwei anderen Stationen aus. Die Nachricht muss nicht ausdrücklich als Sked gekennzeichnet sein. Entscheidend sind der Absender, der Empfänger und deren Locator.
### Wie funktioniert das? ### Wie wird die Richtung hergeleitet?
Die Berechnung basiert auf folgender Logik: Angenommen, Station A schreibt eine gerichtete Nachricht an Station B:
- Wenn Station A eine Sked-Anfrage an Station B sendet, wird angenommen, dass A ihre Antenne auf B ausrichtet. 1. KST4Contest berechnet die Richtung von Station A zu Station B.
- Wenn die daraus resultierende Richtung von A zur eigenen Station innerhalb des halben Öffnungswinkels der eigenen Antenne liegt, wird A hervorgehoben. 2. Diese Richtung wird als wahrscheinliche Antennenrichtung von Station A verwendet.
3. Anschließend wird die Richtung von Station A zur eigenen Station berechnet.
4. Die Winkeldifferenz wird mit der Hälfte des konfigurierten Antennen-Öffnungswinkels verglichen.
5. Zusätzlich muss Station A innerhalb des konfigurierten maximalen QRB liegen.
**Beispiel** (Öffnungswinkel 69°, Halbwinkel 34,5°): Ein eingetragener Öffnungswinkel von `70°` ergibt damit einen angenommenen Korridor von jeweils `35°` links und rechts der Richtung von Station A zu Station B.
| Situation | Ergebnis für DO5AMF in JN49 | | Beispiel | Ergebnis |
|---|---| |---|---|
| Sked von F5FEN → DM5M | ✅ Hervorhebung (F5FEN zeigt Richtung DM5M, das liegt nahe JN49) | | Richtung A → B: `120°`, Richtung A → eigene Station: `145°` | Winkeldifferenz `25°`: Richtungsgelegenheit erkannt |
| Sked von DM5M → F5FEN | ✅ Hervorhebung (DM5M antwortet in Richtung F5FEN) | | Richtung A → B: `120°`, Richtung A → eigene Station: `165°` | Winkeldifferenz `45°`: außerhalb des angenommenen Korridors |
| F1DBN ist unbeteiligt | Keine Hervorhebung | | Locator von A oder B fehlt | Keine Richtungsberechnung möglich |
| DO5AMF/P (anderer Standort) | ❌ Keine Hervorhebung für Sked-Antwort | | A liegt außerhalb des maximalen QRB | Keine Richtungsgelegenheit |
Die Berechnung berücksichtigt keine topografischen Wegberechnungen das ist eine bewusste Vereinfachung. Möglicherweise wird das in einer späteren Version ergänzt. ### Was wird in der Benutzerliste angezeigt?
> Konfiguration: [Konfiguration Antennen-Öffnungswinkel](Konfiguration#antennen-öffnungswinkel-antenna-beamwidth) Wird eine Richtungsgelegenheit erkannt, erscheint das Rufzeichen des Absenders in der Benutzerliste grün und fett. Im Evening-Modus wird dafür ein helleres Grün verwendet. Der Empfänger der Nachricht wird nicht allein deshalb markiert; eine Antwort in Gegenrichtung wird als eigene Nachricht und damit als neuer Fall berechnet.
![Erkannte Richtungsgelegenheit in der Benutzerliste](direction_opportunity_highlight.png)
Im Bild sendete DF0GEB eine gerichtete Nachricht an DN9APW und bekam eine Antwort. KST4Contest erkannte die Richtungsgelegenheit und markierte DN9APW in der Benutzerliste.
Zur Verdeutlichung ist die MAP eingeblendet. Ich stehe als Empfänger zwischen beiden Stationen und bekomme deswegen die Warnung.
Die Markierung bleibt fünf Minuten ab der letzten passenden Nachricht sichtbar. Eine weitere passende Nachricht derselben Station beginnt diesen Zeitraum erneut. Sendet die Station vorher eine gerichtete Nachricht, deren Richtung die Bedingungen nicht erfüllt, wird die Markierung unmittelbar entfernt.
Ist die einfache Soundausgabe aktiviert, gibt KST4Contest beim erstmaligen Erkennen der Richtungsgelegenheit zusätzlich einen kurzen Hinweis aus. Solange die Station bereits markiert ist, wird derselbe Hinweis nicht mit jeder weiteren passenden Nachricht wiederholt.
### Was bedeutet die Markierung und was nicht?
Die Berechnung ist eine geometrische Herleitung. Sie beweist nicht, dass Station A ihre Antenne tatsächlich auf Station B ausgerichtet hat. Ebenso wenig berücksichtigt sie Gelände, aktuelle Ausbreitungsbedingungen, die reale Antennencharakteristik der fremden Station oder deren Rotatorposition.
ON4KST liefert keinen individuellen Öffnungswinkel für die fremde Station. KST4Contest verwendet deshalb den für die eigene Antenne konfigurierten Wert auch als Näherung für Station A. Ein zu großer Wert erzeugt entsprechend mehr mögliche Richtungsgelegenheiten, ein zu kleiner Wert kann brauchbare Situationen übersehen.
Im Klartext: Die grüne Markierung ist ein begründeter Hinweis auf eine mögliche Gelegenheit. Sie ist weder eine Ausbreitungsvorhersage noch eine Garantie für ein QSO.
Konfiguration:
- [Antennen-Öffnungswinkel](de-Konfiguration#antennen-öffnungswinkel-antenna-beamwidth)
- [Standard-Maximum-QRB](de-Konfiguration#standard-maximum-qrb)
--- ---
## Sked-Richtungs-Spots (Integrierter DX-Cluster) ## Weitergabe als DX-Cluster-Spot
Ab **v1.23**: Richtungs-Warnungen werden als DX-Cluster-Spots an das Logprogramm weitergeleitet, wenn eine QRG bekannt ist. Details: [DX-Cluster-Server](de-DX-Cluster-Server). Seit Version 1.23 kann KST4Contest eine erkannte Richtungsgelegenheit an den DX-Cluster-Client eines Logprogramms weitergeben. Dafür muss der lokale DX-Cluster-Server aktiviert und für den Absender eine verwertbare Frequenz bekannt sein.
Die Frequenz kann bereits aus einer früheren Nachricht stammen oder erstmals in der aktuell auslösenden Nachricht stehen. In beiden Fällen steht sie der Spot-Prüfung zur Verfügung. KST4Contest überträgt damit nicht jede im Chat gefundene QRG, sondern nur Frequenzen, die mit einer geometrisch passenden gerichteten Nachricht zusammenfallen.
Die Fünf-Minuten-Markierung und der DX-Cluster-Spot beruhen auf derselben Richtungsberechnung, haben aber einen unterschiedlichen Lebenszyklus: Die Markierung bleibt vorübergehend in der Benutzerliste sichtbar. Der Spot wird unmittelbar beim Verarbeiten der passenden Nachricht erzeugt.
Einrichtung, Frequenzbehandlung und Grenzen: [Integrierter DX-Cluster-Server](de-DX-Cluster-Server).
--- ---
## QRG-Erkennung (QRG Reading) ## QRG-Erkennung
KST4Contest verarbeitet jede Chat-Nachricht und extrahiert automatisch **Frequenzangaben**. Diese werden in der Benutzerliste in der **QRG-Spalte** angezeigt. Im ON4KST-Chat werden Frequenzen selten einheitlich geschrieben. Eine Station nennt beispielsweise zuerst `432.088`, später nur noch `.100` und in einer weiteren Nachricht `qrg 120`. Für einen Menschen ist der Zusammenhang meistens klar. Ein Programm muss dagegen unterscheiden, ob `120` eine Frequenz, eine Zeitangabe, eine Entfernung oder etwas völlig anderes bedeutet.
Erkannte Formate: `144.205`, `432.088`, `.205` (mit konfigurierter Bandannahme), etc. KST4Contest wertet deshalb den Text jeder öffentlichen und gerichteten Chat-Nachricht aus. Eine erkannte QRG wird dem Absender zugeordnet und in der **QRG-Spalte** der Benutzerliste angezeigt. Die Spalte enthält die zuletzt erkannte Frequenz und stellt mindestens drei Nachkommastellen dar. Ein intern als `144.21` gespeicherter Wert erscheint damit als `144.210`.
**Nutzen**: Ohne nachzufragen kann man direkt auf die QRG einer Station schauen und entscheiden, ob eine Verbindung möglich ist. ### Welche Angaben werden erkannt?
| Schreibweise | Beispiel | Verarbeitung |
|---|---|---|
| Vollständige Frequenz | `144.210`, `432,088`, `10368.100` | Das Band ergibt sich direkt aus der Frequenz. |
| Relative Frequenz mit Punkt oder Komma | `.210`, `,088` | Das Band wird aus dem Stationskontext oder dem konfigurierten Fallback ergänzt. |
| Dreistellige Frequenz mit Textkontext | `qrg 210`, `freq is 210`, `on 210`, `210 MHz` | Die Zahl wird als relative Frequenz behandelt. |
| Dreistellige Zahl ohne Frequenzkontext | `210`, `599`, `144` | Die Zahl wird absichtlich nicht als QRG übernommen. |
Die letzte Einschränkung verhindert plausible, aber falsche Ergebnisse. Mit einem Fallback von `144 MHz` ließe sich ein Signalrapport `599` technisch problemlos zu `144.599 MHz` zusammensetzen. Das Ergebnis wäre formal gültig und fachlich trotzdem Unsinn.
### Wie wird das Band einer relativen QRG bestimmt?
KST4Contest verwendet folgende Reihenfolge:
1. Wurde für denselben Absender innerhalb der letzten 30 Minuten bereits eine passende vollständige Frequenz erkannt, verwendet KST4Contest deren Band.
2. Sind mehrere aktuelle Bänder bekannt, wird der zuletzt aktualisierte plausible Bandkontext verwendet.
3. Fehlt ein geeigneter Stationskontext, verwendet KST4Contest das unter **Fallback band for relative QRG detection** ausgewählte Band.
Beispiel: Das globale Fallback steht auf `144 MHz`. Eine Station nennt zunächst `432.088` und schreibt wenige Minuten später `.100`. KST4Contest ergänzt nicht das globale Fallback, sondern den aktuelleren Stationskontext. Das Ergebnis ist `432.100 MHz`. Schreibt eine andere Station ohne vorherige Bandinformation `.100`, wird daraus `144.100 MHz`.
Das Fallback-Band ist damit tatsächlich nur der letzte Ausweg. Es wird aus den von KST4Contest unterstützten Bandwerten ausgewählt und wirkt auf die gesamte QRG-Erkennung nicht nur auf den integrierten DX-Cluster.
### Wofür wird die erkannte QRG verwendet?
Die zuletzt erkannte Frequenz erscheint in der Benutzerliste. Der zugehörige Bandkontext kann außerdem in weitere Funktionen einfließen, beispielsweise in:
- die Erkennung aktiver Bänder einer Station,
- den Chatmember-Score und die Prioritätslisten,
- Band-Upgrade-Hinweise nach einem Logeintrag,
- die Frequenzwahl bei Skeds,
- einen DX-Cluster-Spot aus einer erkannten Richtungsgelegenheit.
Steht die QRG erstmals in der Nachricht, die zugleich eine Richtungsgelegenheit auslöst, wird sie vor der Richtungs- und Spotprüfung verarbeitet. Der daraus erzeugte Spot kann deshalb bereits die Frequenz dieser Nachricht verwenden.
Die Erkennung bleibt eine Textauswertung. KST4Contest kann nicht beweisen, dass die Station noch auf der genannten Frequenz arbeitet oder ob sich eine mehrdeutige Angabe auf einen anderen Zusammenhang bezieht. Genau deshalb werden nackte dreistellige Zahlen ohne Frequenzkontext nicht mehr übernommen.
Konfiguration und unterstützte Fallback-Bänder: [Fallback-Band für relative QRG-Erkennung](de-Konfiguration#fallback-band-für-relative-qrg-erkennung).
Verwendung in der Bandmap eines Logprogramms: [Integrierter DX-Cluster-Server](de-DX-Cluster-Server).
--- ---
## Gearbeitete Rufzeichen, neue Bänder und neue Großfelder
## Worked-Markierung KST4Contest unterscheidet drei Informationen, die im Contest ähnlich aussehen können, aber unterschiedliche Fragen beantworten:
Gearbeitete Stationen werden in der Benutzerliste visuell markiert pro Band. Grundlage ist die [Log-Synchronisation](de-Log-Synchronisation) via UDP oder Simplelogfile. 1. Wurde dieses Rufzeichen bereits gearbeitet?
2. Wurde dieses Rufzeichen auf einem bestimmten Band gearbeitet?
3. Wurde das vierstellige Maidenhead-Großfeld bereits gearbeitet möglicherweise mit einer anderen Station?
Diese Trennung ist notwendig. Ein bereits gearbeitetes Rufzeichen kann auf einem anderen Band weiterhin interessant sein. Umgekehrt kann eine noch nicht gearbeitete Station in einem Großfeld liegen, das bereits im Log steht.
### Worked-Informationen aus dem Log
Die [Log-Synchronisation](de-Log-Synchronisation) übernimmt neue QSOs aus dem Logprogramm. Welche Informationen dabei zur Verfügung stehen, hängt von der verwendeten Schnittstelle ab:
- Der dateibasierte Simplelogfile-Interpreter erkennt nur das Rufzeichen. Er kann deshalb lediglich den globalen Worked-Status setzen.
- Die QSO-UDP-Schnittstellen und der Win-Test-Netzwerk-Listener können zusätzlich das Band übernehmen.
- Enthält das Logpaket einen gültigen Locator, speichert KST4Contest außerdem das gearbeitete vierstellige Großfeld für dieses Band.
Fehlt eine Information im Logpaket, wird sie nicht geraten. Ein QSO ohne Locator erzeugt deshalb keinen Großfeld-Eintrag; ein Simplelogfile-Treffer erzeugt keine bandbezogene Worked-Markierung.
### Bedeutung der Bandspalten
Unter der gemeinsamen Spalte **worked** erscheinen nur die Bänder, die unter **Station → my station uses …** aktiviert wurden. Die Zellen verwenden bewusst kurze Kennzeichen:
| Anzeige | Bedeutung |
|---|---|
| `X` | Das Rufzeichen wurde auf diesem Band gearbeitet. |
| `a` | Die Station bietet dieses Band an, das Band ist noch nicht gearbeitet und das Rufzeichen wurde bisher auf keinem Band gearbeitet. |
| `B+` | Die Station bietet dieses Band an und das Band ist noch nicht gearbeitet. Das Rufzeichen wurde bereits auf einem anderen Band gearbeitet. Ist die getrennte `a`-Anzeige deaktiviert, wird auch ein vollständig neues Rufzeichen als `B+` dargestellt. |
| `o` | Das vierstellige Großfeld der Station wurde auf diesem Band bereits gearbeitet unabhängig vom Rufzeichen. |
| leer | Für dieses Band liegt keine passende Information vor. Das ist nicht gleichbedeutend mit „nicht QRV“. |
Das `o` ist eine unabhängige Zusatzinformation und kann deshalb mit den anderen Kennzeichen kombiniert werden. Möglich sind beispielsweise `Xo`, `ao` oder `B+o`. Ein einzelnes `o` bedeutet: Das Großfeld wurde auf diesem Band bereits gearbeitet, für das angezeigte Rufzeichen liegt aber weder eine Worked-Markierung noch eine aktuelle Bandmöglichkeit vor.
![Bandbezogener Worked-Status und Worked-Großfelder](worked_band_status.png)
### Wie entsteht eine Bandmöglichkeit?
KST4Contest zeigt `a` oder `B+` nur an, wenn sich eine noch offene gemeinsame Bandmöglichkeit herleiten lässt. Dafür werden folgende Informationen zusammengeführt:
1. die in den Stationseinstellungen aktivierten eigenen Bänder,
2. höchstens 30 Minuten alte QRG-Erkennungen der Gegenstation,
3. eindeutige Bandangaben im Namensfeld der Gegenstation,
4. die pro Band gespeicherten Worked-Markierungen und
5. manuell gesetzte NOT-QRV-Markierungen.
Aktive Chat-Einträge mit demselben normalisierten Rufzeichen werden gemeinsam ausgewertet. Das ist insbesondere bei mehreren Chat-Kategorien oder unterschiedlichen sichtbaren Rufzeichenvarianten wichtig. Eine eindeutige Bandangabe im Namensfeld bleibt dabei so lange nutzbar, wie der betreffende Chat-Eintrag aktiv ist; eine aus einer Nachricht erkannte QRG läuft nach 30 Minuten aus.
Anschließend werden nur die Bänder berücksichtigt, die an der eigenen Station aktiviert, für die Gegenstation bekannt und noch nicht gearbeitet sind. Ein manuelles NOT-QRV-Tag übersteuert die automatisch erkannten Hinweise. Die Chat-Kategorie allein reicht dagegen nicht als Nachweis, dass eine einzelne Station auf einem bestimmten Band QRV ist.
Die globale Worked-Markierung entscheidet nicht darüber, ob eine Bandmöglichkeit besteht. Sie unterscheidet in der Darstellung lediglich zwischen `a` und `B+`. Die eigentliche Bandprüfung arbeitet mit den bandbezogenen Worked-Informationen.
### Bedeutung von `wkdany`
Die Unterspalte **wkdany** fasst den globalen Rufzeichen- und Großfeldstatus zusammen:
| Anzeige | Bedeutung |
|---|---|
| leer | Weder das Rufzeichen noch das vierstellige Großfeld wurden gearbeitet. |
| `x` | Das Rufzeichen wurde auf mindestens einem Band gearbeitet. |
| `o` | Das vierstellige Großfeld wurde auf mindestens einem Band gearbeitet. |
| `xo` | Rufzeichen und Großfeld wurden bereits gearbeitet. |
`wkdany` ist eine bandunabhängige Übersicht. Das kleine `x` darf daher nicht mit dem großen `X` in einer Bandspalte verwechselt werden. Der globale Status wird für die Anzeige und den globalen **wkd**-Filter verwendet, nicht als Ersatz für bandbezogene Worked-Informationen.
### NOT-QRV-Markierungen
Teilt eine Station mit, dass sie auf einem bestimmten Band nicht QRV ist, kann dies im **Further Info**-Bereich der ausgewählten Station markiert werden:
1. Station in der Benutzerliste auswählen.
2. Im Bereich **Not QRV** das betreffende Band aktivieren.
3. **tag not qrv all** nur verwenden, wenn die Station auf keinem der unterstützten Bänder angefragt werden soll.
Angezeigt werden die einzelnen NOT-QRV-Schalter der Bänder, die für die eigene Station aktiviert sind. **tag not qrv all** setzt dagegen alle unterstützten Bänder, auch wenn einzelne davon momentan nicht in der Benutzeroberfläche eingeblendet sind. Die Markierung wird bandbezogen unter dem normalisierten Rufzeichen gespeichert und auf dessen aktive Chat-Varianten übertragen.
![Bandbezogene NOT-QRV-Markierungen im Further-Info-Bereich](not_qrv_controls.png)
NOT-QRV ist eine manuelle Korrektur und hat deshalb Vorrang vor automatisch erkannten QRGs und Bandangaben im Namensfeld. Das betreffende Band wird nicht mehr als `a` oder `B+` angeboten, vom **New bands**-Filter nicht als Gelegenheit gewertet und von den zugehörigen Bandfiltern ausgeblendet.
Im Klartext: Ein erkannter Hinweis bedeutet „wahrscheinlich auf diesem Band aktiv“. Ein manuelles NOT-QRV-Tag bedeutet „für unsere weitere Auswahl nicht auf diesem Band anfragen“. Diese Entscheidung soll nicht durch die nächste erkannte Zahl wieder aufgehoben werden.
### Speicherung und Lebensdauer
Worked-, NOT-QRV- und Großfeldinformationen werden in der internen SQLite-Datenbank gespeichert und beim nächsten Start wieder geladen. Die Einträge laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist deshalb normalerweise nicht erforderlich.
Ein manueller Reset unter **Workedstn database** entfernt sämtliche Worked-Markierungen, NOT-QRV-Tags und gespeicherten Worked-Großfelder. Die bekannten Rufzeichenzeilen bleiben dabei in der Datenbank erhalten. Einzelheiten: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
Vor jedem Contest die Datenbank zurücksetzen: [Konfiguration Worked Station Database Settings](Konfiguration#worked-station-database-settings).
--- ---
@@ -83,9 +235,19 @@ Stationen jenseits einer maximalen Entfernung ausblenden. Schaltfläche **„Sho
--- ---
## Worked- und NOT-QRV-Filter ## Filter für Worked-Status, neue Bänder und neue Großfelder
Toggle-Buttons (einer pro Band) zum Ausblenden bereits gearbeiteter Stationen und/oder NOT-QRV-markierter Stationen. Der Filter wirkt **sofort** ohne manuelles Neu-Aktivieren (ab v1.22 live). Die Filter oberhalb der Benutzerliste greifen auf dieselben Informationen zurück wie die Worked-Spalten:
- **wkd** blendet Rufzeichen aus, die auf mindestens einem Band gearbeitet wurden.
- Die einzelnen Band-Schaltflächen blenden Stationen aus, wenn das Rufzeichen auf diesem Band bereits gearbeitet oder für dieses Band manuell als NOT QRV markiert wurde.
- **New bands** zeigt nur Stationen, für die mindestens ein eigenes aktiviertes, noch nicht gearbeitetes Band bekannt ist. Berücksichtigt werden aktuelle QRG-Erkennungen und Bandangaben im Namensfeld; NOT-QRV hat Vorrang.
- **Only new grids** zeigt nur Stationen, deren vierstelliges Großfeld auf noch keinem Band gearbeitet wurde. Stationen ohne auswertbaren Locator erfüllen den Filter nicht.
- **Grid color** verändert die Liste nicht. Ist die Funktion aktiv, wird das QRA-Feld eines bereits gearbeiteten Großfelds dezent dunkler dargestellt. Neue Großfelder behalten die normale Tabellenfarbe.
Mehrere aktivierte Filter werden gemeinsam angewendet. Eine Station bleibt nur sichtbar, wenn sie alle gewählten Bedingungen erfüllt. Die Filter reagieren unmittelbar auf neue Logeinträge und geänderte NOT-QRV-Markierungen.
Bedienung und Aufbau der Filterleiste: [Benutzeroberfläche Filter](de-Benutzeroberflaeche#filter).
--- ---
@@ -109,6 +271,22 @@ KST4Contest erkennt solche Nachrichten, die das eigene Rufzeichen enthalten, und
--- ---
## Automatische Antworten auf Privatnachrichten (ab v1.25)
Nicht jede im ON4KST-Chat eingeloggte Station nimmt am gerade laufenden Contest teil. Trotzdem werden Sked-Anfragen während größerer Contests teilweise unkoordiniert und in großer Zahl an erreichbare Rufzeichen verteilt. Ohne automatische Antwort müssten diese Stationen immer wieder von Hand erklären, dass sie nicht mitfunken oder keine Skeds fahren.
KST4Contest kann darauf mit einem vorher festgelegten Text reagieren. Die eingehende Privatnachricht bleibt dabei sichtbar; sie wird weder blockiert noch verworfen. Davon getrennt lässt sich eine QRG-Antwort aktivieren, die auf typische Fragen wie `qrg?`, `freq?` oder `pse qrg` reagiert.
Bei zwei gleichzeitig geöffneten Chat-Kategorien bleibt der Zusammenhang erhalten: Die Antwort wird in der Kategorie der eingegangenen Nachricht gesendet. Eine QRG-Anfrage erhält außerdem nur die QRG dieser Kategorie und nicht eine Liste aller konfigurierten Frequenzen.
Automatische Antworten benötigen Grenzen. KST4Contest versieht sie daher mit `[KST4C Automsg]`, ignoriert entsprechend gekennzeichnete Nachrichten bei der allgemeinen und QRG-bezogenen Antwort und begrenzt weitere Antworten an dieselbe Station in derselben Kategorie auf eine Nachricht innerhalb von zwei Minuten. Der Schutz gilt gemeinsam für beide Antwortarten.
Im Klartext: Die Funktion verhindert keine Massenanfragen. Sie verhindert aber, dass der Empfänger jede davon einzeln mit derselben Absage beantworten muss. Sie soll keine Unterhaltung simulieren und erst recht keine endlose Diskussion mit einem zweiten automatischen Client beginnen.
Konfiguration, erkannte QRG-Anfragen und genaue Kategorienzuordnung: [Konfiguration Messagehandling Settings](de-Konfiguration#messagehandling-settings-ab-v125).
---
## Multi-Channel-Login (ab v1.26) ## Multi-Channel-Login (ab v1.26)
Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Beide Chats werden parallel überwacht. Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Beide Chats werden parallel überwacht.
@@ -135,39 +313,114 @@ 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. 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.
- **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.
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.
## QSO-Sniffer (ab v1.31) ### Sked anlegen
Der QSO-Sniffer überwacht den Chat auf Nachrichten von einer konfigurierbaren Rufzeichen-Liste und leitet diese automatisch in das **PM-Fenster** weiter. So gehen keine relevanten Nachrichten im allgemeinen Chat-Rauschen unter. 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)
Für ausgewählte Rufzeichen kann KST4Contest gerichtete Nachrichten zusätzlich in der PM-Tabelle anzeigen. Dabei werden sowohl Nachrichten berücksichtigt, die das überwachte Rufzeichen sendet, als auch Nachrichten, die an dieses Rufzeichen gerichtet sind.
Die Nachricht bleibt gleichzeitig in ihrer ursprünglichen Tabelle erhalten und wird im PM-Fenster mit Absender und Empfänger als überwachte Kommunikation gekennzeichnet.
Konfiguration: [Konfiguration Sniffer-Einstellungen](de-Konfiguration#sniffer-einstellungen-ab-v131) Konfiguration: [Konfiguration Sniffer-Einstellungen](de-Konfiguration#sniffer-einstellungen-ab-v131)
--- ---
## 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. - neue QSOs einschließlich Band- und gegebenenfalls Locatorinformation übernehmen,
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz wird aus Win-Test-UDP-Paketen ausgewertet und befüllt die `MYQRG`-Variable. - die aktuelle QRG aus Win-Test-STATUS-Paketen auswerten und
- **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. - 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) ## PSTRotator-Interface (ab v1.31, vollständig ab v1.40)
@@ -177,50 +430,185 @@ Konfiguration: [Konfiguration PSTRotator-Einstellungen](de-Konfiguration#pst
--- ---
## Band-Alert bei neuen QSOs (ab v1.40) ## Band-Upgrade-Hinweis nach einem Logeintrag
Wenn eine Station geloggt wird, prüft KST4Contest automatisch, ob diese Station im Chat weitere aktive Bänder angezeigt hat, auf denen man selbst ebenfalls QRV ist. Falls ja, erscheint ein **Hinweis-Alert**, damit keine Multi-Band-Möglichkeit übersehen wird. Meldet UCXLog oder Win-Test einen neuen Logeintrag mit Bandinformation, prüft KST4Contest, ob die gearbeitete Station noch ein weiteres gemeinsames Band anbietet.
Die Herleitung verwendet dieselben Regeln wie `a`, `B+` und der Filter **New bands**: aktivierte eigene Bänder, aktuelle QRG-Erkennungen, Bandangaben im Namensfeld, bandbezogene Worked-Markierungen und NOT-QRV-Tags. Ausgewertet werden die aktiven Chat-Varianten desselben normalisierten Rufzeichens.
Bleibt mindestens ein gemeinsames, noch nicht gearbeitetes Band übrig, erscheint für ungefähr zwölf Sekunden ein blinkender Hinweis mit Rufzeichen und den betreffenden Bändern, beispielsweise `BAND+ DL0ABC 432, 1296`. Der Tooltip zeigt zusätzlich die bei der Entscheidung berücksichtigten aktivierten, gearbeiteten und als NOT QRV markierten Bänder. Ist die allgemeine Soundausgabe eingeschaltet, wird außerdem ein kurzer Hinweiston abgespielt.
Der einfache Simplelogfile-Interpreter kann diesen Hinweis nicht zuverlässig auslösen, weil er keine Bandinformation für das gerade geloggte QSO liefert.
Konfiguration: [Band-Upgrade-Hinweis nach einem Logeintrag](de-Konfiguration#band-upgrade-hinweis-nach-einem-logeintrag).
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).
---
## Prioritätsscore und Prioritätsliste (ab v1.40)
### Warum wird überhaupt ein Score benötigt?
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.
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.
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)
--- ---
## Worked-Tag-Lebensdauer (ab v1.40) ## AP- und Sked-Timeline
Gearbeitete Stationen werden nach **3 Tagen** automatisch aus der Datenbank entfernt. Ein manuelles Zurücksetzen der Worked-Datenbank vor jedem Contest ist damit nicht mehr zwingend notwendig die Datenbank hält sich selbst aktuell. 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:
--- - Wann entsteht voraussichtlich eine interessante AP-Gelegenheit?
- Welcher bereits vereinbarte Sked nähert sich unabhängig davon?
## Chatmember Score-System / Prioritätsliste (ab v1.40) Weiter in der Zukunft liegende Ereignisse erscheinen rechts. Mit ablaufender Zeit wandern sie nach links in Richtung des aktuellen Zeitpunkts.
KST4Contest berechnet automatisch eine **Prioritätsbewertung** für jeden aktiven Chatmember. Der Score setzt sich zusammen aus: ![AP-Kandidaten und Skeds in der Timeline](sked_timeline.png)
- Antennenrichtung der Gegenstation (zeigt sie auf mich?) ### AP-Kandidaten
- QRB (Entfernung)
- Aktivitätszeit und Nachrichtenanzahl
- Aktive Bänder und Frequenzen
- AP-Verfügbarkeit (AirScout)
- Sked-Richtung
- Sked-Erfolgsrate und Skedfail-Markierungen
Die Top-Kandidaten werden in einer eigenen Prioritätsliste hervorgehoben und helfen, im Contest-Stress die wichtigsten Stationen nicht zu übersehen. 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.
Stationen, bei denen ein Sked gescheitert ist, können über den **Skedfail-Button** im FurtherInfo-Panel markiert werden das senkt ihren Score vorübergehend. Die Farbe des AP-Symbols kennzeichnet das Reflexionspotenzial:
--- | Farbe | Reflexionspotenzial |
|---|---:|
| Magenta | mindestens 95 % |
| Rot | mindestens 75 % |
| Gelb | mindestens 50 % |
| Blau | unter 50 % |
## AP-Timeline (ab v1.40) Die Farbe ist keine QSO-Wahrscheinlichkeit. Sie gibt den von AirScout übernommenen Wert für die berechnete Reflexionsgeometrie wieder.
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: 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.
- Bevorzugt werden APs mit dem **höchsten Reflexionspotenzial** (nicht unbedingt die schnellste Ankunft). ### Skeds
- Stationen, auf die die eigene Antenne nicht zeigt, werden **transparent** dargestellt.
So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und über welche Flugzeuge erreichbar sein werden. 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 ## Intervall-Beacon
Automatische CQ-Meldungen im öffentlichen Kanal in konfigurierbarem Intervall. Empfohlene Verwendung mit der Variable `MYQRG` für aktuelle Frequenzangabe. Details: [Konfiguration Beacon Settings](Konfiguration#beacon-settings-automatischer-beacon). KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Globale Variablen wie `MYQRG`, `SECONDQRG` oder `MYLOCATOR` werden unmittelbar vor jeder Aussendung aktualisiert.
Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim Absuchen oder häufigen Wechseln der QRG sollte er ausgeschaltet werden, damit keine inzwischen falsche Frequenz verbreitet wird. Details: [Konfiguration Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
--- ---
@@ -234,7 +622,7 @@ Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synch
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht. Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
Dieses Fenster kann miniaturisiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF. Dieses Fenster kann minimiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
--- ---
+289 -33
View File
@@ -25,17 +25,40 @@ Eigenes Rufzeichen und Maidenhead-Locator (6-stellig, z. B. `JN49IJ`) eintragen.
### Aktivierte Bänder ### Aktivierte Bänder
Über die **„my station uses band"**-Checkboxen werden die aktiven Bänder ausgewählt. Nur für ausgewählte Bänder erscheinen Schaltflächen und Tabellenzeilen in der Benutzeroberfläche. Nach Änderungen muss die Software neu gestartet werden. Über die Checkboxen **My station uses …** wird festgelegt, auf welchen Bändern die eigene Station im aktuellen Setup arbeiten kann. Unterstützt werden 50 MHz, 70 MHz, 144 MHz, 432 MHz, 1296 MHz, 2320 MHz, 3400 MHz, 5760 MHz und 10 GHz.
Die Auswahl steuert nicht nur die sichtbaren Bandspalten. Sie wird außerdem verwendet für:
- die bandbezogenen Worked- und NOT-QRV-Filter,
- die im **Further Info**-Bereich sichtbaren NOT-QRV-Schalter,
- die Herleitung von `a`- und `B+`-Bandmöglichkeiten,
- den Filter **New bands**,
- den Band-Upgrade-Hinweis nach einem Logeintrag und
- bandbezogene Prioritäts- und Reachability-Funktionen.
Nach einer Änderung **Save Settings** verwenden und KST4Contest neu starten. Die Bandspalten und mehrere zugehörige Bedienelemente werden beim Aufbau der Benutzeroberfläche erzeugt und deshalb nicht vollständig in der laufenden Sitzung ergänzt oder entfernt.
### Antennen-Öffnungswinkel (Antenna Beamwidth) ### Antennen-Öffnungswinkel (Antenna Beamwidth)
Einen realistischen Wert für den Öffnungswinkel der eigenen Antenne eintragen (in Grad). Dieser Wert wird für die [Sked-Richtungs-Hervorhebung](Funktionen#sked-richtungs-hervorhebung) verwendet. Ein Testwert von 50° hat sich bewährt; DM5M nutzt Quads mit 69°. Trage den vollständigen horizontalen Öffnungswinkel der eigenen Antenne in Grad ein. KST4Contest verwendet jeweils die Hälfte dieses Werts links und rechts der gewählten beziehungsweise hergeleiteten Antennenrichtung. Ein eingetragener Wert von `70°` entspricht daher einem Korridor von `±35°`.
> **Keinesfalls** Fantasy-Werte eintragen die Richtungsberechnungen werden sonst unbrauchbar. Der Wert wird an mehreren Stellen verwendet:
- für den QTF-Filter der Benutzerliste,
- für die Darstellung des eigenen Antennenkorridors,
- als angenommener Öffnungswinkel einer fremden Station bei der [Herleitung von Richtungsgelegenheiten](de-Funktionen#richtungsgelegenheiten-aus-gerichteten-nachrichten).
Der letzte Punkt ist bewusst eine Näherung. ON4KST überträgt weder die verwendete Antenne noch deren Öffnungswinkel. KST4Contest verwendet deshalb den eigenen Wert als praktikable Annahme für die Gegenstation.
Wähle einen realistischen Wert. Ein zu großer Öffnungswinkel erzeugt viele geometrische Treffer, die praktisch kaum noch eine Aussage haben. Ein zu kleiner Wert kann dagegen brauchbare Richtungsgelegenheiten ausblenden.
### Standard-Maximum-QRB ### Standard-Maximum-QRB
Maximale Entfernung (in km), für die Richtungs-Warnungen ausgelöst werden sollen. Realistischer Wert für DM5M: 900 km. Stationen, die weiter entfernt sind, werden für Highlighting-Zwecke ignoriert. Trage die maximale Entfernung in Kilometern ein, innerhalb der KST4Contest Richtungsgelegenheiten berücksichtigen soll. Maßgeblich ist die Entfernung zwischen der eigenen Station und dem Absender der gerichteten Nachricht nicht die Entfernung zwischen Absender und Empfänger.
Liegt der Absender weiter entfernt, wird die Situation auch dann nicht hervorgehoben und nicht als Richtungsgelegenheit an den lokalen DX-Cluster-Server weitergegeben, wenn der berechnete Winkel passen würde.
Der Wert sollte zum eigenen Stationsaufbau und zum vorgesehenen Contestbetrieb passen. Ein unnötig großer Bereich erzeugt Hinweise für Stationen, die praktisch nicht mehr zum Arbeitsbereich gehören; ein zu kleiner Bereich blendet mögliche Kandidaten bereits vor der Richtungsbewertung aus.
--- ---
@@ -87,11 +110,115 @@ Konfiguration der Schnittstelle zu AirScout für die Flugzeug-Scatter-Erkennung.
## Notification Settings (Benachrichtigungen) ## Notification Settings (Benachrichtigungen)
Drei Benachrichtigungstypen stehen zur Wahl: ![Benachrichtigungen, DX-Cluster-Ausgabe und QSO-Monitoring](client_settings_window_notification.png)
1. **Einfache Sounds**: TADA-Sound für eingehende Nachrichten, Tick für Sked-Richtungserkennung usw. Im Reiter **Notification** werden nicht nur akustische Hinweise konfiguriert. Hier befinden sich auch die Einstellungen für den lokalen DX-Cluster-Server, den Band-Upgrade-Hinweis und das QSO-Monitoring.
2. **CW-Ansage**: Das Rufzeichen einer Station, die eine Privatnachricht sendet, wird als CW-Signal ausgegeben.
3. **Phonetische Ansage**: Das Rufzeichen wird phonetisch ausgesprochen. ### Akustische Hinweise
Die drei Audiofunktionen arbeiten unabhängig voneinander:
- **Play notification sounds …** aktiviert kurze Hinweistöne für neue Privatnachrichten, erkannte Richtungsgelegenheiten, Sked-Erinnerungen und Band-Upgrade-Hinweise.
- **Spell the sender's callsign in CW …** gibt das Rufzeichen des Absenders einer neuen Privatnachricht als CW-Signal aus.
- **Speak the sender's callsign phonetically …** spricht das Rufzeichen des Absenders phonetisch aus.
CW- und Sprachausgabe können gleichzeitig aktiviert werden. Das ist technisch möglich, im Contest aber nicht zwingend hilfreich. In der Praxis sollte nur die Ausgabe eingeschaltet werden, die im eigenen Stationsbetrieb tatsächlich wahrgenommen werden kann, ohne den Operator dauerhaft zu beschäftigen.
### Fallback-Band für relative QRG-Erkennung
Das Dropdown **Fallback band for relative QRG detection** legt fest, welches Band KST4Contest verwendet, wenn eine relative QRG keinem aktuellen Stationskontext zugeordnet werden kann.
Zur Auswahl stehen ausschließlich die vom Frequenzparser unterstützten Bandpräfixe:
```text
50 MHz
70 MHz
144 MHz
432 MHz
1296 MHz
2320 MHz
3400 MHz
5760 MHz
10368 MHz (10G)
24048 MHz (24G)
```
Das Dropdown ist kein Filter und keine Vorgabe für vollständig angegebene Frequenzen. `432.088` wird unabhängig von der Auswahl als Frequenz im 432-MHz-Band erkannt. Benötigt wird das Fallback bei relativen Angaben wie `.205`, `,205` oder `qrg 205`.
Bevor KST4Contest auf das Fallback zurückgreift, prüft es den Bandkontext des Absenders. Wurde für dieselbe Station innerhalb der letzten 30 Minuten bereits eine passende vollständige Frequenz erkannt, hat dieses Band Vorrang. Ein Fallback von `144 MHz` macht aus `.100` daher `432.100 MHz`, wenn die Station kurz zuvor beispielsweise `432.088` genannt hat.
Die Einstellung befindet sich im Notification-Bereich, wirkt aber auf die gesamte QRG-Erkennung. Damit beeinflusst sie nicht nur mögliche DX-Cluster-Spots, sondern auch die QRG-Spalte, erkannte aktive Bänder, Priorisierung, Band-Upgrade-Hinweise und Funktionen, die eine bekannte Stationsfrequenz verwenden.
Mehr zur Erkennungslogik und zu absichtlich ignorierten Zahlen: [QRG-Erkennung](de-Funktionen#qrg-erkennung).
### Local DX Cluster output
KST4Contest kann erkannte Richtungsgelegenheiten als DX-Cluster-Spots an ein Logprogramm weitergeben. Eine im Chat erkannte Frequenz erscheint dadurch direkt in der Bandmap des Logprogramms und muss nicht erst von Hand übertragen werden.
Die Checkbox **Enable the local DX Cluster server …** startet beziehungsweise beendet den lokalen TCP-Server. Bei einer laufenden Chat-Verbindung wird die Änderung sofort wirksam.
Folgende Einstellungen und Schaltflächen gehören zur lokalen DX-Cluster-Ausgabe:
- **TCP port**: Port, auf dem KST4Contest Verbindungen von DX-Cluster-Clients annimmt. Der Standardwert ist `8000`. Wird der Port während einer laufenden Verbindung geändert, startet KST4Contest den Server auf dem neuen Port neu. Der Logger muss sich anschließend ebenfalls mit dem neuen Port verbinden.
- **Fallback band for relative QRG detection**: Das oben beschriebene globale Fallback-Band. Der Testspot verwendet `.300` dieses Bandes. Reale Spots verwenden dagegen die für den jeweiligen Absender erkannte QRG.
- **Spotter callsign**: Rufzeichen, das im erzeugten DX-Cluster-Spot als Spotter erscheint. Hier sollte ein anderes Rufzeichen als das im Contest verwendete Stationsrufzeichen eingetragen werden. Einige Logprogramme filtern Spots des eigenen Rufzeichens oder behandeln sie anders als fremde Spots.
- **Send test spot**: Sendet einen Testspot für `DL0TEST` auf `.300` des ausgewählten Fallback-Bandes. Der Test funktioniert nur, wenn KST4Contest mit dem Chat verbunden, der lokale DX-Cluster-Server aktiviert und mindestens ein DX-Cluster-Client verbunden ist.
KST4Contest erzeugt nicht bei jeder im Chat gefundenen Frequenz automatisch einen Spot. Ein Spot entsteht nur dann, wenn eine gerichtete Nachricht zwischen zwei Stationen auf eine für die eigene Station interessante Antennenrichtung schließen lässt und für den Absender eine nutzbare Frequenz bekannt ist.
Die vollständige Herleitung und die Einrichtung des Logprogramms sind im Kapitel [Integrierter DX-Cluster-Server](de-DX-Cluster-Server) beschrieben.
### Band-Upgrade-Hinweis nach einem Logeintrag
Nach einem über UCXLog oder Win-Test empfangenen Logeintrag kann KST4Contest prüfen, ob die gerade gearbeitete Station noch ein weiteres gemeinsames, aber bisher nicht gearbeitetes Band anbietet.
Die Prüfung verwendet dieselbe Bandherleitung wie die `a`- und `B+`-Anzeige:
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.
Aktive Chat-Varianten desselben normalisierten Rufzeichens werden gemeinsam ausgewertet. NOT-QRV hat Vorrang vor einer automatisch erkannten QRG oder Bandangabe.
Bleibt mindestens ein gemeinsames, noch nicht gearbeitetes Band übrig, erscheint im Hauptfenster für ungefähr zwölf Sekunden ein blinkender **BAND+**-Hinweis mit Rufzeichen und den noch offenen Bändern. Der Tooltip zeigt die vollständige Herleitung. Ist die allgemeine Soundausgabe aktiviert, wird zusätzlich ein kurzer Hinweiston abgespielt.
Die beiden Optionen haben unterschiedliche Aufgaben:
- **Blink + sound …** aktiviert den Hinweis nach einem passenden Logeintrag.
- **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.
Weitere Hintergründe: [Band-Upgrade-Hinweis nach einem Logeintrag](de-Funktionen#band-upgrade-hinweis-nach-einem-logeintrag).
### Sniffer-Einstellungen (ab v1.31)
Das QSO-Monitoring ist für Stationen gedacht, deren Kommunikation man gezielt verfolgen möchte. Das kann beispielsweise eine seltene Station, eine DXpedition oder eine andere Station des eigenen Contest-Teams sein.
Für jedes eingetragene Rufzeichen zeigt KST4Contest Nachrichten zusätzlich in der PM-Tabelle an, wenn das Rufzeichen entweder Absender oder Empfänger der Nachricht ist. Die ursprüngliche Nachricht wird dabei nicht aus ihrer normalen Tabelle entfernt.
Überwachte Nachrichten werden in der PM-Tabelle eindeutig gekennzeichnet:
```text
Sniffed: (SENDER > RECEIVER) Nachrichtentext
```
So wird sichtbar, dass die Nachricht nicht an die eigene Station gerichtet war.
Rufzeichen werden folgendermaßen verwaltet:
1. Mit **Add monitored callsign** ein neues Rufzeichen hinzufügen.
2. Ein vorhandenes Rufzeichen per Doppelklick bearbeiten und die Änderung mit `Enter` übernehmen.
3. Zum Entfernen den Inhalt einer Tabellenzelle löschen und mit `Enter` bestätigen.
Doppelte oder syntaktisch ungültige Rufzeichen werden nicht übernommen. Die Liste wird mit **Save Settings** in der `preferences.xml` gespeichert und beim nächsten Programmstart wiederhergestellt.
--- ---
@@ -117,38 +244,155 @@ Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als D
## Beacon Settings (Automatischer Beacon) ## Beacon Settings (Automatischer Beacon)
Konfiguration eines automatischen Intervall-Beacons im öffentlichen Chat-Kanal. Empfohlen: Variable `MYQRG` im Text verwenden, damit die aktuelle Frequenz immer aktuell ist. Intervall und Text sind frei konfigurierbar. ![Beacon-Einstellungen](client_settings_window_beacon.png)
> **Tipp**: Beacon beim CQ-Rufen aktivieren und im Einstellungsfenster schnell deaktivieren, wenn kein CQ gerufen wird. Ein Beacon sendet in regelmäßigen Abständen eine öffentliche CQ-Nachricht. Er ist für Betriebssituationen gedacht, in denen die eigene Station über längere Zeit auf einer festen Frequenz ruft. Andere Stationen erhalten dadurch eine aktuelle QRG-Information, ohne dass der Operator denselben Text wiederholt von Hand in den Chat schreiben muss.
KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivierung und Nachrichtentext werden trotzdem getrennt konfiguriert:
- **Enable CQ beacon** aktiviert den Beacon der betreffenden Kategorie.
- **Beacon message** enthält den öffentlichen Nachrichtentext dieser Kategorie.
- **Shared beacon interval** legt das gemeinsame Intervall für beide Kategorien fest.
Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in ihren jeweiligen Kategorien gesendet. Der zweite Beacon wird nur berücksichtigt, wenn auch der zweite Chat aktiviert und verbunden ist.
### Intervall und Timer-Verhalten
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert ist eine Minute.
Nach dem Aufbau der Chat-Verbindung prüft KST4Contest die Beacons erstmals nach ungefähr zehn Sekunden. Anschließend gilt das eingestellte Intervall. Wird der Wert während einer laufenden Verbindung geändert, beginnt der Countdown mit dem neuen Intervall erneut. Die Änderung selbst löst keine sofortige Nachricht aus.
### Nachrichtentext und Variablen
Ein Beacon darf nach der Variablenauflösung höchstens 120 Zeichen enthalten. KST4Contest prüft deshalb nicht nur das eingetragene Template, sondern den tatsächlich zu sendenden Text.
Im Beacon können alle [globalen Variablen](de-Makros-und-Variablen#variablen-im-beacon) verwendet werden, beispielsweise:
```text
calling cq at MYQRG, ant MYQTF deg, loc MYLOCATOR
```
Die Variablen werden bei jedem Timer-Lauf neu aufgelöst. Ändert die Logsoftware zwischenzeitlich die in `MYQRG` gespeicherte Frequenz, verwendet bereits der nächste Beacon den neuen Wert.
Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` benötigen dagegen eine ausgewählte Gegenstation. Da ein öffentlicher Beacon keine Gegenstation adressiert, werden diese Variablen im Beacon nicht aufgelöst.
### Wann sollte der Beacon ausgeschaltet werden?
Der Beacon ist nur dann hilfreich, wenn seine QRG-Angabe zum tatsächlichen Betrieb passt. Bleibt er beim Absuchen oder häufigen Wechseln von Frequenzen aktiviert, können andere Stationen auf einer inzwischen falschen Frequenz nach der eigenen Station suchen.
Im Klartext: Solange auf einer festen QRG CQ gerufen wird, spart der Beacon Arbeit. Beim „Schleichen“ über das Band sollte er ausgeschaltet werden.
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung, Texte und Intervall auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
--- ---
## Messagehandling Settings (ab v1.25) ## Messagehandling Settings (ab v1.25)
Neuer Einstellungsbereich mit folgenden Optionen: ![Automatische Antworten](client_settings_window_messagehandling.png)
- **Auto-Antwort auf alle eingehenden Nachrichten**: Automatische Antwort auf Privatnachrichten konfigurierbar. Die wichtigste Anwendung der allgemeinen automatischen Antwort betrifft Stationen, die zwar im ON4KST-Chat eingeloggt sind, den laufenden Contest aber nicht mitfunken. Gerade während größerer Contests werden Sked-Anfragen teilweise unkoordiniert und in großer Zahl an eingeloggte Stationen verteilt, ohne vorher zu prüfen, ob sie überhaupt teilnehmen. Die Empfänger müssten sonst immer wieder dieselbe Absage schreiben.
- **Auto-Antwort mit eigener CQ-QRG**: Wenn jemand nach der eigenen QRG fragt, antwortet KST4Contest automatisch mit dem Inhalt der `MYQRG`-Variable.
- **Standard-Filter für das Userinfo-Fenster**: Voreingestellter Nachrichtenfilter für das Stationsinfo-Fenster konfigurierbar *(für Gianluca :-) )*. KST4Contest kann darauf mit einem vorher festgelegten Text reagieren. Davon getrennt steht eine gezielte QRG-Auskunft zur Verfügung. Beide Funktionen können unabhängig voneinander aktiviert werden.
### Allgemeine automatische Antwort
**Enable automatic reply to all private messages** beantwortet eingehende Privatnachrichten mit dem Text im Feld rechts daneben. Es gibt einen gemeinsamen Text für beide Chat-Kategorien. Die beim Eingeben verwendete Groß- und Kleinschreibung bleibt erhalten.
Eine zweckmäßige Nachricht ist beispielsweise:
```text
Sri, I am not taking part in this contest. No skeds.
```
Die eingegangene Privatnachricht bleibt sichtbar. Die Funktion blockiert oder verwirft keine Anfrage, sondern erspart lediglich die wiederholte manuelle Antwort.
Die Antwort wird in derselben Chat-Kategorie gesendet, in der die Privatnachricht eingegangen ist. Das ist bei einem parallelen Login in zwei Kategorien entscheidend: Eine Nachricht aus dem Microwave-Chat darf nicht versehentlich im VHF/UHF-Chat beantwortet werden.
### Automatische QRG-Antwort
**Enable automatic QRG replies** reagiert auf typische QRG-Anfragen. Die Erkennung unterscheidet nicht zwischen Groß- und Kleinschreibung und sucht nach folgenden Textbestandteilen:
```text
ur qrg?
your qrg?
qrg?
freq?
pse qrg
```
Die Antwort enthält nur die QRG der Kategorie, in der die Anfrage eingegangen ist:
| Eingegangene Privatnachricht | Verwendete QRG |
|---|---|
| Hauptkategorie | aktuelle QRG der Hauptkategorie |
| zweite Chat-Kategorie | aktuelle QRG der zweiten Kategorie |
Die Werte stammen aus denselben QRG-Feldern, die auch von `MYQRG` und `SECONDQRG` verwendet werden. Die Haupt-QRG kann manuell eingetragen oder durch die [TRX-Synchronisation](#trx-sync-einstellungen) aktualisiert werden. Für die zweite Kategorie wird der dort konfigurierte beziehungsweise manuell eingetragene Wert verwendet.
Sind die allgemeine und die QRG-bezogene Antwort gleichzeitig aktiviert, hat die QRG-Antwort Vorrang. Eine erkannte QRG-Anfrage erzeugt daher nicht zusätzlich den allgemeinen Antworttext.
### Schutz vor wiederholten Antworten
Jede automatisch erzeugte Nachricht trägt das feste Präfix:
```text
[KST4C Automsg]
```
Die allgemeine und die QRG-bezogene Antwort reagieren nicht auf Nachrichten, die dieses Präfix bereits enthalten. Dadurch beantworten sich zwei entsprechend arbeitende Clients nicht gegenseitig in einer Schleife.
Zusätzlich gilt eine gemeinsame Sperrzeit von zwei Minuten für beide Antwortarten. Die Sperre wird getrennt je Rufzeichen und Chat-Kategorie geführt. Hat eine Station gerade in der Hauptkategorie eine automatische Antwort erhalten, kann sie deshalb weiterhin eine Antwort in der zweiten Kategorie erhalten. Weitere Nachrichten derselben Station in derselben Kategorie lösen während der folgenden zwei Minuten dagegen keine neue automatische Antwort aus.
Die Sperrzeit beginnt nur, wenn KST4Contest tatsächlich eine Antwort sendet.
> **Hinweis**: Der Antworttext sollte den tatsächlichen Status eindeutig benennen. Wer den Contest nur beobachtet und keine Skeds fahren möchte, sollte genau das mitteilen. Eine vage Nachricht erzeugt im Zweifel nur die nächste Rückfrage und damit exakt die Arbeit, welche die Funktion vermeiden soll.
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung und Text nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
Weitere Hintergründe: [Automatische Antworten auf Privatnachrichten](de-Funktionen#automatische-antworten-auf-privatnachrichten-ab-v125).
--- ---
## Win-Test-Netzwerk-Listener (ab v1.31) ## 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. - QSOs einschließlich Band- und Locatorinformation auswerten,
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz aus Win-Test befüllt die `MYQRG`-Variable. - STATUS-Pakete für die eigene QRG verarbeiten und
- **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. - Skeds an das Win-Test-Netzwerk übergeben.
Einstellungen: ### Einstellungen unter Log sync
- **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.
> **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) ## PSTRotator-Einstellungen (ab v1.31)
@@ -163,25 +407,37 @@ Einstellungen:
--- ---
## Sniffer-Einstellungen (ab v1.31) ## GUI Settings: Hinweise in den Bandspalten
Der QSO-Sniffer filtert Chat-Nachrichten von konfigurierbaren Rufzeichen und leitet sie ins PM-Fenster weiter. Im Reiter **GUI** lassen sich zwei Zusatzinformationen der Bandspalten ein- oder ausblenden:
Einstellungen: - **Show "o" in band columns …** zeigt ein `o`, wenn das vierstellige Großfeld auf dem betreffenden Band bereits gearbeitet wurde. Das Abschalten entfernt keine Daten aus der Datenbank; nur die zusätzliche Anzeige in den Bandspalten wird ausgeblendet. `wkdany` bleibt davon unberührt.
- **Rufzeichen-Liste**: Kommagetrennte Liste von Rufzeichen, deren Nachrichten immer in das PM-Fenster weitergeleitet werden sollen. - **Show "a" in band columns …** unterscheidet ein vollständig neues Rufzeichen von einer Bandmöglichkeit mit einem bereits auf einem anderen Band gearbeiteten Rufzeichen. Ist die Option ausgeschaltet, werden beide Fälle als `B+` dargestellt. Die Bandherleitung selbst ändert sich dadurch nicht.
Anwendungsfall: Wichtige Stationen (z. B. DX-Peditionen oder feste Verbündete im Contest) im Auge behalten, ohne den Haupt-Chat ständig zu beobachten. Änderungen werden in der laufenden Benutzeroberfläche unmittelbar sichtbar. Damit sie nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
![GUI-Einstellungen für die Hinweise in den Bandspalten](client_settings_window_gui.png)
--- ---
## Worked Station Database Settings (Gearbeitete-Stationen-Datenbank) ## Worked Station Database Settings (Gearbeitete-Stationen-Datenbank)
Die interne Worked-Datenbank enthält: Die interne SQLite-Datenbank speichert die contestbezogenen Zustände unabhängig von der Datenbank des Logprogramms:
- Worked-Status aller Stationen (pro Band) - globaler Worked-Status eines Rufzeichens,
- NOT-QRV-Tags (seit v1.2) - Worked-Status pro Band,
- manuell gesetzte NOT-QRV-Tags pro Band und
- gearbeitete vierstellige Großfelder pro Band.
**Ab v1.40**: Einträge haben eine automatische Lebensdauer von **3 Tagen** ein manuelles Zurücksetzen vor jedem Contest ist nicht mehr zwingend notwendig. Für ein vollständiges Reset kann trotzdem die Schaltfläche **„Reinitialize"** verwendet werden. Als Schlüssel wird das normalisierte Rufzeichen ohne sichtbare Chat-Klammern oder Kategorieformatierung verwendet. Dadurch können aktive Varianten desselben Rufzeichens konsistent ausgewertet werden.
Worked- und NOT-QRV-Informationen laufen drei Tage nach ihrer letzten Änderung automatisch ab. Gespeicherte Großfelder laufen drei Tage nach dem zugehörigen Logeintrag ab. Ein manuelles Zurücksetzen vor jedem Contest ist deshalb normalerweise nicht erforderlich.
Die Schaltfläche **Reset worked, NOT-QRV and grid data...** entfernt sämtliche Worked-Markierungen, NOT-QRV-Tags und gespeicherten Großfelder. Vor dem Reset erscheint eine Sicherheitsabfrage. Die bekannten Rufzeichenzeilen bleiben erhalten; zurückgesetzt werden nur die contestbezogenen Zustände.
Ein Reset ist sinnvoll, wenn bewusst mit einem leeren Conteststand begonnen werden soll oder Testdaten eingelesen wurden. Als tägliche Wartungsmaßnahme ist er nicht vorgesehen.
Anzeige und Herleitung: [Gearbeitete Rufzeichen, neue Bänder und neue Großfelder](de-Funktionen#gearbeitete-rufzeichen-neue-bänder-und-neue-großfelder).
--- ---
+90 -33
View File
@@ -2,7 +2,7 @@
> 🇬🇧 [English version](en-Log-Sync) | 🇩🇪 Du liest gerade die deutsche Version > 🇬🇧 [English version](en-Log-Sync) | 🇩🇪 Du liest gerade die deutsche Version
KST4Contest markiert gearbeitete Stationen automatisch in der Chat-Benutzerliste. Dafür gibt es zwei grundlegende Methoden: KST4Contest übernimmt gearbeitete Stationen aus dem Logprogramm und stellt daraus den globalen Worked-Status, bandbezogene Worked-Markierungen und sofern ein Locator übertragen wurde gearbeitete Großfelder bereit. Dafür gibt es drei Wege: den dateibasierten Simplelogfile-Interpreter, den allgemeinen QSO-UDP-Listener und den eigenen Win-Test-Netzwerk-Listener.
--- ---
@@ -10,24 +10,25 @@ KST4Contest markiert gearbeitete Stationen automatisch in der Chat-Benutzerliste
## Methode 1: Universal File Based Callsign Interpreter (Simplelogfile) ## Methode 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest liest eine Log-Datei und sucht mittels regulärem Ausdruck nach Rufzeichen-Mustern. Dabei werden auch binäre Logdateien unterstützt unlesbarer Binärinhalt wird einfach ignoriert. KST4Contest liest eine Logdatei und sucht mit einem konfigurierbaren regulären Ausdruck nach Rufzeichen. Die Datei wird ausschließlich gelesen und nicht verändert. Auch binäre Logdateien können verwendet werden; nicht als Text interpretierbare Inhalte werden übersprungen.
**Vorteil**: Funktioniert mit nahezu jedem Logprogramm, das eine Datei schreibt. Der Vorteil liegt in der breiten Kompatibilität: Die Funktion benötigt keine besondere Netzwerkschnittstelle des Logprogramms.
**Nachteil**: Keine Bandinformation möglich es wird nur „gearbeitet" markiert, nicht auf welchem Band.
Pfad der Log-Datei in den Preferences eintragen. Die Datei wird nur gelesen, nie verändert (read-only). Die Grenze ist ebenso eindeutig: Aus einem reinen Rufzeichentreffer lassen sich weder Band noch Locator zuverlässig ableiten. Der Simplelogfile-Interpreter kann deshalb nur den globalen Worked-Status setzen. Er erzeugt keine bandbezogene `X`-Markierung, kein Worked-Großfeld und keine belastbare Grundlage für den Band-Upgrade-Hinweis nach einem Logeintrag.
> **Tipp**: Die Simplelogfile-Funktion kann auch genutzt werden, um Stationen zu markieren, die definitiv nicht erreichbar sind (z. B. eigene Notizen). Das wird in einer späteren Version durch ein besseres Tagging-System ersetzt. Den Pfad der Logdatei und den regulären Ausdruck im Reiter **Log sync** eintragen. Für bandbezogene Auswertungen sollte nach Möglichkeit eine der Netzwerkschnittstellen verwendet werden.
--- ---
## Methode 2: Netzwerk-Listener (UDP-Broadcast) Empfohlen ## Methode 2: Netzwerk-Listener für QSO-UDP-Pakete empfohlen
Das Logprogramm sendet beim Speichern eines QSOs ein UDP-Paket an die Broadcast-Adresse des Heimnetzwerks. KST4Contest empfängt dieses Paket und markiert die Station inklusive **Bandinformation** in der internen SQLite-Datenbank. UCXLog, QARTest, N1MM+ und DXLog.net können beim Speichern eines QSOs ein UDP-Paket senden. KST4Contest empfängt diese Pakete standardmäßig auf Port `12060` und übernimmt das Rufzeichen sowie die enthaltenen Band- und Locatorinformationen.
> **Wichtig**: KST4Contest muss **parallel zum Logprogramm laufen**. QSOs, die während einer Abwesenheit von KST4Contest geloggt werden, werden nicht erfasst außer bei QARTest (kann das komplette Log senden). Liegt eine Bandinformation vor, wird das Rufzeichen für dieses Band als gearbeitet markiert. Enthält das Paket zusätzlich einen gültigen Locator, speichert KST4Contest dessen vierstelliges Großfeld für das betreffende Band. Fehlende Informationen werden nicht aus anderen Feldern geraten.
**Standard UDP-Port**: 12060 (entspricht dem Standard der meisten Logprogramme) KST4Contest muss zum Zeitpunkt der Übertragung laufen. Einige Logprogramme können jedoch das vorhandene Log erneut senden: QARTest bietet dafür **Invia log completo**; DXLog.net sendet beim Broadcast des vollständigen Logs `contactreplace`-Pakete, die KST4Contest ebenfalls verarbeitet.
**Standardport:** `12060`
--- ---
@@ -81,34 +82,80 @@ Für den integrierten DX-Cluster-Server: N1MM+ als DX-Cluster-Client konfigurier
- IP des KST4Contest-Computers eintragen (grün markierte Felder) - IP des KST4Contest-Computers eintragen (grün markierte Felder)
- Port: 12060 - Port: 12060
Beim Broadcast des vollständigen Logbuchs verwendet DXLog.net `contactreplace` anstelle von `contactinfo`. KST4Contest verarbeitet beide Pakettypen. Damit können auch ältere QSOs übernommen werden, wenn der vollständige Broadcast ausgelöst wird, während KST4Contest läuft.
### Win-Test ### 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.
**Vorteile der Win-Test Integration:** #### QSO- und Worked-Synchronisation
- 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).
**Einstellungen im Reiter „Log-Synchronisation":** Bei einem neuen QSO übernimmt KST4Contest:
- `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.
**Einstellungen im Reiter „TRX-Synchronisation":** - das geloggte Rufzeichen,
- `Win-Test STATUS QRG Sync`: Wenn aktiviert, übernimmt KST4Contest die aktuelle Transceiverfrequenz aus dem Win-Test STATUS-Paket als eigene QRG (MYQRG). - die native Win-Test-Band-ID und
- `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). - einen gültigen Locator, sofern er im Paket enthalten ist.
- `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.
**Einstellungen in Win-Test:** 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.
- 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 ## TRX-Frequenz-Synchronisation
@@ -144,6 +191,16 @@ Für DM5M-typische Setups (2 Radios, 2 Computer, eine KST4Contest-Instanz oder z
## Interne Datenbank ## Interne Datenbank
KST4Contest speichert die Worked-Information in einer internen **SQLite-Datenbank**. Diese ist von der Logprogramm-Datenbank unabhängig und wird nur über den UDP-Broadcast befüllt. KST4Contest speichert Worked-, NOT-QRV- und Großfeldinformationen in einer eigenen SQLite-Datenbank. Sie ist von der Datenbank des Logprogramms unabhängig.
Vor jedem neuen Contest: Datenbank zurücksetzen! → [Konfiguration Worked Station Database Settings](Konfiguration#worked-station-database-settings) Die Datenquellen liefern unterschiedlich genaue Informationen:
| Quelle | Rufzeichen global | Bandbezogen | Großfeld |
|---|---:|---:|---:|
| Simplelogfile | ja | nein | nein |
| QSO-UDP-Listener | ja | ja, wenn im Paket enthalten | ja, wenn Band und Locator enthalten sind |
| Win-Test-Netzwerk-Listener | ja | ja | ja, wenn ein Locator enthalten ist |
Die Daten werden beim Programmstart wieder geladen und bei neuen Logeinträgen während des Betriebs aktualisiert. Sie laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist daher normalerweise nicht erforderlich.
Ein vollständiger manueller Reset entfernt Worked-Markierungen, NOT-QRV-Tags und Worked-Großfelder gemeinsam. Weitere Einzelheiten: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
+19 -3
View File
@@ -138,18 +138,34 @@ Wird durch die aktuelle Antennenrichtung in Worten ersetzt (z. B. `north`, `nort
--- ---
## Variablen im Beacon
## Variablen im Beacon ## Variablen im Beacon
Alle Variablen können auch im **automatischen Beacon** (Intervall-Nachrichten) verwendet werden. Empfohlene Beacon-Konfiguration: Ein öffentlicher Beacon besitzt keine ausgewählte Gegenstation. Deshalb können hier ausschließlich Variablen verwendet werden, die nur von der eigenen Station und ihrer aktuellen Konfiguration abhängen:
| Variable | Wert im Beacon |
|---|---|
| `MYQRG` | aktuelle QRG der ersten Chat-Kategorie |
| `MYQRGSHORT` | auf sieben Zeichen gekürzte QRG der ersten Kategorie |
| `SECONDQRG` | aktuelle QRG der zweiten Chat-Kategorie |
| `MYLOCATOR` | eigener vollständiger Locator |
| `MYLOCATORSHORT` | eigener vierstelliger Locator |
| `MYCALL` | eigenes Rufzeichen |
| `MYQTF` | aktuelle Antennenrichtung |
`QRZNAME`, `FIRSTAP` und `SECONDAP` benötigen eine ausgewählte Station. In einem öffentlichen Beacon werden sie daher nicht aufgelöst.
Eine zweckmäßige Konfiguration ist beispielsweise:
``` ```
calling cq at MYQRG, loc MYLOCATOR, GL all! calling cq at MYQRG, loc MYLOCATOR, GL all!
``` ```
Da KST4Contest QRG-Daten automatisch aus Chat-Nachrichten ausliest: Wenn andere Stationen ebenfalls KST4Contest nutzen, sehen sie die eigene QRG sofort in der QRG-Spalte der Benutzerliste. Die globalen Variablen werden bei jedem Timer-Lauf neu ausgewertet. Dadurch kann eine vom Logprogramm aktualisierte QRG bereits in der nächsten Beacon-Nachricht erscheinen.
Der vollständig aufgelöste Nachrichtentext darf höchstens 120 Zeichen enthalten. Weitere Angaben zum gemeinsamen Intervall und zum Verhalten beider Chat-Kategorien stehen unter [Konfiguration Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
--- ---
## Beispiel-Workflow mit Makros im Contest ## Beispiel-Workflow mit Makros im Contest
1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt. 1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt.
Binary file not shown.

After

Width:  |  Height:  |  Size: 961 KiB

+133 -14
View File
@@ -25,7 +25,18 @@ Enter your own callsign and Maidenhead locator (6 characters, e.g., `JN49IJ`). T
### Active Bands ### 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 ### Antenna Beamwidth
@@ -87,12 +98,71 @@ Configuration of the interface to AirScout for aircraft scatter detection. Detai
## Notification Settings ## Notification Settings
![Notifications, DX cluster output and QSO monitoring](client_settings_window_notification.png)
Three notification types are available: Three notification types are available:
1. **Simple sounds**: TADA sound for incoming messages, tick for sked direction detection, etc. 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. 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. 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 [enabled bands](#enabled-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 ## Shortcut Settings
@@ -135,18 +205,44 @@ New settings section with the following options:
## Win-Test Network Listener (from v1.31) ## 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. - processing QSOs including band and locator information,
- **Frequency parsing**: The current TRX frequency from Win-Test populates the `MYQRG` variable. - processing STATUS packets for the local QRG, and
- **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. - handing skeds over to the Win-Test network.
Settings: ### Log sync 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.
> **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 +270,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 ## 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) - the global Worked status of a callsign,
- NOT-QRV tags (since v1.2) - 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).
--- ---
+332 -49
View File
@@ -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. 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 ## 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. 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.
- **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.
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) ## 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. - importing new QSOs including band and, where available, locator information,
- **Frequency parsing**: The current TRX frequency is read from Win-Test UDP packets and populates the `MYQRG` variable. - reading the current QRG from Win-Test STATUS packets, and
- **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. - 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) ## PSTRotator Interface (from v1.31, fully configurable from v1.40)
@@ -177,46 +325,181 @@ 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:
- [Enabled Bands](en-Configuration#enabled-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?) - When is an interesting AP opportunity expected?
- QRB (distance) - Which previously agreed sked is approaching independently of that opportunity?
- Activity time and message count
- Active bands and frequencies
- AP availability (AirScout)
- Sked direction (degrees)
- Sked success rate and skedfail markings
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). | Colour | Reflection potential |
- Stations towards which your antenna is not pointing are shown **transparently**. |---|---:|
| 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 ## Interval Beacon
+91 -33
View File
@@ -2,7 +2,7 @@
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Log-Synchronisation) > 🇬🇧 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) ## 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. The advantage is broad compatibility: no dedicated network interface is required from the logging application.
**Disadvantage**: No band information available stations are only marked as "worked", not on which band.
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) - Enter the IP of the KST4Contest computer (green-highlighted fields)
- Port: 12060 - 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
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:** #### QSO and Worked synchronisation
- 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).
**Settings in the "Log Synchronisation" tab:** For a new QSO, KST4Contest imports:
- 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.
**Settings in the "TRX Synchronisation" tab:** - the logged callsign,
- `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). - the native Win-Test band ID, and
- `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). - a valid locator where one is included in the packet.
- `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.
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 ## TRX Frequency Synchronisation
@@ -144,6 +192,16 @@ For DM5M-style setups (2 radios, 2 computers, one KST4Contest instance or two se
## Internal Database ## 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.
+96 -15
View File
@@ -26,14 +26,29 @@ The central table of all currently active chat users. Columns (depending on conf
| Column | Content | | Column | Content |
|---|---| |---|---|
| Call | Station's callsign | | Callsign | Station callsign |
| Name | Name from the chat name field | | Name | Name and additional information from the chat name field |
| Loc | Maidenhead locator | | QRA | Maidenhead locator |
| QRB | Distance in km | | QRB | Distance in km |
| QTF | Direction in degrees | | QTF | Direction in degrees |
| QRG | Automatically detected frequency | | QRG | Most recent frequency detected in a chat message |
| AP | AirScout aircraft data (when active) | | Tropo | Result of the band-specific tropo or path assessment |
| Band colours | Worked / NOT-QRV status per band | | 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). **Sorting**: Click column headers. QRB sorting is numerical (corrected in v1.22).
@@ -53,26 +68,91 @@ Input field for the current antenna direction. Used for the planned `MYQTF` vari
## Filters ## 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 QTF** limits the list to a selected antenna direction.
- **Show only QRB [km] <=**: Activate distance filter (toggle button) - **Show only QRB [km] <=** sets a maximum distance.
- **Hide Worked [Band]**: Hide worked stations per band (one toggle per band) - **Find** searches for a callsign.
- **Hide NOT-QRV [Band]**: Hide NOT-QRV-tagged stations per band - **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) ## 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 ## 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).
--- ---
@@ -100,6 +180,7 @@ If you encounter display problems: delete the configuration file → KST4Contest
## Operating Tips ## Operating Tips
- **Keep the settings window open**: Quick access to enable/disable the beacon. - **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. - **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. - **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: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 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: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

+21
View File
@@ -150,6 +150,18 @@
<version>${jetbrains.annotations.version}</version> <version>${jetbrains.annotations.version}</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.17.0</version>
<scope>compile</scope>
</dependency>
<!-- <!--
<dependency> <dependency>
@@ -254,6 +266,15 @@
<version>${maven.surfire.plugin}</version> <version>${maven.surfire.plugin}</version>
<configuration> <configuration>
<testFailureIgnore>true</testFailureIgnore> <testFailureIgnore>true</testFailureIgnore>
<!--
Without this, Surefire's automatic module-path detection splits the
JUnit Platform jars across classpath and module-path inconsistently
(module org.junit.platform.commons ends up loaded while a class from
junit-platform-engine stays in the unnamed module), which fails with
an IllegalAccessError before any test can run. The project itself has
no module-info.java, so plain classpath execution is correct here.
-->
<useModulePath>false</useModulePath>
</configuration> </configuration>
</plugin> </plugin>
@@ -43,7 +43,7 @@ public class ApplicationConstants {
public static final String DISCONNECT_RDR_POISONPILL = "UNKNOWN: KST4C KILL POISONPILL_KILLTHREAD=: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop public static final String DISCONNECT_RDR_POISONPILL = "UNKNOWN: KST4C KILL POISONPILL_KILLTHREAD=: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg] "; // hard-coded marker (user can't remove it) public static final String AUTOANSWER_PREFIX = "[KST4C Automsg]"; // hard-coded marker (user cannot remove it)
/** /**
* UI message retention limits. * UI message retention limits.
@@ -1,150 +1,198 @@
package kst4contest.controller; package kst4contest.controller;
import java.io.IOException; import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.NoRouteToHostException; import java.nio.charset.StandardCharsets;
import java.net.SocketException; import java.util.List;
import java.net.UnknownHostException;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import javafx.collections.ObservableList;
import kst4contest.locatorUtils.Location;
import kst4contest.model.ChatMember; import kst4contest.model.ChatMember;
/**
* Sends periodical path requests and an AirScout watchlist for the currently
* active ON4KST stations.
*/
public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask { public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
private static final Logger LOGGER = Logger.getLogger(AirScoutPeriodicalAPReflectionInquirerTask.class.getName()); private static final Logger LOGGER = Logger.getLogger(
private ChatController client; AirScoutPeriodicalAPReflectionInquirerTask.class.getName()
);
public AirScoutPeriodicalAPReflectionInquirerTask(ChatController client) { private static final String BROADCAST_ADDRESS = "255.255.255.255";
private final ChatController client;
public AirScoutPeriodicalAPReflectionInquirerTask(
ChatController client
) {
this.client = client; this.client = client;
} }
@Override @Override
public void run() { public void run() {
Thread.currentThread().setName(
"AirscoutPeriodicalReflectionInquirerTask"
);
Thread.currentThread().setName("AirscoutPeriodicalReflectionInquirierTask"); /*
* Keep the scheduled task installed so that AirScout can be enabled at
String KSTClientsNameForQuery = this.client.getChatPreferences().getAirScout_asClientNameString(); * runtime, but do not send anything while the integration is disabled.
String ASServerNameStringForAnswer = this.client.getChatPreferences().getAirScout_asServerNameString();
//TODO: Manage prefixes kst and as via preferences file and instance
//TODO: Check if locator is changeable via the preferences object, need to be correct if it changes
DatagramSocket dsocket;
// String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" "; //working original
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String prefix_asWatchList = "ASWATCHLIST: \""+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String bandString = "1440000"; //TODO: this must variable in case of higher bands! ... default: 1440000
// String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //before fix 1.266
String ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
try {
if (this.client.getChatPreferences().getStn_loginCallSign().contains("-")) {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign().split("-")[0];
} else {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
}
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Error parsing callsign", e);
}
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
String asWatchListStringSuffix = asWatchListString;
String host = "255.255.255.255";
// int port = 9872;
int port = client.getChatPreferences().getAirScout_asCommunicationPort();
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
InetAddress address;
/**
* Iterate over chatmemberlist and asking airscout for plane reflection information
* To avoid a concurrentmodifyexception, we have to convert the original list to an array at first
* since the iterator brakes if the list changing during the iteration time
*/ */
ObservableList<ChatMember> praktiKSTActiveUserList = this.client.getLst_chatMemberList(); if (!client.getChatPreferences().isAirScout_asUDPListenerEnabled()) {
return;
ChatMember[] ary_threadSafeChatMemberArray = new ChatMember[praktiKSTActiveUserList.size()];
praktiKSTActiveUserList.toArray(ary_threadSafeChatMemberArray);
for (ChatMember i : ary_threadSafeChatMemberArray) {
if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault())
//Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower!
//this counts for AS request and Aswatchlist
{
suffix = i.getCallSign() + "," + i.getQra() + " ";
String queryStringToAirScout = "";
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
try {
address = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
dsocket = new DatagramSocket();
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (UnknownHostException e1) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Unknown host", e1);
} catch (NoRouteToHostException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] No route to host", e);
} catch (SocketException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Socket error", e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending query", e);
} }
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra(); String clientIdentifier =
client.getChatPreferences().getAirScout_asClientNameString();
String serverIdentifier =
client.getChatPreferences().getAirScout_asServerNameString();
String bandValue =
client.getChatPreferences().getAirScout_asBandString();
String ownCallSign = normalizeOwnCallSign(
client.getChatPreferences().getStn_loginCallSign()
);
String ownLocator =
client.getChatPreferences().getStn_loginLocatorMainCat();
if (ownCallSign == null
|| ownCallSign.isBlank()
|| ownLocator == null
|| ownLocator.isBlank()) {
LOGGER.warning(
"AirScout queries were skipped because the own callsign "
+ "or locator is missing."
);
return;
}
String setPathPrefix =
"ASSETPATH: \"" + clientIdentifier
+ "\" \"" + serverIdentifier + "\" ";
String watchListPrefix =
"ASWATCHLIST: \"" + clientIdentifier
+ "\" \"" + serverIdentifier + "\" ";
String ownStation = ownCallSign + "," + ownLocator;
StringBuilder watchListMessage = new StringBuilder(
watchListPrefix
+ bandValue
+ ","
+ ownStation
);
List<ChatMember> activeMembers = client.snapshotChatMembers();
int port = client.getChatPreferences()
.getAirScout_asCommunicationPort();
try (
DatagramSocket socket = new DatagramSocket()
) {
socket.setBroadcast(true);
InetAddress broadcastAddress =
InetAddress.getByName(BROADCAST_ADDRESS);
for (ChatMember member : activeMembers) {
if (!isUsableAirScoutTarget(member)) {
continue;
}
if (member.getQrb()
>= client.getChatPreferences().getStn_maxQRBDefault()) {
continue;
}
String targetStation =
member.getCallSign() + "," + member.getQra();
String pathQuery =
setPathPrefix
+ bandValue
+ ","
+ ownStation
+ ","
+ targetStation
+ " ";
sendPacket(
socket,
broadcastAddress,
port,
pathQuery
);
watchListMessage
.append(",")
.append(targetStation);
}
watchListMessage.append(" ");
sendPacket(
socket,
broadcastAddress,
port,
watchListMessage.toString()
);
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"Could not send the periodical AirScout queries.",
exception
);
} }
} }
/** /**
* As next we will set the ASWatchlist. All stations in chat will be watched by airscout causing following code.\n\n * Removes the ON4KST login suffix because AirScout expects the actual
* ASWATCHLIST: "KST" "AS" 4320000,DO5AMF,JN49GL,DF9QX,JO42HD,DG2KBC,JN58MI,DJ0PY,JO32MF,DL1YDI,JO42FA,DL6BF,JO32QI,F1NZC,JN15MR,F4TXU,JN23CX,F5GHP,IN96LE,F6HTJ,JN12KQ,G0GGG,IO81VE,G0JCC,IO82MA,G0JDL,JO02SI,G0MBL,JO01QH,G4AEP,IO91MB,G4CLA,IO92JL,G4DCV,IO91OF,G4LOH,IO70JC,G4MKF,IO91HJ,G4TRA,IO81WN,G8GXP,IO93FQ,G8VHI,IO92FM,GW0RHC,IO71UN,HA4ND,JN97MJ,I5/HB9SJV/P,JN52JS,IW2DAL,JN45NN,OK1FPR,JO80CE,OK6M,JN99CR,OV3T,JO46CM,OZ2M,JO65FR,PA0V,JO33II,PA2RU,JO32LT,PA3DOL,JO22MT,PA9R,JO22JK,PE1EVX,JO22MP,S51AT,JN75GW,SM7KOJ,JO66ND,SP9TTG,JO90KW * station callsign, for example 9A1W instead of 9A1W-2.
* The watchlist-String is bult by the for loop which builds the AP queries *
* @param callSign configured ON4KST login callsign
* @return callsign without an ON4KST login suffix
*/ */
asWatchListStringSuffix += " "; private String normalizeOwnCallSign(String callSign) {
if (callSign == null) {
byte[] queryStringToAirScoutMSG = asWatchListStringSuffix.getBytes(); return null;
try {
address = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
dsocket = new DatagramSocket();
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending watchlist", e);
} }
// System.out.println("[ASUDPTask, info:] set watchlist: " + asWatchListStringSuffix); String normalizedCallSign = callSign.trim();
int suffixSeparator = normalizedCallSign.indexOf("-");
if (suffixSeparator > 0) {
return normalizedCallSign.substring(0, suffixSeparator);
} }
return normalizedCallSign;
}
private boolean isUsableAirScoutTarget(ChatMember member) {
return member != null
&& member.getCallSign() != null
&& !member.getCallSign().isBlank()
&& member.getQra() != null
&& !member.getQra().isBlank();
}
private void sendPacket(
DatagramSocket socket,
InetAddress address,
int port,
String message
) throws IOException {
byte[] payload = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(
payload,
payload.length,
address,
port
);
socket.send(packet);
}
} }
@@ -1,122 +1,178 @@
package kst4contest.controller; package kst4contest.controller;
import java.util.Arrays;
import java.util.TimerTask; import java.util.TimerTask;
import kst4contest.model.ChatMessage; import kst4contest.model.ChatMessage;
import kst4contest.model.ThreadStateMessage; import kst4contest.model.ThreadStateMessage;
/** /**
* This class is for sending beacons intervalled to the public chat. Gets all * Sends the configured public-chat beacons for both active chat categories.
* preferences and instances via the Chatpreferences-object of the
* Chatcontroller.
* <br/><br/>
* The task will be runned out of the singleton ChatController instance in an
* intervall as specified by the Chatpreferences-instance (typically as
* configured in the xml file.
*
*
* @author prakt
* *
* <p>Both categories deliberately share one timer and therefore the same
* interval. Their enable flags and message templates remain independent. Every
* run reads the current preferences, resolves global message variables and
* sends only the categories which are currently enabled.</p>
*/ */
public class BeaconTask extends TimerTask { public class BeaconTask extends TimerTask {
private ChatController chatController; private static final String THREAD_NICKNAME = "MyBeacon";
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "MyBeacon";
public BeaconTask(ChatController client, ThreadStatusCallback callback) { private final ChatController chatController;
this.callBackToController = callback; private final ThreadStatusCallback callbackToController;
this.chatController = client;
/**
* Creates one execution of the shared beacon timer.
*
* @param chatController controller providing preferences and the TX queue
* @param callbackToController callback used by the thread-status display
*/
public BeaconTask(
ChatController chatController,
ThreadStatusCallback callbackToController
) {
this.chatController = chatController;
this.callbackToController = callbackToController;
} }
@Override @Override
public void run() { public void run() {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("BeaconTask"); Thread.currentThread().setName("BeaconTask");
reportStatus(THREAD_NICKNAME, true, "initialized", false);
MessageVariableResolver variableResolver =
new MessageVariableResolver(chatController.getChatPreferences());
ChatMessage beaconMSG = new ChatMessage(); sendMainCategoryBeacon(variableResolver);
sendSecondCategoryBeacon(variableResolver);
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
beaconMSG.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
beaconMSG.setMessageDirectedToServer(true);
ChatMessage beaconMSG2 = new ChatMessage();
String replaceVariables2 = this.chatController.getChatPreferences().getBcn_beaconTextSecondCat();
replaceVariables2 = replaceVariables2.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables2 = replaceVariables2.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables2 = replaceVariables2.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables2 = replaceVariables2.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
replaceVariables2 = replaceVariables2.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
beaconMSG2.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
beaconMSG2.setMessageDirectedToServer(true);
/**
* beacon 1st Chatcategory
*/
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} }
/** /**
* beacon 2nd Chatcategory * Sends the main-category beacon if it is currently enabled.
*/ */
if (this.chatController.getChatPreferences().isLoginToSecondChatEnabled()) { //only send if 2nd cat enabled private void sendMainCategoryBeacon(MessageVariableResolver variableResolver) {
if (!chatController.getChatPreferences().isBcn_beaconsEnabledMainCat()) {
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledSecondCat()) { reportStatus(THREAD_NICKNAME + " 1", false, "off", false);
return;
beaconMSG2.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables2 + "|0|");
beaconMSG2.setMessageDirectedToServer(true);
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG2);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
} }
String resolvedText = variableResolver.resolveGlobalVariables(
chatController.getChatPreferences().getBcn_beaconTextMainCat()
);
ChatMessage beaconMessage = buildBeaconMessage(
chatController.getChatPreferences()
.getLoginChatCategoryMain()
.getCategoryNumber(),
resolvedText,
"main category"
);
if (beaconMessage == null) {
reportStatus(THREAD_NICKNAME + " 1", false, "invalid text", true);
return;
} }
System.out.println(
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending main-category CQ: "
+ beaconMessage.getMessageText()
);
chatController.getMessageTXBus().add(beaconMessage);
reportStatus(THREAD_NICKNAME + " 1", true, "on", false);
}
/**
* Sends the second-category beacon if the second login and its beacon are
* currently enabled.
*/
private void sendSecondCategoryBeacon(
MessageVariableResolver variableResolver
) {
if (!chatController.getChatPreferences().isLoginToSecondChatEnabled()
|| !chatController.getChatPreferences()
.isBcn_beaconsEnabledSecondCat()) {
reportStatus(THREAD_NICKNAME + " 2", false, "off", false);
return;
}
String resolvedText = variableResolver.resolveGlobalVariables(
chatController.getChatPreferences().getBcn_beaconTextSecondCat()
);
ChatMessage beaconMessage = buildBeaconMessage(
chatController.getChatPreferences()
.getLoginChatCategorySecond()
.getCategoryNumber(),
resolvedText,
"second category"
);
if (beaconMessage == null) {
reportStatus(THREAD_NICKNAME + " 2", false, "invalid text", true);
return;
}
System.out.println(
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending second-category CQ: "
+ beaconMessage.getMessageText()
);
chatController.getMessageTXBus().add(beaconMessage);
reportStatus(THREAD_NICKNAME + " 2", true, "on", false);
}
/**
* Builds the server-directed message after validating the resolved payload.
*
* <p>The resolved text is checked rather than only the configured template
* because inserted values can increase the final message length.</p>
*
* @param categoryNumber ON4KST category number
* @param resolvedText fully resolved beacon payload
* @param categoryDescription text used in diagnostic output
* @return prepared message, or {@code null} if the payload is invalid
*/
private ChatMessage buildBeaconMessage(
int categoryNumber,
String resolvedText,
String categoryDescription
) {
if (resolvedText == null
|| resolvedText.length() > ChatController.MAX_BEACON_TEXT_LENGTH) {
int actualLength = resolvedText == null ? 0 : resolvedText.length();
System.out.println(
"[BeaconTask, Warning]: Beacon for "
+ categoryDescription
+ " was not sent because the resolved text contains "
+ actualLength
+ " characters; maximum is "
+ ChatController.MAX_BEACON_TEXT_LENGTH
+ "."
);
return null;
}
ChatMessage beaconMessage = new ChatMessage();
beaconMessage.setMessageText(
"MSG|" + categoryNumber + "|0|" + resolvedText + "|0|"
);
beaconMessage.setMessageDirectedToServer(true);
return beaconMessage;
}
/**
* Forwards one state update to the existing thread-status display.
*/
private void reportStatus(
String threadName,
boolean running,
String information,
boolean criticalState
) {
ThreadStateMessage stateMessage = new ThreadStateMessage(
threadName,
running,
information,
criticalState
);
callbackToController.onThreadStatus(THREAD_NICKNAME, stateMessage);
}
} }
File diff suppressed because it is too large Load Diff
@@ -36,8 +36,8 @@ public class DBController {
* Number of milliseconds after which worked/not-QRV data is considered outdated * Number of milliseconds after which worked/not-QRV data is considered outdated
* and therefore automatically reset. * and therefore automatically reset.
*/ */
private static final long WORKED_DATA_EXPIRATION_IN_MILLISECONDS = 65L * 60L * 60L * 1000L; private static final long WORKED_DATA_EXPIRATION_IN_MILLISECONDS =
3L * 24L * 60L * 60L * 1000L;
/** /**
* Database schema version that includes the raw-callsign normalization migration * Database schema version that includes the raw-callsign normalization migration
* marker. The marker is stored in SQLite PRAGMA user_version so the expensive * marker. The marker is stored in SQLite PRAGMA user_version so the expensive
@@ -145,6 +145,7 @@ public class DBController {
createWorkedGrossFieldTableIfRequired(); createWorkedGrossFieldTableIfRequired();
versionUpdateOfDBCheckAndChangeV11ToV12(); versionUpdateOfDBCheckAndChangeV11ToV12();
versionUpdateOfDBCheckAndChangeV12ToV13(); versionUpdateOfDBCheckAndChangeV12ToV13();
versionUpdateOfDBCheckAndChangeV13ToV14();
if (helper_isDatabaseSchemaVersionOlderThanCurrent() || helper_isCallsignNormalizationMigrationRequired()) { if (helper_isDatabaseSchemaVersionOlderThanCurrent() || helper_isCallsignNormalizationMigrationRequired()) {
normalizeStoredCallsignsToRawCallsigns(); normalizeStoredCallsignsToRawCallsigns();
@@ -230,6 +231,8 @@ public class DBController {
+ "worked3400 BOOLEAN DEFAULT 0, " + "worked3400 BOOLEAN DEFAULT 0, "
+ "worked5600 BOOLEAN DEFAULT 0, " + "worked5600 BOOLEAN DEFAULT 0, "
+ "worked10G BOOLEAN DEFAULT 0, " + "worked10G BOOLEAN DEFAULT 0, "
+ "worked50 BOOLEAN DEFAULT 0, "
+ "worked70 BOOLEAN DEFAULT 0, "
+ "notQRV144 BOOLEAN DEFAULT 0, " + "notQRV144 BOOLEAN DEFAULT 0, "
+ "notQRV432 BOOLEAN DEFAULT 0, " + "notQRV432 BOOLEAN DEFAULT 0, "
+ "notQRV1240 BOOLEAN DEFAULT 0, " + "notQRV1240 BOOLEAN DEFAULT 0, "
@@ -237,6 +240,8 @@ public class DBController {
+ "notQRV3400 BOOLEAN DEFAULT 0, " + "notQRV3400 BOOLEAN DEFAULT 0, "
+ "notQRV5600 BOOLEAN DEFAULT 0, " + "notQRV5600 BOOLEAN DEFAULT 0, "
+ "notQRV10G BOOLEAN DEFAULT 0, " + "notQRV10G BOOLEAN DEFAULT 0, "
+ "notQRV50 BOOLEAN DEFAULT 0, "
+ "notQRV70 BOOLEAN DEFAULT 0, "
+ "lastFlagsChangeEpochMs INTEGER DEFAULT 0" + "lastFlagsChangeEpochMs INTEGER DEFAULT 0"
+ ");"; + ");";
@@ -303,6 +308,22 @@ public class DBController {
} }
} }
/**
* Updates old v1.3 databases to the v1.4 schema by adding the worked/not-QRV
* columns for the 50 MHz and 70 MHz bands if they are missing.
*/
public synchronized void versionUpdateOfDBCheckAndChangeV13ToV14() {
try {
ensureColumnExists("ChatMember", "worked50", "BOOLEAN DEFAULT 0");
ensureColumnExists("ChatMember", "worked70", "BOOLEAN DEFAULT 0");
ensureColumnExists("ChatMember", "notQRV50", "BOOLEAN DEFAULT 0");
ensureColumnExists("ChatMember", "notQRV70", "BOOLEAN DEFAULT 0");
} catch (SQLException e) {
throw new RuntimeException("[DBH, ERROR:] Could not migrate database from v1.3 to v1.4", e);
}
}
/** /**
* Adds a missing column to an existing table. This method is used for safe schema * Adds a missing column to an existing table. This method is used for safe schema
* upgrades on customer systems which still contain older database files. * upgrades on customer systems which still contain older database files.
@@ -452,7 +473,11 @@ public class DBController {
targetChatMember.setWorked3400(targetChatMember.isWorked3400() || sourceChatMember.isWorked3400()); targetChatMember.setWorked3400(targetChatMember.isWorked3400() || sourceChatMember.isWorked3400());
targetChatMember.setWorked5600(targetChatMember.isWorked5600() || sourceChatMember.isWorked5600()); targetChatMember.setWorked5600(targetChatMember.isWorked5600() || sourceChatMember.isWorked5600());
targetChatMember.setWorked10G(targetChatMember.isWorked10G() || sourceChatMember.isWorked10G()); targetChatMember.setWorked10G(targetChatMember.isWorked10G() || sourceChatMember.isWorked10G());
targetChatMember.setWorked50(targetChatMember.isWorked50() || sourceChatMember.isWorked50());
targetChatMember.setWorked70(targetChatMember.isWorked70() || sourceChatMember.isWorked70());
targetChatMember.setQrv50(targetChatMember.isQrv50() && sourceChatMember.isQrv50());
targetChatMember.setQrv70(targetChatMember.isQrv70() && sourceChatMember.isQrv70());
targetChatMember.setQrv144(targetChatMember.isQrv144() && sourceChatMember.isQrv144()); targetChatMember.setQrv144(targetChatMember.isQrv144() && sourceChatMember.isQrv144());
targetChatMember.setQrv432(targetChatMember.isQrv432() && sourceChatMember.isQrv432()); targetChatMember.setQrv432(targetChatMember.isQrv432() && sourceChatMember.isQrv432());
targetChatMember.setQrv1240(targetChatMember.isQrv1240() && sourceChatMember.isQrv1240()); targetChatMember.setQrv1240(targetChatMember.isQrv1240() && sourceChatMember.isQrv1240());
@@ -491,6 +516,8 @@ public class DBController {
+ "worked3400 = 0, " + "worked3400 = 0, "
+ "worked5600 = 0, " + "worked5600 = 0, "
+ "worked10G = 0, " + "worked10G = 0, "
+ "worked50 = 0, "
+ "worked70 = 0, "
+ "notQRV144 = 0, " + "notQRV144 = 0, "
+ "notQRV432 = 0, " + "notQRV432 = 0, "
+ "notQRV1240 = 0, " + "notQRV1240 = 0, "
@@ -498,6 +525,8 @@ public class DBController {
+ "notQRV3400 = 0, " + "notQRV3400 = 0, "
+ "notQRV5600 = 0, " + "notQRV5600 = 0, "
+ "notQRV10G = 0, " + "notQRV10G = 0, "
+ "notQRV50 = 0, "
+ "notQRV70 = 0, "
+ "lastFlagsChangeEpochMs = 0 " + "lastFlagsChangeEpochMs = 0 "
+ "WHERE lastFlagsChangeEpochMs > 0 AND lastFlagsChangeEpochMs < ?;"; + "WHERE lastFlagsChangeEpochMs > 0 AND lastFlagsChangeEpochMs < ?;";
@@ -534,9 +563,9 @@ public class DBController {
String insertOrUpdateSql = String insertOrUpdateSql =
"INSERT INTO ChatMember (" "INSERT INTO ChatMember ("
+ "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, " + "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, worked50, worked70, "
+ "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, lastFlagsChangeEpochMs" + "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, notQRV50, notQRV70, lastFlagsChangeEpochMs"
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
+ "ON CONFLICT(callsign) DO UPDATE SET " + "ON CONFLICT(callsign) DO UPDATE SET "
+ "qra = excluded.qra, " + "qra = excluded.qra, "
+ "name = excluded.name, " + "name = excluded.name, "
@@ -557,14 +586,18 @@ public class DBController {
preparedStatement.setInt(10, helper_booleanIntConverter(chatMemberToStore.isWorked3400())); preparedStatement.setInt(10, helper_booleanIntConverter(chatMemberToStore.isWorked3400()));
preparedStatement.setInt(11, helper_booleanIntConverter(chatMemberToStore.isWorked5600())); preparedStatement.setInt(11, helper_booleanIntConverter(chatMemberToStore.isWorked5600()));
preparedStatement.setInt(12, helper_booleanIntConverter(chatMemberToStore.isWorked10G())); preparedStatement.setInt(12, helper_booleanIntConverter(chatMemberToStore.isWorked10G()));
preparedStatement.setInt(13, helper_booleanIntConverter(!chatMemberToStore.isQrv144())); preparedStatement.setInt(13, helper_booleanIntConverter(chatMemberToStore.isWorked50()));
preparedStatement.setInt(14, helper_booleanIntConverter(!chatMemberToStore.isQrv432())); preparedStatement.setInt(14, helper_booleanIntConverter(chatMemberToStore.isWorked70()));
preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv1240())); preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv144()));
preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv2300())); preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv432()));
preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv3400())); preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv1240()));
preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv5600())); preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv2300()));
preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv10G())); preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
preparedStatement.setLong(20, resolvedLastFlagsChangeEpochMs); preparedStatement.setInt(20, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
preparedStatement.setInt(21, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
preparedStatement.setInt(22, helper_booleanIntConverter(!chatMemberToStore.isQrv50()));
preparedStatement.setInt(23, helper_booleanIntConverter(!chatMemberToStore.isQrv70()));
preparedStatement.setLong(24, resolvedLastFlagsChangeEpochMs);
preparedStatement.executeUpdate(); preparedStatement.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
System.err.println("[DBH, ERROR:] Chatmember could not been stored."); System.err.println("[DBH, ERROR:] Chatmember could not been stored.");
@@ -646,8 +679,8 @@ public class DBController {
String resetAllWorkedDataSql = String resetAllWorkedDataSql =
"UPDATE ChatMember SET " "UPDATE ChatMember SET "
+ "worked = 0, worked144 = 0, worked432 = 0, worked1240 = 0, worked2300 = 0, worked3400 = 0, worked5600 = 0, worked10G = 0, " + "worked = 0, worked144 = 0, worked432 = 0, worked1240 = 0, worked2300 = 0, worked3400 = 0, worked5600 = 0, worked10G = 0, worked50 = 0, worked70 = 0, "
+ "notQRV144 = 0, notQRV432 = 0, notQRV1240 = 0, notQRV2300 = 0, notQRV3400 = 0, notQRV5600 = 0, notQRV10G = 0, " + "notQRV144 = 0, notQRV432 = 0, notQRV1240 = 0, notQRV2300 = 0, notQRV3400 = 0, notQRV5600 = 0, notQRV10G = 0, notQRV50 = 0, notQRV70 = 0, "
+ "lastFlagsChangeEpochMs = 0;"; + "lastFlagsChangeEpochMs = 0;";
try (Statement statement = connection.createStatement()) { try (Statement statement = connection.createStatement()) {
@@ -781,6 +814,8 @@ public class DBController {
+ "notQRV3400 = ?, " + "notQRV3400 = ?, "
+ "notQRV5600 = ?, " + "notQRV5600 = ?, "
+ "notQRV10G = ?, " + "notQRV10G = ?, "
+ "notQRV50 = ?, "
+ "notQRV70 = ?, "
+ "lastFlagsChangeEpochMs = ? " + "lastFlagsChangeEpochMs = ? "
+ "WHERE callsign = ?;"; + "WHERE callsign = ?;";
@@ -792,8 +827,10 @@ public class DBController {
preparedStatement.setInt(5, helper_booleanIntConverter(!chatMemberToStore.isQrv3400())); preparedStatement.setInt(5, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
preparedStatement.setInt(6, helper_booleanIntConverter(!chatMemberToStore.isQrv5600())); preparedStatement.setInt(6, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
preparedStatement.setInt(7, helper_booleanIntConverter(!chatMemberToStore.isQrv10G())); preparedStatement.setInt(7, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
preparedStatement.setLong(8, System.currentTimeMillis()); preparedStatement.setInt(8, helper_booleanIntConverter(!chatMemberToStore.isQrv50()));
preparedStatement.setString(9, chatMemberToStore.getCallSignRaw()); preparedStatement.setInt(9, helper_booleanIntConverter(!chatMemberToStore.isQrv70()));
preparedStatement.setLong(10, System.currentTimeMillis());
preparedStatement.setString(11, chatMemberToStore.getCallSignRaw());
int affectedRows = preparedStatement.executeUpdate(); int affectedRows = preparedStatement.executeUpdate();
@@ -821,9 +858,9 @@ public class DBController {
String upsertCompleteRowSql = String upsertCompleteRowSql =
"INSERT INTO ChatMember (" "INSERT INTO ChatMember ("
+ "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, " + "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, worked50, worked70, "
+ "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, lastFlagsChangeEpochMs" + "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, notQRV50, notQRV70, lastFlagsChangeEpochMs"
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) " + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
+ "ON CONFLICT(callsign) DO UPDATE SET " + "ON CONFLICT(callsign) DO UPDATE SET "
+ "qra = excluded.qra, " + "qra = excluded.qra, "
+ "name = excluded.name, " + "name = excluded.name, "
@@ -836,6 +873,8 @@ public class DBController {
+ "worked3400 = excluded.worked3400, " + "worked3400 = excluded.worked3400, "
+ "worked5600 = excluded.worked5600, " + "worked5600 = excluded.worked5600, "
+ "worked10G = excluded.worked10G, " + "worked10G = excluded.worked10G, "
+ "worked50 = excluded.worked50, "
+ "worked70 = excluded.worked70, "
+ "notQRV144 = excluded.notQRV144, " + "notQRV144 = excluded.notQRV144, "
+ "notQRV432 = excluded.notQRV432, " + "notQRV432 = excluded.notQRV432, "
+ "notQRV1240 = excluded.notQRV1240, " + "notQRV1240 = excluded.notQRV1240, "
@@ -843,6 +882,8 @@ public class DBController {
+ "notQRV3400 = excluded.notQRV3400, " + "notQRV3400 = excluded.notQRV3400, "
+ "notQRV5600 = excluded.notQRV5600, " + "notQRV5600 = excluded.notQRV5600, "
+ "notQRV10G = excluded.notQRV10G, " + "notQRV10G = excluded.notQRV10G, "
+ "notQRV50 = excluded.notQRV50, "
+ "notQRV70 = excluded.notQRV70, "
+ "lastFlagsChangeEpochMs = excluded.lastFlagsChangeEpochMs;"; + "lastFlagsChangeEpochMs = excluded.lastFlagsChangeEpochMs;";
try (PreparedStatement preparedStatement = connection.prepareStatement(upsertCompleteRowSql)) { try (PreparedStatement preparedStatement = connection.prepareStatement(upsertCompleteRowSql)) {
@@ -858,14 +899,18 @@ public class DBController {
preparedStatement.setInt(10, helper_booleanIntConverter(chatMemberToStore.isWorked3400())); preparedStatement.setInt(10, helper_booleanIntConverter(chatMemberToStore.isWorked3400()));
preparedStatement.setInt(11, helper_booleanIntConverter(chatMemberToStore.isWorked5600())); preparedStatement.setInt(11, helper_booleanIntConverter(chatMemberToStore.isWorked5600()));
preparedStatement.setInt(12, helper_booleanIntConverter(chatMemberToStore.isWorked10G())); preparedStatement.setInt(12, helper_booleanIntConverter(chatMemberToStore.isWorked10G()));
preparedStatement.setInt(13, helper_booleanIntConverter(!chatMemberToStore.isQrv144())); preparedStatement.setInt(13, helper_booleanIntConverter(chatMemberToStore.isWorked50()));
preparedStatement.setInt(14, helper_booleanIntConverter(!chatMemberToStore.isQrv432())); preparedStatement.setInt(14, helper_booleanIntConverter(chatMemberToStore.isWorked70()));
preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv1240())); preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv144()));
preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv2300())); preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv432()));
preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv3400())); preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv1240()));
preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv5600())); preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv2300()));
preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv10G())); preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
preparedStatement.setLong(20, chatMemberToStore.getLastFlagsChangeEpochMs()); preparedStatement.setInt(20, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
preparedStatement.setInt(21, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
preparedStatement.setInt(22, helper_booleanIntConverter(!chatMemberToStore.isQrv50()));
preparedStatement.setInt(23, helper_booleanIntConverter(!chatMemberToStore.isQrv70()));
preparedStatement.setLong(24, chatMemberToStore.getLastFlagsChangeEpochMs());
preparedStatement.executeUpdate(); preparedStatement.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
throw new RuntimeException("[DBH, ERROR:] Could not rebuild normalized ChatMember row", e); throw new RuntimeException("[DBH, ERROR:] Could not rebuild normalized ChatMember row", e);
@@ -894,6 +939,8 @@ public class DBController {
builtChatMember.setWorked3400(helper_IntToBooleanConverter(resultSet.getInt("worked3400"))); builtChatMember.setWorked3400(helper_IntToBooleanConverter(resultSet.getInt("worked3400")));
builtChatMember.setWorked5600(helper_IntToBooleanConverter(resultSet.getInt("worked5600"))); builtChatMember.setWorked5600(helper_IntToBooleanConverter(resultSet.getInt("worked5600")));
builtChatMember.setWorked10G(helper_IntToBooleanConverter(resultSet.getInt("worked10G"))); builtChatMember.setWorked10G(helper_IntToBooleanConverter(resultSet.getInt("worked10G")));
builtChatMember.setWorked50(helper_IntToBooleanConverter(resultSet.getInt("worked50")));
builtChatMember.setWorked70(helper_IntToBooleanConverter(resultSet.getInt("worked70")));
builtChatMember.setQrv144(!helper_IntToBooleanConverter(resultSet.getInt("notQRV144"))); builtChatMember.setQrv144(!helper_IntToBooleanConverter(resultSet.getInt("notQRV144")));
builtChatMember.setQrv432(!helper_IntToBooleanConverter(resultSet.getInt("notQRV432"))); builtChatMember.setQrv432(!helper_IntToBooleanConverter(resultSet.getInt("notQRV432")));
builtChatMember.setQrv1240(!helper_IntToBooleanConverter(resultSet.getInt("notQRV1240"))); builtChatMember.setQrv1240(!helper_IntToBooleanConverter(resultSet.getInt("notQRV1240")));
@@ -901,6 +948,8 @@ public class DBController {
builtChatMember.setQrv3400(!helper_IntToBooleanConverter(resultSet.getInt("notQRV3400"))); builtChatMember.setQrv3400(!helper_IntToBooleanConverter(resultSet.getInt("notQRV3400")));
builtChatMember.setQrv5600(!helper_IntToBooleanConverter(resultSet.getInt("notQRV5600"))); builtChatMember.setQrv5600(!helper_IntToBooleanConverter(resultSet.getInt("notQRV5600")));
builtChatMember.setQrv10G(!helper_IntToBooleanConverter(resultSet.getInt("notQRV10G"))); builtChatMember.setQrv10G(!helper_IntToBooleanConverter(resultSet.getInt("notQRV10G")));
builtChatMember.setQrv50(!helper_IntToBooleanConverter(resultSet.getInt("notQRV50")));
builtChatMember.setQrv70(!helper_IntToBooleanConverter(resultSet.getInt("notQRV70")));
builtChatMember.setLastFlagsChangeEpochMs(resultSet.getLong("lastFlagsChangeEpochMs")); builtChatMember.setLastFlagsChangeEpochMs(resultSet.getLong("lastFlagsChangeEpochMs"));
return builtChatMember; return builtChatMember;
@@ -922,6 +971,8 @@ public class DBController {
targetChatMember.setWorked3400(sourceChatMember.isWorked3400()); targetChatMember.setWorked3400(sourceChatMember.isWorked3400());
targetChatMember.setWorked5600(sourceChatMember.isWorked5600()); targetChatMember.setWorked5600(sourceChatMember.isWorked5600());
targetChatMember.setWorked10G(sourceChatMember.isWorked10G()); targetChatMember.setWorked10G(sourceChatMember.isWorked10G());
targetChatMember.setWorked50(sourceChatMember.isWorked50());
targetChatMember.setWorked70(sourceChatMember.isWorked70());
targetChatMember.setQrv144(sourceChatMember.isQrv144()); targetChatMember.setQrv144(sourceChatMember.isQrv144());
targetChatMember.setQrv432(sourceChatMember.isQrv432()); targetChatMember.setQrv432(sourceChatMember.isQrv432());
targetChatMember.setQrv1240(sourceChatMember.isQrv1240()); targetChatMember.setQrv1240(sourceChatMember.isQrv1240());
@@ -929,6 +980,8 @@ public class DBController {
targetChatMember.setQrv3400(sourceChatMember.isQrv3400()); targetChatMember.setQrv3400(sourceChatMember.isQrv3400());
targetChatMember.setQrv5600(sourceChatMember.isQrv5600()); targetChatMember.setQrv5600(sourceChatMember.isQrv5600());
targetChatMember.setQrv10G(sourceChatMember.isQrv10G()); targetChatMember.setQrv10G(sourceChatMember.isQrv10G());
targetChatMember.setQrv50(sourceChatMember.isQrv50());
targetChatMember.setQrv70(sourceChatMember.isQrv70());
targetChatMember.setLastFlagsChangeEpochMs(sourceChatMember.getLastFlagsChangeEpochMs()); targetChatMember.setLastFlagsChangeEpochMs(sourceChatMember.getLastFlagsChangeEpochMs());
} }
@@ -954,6 +1007,10 @@ public class DBController {
return "worked5600"; return "worked5600";
} else if (chatMemberToStore.isWorked10G()) { } else if (chatMemberToStore.isWorked10G()) {
return "worked10G"; return "worked10G";
} else if (chatMemberToStore.isWorked50()) {
return "worked50";
} else if (chatMemberToStore.isWorked70()) {
return "worked70";
} }
return null; return null;
@@ -996,13 +1053,17 @@ public class DBController {
|| chatMemberToStore.isWorked3400() || chatMemberToStore.isWorked3400()
|| chatMemberToStore.isWorked5600() || chatMemberToStore.isWorked5600()
|| chatMemberToStore.isWorked10G() || chatMemberToStore.isWorked10G()
|| chatMemberToStore.isWorked50()
|| chatMemberToStore.isWorked70()
|| !chatMemberToStore.isQrv144() || !chatMemberToStore.isQrv144()
|| !chatMemberToStore.isQrv432() || !chatMemberToStore.isQrv432()
|| !chatMemberToStore.isQrv1240() || !chatMemberToStore.isQrv1240()
|| !chatMemberToStore.isQrv2300() || !chatMemberToStore.isQrv2300()
|| !chatMemberToStore.isQrv3400() || !chatMemberToStore.isQrv3400()
|| !chatMemberToStore.isQrv5600() || !chatMemberToStore.isQrv5600()
|| !chatMemberToStore.isQrv10G(); || !chatMemberToStore.isQrv10G()
|| !chatMemberToStore.isQrv50()
|| !chatMemberToStore.isQrv70();
} }
/** /**
@@ -1,238 +1,363 @@
package kst4contest.controller; package kst4contest.controller;
import kst4contest.model.ChatMember; import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ThreadStateMessage; import kst4contest.model.ThreadStateMessage;
import java.io.*; import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.*; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class DXClusterThreadPooledServer implements Runnable { public class DXClusterThreadPooledServer implements Runnable {
private static final Logger LOGGER = Logger.getLogger(DXClusterThreadPooledServer.class.getName()); private static final Logger LOGGER =
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients Logger.getLogger(DXClusterThreadPooledServer.class.getName());
private static final String THREAD_NICKNAME = "DXCluster-Server";
private ThreadStatusCallback callBackToController; private final List<Socket> clientSockets =
private String ThreadNickName = "DXCluster-Server"; Collections.synchronizedList(new ArrayList<>());
ChatController chatController = null;
protected int serverPort = 8080; private final ChatController chatController;
protected ServerSocket serverSocket = null; private final ThreadStatusCallback callBackToController;
protected boolean isStopped = false; private final int serverPort;
protected Thread runningThread= null;
protected ExecutorService threadPool = private final ExecutorService threadPool =
Executors.newFixedThreadPool(10); Executors.newFixedThreadPool(10);
Socket clientSocket;
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){ private final ScheduledExecutorService keepAliveExecutor =
Executors.newSingleThreadScheduledExecutor();
private volatile boolean stopped;
private ServerSocket serverSocket;
public DXClusterThreadPooledServer(
int port,
ChatController chatController,
ThreadStatusCallback callback
) {
this.serverPort = port; this.serverPort = port;
this.chatController = chatController; this.chatController = chatController;
this.callBackToController = callback; this.callBackToController = callback;
} }
@Override
public void run() { public void run() {
Thread.currentThread().setName("DXCluster-thread-pooled-server");
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
synchronized(this){
this.runningThread = Thread.currentThread();
runningThread.setName("DXCluster-thread-pooled-server");
}
openServerSocket();
while(! isStopped()){
clientSocket = null;
try { try {
clientSocket = this.serverSocket.accept(); serverSocket = new ServerSocket(serverPort);
synchronized(clientSockets) { if (stopped) {
clientSockets.add(clientSocket); // add dx cluster client to the "clients list" for broadcasting return;
} }
} catch (IOException e) { callBackToController.onThreadStatus(
if(isStopped()) { THREAD_NICKNAME,
System.out.println("Server Stopped.") ; new ThreadStateMessage(
break; THREAD_NICKNAME,
} true,
throw new RuntimeException( "Listening on TCP port " + serverPort,
"Error accepting client connection", e); false
} )
);
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController); keepAliveExecutor.scheduleAtFixedRate(
this::sendKeepAlive,
30,
30,
TimeUnit.SECONDS
);
this.threadPool.execute(worker); while (!stopped) {
try {
Socket clientSocket = serverSocket.accept();
clientSockets.add(clientSocket);
threadPool.execute(
new DXClusterServerWorkerRunnable(
clientSocket,
clientSockets
)
);
} catch (IOException exception) {
if (!stopped) {
LOGGER.log(
Level.SEVERE,
"Error accepting DX Cluster client connection",
exception
);
} }
this.threadPool.shutdown();
System.out.println("Server Stopped.") ;
} }
}
} catch (IOException exception) {
if (!stopped) {
LOGGER.log(
Level.SEVERE,
"Cannot open DX Cluster TCP port " + serverPort,
exception
);
private synchronized boolean isStopped() { callBackToController.onThreadStatus(
return this.isStopped; THREAD_NICKNAME,
new ThreadStateMessage(
THREAD_NICKNAME,
false,
"Cannot open TCP port "
+ serverPort
+ ": "
+ exception.getMessage(),
true
)
);
}
} finally {
closeServerSocket();
closeClientSockets();
keepAliveExecutor.shutdownNow();
threadPool.shutdownNow();
}
} }
public synchronized void stop() { public synchronized void stop() {
this.isStopped = true; stopped = true;
try { closeServerSocket();
this.serverSocket.close(); closeClientSockets();
synchronized(clientSockets) { keepAliveExecutor.shutdownNow();
for (Socket socket : clientSockets) { threadPool.shutdownNow();
socket.close(); // close all client connections
}
}
} catch (IOException e) {
throw new RuntimeException("DXCCSERVER Error closing server", e);
}
} }
private void openServerSocket() { public boolean hasConnectedClients() {
try { synchronized (clientSockets) {
this.serverSocket = new ServerSocket(this.serverPort); removeClosedClients();
} catch (IOException e) { return !clientSockets.isEmpty();
throw new RuntimeException("DXCCSERVER Cannot open port ", e);
} }
} }
/** /**
* Sends a DX cluster message to ALL connected log programs via telnet, returns true if sent * Sends one DX Cluster spot to all currently connected clients.
* *
* @param aChatMember * @return true if the spot was delivered to at least one client
* @return boolean true if message had been sent
*/ */
public boolean broadcastSingleDXClusterEntryToLoggers(ChatMember aChatMember) { public boolean broadcastSingleDXClusterEntryToLoggers(
synchronized(clientSockets) { ChatMember chatMember
) {
System.out.println("DXClusterSrvr: broadcasting message to clients: " + clientSockets.size()); final String clusterMessage;
try { try {
String frequency = Utils4KST.normalizeFrequencyString(
chatMember.getFrequency().getValue(),
chatController
.getChatPreferences()
.getNotify_optionalFrequencyPrefix()
);
System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue()); clusterMessage =
System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " "); "DX de "
} catch (Exception e) { + chatController
LOGGER.log(Level.SEVERE, "DXCThPooledServer: Error accessing value in chatmember object", e); .getChatPreferences()
} .getNotify_DXCSrv_SpottersCallSign()
.getValue()
for (Socket socket : clientSockets) { + ": "
+ frequency
try { + " "
+ chatMember.getCallSign().toUpperCase()
OutputStream output = socket.getOutputStream(); + " "
+ chatMember.getQra().toUpperCase()
String singleDXClusterMessage = "DX de "; + " "
+ new Utils4KST()
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": "; .time_generateCurrenthhmmZTimeStringForClusterMessage()
+ ((char) 7)
+ ((char) 7)
+ "\r\n";
} catch (Exception exception) {
singleDXClusterMessage += this.chatController.getChatPreferences().getNotify_DXCSrv_SpottersCallSign().getValue() + ": "; LOGGER.log(
singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " "; Level.SEVERE,
singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " "; //we need such an amount of spaces for n1mm to work, otherwise bullshit happens "Cannot build DX Cluster message",
singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " "; exception
singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n"; );
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
// singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
output.write((singleDXClusterMessage).getBytes());
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last msg to " + clientSockets.size() + " Cluster Clients:\n" + singleDXClusterMessage, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] broadcasting DXC-message to clients went wrong", e);
return false; return false;
} }
}
} int deliveredClients = 0;
return true; //if message had been sent, return true for "ok"
synchronized (clientSockets) {
Iterator<Socket> iterator = clientSockets.iterator();
while (iterator.hasNext()) {
Socket socket = iterator.next();
if (socket == null || socket.isClosed()) {
iterator.remove();
continue;
} }
try {
OutputStream output = socket.getOutputStream();
output.write(
clusterMessage.getBytes(
StandardCharsets.US_ASCII
)
);
output.flush();
deliveredClients++;
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"DX Cluster client disconnected while sending a spot",
exception
);
closeSocket(socket);
iterator.remove();
}
}
}
if (deliveredClients > 0) {
callBackToController.onThreadStatus(
THREAD_NICKNAME,
new ThreadStateMessage(
THREAD_NICKNAME,
true,
"Last spot sent to "
+ deliveredClients
+ " DX Cluster client(s):\n"
+ clusterMessage,
false
)
);
}
return deliveredClients > 0;
}
private void sendKeepAlive() {
synchronized (clientSockets) {
Iterator<Socket> iterator = clientSockets.iterator();
while (iterator.hasNext()) {
Socket socket = iterator.next();
if (socket == null || socket.isClosed()) {
iterator.remove();
continue;
}
try {
OutputStream output = socket.getOutputStream();
output.write(
"\r\n".getBytes(StandardCharsets.US_ASCII)
);
output.flush();
} catch (IOException exception) {
closeSocket(socket);
iterator.remove();
}
}
}
}
private void removeClosedClients() {
clientSockets.removeIf(
socket -> socket == null || socket.isClosed()
);
}
private synchronized void closeServerSocket() {
if (serverSocket == null || serverSocket.isClosed()) {
return;
}
try {
serverSocket.close();
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"Error closing DX Cluster server socket",
exception
);
}
}
private void closeClientSockets() {
synchronized (clientSockets) {
for (Socket socket : clientSockets) {
closeSocket(socket);
}
clientSockets.clear();
}
}
private static void closeSocket(Socket socket) {
if (socket == null || socket.isClosed()) {
return;
}
try {
socket.close();
} catch (IOException ignored) {
// The connection is already unusable.
}
}
} }
class DXClusterServerWorkerRunnable implements Runnable { class DXClusterServerWorkerRunnable implements Runnable {
private static final Logger LOGGER = Logger.getLogger(DXClusterServerWorkerRunnable.class.getName()); private static final Logger LOGGER =
protected Socket clientSocket = null; Logger.getLogger(DXClusterServerWorkerRunnable.class.getName());
protected String serverText = null;
private ChatController client = null;
private List<Socket> dxClusterClientSocketsConnectedList;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) { private final Socket clientSocket;
private final List<Socket> clientSockets;
DXClusterServerWorkerRunnable(
Socket clientSocket,
List<Socket> clientSockets
) {
this.clientSocket = clientSocket; this.clientSocket = clientSocket;
this.serverText = serverText; this.clientSockets = clientSockets;
this.client = chatController;
this.dxClusterClientSocketsConnectedList = clientSockets;
this.callBackToController = callback;
} }
public void run() {
try {
OutputStream output = clientSocket.getOutputStream();
dxClusterClientSocketsConnectedList.add(clientSocket);
Timer dXCkeepAliveTimer = new Timer();
dXCkeepAliveTimer.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() {
try {
OutputStream output = clientSocket.getOutputStream();
output.write(
"login: ".getBytes(StandardCharsets.US_ASCII)
);
output.flush();
StringBuilder connectedClients = new StringBuilder(); //only for statistics System.out.println(
"[DXClusterServer] New client connected: "
+ clientSocket.getInetAddress()
);
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
"Cannot initialise DX Cluster client connection",
exception
);
for (Socket socket : dxClusterClientSocketsConnectedList) { synchronized (clientSockets) {
clientSockets.remove(clientSocket);
connectedClients.append(socket.getInetAddress()).append("\n"); }
try { try {
OutputStream output = socket.getOutputStream(); clientSocket.close();
output.write(("\r\n").getBytes()); } catch (IOException ignored) {
// The connection is already unusable.
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] keep-alive broadcast to client failed", e);
dXCkeepAliveTimer.purge();
try {
socket.close();
} catch (IOException ex) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error closing client socket", ex);
}
finally {
this.cancel();
}
dxClusterClientSocketsConnectedList.remove(socket); //if socket is closed by client, remove it from the broadcast list and close it
}
}
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
}, 30000, 30000);
output.write(("login: ").getBytes()); //say hello to the client, it will answer with a callsign
System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error in worker runnable", e);
} finally {
synchronized(dxClusterClientSocketsConnectedList) {
dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung
} }
} }
} }
} }
@@ -46,14 +46,56 @@ public class MessageBusManagementThread extends Thread {
private 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)?)"; private 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)?)";
private final String PTRN_QRG_CAT3 = "(([0-9]{3,5}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; private final String PTRN_QRG_CAT3 = "(([0-9]{3,5}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
/*
* Frequency formats handled by the smart parser:
*
* Group 1: full frequencies, for example 144.210 or 10368.100
* Group 2: relative frequencies with a separator, for example .210 or ,210
* Group 3: bare three-digit values, for example 210
*
* Group 3 is deliberately subjected to an additional context check. Without
* that check, ordinary chat values such as 599 or a bare band name such as 144
* 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{3}(?:[.,]\\d{1,3})?)(?![\\d])"
+ "|(?<=\\s|^)(\\d{3})(?=\\s|$)"
);
// ==== Autoanswer Flood/Pingpong Protection ==== /*
private static final String AUTOANSWER_PREFIX = ApplicationConstants.AUTOANSWER_PREFIX; // hard-coded marker (user can't remove it) * A bare three-digit value is accepted only when the nearby text makes its
private static final long AUTOANSWER_COOLDOWN_MS = 45_000L; // 45_000L = 45s * meaning sufficiently clear. Examples:
*
* qrg 210
* QRG: 210
* freq is 210
* on 210
* pse 210
* at 210
* 210 MHz
* 210 qrg
*/
private static final Pattern BARE_FREQUENCY_PREFIX_CONTEXT_PATTERN =
Pattern.compile(
"(?i)\\b(?:qrg|freq(?:uency)?|on|pse|at)\\b"
+ "\\s*(?:is\\s*)?[:=@-]?\\s*$"
);
// Cooldown per opponent station (and ChatCategory) only setted if this client sends private static final Pattern BARE_FREQUENCY_SUFFIX_CONTEXT_PATTERN =
Pattern.compile(
"(?i)^\\s*(?:mhz|qrg|freq(?:uency)?)\\b"
);
private static final int BARE_FREQUENCY_CONTEXT_CHARACTERS = 32;
// ==== Auto-answer flood/ping-pong protection ====
private static final String AUTOANSWER_PREFIX = ApplicationConstants.AUTOANSWER_PREFIX;
private static final long AUTOANSWER_COOLDOWN_MS = 120_000L; // two minutes
// Cooldown per remote station and chat category; updated only after this client sends.
private final Hashtable<String, Long> lastLocalAutoAnswerPerRemoteMs = new Hashtable<>(); private final Hashtable<String, Long> lastLocalAutoAnswerPerRemoteMs = new Hashtable<>();
// BufferedWriter bufwrtrDBGMSGOut; // BufferedWriter bufwrtrDBGMSGOut;
// private String text; // private String text;
@@ -201,143 +243,218 @@ public class MessageBusManagementThread extends Thread {
} }
/** /**
* Smart Frequency Parser (V1.32) * Detects complete and relative frequencies in a chat message and stores the
* Replaces the old RegEx logic. * result on the sender.
* Features: *
* 1. Handles full frequencies (144.210) and short forms (.210, 210). * <p>Complete frequencies determine their band directly. Relative frequencies
* 2. Handles extended precision/weird formatting (144.210.10, 144,210,10). * first use the sender's most recent band context if that context is not older
* 3. Prioritizes USER CONTEXT (History) over GLOBAL CONTEXT (Preferences). * than 30 minutes. Only when no suitable sender context exists does the parser
* use the globally configured fallback band.</p>
*
* <p>A relative value beginning with a dot or comma is sufficiently explicit
* on its own. A bare three-digit value is ambiguous and is therefore accepted
* only with nearby frequency-related text.</p>
*
* @param message message whose text is inspected
* @param prefs preferences containing the global fallback band
*/ */
private void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) { private void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
if (message == null || message.getMessageText() == null) {
// Regex Explanation: return;
// Part 1 (Full): Start (not digit), 3-5 digits, sep, 1-3 digits, OPTIONAL (sep, 1-3 digits) }
// Matches: 144.210, 144.210.10, 10368.100
// Part 2 (Short1): Start (not digit), sep, 3 digits, OPTIONAL (sep, 1-3 digits)
// Matches: .210, .210.10, ,210
// Part 3 (Short2): Whitespace/Start, 3 digits, Whitespace/End
// Matches: " 210 ", " 144 "
String smartPattern = "(?<![\\d])(\\d{3,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])|(?<=\\s|^)(\\d{3})(?=\\s|$)";
Pattern pattern = Pattern.compile(smartPattern);
Matcher matcher = pattern.matcher(message.getMessageText());
ChatMember sender = message.getSender(); ChatMember sender = message.getSender();
// Safety check, in case sender is null (e.g., server message) if (sender == null) {
if (sender == null) return; return;
}
String messageText = message.getMessageText();
Matcher matcher = SMART_FREQUENCY_PATTERN.matcher(messageText);
while (matcher.find()) { while (matcher.find()) {
String foundRaw = matcher.group().trim(); boolean dottedShortForm = matcher.group(2) != null;
boolean bareShortForm = matcher.group(3) != null;
boolean shortForm = dottedShortForm || bareShortForm;
// --- PRE-PROCESSING: Normalize separators --- if (bareShortForm
// 1. Replace all commas with dots to unify format (144,210,10 -> 144.210.10) && !hasExplicitBareFrequencyContext(
foundRaw = foundRaw.replace(",", "."); messageText,
matcher.start(),
matcher.end()
)) {
continue;
}
String foundRaw = matcher.group().trim().replace(',', '.');
double finalDetectedFrequency = 0.0; double finalDetectedFrequency = 0.0;
Band finalDetectedBand = null; Band finalDetectedBand = null;
boolean isShortForm = false;
// --- STEP 1: Type Determination (Short or Full?) --- if (shortForm) {
if (foundRaw.startsWith(".")) {
// Check if it starts with a dot (e.g. ".210") OR is just 3 digits ("210") foundRaw = foundRaw.substring(1);
if (foundRaw.startsWith(".") || foundRaw.length() == 3) {
// It is a short form.
// We strip the leading dot for calculation if present -> "210.10" or "210"
if (foundRaw.startsWith(".")) foundRaw = foundRaw.substring(1);
isShortForm = true;
} else {
// It is a full frequency (e.g., 144.210.10 or 144.210)
try {
// Normalize "144.210.10" to "144.21010" for Double.parseDouble
String normalizedFull = normalizeFrequencyString(foundRaw);
finalDetectedFrequency = Double.parseDouble(normalizedFull);
finalDetectedBand = Band.fromFrequency(finalDetectedFrequency);
} catch (NumberFormatException e) { continue; }
} }
// --- STEP 2: Context Resolution (Only needed for Short Forms) --- /*
if (isShortForm) { * Priority 1: use this sender's most recently observed band if its
* information is not older than 30 minutes and the reconstructed
* frequency lies inside that band.
*/
long bestTimestamp = 0L;
// A) HISTORY CHECK (Priority 1: What did THIS USER do recently?)
// We search for the most recent band where this short form makes physical sense.
long bestTimestamp = 0;
// Iterate over all bands where the user is known
// (Assumption: ChatMember has a getter getKnownActiveBands())
if (sender.getKnownActiveBands() != null) { if (sender.getKnownActiveBands() != null) {
for (java.util.Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry : sender.getKnownActiveBands().entrySet()) { for (java.util.Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry
: sender.getKnownActiveBands().entrySet()) {
Band candidateBand = entry.getKey(); Band candidateBand = entry.getKey();
ChatMember.ActiveFrequencyInfo info = entry.getValue(); ChatMember.ActiveFrequencyInfo info = entry.getValue();
// Timeout Check: Info must not be older than 30 mins (1,800,000 ms) if (candidateBand == null || info == null) {
if (System.currentTimeMillis() - info.timestampEpoch > 1800000) continue; continue;
}
if (System.currentTimeMillis() - info.timestampEpoch > 1_800_000L) {
continue;
}
// Try Reconstruction: Band Prefix + ShortForm
// Example: Band 144 (Prefix "144") + "." + "210.10" -> "144.210.10"
try { try {
String reconstructedStr = candidateBand.getPrefix() + "." + foundRaw; String reconstructed =
String normalizedReconstruction = normalizeFrequencyString(reconstructedStr); candidateBand.getPrefix() + "." + foundRaw;
double candidateFrequency = Double.parseDouble(
normalizeFrequencyString(reconstructed)
);
double attemptFreq = Double.parseDouble(normalizedReconstruction); if (candidateBand.isPlausible(candidateFrequency)
&& info.timestampEpoch > bestTimestamp) {
// Does this frequency fit into the candidate band? finalDetectedFrequency = candidateFrequency;
if (candidateBand.isPlausible(attemptFreq)) {
// If we have multiple matches, pick the most recent one
if (info.timestampEpoch > bestTimestamp) {
finalDetectedFrequency = attemptFreq;
finalDetectedBand = candidateBand; finalDetectedBand = candidateBand;
bestTimestamp = info.timestampEpoch; bestTimestamp = info.timestampEpoch;
} }
} } catch (NumberFormatException ignored) {
} catch (Exception e) { /* Ignore parsing errors */ } // Try the next known band.
}
}
// B) GLOBAL PREFERENCES CHECK (Priority 2: Fallback if history is empty/old)
if (finalDetectedBand == null) {
// Get standard band from prefs (e.g., "144" or "432")
String defaultPrefix = prefs.getNotify_optionalFrequencyPrefix().get();
try {
String reconstructedStr = defaultPrefix + "." + foundRaw;
String normalizedReconstruction = normalizeFrequencyString(reconstructedStr);
double attemptFreq = Double.parseDouble(normalizedReconstruction);
// Check if this results in a valid amateur radio band
Band defaultBandCandidate = Band.fromFrequency(attemptFreq);
if (defaultBandCandidate != null) {
finalDetectedFrequency = attemptFreq;
finalDetectedBand = defaultBandCandidate;
}
} catch (NumberFormatException e) {
// Number was likely not a frequency (e.g., "73" or "599") and didn't fit any band
continue;
} }
} }
} }
// --- STEP 3: Process Result ---
if (finalDetectedBand != null && finalDetectedFrequency > 0) {
/* /*
* Store the detected QRG in the thread-safe active-member model. * Priority 2: use the configured fallback band. Invalid values from
* The UI table is only a JavaFX mirror, so the MessageBus must not scan * an older hand-edited configuration fall back safely to 144 MHz.
* or mutate getLst_chatMemberList() here. * The current UI itself offers only values from Band.values().
*/ */
client.applyDetectedFrequencyToActiveMembers(sender, finalDetectedBand, finalDetectedFrequency); if (finalDetectedBand == null) {
String configuredPrefix = null;
System.out.println("[SmartParser] Detected for " + sender.getCallSign() + ": " + if (prefs != null
finalDetectedFrequency + " MHz (" + finalDetectedBand + ") " + && prefs.getNotify_optionalFrequencyPrefix() != null) {
(isShortForm ? "[derived from " + foundRaw + "]" : "[full match]")); configuredPrefix =
prefs.getNotify_optionalFrequencyPrefix().get();
}
// Optional: Trigger Cluster-Spot here if enabled Band fallbackBand = Band.fromPrefix(configuredPrefix);
if (fallbackBand == null) {
fallbackBand = Band.B_144;
}
try {
String reconstructed =
fallbackBand.getPrefix() + "." + foundRaw;
double candidateFrequency = Double.parseDouble(
normalizeFrequencyString(reconstructed)
);
if (fallbackBand.isPlausible(candidateFrequency)) {
finalDetectedFrequency = candidateFrequency;
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.
}
}
if (finalDetectedBand == null || finalDetectedFrequency <= 0.0) {
continue;
}
/*
* Store the result in the thread-safe active-member model. The existing
* compatibility property used by the TableView and DX-Cluster code is
* updated by applyDetectedFrequencyToActiveMembers(...), too.
*/
client.applyDetectedFrequencyToActiveMembers(
sender,
finalDetectedBand,
finalDetectedFrequency
);
System.out.println(
"[SmartParser] Detected for "
+ sender.getCallSign()
+ ": "
+ finalDetectedFrequency
+ " MHz ("
+ finalDetectedBand
+ ") "
+ (shortForm
? "[derived from " + foundRaw + "]"
: "[full match]")
);
}
}
/**
* Checks whether a bare three-digit value is surrounded by text that identifies
* it as a frequency.
*
* <p>The check intentionally uses only a small area around the value. A remote
* occurrence of the word "QRG" elsewhere in a long message must not turn every
* three-digit number in that message into a frequency.</p>
*
* @param messageText complete chat message
* @param matchStart start index of the three-digit match
* @param matchEnd end index of the three-digit match
* @return {@code true} if the value has explicit frequency context
*/
static boolean hasExplicitBareFrequencyContext(
String messageText,
int matchStart,
int matchEnd
) {
if (messageText == null
|| matchStart < 0
|| matchEnd < matchStart
|| matchEnd > messageText.length()) {
return false;
}
int prefixStart = Math.max(
0,
matchStart - BARE_FREQUENCY_CONTEXT_CHARACTERS
);
int suffixEnd = Math.min(
messageText.length(),
matchEnd + BARE_FREQUENCY_CONTEXT_CHARACTERS
);
String prefixContext = messageText.substring(prefixStart, matchStart);
String suffixContext = messageText.substring(matchEnd, suffixEnd);
return BARE_FREQUENCY_PREFIX_CONTEXT_PATTERN
.matcher(prefixContext)
.find()
|| BARE_FREQUENCY_SUFFIX_CONTEXT_PATTERN
.matcher(suffixContext)
.find();
} }
/** /**
@@ -485,76 +602,129 @@ public class MessageBusManagementThread extends Thread {
} }
/** /**
* Resolves a message sender from the thread-safe active-member model. If a * Resolves a message sender from the thread-safe active-member model.
* CH/CR message arrives before the matching user-enter message, a marked *
* fallback sender is used. The returned sender is never null. * <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(); ChatMember lookup = new ChatMember();
lookup.setCallSign(senderCallSign); lookup.setCallSign(senderCallSign);
lookup.setChatCategory(category); lookup.setChatCategory(category);
ChatMember senderObj = this.client.findActiveChatMember(lookup); ChatMember senderObj =
this.client.findActiveChatMember(lookup);
if (senderObj != null) { 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(
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory()); senderObj.getCallSignRaw(),
senderObj.getChatCategory()
);
// Metrics influence priority scoring; process them after message text is known.
this.client.getStationMetricsService().onInboundMessage( this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(), senderObj.getCallSignRaw(),
System.currentTimeMillis(), System.currentTimeMillis(),
message == null ? null : message.getMessageText(), message == null ? null : message.getMessageText(),
this.client.getChatPreferences(), 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; return senderObj;
} }
ChatMember fallbackSender = new ChatMember(); 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.setChatCategory(category);
fallbackSender.setAirPlaneReflectInfo(new AirPlaneReflectionInfo()); fallbackSender.setAirPlaneReflectInfo(
new AirPlaneReflectionInfo()
);
return fallbackSender; return fallbackSender;
} }
/** /**
* Resolves a message receiver from the active-member model. Unknown receivers * Resolves a message receiver from the thread-safe active-member model.
* are represented as explicit fallback objects instead of null. This keeps PM *
* echo display and historic messages stable even if the target station already * <p>The local login is handled before the active-user lookup. The receiver
* left the chat. * 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) { private ChatMember resolveInboundReceiver(
if (receiverCallSign == null || receiverCallSign.equals("0")) { String receiverCallSign,
ChatCategory category
) {
if (receiverCallSign == null
|| receiverCallSign.equals("0")) {
return createAllReceiver(); 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(); ChatMember lookup = new ChatMember();
lookup.setCallSign(receiverCallSign); lookup.setCallSign(receiverCallSign);
lookup.setChatCategory(category); lookup.setChatCategory(category);
ChatMember receiverObj = this.client.findActiveChatMember(lookup); ChatMember receiverObj =
this.client.findActiveChatMember(lookup);
if (receiverObj != null) { if (receiverObj != null) {
return receiverObj; return receiverObj;
} }
ChatMember fallbackReceiver = new ChatMember(); 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.setChatCategory(category);
fallbackReceiver.setAirPlaneReflectInfo(new AirPlaneReflectionInfo()); fallbackReceiver.setAirPlaneReflectInfo(
new AirPlaneReflectionInfo()
);
return fallbackReceiver; return fallbackReceiver;
} }
@@ -777,6 +947,19 @@ public class MessageBusManagementThread extends Thread {
newMessageArrived)); newMessageArrived));
} }
/*
* Detect and store the sender's QRG before any directional-opportunity
* or DX-Cluster processing. A frequency first mentioned in this directed
* message is then immediately available for the resulting spot.
*
* The parser needs only the message text, sender and preferences. The
* receiver does not have to be resolved yet.
*/
smartFrequencyExtraction(
newMessageArrived,
this.client.getChatPreferences());
// TODO: Next: get frequency infos out of name?
if (splittedMessageLine[7].equals("0")) { if (splittedMessageLine[7].equals("0")) {
// message is not directed to anyone, move it to the cq messages. // message is not directed to anyone, move it to the cq messages.
@@ -829,7 +1012,11 @@ public class MessageBusManagementThread extends Thread {
versionInfo.setSender(itsMe); versionInfo.setSender(itsMe);
versionInfo.setReceiver(newMessageArrived.getSender()); versionInfo.setReceiver(newMessageArrived.getSender());
versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + ApplicationConstants.AUTOANSWER_PREFIX + " " + "KST4Contest " + " v" + ApplicationConstants.APPLICATION_CURRENT_VERSION + " by DO5AMF"); versionInfo.setChatCategory(newMessageArrived.getChatCategory());
versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
+ " " + AUTOANSWER_PREFIX
+ " KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION
+ " by DO5AMF");
this.client.getMessageTXBus().add(versionInfo); this.client.getMessageTXBus().add(versionInfo);
} }
@@ -876,11 +1063,11 @@ public class MessageBusManagementThread extends Thread {
// } // }
// } // }
// ==== Unified Autoanswer (Generic + QRG) with Pingpong-Guard + per-Remote Cooldown ==== // ==== Unified auto-answer (generic + QRG) with ping-pong guard and per-remote cooldown ====
final String incomingText = newMessageArrived.getMessageText(); final String incomingText = newMessageArrived.getMessageText();
final String incomingLower = (incomingText == null) ? "" : incomingText.toLowerCase(Locale.ROOT); final String incomingLower = (incomingText == null) ? "" : incomingText.toLowerCase(Locale.ROOT);
// 1) Pingpong-security: never ever react to auto generated messages // Never answer another automatically generated message.
if (!isAutoMessage(newMessageArrived)) { if (!isAutoMessage(newMessageArrived)) {
boolean qrgRequested = false; boolean qrgRequested = false;
@@ -896,24 +1083,17 @@ public class MessageBusManagementThread extends Thread {
boolean genericEnabled = this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled(); boolean genericEnabled = this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled();
// 2) Entscheide, ob überhaupt geantwortet wird (QRG hat Vorrang vor Generic) // A QRG reply takes precedence over the generic reply.
String payload = null; String payload = null;
if (qrgRequested) { if (qrgRequested) {
payload = "QRG is: " + getAutoAnswerQrgForCategory(newMessageArrived.getChatCategory());
if (this.client.getChatPreferences().isLoginToSecondChatEnabled()) {
payload = "QRGs: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue()
+ " / " + this.client.getChatPreferences().getMYQRGSecondCat().getValue();
} else {
payload = "QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue();
}
} else if (genericEnabled) { } else if (genericEnabled) {
payload = this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat(); payload = this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat();
} }
// 3) Cooldown pro Gegenstation: nur wenn DIESER Client jetzt wirklich sendet // Apply the cooldown only when this client is about to send a reply.
if (payload != null && isAutoAnswerAllowedNow(newMessageArrived)) { if (payload != null && isAutoAnswerAllowedNow(newMessageArrived)) {
ChatMessage automaticAnswer = new ChatMessage(); ChatMessage automaticAnswer = new ChatMessage();
@@ -922,15 +1102,15 @@ public class MessageBusManagementThread extends Thread {
automaticAnswer.setSender(itsMe); automaticAnswer.setSender(itsMe);
automaticAnswer.setReceiver(newMessageArrived.getSender()); automaticAnswer.setReceiver(newMessageArrived.getSender());
automaticAnswer.setChatCategory(newMessageArrived.getChatCategory());
// Prefix fest + nicht entfernbar, damit Auto↔Auto nicht pingpongt // The fixed prefix prevents automatic clients from answering each other.
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
+ " " + AUTOANSWER_PREFIX + " " + payload); + " " + AUTOANSWER_PREFIX + " " + payload);
this.client.getMessageTXBus().add(automaticAnswer); this.client.getMessageTXBus().add(automaticAnswer);
// Cooldown wird NUR hier gesetzt (nicht bei 'message sent by me' Echo), // Record only locally generated replies, not the later server echo.
// damit nur lokale Auto-Sends zählen.
markLocalAutoAnswerSent(newMessageArrived); markLocalAutoAnswerSent(newMessageArrived);
} }
} }
@@ -1062,9 +1242,6 @@ public class MessageBusManagementThread extends Thread {
exceptionOccured.printStackTrace(); exceptionOccured.printStackTrace();
} }
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
smartFrequencyExtraction(newMessageArrived, this.client.getChatPreferences());
// TODO: Next: get frequency infos out of name? // TODO: Next: get frequency infos out of name?
} else } else
@@ -1196,9 +1373,12 @@ public class MessageBusManagementThread extends Thread {
newDXCListSender3.setQra(splittedMessageLine[5]); newDXCListSender3.setQra(splittedMessageLine[5]);
ChatMember newDXCListReceiver3 = new ChatMember(); ChatMember newDXCListReceiver3 = new ChatMember();
// newDXCListReceiver3.setFrequency(splittedMessageLine[4]);
newDXCListReceiver3.setCallSign(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.setSender(newDXCListSender3);
dxcMsg3.setReceiver(newDXCListReceiver3); dxcMsg3.setReceiver(newDXCListReceiver3);
@@ -1544,9 +1724,7 @@ public class MessageBusManagementThread extends Thread {
/** /**
* check if message had been auto generated * Returns whether a message carries the fixed marker used for automatic replies.
* @param msg
* @return
*/ */
private boolean isAutoMessage(ChatMessage msg) { private boolean isAutoMessage(ChatMessage msg) {
return msg != null return msg != null
@@ -1554,6 +1732,22 @@ public class MessageBusManagementThread extends Thread {
&& msg.getMessageText().contains(AUTOANSWER_PREFIX); && msg.getMessageText().contains(AUTOANSWER_PREFIX);
} }
/**
* Returns the configured QRG for the category in which the request was received.
* If the category cannot be resolved, the main category remains the safe fallback.
*/
private String getAutoAnswerQrgForCategory(ChatCategory incomingCategory) {
ChatCategory secondCategory = this.client.getChatCategorySecondChat();
if (incomingCategory != null
&& secondCategory != null
&& incomingCategory.getCategoryNumber() == secondCategory.getCategoryNumber()) {
return this.client.getChatPreferences().getMYQRGSecondCat().getValue();
}
return this.client.getChatPreferences().getMYQRGFirstCat().getValue();
}
private String autoAnswerCooldownKey(ChatMessage incoming) { private String autoAnswerCooldownKey(ChatMessage incoming) {
String remoteCall = "UNKNOWN"; String remoteCall = "UNKNOWN";
@@ -1561,13 +1755,16 @@ public class MessageBusManagementThread extends Thread {
remoteCall = incoming.getSender().getCallSign().toUpperCase(); remoteCall = incoming.getSender().getCallSign().toUpperCase();
} }
int cat = 0; // fallback int categoryNumber = 0;
if (incoming != null && incoming.getSender() != null && incoming.getSender().getChatCategory() != null) { if (incoming != null && incoming.getChatCategory() != null) {
cat = incoming.getSender().getChatCategory().getCategoryNumber(); categoryNumber = incoming.getChatCategory().getCategoryNumber();
} else if (incoming != null
&& incoming.getSender() != null
&& incoming.getSender().getChatCategory() != null) {
categoryNumber = incoming.getSender().getChatCategory().getCategoryNumber();
} }
// pro Gegenstation + pro Chat-Kategorie (falls derselbe Call in Cat2/Cat3 PMs macht) return remoteCall + "|" + categoryNumber;
return remoteCall + "|" + cat;
} }
private boolean isAutoAnswerAllowedNow(ChatMessage incoming) { private boolean isAutoAnswerAllowedNow(ChatMessage incoming) {
@@ -0,0 +1,81 @@
//package kst4contest.controller;
//
//import kst4contest.model.Band;
//import org.junit.jupiter.api.Test;
//import org.junit.jupiter.params.ParameterizedTest;
//import org.junit.jupiter.params.provider.ValueSource;
//
//import java.util.regex.Matcher;
//import java.util.regex.Pattern;
//
//import static org.junit.jupiter.api.Assertions.assertEquals;
//import static org.junit.jupiter.api.Assertions.assertFalse;
//import static org.junit.jupiter.api.Assertions.assertNotNull;
//import static org.junit.jupiter.api.Assertions.assertNull;
//import static org.junit.jupiter.api.Assertions.assertTrue;
//
//class MessageBusManagementThreadFrequencyContextTest {
//
// private static final Pattern THREE_DIGIT_VALUE =
// Pattern.compile("\\b\\d{3}\\b");
//
// @ParameterizedTest
// @ValueSource(strings = {
// "qrg 210",
// "QRG: 210",
// "freq is 210",
// "frequency = 210",
// "on 210",
// "210 MHz",
// "210 qrg"
// })
// void acceptsBareThreeDigitValueWithFrequencyContext(String messageText) {
// Matcher matcher = findThreeDigitValue(messageText);
//
// assertTrue(
// MessageBusManagementThread
// .hasExplicitBareFrequencyContext(
// messageText,
// matcher.start(),
// matcher.end()
// )
// );
// }
//
// @ParameterizedTest
// @ValueSource(strings = {
// "599",
// "144",
// "serial 210",
// "score 210",
// "worked 210 stations"
// })
// void rejectsBareThreeDigitValueWithoutFrequencyContext(String messageText) {
// Matcher matcher = findThreeDigitValue(messageText);
//
// assertFalse(
// MessageBusManagementThread
// .hasExplicitBareFrequencyContext(
// messageText,
// matcher.start(),
// matcher.end()
// )
// );
// }
//
// @Test
// void resolvesOnlySupportedFallbackPrefixes() {
// assertEquals(Band.B_144, Band.fromPrefix("144"));
// assertEquals(Band.B_432, Band.fromPrefix(" 432 "));
// assertEquals(Band.B_10G, Band.fromPrefix("10368"));
// assertNull(Band.fromPrefix("999"));
// assertNull(Band.fromPrefix(null));
// }
//
// private Matcher findThreeDigitValue(String messageText) {
// Matcher matcher = THREE_DIGIT_VALUE.matcher(messageText);
// assertTrue(matcher.find(), "Test message must contain a three-digit value");
// assertNotNull(matcher.group());
// return matcher;
// }
//}
@@ -0,0 +1,154 @@
package kst4contest.controller;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
/**
* Resolves variables used in operator messages, shortcuts, snippets and
* beacons.
*
* <p>Global variables only depend on the local station configuration and can
* therefore be used in every message context. Station variables additionally
* require a selected remote station. Keeping both groups in one resolver
* prevents the send field and the beacon tasks from implementing different
* replacement rules.</p>
*/
public final class MessageVariableResolver {
private static final int SHORT_VALUE_LENGTH = 7;
private final ChatPreferences chatPreferences;
/**
* Creates a resolver backed by the live application preferences.
*
* @param chatPreferences preferences that provide current station values
*/
public MessageVariableResolver(ChatPreferences chatPreferences) {
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
}
/**
* Resolves variables which do not require a selected remote station.
*
* <p>The replacement is literal rather than regular-expression based.
* Callsigns, locators and frequencies are data, not regular expressions.</p>
*
* @param template text that may contain variables
* @return text with all available global variables resolved, or {@code null}
* when the supplied template is {@code null}
*/
public String resolveGlobalVariables(String template) {
if (template == null) {
return null;
}
String primaryFrequency = valueOrEmpty(chatPreferences.getMYQRGFirstCat().getValue());
String secondaryFrequency = valueOrEmpty(chatPreferences.getMYQRGSecondCat().getValue());
String ownLocator = valueOrEmpty(chatPreferences.getStn_loginLocatorMainCat());
String resolvedText = template;
resolvedText = resolvedText.replace("MYQRGSHORT", abbreviate(primaryFrequency));
resolvedText = resolvedText.replace("MYQRG", primaryFrequency);
resolvedText = resolvedText.replace("SECONDQRG", secondaryFrequency);
resolvedText = resolvedText.replace("MYLOCATORSHORT", abbreviateLocator(ownLocator));
resolvedText = resolvedText.replace("MYLOCATOR", ownLocator);
resolvedText = resolvedText.replace("MYCALL", valueOrEmpty(chatPreferences.getStn_loginCallSign()));
resolvedText = resolvedText.replace("MYQTF", formatHeading(chatPreferences.getActualQTF().getValue().doubleValue()));
return resolvedText;
}
/**
* Resolves global variables and variables derived from a selected station.
*
* <p>If no station is selected, station-specific placeholders remain visible.
* This is intentional: silently removing {@code QRZNAME}, {@code FIRSTAP} or
* {@code SECONDAP} could create a plausible-looking but incomplete message.</p>
*
* @param template text that may contain variables
* @param selectedStation currently selected remote station, may be {@code null}
* @return resolved message text
*/
public String resolveForSelectedStation(String template, ChatMember selectedStation) {
String resolvedText = resolveGlobalVariables(template);
if (resolvedText == null || selectedStation == null) {
return resolvedText;
}
resolvedText = resolvedText.replace("QRZNAME", resolveStationName(selectedStation));
resolvedText = resolvedText.replace("FIRSTAP", resolveFirstAirPlane(selectedStation));
resolvedText = resolvedText.replace("SECONDAP", resolveSecondAirPlane(selectedStation));
return resolvedText;
}
private String resolveStationName(ChatMember selectedStation) {
String stationName = valueOrEmpty(selectedStation.getName()).trim();
if (!stationName.isEmpty()) {
return stationName;
}
return valueOrEmpty(selectedStation.getCallSign());
}
private String resolveFirstAirPlane(ChatMember selectedStation) {
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
if (risingAirPlanes.isEmpty()) {
return "no ap available";
}
AirPlane firstAirPlane = risingAirPlanes.get(0);
return "a " + firstAirPlane.getPotencialDescriptionAsWord()
+ " in " + firstAirPlane.getArrivingDurationMinutes() + " min";
}
private String resolveSecondAirPlane(ChatMember selectedStation) {
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
if (risingAirPlanes.size() < 2) {
return "";
}
AirPlane secondAirPlane = risingAirPlanes.get(1);
return "Next " + secondAirPlane.getPotencialDescriptionAsWord()
+ " in " + secondAirPlane.getArrivingDurationMinutes() + " min";
}
private List<AirPlane> getRisingAirPlanes(ChatMember selectedStation) {
AirPlaneReflectionInfo reflectionInfo = selectedStation.getAirPlaneReflectInfo();
if (reflectionInfo == null || reflectionInfo.getRisingAirplanes() == null) {
return List.of();
}
return reflectionInfo.getRisingAirplanes();
}
private String abbreviate(String value) {
return value.substring(0, Math.min(value.length(), SHORT_VALUE_LENGTH));
}
private String abbreviateLocator(String locator) {
return locator.substring(0, Math.min(locator.length(), 4));
}
private String formatHeading(double headingDegrees) {
if (!Double.isFinite(headingDegrees)) {
return "";
}
return BigDecimal.valueOf(headingDegrees).stripTrailingZeros().toPlainString();
}
private String valueOrEmpty(String value) {
return value == null ? "" : value;
}
}
@@ -1,4 +1,5 @@
package kst4contest.controller; package kst4contest.controller;
import kst4contest.logic.BandOpportunityResolver;
import kst4contest.view.map.MapCallsignRawSnapshot; import kst4contest.view.map.MapCallsignRawSnapshot;
import java.util.ArrayList; import java.util.ArrayList;
@@ -168,14 +169,31 @@ public final class ReachabilityService {
selectedSnapshot.lastKnownFrequenciesByBand() selectedSnapshot.lastKnownFrequenciesByBand()
); );
if (!Double.isFinite(analysisFrequencyMHz) || analysisFrequencyMHz <= 0.0) { Band analysisBand = Band.fromFrequency(analysisFrequencyMHz);
Band fallbackBand = member == null ? Band.B_144 : resolveAutoBand(member); if (analysisBand != null && !isUsableAutomaticBand(member, analysisBand)) {
analysisFrequencyMHz = resolveAnalysisFrequencyForBand(member, fallbackBand); analysisFrequencyMHz = Double.NaN;
analysisBand = null;
} }
Band analysisBand = Band.fromFrequency(analysisFrequencyMHz); if (!Double.isFinite(analysisFrequencyMHz)
if (analysisBand == null) { || analysisFrequencyMHz <= 0.0
analysisBand = member == null ? Band.B_144 : resolveAutoBand(member); || analysisBand == null) {
Band fallbackBand = resolveAutoBand(member);
if (fallbackBand == null) {
dispatchFxCallback(
fxCallback,
PathAnalysisResult.waitingForUsableBand(
ownLocator6,
targetLocator6,
selectedSnapshot.callSignRaw()
)
);
return;
}
analysisBand = fallbackBand;
analysisFrequencyMHz = resolveAnalysisFrequencyForBand(member, fallbackBand);
} }
PathAnalysisRequest request = buildRequest( PathAnalysisRequest request = buildRequest(
@@ -230,22 +248,62 @@ public final class ReachabilityService {
* @return resolved band * @return resolved band
*/ */
public Band resolveAutoBand(ChatMember member) { public Band resolveAutoBand(ChatMember member) {
if (member != null && member.getKnownActiveBands() != null && !member.getKnownActiveBands().isEmpty()) { EnumSet<Band> enabledBands = getEnabledStationBands();
return member.getKnownActiveBands().keySet().stream() if (enabledBands.isEmpty()) {
.filter(Objects::nonNull) return null;
}
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)) .min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(Band.B_144); .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 if (member != null
&& member.getChatCategory() != null && member.getChatCategory() != null
&& member.getChatCategory().getCategoryNumber() == ChatCategory.MICROWAVE) { && member.getChatCategory().getCategoryNumber() == ChatCategory.MICROWAVE
&& fallbackBands.contains(Band.B_1296)) {
return 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 Band.B_144;
} }
return fallbackBands.stream()
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(null);
}
/** /**
* Returns the active own bands configured in the station preferences. High bands * Returns the active own bands configured in the station preferences. High bands
* above 10 GHz are intentionally ignored for the first version. * above 10 GHz are intentionally ignored for the first version.
@@ -253,22 +311,44 @@ public final class ReachabilityService {
* @return set of enabled bands * @return set of enabled bands
*/ */
public EnumSet<Band> getEnabledStationBands() { public EnumSet<Band> getEnabledStationBands() {
ChatPreferences preferences = chatController.getChatPreferences(); return BandOpportunityResolver.getEnabledStationBands(
EnumSet<Band> enabledBands = EnumSet.noneOf(Band.class); chatController.getChatPreferences()
);
if (preferences == null) {
return enabledBands;
} }
if (preferences.isStn_bandActive144()) enabledBands.add(Band.B_144); private List<ChatMember> resolveCallsignVariants(ChatMember member) {
if (preferences.isStn_bandActive432()) enabledBands.add(Band.B_432); if (member == null) {
if (preferences.isStn_bandActive1240()) enabledBands.add(Band.B_1296); return List.of();
if (preferences.isStn_bandActive2300()) enabledBands.add(Band.B_2320); }
if (preferences.isStn_bandActive3400()) enabledBands.add(Band.B_3400);
if (preferences.isStn_bandActive5600()) enabledBands.add(Band.B_5760);
if (preferences.isStn_bandActive10G()) enabledBands.add(Band.B_10G);
return enabledBands; String rawCall = member.getCallSignRaw() != null
? member.getCallSignRaw()
: member.getCallSign();
List<ChatMember> variants = chatController.findActiveChatMembersByRawCall(rawCall);
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);
} }
/** /**
@@ -303,17 +303,19 @@ public class ReadUDPByWintestThread extends Thread {
return null; return null;
} }
switch (bandId.trim()) { return switch (bandId.trim()) {
case "12": return Band.B_144; case "10" -> Band.B_50;
case "14": return Band.B_432; case "11" -> Band.B_70;
case "16": return Band.B_1296; case "12" -> Band.B_144;
case "17": return Band.B_2320; case "14" -> Band.B_432;
case "18": return Band.B_3400; case "16" -> Band.B_1296;
case "19": return Band.B_5760; case "17" -> Band.B_2320;
case "20": return Band.B_10G; case "18" -> Band.B_3400;
case "21": return Band.B_24G; case "19" -> Band.B_5760;
default: return null; case "20" -> Band.B_10G;
} case "21" -> Band.B_24G;
default -> null;
};
} }
/** /**
@@ -1,9 +1,13 @@
package kst4contest.controller; package kst4contest.controller;
import java.io.*; import java.io.IOException;
import java.net.*; import java.net.DatagramPacket;
import java.util.ArrayList; import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@@ -21,188 +25,245 @@ import kst4contest.model.ThreadStateMessage;
* @author www.codejava.net * @author www.codejava.net
*/ */
public class ReadUDPbyAirScoutMessageThread extends Thread { public class ReadUDPbyAirScoutMessageThread extends Thread {
private BufferedReader reader;
private Socket socket;
private ChatController client;
private int localPort;
private String ASIdentificator, ChatClientIdentificator;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "AirScout msg";
// public ReadUDPbyAirScoutMessageThread(int localPort) {
// this.localPort = localPort;
// }
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator, private final ChatController client;
String ChatClientIdentificator, ThreadStatusCallback callback) { private final int localPort;
private final ThreadStatusCallback callBackToController;
this.callBackToController = callback; private final String threadNickName = "AirScout msg";
private DatagramSocket socket;
public ReadUDPbyAirScoutMessageThread(
int localPort,
ChatController client,
ThreadStatusCallback callback
) {
this.localPort = localPort; this.localPort = localPort;
this.client = client; this.client = client;
this.ASIdentificator = ASIdentificator; this.callBackToController = callback;
this.ChatClientIdentificator = ChatClientIdentificator;
} }
@Override @Override
public void interrupt() { public void interrupt() {
System.out.println("ReadUDP");
super.interrupt(); super.interrupt();
try {
if (this.socket != null) {
this.socket.close(); if (socket != null && !socket.isClosed()) {
} socket.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} }
} }
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) { /**
if (callBackToController != null) { * Checks whether an AirScout response is addressed to the currently
//update the visual control of running thread * configured server and client identifiers.
callBackToController.onThreadStatus("AirScout", threadStateMessage); *
} * Outgoing message:
* ASSETPATH: "client" "server" ...
*
* Corresponding response:
* ASNEAREST: "server" "client" ...
*
* The comparison is deliberately case-sensitive. In a setup with several
* clients, KST-A and kst-a must not silently become the same destination.
*
* @param message received UDP message
* @return true if the response belongs to this KST4Contest instance
*/
private boolean isMessageForConfiguredClient(String message) {
if (message == null || !message.startsWith("ASNEAREST:")) {
return false;
} }
String[] quotedParts = message.split("\"");
if (quotedParts.length < 4) {
return false;
}
String receivedServerIdentifier = quotedParts[1].trim();
String receivedClientIdentifier = quotedParts[3].trim();
String configuredServerIdentifier =
client.getChatPreferences()
.getAirScout_asServerNameString();
String configuredClientIdentifier =
client.getChatPreferences()
.getAirScout_asClientNameString();
if (configuredServerIdentifier == null
|| configuredClientIdentifier == null) {
return false;
}
return configuredServerIdentifier.equals(
receivedServerIdentifier
) && configuredClientIdentifier.equals(
receivedClientIdentifier
);
}
@Override
public void run() { public void run() {
Thread.currentThread().setName("ReadUDPByAirScoutThread"); Thread.currentThread().setName(
"ReadUDPByAirScoutThread"
DatagramSocket socket = null; );
boolean running;
byte[] buf = new byte[1777];
DatagramPacket packet;
// DatagramPacket packet = new DatagramPacket(buf, buf.length); //changed due to save memory
packet = new DatagramPacket(buf, buf.length);
try { try {
socket = new DatagramSocket(null); socket = new DatagramSocket(null);
socket.setReuseAddress(true); socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(localPort)); socket.bind(new InetSocketAddress(localPort));
socket.receive(packet);
socket.setSoTimeout(3000); socket.setSoTimeout(3000);
} catch (SocketException e) { while (!Thread.currentThread().isInterrupted()) {
// TODO Auto-generated catch block if (client.isDisconnectionPerformedByUser()) {
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
// e.printStackTrace();
}
while (true) {
// packet = new DatagramPacket(buf, buf.length);
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH);
try {
if (this.client.isDisconnectionPerformedByUser()) {
break;//TODO: what if it´s not the finally closage but a band channel change?
}
socket.receive(packet);
} catch (SocketTimeoutException e2) {
// this will catch the repeating Sockettimeoutexception...nothing to do
// e2.printStackTrace();
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
InetAddress address = packet.getAddress();
int port = packet.getPort();
// packet = new DatagramPacket(buf, buf.length, address, port);
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
received = received.trim();
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
System.out.println("ReadUdpByASMsgTh, Info: got poison, now dieing....");
try {
terminateConnection();
} catch (Exception e) {
System.out.println("ASUDPRDR: catched error " + e.getMessage());
}
break; break;
} }
byte[] buffer = new byte[1777];
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) { DatagramPacket packet = new DatagramPacket(
// do nothing, that is your own message buffer,
} else if (received.contains("ASNEAREST:")) { //answer by airscout buffer.length
);
// processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
AirPlaneReflectionInfo apReflectInfoForChatMember;
apReflectInfoForChatMember = processASUDPMessage(received);
if (!this.client.getLst_chatMemberList().isEmpty()) {
try { try {
socket.receive(packet);
} catch (SocketTimeoutException timeoutException) {
/*
* AirScout may remain silent for some time. Continue with a
* new packet instead of processing the previous packet again.
*/
continue;
}
// this.client.getLst_chatMemberList() String received = new String(
// .get(this.client.checkListForChatMemberIndexByCallSign( packet.getData(),
// apReflectInfoForChatMember.getReceiver())) packet.getOffset(),
// .setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at packet.getLength()
// // the central instance of ).trim();
// // chatmember list .... -1 is a
// // problem!
ArrayList<Integer> addApInfoToThese = this.client.checkListForChatMemberIndexesByCallSign(apReflectInfoForChatMember.getReceiver()); if (received.contains(
addApInfoToThese.forEach((integerIndex) -> {this.client.getLst_chatMemberList().get(integerIndex).setAirPlaneReflectInfo(apReflectInfoForChatMember); }); ApplicationConstants.DISCONNECT_RDR_POISONPILL
)) {
System.out.println(
"[AirScout UDP, info]: Received shutdown packet."
);
break;
}
// AirScout availability strongly affects priority => request recompute the score of the chatmember /*
this.client.getScoreService().requestRecompute("airscout-update"); * The socket remains bound so that AirScout can be enabled
* without reconnecting. Disabled means that received data is
* discarded and no station state is changed.
*/
if (!client.getChatPreferences()
.isAirScout_asUDPListenerEnabled()) {
continue;
}
if (!isMessageForConfiguredClient(received)) {
continue;
}
processAirScoutResponse(received);
}
} catch (SocketException exception) {
if (!Thread.currentThread().isInterrupted()) {
System.out.println(
"[AirScout UDP, error]: Could not use UDP port "
+ localPort
+ ": "
+ exception.getMessage()
);
}
} catch (IOException exception) {
if (!Thread.currentThread().isInterrupted()) {
System.out.println(
"[AirScout UDP, error]: Communication failed: "
+ exception.getMessage()
);
}
} finally {
if (socket != null && !socket.isClosed()) {
socket.close();
}
socket = null;
}
}
/** /**
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10" * Parses an AirScout response and applies it to every active category
* java.util.ConcurrentModificationException at * instance of the reported station.
* java.base/java.util.AbstractList$Itr.checkForComodification(AbstractList.java:399)
* at java.base/java.util.AbstractList$Itr.next(AbstractList.java:368) at
* kst4contest.controller.ChatController.checkListForChatMemberIndexByCallSign(ChatController.java:173)
* at
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
* *
* @param received received ASNEAREST message
*/ */
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString()); private void processAirScoutResponse(String received) {
// } try {
} catch (Exception e) { AirPlaneReflectionInfo reflectionInfo =
processASUDPMessage(received);
System.out.println("ReadUdpByAsMsgTh, Warning:" if (reflectionInfo == null
+ apReflectInfoForChatMember.getReceiver().getCallSign() || reflectionInfo.getReceiver() == null) {
+ " is not in the Chatmemberlist or the Chatmemberlist is modified by another Thread"); return;
// TODO: handle exception
} }
// String[] newState = new String[3]; String receiverCallSign =
// newState[0] = "On"; reflectionInfo.getReceiver().getCallSignRaw();
// newState[1] = "received line";
// newState[2] = apReflectInfoForChatMember.toString(); if (receiverCallSign == null
// callThreadStateToUi(newState); || receiverCallSign.isBlank()) {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + apReflectInfoForChatMember.toString(), false); receiverCallSign =
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage); reflectionInfo.getReceiver().getCallSign();
} }
} if (receiverCallSign == null
// packet = null; //reset packet || receiverCallSign.isBlank()) {
buf = new byte[1777]; // reset buffer for future smaller packets return;
} }
List<ChatMember> matchingMembers =
client.findActiveChatMembersByRawCall(
receiverCallSign
);
for (ChatMember matchingMember : matchingMembers) {
matchingMember.setAirPlaneReflectInfo(
reflectionInfo
);
} }
if (!matchingMembers.isEmpty()) {
client.getScoreService().requestRecompute(
"airscout-update"
);
}
if (callBackToController != null) {
ThreadStateMessage threadStateMessage =
new ThreadStateMessage(
threadNickName,
true,
"Received AirScout response\n"
+ reflectionInfo,
false
);
callBackToController.onThreadStatus(
threadNickName,
threadStateMessage
);
}
} catch (RuntimeException exception) {
System.out.println(
"[AirScout UDP, warning]: Could not process response: "
+ exception.getMessage()
);
}
}
public AirPlaneReflectionInfo processASUDPMessage(String udpStringToProcess) { public AirPlaneReflectionInfo processASUDPMessage(String udpStringToProcess) {
// System.out.println("RDUDPAS RECV: " + udpStringToProcess); // System.out.println("RDUDPAS RECV: " + udpStringToProcess);
// TODO: filter messages which are directed to another client
/* /*
* Example mesage: ASNEAREST: "AS" "KST" * Example mesage: ASNEAREST: "AS" "KST"
@@ -304,11 +365,8 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
} }
public boolean terminateConnection() { public boolean terminateConnection() {
if (socket != null && !socket.isClosed()) {
try { socket.close();
this.socket.close();
} catch (Exception e) {
System.out.println("udpbyas: catched " + e.getMessage());
} }
return true; return true;
@@ -83,6 +83,9 @@ public class ReadUDPbyUCXMessageThread extends Thread {
if (xmlStart < 0) { if (xmlStart < 0) {
xmlStart = rawPacket.indexOf("<contactinfo"); xmlStart = rawPacket.indexOf("<contactinfo");
} }
if (xmlStart < 0) {
xmlStart = rawPacket.indexOf("<contactreplace");
}
if (xmlStart < 0) { if (xmlStart < 0) {
xmlStart = rawPacket.indexOf("<RadioInfo"); xmlStart = rawPacket.indexOf("<RadioInfo");
} }
@@ -141,6 +144,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
} }
switch (band.trim()) { switch (band.trim()) {
case "50":
case "6m":
return Band.B_50;
case "70":
case "4m":
return Band.B_70;
case "144": case "144":
case "2m": case "2m":
return Band.B_144; return Band.B_144;
@@ -288,14 +297,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
ChatMember modifyThat = null; ChatMember modifyThat = null;
// System.out.println("ReadUDPByUCX, message catched: " + udpMsg);
// String[] threadStatusMessage = new String[2];
// threadStatusMessage = new String[3];
// threadStatusMessage[0] = "on";
// threadStatusMessage[1] = "received message:";
// threadStatusMessage[2] = udpMsg;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false); ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage); callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
@@ -304,7 +305,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
try { try {
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e1) { } catch (ParserConfigurationException e1) {
// TODO Auto-generated catch block
e1.printStackTrace(); e1.printStackTrace();
} }
@@ -313,10 +314,16 @@ public class ReadUDPbyUCXMessageThread extends Thread {
Document doc = db.parse(new InputSource(new StringReader(udpMsg))); Document doc = db.parse(new InputSource(new StringReader(udpMsg)));
/** /**
* case Log-QSO-Packet in ucxlog * case Log-QSO-Packet in ucxlog / DXLog and compatible
* *
*/ */
NodeList list = doc.getElementsByTagName("contactinfo"); NodeList list = doc.getElementsByTagName("contactinfo");
if (list.getLength() == 0) {
list = doc.getElementsByTagName("contactreplace");
//DXlog will send contactreplace instead of contactinfo on clicking "broadcast whole logbook"
}
if (list.getLength() != 0) { if (list.getLength() != 0) {
/* /*
@@ -356,6 +363,20 @@ public class ReadUDPbyUCXMessageThread extends Thread {
Band workedBand = helper_resolveBandFromLoggerBand(band); Band workedBand = helper_resolveBandFromLoggerBand(band);
switch (band) { switch (band) {
case "50":
case "6m":
{
workedCall.setWorked50(true);
break;
}
case "70":
case "4m":
{
workedCall.setWorked70(true);
break;
}
case "144": case "144":
case "2m": //minos contest logger case "2m": //minos contest logger
{ {
@@ -433,7 +454,13 @@ public class ReadUDPbyUCXMessageThread extends Thread {
modifyThat.setWorked(true); modifyThat.setWorked(true);
if (workedCall.isWorked144()) { if (workedCall.isWorked50()) {
modifyThat.setWorked50(true);
} else if (workedCall.isWorked70()) {
modifyThat.setWorked70(true);
} else if (workedCall.isWorked144()) {
modifyThat.setWorked144(true); modifyThat.setWorked144(true);
} else if (workedCall.isWorked432()) { } else if (workedCall.isWorked432()) {
@@ -140,7 +140,9 @@ public final class ScoreService {
controller.getStationMetricsService().snapshot(nowEpochMs, prefs); controller.getStationMetricsService().snapshot(nowEpochMs, prefs);
// 1) Choose one representative per callsignRaw // 1) Choose one representative per callsignRaw
Map<String, ChatMember> representativeByCallRaw = chooseRepresentativeMembers(members, lastInbound); Map<String, List<ChatMember>> variantsByCallRaw = groupMembersByCallRaw(members);
Map<String, ChatMember> representativeByCallRaw =
chooseRepresentativeMembers(variantsByCallRaw, lastInbound);
// 2) Compute score once per callsignRaw // 2) Compute score once per callsignRaw
Map<String, Double> scoreByCallRaw = new HashMap<>(representativeByCallRaw.size()); Map<String, Double> scoreByCallRaw = new HashMap<>(representativeByCallRaw.size());
@@ -154,6 +156,7 @@ public final class ScoreService {
double score = priorityCalculator.calculatePriority( double score = priorityCalculator.calculatePriority(
representative, representative,
variantsByCallRaw.getOrDefault(callRaw, List.of(representative)),
prefs, prefs,
activeSkeds, activeSkeds,
metricsSnapshot, metricsSnapshot,
@@ -163,7 +166,15 @@ public final class ScoreService {
scoreByCallRaw.put(callRaw, score); scoreByCallRaw.put(callRaw, score);
preferredCategoryByCallRaw.put(callRaw, representative.getChatCategory()); 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 // 3) Build Top-N
@@ -189,6 +200,21 @@ public final class ScoreService {
}); });
} }
private Map<String, List<ChatMember>> groupMembersByCallRaw(List<ChatMember> members) {
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
for (ChatMember member : members) {
if (member == null) continue;
String callRaw = normalizeCallRaw(member.getCallSignRaw());
if (callRaw == null || callRaw.isEmpty()) continue;
byCallRaw.computeIfAbsent(callRaw, ignored -> new ArrayList<>()).add(member);
}
return byCallRaw;
}
/** /**
* Picks one ChatMember object per callsignRaw. * Picks one ChatMember object per callsignRaw.
* Preference order: * Preference order:
@@ -196,18 +222,9 @@ public final class ScoreService {
* 2) Most recently active variant (fallback) * 2) Most recently active variant (fallback)
*/ */
private Map<String, ChatMember> chooseRepresentativeMembers( private Map<String, ChatMember> chooseRepresentativeMembers(
List<ChatMember> members, Map<String, List<ChatMember>> byCallRaw,
Map<String, ChatCategory> lastInboundCategoryByCallRaw Map<String, ChatCategory> lastInboundCategoryByCallRaw
) { ) {
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
for (ChatMember m : members) {
if (m == null) continue;
String callRaw = normalizeCallRaw(m.getCallSignRaw());
if (callRaw == null || callRaw.isEmpty()) continue;
byCallRaw.computeIfAbsent(callRaw, k -> new ArrayList<>()).add(m);
}
Map<String, ChatMember> representative = new HashMap<>(byCallRaw.size()); Map<String, ChatMember> representative = new HashMap<>(byCallRaw.size());
for (Map.Entry<String, List<ChatMember>> entry : byCallRaw.entrySet()) { for (Map.Entry<String, List<ChatMember>> entry : byCallRaw.entrySet()) {
@@ -218,12 +235,11 @@ public final class ScoreService {
ChatMember chosen = null; ChatMember chosen = null;
if (preferredCat != null) { if (preferredCat != null) {
for (ChatMember v : variants) { chosen = variants.stream()
if (v != null && v.getChatCategory() == preferredCat) { .filter(Objects::nonNull)
chosen = v; .filter(v -> isSameChatCategory(v.getChatCategory(), preferredCat))
break; .max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
} .orElse(null);
}
} }
if (chosen == null) { if (chosen == null) {
@@ -238,6 +254,14 @@ public final class ScoreService {
return representative; 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 * Projects the immutable score snapshot back into ChatMember display fields so
* the normal station table can sort/filter by score without knowing the score * the normal station table can sort/filter by score without knowing the score
@@ -35,13 +35,11 @@ public class ScoreboardUpdateTask extends TimerTask {
ChatMessage beaconMSG = new ChatMessage(); ChatMessage beaconMSG = new ChatMessage();
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat(); MessageVariableResolver variableResolver =
// replaceVariables = bcn_beaconText; new MessageVariableResolver(this.chatController.getChatPreferences());
String replaceVariables = variableResolver.resolveGlobalVariables(
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue()); this.chatController.getChatPreferences().getBcn_beaconTextMainCat()
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign()); );
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
beaconMSG.setMessageText( beaconMSG.setMessageText(
@@ -2,6 +2,7 @@ package kst4contest.controller;
import kst4contest.logic.SignalDetector; import kst4contest.logic.SignalDetector;
import kst4contest.model.ChatPreferences; import kst4contest.model.ChatPreferences;
import kst4contest.model.ChatMember;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Deque; import java.util.Deque;
@@ -21,7 +22,7 @@ import java.util.regex.Pattern;
public final class StationMetricsService { public final class StationMetricsService {
/** /cq <CALL> ... */ /** /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. */ /** Rolling window timestamps for momentum scoring. */
private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32; private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32;
@@ -195,7 +196,7 @@ public final class StationMetricsService {
private static String normalizeCallRaw(String s) { private static String normalizeCallRaw(String s) {
if (s == null) return null; if (s == null) return null;
return s.trim().toUpperCase(); return ChatMember.normalizeCallSignToBaseCallSign(s);
} }
private static final class StationMetrics { private static final class StationMetrics {
@@ -39,26 +39,63 @@ public class WinTestSkedSender {
} }
/** /**
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED * Pushes a ContestSked into Win-Test by sending the
* sequence via UDP broadcast. * LOCKSKED / ADDSKED / UNLOCKSKED sequence.
* *
* @param sked the sked to push * @param sked sked to push
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0) * @param targetCallsign callsign prepared for Win-Test
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST") * @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 { try {
sendLockSked(); sendLockSked();
sendAddSked(sked, frequencyKHz, notes, modeOverride);
sendAddSked(
sked,
targetCallsign,
frequencyKHz,
notes,
mode
);
sendUnlockSked(); sendUnlockSked();
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false); reportStatus(
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign() "Sked pushed to WT: " + targetCallsign,
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand()); false
} catch (Exception e) { );
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage()); System.out.println(
e.printStackTrace(); "[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. * Sends an ADDSKED message with the sked details.
* <p> *
* Win-Test ADDSKED data format (from wtKST): * <p>The wtKST implementation subtracts a reference time of
* <pre> * 1970-01-01 00:01:00 UTC and subsequently adds 60 seconds. Both
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}" * operations cancel each other out. The transmitted value is therefore
* </pre> * an ordinary Unix timestamp and must not receive another offset here.</p>
* <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.
*/ */
private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception { private void sendAddSked(ContestSked sked,
// Win-Test timestamp: epoch seconds with 60s offset String targetCallsign,
long epochSeconds = sked.getSkedTimeEpoch() / 1000; double frequencyKHz,
long wtTimestamp = epochSeconds + 60; String notes,
int mode) throws Exception {
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10 long wtTimestamp =
long freqTenthKHz = Math.round(frequencyKHz * 10.0); sked.getSkedTimeEpoch() / 1000L;
// Win-Test band ID // Frequency in 0.1 kHz units.
int bandId = toWinTestBandId(sked.getBand()); long frequencyTenthKHz =
Math.round(frequencyKHz * 10.0);
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB int bandId =
int mode; toWinTestBandId(sked.getBand());
if (modeOverride >= 0) {
mode = modeOverride;
} else {
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
}
String data = wtTimestamp /*
+ " " + freqTenthKHz * Accept only the mode IDs supported by this UI.
* Any unexpected value falls back to SSB.
*/
int winTestMode =
mode == 0
? 0
: 1;
String data =
wtTimestamp
+ " " + frequencyTenthKHz
+ " " + bandId + " " + bandId
+ " " + mode + " " + winTestMode
+ " \"" + sked.getTargetCallsign() + "\"" + " \"" + targetCallsign + "\""
+ " \"" + (notes != null ? notes : "") + "\""; + " \"" + (notes != null ? notes : "") + "\"";
WinTestMessage msg = new WinTestMessage( WinTestMessage message = new WinTestMessage(
WinTestMessage.MessageType.ADDSKED, WinTestMessage.MessageType.ADDSKED,
stationName, "", stationName,
data); "",
sendUdp(msg); data
);
sendUdp(message);
} }
/** /**
@@ -155,6 +199,8 @@ public class WinTestSkedSender {
public static int toWinTestBandId(Band band) { public static int toWinTestBandId(Band band) {
if (band == null) return 12; // default to 144 MHz if (band == null) return 12; // default to 144 MHz
return switch (band) { return switch (band) {
case B_50 -> 10;
case B_70 -> 11;
case B_144 -> 12; case B_144 -> 12;
case B_432 -> 14; case B_432 -> 14;
case B_1296 -> 16; case B_1296 -> 16;
@@ -166,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) { private void reportStatus(String text, boolean isError) {
if (callback != null) { if (callback != null) {
callback.onThreadStatus(THREAD_NICKNAME, callback.onThreadStatus(THREAD_NICKNAME,
@@ -98,6 +98,8 @@ public final class WorkedGrossFieldCache {
if (member.isWorked3400()) addWorked(Band.B_3400, locator); if (member.isWorked3400()) addWorked(Band.B_3400, locator);
if (member.isWorked5600()) addWorked(Band.B_5760, locator); if (member.isWorked5600()) addWorked(Band.B_5760, locator);
if (member.isWorked10G()) addWorked(Band.B_10G, locator); if (member.isWorked10G()) addWorked(Band.B_10G, locator);
if (member.isWorked50()) addWorked(Band.B_50, locator);
if (member.isWorked70()) addWorked(Band.B_70, locator);
} }
} }
@@ -0,0 +1,264 @@
package kst4contest.logic;
import kst4contest.model.Band;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Resolves band availability and band-upgrade opportunities from one or more
* active {@link ChatMember} variants of the same base callsign.
*
* <p>The resolver deliberately separates a band hint from an exact frequency:
* {@code knownActiveBands} remains the source for detected QRGs with timestamps,
* while the station name may add a band without inventing a frequency.</p>
*
* <p>A manual NOT-QRV flag always overrides automatic evidence. Worked flags are
* evaluated separately because an offered band may still be useful for display,
* even when it is no longer a band-upgrade opportunity.</p>
*/
public final class BandOpportunityResolver {
public static final long RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS = 30L * 60L * 1000L;
private static final Map<Band, Pattern> STATION_NAME_BAND_PATTERNS = createStationNameBandPatterns();
private BandOpportunityResolver() {
}
/**
* Resolves the common band state using the application-wide 30-minute window
* for frequency evidence. Name-derived band hints remain valid while the
* ChatMember is present in the active chat model.
*/
public static Resolution resolve(Collection<ChatMember> variants, long nowEpochMs) {
return resolve(variants, nowEpochMs, RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS);
}
/**
* Resolves offered, worked and manually excluded bands across all supplied
* category/callsign variants.
*/
public static Resolution resolve(Collection<ChatMember> variants,
long nowEpochMs,
long dynamicEvidenceMaxAgeMs) {
EnumSet<Band> offeredBands = EnumSet.noneOf(Band.class);
EnumSet<Band> workedBands = EnumSet.noneOf(Band.class);
EnumSet<Band> notQrvBands = EnumSet.noneOf(Band.class);
if (variants == null) {
return new Resolution(offeredBands, workedBands, notQrvBands);
}
for (ChatMember member : variants) {
if (member == null) {
continue;
}
collectRecentFrequencyBands(
member,
offeredBands,
nowEpochMs,
dynamicEvidenceMaxAgeMs
);
offeredBands.addAll(detectBandsFromStationName(member.getName()));
collectWorkedBands(member, workedBands);
collectNotQrvBands(member, notQrvBands);
}
return new Resolution(offeredBands, workedBands, notQrvBands);
}
/**
* Returns the bands enabled in the local station setup. Bands above 10 GHz
* remain excluded because the current preferences do not provide active-band
* flags for them.
*/
public static EnumSet<Band> getEnabledStationBands(ChatPreferences preferences) {
EnumSet<Band> enabledBands = EnumSet.noneOf(Band.class);
if (preferences == null) {
return enabledBands;
}
if (preferences.isStn_bandActive50()) enabledBands.add(Band.B_50);
if (preferences.isStn_bandActive70()) enabledBands.add(Band.B_70);
if (preferences.isStn_bandActive144()) enabledBands.add(Band.B_144);
if (preferences.isStn_bandActive432()) enabledBands.add(Band.B_432);
if (preferences.isStn_bandActive1240()) enabledBands.add(Band.B_1296);
if (preferences.isStn_bandActive2300()) enabledBands.add(Band.B_2320);
if (preferences.isStn_bandActive3400()) enabledBands.add(Band.B_3400);
if (preferences.isStn_bandActive5600()) enabledBands.add(Band.B_5760);
if (preferences.isStn_bandActive10G()) enabledBands.add(Band.B_10G);
return enabledBands;
}
/**
* Detects explicit amateur-band designators in a station name.
*
* <p>The boundary rules are intentionally stricter than simple token splitting.
* In particular, the trailing {@code 2} in {@code 1.2 cm} must not be mistaken
* for the 2 m band.</p>
*/
public static EnumSet<Band> detectBandsFromStationName(String stationName) {
EnumSet<Band> detectedBands = EnumSet.noneOf(Band.class);
if (stationName == null || stationName.isBlank()) {
return detectedBands;
}
for (Map.Entry<Band, Pattern> entry : STATION_NAME_BAND_PATTERNS.entrySet()) {
if (entry.getValue().matcher(stationName).find()) {
detectedBands.add(entry.getKey());
}
}
return detectedBands;
}
private static void collectRecentFrequencyBands(ChatMember member,
EnumSet<Band> target,
long nowEpochMs,
long dynamicEvidenceMaxAgeMs) {
if (member.getKnownActiveBands() == null || member.getKnownActiveBands().isEmpty()) {
return;
}
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry
: member.getKnownActiveBands().entrySet()) {
Band band = entry.getKey();
ChatMember.ActiveFrequencyInfo info = entry.getValue();
if (band == null || info == null) {
continue;
}
long ageMs = nowEpochMs - info.timestampEpoch;
boolean ageAccepted = dynamicEvidenceMaxAgeMs <= 0L
? ageMs >= 0L
: ageMs >= 0L && ageMs <= dynamicEvidenceMaxAgeMs;
if (ageAccepted) {
target.add(band);
}
}
}
private static void collectWorkedBands(ChatMember member, EnumSet<Band> target) {
if (member.isWorked50()) target.add(Band.B_50);
if (member.isWorked70()) target.add(Band.B_70);
if (member.isWorked144()) target.add(Band.B_144);
if (member.isWorked432()) target.add(Band.B_432);
if (member.isWorked1240()) target.add(Band.B_1296);
if (member.isWorked2300()) target.add(Band.B_2320);
if (member.isWorked3400()) target.add(Band.B_3400);
if (member.isWorked5600()) target.add(Band.B_5760);
if (member.isWorked10G()) target.add(Band.B_10G);
if (member.isWorked24G()) target.add(Band.B_24G);
}
private static void collectNotQrvBands(ChatMember member, EnumSet<Band> target) {
if (!member.isQrv50()) target.add(Band.B_50);
if (!member.isQrv70()) target.add(Band.B_70);
if (!member.isQrv144()) target.add(Band.B_144);
if (!member.isQrv432()) target.add(Band.B_432);
if (!member.isQrv1240()) target.add(Band.B_1296);
if (!member.isQrv2300()) target.add(Band.B_2320);
if (!member.isQrv3400()) target.add(Band.B_3400);
if (!member.isQrv5600()) target.add(Band.B_5760);
if (!member.isQrv10G()) target.add(Band.B_10G);
// There is currently no persisted NOT-QRV flag for 24 GHz.
}
private static Map<Band, Pattern> createStationNameBandPatterns() {
Map<Band, Pattern> patterns = new EnumMap<>(Band.class);
// Bare "70" and bare "6" are already claimed by the 70cm/6cm shorthand below
// (their "CM" suffix is optional), so 4m/6m must require an explicit MHz/"M"
// suffix here to avoid misreading a cm-band shorthand as 70/50 MHz.
patterns.put(Band.B_50, bandPattern("50(?:\\s*MHZ)?|6\\s*M"));
patterns.put(Band.B_70, bandPattern("70\\s*MHZ|4\\s*M"));
patterns.put(Band.B_144, bandPattern("144(?:\\s*MHZ)?|2(?:\\s*M)?"));
patterns.put(Band.B_432, bandPattern("432(?:\\s*MHZ)?|70(?:\\s*CM)?"));
patterns.put(Band.B_1296, bandPattern("1296(?:\\s*MHZ)?|23(?:\\s*CM)?"));
patterns.put(Band.B_2320, bandPattern("(?:2300|2320)(?:\\s*MHZ)?|13(?:\\s*CM)?"));
patterns.put(Band.B_3400, bandPattern("3400(?:\\s*MHZ)?|9(?:\\s*CM)?"));
patterns.put(Band.B_5760, bandPattern("(?:5600|5760)(?:\\s*MHZ)?|6(?:\\s*CM)?"));
patterns.put(Band.B_10G, bandPattern("10368(?:\\s*MHZ)?|10\\s*G(?:HZ)?|3(?:\\s*CM)?"));
patterns.put(Band.B_24G, bandPattern("24048(?:\\s*MHZ)?|24\\s*G(?:HZ)?|1[.,]2(?:\\s*CM)?"));
return Collections.unmodifiableMap(patterns);
}
private static Pattern bandPattern(String alternatives) {
return Pattern.compile(
"(?<![A-Z0-9.,])(?:" + alternatives + ")(?![A-Z0-9.,])",
Pattern.CASE_INSENSITIVE
);
}
/** Immutable result of one callsign-wide band resolution. */
public static final class Resolution {
private final EnumSet<Band> offeredBands;
private final EnumSet<Band> workedBands;
private final EnumSet<Band> notQrvBands;
private Resolution(EnumSet<Band> offeredBands,
EnumSet<Band> workedBands,
EnumSet<Band> notQrvBands) {
this.offeredBands = copyOf(offeredBands);
this.workedBands = copyOf(workedBands);
this.notQrvBands = copyOf(notQrvBands);
}
/** Returns all recent/name-derived bands before NOT-QRV is applied. */
public EnumSet<Band> getOfferedBands() {
return copyOf(offeredBands);
}
public EnumSet<Band> getWorkedBands() {
return copyOf(workedBands);
}
public EnumSet<Band> getNotQrvBands() {
return copyOf(notQrvBands);
}
/** Returns offered bands after manual NOT-QRV exclusions. */
public EnumSet<Band> getAvailableBands() {
EnumSet<Band> availableBands = copyOf(offeredBands);
availableBands.removeAll(notQrvBands);
return availableBands;
}
/** Returns offered, QRV, enabled and not-yet-worked bands. */
public EnumSet<Band> getUnworkedEnabledBands(EnumSet<Band> enabledBands) {
EnumSet<Band> opportunities = getAvailableBands();
if (enabledBands == null || enabledBands.isEmpty()) {
opportunities.clear();
return opportunities;
}
opportunities.retainAll(enabledBands);
opportunities.removeAll(workedBands);
return opportunities;
}
public boolean hasBandEvidence() {
return !offeredBands.isEmpty();
}
private static EnumSet<Band> copyOf(EnumSet<Band> source) {
return source == null || source.isEmpty()
? EnumSet.noneOf(Band.class)
: EnumSet.copyOf(source);
}
}
}
@@ -3,9 +3,9 @@ package kst4contest.logic;
import kst4contest.controller.StationMetricsService; import kst4contest.controller.StationMetricsService;
import kst4contest.model.*; import kst4contest.model.*;
import java.util.Collection;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Priority score calculation (off FX-thread). * Priority score calculation (off FX-thread).
@@ -16,10 +16,8 @@ import java.util.Map;
*/ */
public class PriorityCalculator { public class PriorityCalculator {
/** Max age for "known active bands" (derived from chat history). */
private static final long RX_BANDS_MAX_AGE_MS = 30L * 60L * 1000L; // 30 minutes
public double calculatePriority(ChatMember member, public double calculatePriority(ChatMember member,
Collection<ChatMember> callsignVariants,
ChatPreferences prefs, ChatPreferences prefs,
List<ContestSked> activeSkeds, List<ContestSked> activeSkeds,
StationMetricsService.Snapshot metricsSnapshot, StationMetricsService.Snapshot metricsSnapshot,
@@ -33,41 +31,51 @@ public class PriorityCalculator {
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands" // 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// -------------------------------------------------------------------- Collection<ChatMember> variants = callsignVariants == null || callsignVariants.isEmpty()
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands" ? List.of(member)
// -------------------------------------------------------------------- : callsignVariants;
EnumSet<Band> myEnabledBands = getMyEnabledBands(prefs);
// "worked" for scoring is derived ONLY from per-band flags (worked144/432/...) BandOpportunityResolver.Resolution bandResolution =
// IMPORTANT: ChatMember.worked is UI-only and NOT used in scoring. BandOpportunityResolver.resolve(variants, nowEpochMs);
EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
EnumSet<Band> myEnabledBands =
BandOpportunityResolver.getEnabledStationBands(prefs);
// ChatMember.worked remains UI-only. Scoring uses only per-band worked flags.
EnumSet<Band> workedBandsForScoring = bandResolution.getWorkedBands();
// Remaining bands that are:
// - recently offered by the station (from knownActiveBands history)
// - enabled at our station
// - NOT worked yet (per-band flags)
// If we do not know offered bands (history empty), this remains empty.
EnumSet<Band> unworkedPossible = EnumSet.noneOf(Band.class); EnumSet<Band> unworkedPossible = EnumSet.noneOf(Band.class);
EnumSet<Band> stationOfferedBands = getStationOfferedBandsFromHistory(member, nowEpochMs); EnumSet<Band> stationOfferedBands = bandResolution.getOfferedBands();
EnumSet<Band> stationAvailableBands = bandResolution.getAvailableBands();
EnumSet<Band> possibleBands = stationOfferedBands.isEmpty() EnumSet<Band> possibleBands = stationOfferedBands.isEmpty()
? EnumSet.noneOf(Band.class) // unknown => don't hard-filter ? EnumSet.noneOf(Band.class)
: EnumSet.copyOf(stationOfferedBands); : EnumSet.copyOf(stationAvailableBands);
if (!possibleBands.isEmpty()) { if (!stationOfferedBands.isEmpty()) {
possibleBands.retainAll(myEnabledBands); possibleBands.retainAll(myEnabledBands);
if (possibleBands.isEmpty()) { if (possibleBands.isEmpty()) {
// We know their bands, but none of them are enabled at our station. // Known bands are disabled locally or manually marked NOT QRV.
return 0.0; return 0.0;
} }
unworkedPossible = EnumSet.copyOf(possibleBands); unworkedPossible = EnumSet.copyOf(possibleBands);
unworkedPossible.removeAll(workedBandsForScoring); unworkedPossible.removeAll(workedBandsForScoring);
// If already worked on all possible bands => no priority on them anymore (contest logic).
if (unworkedPossible.isEmpty()) { if (unworkedPossible.isEmpty()) {
return 0.0; return 0.0;
} }
} else {
/*
* Missing band evidence is not automatically negative. A complete manual
* NOT-QRV exclusion is different: if every enabled own band is excluded,
* this station cannot be a current contest candidate.
*/
EnumSet<Band> notExplicitlyExcluded = EnumSet.copyOf(myEnabledBands);
notExplicitlyExcluded.removeAll(bandResolution.getNotQrvBands());
if (!myEnabledBands.isEmpty() && notExplicitlyExcluded.isEmpty()) {
return 0.0;
}
} }
// -------------------------------------------------------------------- // --------------------------------------------------------------------
@@ -225,46 +233,6 @@ public class PriorityCalculator {
return Math.max(0.0, score); return Math.max(0.0, score);
} }
private static EnumSet<Band> getMyEnabledBands(ChatPreferences prefs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (prefs.isStn_bandActive144()) out.add(Band.B_144);
if (prefs.isStn_bandActive432()) out.add(Band.B_432);
if (prefs.isStn_bandActive1240()) out.add(Band.B_1296);
if (prefs.isStn_bandActive2300()) out.add(Band.B_2320);
if (prefs.isStn_bandActive3400()) out.add(Band.B_3400);
if (prefs.isStn_bandActive5600()) out.add(Band.B_5760);
if (prefs.isStn_bandActive10G()) out.add(Band.B_10G);
return out;
}
private static EnumSet<Band> getStationOfferedBandsFromHistory(ChatMember member, long nowEpochMs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
Map<Band, ChatMember.ActiveFrequencyInfo> map = member.getKnownActiveBands();
if (map == null || map.isEmpty()) return out;
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> e : map.entrySet()) {
if (e == null || e.getKey() == null || e.getValue() == null) continue;
long age = nowEpochMs - e.getValue().timestampEpoch;
if (age <= RX_BANDS_MAX_AGE_MS) {
out.add(e.getKey());
}
}
return out;
}
private static EnumSet<Band> getWorkedBands(ChatMember member) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (member.isWorked144()) out.add(Band.B_144);
if (member.isWorked432()) out.add(Band.B_432);
if (member.isWorked1240()) out.add(Band.B_1296);
if (member.isWorked2300()) out.add(Band.B_2320);
if (member.isWorked3400()) out.add(Band.B_3400);
if (member.isWorked5600()) out.add(Band.B_5760);
if (member.isWorked10G()) out.add(Band.B_10G);
if (member.isWorked24G()) out.add(Band.B_24G);
return out;
}
private static int findNextAirplaneArrivingMinutes(AirPlaneReflectionInfo apInfo) { private static int findNextAirplaneArrivingMinutes(AirPlaneReflectionInfo apInfo) {
try { try {
if (apInfo.getRisingAirplanes() == null || apInfo.getRisingAirplanes().isEmpty()) return -1; if (apInfo.getRisingAirplanes() == null || apInfo.getRisingAirplanes().isEmpty()) return -1;
+33
View File
@@ -5,6 +5,8 @@ package kst4contest.model;
* Used for plausibility checks in the Smart Parser. * Used for plausibility checks in the Smart Parser.
*/ */
public enum Band { public enum Band {
B_50(50.000, 54.000, "50"),
B_70(70.000, 70.500, "70"),
B_144(144.000, 146.000, "144"), B_144(144.000, 146.000, "144"),
B_432(432.000, 434.000, "432"), B_432(432.000, 434.000, "432"),
B_1296(1296.000, 1298.000, "1296"), B_1296(1296.000, 1298.000, "1296"),
@@ -29,6 +31,35 @@ public enum Band {
return prefix; return prefix;
} }
/**
* Resolves a configured MHz prefix to one of the bands supported by the
* frequency parser.
*
* <p>The former free-text preference accepted arbitrary numeric values even
* though only prefixes represented by this enum can be used for a plausible
* frequency. Keeping the lookup here gives the UI and the parser one common
* definition of a valid fallback band.</p>
*
* @param prefix configured MHz prefix, for example {@code 144} or {@code 10368}
* @return matching band, or {@code null} if the prefix is not supported
*/
public static Band fromPrefix(String prefix) {
if (prefix == null) {
return null;
}
String normalizedPrefix = prefix.trim();
for (Band band : values()) {
if (band.prefix.equals(normalizedPrefix)) {
return band;
}
}
return null;
}
/** /**
* Returns the lower edge used as practical analysis frequency when only the band * Returns the lower edge used as practical analysis frequency when only the band
* is known. This keeps the batch reachability calculation deterministic. * is known. This keeps the batch reachability calculation deterministic.
@@ -46,6 +77,8 @@ public enum Band {
*/ */
public String getDisplayLabel() { public String getDisplayLabel() {
switch (this) { switch (this) {
case B_50: return "50";
case B_70: return "70";
case B_144: return "144"; case B_144: return "144";
case B_432: return "432"; case B_432: return "432";
case B_1296: return "1296"; case B_1296: return "1296";
@@ -26,9 +26,16 @@ public class ChatMember {
String name; String name;
String callSignRaw; //without -2 or -70 etc. String callSignRaw; //without -2 or -70 etc.
/**
* A directional opportunity inferred from a directed chat message remains
* relevant for five minutes. The timestamp is used instead of a permanent
* boolean so an old antenna-direction assumption cannot remain visible
* indefinitely.
*/
static final long DIRECTION_OPPORTUNITY_VALIDITY_MILLIS = 5L * 60L * 1000L;
private volatile long directionOpportunityValidUntilEpochMs;
boolean isInAngleAndRange; //if he tries a sked in my dir, he is in range, will process that in the messages
// String frequency; // last known qrg of the station // String frequency; // last known qrg of the station
@@ -65,6 +72,8 @@ public class ChatMember {
/** /**
* Chatmember is qrv at all band except we initialize anything other, depending to user entry * Chatmember is qrv at all band except we initialize anything other, depending to user entry
*/ */
boolean qrv50 = true;
boolean qrv70 = true;
boolean qrv144 = true; boolean qrv144 = true;
boolean qrv432 = true; boolean qrv432 = true;
boolean qrv1240 = true; boolean qrv1240 = true;
@@ -108,12 +117,37 @@ public class ChatMember {
this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs; this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs;
} }
/**
* Returns whether the most recently inferred directional opportunity is
* still valid.
*
* @return {@code true} until the five-minute validity period has expired
*/
public boolean isInAngleAndRange() { public boolean isInAngleAndRange() {
return isInAngleAndRange; return isInAngleAndRangeAt(System.currentTimeMillis());
} }
/**
* Time-aware variant used by the public getter and by unit tests.
*
* @param nowEpochMs time against which the validity is checked
* @return {@code true} while the stored validity timestamp is still in the future
*/
boolean isInAngleAndRangeAt(long nowEpochMs) {
return directionOpportunityValidUntilEpochMs > nowEpochMs;
}
/**
* Starts a new five-minute validity period or removes the current
* directional opportunity immediately.
*
* @param inAngleAndRange {@code true} for a newly detected opportunity;
* {@code false} to clear it
*/
public void setInAngleAndRange(boolean inAngleAndRange) { public void setInAngleAndRange(boolean inAngleAndRange) {
isInAngleAndRange = inAngleAndRange; directionOpportunityValidUntilEpochMs = inAngleAndRange
? System.currentTimeMillis() + DIRECTION_OPPORTUNITY_VALIDITY_MILLIS
: 0L;
} }
public AirPlaneReflectionInfo getAirPlaneReflectInfo() { public AirPlaneReflectionInfo getAirPlaneReflectInfo() {
@@ -196,6 +230,22 @@ public class ChatMember {
worked10G = worked10g; worked10G = worked10g;
} }
public boolean isQrv50() {
return qrv50;
}
public void setQrv50(boolean qrv50) {
this.qrv50 = qrv50;
}
public boolean isQrv70() {
return qrv70;
}
public void setQrv70(boolean qrv70) {
this.qrv70 = qrv70;
}
public boolean isQrv144() { public boolean isQrv144() {
return qrv144; return qrv144;
} }
@@ -575,6 +625,8 @@ public class ChatMember {
public void resetQRVInformationAtAllBands() { public void resetQRVInformationAtAllBands() {
this.setQrvAny(true); this.setQrvAny(true);
this.setQrv50(true);
this.setQrv70(true);
this.setQrv144(true); this.setQrv144(true);
this.setQrv432(true); this.setQrv432(true);
this.setQrv1240(true); this.setQrv1240(true);
@@ -0,0 +1,34 @@
package kst4contest.model;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ChatMemberDirectionOpportunityTest {
@Test
void directionalOpportunityExpiresAfterConfiguredValidityPeriod() {
ChatMember member = new ChatMember();
member.setInAngleAndRange(true);
long afterActivationEpochMs = System.currentTimeMillis();
assertTrue(member.isInAngleAndRangeAt(afterActivationEpochMs));
assertFalse(member.isInAngleAndRangeAt(
afterActivationEpochMs
+ ChatMember.DIRECTION_OPPORTUNITY_VALIDITY_MILLIS
+ 1L));
}
@Test
void directionalOpportunityCanBeClearedImmediately() {
ChatMember member = new ChatMember();
member.setInAngleAndRange(true);
assertTrue(member.isInAngleAndRange());
member.setInAngleAndRange(false);
assertFalse(member.isInAngleAndRange());
}
}
@@ -50,7 +50,7 @@ public class ChatPreferences {
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults. * Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
*/ */
// private static final int CONFIG_VERSION = 2; // private static final int CONFIG_VERSION = 2;
public static final int CONFIG_VERSION = 3; public static final int CONFIG_VERSION = 5;
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility. // Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
private static final String TAG_CONFIG_VERSION = "configVersion"; private static final String TAG_CONFIG_VERSION = "configVersion";
@@ -197,6 +197,8 @@ public class ChatPreferences {
boolean loginToSecondChatEnabled; boolean loginToSecondChatEnabled;
DoubleProperty actualQTF = new SimpleDoubleProperty(360); // will be updated by user at runtime! DoubleProperty actualQTF = new SimpleDoubleProperty(360); // will be updated by user at runtime!
boolean stn_bandActive50;
boolean stn_bandActive70;
boolean stn_bandActive144; boolean stn_bandActive144;
boolean stn_bandActive432; boolean stn_bandActive432;
boolean stn_bandActive1240; boolean stn_bandActive1240;
@@ -222,7 +224,7 @@ public class ChatPreferences {
boolean logsynch_wintestNetworkListenerEnabled = true; // default true = bisheriges Verhalten boolean logsynch_wintestNetworkListenerEnabled = true; // default true = bisheriges Verhalten
String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test 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 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_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet
boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet
@@ -240,9 +242,11 @@ public class ChatPreferences {
/** /**
* AirScout prefs * AirScout prefs
*/ */
boolean AirScout_asUDPListenerEnabled; boolean AirScout_asUDPListenerEnabled = true;
String AirScout_asServerNameString, AirScout_asClientNameString, AirScout_asBandString; String AirScout_asServerNameString = "AS";
int AirScout_asCommunicationPort; String AirScout_asClientNameString = "KST";
String AirScout_asBandString = "1440000";
int AirScout_asCommunicationPort = 9872;
/** /**
* Notification prefs * Notification prefs
@@ -265,6 +269,9 @@ public class ChatPreferences {
boolean notify_DXClusterServerTriggerBearing; boolean notify_DXClusterServerTriggerBearing;
boolean notify_DXClusterServerTriggerOnQRGDetect; boolean notify_DXClusterServerTriggerOnQRGDetect;
ObservableList<String>
lstNotify_QSOSniffer_sniffedCallSignList =
FXCollections.observableArrayList();
// ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList(); // ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
ObservableList<String> lstNotify_QSOSniffer_sniffedWordsList = FXCollections.observableArrayList(); ObservableList<String> lstNotify_QSOSniffer_sniffedWordsList = FXCollections.observableArrayList();
ObservableList<String> lstNotify_QSOSniffer_sniffedPrefixLocList = FXCollections.observableArrayList(); ObservableList<String> lstNotify_QSOSniffer_sniffedPrefixLocList = FXCollections.observableArrayList();
@@ -327,9 +334,12 @@ public class ChatPreferences {
boolean guiOptions_defaultFilterPmToMe; boolean guiOptions_defaultFilterPmToMe;
boolean guiOptions_defaultFilterPmToOther; boolean guiOptions_defaultFilterPmToOther;
boolean guiOptions_defaultFilterPublicMsgs; boolean guiOptions_defaultFilterPublicMsgs;
boolean guiOptions_showGrossFieldWorkedHintInBandColumns = true; // show "o" (grid square already worked on this band) in the band columns
boolean guiOptions_showFreshCallHintInBandColumns = true; // show "a" (band available, call not worked on any band yet) instead of always "B+" in the band columns
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 }; private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN }; private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
private boolean GUIstationMapPathAnalysisVisible = true;
/********************************************************************************* /*********************************************************************************
@@ -453,6 +463,11 @@ public class ChatPreferences {
this.stn_on4kstServersDns = stn_on4kstServersDns; this.stn_on4kstServersDns = stn_on4kstServersDns;
} }
public ObservableList<String>
getLstNotify_QSOSniffer_sniffedCallSignList() {
return lstNotify_QSOSniffer_sniffedCallSignList;
}
public ObservableList<String> getLstNotify_QSOSniffer_sniffedWordsList() { public ObservableList<String> getLstNotify_QSOSniffer_sniffedWordsList() {
return lstNotify_QSOSniffer_sniffedWordsList; return lstNotify_QSOSniffer_sniffedWordsList;
} }
@@ -621,6 +636,14 @@ public class ChatPreferences {
this.GUIstationMapStagePositionXY = GUIstationMapStagePositionXY; this.GUIstationMapStagePositionXY = GUIstationMapStagePositionXY;
} }
public boolean isGUIstationMapPathAnalysisVisible() {
return GUIstationMapPathAnalysisVisible;
}
public void setGUIstationMapPathAnalysisVisible(boolean GUIstationMapPathAnalysisVisible) {
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
}
public boolean isGuiOptions_defaultFilterNothing() { public boolean isGuiOptions_defaultFilterNothing() {
return guiOptions_defaultFilterNothing; return guiOptions_defaultFilterNothing;
} }
@@ -637,6 +660,22 @@ public class ChatPreferences {
this.guiOptions_defaultFilterPmToMe = guiOptions_defaultFilterPmToMe; this.guiOptions_defaultFilterPmToMe = guiOptions_defaultFilterPmToMe;
} }
public boolean isGuiOptions_showGrossFieldWorkedHintInBandColumns() {
return guiOptions_showGrossFieldWorkedHintInBandColumns;
}
public void setGuiOptions_showGrossFieldWorkedHintInBandColumns(boolean guiOptions_showGrossFieldWorkedHintInBandColumns) {
this.guiOptions_showGrossFieldWorkedHintInBandColumns = guiOptions_showGrossFieldWorkedHintInBandColumns;
}
public boolean isGuiOptions_showFreshCallHintInBandColumns() {
return guiOptions_showFreshCallHintInBandColumns;
}
public void setGuiOptions_showFreshCallHintInBandColumns(boolean guiOptions_showFreshCallHintInBandColumns) {
this.guiOptions_showFreshCallHintInBandColumns = guiOptions_showFreshCallHintInBandColumns;
}
public boolean isGuiOptions_defaultFilterPmToOther() { public boolean isGuiOptions_defaultFilterPmToOther() {
return guiOptions_defaultFilterPmToOther; return guiOptions_defaultFilterPmToOther;
} }
@@ -799,8 +838,16 @@ public class ChatPreferences {
return notify_dxclusterServerPort; return notify_dxclusterServerPort;
} }
public void setNotify_dxclusterServerPort(int notify_dxclusterServerPort) { public void setNotify_dxclusterServerPort(
this.notify_dxclusterServerPort = notify_dxclusterServerPort; int notify_dxclusterServerPort
) {
if (notify_dxclusterServerPort < 1
|| notify_dxclusterServerPort > 65535) {
this.notify_dxclusterServerPort = 8000;
} else {
this.notify_dxclusterServerPort =
notify_dxclusterServerPort;
}
} }
public double[] getGUIscn_ChatwindowMainSceneSizeHW() { public double[] getGUIscn_ChatwindowMainSceneSizeHW() {
@@ -921,35 +968,100 @@ public class ChatPreferences {
return stn_loginCallSignRaw; return stn_loginCallSignRaw;
} }
/**
* Normalizes an AirScout routing identifier.
*
* AirScout encloses the identifiers in quotation marks. Empty identifiers,
* quotation marks and line breaks would therefore produce an invalid protocol
* message and are replaced with the supplied default value.
*
* @param identifier configured identifier
* @param defaultIdentifier fallback value
* @return normalized identifier
*/
private String normalizeAirScoutIdentifier(
String identifier,
String defaultIdentifier
) {
if (identifier == null) {
return defaultIdentifier;
}
String normalizedIdentifier = identifier.trim();
if (normalizedIdentifier.isEmpty()
|| normalizedIdentifier.contains("\"")
|| normalizedIdentifier.contains("\r")
|| normalizedIdentifier.contains("\n")) {
return defaultIdentifier;
}
return normalizedIdentifier;
}
public String getAirScout_asBandString() { public String getAirScout_asBandString() {
return AirScout_asBandString; return AirScout_asBandString;
} }
public void setAirScout_asBandString(String airScout_asBandString) { public void setAirScout_asBandString(String airScout_asBandString) {
AirScout_asBandString = airScout_asBandString; if (airScout_asBandString == null) {
AirScout_asBandString = "1440000";
return;
}
try {
long parsedBandValue = Long.parseLong(
airScout_asBandString.trim()
);
AirScout_asBandString = parsedBandValue > 0
? Long.toString(parsedBandValue)
: "1440000";
} catch (NumberFormatException exception) {
AirScout_asBandString = "1440000";
}
} }
public String getAirScout_asServerNameString() { public String getAirScout_asServerNameString() {
return AirScout_asServerNameString; return AirScout_asServerNameString;
} }
public void setAirScout_asServerNameString(String airScout_asServerNameString) { public void setAirScout_asServerNameString(
AirScout_asServerNameString = airScout_asServerNameString; String airScout_asServerNameString
) {
AirScout_asServerNameString = normalizeAirScoutIdentifier(
airScout_asServerNameString,
"AS"
);
} }
public String getAirScout_asClientNameString() { public String getAirScout_asClientNameString() {
return AirScout_asClientNameString; return AirScout_asClientNameString;
} }
public void setAirScout_asClientNameString(String airScout_asClientNameString) { public void setAirScout_asClientNameString(
AirScout_asClientNameString = airScout_asClientNameString; String airScout_asClientNameString
) {
AirScout_asClientNameString = normalizeAirScoutIdentifier(
airScout_asClientNameString,
"KST"
);
} }
public int getAirScout_asCommunicationPort() { public int getAirScout_asCommunicationPort() {
return AirScout_asCommunicationPort; return AirScout_asCommunicationPort;
} }
public void setAirScout_asCommunicationPort(int airScout_asCommunicationPort) { public void setAirScout_asCommunicationPort(
int airScout_asCommunicationPort
) {
if (airScout_asCommunicationPort < 1
|| airScout_asCommunicationPort > 65535) {
AirScout_asCommunicationPort = 9872;
return;
}
AirScout_asCommunicationPort = airScout_asCommunicationPort; AirScout_asCommunicationPort = airScout_asCommunicationPort;
} }
@@ -957,10 +1069,13 @@ public class ChatPreferences {
return AirScout_asUDPListenerEnabled; return AirScout_asUDPListenerEnabled;
} }
public void setAirScout_asUDPListenerEnabled(boolean airScout_asUDPListenerEnabled) { public void setAirScout_asUDPListenerEnabled(
boolean airScout_asUDPListenerEnabled
) {
AirScout_asUDPListenerEnabled = airScout_asUDPListenerEnabled; AirScout_asUDPListenerEnabled = airScout_asUDPListenerEnabled;
} }
public String getChatState() { public String getChatState() {
return chatState; return chatState;
} }
@@ -1400,6 +1515,14 @@ public class ChatPreferences {
Element stn_bandActive50 = doc.createElement("stn_bandActive50");
stn_bandActive50.setTextContent(this.stn_bandActive50+"");
station.appendChild(stn_bandActive50);
Element stn_bandActive70 = doc.createElement("stn_bandActive70");
stn_bandActive70.setTextContent(this.stn_bandActive70+"");
station.appendChild(stn_bandActive70);
Element stn_bandActive144 = doc.createElement("stn_bandActive144"); Element stn_bandActive144 = doc.createElement("stn_bandActive144");
stn_bandActive144.setTextContent(this.stn_bandActive144+""); stn_bandActive144.setTextContent(this.stn_bandActive144+"");
station.appendChild(stn_bandActive144); station.appendChild(stn_bandActive144);
@@ -1667,6 +1790,18 @@ public class ChatPreferences {
snifferWords.appendChild(temp); snifferWords.appendChild(temp);
} }
Element snifferCallSigns =
doc.createElement("snifferCallSigns");
rootElement.appendChild(snifferCallSigns);
for (String callSign
: lstNotify_QSOSniffer_sniffedCallSignList) {
Element temp = doc.createElement("callSign");
temp.setTextContent(callSign);
snifferCallSigns.appendChild(temp);
}
Element snifferPrefixes = doc.createElement("snifferPrefixes"); Element snifferPrefixes = doc.createElement("snifferPrefixes");
rootElement.appendChild(snifferPrefixes); rootElement.appendChild(snifferPrefixes);
@@ -1837,6 +1972,14 @@ public class ChatPreferences {
guiOptions_defaultFilterPublicMsgs.setTextContent(this.isGuiOptions_defaultFilterPublicMsgs()+""); guiOptions_defaultFilterPublicMsgs.setTextContent(this.isGuiOptions_defaultFilterPublicMsgs()+"");
guiSaveableOptions.appendChild(guiOptions_defaultFilterPublicMsgs); guiSaveableOptions.appendChild(guiOptions_defaultFilterPublicMsgs);
Element guiOptions_showGrossFieldWorkedHintInBandColumns = doc.createElement("guiOptions_showGrossFieldWorkedHintInBandColumns");
guiOptions_showGrossFieldWorkedHintInBandColumns.setTextContent(this.isGuiOptions_showGrossFieldWorkedHintInBandColumns()+"");
guiSaveableOptions.appendChild(guiOptions_showGrossFieldWorkedHintInBandColumns);
Element guiOptions_showFreshCallHintInBandColumns = doc.createElement("guiOptions_showFreshCallHintInBandColumns");
guiOptions_showFreshCallHintInBandColumns.setTextContent(this.isGuiOptions_showFreshCallHintInBandColumns()+"");
guiSaveableOptions.appendChild(guiOptions_showFreshCallHintInBandColumns);
Element guiOptions_darkModeActive = doc.createElement("guiOptions_darkModeActive"); Element guiOptions_darkModeActive = doc.createElement("guiOptions_darkModeActive");
guiOptions_darkModeActive.setTextContent(this.GUI_darkModeActive + ""); guiOptions_darkModeActive.setTextContent(this.GUI_darkModeActive + "");
guiSaveableOptions.appendChild(guiOptions_darkModeActive); guiSaveableOptions.appendChild(guiOptions_darkModeActive);
@@ -1914,6 +2057,12 @@ public class ChatPreferences {
); );
guiOptions.appendChild(GUIstationMapStagePositionXY); guiOptions.appendChild(GUIstationMapStagePositionXY);
Element GUIstationMapPathAnalysisVisible = doc.createElement("GUIstationMapPathAnalysisVisible");
GUIstationMapPathAnalysisVisible.setTextContent(
String.valueOf(this.isGUIstationMapPathAnalysisVisible())
);
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
/**************************************************************************************** /****************************************************************************************
****************************** now write this XML! ************************************* ****************************** now write this XML! *************************************
****************************************************************************************/ ****************************************************************************************/
@@ -2115,6 +2264,8 @@ public class ChatPreferences {
); );
// Band activity flags (introduced later; if missing -> keep defaults) // Band activity flags (introduced later; if missing -> keep defaults)
stn_bandActive50 = getBoolean(stationEl, stn_bandActive50, "stn_bandActive50");
stn_bandActive70 = getBoolean(stationEl, stn_bandActive70, "stn_bandActive70");
stn_bandActive144 = getBoolean(stationEl, stn_bandActive144, "stn_bandActive144"); stn_bandActive144 = getBoolean(stationEl, stn_bandActive144, "stn_bandActive144");
stn_bandActive432 = getBoolean(stationEl, stn_bandActive432, "stn_bandActive432"); stn_bandActive432 = getBoolean(stationEl, stn_bandActive432, "stn_bandActive432");
stn_bandActive1240 = getBoolean(stationEl, stn_bandActive1240, "stn_bandActive1240"); stn_bandActive1240 = getBoolean(stationEl, stn_bandActive1240, "stn_bandActive1240");
@@ -2265,7 +2416,13 @@ public class ChatPreferences {
notify_dxClusterServerEnabled = getBoolean(notificationsEl, notify_dxClusterServerEnabled, "notify_dxClusterServerEnabled"); notify_dxClusterServerEnabled = getBoolean(notificationsEl, notify_dxClusterServerEnabled, "notify_dxClusterServerEnabled");
notify_DXClusterServerTriggerBearing = getBoolean(notificationsEl, notify_DXClusterServerTriggerBearing, "notify_DXClusterServerTriggerBearing"); notify_DXClusterServerTriggerBearing = getBoolean(notificationsEl, notify_DXClusterServerTriggerBearing, "notify_DXClusterServerTriggerBearing");
notify_DXClusterServerTriggerOnQRGDetect = getBoolean(notificationsEl, notify_DXClusterServerTriggerOnQRGDetect, "notify_DXClusterServerTriggerOnQRGDetect"); notify_DXClusterServerTriggerOnQRGDetect = getBoolean(notificationsEl, notify_DXClusterServerTriggerOnQRGDetect, "notify_DXClusterServerTriggerOnQRGDetect");
notify_dxclusterServerPort = getInt(notificationsEl, notify_dxclusterServerPort, "notify_dxclusterServerPort"); setNotify_dxclusterServerPort(
getInt(
notificationsEl,
notify_dxclusterServerPort,
"notify_dxclusterServerPort"
)
);
String spotter = getText(notificationsEl, null, "notify_DXCSrv_SpottersCallSign"); String spotter = getText(notificationsEl, null, "notify_DXCSrv_SpottersCallSign");
if (spotter != null) { if (spotter != null) {
@@ -2281,8 +2438,11 @@ public class ChatPreferences {
notify_noReplyPenaltyMinutes = noReply; notify_noReplyPenaltyMinutes = noReply;
} }
Integer momentum = getInt(notificationsEl, 666, "notify_momentumWindowSeconds"); Integer momentum = getInt(
if (momentum != null) { notificationsEl,
notify_momentumWindowSeconds,
"notify_momentumWindowSeconds"
); if (momentum != null) {
notify_momentumWindowSeconds = momentum; notify_momentumWindowSeconds = momentum;
} }
@@ -2316,15 +2476,58 @@ public class ChatPreferences {
Element airScoutEl = getFirstElement(doc, "AirScoutQuerier"); Element airScoutEl = getFirstElement(doc, "AirScoutQuerier");
if (airScoutEl != null) { if (airScoutEl != null) {
AirScout_asUDPListenerEnabled = getBoolean(airScoutEl, AirScout_asUDPListenerEnabled, "asQry_airScoutCommunicationEnabled"); setAirScout_asUDPListenerEnabled(
AirScout_asServerNameString = getText(airScoutEl, AirScout_asServerNameString, "asQry_airScoutServerName"); getBoolean(
AirScout_asClientNameString = getText(airScoutEl, AirScout_asClientNameString, "asQry_airScoutClientName"); airScoutEl,
AirScout_asCommunicationPort = getInt(airScoutEl, AirScout_asCommunicationPort, "asQry_airScoutUDPPort"); AirScout_asUDPListenerEnabled,
AirScout_asBandString = getText(airScoutEl, AirScout_asBandString, "asQry_airScoutBandValue"); "asQry_airScoutCommunicationEnabled"
)
);
setAirScout_asServerNameString(
getText(
airScoutEl,
AirScout_asServerNameString,
"asQry_airScoutServerName"
)
);
setAirScout_asClientNameString(
getText(
airScoutEl,
AirScout_asClientNameString,
"asQry_airScoutClientName"
)
);
setAirScout_asCommunicationPort(
getInt(
airScoutEl,
AirScout_asCommunicationPort,
"asQry_airScoutUDPPort"
)
);
setAirScout_asBandString(
getText(
airScoutEl,
AirScout_asBandString,
"asQry_airScoutBandValue"
)
);
System.out.println( System.out.println(
"[ChatPreferences, info]: AirScout querier enabled=" + AirScout_asUDPListenerEnabled "[ChatPreferences, info]: AirScout integration enabled="
+ ", band=" + AirScout_asBandString); + AirScout_asUDPListenerEnabled
+ ", server identifier="
+ AirScout_asServerNameString
+ ", client identifier="
+ AirScout_asClientNameString
+ ", port="
+ AirScout_asCommunicationPort
+ ", band="
+ AirScout_asBandString
);
} }
/** /**
@@ -2374,6 +2577,45 @@ public class ChatPreferences {
/** /**
* Case QSO-sniffer lists (added later; older configs won't have them) * Case QSO-sniffer lists (added later; older configs won't have them)
*/ */
list = doc.getElementsByTagName("snifferCallSigns");
if (list != null && list.getLength() != 0) {
lstNotify_QSOSniffer_sniffedCallSignList.clear();
for (int temp = 0; temp < list.getLength(); temp++) {
Node node = list.item(temp);
if (node.getNodeType() == Node.ELEMENT_NODE) {
NodeList children = node.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType()
== Node.ELEMENT_NODE) {
String callSign =
child.getTextContent();
if (callSign != null
&& !callSign.isBlank()) {
String normalizedCallSign =
callSign
.trim()
.toUpperCase();
if (!lstNotify_QSOSniffer_sniffedCallSignList
.contains(normalizedCallSign)) {
lstNotify_QSOSniffer_sniffedCallSignList
.add(normalizedCallSign);
}
}
}
}
}
}
}
list = doc.getElementsByTagName("snifferWords"); list = doc.getElementsByTagName("snifferWords");
if (list != null && list.getLength() != 0) { if (list != null && list.getLength() != 0) {
// reset to avoid duplicates when reloading // reset to avoid duplicates when reloading
@@ -2506,6 +2748,14 @@ public class ChatPreferences {
messageHandling_autoAnswerEnabledSecondCat, messageHandling_autoAnswerEnabledSecondCat,
"messageHandling_autoAnswerEnabledSecondCat", "messageHandling_autoAnswerEnabledSecondCat",
"autoAnswerEnabledSecondCat"); "autoAnswerEnabledSecondCat");
/*
* The user interface intentionally exposes one shared generic auto-answer
* setting for both chat categories. Keep the legacy second-category XML
* fields synchronized so existing configuration files remain compatible.
*/
messageHandling_autoAnswerTextSecondCat = messageHandling_autoAnswerTextMainCat;
messageHandling_autoAnswerEnabledSecondCat = messageHandling_autoAnswerEnabled;
} }
@@ -2548,6 +2798,17 @@ public class ChatPreferences {
this.getGUIstationMapStagePositionXY() 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 // Splitpane divider positions
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition"); String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
if (s1 != null) { if (s1 != null) {
@@ -2621,6 +2882,8 @@ public class ChatPreferences {
this.setGuiOptions_defaultFilterPmToMe(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPmToMe(), "guiOptions_defaultFilterPmToMe")); this.setGuiOptions_defaultFilterPmToMe(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPmToMe(), "guiOptions_defaultFilterPmToMe"));
this.setGuiOptions_defaultFilterPmToOther(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPmToOther(), "guiOptions_defaultFilterPmToOther")); this.setGuiOptions_defaultFilterPmToOther(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPmToOther(), "guiOptions_defaultFilterPmToOther"));
this.setGuiOptions_defaultFilterPublicMsgs(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPublicMsgs(), "guiOptions_defaultFilterPublicMsgs")); this.setGuiOptions_defaultFilterPublicMsgs(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPublicMsgs(), "guiOptions_defaultFilterPublicMsgs"));
this.setGuiOptions_showGrossFieldWorkedHintInBandColumns(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_showGrossFieldWorkedHintInBandColumns(), "guiOptions_showGrossFieldWorkedHintInBandColumns"));
this.setGuiOptions_showFreshCallHintInBandColumns(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_showFreshCallHintInBandColumns(), "guiOptions_showFreshCallHintInBandColumns"));
// Added in later versions: dark mode flags // Added in later versions: dark mode flags
this.GUI_darkModeActive = getBoolean(guiSaveableOptionsEl, this.GUI_darkModeActive, "guiOptions_darkModeActive"); this.GUI_darkModeActive = getBoolean(guiSaveableOptionsEl, this.GUI_darkModeActive, "guiOptions_darkModeActive");
@@ -2674,6 +2937,22 @@ public class ChatPreferences {
return result; return result;
} }
public boolean isStn_bandActive50() {
return stn_bandActive50;
}
public void setStn_bandActive50(boolean stn_bandActive50) {
this.stn_bandActive50 = stn_bandActive50;
}
public boolean isStn_bandActive70() {
return stn_bandActive70;
}
public void setStn_bandActive70(boolean stn_bandActive70) {
this.stn_bandActive70 = stn_bandActive70;
}
public boolean isStn_bandActive144() { public boolean isStn_bandActive144() {
return stn_bandActive144; return stn_bandActive144;
} }
@@ -3,25 +3,58 @@ package kst4contest.model;
/** /**
* Represents a scheduled event or an AirScout opportunity in the future. * Represents a scheduled event or an AirScout opportunity in the future.
* Used for the Timeline View and Priority Calculation. * 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 { public class ContestSked {
private String targetCallsign; private String targetCallsign;
private double targetAzimuth; // Required for Antenna-Visuals private String targetChatCallsign;
private long skedTimeEpoch; // The peak time (e.g., AP) private ChatCategory targetChatCategory;
private double targetAzimuth;
private long skedTimeEpoch;
private Band band; private Band band;
// Opportunity potential (0..100). -1 means "unknown". // Opportunity potential (0..100). -1 means "unknown".
int opportunityPotentialPercent = -1; int opportunityPotentialPercent = -1;
// Status flags to prevent spamming alarms // Status flags to prevent spamming alarms.
private boolean warning3MinSent = false; private boolean warning3MinSent = false;
private boolean warningNowSent = 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.targetAzimuth = azimuth;
this.skedTimeEpoch = time; this.skedTimeEpoch = time;
this.band = b; this.band = band;
} }
/** /**
@@ -32,15 +65,54 @@ public class ContestSked {
return (skedTimeEpoch - System.currentTimeMillis()) / 1000; return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
} }
// Getters and Setters... /**
public String getTargetCallsign() { return targetCallsign; } * Returns the base callsign used for scoring and worked-state grouping.
public double getTargetAzimuth() { return targetAzimuth; } */
public long getSkedTimeEpoch() { return skedTimeEpoch; } public String getTargetCallsign() {
public Band getBand() { return band; } return targetCallsign;
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 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() { public int getOpportunityPotentialPercent() {
return opportunityPotentialPercent; 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,112 @@
package kst4contest.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import kst4contest.controller.MessageVariableResolver;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
class MessageVariableResolverTest {
private ChatPreferences chatPreferences;
private MessageVariableResolver resolver;
@BeforeEach
void setUp() {
chatPreferences = mock(ChatPreferences.class);
when(chatPreferences.getMYQRGFirstCat()).thenReturn(new SimpleStringProperty("144.388.03"));
when(chatPreferences.getMYQRGSecondCat()).thenReturn(new SimpleStringProperty("432.088.00"));
when(chatPreferences.getStn_loginLocatorMainCat()).thenReturn("JO51IJ");
when(chatPreferences.getStn_loginCallSign()).thenReturn("DO5AMF");
when(chatPreferences.getActualQTF()).thenReturn(new SimpleDoubleProperty(135.0));
resolver = new MessageVariableResolver(chatPreferences);
}
@Test
void resolvesAllGlobalVariablesInEveryMessageContext() {
String template = "MYCALL MYQRGSHORT MYQRG SECONDQRG MYLOCATORSHORT MYLOCATOR MYQTF";
assertEquals(
"DO5AMF 144.388 144.388.03 432.088.00 JO51 JO51IJ 135",
resolver.resolveGlobalVariables(template)
);
}
@Test
void shortValuesRemainSafeWhenTheConfiguredValueIsShorterThanExpected() {
when(chatPreferences.getMYQRGFirstCat()).thenReturn(new SimpleStringProperty("144"));
when(chatPreferences.getStn_loginLocatorMainCat()).thenReturn("JO5");
assertEquals("144 JO5", resolver.resolveGlobalVariables("MYQRGSHORT MYLOCATORSHORT"));
}
@Test
void resolvesSelectedStationNameAndTheFirstTwoAirPlanes() {
ChatMember selectedStation = new ChatMember();
selectedStation.setCallSign("DL0TEST");
selectedStation.setName("Test Operator");
AirPlane firstAirPlane = new AirPlane();
firstAirPlane.setPotential(100);
firstAirPlane.setArrivingDurationMinutes(1);
AirPlane secondAirPlane = new AirPlane();
secondAirPlane.setPotential(75);
secondAirPlane.setArrivingDurationMinutes(9);
AirPlaneReflectionInfo reflectionInfo = new AirPlaneReflectionInfo();
reflectionInfo.setRisingAirplanes(FXCollections.observableArrayList(firstAirPlane, secondAirPlane));
selectedStation.setAirPlaneReflectInfo(reflectionInfo);
assertEquals(
"Hi Test Operator, a very big AP in 1 min; Next big AP in 9 min",
resolver.resolveForSelectedStation(
"Hi QRZNAME, FIRSTAP; SECONDAP",
selectedStation
)
);
}
@Test
void usesCallsignWhenTheSelectedStationHasNoName() {
ChatMember selectedStation = new ChatMember();
selectedStation.setCallSign("DL0TEST");
selectedStation.setName(" ");
assertEquals(
"Hi DL0TEST",
resolver.resolveForSelectedStation("Hi QRZNAME", selectedStation)
);
}
@Test
void keepsStationVariablesVisibleWhenNoStationIsSelected() {
assertEquals(
"QRZNAME FIRSTAP SECONDAP",
resolver.resolveForSelectedStation("QRZNAME FIRSTAP SECONDAP", null)
);
}
@Test
void returnsUsefulFallbacksWhenNoAirPlaneIsAvailable() {
ChatMember selectedStation = new ChatMember();
selectedStation.setCallSign("DL0TEST");
assertEquals(
"no ap available ",
resolver.resolveForSelectedStation("FIRSTAP SECONDAP", selectedStation)
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,327 @@
package kst4contest.view;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import java.net.URI;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Displays a single-line message text in a TableView.
*
* <p>The cell provides two functions that JavaFX's default text cell cannot
* combine:
* <ul>
* <li>a tooltip containing the complete text, but only while the visible
* cell is too narrow for that text;</li>
* <li>clickable HTTP, HTTPS and www links inside otherwise normal text.</li>
* </ul>
*
* <p>The row type is generic because the same implementation is used by the
* ChatMessage tables and the DX-cluster table.</p>
*
* @param <S> row type of the surrounding TableView
*/
public final class MessageTextTableCell<S> extends TableCell<S, String> {
private static final Pattern WEB_LINK_PATTERN = Pattern.compile(
"(?i)\\b(?:https?://|www\\.)[^\\s<>\"']+"
);
private static final String TRAILING_LINK_PUNCTUATION = ".,;:!?)]}";
private final Consumer<String> linkOpener;
private final Predicate<String> highlightedTextPredicate;
private final String normalStyleClass;
private final String highlightedStyleClass;
private final HBox contentBox = new HBox(0);
private final Rectangle contentClip = new Rectangle();
private final Tooltip fullTextTooltip = new Tooltip();
private String displayedText = "";
private boolean fullTextTooltipInstalled = false;
/**
* Creates a normal message cell without additional text highlighting.
*
* @param linkOpener callback which opens a validated HTTP or HTTPS URL
*/
public MessageTextTableCell(Consumer<String> linkOpener) {
this(linkOpener, text -> false, null, null);
}
/**
* Creates a message cell with optional CSS highlighting.
*
* <p>KST4Contest uses this variant for public messages which contain the
* operator's own callsign. Only the supplied CSS classes are added or
* removed. The standard {@code table-cell} class and all other JavaFX
* state remain untouched.</p>
*
* @param linkOpener callback which opens a validated URL
* @param highlightedTextPredicate identifies highlighted messages
* @param normalStyleClass CSS class for normal messages, may be null
* @param highlightedStyleClass CSS class for highlighted messages, may be null
*/
public MessageTextTableCell(
Consumer<String> linkOpener,
Predicate<String> highlightedTextPredicate,
String normalStyleClass,
String highlightedStyleClass
) {
this.linkOpener = Objects.requireNonNull(linkOpener, "linkOpener");
this.highlightedTextPredicate = highlightedTextPredicate == null
? text -> false
: highlightedTextPredicate;
this.normalStyleClass = normalStyleClass;
this.highlightedStyleClass = highlightedStyleClass;
contentBox.setAlignment(Pos.CENTER_LEFT);
contentBox.setFillHeight(false);
contentBox.setClip(contentClip);
fullTextTooltip.setWrapText(true);
fullTextTooltip.setMaxWidth(800);
fullTextTooltip.setShowDelay(Duration.millis(250));
fullTextTooltip.setShowDuration(Duration.seconds(30));
setContentDisplay(javafx.scene.control.ContentDisplay.GRAPHIC_ONLY);
}
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
removeOptionalStyleClasses();
contentBox.getChildren().clear();
if (empty || item == null || item.isEmpty()) {
displayedText = "";
fullTextTooltip.setText("");
setText(null);
setGraphic(null);
setClippedTooltipActive(false);
return;
}
displayedText = item;
fullTextTooltip.setText(item);
applyOptionalStyleClass(item);
buildContent(item);
setText(null);
setGraphic(contentBox);
updateClipAndTooltip();
}
@Override
protected void layoutChildren() {
super.layoutChildren();
updateClipAndTooltip();
}
/**
* Splits the displayed text into ordinary labels and clickable links.
*/
private void buildContent(String text) {
Matcher matcher = WEB_LINK_PATTERN.matcher(text);
int nextPlainTextStart = 0;
while (matcher.find()) {
String rawMatch = matcher.group();
String linkText = removeTrailingPunctuation(rawMatch);
if (linkText.isEmpty()) {
continue;
}
appendPlainText(text.substring(nextPlainTextStart, matcher.start()));
appendLink(linkText);
/*
* Punctuation removed from the URL remains ordinary message text.
* Starting here ensures it is included by the next substring.
*/
nextPlainTextStart = matcher.start() + linkText.length();
}
appendPlainText(text.substring(nextPlainTextStart));
}
private void appendPlainText(String text) {
if (text == null || text.isEmpty()) {
return;
}
Label textFragment = new Label(text);
textFragment.setPadding(Insets.EMPTY);
textFragment.setMinWidth(Region.USE_PREF_SIZE);
textFragment.setMaxWidth(Region.USE_PREF_SIZE);
textFragment.setMouseTransparent(true);
textFragment.textFillProperty().bind(textFillProperty());
textFragment.getStyleClass().add("message-text-fragment");
contentBox.getChildren().add(textFragment);
}
private void appendLink(String linkText) {
Hyperlink hyperlink = new Hyperlink(linkText);
hyperlink.setPadding(Insets.EMPTY);
hyperlink.setMinWidth(Region.USE_PREF_SIZE);
hyperlink.setMaxWidth(Region.USE_PREF_SIZE);
hyperlink.setFocusTraversable(false);
hyperlink.getStyleClass().add("message-table-link");
hyperlink.setOnAction(event -> {
openLink(linkText);
event.consume();
});
contentBox.getChildren().add(hyperlink);
}
/**
* Opens only HTTP and HTTPS targets. A visible www address receives an
* HTTPS scheme before it is passed to the application.
*/
private void openLink(String linkText) {
try {
String normalizedLink = linkText.toLowerCase(Locale.ROOT).startsWith("www.")
? "https://" + linkText
: linkText;
URI uri = URI.create(normalizedLink);
String scheme = uri.getScheme();
if (scheme == null
|| (!scheme.equalsIgnoreCase("http")
&& !scheme.equalsIgnoreCase("https"))) {
return;
}
linkOpener.accept(uri.toASCIIString());
} catch (RuntimeException exception) {
System.out.println(
"[MessageTextTableCell] Cannot open malformed link: "
+ linkText
+ " / "
+ exception.getMessage()
);
}
}
/**
* Removes punctuation which commonly follows a link in normal prose.
*/
private String removeTrailingPunctuation(String rawLink) {
String result = rawLink;
while (!result.isEmpty()
&& TRAILING_LINK_PUNCTUATION.indexOf(
result.charAt(result.length() - 1)
) >= 0) {
result = result.substring(0, result.length() - 1);
}
return result;
}
/**
* Clips the one-line content and installs the full-text tooltip only if
* the rendered nodes are wider than the usable cell area.
*/
private void updateClipAndTooltip() {
if (displayedText.isEmpty() || getGraphic() == null) {
setClippedTooltipActive(false);
return;
}
double availableWidth = Math.max(
0,
getWidth() - snappedLeftInset() - snappedRightInset()
);
double availableHeight = Math.max(
0,
getHeight() - snappedTopInset() - snappedBottomInset()
);
contentClip.setWidth(availableWidth);
contentClip.setHeight(availableHeight);
boolean textIsClipped = contentBox.prefWidth(-1) > availableWidth + 1;
setClippedTooltipActive(textIsClipped);
}
/**
* Installs the tooltip on the actual graphic node below the mouse pointer.
*
* <p>Installing it on the TableCell itself is not reliable when the cell
* displays an HBox containing labels and hyperlinks. The graphic node is
* the effective mouse target. Installation is tracked explicitly so
* repeated layout passes neither add duplicate handlers nor leave a
* tooltip attached to a reused empty cell.</p>
*/
private void setClippedTooltipActive(boolean active) {
/*
* The tooltip is handled exclusively by the graphic node. Keeping a
* second tooltip on the TableCell would allow two competing tooltip
* targets for the same visible content.
*/
setTooltip(null);
if (active == fullTextTooltipInstalled) {
return;
}
if (active) {
Tooltip.install(contentBox, fullTextTooltip);
} else {
fullTextTooltip.hide();
Tooltip.uninstall(contentBox, fullTextTooltip);
}
fullTextTooltipInstalled = active;
}
private void applyOptionalStyleClass(String item) {
boolean highlighted;
try {
highlighted = highlightedTextPredicate.test(item);
} catch (RuntimeException exception) {
highlighted = false;
}
String styleClass = highlighted
? highlightedStyleClass
: normalStyleClass;
if (styleClass != null
&& !styleClass.isBlank()
&& !getStyleClass().contains(styleClass)) {
getStyleClass().add(styleClass);
}
}
private void removeOptionalStyleClasses() {
if (normalStyleClass != null) {
getStyleClass().remove(normalStyleClass);
}
if (highlightedStyleClass != null) {
getStyleClass().remove(highlightedStyleClass);
}
}
}
@@ -208,7 +208,12 @@ public class TimelineView extends Pane {
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent())); 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) { if (skedTooltipExtraTextProvider != null) {
String extra = skedTooltipExtraTextProvider.apply(sked); String extra = skedTooltipExtraTextProvider.apply(sked);
@@ -220,7 +225,9 @@ public class TimelineView extends Pane {
Tooltip t = new Tooltip(baseToolTipFallBack); Tooltip t = new Tooltip(baseToolTipFallBack);
Tooltip.install(diamond, t); Tooltip.install(diamond, t);
Label lbl = new Label("SKED: " + sked.getTargetCallsign()); Label lbl = new Label(
"SKED: " + sked.getTargetChatCallsign()
);
// lbl.setFont(new Font(9)); // lbl.setFont(new Font(9));
// lbl.setTextFill(Color.WHITE); // lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(14); lbl.setLayoutY(14);
@@ -1,6 +1,7 @@
package kst4contest.view.map; package kst4contest.view.map;
import kst4contest.locatorUtils.Location; import kst4contest.locatorUtils.Location;
import kst4contest.logic.BandOpportunityResolver;
import kst4contest.model.AirPlaneReflectionInfo; import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.Band; import kst4contest.model.Band;
import kst4contest.model.ChatMember; import kst4contest.model.ChatMember;
@@ -14,7 +15,6 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.regex.Pattern;
/** /**
* Builds immutable map snapshots from the currently visible chat members. * Builds immutable map snapshots from the currently visible chat members.
@@ -24,8 +24,6 @@ import java.util.regex.Pattern;
*/ */
public final class MapCallsignRawSnapshotBuilder { public final class MapCallsignRawSnapshotBuilder {
private static final Pattern TOKEN_SPLIT_PATTERN = Pattern.compile("[^A-Z0-9]+");
public List<MapCallsignRawSnapshot> buildSnapshots(Collection<ChatMember> visibleChatMembers, public List<MapCallsignRawSnapshot> buildSnapshots(Collection<ChatMember> visibleChatMembers,
ChatMember selectedChatMember, ChatMember selectedChatMember,
EnumSet<Band> selectedBands) { EnumSet<Band> selectedBands) {
@@ -73,10 +71,21 @@ public final class MapCallsignRawSnapshotBuilder {
Location location = new Location(locator6); Location location = new Location(locator6);
LinkedHashMap<String, String> frequenciesByBand = collectLastKnownFrequenciesByBand(variants); long nowEpochMs = System.currentTimeMillis();
EnumSet<Band> sureBands = collectSureBands(variants, frequenciesByBand); BandOpportunityResolver.Resolution bandResolution =
String bandSummary = buildBandSummary(sureBands); BandOpportunityResolver.resolve(variants, nowEpochMs);
boolean offersSelectedBand = hasAnySelectedBand(sureBands, selectedBands);
EnumSet<Band> availableBands = bandResolution.getAvailableBands();
LinkedHashMap<String, String> frequenciesByBand = collectLastKnownFrequenciesByBand(
variants,
availableBands,
nowEpochMs
);
String bandSummary = buildBandSummary(availableBands);
boolean offersSelectedBand = !bandResolution
.getUnworkedEnabledBands(selectedBands)
.isEmpty();
boolean warningToMyDirection = variants.stream().anyMatch(ChatMember::isInAngleAndRange); boolean warningToMyDirection = variants.stream().anyMatch(ChatMember::isInAngleAndRange);
boolean worked = variants.stream().anyMatch(this::isWorkedAtAnyBand); boolean worked = variants.stream().anyMatch(this::isWorkedAtAnyBand);
@@ -152,7 +161,11 @@ public final class MapCallsignRawSnapshotBuilder {
.orElse(""); .orElse("");
} }
private LinkedHashMap<String, String> collectLastKnownFrequenciesByBand(List<ChatMember> variants) { private LinkedHashMap<String, String> collectLastKnownFrequenciesByBand(
List<ChatMember> variants,
EnumSet<Band> availableBands,
long nowEpochMs
) {
Map<Band, FrequencyCandidate> latestByBand = new EnumMap<>(Band.class); Map<Band, FrequencyCandidate> latestByBand = new EnumMap<>(Band.class);
@@ -161,16 +174,29 @@ public final class MapCallsignRawSnapshotBuilder {
continue; continue;
} }
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> bandEntry : variant.getKnownActiveBands().entrySet()) { for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> bandEntry
: variant.getKnownActiveBands().entrySet()) {
Band band = bandEntry.getKey(); Band band = bandEntry.getKey();
ChatMember.ActiveFrequencyInfo activeFrequencyInfo = bandEntry.getValue(); ChatMember.ActiveFrequencyInfo activeFrequencyInfo = bandEntry.getValue();
if (band == null || activeFrequencyInfo == null) { if (band == null
|| activeFrequencyInfo == null
|| availableBands == null
|| !availableBands.contains(band)) {
continue;
}
long ageMs = nowEpochMs - activeFrequencyInfo.timestampEpoch;
if (ageMs < 0L
|| ageMs > BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS) {
continue; continue;
} }
FrequencyCandidate previous = latestByBand.get(band); FrequencyCandidate previous = latestByBand.get(band);
if (previous == null || activeFrequencyInfo.timestampEpoch > previous.timestampEpochMs()) { if (previous == null
|| activeFrequencyInfo.timestampEpoch > previous.timestampEpochMs()) {
latestByBand.put(band, new FrequencyCandidate( latestByBand.put(band, new FrequencyCandidate(
band, band,
formatFrequency(activeFrequencyInfo.frequency), formatFrequency(activeFrequencyInfo.frequency),
@@ -178,152 +204,31 @@ public final class MapCallsignRawSnapshotBuilder {
)); ));
} }
} }
addFallbackCurrentFrequencyIfUseful(variant, latestByBand);
} }
LinkedHashMap<String, String> ordered = new LinkedHashMap<>(); LinkedHashMap<String, String> ordered = new LinkedHashMap<>();
latestByBand.entrySet().stream() latestByBand.entrySet().stream()
.sorted(Map.Entry.comparingByKey()) .sorted(Map.Entry.comparingByKey())
.forEach(entry -> ordered.put(toBandDisplayLabel(entry.getKey()), entry.getValue().formattedFrequency())); .forEach(entry -> ordered.put(
toBandDisplayLabel(entry.getKey()),
entry.getValue().formattedFrequency()
));
return ordered; return ordered;
} }
private EnumSet<Band> collectSureBands(List<ChatMember> variants, private String buildBandSummary(EnumSet<Band> availableBands) {
LinkedHashMap<String, String> frequenciesByBand) { if (availableBands == null || availableBands.isEmpty()) {
EnumSet<Band> sureBands = EnumSet.noneOf(Band.class);
if (frequenciesByBand != null) {
for (String label : frequenciesByBand.keySet()) {
Band mappedBand = bandFromDisplayLabel(label);
if (mappedBand != null) {
sureBands.add(mappedBand);
}
}
}
for (ChatMember variant : variants) {
if (variant == null || variant.getName() == null || variant.getName().isBlank()) {
continue;
}
sureBands.addAll(detectBandsFromStationName(variant.getName()));
}
return sureBands;
}
private EnumSet<Band> detectBandsFromStationName(String stationName) {
EnumSet<Band> detectedBands = EnumSet.noneOf(Band.class);
if (stationName == null || stationName.isBlank()) {
return detectedBands;
}
String normalized = stationName.toUpperCase(Locale.ROOT);
String[] tokens = TOKEN_SPLIT_PATTERN.split(normalized);
for (String token : tokens) {
if (token == null || token.isBlank()) {
continue;
}
switch (token) {
case "2", "2M", "144", "144MHZ" -> detectedBands.add(Band.B_144);
case "70", "70CM", "432", "432MHZ" -> detectedBands.add(Band.B_432);
case "23", "23CM", "1296", "1296MHZ" -> detectedBands.add(Band.B_1296);
case "13", "13CM", "2300", "2320", "2320MHZ" -> detectedBands.add(Band.B_2320);
case "9", "9CM", "3400", "3400MHZ" -> detectedBands.add(Band.B_3400);
case "6", "6CM", "5600", "5760", "5760MHZ" -> detectedBands.add(Band.B_5760);
case "3", "3CM", "10G", "10GHZ", "10368", "10368MHZ" -> detectedBands.add(Band.B_10G);
case "24G", "24GHZ", "24048", "24048MHZ" -> detectedBands.add(Band.B_24G);
default -> {
}
}
}
return detectedBands;
}
private String buildBandSummary(EnumSet<Band> sureBands) {
if (sureBands == null || sureBands.isEmpty()) {
return ""; return "";
} }
List<String> labels = sureBands.stream() List<String> labels = availableBands.stream()
.sorted() .sorted()
.map(this::toBandDisplayLabel) .map(this::toBandDisplayLabel)
.toList(); .toList();
return String.join(", ", labels); return String.join(", ", labels);
} }
private boolean hasAnySelectedBand(EnumSet<Band> sureBands, EnumSet<Band> selectedBands) {
if (sureBands == null || sureBands.isEmpty() || selectedBands == null || selectedBands.isEmpty()) {
return false;
}
for (Band band : selectedBands) {
if (sureBands.contains(band)) {
return true;
}
}
return false;
}
private Band bandFromDisplayLabel(String label) {
if (label == null || label.isBlank()) {
return null;
}
return switch (label) {
case "144" -> Band.B_144;
case "432" -> Band.B_432;
case "1296" -> Band.B_1296;
case "2320" -> Band.B_2320;
case "3400" -> Band.B_3400;
case "5760" -> Band.B_5760;
case "10368" -> Band.B_10G;
case "24048" -> Band.B_24G;
default -> null;
};
}
/**
* Fallback for stations where the current displayed QRG exists but the
* knownActiveBands history has not yet been filled.
*
* This parsing is intentionally tolerant so strings like "144.300 MHz"
* can still be used.
*/
private void addFallbackCurrentFrequencyIfUseful(ChatMember variant, Map<Band, FrequencyCandidate> latestByBand) {
if (variant == null || variant.getFrequency() == null || variant.getFrequency().getValue() == null) {
return;
}
String rawFrequency = variant.getFrequency().getValue().trim();
if (rawFrequency.isBlank()) {
return;
}
double parsedFrequencyMHz = PathGeometryUtils.tryParseFrequencyMHz(rawFrequency);
if (!Double.isFinite(parsedFrequencyMHz) || parsedFrequencyMHz <= 0.0) {
return;
}
Band detectedBand = Band.fromFrequency(parsedFrequencyMHz);
if (detectedBand == null) {
return;
}
FrequencyCandidate previous = latestByBand.get(detectedBand);
if (previous != null && previous.timestampEpochMs() >= variant.getActivityTimeLastInEpoch()) {
return;
}
latestByBand.put(detectedBand, new FrequencyCandidate(
detectedBand,
formatFrequency(parsedFrequencyMHz),
variant.getActivityTimeLastInEpoch()
));
}
private boolean isWorkedAtAnyBand(ChatMember member) { private boolean isWorkedAtAnyBand(ChatMember member) {
return member.isWorked() return member.isWorked()
|| member.isWorked50() || member.isWorked50()
@@ -370,6 +275,8 @@ public final class MapCallsignRawSnapshotBuilder {
private String toBandDisplayLabel(Band band) { private String toBandDisplayLabel(Band band) {
return switch (band) { return switch (band) {
case B_50 -> "50";
case B_70 -> "70";
case B_144 -> "144"; case B_144 -> "144";
case B_432 -> "432"; case B_432 -> "432";
case B_1296 -> "1296"; case B_1296 -> "1296";
@@ -240,6 +240,36 @@ public record PathAnalysisResult(
); );
} }
/**
* Creates a placeholder when automatic band resolution has no usable result.
*/
public static PathAnalysisResult waitingForUsableBand(String fromLocator6,
String toLocator6,
String toCallsignRaw) {
return new PathAnalysisResult(
"Waiting",
fromLocator6,
toLocator6,
toCallsignRaw,
Double.NaN,
Double.NaN,
Double.NaN,
Double.NaN,
Double.NaN,
false,
false,
Double.NaN,
Double.NaN,
Double.NaN,
Double.NaN,
Double.NaN,
-1,
"No usable automatic band is available. "
+ "Check own enabled bands and the station's NOT-QRV tags.",
List.of()
);
}
/** /**
* Creates a finished result without a usable terrain profile. * Creates a finished result without a usable terrain profile.
* *
@@ -343,6 +343,8 @@ public final class StationMapBridge {
+ "|" + "|"
+ String.format(Locale.US, "%.3f", analysisFrequencyMHz) + String.format(Locale.US, "%.3f", analysisFrequencyMHz)
+ "|" + "|"
+ selectedSnapshot.bandSummary()
+ "|"
+ String.format(Locale.US, "%.1f", preferences.getStn_pathAnalysisOwnAntennaHeightMeters()) + String.format(Locale.US, "%.1f", preferences.getStn_pathAnalysisOwnAntennaHeightMeters())
+ "|" + "|"
+ String.format(Locale.US, "%.1f", preferences.getStn_pathAnalysisDefaultTargetAntennaHeightMeters()) + String.format(Locale.US, "%.1f", preferences.getStn_pathAnalysisDefaultTargetAntennaHeightMeters())
@@ -28,6 +28,8 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.ColumnConstraints;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
/** /**
* Standalone station map window. * Standalone station map window.
@@ -49,6 +51,9 @@ public final class StationMapView {
*/ */
private static final boolean MAP_DEBUG_LOGGING = false; 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 PathProfileChart detailPathProfileChart = new PathProfileChart();
private final Label detailPathModeValue = new Label("-"); private final Label detailPathModeValue = new Label("-");
@@ -76,6 +81,12 @@ public final class StationMapView {
private VBox detailPane; private VBox detailPane;
private final Label statusLabel = new Label("Station map not initialized yet."); private final Label statusLabel = new Label("Station map not initialized yet.");
private final Label pathAnalysisHiddenHintLabel = new Label("Path analysis is hidden.");
private final Button pathAnalysisVisibilityButton = new Button();
private final Tooltip pathAnalysisVisibilityTooltip = new Tooltip();
private final Label detailCallsignValue = new Label("-"); private final Label detailCallsignValue = new Label("-");
private final Label detailLocatorValue = new Label("-"); private final Label detailLocatorValue = new Label("-");
private final Label detailQrbValue = new Label("-"); private final Label detailQrbValue = new Label("-");
@@ -138,8 +149,12 @@ public final class StationMapView {
private PathAnalysisResult lastPathAnalysisResult = PathAnalysisResult.waitingForSelection(""); private PathAnalysisResult lastPathAnalysisResult = PathAnalysisResult.waitingForSelection("");
private VBox mapAndProfilePane; private VBox mapAndProfilePane;
private VBox profileSection;
private VBox pathAnalysisSection;
private ScrollPane detailScrollPane; private ScrollPane detailScrollPane;
private final Label detailPathLosValue = new Label("-"); private final Label detailPathLosValue = new Label("-");
private final Label detailPathWorstClearanceValue = new Label("-"); private final Label detailPathWorstClearanceValue = new Label("-");
@@ -287,6 +302,14 @@ public final class StationMapView {
} }
}); });
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.setFocusTraversable(true);
webView.setPickOnBounds(true); webView.setPickOnBounds(true);
@@ -323,7 +346,8 @@ public final class StationMapView {
webView.widthProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize()); webView.widthProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
webView.heightProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize()); webView.heightProperty().addListener((obs, oldValue, newValue) -> requestMapInvalidateSize());
VBox profileSection = createProfileSection(); profileSection = createProfileSection();
mapAndProfilePane = new VBox(6, webView, profileSection); mapAndProfilePane = new VBox(6, webView, profileSection);
mapAndProfilePane.setPadding(new Insets(0)); mapAndProfilePane.setPadding(new Insets(0));
@@ -349,10 +373,9 @@ public final class StationMapView {
mapAndProfilePane.widthProperty().subtract(20) mapAndProfilePane.widthProperty().subtract(20)
); );
detailPane = new VBox(10, pathAnalysisSection = createPathAnalysisSection();
createSelectedStationSection(), detailPane = new VBox(10, createSelectedStationSection(), pathAnalysisSection);
createPathAnalysisSection()
);
detailPane.setPadding(new Insets(10)); detailPane.setPadding(new Insets(10));
detailPane.setMinWidth(0); detailPane.setMinWidth(0);
@@ -364,8 +387,14 @@ public final class StationMapView {
detailScrollPane.setFitToWidth(true); detailScrollPane.setFitToWidth(true);
detailScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); detailScrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
detailScrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); 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.setPrefWidth(350);
detailScrollPane.setMaxWidth(Double.MAX_VALUE); detailScrollPane.setMaxWidth(Double.MAX_VALUE);
detailScrollPane.setFitToWidth(true); detailScrollPane.setFitToWidth(true);
@@ -378,8 +407,7 @@ public final class StationMapView {
SplitPane.setResizableWithParent(detailScrollPane, true); SplitPane.setResizableWithParent(detailScrollPane, true);
rootPane = new BorderPane(); rootPane = new BorderPane();
rootPane.setTop(statusLabel); rootPane.setTop(createMapHeader());
BorderPane.setMargin(statusLabel, new Insets(8));
rootPane.setCenter(mainSplitPane); rootPane.setCenter(mainSplitPane);
double[] size = chatPreferences.getGUIstationMapStageSceneSizeHW(); double[] size = chatPreferences.getGUIstationMapStageSceneSizeHW();
@@ -390,7 +418,12 @@ public final class StationMapView {
scene = new Scene(rootPane, initialWidth, initialHeight); scene = new Scene(rootPane, initialWidth, initialHeight);
stage.setMinWidth(900); stage.setMinWidth(900);
stage.setMinHeight(650); stage.setMinHeight(resolveMinimumStationMapHeight(
chatPreferences.isGUIstationMapPathAnalysisVisible()));
stage.setScene(scene);
setPathAnalysisVisible(chatPreferences.isGUIstationMapPathAnalysisVisible(), false);
applyThemeFromPreferences();
stage.setScene(scene); stage.setScene(scene);
applyThemeFromPreferences(); applyThemeFromPreferences();
@@ -446,6 +479,91 @@ public final class StationMapView {
} }
/**
* 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() {
// The status may be shortened before the show/hide control is ever clipped.
statusLabel.setMinWidth(0);
statusLabel.setMaxWidth(Double.MAX_VALUE);
statusLabel.setTextOverrun(OverrunStyle.ELLIPSIS);
HBox header = new HBox(
10,
statusLabel,
pathAnalysisHiddenHintLabel,
pathAnalysisVisibilityButton
);
header.setAlignment(Pos.CENTER_LEFT);
header.setPadding(new Insets(8));
HBox.setHgrow(statusLabel, Priority.ALWAYS);
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);
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;
}
private VBox createSelectedStationSection() { private VBox createSelectedStationSection() {
GridPane detailGrid = new GridPane(); GridPane detailGrid = new GridPane();
detailGrid.setHgap(8); detailGrid.setHgap(8);
@@ -494,13 +612,18 @@ public final class StationMapView {
gridPane.getColumnConstraints().clear(); gridPane.getColumnConstraints().clear();
ColumnConstraints labelColumn = new ColumnConstraints(); ColumnConstraints labelColumn = new ColumnConstraints();
labelColumn.setMinWidth(105); labelColumn.setMinWidth(70);
labelColumn.setPrefWidth(115); labelColumn.setPrefWidth(115);
labelColumn.setMaxWidth(130); labelColumn.setMaxWidth(130);
labelColumn.setHgrow(Priority.NEVER); labelColumn.setHgrow(Priority.NEVER);
ColumnConstraints valueColumn = new ColumnConstraints(); 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); valueColumn.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(labelColumn, valueColumn); gridPane.getColumnConstraints().addAll(labelColumn, valueColumn);
@@ -1530,8 +1653,11 @@ public final class StationMapView {
return 768.0; return 768.0;
} }
double minimumHeight = resolveMinimumStationMapHeight(
chatPreferences.isGUIstationMapPathAnalysisVisible());
// Avoid restoring very large old test sizes after the layout changed. // 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; return 768.0;
} }
+2
View File
@@ -10,6 +10,8 @@ module praktiKST {
requires java.net.http; requires java.net.http;
requires java.desktop; requires java.desktop;
requires jdk.crypto.ec; requires jdk.crypto.ec;
requires org.junit.jupiter.api;
requires org.mockito;
exports kst4contest.controller.interfaces; exports kst4contest.controller.interfaces;
exports kst4contest.controller; exports kst4contest.controller;
exports kst4contest.locatorUtils; exports kst4contest.locatorUtils;
@@ -199,3 +199,19 @@
-fx-text-fill: #fa9f66; -fx-text-fill: #fa9f66;
-fx-font-weight: bold; -fx-font-weight: bold;
} }
/*
* Clickable web links inside message-table cells.
* Text fragments keep the current table-cell color; links remain visible.
*/
.message-table-link {
-fx-padding: 0;
-fx-border-color: transparent;
-fx-underline: true;
-fx-text-fill: #005eb8;
}
.message-table-link:visited {
-fx-text-fill: #6b3fa0;
}
@@ -241,3 +241,18 @@
-fx-text-fill: #fa9f66; -fx-text-fill: #fa9f66;
-fx-font-weight: bold; -fx-font-weight: bold;
} }
/*
* Clickable web links inside message-table cells.
* Brighter colors preserve contrast on the dark table background.
*/
.message-table-link {
-fx-padding: 0;
-fx-border-color: transparent;
-fx-underline: true;
-fx-text-fill: #73b7ff;
}
.message-table-link:visited {
-fx-text-fill: #c59cff;
}
@@ -0,0 +1,120 @@
package kst4contest.test;
import kst4contest.logic.BandOpportunityResolver;
import kst4contest.model.Band;
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BandOpportunityResolverTest {
@Test
void resolvesCommonShorthandBandsFromStationName() {
EnumSet<Band> bands = BandOpportunityResolver.detectBandsFromStationName(
"Peter QRV 2-70-23/13/9/6/3"
);
assertEquals(
EnumSet.of(
Band.B_144,
Band.B_432,
Band.B_1296,
Band.B_2320,
Band.B_3400,
Band.B_5760,
Band.B_10G
),
bands
);
}
@Test
void doesNotMistakeOnePointTwoCentimetersForTwoMeters() {
EnumSet<Band> bands = BandOpportunityResolver.detectBandsFromStationName(
"David 23/3/1.2"
);
assertEquals(EnumSet.of(Band.B_1296, Band.B_10G, Band.B_24G), bands);
assertFalse(bands.contains(Band.B_144));
}
@Test
void keepsOnlyRecentDynamicBandEvidence() {
long now = 1_000_000L;
ChatMember station = new ChatMember();
station.addKnownFrequency(Band.B_144, 144.210);
station.addKnownFrequency(Band.B_432, 432.210);
station.getKnownActiveBands().get(Band.B_144).timestampEpoch = now - 5_000L;
station.getKnownActiveBands().get(Band.B_432).timestampEpoch =
now - BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS - 1L;
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(List.of(station), now);
assertEquals(EnumSet.of(Band.B_144), resolution.getOfferedBands());
}
@Test
void notQrvOverridesNameAndFrequencyEvidenceAcrossVariants() {
long now = 1_000_000L;
ChatMember categoryTwo = new ChatMember();
categoryTwo.setName("QRV 2m 70cm");
categoryTwo.addKnownFrequency(Band.B_432, 432.210);
categoryTwo.getKnownActiveBands().get(Band.B_432).timestampEpoch = now - 1_000L;
ChatMember categoryThree = new ChatMember();
categoryThree.setQrv432(false);
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(List.of(categoryTwo, categoryThree), now);
assertTrue(resolution.getOfferedBands().contains(Band.B_432));
assertFalse(resolution.getAvailableBands().contains(Band.B_432));
assertTrue(resolution.getAvailableBands().contains(Band.B_144));
}
@Test
void detectsFourAndSixMetersWithoutStealingCmShorthandBands() {
EnumSet<Band> bands = BandOpportunityResolver.detectBandsFromStationName(
"QRV 50 70cm 6M 4M"
);
assertEquals(
EnumSet.of(Band.B_50, Band.B_432, Band.B_70),
bands
);
}
@Test
void bareSeventyAndBareSixStillMeanCentimeterBands() {
EnumSet<Band> bands = BandOpportunityResolver.detectBandsFromStationName(
"QRV 70 6"
);
assertEquals(EnumSet.of(Band.B_432, Band.B_5760), bands);
assertFalse(bands.contains(Band.B_70));
assertFalse(bands.contains(Band.B_50));
}
@Test
void opportunityRequiresAvailableEnabledAndUnworkedBand() {
ChatMember station = new ChatMember();
station.setName("2m 70cm");
station.setWorked144(true);
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(List.of(station), System.currentTimeMillis());
assertEquals(
EnumSet.of(Band.B_432),
resolution.getUnworkedEnabledBands(EnumSet.of(Band.B_144, Band.B_432))
);
}
}
@@ -45,6 +45,38 @@ class MapCallsignRawSnapshotBuilderTest {
assertFalse(snapshots.get(0).offersSelectedBand()); assertFalse(snapshots.get(0).offersSelectedBand());
} }
@Test
void notQrvOverridesNameDerivedMapOpportunity() {
ChatMember station = buildStation("DL1ABC", "QRV 2m 70cm", "JN58TD", 1_000L);
station.setQrv432(false);
MapCallsignRawSnapshotBuilder builder = new MapCallsignRawSnapshotBuilder();
MapCallsignRawSnapshot snapshot = builder.buildSnapshots(
List.of(station),
null,
EnumSet.of(Band.B_432)
).get(0);
assertFalse(snapshot.offersSelectedBand());
assertFalse(snapshot.bandSummary().contains("432"));
}
@Test
void workedBandIsShownAsInformationButNotAsUpgradeOpportunity() {
ChatMember station = buildStation("DL1ABC", "QRV 2m", "JN58TD", 1_000L);
station.setWorked144(true);
MapCallsignRawSnapshotBuilder builder = new MapCallsignRawSnapshotBuilder();
MapCallsignRawSnapshot snapshot = builder.buildSnapshots(
List.of(station),
null,
EnumSet.of(Band.B_144)
).get(0);
assertTrue(snapshot.bandSummary().contains("144"));
assertFalse(snapshot.offersSelectedBand());
}
private ChatMember buildStation(String callSign, String name, String locator, long activityEpoch) { private ChatMember buildStation(String callSign, String name, String locator, long activityEpoch) {
ChatMember chatMember = new ChatMember(); ChatMember chatMember = new ChatMember();
chatMember.setCallSign(callSign); chatMember.setCallSign(callSign);
+12
View File
@@ -2371,3 +2371,15 @@ DO5SA;unknown;unknown;StringProperty [value: null]; wkd true; wkd144 true; wkd43
9A2HM;null;null;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null 9A2HM;null;null;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
9A2HM;null;null;StringProperty [value: null]; wkd true; wkd144 false; wkd432false; wkd1240false; wkd2300true; wkd3400false; wkd5600false; wkd10Gfalse ; null 9A2HM;null;null;StringProperty [value: null]; wkd true; wkd144 false; wkd432false; wkd1240false; wkd2300true; wkd3400false; wkd5600false; wkd10Gfalse ; null
OV3T;Thomas;JO46CM;StringProperty [value: null]; 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
OZ1ALS;144307;JO44XX;StringProperty [value: 144.307]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ1FDH;Claus;JO55QX;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ2AR;FRS Club;JO65BT;StringProperty [value: 144.243]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
SM7NCL;Chris;JO66JI;StringProperty [value: 144.255]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
OZ1DLD/P;Bent;JO45SK;StringProperty [value: 144.285]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
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
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
+10 -8
View File
@@ -97,15 +97,17 @@ function rewriteManualLinks(content, lang) {
return `[${label}](/manual/${linkLang}/${slug.toLowerCase()}/)`; return `[${label}](/manual/${linkLang}/${slug.toLowerCase()}/)`;
}) })
// Markdown-Links auf de-/en-Dateien ohne .md // Markdown links to German or English manual pages.
.replace(/\]\((en|de)-([^)#]+)\)/g, (match, linkLang, slug) => { // The source may contain an optional ".md" suffix and an optional anchor.
return `](/manual/${linkLang}/${slug.toLowerCase()}/)`; // 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 return `](/manual/${linkLang}/${slug.toLowerCase()}/${fragment})`;
.replace(/\]\((en|de)-([^)#]+)\.md\)/g, (match, linkLang, slug) => { }
return `](/manual/${linkLang}/${slug.toLowerCase()}/)`; );
});
} }
module.exports = function (eleventyConfig) { module.exports = function (eleventyConfig) {
-137
View File
@@ -1,137 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>About KST4Contest</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="About KST4Contest and its contest-oriented ON4KST workflow.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/about/">
<meta property="og:type" content="website">
<meta property="og:title" content="About KST4Contest">
<meta property="og:description" content="About KST4Contest and its contest-oriented ON4KST workflow.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/about/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="About KST4Contest">
<meta name="twitter:description" content="About KST4Contest and its contest-oriented ON4KST workflow.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">About</p>
<h1>Built for real contest operation.</h1>
<p class="lead">
KST4Contest was created to turn ON4KST chat activity into actionable contest workflow.
</p>
</section>
<section class="section narrow">
<article class="card content-card">
<h2>Why KST4Contest exists</h2>
<p>
Generic chat clients display messages. KST4Contest goes further:
it helps operators identify candidates, manage skeds, use AirScout timing,
synchronize with loggers and reduce workload during VHF/UHF/SHF contests.
</p>
<h2>Open source and practical</h2>
<p>
The project is open source and focused on practical contest station workflows.
Features are designed around real operating pressure, not theoretical UI concepts.
</p>
<h2>Community supported</h2>
<p>
KST4Contest is developed independently.
Community support helps finance hosting,
online services,
testing across multiple operating systems
and future contest-oriented features.
</p>
</article>
</section></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-807
View File
@@ -1,807 +0,0 @@
:root {
--bg: #050816;
--bg2: #0b1224;
--panel: rgba(15, 23, 42, 0.78);
--panel2: rgba(30, 41, 59, 0.72);
--border: rgba(148, 163, 184, 0.22);
--text: #f8fafc;
--muted: #aab6ca;
--soft: #64748b;
--accent: #38bdf8;
--accent2: #a855f7;
--accent3: #22c55e;
--shadow: 0 24px 80px rgba(0, 0, 0, 0.45);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
background: var(--bg);
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 14% 12%, rgba(168, 85, 247, 0.30), transparent 28%),
radial-gradient(circle at 82% 16%, rgba(56, 189, 248, 0.24), transparent 30%),
radial-gradient(circle at 50% 85%, rgba(34, 197, 94, 0.11), transparent 35%),
linear-gradient(180deg, #050816 0%, #07111f 100%);
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.65;
overflow-x: hidden;
}
body::before {
content: "";
position: fixed;
inset: -20%;
z-index: -3;
background:
radial-gradient(circle at 20% 20%, rgba(168, 85, 247, 0.22), transparent 28%),
radial-gradient(circle at 78% 18%, rgba(56, 189, 248, 0.18), transparent 30%),
radial-gradient(circle at 45% 80%, rgba(34, 197, 94, 0.10), transparent 26%);
filter: blur(48px);
transform: translateZ(0);
}
body::after {
content: "";
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
background-image:
linear-gradient(rgba(255,255,255,0.038) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.038) 1px, transparent 1px);
background-size: 54px 54px;
-webkit-mask-image: radial-gradient(circle at center, black 0%, black 46%, transparent 78%);
mask-image: radial-gradient(circle at center, black 0%, black 46%, transparent 78%);
}
.site-bg {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background:
linear-gradient(180deg, rgba(5, 8, 22, 0.10), rgba(5, 8, 22, 0.84)),
radial-gradient(circle at 8% 24%, rgba(168, 85, 247, 0.18), transparent 26%),
radial-gradient(circle at 82% 20%, rgba(56, 189, 248, 0.16), transparent 30%);
}
.site-bg::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at top, transparent 0%, rgba(5, 8, 22, 0.42) 72%),
linear-gradient(90deg, rgba(5, 8, 22, 0.25), transparent 50%, rgba(5, 8, 22, 0.25));
}
.site-bg::after {
content: "";
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 1px 1px, rgba(255,255,255,0.10) 1px, transparent 0);
background-size: 28px 28px;
opacity: 0.11;
-webkit-mask-image: radial-gradient(circle at 50% 22%, black 0%, transparent 68%);
mask-image: radial-gradient(circle at 50% 22%, black 0%, transparent 68%);
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
color: white;
}
.site-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
justify-content: space-between;
gap: 24px;
align-items: center;
padding: 18px 6vw;
background: rgba(5, 8, 22, 0.76);
-webkit-backdrop-filter: blur(18px);
backdrop-filter: blur(18px);
border-bottom: 1px solid var(--border);
}
.brand {
display: flex;
gap: 12px;
align-items: center;
color: white;
}
.brand-mark {
display: grid;
place-items: center;
width: 40px;
height: 40px;
border-radius: 14px;
background:
radial-gradient(circle at 30% 20%, white, transparent 22%),
linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow:
0 0 26px rgba(168, 85, 247, 0.45),
0 0 50px rgba(56, 189, 248, 0.18);
}
.brand strong {
display: block;
font-size: 1.05rem;
letter-spacing: 0.02em;
}
.brand small {
display: block;
color: var(--muted);
font-size: 0.78rem;
margin-top: -4px;
}
.site-nav {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.site-nav a {
color: var(--muted);
font-size: 0.95rem;
}
.site-nav a:hover {
color: white;
}
.nav-cta {
padding: 9px 14px;
border-radius: 999px;
color: white !important;
background: linear-gradient(135deg, var(--accent), var(--accent2));
box-shadow: 0 0 22px rgba(56, 189, 248, 0.28);
}
.hero {
position: relative;
padding: 92px 6vw 62px;
max-width: 1180px;
}
.hero::before {
content: "";
position: absolute;
inset: -160px -12vw auto -12vw;
height: 420px;
z-index: -1;
pointer-events: none;
background:
radial-gradient(circle at 24% 40%, rgba(168, 85, 247, 0.20), transparent 36%),
radial-gradient(circle at 62% 30%, rgba(56, 189, 248, 0.14), transparent 40%);
filter: blur(74px);
}
.hero-split {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(360px, 0.9fr);
gap: 42px;
align-items: center;
max-width: 1500px;
}
.hero h1 {
margin: 0 0 22px;
font-size: clamp(3rem, 8vw, 6.5rem);
line-height: 0.95;
letter-spacing: -0.07em;
background: linear-gradient(90deg, #ffffff 0%, #7dd3fc 40%, #a855f7 78%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.hero .lead,
.hero > p,
.section-heading p,
.cta-panel p {
color: var(--muted);
font-size: 1.18rem;
max-width: 850px;
}
.badge,
.eyebrow {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 8px;
color: #e9d5ff;
background: rgba(168, 85, 247, 0.13);
border: 1px solid rgba(168, 85, 247, 0.34);
padding: 6px 12px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.74rem;
font-weight: 800;
box-shadow: 0 0 22px rgba(168, 85, 247, 0.12);
}
.actions {
display: flex;
gap: 14px;
margin-top: 30px;
flex-wrap: wrap;
}
.button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 46px;
padding: 12px 18px;
border-radius: 14px;
color: #020617;
background: linear-gradient(135deg, var(--accent), var(--accent2));
font-weight: 800;
box-shadow:
0 16px 44px rgba(56, 189, 248, 0.25),
inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.button:hover {
color: white;
transform: translateY(-1px);
}
.button.secondary {
color: white;
background: rgba(30, 41, 59, 0.85);
border: 1px solid var(--border);
box-shadow: none;
}
.button.ghost {
color: white;
background: transparent;
border: 1px solid var(--border);
box-shadow: none;
}
.hero-panel,
.card,
.cta-panel {
background:
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 42%),
var(--panel);
border: 1px solid var(--border);
border-radius: 26px;
box-shadow: var(--shadow);
-webkit-backdrop-filter: blur(16px);
backdrop-filter: blur(16px);
}
.hero-panel {
position: relative;
overflow: hidden;
transform: perspective(1200px) rotateY(-5deg) rotateX(2deg);
}
.hero-panel::before {
content: "";
position: absolute;
inset: -1px;
pointer-events: none;
background:
linear-gradient(135deg, rgba(56, 189, 248, 0.45), transparent 35%, rgba(168, 85, 247, 0.38));
opacity: 0.18;
}
.terminal-bar {
display: flex;
gap: 8px;
align-items: center;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
background: rgba(2, 6, 23, 0.74);
}
.terminal-bar span {
width: 11px;
height: 11px;
border-radius: 50%;
background: var(--accent2);
}
.terminal-bar span:nth-child(2) {
background: #f59e0b;
}
.terminal-bar span:nth-child(3) {
background: var(--accent3);
}
.terminal-bar strong {
margin-left: 8px;
color: var(--muted);
font-size: 0.85rem;
}
.mock-grid {
display: grid;
gap: 12px;
padding: 18px;
}
.mini-card {
padding: 14px;
border-radius: 16px;
background: rgba(15, 23, 42, 0.88);
border: 1px solid rgba(148, 163, 184, 0.16);
}
.mini-card strong,
.mini-card small {
display: block;
}
.mini-card small {
color: var(--muted);
margin-top: 4px;
}
.section {
padding: 52px 6vw;
}
.section.narrow {
max-width: 1040px;
}
.section-heading {
margin-bottom: 26px;
}
.section h2 {
font-size: clamp(2rem, 4vw, 3.4rem);
line-height: 1.05;
margin: 12px 0 14px;
letter-spacing: -0.04em;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 18px;
}
.card {
padding: 24px;
transition: transform 180ms ease, border-color 180ms ease, background 180ms ease, box-shadow 180ms ease;
}
.card:hover {
transform: translateY(-4px);
border-color: rgba(56, 189, 248, 0.45);
background:
linear-gradient(180deg, rgba(255,255,255,0.055), transparent 42%),
rgba(15, 23, 42, 0.92);
box-shadow:
0 26px 90px rgba(0, 0, 0, 0.50),
0 0 30px rgba(56, 189, 248, 0.08);
}
.card h3,
.card h2 {
margin-top: 0;
}
.card p {
color: var(--muted);
}
.roadmap-version + .roadmap-version {
margin-top: 24px;
}
.roadmap-version ul {
padding-left: 20px;
}
.roadmap-version li {
margin-bottom: 8px;
}
.roadmap-done a {
color: var(--muted);
text-decoration: line-through;
}
.feature-icon {
font-size: 2rem;
margin-bottom: 14px;
}
.feature-icon.large {
font-size: 4rem;
}
.content-card {
max-width: 980px;
}
.tag-list {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.tag-list span {
padding: 7px 11px;
border-radius: 999px;
color: var(--muted);
background: rgba(15, 23, 42, 0.85);
border: 1px solid var(--border);
}
.download-card {
min-height: 220px;
}
.screenshot-placeholder {
display: grid;
place-items: center;
min-height: 180px;
margin-bottom: 16px;
border-radius: 18px;
background:
linear-gradient(135deg, rgba(56, 189, 248, 0.20), rgba(168, 85, 247, 0.20)),
rgba(2, 6, 23, 0.7);
border: 1px solid var(--border);
color: white;
font-weight: 800;
}
.cta-panel {
padding: clamp(28px, 5vw, 60px);
}
.manual-content {
max-width: 980px;
}
.manual-content h1,
.manual-content h2,
.manual-content h3 {
line-height: 1.2;
}
.manual-content pre {
overflow-x: auto;
padding: 16px;
border-radius: 14px;
background: #020617;
}
.manual-content code {
color: #bae6fd;
}
.manual-content table {
width: 100%;
border-collapse: collapse;
overflow-x: auto;
}
.manual-content th,
.manual-content td {
border-bottom: 1px solid rgba(148, 163, 184, 0.22);
padding: 10px;
text-align: left;
}
.site-footer {
padding: 42px 6vw;
color: var(--muted);
border-top: 1px solid var(--border);
}
@media (max-width: 900px) {
.hero-split {
grid-template-columns: 1fr;
}
.hero-panel {
transform: none;
}
.site-header {
align-items: flex-start;
flex-direction: column;
}
}
@media (prefers-reduced-motion: no-preference) {
.button,
.card {
transition: all 180ms ease;
}
}
.footer-grid {
display: grid;
grid-template-columns: 2fr repeat(3, 1fr);
gap: 24px;
}
.footer-grid strong {
color: white;
}
.footer-grid p {
margin: 8px 0;
}
@media (max-width: 800px) {
.footer-grid {
grid-template-columns: 1fr;
}
}
/* ================================================================
KST4Contest brand recovery: green operator-console identity
and full-screen interactive hero
================================================================ */
:root {
--bg: #050806;
--bg2: #0b140f;
--panel: rgba(15, 28, 20, 0.82);
--panel2: rgba(24, 43, 31, 0.84);
--border: rgba(125, 178, 134, 0.22);
--text: #f4f7f3;
--muted: #a9b8ad;
--soft: #6e8173;
--accent: #54d63d;
--accent2: #8aef64;
--accent3: #31b54a;
--fx-plane: #79ff73;
--fx-plane-muted: rgba(121, 255, 115, 0.42);
--fx-beam: #52ff65;
--fx-beam-glow: rgba(82, 255, 101, 0.28);
--fx-reflection: #d2ffc8;
--fx-trail: rgba(121, 255, 115, 0.17);
--fx-grid: rgba(121, 255, 115, 0.055);
}
body {
background:
radial-gradient(circle at 14% 12%, rgba(52, 165, 57, 0.20), transparent 30%),
radial-gradient(circle at 82% 16%, rgba(95, 218, 82, 0.14), transparent 32%),
radial-gradient(circle at 50% 85%, rgba(32, 126, 55, 0.12), transparent 36%),
linear-gradient(180deg, #070b09 0%, #0b1410 100%);
}
body::before {
background:
radial-gradient(circle at 20% 20%, rgba(60, 173, 61, 0.18), transparent 28%),
radial-gradient(circle at 78% 18%, rgba(97, 222, 80, 0.13), transparent 30%),
radial-gradient(circle at 45% 80%, rgba(34, 122, 53, 0.11), transparent 26%);
}
body::after {
background-image:
linear-gradient(rgba(160,255,160,0.032) 1px, transparent 1px),
linear-gradient(90deg, rgba(160,255,160,0.032) 1px, transparent 1px);
}
.site-bg {
background:
linear-gradient(180deg, rgba(5, 8, 6, 0.10), rgba(5, 8, 6, 0.84)),
radial-gradient(circle at 8% 24%, rgba(52, 165, 57, 0.14), transparent 26%),
radial-gradient(circle at 82% 20%, rgba(95, 218, 82, 0.12), transparent 30%);
}
.site-header {
background: rgba(5, 8, 7, 0.82);
}
.brand-mark,
.nav-cta,
.button {
background: linear-gradient(135deg, var(--accent), var(--accent2));
}
.brand-mark {
box-shadow:
0 0 26px rgba(84, 214, 61, 0.42),
0 0 50px rgba(49, 181, 74, 0.16);
}
.nav-cta {
color: #061008 !important;
box-shadow: 0 0 22px rgba(84, 214, 61, 0.28);
}
main {
width: 100%;
}
.hero.hero-split {
position: relative;
width: 100%;
max-width: none;
min-height: calc(100vh - 73px);
display: grid;
grid-template-columns: minmax(420px, 0.9fr) minmax(520px, 1.1fr);
gap: clamp(40px, 5vw, 110px);
align-items: center;
align-content: center;
padding: clamp(56px, 7vh, 100px) clamp(28px, 6vw, 110px);
overflow: hidden;
}
.hero.hero-split > *:not(.hero-radio-fx):not(.hero-fx-vignette) {
position: relative;
z-index: 3;
}
.hero-copy {
max-width: 760px;
}
.hero.hero-split h1 {
font-size: clamp(3.4rem, 8vw, 7rem);
background: linear-gradient(90deg, #ffffff 0%, #c9ffd0 28%, #77ec70 58%, #45bd4d 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.badge,
.eyebrow {
color: #caffc0;
background: rgba(52, 165, 57, 0.12);
border-color: rgba(105, 232, 88, 0.32);
box-shadow: 0 0 22px rgba(82, 255, 101, 0.08);
}
.button {
color: #041007;
box-shadow:
0 16px 44px rgba(84, 214, 61, 0.22),
inset 0 1px 0 rgba(255,255,255,0.35);
}
.button.secondary {
color: white;
background: rgba(28, 43, 32, 0.90);
}
.hero-panel {
width: min(100%, 760px);
justify-self: center;
transform: perspective(1200px) rotateY(-4deg) rotateX(1.5deg);
}
.hero-panel,
.card,
.cta-panel {
background:
linear-gradient(180deg, rgba(255,255,255,0.045), transparent 42%),
rgba(15, 28, 20, 0.82);
}
.mini-card {
background: rgba(12, 25, 17, 0.88);
border-color: rgba(148, 184, 155, 0.16);
}
.card:hover {
border-color: rgba(84, 214, 61, 0.42);
background:
linear-gradient(180deg, rgba(255,255,255,0.055), transparent 42%),
rgba(12, 25, 17, 0.94);
box-shadow:
0 26px 90px rgba(0,0,0,0.50),
0 0 30px rgba(84,214,61,0.07);
}
.hero-radio-fx {
position: absolute;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
opacity: 0.78;
}
.hero-fx-vignette {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
background:
linear-gradient(90deg,
rgba(7,11,9,0.92) 0%,
rgba(7,11,9,0.70) 32%,
rgba(7,11,9,0.17) 62%,
rgba(7,11,9,0.38) 100%),
linear-gradient(180deg,
rgba(7,11,9,0.08),
rgba(7,11,9,0.58));
}
html[data-theme="light"] {
--bg: #f4f4f2;
--bg2: #ffffff;
--panel: rgba(255,255,255,0.90);
--panel2: rgba(242,242,239,0.94);
--border: rgba(91,91,86,0.20);
--text: #252522;
--muted: #656560;
--soft: #888881;
--accent: #d33732;
--accent2: #f25a51;
--accent3: #ae2727;
--fx-plane: #df3732;
--fx-plane-muted: rgba(211,55,50,0.38);
--fx-beam: #ef4039;
--fx-beam-glow: rgba(239,64,57,0.22);
--fx-reflection: #ff8d84;
--fx-trail: rgba(211,55,50,0.15);
--fx-grid: rgba(185,47,43,0.045);
}
@media (min-width: 1600px) {
.hero.hero-split {
grid-template-columns: minmax(520px, 0.85fr) minmax(620px, 1.15fr);
}
.hero-panel {
width: min(100%, 820px);
}
}
@media (max-width: 1000px) {
.hero.hero-split {
min-height: auto;
grid-template-columns: 1fr;
padding-top: 64px;
padding-bottom: 64px;
}
.hero-panel {
width: min(100%, 760px);
transform: none;
}
.hero-radio-fx {
opacity: 0.50;
}
.hero-fx-vignette {
background: linear-gradient(180deg, rgba(7,11,9,0.84), rgba(7,11,9,0.34));
}
}
@media (prefers-reduced-motion: reduce) {
.hero-radio-fx {
opacity: 0.28;
}
}
-509
View File
@@ -1,509 +0,0 @@
(() => {
"use strict";
const canvas = document.querySelector("[data-hero-radio-fx]");
if (!(canvas instanceof HTMLCanvasElement)) return;
const hero = canvas.closest(".hero");
const ctx = canvas.getContext("2d", { alpha: true, desynchronized: true });
if (!(hero instanceof HTMLElement) || !ctx) return;
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
const coarsePointer = matchMedia("(pointer: coarse)");
const state = {
width: 0,
height: 0,
dpr: 1,
running: false,
visible: true,
lastFrame: 0,
spawnTime: 0,
beamTime: 0,
frameId: 0,
pointer: { x: 0, y: 0, active: false },
planes: [],
beams: [],
reflections: []
};
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const random = (min, max) => min + Math.random() * (max - min);
function css(name, fallback) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
}
function palette() {
return {
plane: css("--fx-plane", "#79ff73"),
planeMuted: css("--fx-plane-muted", "rgba(121,255,115,.42)"),
beam: css("--fx-beam", "#52ff65"),
beamGlow: css("--fx-beam-glow", "rgba(82,255,101,.28)"),
reflection: css("--fx-reflection", "#d2ffc8"),
trail: css("--fx-trail", "rgba(121,255,115,.17)"),
grid: css("--fx-grid", "rgba(121,255,115,.055)")
};
}
function roundedRect(x, y, width, height, radius) {
const r = Math.min(radius, width / 2, height / 2);
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.arcTo(x + width, y, x + width, y + height, r);
ctx.arcTo(x + width, y + height, x, y + height, r);
ctx.arcTo(x, y + height, x, y, r);
ctx.arcTo(x, y, x + width, y, r);
ctx.closePath();
}
function resize() {
const rect = hero.getBoundingClientRect();
state.width = Math.max(1, Math.round(rect.width));
state.height = Math.max(1, Math.round(rect.height));
state.dpr = Math.min(devicePixelRatio || 1, 1.5);
canvas.width = Math.round(state.width * state.dpr);
canvas.height = Math.round(state.height * state.dpr);
canvas.style.width = `${state.width}px`;
canvas.style.height = `${state.height}px`;
ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
if (!state.pointer.active) {
state.pointer.x = state.width * 0.55;
state.pointer.y = state.height * 0.42;
}
}
function stations() {
const targetX = state.width * 0.54;
const targetY = state.height * 0.43;
const left = { x: state.width * 0.075, y: state.height * 0.82, label: "JO51IJ" };
const right = { x: state.width * 0.925, y: state.height * 0.80, label: "JN49FL" };
left.angle = Math.atan2(targetY - left.y, targetX - left.x);
right.angle = Math.atan2(targetY - right.y, targetX - right.x);
return [left, right];
}
function planeLimit() {
if (coarsePointer.matches) return 5;
return state.width < 900 ? 7 : 12;
}
function createPlane() {
const margin = 55;
const edge = Math.floor(Math.random() * 4);
let x;
let y;
if (edge === 0) {
x = random(0, state.width);
y = -margin;
} else if (edge === 1) {
x = state.width + margin;
y = random(0, state.height);
} else if (edge === 2) {
x = random(0, state.width);
y = state.height + margin;
} else {
x = -margin;
y = random(0, state.height);
}
const tx = state.pointer.active ? state.pointer.x : state.width * random(0.38, 0.72);
const ty = state.pointer.active ? state.pointer.y : state.height * random(0.20, 0.72);
const angle = Math.atan2(ty - y, tx - x);
const speed = random(18, 34);
return {
x, y, angle,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
size: random(5.5, 9),
age: 0,
maxAge: random(12, 20),
steering: random(0.65, 1.2),
wobble: random(0, Math.PI * 2),
wobbleSpeed: random(0.7, 1.4),
hit: 0
};
}
function updatePlane(plane, dt) {
plane.age += dt;
plane.wobble += dt * plane.wobbleSpeed;
const tx = state.pointer.active ? state.pointer.x : state.width * 0.56;
const ty = state.pointer.active ? state.pointer.y : state.height * 0.44;
const desired = Math.atan2(ty - plane.y, tx - plane.x);
let diff = desired - plane.angle;
while (diff > Math.PI) diff -= Math.PI * 2;
while (diff < -Math.PI) diff += Math.PI * 2;
plane.angle += diff * plane.steering * dt;
const speed = Math.hypot(plane.vx, plane.vy);
const wobble = Math.sin(plane.wobble) * 0.14;
plane.vx = Math.cos(plane.angle + wobble) * speed;
plane.vy = Math.sin(plane.angle + wobble) * speed;
plane.x += plane.vx * dt;
plane.y += plane.vy * dt;
plane.hit = Math.max(0, plane.hit - dt * 1.8);
}
function expired(plane) {
const margin = 180;
return plane.age > plane.maxAge || plane.x < -margin || plane.x > state.width + margin || plane.y < -margin || plane.y > state.height + margin;
}
function beamTarget() {
if (!state.planes.length) return null;
const tx = state.pointer.active ? state.pointer.x : state.width * 0.56;
const ty = state.pointer.active ? state.pointer.y : state.height * 0.44;
const nearby = state.planes
.map(plane => ({ plane, distance: Math.hypot(plane.x - tx, plane.y - ty) }))
.filter(item => item.distance < 280)
.sort((a, b) => a.distance - b.distance);
return nearby[0]?.plane || state.planes[Math.floor(Math.random() * state.planes.length)];
}
function createBeam(target) {
const source = stations()
.map(station => ({ station, distance: Math.hypot(target.x - station.x, target.y - station.y) }))
.sort((a, b) => a.distance - b.distance)[0].station;
const angle = Math.atan2(target.y - source.y, target.x - source.x);
const tip = 45;
state.beams.push({
startX: source.x + Math.cos(angle) * tip,
startY: source.y + Math.sin(angle) * tip,
endX: target.x,
endY: target.y,
age: 0,
maxAge: 0.72
});
target.hit = 1;
const values = [50, 75, 100];
state.reflections.push({
x: target.x,
y: target.y,
age: 0,
maxAge: 1.25,
radius: random(8, 13),
probability: values[Math.floor(Math.random() * values.length)]
});
}
function updateEffects(dt) {
state.beams.forEach(beam => beam.age += dt);
state.beams = state.beams.filter(beam => beam.age < beam.maxAge);
state.reflections.forEach(r => {
r.age += dt;
r.radius += dt * 30;
});
state.reflections = state.reflections.filter(r => r.age < r.maxAge);
}
function drawGrid(p) {
ctx.save();
ctx.strokeStyle = p.grid;
ctx.lineWidth = 1;
const spacing = 54;
for (let x = spacing; x < state.width; x += spacing) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, state.height);
ctx.stroke();
}
for (let y = spacing; y < state.height; y += spacing) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(state.width, y);
ctx.stroke();
}
ctx.restore();
}
function drawRadar(p) {
const x = state.pointer.active ? state.pointer.x : state.width * 0.56;
const y = state.pointer.active ? state.pointer.y : state.height * 0.44;
ctx.save();
ctx.strokeStyle = p.planeMuted;
ctx.lineWidth = 1;
for (const radius of [24, 48, 82]) {
ctx.globalAlpha = 0.24 - radius / 500;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.stroke();
}
ctx.globalAlpha = 0.2;
ctx.beginPath();
ctx.moveTo(x - 100, y);
ctx.lineTo(x + 100, y);
ctx.moveTo(x, y - 100);
ctx.lineTo(x, y + 100);
ctx.stroke();
ctx.restore();
}
function drawYagi(station, p) {
ctx.save();
ctx.translate(station.x, station.y);
ctx.rotate(station.angle);
ctx.strokeStyle = p.plane;
ctx.fillStyle = p.plane;
ctx.lineWidth = 1.5;
ctx.lineCap = "round";
ctx.shadowColor = p.beamGlow;
ctx.shadowBlur = 9;
ctx.beginPath();
ctx.moveTo(-30, 26);
ctx.lineTo(-30, -5);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-32, 0);
ctx.lineTo(43, 0);
ctx.stroke();
for (const element of [
{ x: -27, length: 34 },
{ x: -12, length: 28 },
{ x: 2, length: 22 },
{ x: 14, length: 19 },
{ x: 25, length: 16 },
{ x: 35, length: 13 }
]) {
ctx.beginPath();
ctx.moveTo(element.x, -element.length / 2);
ctx.lineTo(element.x, element.length / 2);
ctx.stroke();
}
ctx.beginPath();
ctx.arc(-12, 0, 3.2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.save();
ctx.font = "700 11px Inter, system-ui, sans-serif";
const width = ctx.measureText(station.label).width + 14;
const x = station.x - width / 2;
const y = station.y + 32;
ctx.fillStyle = "rgba(6,18,10,.86)";
ctx.strokeStyle = p.planeMuted;
roundedRect(x, y, width, 23, 6);
ctx.fill();
ctx.stroke();
ctx.fillStyle = p.plane;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(station.label, station.x, y + 11.5);
ctx.restore();
}
function drawPlane(plane, p) {
ctx.save();
ctx.translate(plane.x, plane.y);
ctx.rotate(plane.angle);
ctx.globalAlpha = clamp(Math.min(plane.age * 1.8, (plane.maxAge - plane.age) * 1.8), 0, 1);
ctx.strokeStyle = p.trail;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-plane.size * 4.5, 0);
ctx.lineTo(-plane.size * 1.4, 0);
ctx.stroke();
ctx.fillStyle = plane.hit > 0 ? p.reflection : p.plane;
ctx.shadowColor = plane.hit > 0 ? p.reflection : p.beamGlow;
ctx.shadowBlur = plane.hit > 0 ? 16 : 5;
const s = plane.size;
ctx.beginPath();
ctx.moveTo(s * 1.65, 0);
ctx.lineTo(-s * 0.7, s * 0.34);
ctx.lineTo(-s * 0.15, s * 1.05);
ctx.lineTo(-s * 0.65, s * 1.08);
ctx.lineTo(-s * 1.35, s * 0.3);
ctx.lineTo(-s * 1.7, s * 0.28);
ctx.lineTo(-s * 1.12, 0);
ctx.lineTo(-s * 1.7, -s * 0.28);
ctx.lineTo(-s * 1.35, -s * 0.3);
ctx.lineTo(-s * 0.65, -s * 1.08);
ctx.lineTo(-s * 0.15, -s * 1.05);
ctx.lineTo(-s * 0.7, -s * 0.34);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawBeam(beam, p) {
const progress = beam.age / beam.maxAge;
const alpha = Math.sin(progress * Math.PI);
ctx.save();
ctx.globalAlpha = alpha;
ctx.strokeStyle = p.beamGlow;
ctx.lineWidth = 8;
ctx.shadowColor = p.beam;
ctx.shadowBlur = 15;
ctx.setLineDash([10, 6]);
ctx.lineDashOffset = -progress * 36;
ctx.beginPath();
ctx.moveTo(beam.startX, beam.startY);
ctx.lineTo(beam.endX, beam.endY);
ctx.stroke();
ctx.setLineDash([]);
ctx.strokeStyle = p.beam;
ctx.lineWidth = 1.8;
ctx.shadowBlur = 5;
ctx.beginPath();
ctx.moveTo(beam.startX, beam.startY);
ctx.lineTo(beam.endX, beam.endY);
ctx.stroke();
ctx.restore();
}
function drawReflection(r, p) {
const progress = r.age / r.maxAge;
const alpha = 1 - progress;
ctx.save();
ctx.translate(r.x, r.y);
ctx.globalAlpha = alpha;
ctx.strokeStyle = p.reflection;
ctx.lineWidth = 1.6;
ctx.shadowColor = p.beam;
ctx.shadowBlur = 18;
for (const scale of [1, 1.65]) {
ctx.globalAlpha = alpha * (scale === 1 ? 1 : 0.55);
ctx.beginPath();
ctx.arc(0, 0, r.radius * scale, 0, Math.PI * 2);
ctx.stroke();
}
ctx.globalAlpha = alpha * 0.8;
const cross = r.radius * 1.25;
ctx.beginPath();
ctx.moveTo(-cross, 0);
ctx.lineTo(cross, 0);
ctx.moveTo(0, -cross);
ctx.lineTo(0, cross);
ctx.stroke();
ctx.globalAlpha = alpha;
ctx.fillStyle = p.reflection;
ctx.beginPath();
ctx.arc(0, 0, 3.8, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
ctx.save();
const label = `${r.probability}%`;
ctx.font = "800 12px Inter, system-ui, sans-serif";
const width = ctx.measureText(label).width + 16;
const height = 25;
const x = r.x + r.radius + 12;
const y = r.y - r.radius - 17;
ctx.globalAlpha = clamp(alpha * 1.25, 0, 1);
ctx.fillStyle = "rgba(6,18,10,.91)";
ctx.strokeStyle = p.plane;
ctx.shadowColor = p.beamGlow;
ctx.shadowBlur = 10;
roundedRect(x, y, width, height, 6);
ctx.fill();
ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = p.reflection;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, x + width / 2, y + height / 2);
ctx.restore();
}
function draw(staticOnly = false) {
const p = palette();
ctx.clearRect(0, 0, state.width, state.height);
drawGrid(p);
drawRadar(p);
stations().forEach(station => drawYagi(station, p));
if (staticOnly) return;
state.planes.forEach(plane => drawPlane(plane, p));
state.beams.forEach(beam => drawBeam(beam, p));
state.reflections.forEach(r => drawReflection(r, p));
}
function animate(timestamp) {
if (!state.running) return;
const dt = clamp((timestamp - state.lastFrame) / 1000 || 0, 0, 0.05);
state.lastFrame = timestamp;
if (!state.visible || document.hidden) {
state.frameId = requestAnimationFrame(animate);
return;
}
state.spawnTime += dt;
state.beamTime += dt;
const spawnInterval = coarsePointer.matches ? 2.2 : 1.15;
const beamInterval = coarsePointer.matches ? 3.3 : 1.8;
if (state.spawnTime >= spawnInterval && state.planes.length < planeLimit()) {
state.spawnTime = 0;
state.planes.push(createPlane());
}
if (state.beamTime >= beamInterval && state.planes.length) {
state.beamTime = 0;
const target = beamTarget();
if (target) createBeam(target);
}
state.planes.forEach(plane => updatePlane(plane, dt));
state.planes = state.planes.filter(plane => !expired(plane));
updateEffects(dt);
draw(false);
state.frameId = requestAnimationFrame(animate);
}
function start() {
cancelAnimationFrame(state.frameId);
resize();
if (reducedMotion.matches) {
state.running = false;
draw(true);
return;
}
state.running = true;
state.lastFrame = performance.now();
if (!state.planes.length) {
const count = coarsePointer.matches ? 2 : 5;
for (let i = 0; i < count; i++) {
const plane = createPlane();
plane.age = Math.random() * 2;
state.planes.push(plane);
}
}
state.frameId = requestAnimationFrame(animate);
}
hero.addEventListener("pointermove", event => {
const rect = hero.getBoundingClientRect();
state.pointer.x = clamp(event.clientX - rect.left, 0, rect.width);
state.pointer.y = clamp(event.clientY - rect.top, 0, rect.height);
state.pointer.active = true;
}, { passive: true });
hero.addEventListener("pointerleave", () => state.pointer.active = false);
addEventListener("resize", resize, { passive: true });
document.addEventListener("visibilitychange", () => state.lastFrame = performance.now());
reducedMotion.addEventListener("change", start);
new ResizeObserver(() => {
resize();
if (reducedMotion.matches) draw(true);
}).observe(hero);
new IntersectionObserver(entries => {
state.visible = entries[0]?.isIntersecting ?? true;
state.lastFrame = performance.now();
}, { threshold: 0.05 }).observe(hero);
start();
})();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

-125
View File
@@ -1,125 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Contact | KST4Contest</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="Contact options for KST4Contest.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/contact/">
<meta property="og:type" content="website">
<meta property="og:title" content="Contact | KST4Contest">
<meta property="og:description" content="Contact options for KST4Contest.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/contact/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Contact | KST4Contest">
<meta name="twitter:description" content="Contact options for KST4Contest.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Contact</p>
<h1>Contact</h1>
<p class="lead">Get in touch, report issues or contribute to KST4Contest.</p>
</section>
<section class="section">
<div class="grid">
<article class="card">
<h3>GitHub Issues</h3>
<p>Report bugs, request features or discuss technical problems.</p>
<a href="https://github.com/praktimarc/kst4contest/issues">Open issues →</a>
</article>
<article class="card">
<h3>GitHub Repository</h3>
<p>Browse source code, releases and documentation.</p>
<a href="https://github.com/praktimarc/kst4contest">Open repository →</a>
</article>
<article class="card">
<h3>Email</h3>
<p>For legal or website-related contact.</p>
<a href="https://groups.google.com/g/kst4contest">Join the mailing list →</a>
</article>
</div>
</section></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-282
View File
@@ -1,282 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Download KST4Contest</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="Download the latest stable KST4Contest packages for Windows, Linux and macOS. The required Java runtime is included.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/download/">
<meta property="og:type" content="website">
<meta property="og:title" content="Download KST4Contest">
<meta property="og:description" content="Download the latest stable KST4Contest packages for Windows, Linux and macOS. The required Java runtime is included.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/download/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Download KST4Contest">
<meta name="twitter:description" content="Download the latest stable KST4Contest packages for Windows, Linux and macOS. The required Java runtime is included.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Stable release</p>
<h1>Download KST4Contest</h1>
<p class="lead">
Choose the package for your operating system. Official builds are published through GitHub Releases,
and the required Java runtime is already included.
</p>
<div class="actions">
<a class="button" href="https://github.com/praktimarc/kst4contest/releases/latest">Latest release on GitHub</a>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases">All releases</a>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Packages by platform</p>
<h2>Choose the format that fits your system</h2>
</div>
<div class="grid">
<article class="card download-card">
<div class="feature-icon">🪟</div>
<p class="badge">Default choice</p>
<h3>Windows</h3>
<p><strong>ZIP x64</strong></p>
<p>Extract the complete archive, then start praktiKST.exe. No separate Java installation is required.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/praktiKST-v1.41.1-windows-x64.zip">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">🐧</div>
<p class="badge">Default choice</p>
<h3>Linux</h3>
<p><strong>Flatpak (.flatpakref)</strong></p>
<p>Installs through Flatpak and can be updated through Flatpak or the desktop software centre.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/de.x08.KST4Contest.flatpakref">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">🐧</div>
<h3>Linux</h3>
<p><strong>AppImage x86_64</strong></p>
<p>Portable build without package installation. Make the downloaded file executable before the first launch.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-linux-x86_64.AppImage">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">📦</div>
<h3>Debian / Ubuntu</h3>
<p><strong>DEB amd64</strong></p>
<p>Native package for Debian, Ubuntu and distributions based on them.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-debian-amd64.deb">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">📦</div>
<h3>Fedora</h3>
<p><strong>RPM x86_64</strong></p>
<p>Native package built for Fedora and compatible RPM-based systems.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-fedora-x86_64.rpm">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">📦</div>
<h3>Arch Linux</h3>
<p><strong>pkg.tar.zst</strong></p>
<p>Release package for direct installation with pacman. AUR alternatives are described in the installation guide.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-archlinux-x86_64.pkg.tar.zst">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">🍎</div>
<h3>macOS Apple Silicon</h3>
<p><strong>DMG arm64</strong></p>
<p>Best-effort build for Apple Silicon Macs. The application is not currently notarized by Apple.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-macos-arm64.dmg">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">🍎</div>
<h3>macOS Intel</h3>
<p><strong>DMG x86_64</strong></p>
<p>Best-effort build for Intel Macs. The application is not currently notarized by Apple.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-macos-x86_64.dmg">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">📘</div>
<h3>Manual English</h3>
<p><strong>PDF</strong></p>
<p>English PDF manual included with the Stable release.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-manual-en.pdf">Download →</a>
</article>
<article class="card download-card">
<div class="feature-icon">📘</div>
<h3>Manual German</h3>
<p><strong>PDF</strong></p>
<p>German PDF manual included with the Stable release.</p>
<a class="button secondary" href="https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-manual-de.pdf">Download →</a>
</article>
</div>
</section>
<section class="section">
<div class="cta-panel">
<p class="eyebrow">Choosing a package</p>
<h2>Which file should I use?</h2>
<p>
Windows users normally need the x64 ZIP package. On Linux, Flatpak is the practical choice when
updates should be managed through the desktop software centre or the Flatpak command line. The
AppImage is useful when a portable file is preferred, while DEB, RPM and Arch packages integrate
with their respective package managers.
</p>
<p>
macOS packages are provided for Apple Silicon and Intel systems on a best-effort basis. They are
not currently notarized by Apple. The installation guide covers the first launch, updates, the
Stable, Beta and Nightly channels, and the location of the settings directory.
</p>
<div class="actions">
<a class="button" href="/manual/en/installation/">Read the installation guide</a>
<a class="button secondary" href="/faq/">Open the FAQ</a>
</div>
</div>
</section>
<section class="section">
<div class="card content-card">
<h2>Development and infrastructure</h2>
<p>
KST4Contest is developed independently. If it is useful in your contest workflow and you want to
help cover continued development, testing and infrastructure costs, the support page explains
what project contributions are used for.
</p>
<div class="actions">
<a class="button" href="/support/">Support the project</a>
</div>
</div>
</section></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-168
View File
@@ -1,168 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KST4Contest FAQ</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/faq/">
<meta property="og:type" content="website">
<meta property="og:title" content="KST4Contest FAQ">
<meta property="og:description" content="Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/faq/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="KST4Contest FAQ">
<meta name="twitter:description" content="Frequently asked questions about KST4Contest, ON4KST contest operation, AirScout integration and VHF/UHF/SHF contest workflow.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<h1>Frequently asked questions</h1>
<p>
Answers about KST4Contest, ON4KST contest operation, downloads,
AirScout integration and supported platforms.
</p>
</section>
<section class="section">
<div class="card">
<h2>What is KST4Contest?</h2>
<p>
KST4Contest is a contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contest operation.
</p>
<h2>Is KST4Contest a replacement for wtKST, KST2Me or KSTChat?</h2>
<p>
KST4Contest follows a different approach. It is focused on contest workflow, priority candidates,
sked handling, AirScout support, log synchronization and operator decision support.
</p>
<h2>Which platforms are supported?</h2>
<p>
KST4Contest provides release artifacts for Windows, Linux and macOS through GitHub Releases.
</p>
<h2>Does KST4Contest support AirScout?</h2>
<p>
Yes. KST4Contest is designed to integrate AirScout-related workflow information into contest operation.
</p>
<h2>Where can I download KST4Contest?</h2>
<p>
Official builds are available on the GitHub Releases page.
</p>
</div>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is KST4Contest?",
"acceptedAnswer": {
"@type": "Answer",
"text": "KST4Contest is a contest-optimized ON4KST chat client for VHF, UHF and SHF ham radio contest operation."
}
},
{
"@type": "Question",
"name": "Is KST4Contest a replacement for wtKST, KST2Me or KSTChat?",
"acceptedAnswer": {
"@type": "Answer",
"text": "KST4Contest follows a different approach and focuses on contest workflow, priority candidates, sked handling, AirScout support, log synchronization and operator decision support."
}
},
{
"@type": "Question",
"name": "Which platforms are supported?",
"acceptedAnswer": {
"@type": "Answer",
"text": "KST4Contest provides release artifacts for Windows, Linux and macOS through GitHub Releases."
}
}
]
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-223
View File
@@ -1,223 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AirScout Integration</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="KST4Contest receives aircraft scatter information from AirScout and relates AP timing to active ON4KST stations.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/airscout/">
<meta property="og:type" content="website">
<meta property="og:title" content="AirScout Integration">
<meta property="og:description" content="KST4Contest receives aircraft scatter information from AirScout and relates AP timing to active ON4KST stations.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/airscout/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="AirScout Integration">
<meta name="twitter:description" content="KST4Contest receives aircraft scatter information from AirScout and relates AP timing to active ON4KST stations.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Aircraft Scatter · since 1.26</p>
<h1>AirScout Integration</h1>
<p class="lead">Use AirScout AP predictions in candidate evaluation, the timeline and sked planning.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">✈️</div>
<h2>Airplane scatter in the operator workflow</h2>
<p>Aircraft reflections can create short but valuable contact opportunities.</p>
<p>KST4Contest helps combine ON4KST chat activity with AirScout-based propagation awareness.</p>
<h2>From candidate to sked decision</h2>
<p>AirScout information can support the decision whether a candidate station should be called immediately, scheduled later or monitored for a better window.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>AirScout</span>
<span>airplane scatter</span>
<span>aircraft scatter</span>
<span>VHF contest</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/timeline/">Timeline View</a>
<a class="button secondary" href="/features/priority-score/">Priority Score System</a>
<a class="button secondary" href="/features/sked-reminder/">Sked Reminder</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "AirScout Integration",
"description": "Use AirScout AP predictions in candidate evaluation, the timeline and sked planning.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/airscout/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-199
View File
@@ -1,199 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Dual Chat Categories</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="KST4Contest combines two ON4KST chat categories while retaining the category required for messages and station context.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/dual-chat/">
<meta property="og:type" content="website">
<meta property="og:title" content="Dual Chat Categories">
<meta property="og:description" content="KST4Contest combines two ON4KST chat categories while retaining the category required for messages and station context.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/dual-chat/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Dual Chat Categories">
<meta name="twitter:description" content="KST4Contest combines two ON4KST chat categories while retaining the category required for messages and station context.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">ON4KST Chat · since 1.26</p>
<h1>Dual Chat Categories</h1>
<p class="lead">Log in to two ON4KST categories and handle both message streams in one station list and interface.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">💬</div>
<h2>Two categories, one workflow</h2>
<p>KST4Contest can operate with two ON4KST chat categories at once.</p>
<p>This is useful when contest activity spans VHF/UHF and microwave operation.</p>
<h2>Less window switching</h2>
<p>The operator can keep more information visible without constantly changing tools.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>ON4KST</span>
<span>dual chat</span>
<span>microwave contest</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/priority-score/">Priority Score System</a>
<a class="button secondary" href="/features/log-sync/">Log Synchronization</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Dual Chat Categories",
"description": "Log in to two ON4KST categories and handle both message streams in one station list and interface.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/dual-chat/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
@@ -1,198 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>DXCluster Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="The built-in DX Cluster server forwards KST4Contest direction warnings and known frequency information to compatible contest loggers.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/dx-cluster/">
<meta property="og:type" content="website">
<meta property="og:title" content="DXCluster Server">
<meta property="og:description" content="The built-in DX Cluster server forwards KST4Contest direction warnings and known frequency information to compatible contest loggers.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/dx-cluster/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="DXCluster Server">
<meta name="twitter:description" content="The built-in DX Cluster server forwards KST4Contest direction warnings and known frequency information to compatible contest loggers.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Radio Workflow · since 1.23</p>
<h1>DXCluster Server</h1>
<p class="lead">Send direction warnings as DX Cluster spots to compatible logging software when a usable QRG is known.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">📡</div>
<h2>More situational awareness</h2>
<p>DXCluster information can support contest operators by adding another source of activity information.</p>
<h2>Integrated workflow</h2>
<p>The goal is not to open another isolated tool, but to connect activity information with the wider KST4Contest workflow.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>DXCluster</span>
<span>ham radio</span>
<span>contest</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/log-sync/">Log Synchronization</a>
<a class="button secondary" href="/features/priority-score/">Priority Score System</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "DXCluster Server",
"description": "Send direction warnings as DX Cluster spots to compatible logging software when a usable QRG is known.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/dx-cluster/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-184
View File
@@ -1,184 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KST4Contest Features</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="Contest-optimized ON4KST features for VHF, UHF and SHF operation.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/">
<meta property="og:type" content="website">
<meta property="og:title" content="KST4Contest Features">
<meta property="og:description" content="Contest-optimized ON4KST features for VHF, UHF and SHF operation.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="KST4Contest Features">
<meta name="twitter:description" content="Contest-optimized ON4KST features for VHF, UHF and SHF operation.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Features</p>
<h1>Contest tools, not just chat windows.</h1>
<p class="lead">
KST4Contest combines ON4KST chat, candidate scoring, AirScout workflow,
sked handling, log synchronization and operator decision support.
</p>
</section>
<section class="section">
<div class="grid">
<article class="card feature-card">
<div class="feature-icon">✈️</div>
<p class="eyebrow">Aircraft Scatter</p>
<h3><a href="/features/airscout/">AirScout Integration</a></h3>
<p>Use AirScout AP predictions in candidate evaluation, the timeline and sked planning.</p>
<a href="/features/airscout/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">💬</div>
<p class="eyebrow">ON4KST Chat</p>
<h3><a href="/features/dual-chat/">Dual Chat Categories</a></h3>
<p>Log in to two ON4KST categories and handle both message streams in one station list and interface.</p>
<a href="/features/dual-chat/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">📡</div>
<p class="eyebrow">Radio Workflow</p>
<h3><a href="/features/dx-cluster/">DXCluster Server</a></h3>
<p>Send direction warnings as DX Cluster spots to compatible logging software when a usable QRG is known.</p>
<a href="/features/dx-cluster/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon"></div>
<p class="eyebrow"></p>
<h3><a href="/features/">KST4Contest Features</a></h3>
<p></p>
<a href="/features/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">🔄</div>
<p class="eyebrow">Logger Integration</p>
<h3><a href="/features/log-sync/">Log Synchronization</a></h3>
<p>Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.</p>
<a href="/features/log-sync/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon"></div>
<p class="eyebrow">Operator Speed</p>
<h3><a href="/features/macros/">Macros and Variables</a></h3>
<p>Build recurring chat messages from configurable text blocks and variables such as the current QRG or locator.</p>
<a href="/features/macros/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">🎯</div>
<p class="eyebrow">Contest Workflow</p>
<h3><a href="/features/priority-score/">Priority Score System</a></h3>
<p>Rank active stations from known context such as direction, activity, band, QRG, AP and sked information.</p>
<a href="/features/priority-score/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">🔔</div>
<p class="eyebrow">Sked Management</p>
<h3><a href="/features/sked-reminder/">Sked Reminder</a></h3>
<p>Store planned contacts and issue configurable chat messages plus local alerts before the agreed time.</p>
<a href="/features/sked-reminder/">Learn more →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">⏱️</div>
<p class="eyebrow">Contest Awareness</p>
<h3><a href="/features/timeline/">Timeline View</a></h3>
<p>Show AP-based priority candidates and scheduled contacts on a shared timeline.</p>
<a href="/features/timeline/">Learn more →</a>
</article>
</div>
</section></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-199
View File
@@ -1,199 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Log Synchronization</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="KST4Contest receives worked-station and, where supported, frequency data from UCXLog, N1MM+, QARTest, DXLog.net and Win-Test through file and UDP interfaces.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/log-sync/">
<meta property="og:type" content="website">
<meta property="og:title" content="Log Synchronization">
<meta property="og:description" content="KST4Contest receives worked-station and, where supported, frequency data from UCXLog, N1MM+, QARTest, DXLog.net and Win-Test through file and UDP interfaces.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/log-sync/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Log Synchronization">
<meta name="twitter:description" content="KST4Contest receives worked-station and, where supported, frequency data from UCXLog, N1MM+, QARTest, DXLog.net and Win-Test through file and UDP interfaces.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Logger Integration · since 1.31</p>
<h1>Log Synchronization</h1>
<p class="lead">Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">🔄</div>
<h2>Connect chat and logging</h2>
<p>Contest operation is faster when chat information and log state are connected.</p>
<p>KST4Contest can use logger information to improve workflow awareness.</p>
<h2>Worked and band awareness</h2>
<p>Log synchronization helps the client understand which stations and bands are relevant.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>Win-Test</span>
<span>UCXLog</span>
<span>contest logger</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/priority-score/">Priority Score System</a>
<a class="button secondary" href="/features/dual-chat/">Dual Chat Categories</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Log Synchronization",
"description": "Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/log-sync/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-198
View File
@@ -1,198 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Macros and Variables</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="KST4Contest macros combine reusable message text with values taken from the current station, frequency and operating context.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/macros/">
<meta property="og:type" content="website">
<meta property="og:title" content="Macros and Variables">
<meta property="og:description" content="KST4Contest macros combine reusable message text with values taken from the current station, frequency and operating context.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/macros/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Macros and Variables">
<meta name="twitter:description" content="KST4Contest macros combine reusable message text with values taken from the current station, frequency and operating context.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Operator Speed · since 1.0</p>
<h1>Macros and Variables</h1>
<p class="lead">Build recurring chat messages from configurable text blocks and variables such as the current QRG or locator.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large"></div>
<h2>Faster messages</h2>
<p>Macros reduce repetitive typing during active contest operation.</p>
<h2>More consistent operation</h2>
<p>Variables help keep messages structured and reduce manual errors.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>macros</span>
<span>ON4KST</span>
<span>operator workflow</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/sked-reminder/">Sked Reminder</a>
<a class="button secondary" href="/features/dual-chat/">Dual Chat Categories</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Macros and Variables",
"description": "Build recurring chat messages from configurable text blocks and variables such as the current QRG or locator.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/macros/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
@@ -1,227 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Priority Score System</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="The Priority Score System combines available contest information to populate a separate list of currently relevant candidates.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/priority-score/">
<meta property="og:type" content="website">
<meta property="og:title" content="Priority Score System">
<meta property="og:description" content="The Priority Score System combines available contest information to populate a separate list of currently relevant candidates.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/priority-score/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Priority Score System">
<meta name="twitter:description" content="The Priority Score System combines available contest information to populate a separate list of currently relevant candidates.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Contest Workflow · since 1.40</p>
<h1>Priority Score System</h1>
<p class="lead">Rank active stations from known context such as direction, activity, band, QRG, AP and sked information.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">🎯</div>
<h2>Why it matters</h2>
<p>During active VHF, UHF and microwave contests, relevant stations can disappear quickly in busy ON4KST chat traffic.</p>
<p>The Priority Score System helps operators focus on stations that are more likely to be useful during contest operation.</p>
<h2>Built for contest decisions</h2>
<p>KST4Contest is not just a chat viewer. It evaluates contest-relevant context and helps turn chat activity into better operating decisions.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>ON4KST</span>
<span>VHF</span>
<span>UHF</span>
<span>SHF</span>
<span>contest</span>
<span>priority candidates</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/timeline/">Timeline View</a>
<a class="button secondary" href="/features/airscout/">AirScout Integration</a>
<a class="button secondary" href="/features/sked-reminder/">Sked Reminder</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Priority Score System",
"description": "Rank active stations from known context such as direction, activity, band, QRG, AP and sked information.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/priority-score/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
@@ -1,221 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Sked Reminder</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="Sked Reminder keeps scheduled contacts visible and provides automatic advance messages together with acoustic and visual operator alerts.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/sked-reminder/">
<meta property="og:type" content="website">
<meta property="og:title" content="Sked Reminder">
<meta property="og:description" content="Sked Reminder keeps scheduled contacts visible and provides automatic advance messages together with acoustic and visual operator alerts.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/sked-reminder/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Sked Reminder">
<meta name="twitter:description" content="Sked Reminder keeps scheduled contacts visible and provides automatic advance messages together with acoustic and visual operator alerts.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Sked Management · since 1.40</p>
<h1>Sked Reminder</h1>
<p class="lead">Store planned contacts and issue configurable chat messages plus local alerts before the agreed time.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">🔔</div>
<h2>Never lose important skeds</h2>
<p>During active contests, it is easy to miss a planned contact while handling chat traffic, logging and band changes.</p>
<p>Sked Reminder keeps scheduled contacts visible.</p>
<h2>Built for real contest pressure</h2>
<p>The workflow supports time-critical operation where missing a few minutes can mean missing a QSO.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>sked</span>
<span>ON4KST</span>
<span>contest reminder</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/priority-score/">Priority Score System</a>
<a class="button secondary" href="/features/airscout/">AirScout Integration</a>
<a class="button secondary" href="/features/timeline/">Timeline View</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Sked Reminder",
"description": "Store planned contacts and issue configurable chat messages plus local alerts before the agreed time.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/sked-reminder/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-199
View File
@@ -1,199 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Timeline View</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="The Timeline View places upcoming aircraft scatter candidates and planned skeds into a common time-based overview.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/features/timeline/">
<meta property="og:type" content="website">
<meta property="og:title" content="Timeline View">
<meta property="og:description" content="The Timeline View places upcoming aircraft scatter candidates and planned skeds into a common time-based overview.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/features/timeline/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Timeline View">
<meta name="twitter:description" content="The Timeline View places upcoming aircraft scatter candidates and planned skeds into a common time-based overview.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Contest Awareness · since 1.40</p>
<h1>Timeline View</h1>
<p class="lead">Show AP-based priority candidates and scheduled contacts on a shared timeline.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<div class="feature-icon large">⏱️</div>
<h2>Timing matters</h2>
<p>Many VHF/UHF/SHF opportunities are short-lived.</p>
<p>The Timeline View helps operators see upcoming timing windows instead of keeping everything in mind manually.</p>
<h2>Designed for fast awareness</h2>
<p>The timeline supports quick decisions during busy contest operation.</p>
<h2>Keywords</h2>
<div class="tag-list">
<span>timeline</span>
<span>AP windows</span>
<span>airplane scatter</span>
</div>
<h2>Related features</h2>
<div class="actions">
<a class="button secondary" href="/features/airscout/">AirScout Integration</a>
<a class="button secondary" href="/features/priority-score/">Priority Score System</a>
</div>
</article>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Timeline View",
"description": "Show AP-based priority candidates and scheduled contacts on a shared timeline.",
"about": "KST4Contest",
"url": "https://kst4contest.hamradioonline.de/features/timeline/",
"author": {
"@type": "Person",
"name": "Praktimarc"
},
"publisher": {
"@type": "Organization",
"name": "KST4Contest"
}
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
-35
View File
@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>KST4Contest News</title>
<link>https://kst4contest.hamradioonline.de/</link>
<description>News and release updates for KST4Contest.</description>
<language>en</language>
<item>
<title>AUR Packages now live of kst4contest</title>
<link>https://kst4contest.hamradioonline.de/news/2026-07-15-AUR-Packages/</link>
<guid>https://kst4contest.hamradioonline.de/news/2026-07-15-AUR-Packages/</guid>
<pubDate>Wed, 15 Jul 2026 00:00:00 GMT</pubDate>
<description>We now uploaded the packages of kst4contest to the AUR</description>
</item>
<item>
<title>Hotfix Version 1.41.1</title>
<link>https://kst4contest.hamradioonline.de/news/2026-07-08-hotfix-version-1-41-1/</link>
<guid>https://kst4contest.hamradioonline.de/news/2026-07-08-hotfix-version-1-41-1/</guid>
<pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
<description>Hotfix of a bug in Version 1.41</description>
</item>
<item>
<title>KST4Contest Website Launch</title>
<link>https://kst4contest.hamradioonline.de/news/2026-07-website-launch/</link>
<guid>https://kst4contest.hamradioonline.de/news/2026-07-website-launch/</guid>
<pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate>
<description>The new KST4Contest website is being built as a static, SEO-friendly product portal with GitHub-based documentation.</description>
</item>
</channel>
</rss>
-366
View File
@@ -1,366 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>KST4Contest ON4KST Contest Client for VHF, UHF and SHF</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="KST4Contest combines ON4KST chat, candidate prioritisation, sked planning, AirScout data and logger integration in one desktop client.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/">
<meta property="og:type" content="website">
<meta property="og:title" content="KST4Contest ON4KST Contest Client for VHF, UHF and SHF">
<meta property="og:description" content="KST4Contest combines ON4KST chat, candidate prioritisation, sked planning, AirScout data and logger integration in one desktop client.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="KST4Contest ON4KST Contest Client for VHF, UHF and SHF">
<meta name="twitter:description" content="KST4Contest combines ON4KST chat, candidate prioritisation, sked planning, AirScout data and logger integration in one desktop client.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero hero-split">
<canvas class="hero-radio-fx" data-hero-radio-fx aria-hidden="true"></canvas>
<div class="hero-fx-vignette" aria-hidden="true"></div>
<div class="hero-copy">
<p class="badge">ON4KST contest workflow for 144 MHz and above</p>
<h1>KST4Contest</h1>
<p class="lead">
ON4KST shows you the traffic. KST4Contest helps turn it into an operating
decision by combining chat, candidate prioritisation, sked planning,
AirScout data and logger integration in one desktop client.
</p>
<p>
The available information is filtered, related to active stations and
presented in a contest-oriented workflow. The final decision still
belongs to the operator.
</p>
<div class="actions">
<a class="button" href="/download/">Download KST4Contest</a>
<a class="button secondary" href="/manual/">Read the manual</a>
<a class="button ghost" href="https://github.com/praktimarc/kst4contest">View source code</a>
</div>
</div>
<div class="hero-panel">
<div class="terminal-bar">
<span></span><span></span><span></span>
<strong>Functions sharing the same station context</strong>
</div>
<div class="mock-grid">
<div class="mini-card">
<strong>✈️ AirScout Integration</strong>
<small>Use AirScout AP predictions in candidate evaluation, the timeline and sked planning.</small>
</div>
<div class="mini-card">
<strong>💬 Dual Chat Categories</strong>
<small>Log in to two ON4KST categories and handle both message streams in one station list and interface.</small>
</div>
<div class="mini-card">
<strong>📡 DXCluster Server</strong>
<small>Send direction warnings as DX Cluster spots to compatible logging software when a usable QRG is known.</small>
</div>
<div class="mini-card">
<strong> KST4Contest Features</strong>
<small></small>
</div>
<div class="mini-card">
<strong>🔄 Log Synchronization</strong>
<small>Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.</small>
</div>
<div class="mini-card">
<strong>⚡ Macros and Variables</strong>
<small>Build recurring chat messages from configurable text blocks and variables such as the current QRG or locator.</small>
</div>
</div>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">The operating problem</p>
<h2>The useful station is rarely the only station in the chat</h2>
<p>
During an active contest, messages, sked requests, frequency information
and band changes arrive continuously. The task is to identify which part
of that traffic matters now, which candidate should be monitored and
which contact is better scheduled for later.
</p>
</div>
<div class="grid">
<article class="card">
<h3>Observe</h3>
<p>
Messages, locators, detected frequencies and known band activity are
assigned to stations across as many as two ON4KST chat categories.
</p>
</article>
<article class="card">
<h3>Evaluate</h3>
<p>
Direction, distance, worked status, NOT-QRV information, chat activity,
sked context and aircraft scatter data feed filters and candidate scores.
</p>
</article>
<article class="card">
<h3>Act</h3>
<p>
Candidates can be contacted, scheduled or monitored. Skeds remain
visible, while supported loggers and station interfaces receive the
information required for the next operating step.
</p>
</article>
</div>
</section>
<section class="section">
<div class="section-heading">
<p class="eyebrow">Functions</p>
<h2>One workflow, shared context</h2>
<p>
The individual functions are not isolated tools. They use the same
station, message, band and timing information, so that a change in one
part of the workflow can also affect filters, priorities and reminders.
</p>
</div>
<div class="grid">
<article class="card feature-card">
<div class="feature-icon">✈️</div>
<p class="eyebrow">Aircraft Scatter</p>
<h3><a href="/features/airscout/">AirScout Integration</a></h3>
<p>Use AirScout AP predictions in candidate evaluation, the timeline and sked planning.</p>
<a href="/features/airscout/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">💬</div>
<p class="eyebrow">ON4KST Chat</p>
<h3><a href="/features/dual-chat/">Dual Chat Categories</a></h3>
<p>Log in to two ON4KST categories and handle both message streams in one station list and interface.</p>
<a href="/features/dual-chat/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">📡</div>
<p class="eyebrow">Radio Workflow</p>
<h3><a href="/features/dx-cluster/">DXCluster Server</a></h3>
<p>Send direction warnings as DX Cluster spots to compatible logging software when a usable QRG is known.</p>
<a href="/features/dx-cluster/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon"></div>
<p class="eyebrow"></p>
<h3><a href="/features/">KST4Contest Features</a></h3>
<p></p>
<a href="/features/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">🔄</div>
<p class="eyebrow">Logger Integration</p>
<h3><a href="/features/log-sync/">Log Synchronization</a></h3>
<p>Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.</p>
<a href="/features/log-sync/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon"></div>
<p class="eyebrow">Operator Speed</p>
<h3><a href="/features/macros/">Macros and Variables</a></h3>
<p>Build recurring chat messages from configurable text blocks and variables such as the current QRG or locator.</p>
<a href="/features/macros/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">🎯</div>
<p class="eyebrow">Contest Workflow</p>
<h3><a href="/features/priority-score/">Priority Score System</a></h3>
<p>Rank active stations from known context such as direction, activity, band, QRG, AP and sked information.</p>
<a href="/features/priority-score/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">🔔</div>
<p class="eyebrow">Sked Management</p>
<h3><a href="/features/sked-reminder/">Sked Reminder</a></h3>
<p>Store planned contacts and issue configurable chat messages plus local alerts before the agreed time.</p>
<a href="/features/sked-reminder/">Read how it works →</a>
</article>
<article class="card feature-card">
<div class="feature-icon">⏱️</div>
<p class="eyebrow">Contest Awareness</p>
<h3><a href="/features/timeline/">Timeline View</a></h3>
<p>Show AP-based priority candidates and scheduled contacts on a shared timeline.</p>
<a href="/features/timeline/">Read how it works →</a>
</article>
</div>
</section>
<section class="section">
<div class="cta-panel">
<p class="eyebrow">Limits</p>
<h2>A priority score is not a propagation forecast</h2>
<p>
KST4Contest can only evaluate the information it knows. Scores, AP
windows, filters and path profiles support the operator's decision; they
do not guarantee a contact. If the input data is incomplete or outdated,
the result can be incomplete or outdated as well.
</p>
<div class="actions">
<a class="button" href="/features/">Explore the functions</a>
<a class="button secondary" href="/manual/">Read the technical details</a>
</div>
</div>
</section>
<section class="section">
<div class="cta-panel">
<p class="eyebrow">Project status</p>
<h2>Stable for operation, Nightly for testing</h2>
<p>
KST4Contest is open-source software. The current stable release is the
normal choice for contest operation. Nightly builds follow ongoing
development and are useful when a particular fix or feature needs
testing. A few minutes before a contest is not the ideal time to discover
what changed.
</p>
<div class="actions">
<a class="button" href="/download/">Download the stable release</a>
<a class="button secondary" href="/roadmap/">Development status</a>
<a class="button ghost" href="/support/">Support development</a>
</div>
</div>
</section>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "KST4Contest",
"applicationCategory": "CommunicationApplication",
"operatingSystem": "Windows, Linux, macOS",
"url": "https://kst4contest.hamradioonline.de/",
"downloadUrl": "https://github.com/praktimarc/kst4contest/releases/latest",
"description": "KST4Contest combines ON4KST chat, candidate prioritisation, sked planning, AirScout data and logger integration for VHF, UHF and SHF contest operation.",
"license": "https://github.com/praktimarc/kst4contest/blob/main/LICENSE"
}
</script></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
<script src="/assets/js/hero-radio-fx.js?v=1784595000789" defer></script>
</body>
</html>
-538
View File
@@ -1,538 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<praktiKST>
<latestVersion>
<versionNumber>1.411</versionNumber>
<semanticVersion>1.41.1</semanticVersion>
<adminMessage></adminMessage>
<majorChanges>- Score of Top-Priority Candidates in the user section
- Broadcast Messages (To ALL Station in Contest) in seperate Window</majorChanges>
<latestVersionPathOnWebserver>https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1</latestVersionPathOnWebserver>
</latestVersion>
<needUpdateSinceLastVersion>
<filename>nothing</filename>
</needUpdateSinceLastVersion>
<roadmap>
<featurerequest>
<featureDescription>Cyclic update of own name in chat to actual QRG</featureDescription>
<requestDate>2024-02</requestDate>
<state>open</state>
</featurerequest>
<featurerequest>
<featureDescription>Acoustic warnings</featureDescription>
<requestDate>2023-12</requestDate>
<state>added</state>
</featurerequest>
<featurerequest>
<featureDescription>Airscout support</featureDescription>
<requestDate>2023-03</requestDate>
<state>added</state>
</featurerequest>
<featurerequest>
<featureDescription>cyclic beacon messages in public chat</featureDescription>
<requestDate>2023-03</requestDate>
<state>added</state>
</featurerequest>
</roadmap>
<bugsReported>
<bug>
<bugdescription>
stations like ol7c-7 and ol7c-2 will not be marked as worked if only "ol7c"
is in the log. Need to interprete that. May ignore suffix starting with "-"</bugdescription>
<state>solved</state>
</bug>
<bug>
<bugdescription>
worked callsigns table is updated only once after connected to chat
Should be available directly after program started.
But Reset works all time</bugdescription>
<state>open</state>
</bug>
<bug>
<bugdescription>feature request: language-specific strings, maybe based to an xml file
</bugdescription>
<state>open</state>
</bug>
<bug>
<bugdescription>feature request: Macros
</bugdescription>
<state>added</state>
</bug>
<bug>
<bugdescription>feature request:
Automatic elimination of calls already connected from the log
</bugdescription>
<state>added</state>
</bug>
<bug>
<bugdescription>feature request:
Limitation of users beyond a certain QRB
</bugdescription>
<state>added</state>
</bug>
<bug>
<bugdescription>feature request:
User division for QTF perhaps settable based on the antenna lobe
</bugdescription>
<state>added</state>
</bug>
<bug>
<bugdescription>feature request:
Notification of new incoming users</bugdescription>
<state>added</state>
</bug>
<bug>
<bugdescription>feature request:
Evidence of a call which was written but not responded to.
</bugdescription>
<state>added</state>
</bug>
<bug>
<bugdescription>feature request:
direction based reachable-warnings:
if the chatclient detects a realised qso between two stations which both must have
directed their beams to my station, there should be an instant warning to me for
working at least one of the stations at the detected qrg</bugdescription>
<state>open</state>
</bug>
<bug>
<bugdescription>feature request:
implement ASShowpath message to airacout;
by doubleclicking a callsign in the chatmember list, the path and plane state should be
provided by airscout </bugdescription>
<state>open</state>
</bug>
<bug>
<bugdescription>feature request:
make chatlines copyable to the clipboard...</bugdescription>
<state>open</state>
</bug>
<bug>
<bugdescription>feature request:
adding an activity detection for sensing if another chatmembers alive-state</bugdescription>
<state>added</state>
</bug>
<bug>
<bugdescription>On sql error close the connection to make a clean exit</bugdescription>
<state>open</state>
</bug>
<bug>
<bugdescription>SETNAME /MYQRG in options menu doesnt work</bugdescription>
<state>solved</state>
</bug>
<bug>
<bugdescription>Disconnect button does not work (DL5ZK)</bugdescription>
<state>solved</state>
</bug>
<bug>
<bugdescription>Password-wrong error not displayed (DO5SA)</bugdescription>
<state>solved</state>
</bug>
<bug>
<bugdescription>/CQ isnt written on own PMs</bugdescription>
<state>solved</state>
</bug>
<bug>
<bugdescription>rightclick-textsnippets arent available in cq-table</bugdescription>
<state>solved</state>
</bug>
</bugsReported>
<changeLog>
<changedVersionNumber>1.41.1</changedVersionNumber>
<date>2026-07-08</date>
<description>Release v1.41.1</description>
<added></added>
<changed>Hotfix of Bug #56</changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.41.0</changedVersionNumber>
<date>2026-07-01</date>
<description>Release v1.41.0</description>
<added>- Score of Top-Priority Candidates in the user section
- Broadcast Messages (To ALL Station in Contest) in seperate Window</added>
<changed>Migration to Github
- Band and Frequency Detection per Chatmember — KST4Contest identifies which bands or frequencies a station appears active or reachable on based on chat data. This information is integrated into the display and prioritization.
- Band Alert — If a logged station could be of interest on other locally active bands, KST14Contest can alert the user.
- Sked Reminder ALERT — New reminder function for planned Skeds to ensure that agreed contacts are not forgotten during contest operation.
- Skedfail Button — Failed Skeds can be marked; this allows the station to be handled differently in terms of score and workflow.
- Win-Test SKED Push via UDP — Skeds can be passed to Win-Test via a LOCKSKED/ADDSKED/UNLOCKSKED sequence. This feature is configurable in the preferences and disabled by default.
- Win-Test Broadcast Defaults and SKed Workflow — Improved broadcast addresses, frequency formats, mode selection, and notes including locator/azimuth.
- PSTRotator Interface — KST4Contest received an interface to PSTRoter to better integrate antenna direction and rotator workflow.
- QSO /-Callsign Sniffer — Callsigns or QSO-relevant information can be detected and utilized for further workflow.
- Chat History at Startup — Historical chat data can be loaded upon startup, allowing the client to quickly gain relevant context for scoring, activity, and station identification.
- Station Map / Map View — New map view with station markers, your own location, locator/Maidenhead reference, connection line, and antenna/beam representation.
- Local Leaflet /-TileProxy Backend — The map function utilizes embedded Leaflet resources and a local TileProxy approach, making the map more portable and independent.
- Maidenhead /-Grid Overlay — The map can display locator/grid information; recently added Grid-Square coloring.
- Path /-Terrain /-Horizon Profile — Map documentation describes a path profile with terrain, Fresnel, and horizon information for the selected link.
- Tropo /-AS Reachability Filters — New filters and sorting were added for Tropospheric reachability, AirScoop availability, large locator fields, and priority.
- Reachability Band Selector — Added a band selection for range/Troposphere calculations.
- 4-Char Locator Filter — Locator evaluations can be reduced to 4-character fields; this improves overview and performance.
- KST Testserver Support — A KST server test server was added into the Source.
@praktimarc and @Rsclub22</changed>
<fixed>- [BUG]
- [BUG] Programmstart kann ohne selektieren keine Nachrichten senden
- KST4Contest crashes after to many messages or to much RAM Usage
- Scaling Problem at 1280x800 screen resolution</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.40</changedVersionNumber>
<date>2026-02-16</date>
<description>Much changes and fixes</description>
<added>
- win-test support (not deactivable yet, beta)
- QSO sniffer which filters changable callsigns lists messages to your pm window
- Chatmember Score system. Each chatmember gets a score depending to antenna direction, activity time,
activity msgcount, positive signals, active bands, active frequencies, his sked direction (degrees),
self handed rate, sked-made-rate and some others
- Chatmember Band and frequency recognition: now we list all active bands of each other chatmember
- Band alert for logged stations
- Sked reminder ALERT with automatic messages in 2+1 / 5+2+1 / 10+5+2+1 minutes
- PSTRotator interface
- AirPlane Timeline
- Skedfail button in furtherinfo panel
- Wintest logsynch interface can now be activated and deactivated in the preferences
- load chat history on startup
</added>
<changed>
- added save option: ServerDNS / Port
- added save option: activate/deactivate pstRotator interface
- added save option: activate/deactivate wintest interface
- added save option: callsign sniffer
- added save option: boolean GUI_darkModeActiveByDefault = false
- added AP notes at the internal DX cluster spots
- Generic Autoanswer fires maximum once in 45 seconds per rx callsign
</changed>
<fixed>
- Userlist sort automatically on new member signon
- Posonpill-messages now kills only exactly one Client instance (bugreport 9A2HM)
- Crash of wtkst by disconnection of kst4Contest
- wtKST crashes when kst4contest is running (many reporters)
- wintest: logged per band excludes pre worked band (bugreport SM6VTZ)
- PSTRotator SOCKET Close on DISCONNECT BUTTON works now
- AirScout Map had not been updated (bugreport SM0RJV)
- QTFDefault had been not saved correctly (bugreport SM0RJV)
- Show path in AS button worked only with fixed server Strings (bugreport 9A2HM)
- Version number had been displayed wrong
- suffixes like /p, -2, etc. problem now fixed everywhere
- Dark mode: passing for qrg fields fix
</fixed>
<removed>
- No more date display. Not needed. Saving space, only time is important
</removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.31</changedVersionNumber>
<date>2025-12-13</date>
<description>Much changes and a hotfix</description>
<added>
- win-test support (not deactivable yet, beta)
- pstRotator support (not deactivable yet, beta)
- airScout fix (calculations depends to cpu load now)
- QSO sniffer
</added>
<changed>
- DNS from www.on4kst.info to www.on4kst.org (hotfix, now configurable in preferences)
</changed>
<fixed>
- Endless loop in error case lets freeze the client
- PSTRotator SOCKET Close on DISCONNECT BUTTON
- Dark mode: passing for qrg fields fix
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.266</changedVersionNumber>
<date>2025-10-03</date>
<description>Airscout interface did not work with multi-signons</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>
Fixed by using raw callsign (without -2 suffix etc.) when communicating with AirScout.
Thank you very much for testing this, Kreso 9A2HM!
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.265</changedVersionNumber>
<date>2025-09-28</date>
<description>Direction buttons stay now colored, if activated</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>The buttons stay now colored, if activated</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.264</changedVersionNumber>
<date>2025-08-02</date>
<description>Changed the callsign recognition pattern for Simplelogfile</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>
Callsigns like S53CC, S51A etc. are now correctly marked as worked in SimpleLogFile.txt.
(bugreport Boris, S53CC)
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.263</changedVersionNumber>
<date>2025-06-08</date>
<description>Airscout communication and Loginname</description>
<added>nothing</added>
<changed>
Only entries with QRB lower than max-QRB are sent to AirScout in 60s intervals (was 12s for all).
</changed>
<fixed>
- AS communication message count hugely reduced
- Name in chat is now saveable
- Fixed AS client name (was hard-wired to "KST")
73 / DO5AMF
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.262</changedVersionNumber>
<date>2025-05-21</date>
<description>Freezes caused by getting messages before user login should be fixed now</description>
<added>nothing</added>
<changed>nothing</changed>
<fixed>
ON4KST delivers messages of stations not yet logged in. That caused a freeze at the message
processing engine which is now fixed.
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.26</changedVersionNumber>
<date>2025-05</date>
<description>Login to multiple Channels via single signon / spend some colors</description>
<added>
1. UI: Dark mode. Switch in "Window -> use dark mode"
2. Usage of two Chatcategories at the same time
3. opposite station multi-callsign login-tagging
73 / DO5AMF
</added>
<changed>
- Coloring mechanic of the software. Modify colors via css by yourself...
</changed>
<fixed>
- Station tagging fixed completely
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.251</changedVersionNumber>
<date>2025-02</date>
<description>BUGFIX of 1.25, tnx Steve Clements!</description>
<added>
- Steve spotted a problem in udp broadcast spot info reading, its now fixed!
73 / DO5AMF
</added>
<changed></changed>
<fixed>- Station tagging</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.25</changedVersionNumber>
<date>2025-02</date>
<description>Wishlist-time</description>
<added>
- New configuration Tab: Messagehandling (auto-answering, CQ qrg auto-answer)
- Coloured lines: new pm rows appear in red, fade rainbow-like to white over 30s (tnx Gianluca)
73 / DO5AMF
</added>
<changed></changed>
<fixed>
- Users with suffixes like "-2 and -70" are now correctly marked as worked
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.24</changedVersionNumber>
<date>2024-11</date>
<description>Wishlist-time</description>
<added>
- Button to show qrz.com profile of a selected station
- Button to show qrzcq.com profile of a selected station
</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.23</changedVersionNumber>
<date>2024-10</date>
<description>DXCluster Server is now implemented</description>
<added>
- DXCluster Server (tnx OMAAO): KST4Contest now provides a DXCluster server to feed
your log client with station-reachable warnings.
</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.22</changedVersionNumber>
<date>2024-05</date>
<description>Increase usability, fixed AS button</description>
<added>- Variables (tnx OMAAO): MYLOCATORSHORT, MYQRGSHORT, QRZNAME</added>
<changed>- Sendtext-field focus on callsign click</changed>
<fixed>
- Worked-station-filter (tnx Gianluca): filter is now live
- Chatters list sorting by QRB (tnx Alessandro): fixed to numeric sort
- Airscout showpath button now maximizes AS and shows the path
</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.21</changedVersionNumber>
<date>2024-04</date>
<description>Increase usability</description>
<added></added>
<changed>
- Window sizes and divider positions are now stored and restored on next startup
- Filters section is now a flowpane for lower resolutions
</changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.2</changedVersionNumber>
<date>2024-04</date>
<description>Increase usability</description>
<added>
- Selectable bands
- Unworkable tags for each callsign per band
- QTF-Arrow on the "show path in AS" button
</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.1</changedVersionNumber>
<date>2024-03</date>
<description>Increased workflow</description>
<added>
- Reachable warning function
- Variables FIRSTAP and SECONDAP for predefined texts
- Marking new connected stations (bold) and away-state (brackets)
- Activity-timer since last message of a chatter
</added>
<changed>- QRB/QTF info in pm-window</changed>
<fixed>
- Behaviour of messagefilter-radiobutton corrected (tnx Gian)
- Error sending macros on selected stations in pm window (tnx Gian)
</fixed>
<removed>- boot sound</removed>
</changeLog>
<changeLog>
<changedVersionNumber>1.0</changedVersionNumber>
<date>2024-02</date>
<description>First stable release</description>
<added>
- audio notification support
- update-available warning
- changelog displaying
- known-bugs displaying
- message filters for selected station
- stationlist-filters (show by qtf / textstring, hide by qrb, qtf, workedstate)
- 10 macros (textsnippets available by hitting strg+numberkey)
</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.9</changedVersionNumber>
<date>2024-02</date>
<description>Now the chat is disconnectable without closing it</description>
<added></added>
<changed></changed>
<fixed>disconnect-button, Textsnippets in CQ Window, "/CQ" at click on own sendtexts,
setname "qrg" works now</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.8</changedVersionNumber>
<date>2023-10</date>
<description>savesettings function and disconnect function added. MYCALL-Highlighting fixed</description>
<added>savesettings function, disconnect function</added>
<changed></changed>
<fixed>MYCALL-Highlighting</fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.7</changedVersionNumber>
<date>2023-07</date>
<description>Internal database for worked stations, shortcut and textsnippet editor</description>
<added>database, shortcut-editor, textsnipped-editor</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.6</changedVersionNumber>
<date>2023-07</date>
<description>Added periodically sent beacon function</description>
<added>beacon to public channel</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.5</changedVersionNumber>
<date>2023-02</date>
<description>Airscout interface, UDP interfaces for worked-station data and TRX info, shortcuts</description>
<added>Airscout-interface, packetreceiver for ucxlog, n1mm, qartest</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.4</changedVersionNumber>
<date>2022-12</date>
<description>Worked info by logfile via universal pattern matching logfile interpreter</description>
<added>simple-logfile</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.3</changedVersionNumber>
<date>2022-10</date>
<description>Added a GUI, chat was text based only</description>
<added>Graphical user interface</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.2</changedVersionNumber>
<date>2022-10</date>
<description>Pattern matching system for frequency information, frequency collection per chatmember</description>
<added>frequency-extraction for every callsign</added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.15</changedVersionNumber>
<date>2022-09</date>
<description>Researched the Chatprotocol and developed the base communication with the chatserver</description>
<added></added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
<changeLog>
<changedVersionNumber>0.1</changedVersionNumber>
<date>2022-09</date>
<description>Primary architecture of the chat, message bus system for handling the message queues</description>
<added></added>
<changed></changed>
<fixed></fixed>
<removed></removed>
</changeLog>
</praktiKST>
-135
View File
@@ -1,135 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Legal Notice | KST4Contest</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="Legal notice for the KST4Contest website.">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/legal-notice/">
<meta property="og:type" content="website">
<meta property="og:title" content="Legal Notice | KST4Contest">
<meta property="og:description" content="Legal notice for the KST4Contest website.">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/legal-notice/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Legal Notice | KST4Contest">
<meta name="twitter:description" content="Legal notice for the KST4Contest website.">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="hero">
<p class="badge">Legal Notice</p>
<h1>Legal Notice</h1>
<p class="lead">Information according to § 5 DDG.</p>
</section>
<section class="section narrow">
<article class="card content-card">
<h2>Provider</h2>
<p>
Marc Fröhlich<br>
Rheingoldstr. 6<br>
68199 Mannheim<br>
Germany
</p>
<h2>Contact</h2>
<p>
Email: <a href="mailto:praktimarc+kst4contest@gmail.com">praktimarc+kst4contest@gmail.com</a>
</p>
<h2>Responsible for content</h2>
<p>
Marc Fröhlich<br>
Rheingoldstr. 6<br>
68199 Mannheim<br>
Germany
</p>
<h2>Project</h2>
<p>
KST4Contest is an open-source ON4KST contest client.
Source code and releases are available on
<a href="https://github.com/praktimarc/kst4contest">GitHub</a>.
</p>
</article>
</section></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

@@ -1,209 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ manual.title }} | KST4Contest Manual</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/manual/de/airscout-integration/">
<meta property="og:type" content="website">
<meta property="og:title" content="{{ manual.title }} | KST4Contest Manual">
<meta property="og:description" content="KST4Contest manual page: {{ manual.title }}">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/manual/de/airscout-integration/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="section">
<div class="card manual-content">
<p><a href="/manual/de/">← Back to manual overview</a></p>
<h1>AirScout-Integration</h1>
<h1 id="airscout-integration" tabindex="-1"><a class="header-anchor" href="#airscout-integration">AirScout-Integration</a></h1>
<blockquote>
<p>🇬🇧 <a href="/manual/en/airscout-integration/">English version</a> | 🇩🇪 Du liest gerade die deutsche Version</p>
</blockquote>
<p>AirScout (von DL2ALF) ist ein Programm zur Erkennung von Flugzeugen für den Aircraft-Scatter-Betrieb. KST4Contest ist eng mit AirScout integriert und zeigt reflektierbare Flugzeuge direkt in der Benutzerliste an.</p>
<blockquote>
<p><strong>Aircraft Scatter</strong> ermöglicht sehr weitreichende Verbindungen auf VHF und höher auch für Stationen mit geringer Höhe über NN oder ungünstigen topografischen Verhältnissen.</p>
</blockquote>
<hr>
<h2 id="airscout-herunterladen" tabindex="-1"><a class="header-anchor" href="#airscout-herunterladen">AirScout herunterladen</a></h2>
<p>Download von AirScout:</p>
<ul>
<li><a href="http://airscout.eu/index.php/download">http://airscout.eu/index.php/download</a></li>
</ul>
<hr>
<h2 id="flugzeugdaten-feeds-(adsb)" tabindex="-1"><a class="header-anchor" href="#flugzeugdaten-feeds-(adsb)">Flugzeugdaten-Feeds (ADSB)</a></h2>
<p>Öffentliche Flugzeugdaten-Feeds im Internet sind oft unzuverlässig und begrenzt nutzbar. Eine empfohlene Alternative bietet <strong>OV3T (Thomas)</strong> mit einem dedizierten ADSB-Feed-Dienst:</p>
<ul>
<li><a href="https://airscatter.dk/">https://airscatter.dk/</a></li>
<li><a href="https://www.facebook.com/groups/825093981868542">https://www.facebook.com/groups/825093981868542</a></li>
</ul>
<p>Für diesen Dienst ist ein Account erforderlich. Bitte eine Spende für Thomas in Betracht ziehen der Server-Betrieb ist nicht kostenlos!</p>
<hr>
<h2 id="airscout-einrichten" tabindex="-1"><a class="header-anchor" href="#airscout-einrichten">AirScout einrichten</a></h2>
<h3 id="schritt-1%3A-adsb-feed-in-airscout-konfigurieren" tabindex="-1"><a class="header-anchor" href="#schritt-1%3A-adsb-feed-in-airscout-konfigurieren">Schritt 1: ADSB-Feed in AirScout konfigurieren</a></h3>
<ol>
<li>AirScout starten.</li>
<li>In den AirScout-Einstellungen den OV3T-Feed-Account eintragen (Benutzername, Passwort, URL).</li>
</ol>
<p><img src="/manual/assets/as_plane_feed_1.png" alt="AirscoutSchritt1">
<img src="/manual/assets/as_plane_feed_2.png" alt="AirscoutSchritt2"></p>
<ol start="3">
<li>Verbindung testen.</li>
</ol>
<h3 id="schritt-2%3A-udp-kommunikation-f%C3%BCr-kst4contest-aktivieren" tabindex="-1"><a class="header-anchor" href="#schritt-2%3A-udp-kommunikation-f%C3%BCr-kst4contest-aktivieren">Schritt 2: UDP-Kommunikation für KST4Contest aktivieren</a></h3>
<p>In AirScout die UDP-Schnittstelle aktivieren:</p>
<ul>
<li>In den AirScout-Einstellungen die entsprechende Checkbox aktivieren (nur eine Checkbox notwendig).</li>
<li>Standard-Ports nicht ändern, wenn kein besonderer Grund vorliegt.</li>
</ul>
<h3 id="schritt-3%3A-kst4contest-einstellungen" tabindex="-1"><a class="header-anchor" href="#schritt-3%3A-kst4contest-einstellungen">Schritt 3: KST4Contest-Einstellungen</a></h3>
<p>In den KST4Contest-Preferences → <strong>AirScout Settings</strong>:</p>
<ul>
<li>AirScout-Kommunikation aktivieren</li>
<li>IP und Port auf Standardwerte lassen (sofern nicht geändert)</li>
</ul>
<p><img src="/manual/assets/as_plane_feed_3.png" alt="AirscoutSchritt3">{ width=85% }</p>
<hr>
<h2 id="kommunikation-zwischen-kst4contest-und-airscout-(ab-v1.263)" tabindex="-1"><a class="header-anchor" href="#kommunikation-zwischen-kst4contest-und-airscout-(ab-v1.263)">Kommunikation zwischen KST4Contest und AirScout (ab v1.263)</a></h2>
<p><strong>Verbesserung in v1.263</strong>: KST4Contest sendet nur noch Stationen an AirScout, deren QRB (Entfernung) kleiner als das eingestellte <strong>Maximum-QRB</strong> ist. Das Abfrageintervall wurde von 12 Sekunden auf <strong>60 Sekunden</strong> verlängert.</p>
<p><strong>Vorteile:</strong></p>
<ul>
<li>Deutlich weniger Berechnungsaufwand für AirScout</li>
<li>Deutlich weniger Nachrichtenverkehr</li>
<li>Das Tracking-Problem mit dem „Show Path in AirScout&quot;-Button wurde dadurch deutlich verbessert</li>
<li>Weniger Rechenleistung insgesamt</li>
</ul>
<p>Außerdem: Der Name des KST4Contest-Clients und des AirScout-Servers war früher hartcodiert (<code>KST</code> und <code>AS</code>). Ab v1.263 werden die in den Preferences eingetragenen Namen verwendet.</p>
<hr>
<h2 id="mehrere-kst4contest-instanzen-und-airscout" tabindex="-1"><a class="header-anchor" href="#mehrere-kst4contest-instanzen-und-airscout">Mehrere KST4Contest-Instanzen und AirScout</a></h2>
<blockquote>
<p><strong>Achtung</strong>: Wenn mehrere KST4Contest-Instanzen gleichzeitig betrieben werden und bei beiden die AirScout-Kommunikation aktiviert ist, antwortet AirScout <strong>an beide Instanzen</strong>.</p>
</blockquote>
<p>Das ist unproblematisch, wenn:</p>
<ul>
<li>Beide Instanzen denselben Locator verwenden, <strong>oder</strong></li>
<li>Beide Instanzen unterschiedliche Login-Rufzeichen haben.</li>
</ul>
<p>Andernfalls kann es zu fehlerhaften AP-Daten kommen.</p>
<hr>
<h2 id="ap-spalte-in-der-benutzerliste" tabindex="-1"><a class="header-anchor" href="#ap-spalte-in-der-benutzerliste">AP-Spalte in der Benutzerliste</a></h2>
<p>Nach der Einrichtung erscheint in der Benutzerliste eine <strong>AP-Spalte</strong> mit bis zu zwei reflektierbaren Flugzeugen pro Station.</p>
<p>Beispiel-Darstellung:</p>
<table>
<thead>
<tr>
<th>Station</th>
<th>AP-Info</th>
</tr>
</thead>
<tbody>
<tr>
<td>DF9QX</td>
<td>2 Planes: 0 min / 0 min, je 100%</td>
</tr>
<tr>
<td>F5DYD</td>
<td>2 Planes: 14 min / 31 min, je 50%</td>
</tr>
</tbody>
</table>
<p>Die AP-Informationen sind auch im <strong>Privatnachrichten-Fenster</strong> verfügbar.</p>
<p>Die Prozentzahl gibt das Reflexionspotenzial an (Größe des Flugzeugs, Höhe, Entfernung).</p>
<hr>
<h2 id="ap-variablen-in-nachrichten" tabindex="-1"><a class="header-anchor" href="#ap-variablen-in-nachrichten">AP-Variablen in Nachrichten</a></h2>
<p>Die Flugzeugdaten können direkt in Nachrichten eingefügt werden:</p>
<ul>
<li><code>FIRSTAP</code> → z. B. <code>a very big AP in 1 min</code></li>
<li><code>SECONDAP</code> → z. B. <code>Next big AP in 9 min</code></li>
</ul>
<p>Details: <a href="Makros-und-Variablen#variablen">Makros und Variablen</a></p>
<hr>
<h2 id="%E2%80%9Eshow-path-in-airscout%22-button" tabindex="-1"><a class="header-anchor" href="#%E2%80%9Eshow-path-in-airscout%22-button">„Show Path in AirScout&quot;-Button</a></h2>
<p>In der Benutzerliste gibt es einen Button mit einem Pfeil, der die Richtung (QTF) zur ausgewählten Station anzeigt. Ein Klick maximiert AirScout und zeigt den Pfad mit reflektierbaren Flugzeugen zum ausgewählten Gesprächspartner.</p>
</div>
</section></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>
@@ -1,211 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{ manual.title }} | KST4Contest Manual</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#08110b">
<meta name="description" content="KST4Contest manual page: {{ manual.title }}">
<link rel="canonical" href="https://kst4contest.hamradioonline.de/manual/de/benutzeroberflaeche/">
<meta property="og:type" content="website">
<meta property="og:title" content="{{ manual.title }} | KST4Contest Manual">
<meta property="og:description" content="KST4Contest manual page: {{ manual.title }}">
<meta property="og:url" content="https://kst4contest.hamradioonline.de/manual/de/benutzeroberflaeche/">
<meta property="og:site_name" content="KST4Contest">
<meta property="og:locale" content="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">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ manual.title }} | KST4Contest Manual">
<meta name="twitter:description" content="KST4Contest manual page: {{ manual.title }}">
<meta name="twitter:image" content="https://kst4contest.hamradioonline.de/assets/social/kst4contest-og.png">
<link rel="stylesheet" href="/assets/css/main.css?v=1784595000789">
<link rel="alternate" type="application/rss+xml" title="KST4Contest News" href="/feed.xml">
</head>
<body>
<div class="site-bg" aria-hidden="true"></div>
<header class="site-header">
<a class="brand" href="/" aria-label="KST4Contest home">
<span class="brand-mark" aria-hidden="true"></span>
<span>
<strong>KST4Contest</strong>
<small>ON4KST Contest Client</small>
</span>
</a>
<nav class="site-nav" aria-label="Main navigation">
<a href="/">Home</a>
<a href="/features/">Features</a>
<a href="/screenshots/">Screenshots</a>
<a href="/manual/">Manual</a>
<a href="/news/">News</a>
<a href="/roadmap/">Roadmap</a>
<a href="/about/">About</a>
<a href="/faq/">FAQ</a>
<a href="/support/">Support</a>
<a class="nav-cta" href="/download/">Download</a>
</nav>
</header>
<main>
<section class="section">
<div class="card manual-content">
<p><a href="/manual/de/">← Back to manual overview</a></p>
<h1>Benutzeroberfläche</h1>
<h1 id="benutzeroberfl%C3%A4che" tabindex="-1"><a class="header-anchor" href="#benutzeroberfl%C3%A4che">Benutzeroberfläche</a></h1>
<blockquote>
<p>🇬🇧 <a href="/manual/en/user-interface/">English version</a> | 🇩🇪 Du liest gerade die deutsche Version</p>
</blockquote>
<h2 id="verbinden-mit-dem-chat" tabindex="-1"><a class="header-anchor" href="#verbinden-mit-dem-chat">Verbinden mit dem Chat</a></h2>
<ol>
<li>Im Einstellungsfenster eine <strong>Chat-Kategorie</strong> auswählen (z. B. 144 MHz VHF, 432 MHz UHF, …).</li>
<li><strong>Connect</strong>-Button klicken.</li>
<li>Warten bis die Verbindung aufgebaut ist.</li>
</ol>
<blockquote>
<p>Trennen und Neu-Verbinden ist nur über das Einstellungsfenster möglich. Es empfiehlt sich daher, das Einstellungsfenster geöffnet zu lassen.</p>
</blockquote>
<hr>
<h2 id="hauptfenster-%C3%BCberblick" tabindex="-1"><a class="header-anchor" href="#hauptfenster-%C3%BCberblick">Hauptfenster-Überblick</a></h2>
<p>Das Hauptfenster besteht aus mehreren Bereichen:</p>
<h3 id="pm-fenster-(oben-links)" tabindex="-1"><a class="header-anchor" href="#pm-fenster-(oben-links)">PM-Fenster (oben links)</a></h3>
<p>Zeigt alle empfangenen <strong>Privatnachrichten</strong> sowie abgefangene öffentliche Nachrichten, die das eigene Rufzeichen enthalten. Neue Nachrichten erscheinen in <strong>Rot</strong> und faden alle 30 Sekunden über Gelb bis Weiß ab.</p>
<h3 id="benutzerliste-(chat-members)" tabindex="-1"><a class="header-anchor" href="#benutzerliste-(chat-members)">Benutzerliste (Chat Members)</a></h3>
<p>Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfiguration):</p>
<table>
<thead>
<tr>
<th>Spalte</th>
<th>Inhalt</th>
</tr>
</thead>
<tbody>
<tr>
<td>Call</td>
<td>Rufzeichen der Station</td>
</tr>
<tr>
<td>Name</td>
<td>Name aus dem Chat-Namenfeld</td>
</tr>
<tr>
<td>Loc</td>
<td>Maidenhead-Locator</td>
</tr>
<tr>
<td>QRB</td>
<td>Entfernung in km</td>
</tr>
<tr>
<td>QTF</td>
<td>Richtung in Grad</td>
</tr>
<tr>
<td>QRG</td>
<td>Automatisch erkannte Frequenz</td>
</tr>
<tr>
<td>AP</td>
<td>AirScout-Flugzeugdaten (wenn aktiv)</td>
</tr>
<tr>
<td>Band-Farben</td>
<td>Worked/NOT-QRV-Status pro Band</td>
</tr>
</tbody>
</table>
<p><strong>Sortierung</strong>: Klick auf Spaltenköpfe. QRB-Sortierung arbeitet numerisch (ab v1.22 korrigiert).</p>
<h3 id="sendfeld" tabindex="-1"><a class="header-anchor" href="#sendfeld">Sendfeld</a></h3>
<p>Texteingabe für ausgehende Nachrichten. Nach Klick auf ein Rufzeichen in der Benutzerliste erhält das Sendfeld automatisch den Fokus sofort tippen ohne Doppelklick (ab v1.22).</p>
<h3 id="myqrg-feld" tabindex="-1"><a class="header-anchor" href="#myqrg-feld">MYQRG-Feld</a></h3>
<p>Rechts neben dem Sendbutton. Zeigt die aktuelle eigene QRG an, kann auch manuell eingetragen werden.</p>
<h3 id="myqtf-feld-(f%C3%BCr-v1.3)" tabindex="-1"><a class="header-anchor" href="#myqtf-feld-(f%C3%BCr-v1.3)">MYQTF-Feld <em>(für v1.3)</em></a></h3>
<p>Eingabefeld für die aktuelle Antennenrichtung. Wird für die geplante <code>MYQTF</code>-Variable verwendet.</p>
<hr>
<h2 id="filter" tabindex="-1"><a class="header-anchor" href="#filter">Filter</a></h2>
<p>Die Filter-Leiste (ab v1.21 als Flowpane für kleine Bildschirme):</p>
<ul>
<li><strong>Show only QTF</strong>: Richtungsfilter aktivieren (Buttons N/NE/E/… oder Grad-Eingabe)</li>
<li><strong>Show only QRB [km] &lt;=</strong>: Entfernungsfilter aktivieren (Toggle-Button)</li>
<li><strong>Hide Worked [Band]</strong>: Gearbeitete Stationen pro Band ausblenden (je ein Toggle pro Band)</li>
<li><strong>Hide NOT-QRV [Band]</strong>: NOT-QRV-markierte Stationen pro Band ausblenden</li>
</ul>
<hr>
<h2 id="stationsinfo-panel-(further-info)" tabindex="-1"><a class="header-anchor" href="#stationsinfo-panel-(further-info)">Stationsinfo-Panel (Further Info)</a></h2>
<p>Rechts unten: Zeigt alle Nachrichten einer ausgewählten Station (CQ-Nachrichten und PMs in einem Panel). Ein Nachrichtenfilter lässt sich über den Standard-Filter in den Preferences vorbelegen.</p>
<p>Hier können auch <strong>Sked-Erinnerungen</strong> aktiviert werden.</p>
<hr>
<h2 id="priorit%C3%A4tsliste" tabindex="-1"><a class="header-anchor" href="#priorit%C3%A4tsliste">Prioritätsliste</a></h2>
<p>Zeigt die vom Score-Service berechneten Top-Kandidaten. Aktualisiert sich automatisch im Hintergrund basierend auf Richtung, Entfernung und AP-Verfügbarkeit.</p>
<hr>
<h2 id="cluster-%26-qso-der-anderen" tabindex="-1"><a class="header-anchor" href="#cluster-%26-qso-der-anderen">Cluster &amp; QSO der anderen</a></h2>
<p>Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen interessant in ruhigeren Phasen.</p>
<hr>
<h2 id="men%C3%BC" tabindex="-1"><a class="header-anchor" href="#men%C3%BC">Menü</a></h2>
<h3 id="window" tabindex="-1"><a class="header-anchor" href="#window">Window</a></h3>
<ul>
<li><strong>Use Dark Mode</strong> (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.</li>
</ul>
<hr>
<h2 id="fenstergr%C3%B6%C3%9Fen-und-divider" tabindex="-1"><a class="header-anchor" href="#fenstergr%C3%B6%C3%9Fen-und-divider">Fenstergrößen und Divider</a></h2>
<p>Ab <strong>v1.21</strong> werden beim Klick auf <strong>„Save Settings&quot;</strong> auch Fenstergrößen und Divider-Positionen aller Panels in der Konfigurationsdatei gespeichert und beim nächsten Start wiederhergestellt.</p>
<p>Bei Problemen mit der Darstellung: Konfigurationsdatei löschen → KST4Contest erstellt neue Standardwerte.</p>
<hr>
<h2 id="tipps-zur-bedienung" tabindex="-1"><a class="header-anchor" href="#tipps-zur-bedienung">Tipps zur Bedienung</a></h2>
<ul>
<li><strong>Einstellungsfenster geöffnet lassen</strong>: Schneller Zugriff auf Beacon-Aktivierung/Deaktivierung.</li>
<li><strong>Rechtsklick in der Benutzerliste</strong>: Öffnet das Snippet-Menü und weitere Aktionen (QRZ.com-Profil, NOT-QRV-Tags setzen).</li>
<li><strong>Enter aus dem Chat heraus</strong>: Wenn im Sendfeld Text steht, sendet Enter direkt auch wenn der Fokus woanders liegt.</li>
<li><strong>Beacon stoppen</strong>: Beim Scannen von Frequenzen den Beacon ausschalten, damit der Chat nicht mit Meldungen überflutet wird.</li>
</ul>
</div>
</section></main>
<footer class="site-footer">
<div class="footer-grid">
<div>
<strong>KST4Contest</strong>
<p>ON4KST chat, candidate selection and sked planning for VHF, UHF and SHF contest operation.</p>
</div>
<div>
<strong>Project</strong>
<p><a href="/features/">Features</a></p>
<p><a href="/download/">Download</a></p>
<p><a href="/manual/">Manual</a></p>
<p><a href="/roadmap/">Roadmap</a></p>
</div>
<div>
<strong>Community</strong>
<p><a href="https://github.com/praktimarc/kst4contest">GitHub Repository</a></p>
<p><a href="https://github.com/praktimarc/kst4contest/issues">Issues</a></p>
<p><a href="/contact/">Contact</a></p>
<p><a href="/support/">❤️ Support KST4Contest</a></p>
</div>
<div>
<strong>Legal</strong>
<p><a href="/legal-notice/">Legal Notice</a></p>
<p><a href="/privacy/">Privacy Policy</a></p>
</div>
</div>
</footer>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More