10 Commits
Author SHA1 Message Date
Marc Froehlich 2828e7ef80 fix: make AirScout and map propagation band-aware by resolving realistic per-station QRGs to canonical AirScout bands, using one shared watchlist, honoring the operator-selected band for Calc selected and map path analysis, and replacing the obsolete 430 MHz fallback with 432 MHz (fixes #67) and parts of #74 2026-08-08 01:53:04 +02:00
Marc Froehlich b0cc5ee9ba updated en-manuals for skeds, user interface and features 2026-08-07 23:59:59 +02:00
Marc Froehlich b00e4c1e3d fixed worked-flag in the qso-of-the-other-table 2026-08-07 23:43:04 +02:00
Marc Froehlich b3ca684f04 updated website for sked and timeline features 2026-08-07 23:38:13 +02:00
Marc Froehlich 1de9673d12 uploaded 2 manual pictures for sked and timeline 2026-08-07 23:34:57 +02:00
Marc Froehlich c119f28b30 Uploaded sked, timeline, priority candidates and wintest connection manuals 2026-08-07 23:13:40 +02:00
Marc Froehlich 6a7f07c62e fix(wintest): replace unreliable band-limited AUTO sked mode detection with explicit SSB/CW selection and show exact KST callsigns in the timeline and fixed (#50 ensured Filter-Reset) 2026-08-07 22:56:24 +02:00
Marc Froehlich 4f574ebec6 fix(wintest): add band-aware sked QRG resolution, preserve portable callsigns, strip KST suffixes and correct ADDSKED timestamps 2026-08-07 22:43:17 +02:00
Marc Froehlich 7ce31e110b fix(wintest): add band-aware sked QRG resolution, preserve portable callsigns, strip KST suffixes and correct ADDSKED timestamps 2026-08-07 22:28:51 +02:00
Marc Froehlich 92804a622a Corrected typo error in MessagebusmanagementThread. Solves (#48) 2026-08-07 22:08:19 +02:00
31 changed files with 2864 additions and 699 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+112 -5
View File
@@ -132,7 +132,24 @@ Im selben Bereich wird der aktuelle **Priority score** der ausgewählten Station
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 können ein Sked und die zugehörigen **Sked-Erinnerungen / Wecker** angelegt werden. Ein nahender Sked erhöht den Priority Score zeitabhängig; unmittelbar vor dem Termin erhält er eine sehr hohe Gewichtung.
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.
---
@@ -160,16 +177,106 @@ Herleitung und Grenzen: [Prioritätsscore und Prioritätsliste](de-Funktionen#pr
---
## Cluster & QSO der anderen
## Stationskarte
Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen interessant in ruhigeren Phasen.
Die Stationskarte kann auf zwei Wegen geöffnet werden:
- **Windows → Show / hide station map** öffnet oder schließt das Kartenfenster.
- **Show on map** im **Further Info**-Bereich öffnet die Karte und zentriert sie auf die ausgewählte Station.
Die Karte zeigt die Stationen, die auch nach Anwendung der aktuellen Benutzerlistenfilter noch sichtbar sind. Ein Hinweis in der Kopfzeile zeigt an, wenn eine gefilterte Ansicht aktiv ist.
![Stationskarte mit ausgewählter Station und eingeblendeter Streckenanalyse](station_map_path_analysis.png)
Ein einzelner Stationsmarker kann direkt angeklickt werden. KST4Contest übernimmt die Station daraufhin als aktuelle Auswahl, scrollt die Benutzerliste zum passenden Chatmember und aktualisiert den **Further Info**-Bereich.
Marker, die bei der aktuellen Zoomstufe zu dicht beieinanderliegen, werden als Cluster mit einer Stationsanzahl angezeigt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus.
Für die ausgewählte Station erscheinen rechts unter **Selected station**:
- Rufzeichen,
- Locator,
- QRB und QTF,
- erkannte aktive Bänder,
- gegebenenfalls `B+` für eine offene Bandmöglichkeit und
- die zuletzt bekannten QRGs.
**Trigger cluster spot** sendet für die ausgewählte Station einen einzelnen Spot an die mit dem integrierten DX-Cluster-Server verbundenen Logprogramme. Die Schaltfläche setzt deshalb einen aktivierten Cluster-Server und mindestens einen verbundenen Client voraus.
Unterhalb der Karte befindet sich das Höhenprofil. Rechts werden die dazugehörigen Detailwerte angezeigt, unter anderem:
- verwendete Datenquelle und Anzahl der Höhenpunkte,
- Analysefrequenz,
- Erdkrümmungs- beziehungsweise Refraktionsmodell,
- Radio- und Geländehorizont,
- Fresnel-Freiheit,
- erkannte Hindernisse,
- Link-Budget,
- geschätzter Empfangspegel und
- eine zusammenfassende Pfadbewertung.
Mit **Hide path analysis** werden das Profil unterhalb der Karte und die ausführlichen Analysewerte rechts gemeinsam ausgeblendet. Der Kartenbereich erhält dadurch mehr Platz.
![Stationskarte mit ausgeblendeter Pfadanalyse](station_map_compact.png)
Der Hinweis **Path analysis is hidden** bleibt zusammen mit **Show path analysis** sichtbar. Die Funktion kann daher ohne Umweg wieder eingeschaltet werden. Der Zustand wird gespeichert.
Der Divider zwischen Karte und Detailbereich lässt sich horizontal verschieben. Bei schmalem Detailbereich werden längere Angaben umgebrochen; falls die Höhe nicht ausreicht, erscheint dort eine vertikale Scrollleiste.
Ausführliche Herleitung und Grenzen: [Stationskarte und Streckenanalyse](de-Funktionen#stationskarte-und-streckenanalyse-ab-v141)
---
## Globale Nachrichtentabs und Monitorfenster
Der untere Bereich des Hauptfensters enthält drei globale Nachrichtentabs. Ihr Inhalt ist nicht von der aktuell in der Benutzerliste ausgewählten Station abhängig.
| Tab | Inhalt |
|---|---|
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
| **DXCluster messages** | Über ON4KST empfangene DX-Cluster-Meldungen |
| **QSO of the other** | Gerichtete Nachrichten zwischen zwei anderen Stationen |
![Globale Nachrichtentabs im Hauptfenster](global_message_tabs.png)
Im Tab **QSO of the other** werden Absender und Empfänger getrennt dargestellt. Die Spalten **Last QRG TX** und **Last QRG RX** enthalten die zuletzt für beide Stationen bekannten Frequenzen. Sie geben nicht zwingend die QRG der angezeigten Unterhaltung wieder.
**wkd TX?** und **wkd RX?** zeigen den globalen Worked-Status der beiden Basisrufzeichen. Die Angaben sind nicht bandbezogen.
Der Tab **DXCluster messages** zeigt den meldenden und den gemeldeten Teilnehmer, deren Locator, die QRG, den Meldungstext und den globalen Worked-Status der gemeldeten Station. Welche Felder tatsächlich gefüllt sind, hängt von der vom ON4KST-Server übertragenen Meldung ab.
Nachrichtentexte bleiben einzeilig. Ist eine Zelle zu schmal, erscheint der vollständige Inhalt als Tooltip. Webadressen im Meldungstext lassen sich anklicken.
### Separates Monitorfenster
Zusätzlich öffnet KST4Contest das Fenster **Cluster & QSO of the other**. Es zeigt oben die DX-Cluster-Meldungen und darunter die gerichteten Nachrichten zwischen anderen Stationen.
![Separates Cluster- und QSO-Monitorfenster](cluster_qso_monitor.png)
Die Position des vertikalen Dividers sowie die Fenstergröße werden zusammen mit den übrigen UI-Einstellungen gespeichert. Nach einer Änderung **Save Settings** verwenden.
Das Fenster lässt sich über das Menü aus- und wieder einblenden:
```text
Windows → Hide cluster / stranger QSOs
Windows → Show cluster / stranger QSOs
```
Die Tabellen im Hauptfenster und im Monitorfenster greifen auf dieselben Daten zu. Das Ausblenden des Monitorfensters beendet daher weder den Empfang noch die Darstellung in den unteren Tabs.
Herleitung und Grenzen: [Globale Nachrichtenansichten](de-Funktionen#globale-nachrichtenansichten).
---
## Menü
### Window
- **Use Dark Mode** (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.
### Windows
- **Hide cluster / stranger QSOs** beziehungsweise **Show cluster / stranger QSOs**: Blendet das zusätzliche Cluster- und QSO-Monitorfenster aus oder wieder ein.
- **hide options** beziehungsweise **show options**: Blendet das Einstellungsfenster aus oder wieder ein.
- **Use dark mode design**: Aktiviert das dunkle Farbschema.
- **Use default mode design**: Aktiviert das normale helle Farbschema.
- **Show / hide station map** öffnet beziehungsweise schließt das separate Fenster mit Stationskarte und Streckenanalyse.
---
+342 -38
View File
@@ -313,19 +313,86 @@ Für ausgewählte Stationen in der Benutzerliste gibt es direkte Buttons, um das
---
## Sked-Erinnerungen mit ALERT (ab v1.40)
## Skeds und Sked-Erinnerungen
Für jeden Chatmember kann ein Sked-Erinnerungsdienst mit automatischen Nachrichten aktiviert werden. Konfigurierbare Intervallmuster:
> Verfügbar ab v1.40; Band-, Rufzeichen- und Win-Test-Behandlung erweitert in Nightly / v1.42.
- **2+1 Minuten**: Nachrichten bei 2 min und 1 min vor dem Sked.
- **5+2+1 Minuten**: Nachrichten bei 5, 2 und 1 min vor dem Sked.
- **10+5+2+1 Minuten**: Nachrichten bei 10, 5, 2 und 1 min vor dem Sked.
Ein Sked ist mehr als eine Erinnerung an eine Uhrzeit. Er muss während des laufenden Contestbetriebs rechtzeitig sichtbar werden, die vereinbarte Station priorisieren und sofern gewünscht die Gegenstation noch einmal an den Termin erinnern.
Zusätzlich zu den Nachrichten an die Gegenstation gibt es eine **akustische und optische Benachrichtigung** für den eigenen Operator, sodass kein Sked vergessen wird.
KST4Contest behandelt deshalb drei voneinander unabhängige Aufgaben:
Aktivierung: FurtherInfo-Panel der entsprechenden Station.
1. Der Sked wird intern gespeichert und in die Prioritätsberechnung einbezogen.
2. Der Termin erscheint in der AP- und Sked-Timeline.
3. Optional werden vor dem Termin automatische Privatnachrichten gesendet.
Ist der Win-Test-Netzwerk-Listener aktiviert, versucht KST4Contest zusätzlich, den Sked an Win-Test zu übergeben. Ein Problem bei dieser Übergabe löscht oder verhindert den internen Sked nicht.
### Sked anlegen
Zuerst die gewünschte Station in der Benutzerliste auswählen. Die Bedienelemente befinden sich anschließend unten im Bereich **Further Info**.
| Bedienelement | Funktion |
|---|---|
| **Sked in** | Legt fest, in wie vielen Minuten der Sked stattfinden soll. Verfügbar sind 2 bis 15 sowie 20 Minuten. |
| **Band** | Wählt das Band des Skeds. Angeboten werden die unter **Station → my station uses …** aktivierten eigenen Bänder. |
| **Mode** | Legt den an Win-Test zu übertragenden Mode fest. Verfügbar sind `SSB` und `CW`. Die Auswahl hat keinen Einfluss auf den internen Sked oder die Reminder-PMs. |
| **Create sked** | Legt den internen Sked an und versucht bei aktiviertem Win-Test-Netzwerk-Listener zusätzlich die Übergabe an Win-Test. |
| **Remind-PM in** | Aktiviert die automatischen Privatnachrichten vor dem Termin. |
| **2+1**, **5+2+1**, **10+5+2+1** | Legt fest, wie viele Minuten vor dem Sked die Reminder-PMs gesendet werden. |
![Sked-Steuerung im Further-Info-Bereich](sked_controls.png)
KST4Contest versucht, ein sinnvolles Band vorzuwählen. Dafür werden nacheinander folgende Informationen verwendet:
1. eine höchstens 30 Minuten alte QRG der ausgewählten Station auf einem eigenen aktivierten Band,
2. eine eindeutige Bandangabe im Namensfeld der Station und
3. das erste aktivierte eigene Band.
Aktive Rufzeichenvarianten desselben Basisrufzeichens werden bei der Suche nach einer aktuellen Bandinformation gemeinsam betrachtet. Eine manuelle NOT-QRV-Markierung wird bei der automatischen Vorauswahl berücksichtigt. Das Band kann trotzdem ausdrücklich geändert werden, wenn der Operator bewusst eine andere Vereinbarung getroffen hat.
### Auswirkung auf den Priority Score
Ein eingetragener Sked erhöht den Score des normalisierten Basisrufzeichens:
| Zeitraum | Sked-Anteil am Score |
|---|---:|
| mehr als 15 Minuten vor dem Termin | `+40` |
| 15 bis 3 Minuten vor dem Termin | kontinuierlicher Anstieg von `+300` bis in Richtung `+1200` |
| weniger als 3 Minuten vor bis 1 Minute nach dem Termin | `+5000` |
| später als 1 Minute nach dem Termin | kein Sked-Boost mehr |
Die starke Gewichtung unmittelbar vor dem Termin ist beabsichtigt. Ein vereinbarter Sked soll dann nicht durch eine gerade sehr aktive, aber nicht fest eingeplante Station aus der Prioritätsliste verdrängt werden.
Der Score wird für das Basisrufzeichen berechnet. Ein Sked mit `DN9APW-2` beeinflusst daher auch den gemeinsamen Score weiterer aktiver Varianten von `DN9APW`. Das konkrete Nachrichtenziel bleibt trotzdem `DN9APW-2` in der beim Anlegen ausgewählten Chat-Kategorie.
Fünf Minuten nach dem Termin wird der Sked aus der internen Liste entfernt.
### Reminder-PMs
Reminder-PMs werden nur angelegt, wenn **Remind-PM in** aktiviert ist. Je nach ausgewähltem Muster sendet KST4Contest beispielsweise zwei und eine Minute vor dem Sked folgende Privatnachricht:
```text
[KST4C Autoreminder] sked in 2 min
```
Die Nachricht geht an das vollständige sichtbare KST-Rufzeichen und in die Chat-Kategorie, in der der Sked angelegt wurde. Ein Sked für `DN9APW-2` wird daher nicht versehentlich an `DN9APW`, `DN9APW-70` oder eine gleichnamige Station in einer anderen Kategorie gesendet.
Beim tatsächlichen Reminder zeigt KST4Contest zusätzlich den optischen **SKED**-Hinweis an. Ist die einfache Soundausgabe aktiviert, wird außerdem ein Hinweiston abgespielt. Das bloße Aktivieren des Reminders löst noch kein Blinken aus.
Wird für dasselbe vollständige Rufzeichen ein neuer Satz Reminder aktiviert, ersetzt dieser die zuvor geplanten Reminder dieses Rufzeichens.
### Speicherung und Grenzen
Skeds und Reminder-Zeitpläne werden nur im Arbeitsspeicher geführt. Nach einem Neustart von KST4Contest müssen noch benötigte Termine erneut angelegt werden.
Die automatische Bandvorauswahl ist eine Herleitung aus vorhandenen Chatinformationen. Sie beweist nicht, dass die Station noch auf der zuletzt genannten QRG arbeitet. Band, Uhrzeit und Mode sollten deshalb vor **Create sked** kontrolliert werden.
Bedienung: [Stationsinfo-Panel](de-Benutzeroberflaeche#stationsinfo-panel-further-info)
Darstellung: [AP- und Sked-Timeline](#ap-und-sked-timeline)
Win-Test-Übergabe: [Log-Synchronisation Win-Test](de-Log-Synchronisation#win-test)
---
## QSO-Monitoring (ab v1.31)
@@ -337,17 +404,23 @@ Konfiguration: [Konfiguration Sniffer-Einstellungen](de-Konfiguration#sniffe
---
## Win-Test-Integration (ab v1.31, vollständig ab v1.40)
## Win-Test-Integration
KST4Contest unterstützt [Win-Test](https://www.win-test.com/) vollständig als Logprogramm:
KST4Contest verwendet für Win-Test einen eigenen Listener für das native Win-Test-Netzwerkprotokoll. Darüber werden drei voneinander getrennte Funktionen bereitgestellt:
- **Log-Synchronisation**: Gearbeitete Stationen werden automatisch aus Win-Test übernommen und in der Benutzerliste markiert.
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz wird aus Win-Test-UDP-Paketen ausgewertet und befüllt die `MYQRG`-Variable.
- **Sked-Übergabe (SKED Push via UDP)**: Vereinbarte Skeds aus KST4Contest können direkt an Win-Test übertragen werden, sodass das Rufzeichen der Gegenstation im Win-Test-Sked-Fenster erscheint.
- neue QSOs einschließlich Band- und gegebenenfalls Locatorinformation übernehmen,
- die aktuelle QRG aus Win-Test-STATUS-Paketen auswerten und
- intern angelegte Skeds als `ADDSKED` an das Win-Test-Netzwerk übergeben.
Bei der Sked-Übergabe wird die QRG nicht durch eine feste Standardfrequenz ersetzt. KST4Contest sendet nur dann einen Win-Test-Sked, wenn eine zum ausgewählten Band passende QRG ermittelt werden konnte. Der interne Sked, die Timeline und die Reminder-PMs funktionieren unabhängig davon weiter.
Ein sichtbarer KST-Suffix wie `-2`, `-70` oder `-144` bleibt innerhalb von KST4Contest erhalten, wird für das Win-Test-Logrufzeichen jedoch entfernt. Portable Bestandteile wie `/P`, `/M` oder ein Länderpräfix bleiben bestehen.
Einrichtung und genaue Datenbehandlung: [Log-Synchronisation Win-Test](de-Log-Synchronisation#win-test)
Einstellungen: [Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener-ab-v131)
Details zur Konfiguration: [Konfiguration Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener)
---
## PSTRotator-Interface (ab v1.31, vollständig ab v1.40)
@@ -482,16 +555,53 @@ Zugehörige Einstellungen:
---
## AP-Timeline (ab v1.40)
## AP- und Sked-Timeline
Eine visuelle Zeitleiste zeigt für jeden möglichen AP-Ankunftsminuten-Slot bis zu 4 hochbewertete Stationen, die per Aircraft Scatter erreichbar wären. Priorisierungskriterien:
Die Timeline stellt bevorstehende Aircraft-Scatter-Gelegenheiten und eingetragene Skeds für die nächsten 30 Minuten gemeinsam dar. Sie beantwortet damit zwei Fragen auf einen Blick:
- Bevorzugt werden APs mit dem **höchsten Reflexionspotenzial** (nicht unbedingt die schnellste Ankunft).
- Stationen, auf die die eigene Antenne nicht zeigt, werden **transparent** dargestellt.
- Wann entsteht voraussichtlich eine interessante AP-Gelegenheit?
- Welcher bereits vereinbarte Sked nähert sich unabhängig davon?
So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und über welche Flugzeuge erreichbar sein werden.
Weiter in der Zukunft liegende Ereignisse erscheinen rechts. Mit ablaufender Zeit wandern sie nach links in Richtung des aktuellen Zeitpunkts.
---
![AP-Kandidaten und Skeds in der Timeline](sked_timeline.png)
### AP-Kandidaten
AP-Kandidaten erscheinen in den oberen Spuren. Pro Ankunftsminute können bis zu vier ausgewählte Kandidaten dargestellt werden. Die Auswahl berücksichtigt den Priority Score und das von AirScout gemeldete Reflexionspotenzial.
Die Farbe des AP-Symbols kennzeichnet das Reflexionspotenzial:
| Farbe | Reflexionspotenzial |
|---|---:|
| Magenta | mindestens 95 % |
| Rot | mindestens 75 % |
| Gelb | mindestens 50 % |
| Blau | unter 50 % |
Die Farbe ist keine QSO-Wahrscheinlichkeit. Sie gibt den von AirScout übernommenen Wert für die berechnete Reflexionsgeometrie wieder.
Ein Klick auf einen AP-Kandidaten wählt den dazugehörigen aktiven Chatmember einschließlich Rufzeichensuffix und Chat-Kategorie aus. Dadurch kann unmittelbar eine passende Nachricht vorbereitet werden.
### Skeds
Skeds erscheinen als Rauten in der unteren Spur. Die Beschriftung verwendet das vollständige KST-Rufzeichen, beispielsweise `SKED: DN9APW-2`. Dadurch bleibt erkennbar, welcher konkrete Login für den Termin ausgewählt wurde.
Der Tooltip eines Skeds zeigt mindestens:
- das vollständige KST-Rufzeichen,
- das vereinbarte Band und
- den QTF zur Gegenstation.
Sind passende AirScout-Daten vorhanden, werden zusätzlich die aktuelle AP-Erreichbarkeit und die nächste berechnete AP-Gelegenheit angezeigt.
### Berücksichtigung der Antennenrichtung
Liegt der QTF eines Ereignisses deutlich außerhalb der aktuellen Antennenrichtung, wird dessen Symbol transparenter dargestellt. Die Beschriftung bleibt lesbar. Liegt das Ziel nahe der Mitte des konfigurierten Antennenbereichs, wird das Symbol zusätzlich hervorgehoben.
Diese Darstellung verändert weder den Sked noch den Priority Score. Sie ist eine optische Hilfe, um Kandidaten in der aktuellen Antennenrichtung schneller zu erkennen.
Die Timeline ist eine Vorschau. AirScout-Daten können sich ändern, und ein eingetragener Sked garantiert weder eine freie Frequenz noch eine tatsächlich vorhandene Ausbreitungsverbindung.
## Intervall-Beacon
@@ -504,39 +614,233 @@ Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim A
## Simplelogfile
Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
Details: [Log-Synchronisation](de-Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Cluster & QSO der anderen
## Globale Nachrichtenansichten
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
Das Stationsinfo-Panel und die PM-Tabelle beantworten Fragen zu einer bestimmten Station oder zur eigenen Kommunikation. Daneben gibt es Nachrichtenströme, die unabhängig von der aktuell ausgewählten Station betrachtet werden müssen.
Dieses Fenster kann minimiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
KST4Contest fasst diese globalen Informationen in drei Ansichten zusammen:
| Ansicht | Inhalt |
|---|---|
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
| **DXCluster messages** | Vom ON4KST-Server gelieferte DX-Cluster-Meldungen |
| **QSO of the other** | Gerichtete Chatnachrichten zwischen zwei anderen Stationen |
Die Ansichten befinden sich als Tabs im unteren Bereich des Hauptfensters. **Public messages** ist nach dem Programmstart vorausgewählt.
![Globale Nachrichtentabs im Hauptfenster](global_message_tabs.png)
### DXCluster messages
Der Tab **DXCluster messages** zeigt DX-Cluster-Meldungen, die über die bestehende ON4KST-Verbindung empfangen werden. Je nach Inhalt der Meldung stehen folgende Informationen zur Verfügung:
- Zeitpunkt,
- sendende beziehungsweise meldende Station,
- deren Locator,
- gemeldete Station,
- deren Locator,
- QRG,
- Meldungstext und
- globaler Worked-Status der gemeldeten Station.
Nicht jede vom Server übertragene Meldung enthält alle Felder. Ein leeres Locator- oder Nachrichtenfeld bedeutet deshalb nicht zwangsläufig einen Verarbeitungsfehler.
Diese Anzeige darf nicht mit dem [integrierten lokalen DX-Cluster-Server](de-DX-Cluster-Server) verwechselt werden. Der Tab zeigt empfangene ON4KST-Clusterinformationen. Der lokale Server erzeugt dagegen aus einer erkannten Richtungsgelegenheit einen Spot und gibt ihn an ein verbundenes Logprogramm weiter.
### QSO of the other
Der Tab **QSO of the other** zeigt gerichtete Chatnachrichten, bei denen weder Absender noch Empfänger die eigene Station sind. Öffentliche Nachrichten an `ALL` werden nicht aufgenommen.
Die Tabelle enthält:
| Spalte | Bedeutung |
|---|---|
| **Time** | Zeitpunkt der Chatnachricht |
| **Call TX** | Absender der Nachricht |
| **Last QRG TX** | zuletzt für den Absender bekannte QRG |
| **wkd TX?** | globaler Worked-Status des Absenders |
| **Call RX** | Empfänger der Nachricht |
| **Last QRG RX** | zuletzt für den Empfänger bekannte QRG |
| **wkd RX?** | globaler Worked-Status des Empfängers |
| **Message** | Inhalt der gerichteten Nachricht |
| **Category** | Chat-Kategorie der Nachricht |
Die beiden QRG-Spalten zeigen den zuletzt in KST4Contest bekannten Wert der jeweiligen Station. Das ist nicht zwangsläufig die Frequenz, auf der sich die beiden Stationen gerade verabreden. Die QRG kann aus einer früheren Nachricht stammen und sich inzwischen geändert haben.
Auch die Worked-Spalten sind bewusst bandunabhängig. Ein `X` bedeutet, dass das betreffende Basisrufzeichen auf mindestens einem Band gearbeitet wurde. Daraus folgt nicht, dass es auf der in der Tabelle sichtbaren oder vermuteten QRG bereits gearbeitet wurde.
Die Bezeichnung **QSO of the other** ist eine praktische Kurzform. Eine gerichtete Nachricht beweist weder, dass anschließend ein Funkkontakt zustande kam, noch dass beide Stationen tatsächlich auf derselben Frequenz arbeiten. Die Ansicht zeigt beobachtbare Koordination im Chat nicht das Logbuch der anderen Stationen.
### Zusätzliches Monitorfenster
Dieselben DX-Cluster-Meldungen und gerichteten Nachrichten stehen weiterhin im separaten Fenster **Cluster & QSO of the other** zur Verfügung. Dort erscheinen die DX-Cluster-Tabelle oben und die Nachrichten zwischen anderen Stationen darunter.
![Separates Cluster- und QSO-Monitorfenster](cluster_qso_monitor.png)
Das separate Fenster und die Tabs verwenden dieselben zugrunde liegenden Listen. Eine Meldung wird dadurch nicht doppelt empfangen oder doppelt gespeichert. Es handelt sich lediglich um zwei Darstellungen derselben Daten.
Das Monitorfenster kann über **Windows → Hide cluster / stranger QSOs** ausgeblendet und mit **Show cluster / stranger QSOs** wieder eingeblendet werden. Wer den Platz nicht benötigt, kann das Fenster daher schließen oder minimieren, ohne auf die entsprechenden Tabs im Hauptfenster verzichten zu müssen.
Die vollständigen Texte abgeschnittener Nachrichten erscheinen als Tooltip. Erkannte Webadressen können wie in den übrigen Nachrichtentabellen angeklickt und im Standardbrowser geöffnet werden.
Die Ansichten helfen dabei, Aktivität und Koordination anderer Stationen zu erkennen. Bei hohem Chat-Aufkommen entsteht daraus allerdings schnell mehr Information als Erkenntnis. Das separate Fenster ist deshalb vor allem dann nützlich, wenn ein bestimmter Kommunikationsfluss gezielt beobachtet werden soll.
---
## Stationskarte (ab v1.41)
## Stationskarte und Streckenanalyse (ab v1.41)
Eine interaktive OpenStreetMap-Karte zeigt die geografische Position aller aktiven Chatmember.
Eine lange Benutzerliste beantwortet zwei geografische Fragen nur unzureichend: Wo befinden sich die eingeloggten Stationen, und welche davon liegen ungefähr in der aktuellen Antennenrichtung? Die Stationskarte überträgt deshalb die bereits bekannten Locator-, Richtungs-, Band- und Worked-Informationen in eine interaktive Kartenansicht.
**Funktionen:**
Die Karte ist keine zweite, unabhängig verwaltete Stationsliste. Sie verwendet die aktuell durch die Filter der Benutzerliste sichtbaren Chatmember. Wird beispielsweise nach Entfernung, Richtung, Worked-Status oder einem bestimmten Band gefiltert, wirkt sich dies auch auf die dargestellten Stationen aus. In der Kopfzeile der Karte wird angezeigt, wie viele Stationen sichtbar sind und ob eine gefilterte Ansicht aktiv ist.
- Stationsmarker mit Rufzeichen-Labels, farblich nach Aktivität und Sked-Status
- **Antennen-Kegel** für die eigene Station
- **Verbindungslinie** zur aktuell ausgewählten Station
- **Maidenhead-Raster** (QRA-Locator-Gitter als Overlay)
- **Wegprofil-Diagramm**: Geländehöhen-Querschnitt zwischen eigener und ausgewählter Station, inklusive Fresnel-Zonen-Analyse und Horizonterkennung
- Mehrere Terrainquellen: **Copernicus GLO-30** (hochauflösendes DEM), **Open-Meteo API**, synthetischer Fallback und **Offline-DEM-Import** für den Betrieb ohne Internetverbindung
- Aircraft-Scatter-Weganalyse verknüpft mit den Geländedaten
![Stationskarte mit ausgewählter Station und eingeblendeter Streckenanalyse](station_map_path_analysis.png)
Die Karte funktioniert in gepackten Umgebungen (AppImage, Flatpak) ohne Zugriff auf externe CDNs: Die Kartenkacheln werden über einen lokalen Tile-Proxy abgerufen, die Leaflet.js-Bibliothek ist in der Anwendung eingebettet.
### Welche Stationen werden dargestellt?
Für einen Kartenmarker benötigt KST4Contest einen brauchbaren sechsstelligen Locator. Chatmember ohne einen solchen Locator können in der Benutzerliste vorhanden sein, erscheinen aber nicht auf der Karte.
Mehrere aktive Chat-Einträge desselben Basisrufzeichens werden für die Kartenansicht zusammengefasst. Das verhindert, dass beispielsweise getrennte Logins in mehreren Chat-Kategorien mehrere Marker an derselben geografischen Position erzeugen. Als sichtbares Rufzeichen und für die Detailinformationen wird die zuletzt geeignete aktive Variante verwendet.
Die Beschriftung eines Markers kann zusätzlich enthalten:
- die für die Station erkannten aktiven Bänder,
- `B+`, wenn mindestens ein eigenes aktiviertes und noch nicht gearbeitetes Band angeboten wird.
Die Bandangaben verwenden dieselbe Herleitung wie die Bandspalten, der Filter **New bands** und der Priority Score. Aktuelle QRG-Erkennungen, Bandangaben im Namensfeld, Worked-Informationen und manuelle NOT-QRV-Markierungen werden daher auch in der Kartenansicht konsistent berücksichtigt.
### Bedeutung der Markerfarben
| Darstellung | Bedeutung |
|---|---|
| Blauer Rand | Station ohne eine der nachfolgenden besonderen Markierungen |
| Gelber Rand | Das Basisrufzeichen wurde bereits auf mindestens einem Band gearbeitet |
| Grün | Für die Station besteht eine aus gerichteten Chatnachrichten hergeleitete Richtungsgelegenheit |
| Orange | Aktuell ausgewählte Station |
Treffen mehrere Zustände gleichzeitig zu, hat die für den Betrieb wichtigere Markierung Vorrang. Eine ausgewählte Station bleibt deshalb orange; eine Richtungsgelegenheit wird grün dargestellt, auch wenn das Rufzeichen bereits gearbeitet wurde.
Bei niedrigen Zoomstufen werden räumlich dicht beieinanderliegende Stationen zu einem Cluster zusammengefasst. Die Zahl im Cluster gibt die Anzahl der enthaltenen Stationen an. Ein Klick zoomt weiter hinein, wählt aber noch keine einzelne Station aus. Die aktuell ausgewählte Station und grün markierte Richtungsgelegenheiten bleiben auch bei niedriger Zoomstufe als einzelne Marker sichtbar.
### Auswahl und geografische Hilfen
Ein Klick auf einen einzelnen Stationsmarker:
1. wählt den dazugehörigen aktiven Chatmember aus,
2. scrollt die Benutzerliste zu diesem Eintrag,
3. aktualisiert den **Further Info**-Bereich und
4. bereitet das Rufzeichen wie bei einer Auswahl in der Benutzerliste als Nachrichtenziel vor.
Für die ausgewählte Station zeichnet KST4Contest eine Verbindungslinie von der eigenen Station zum Ziel. Der eingezeichnete Antennensektor verwendet:
- den aktuellen eigenen QTF,
- den konfigurierten Antennen-Öffnungswinkel und
- das konfigurierte Standard-Maximum-QRB.
Das Maidenhead-Raster passt seine Genauigkeit an die Zoomstufe und den sichtbaren Kartenausschnitt an. Es dient der räumlichen Orientierung; die Position eines Stationsmarkers wird aus dem sechsstelligen Locator abgeleitet und ist deshalb keine exakte GPS-Position.
### Strecken- und Geländeprofil
Nach Auswahl einer Station fordert KST4Contest ein Höhenprofil zwischen dem eigenen und dem fremden Locator an. Die aktive Online-Datenquelle ist die Open-Meteo Elevation API mit Geländedaten auf Basis von **Copernicus GLO-90**.
Die Online-Abfrage ist auf höchstens 100 gleichmäßig über die Strecke verteilte Höhenpunkte begrenzt. Eine 100 Kilometer lange Strecke wird damit grob im Abstand von etwa einem Kilometer abgetastet. Bei kürzeren Strecken wird der Abstand entsprechend kleiner, schmale Hindernisse können trotzdem zwischen zwei Abfragepunkten liegen.
Aus den Höhenpunkten berechnet KST4Contest unter anderem:
- das Geländeprofil,
- die geometrische Sichtlinie,
- die Erdkrümmung mit einem festen effektiven Erdradiusfaktor von `k = 4/3`,
- den geometrischen Radiohorizont beider Stationen,
- relevante Geländehorizonte,
- die erste Fresnel-Zone,
- die geringste Fresnel-Freiheit,
- den stärksten erkannten Eingriff in die Fresnel-Zone und
- eine grobe Einzelhindernis- beziehungsweise Knife-Edge-Abschätzung.
Die eingestellte eigene Antennenhöhe wird zur lokalen Geländehöhe addiert. Für die Gegenstation wird derzeit eine feste angenommene Antennenhöhe von 10 Metern über Grund verwendet.
Bewegst du die Maus über das Profil, wird der dazugehörige Abfragepunkt zusätzlich auf der Karte markiert. Dadurch lässt sich ein auffälliger Berg oder Geländeeinschnitt leichter einer geografischen Position zuordnen.
### Welche Frequenz wird für die Berechnung verwendet?
Die Frequenz beeinflusst insbesondere die Größe der Fresnel-Zone, die Freiraumdämpfung und das Link-Budget. KST4Contest versucht deshalb, für die ausgewählte Station eine passende Analysefrequenz zu bestimmen.
Vorrangig wird eine aktuell bekannte QRG auf einem eigenen aktivierten und für die Gegenstation nutzbaren Band verwendet. Fehlt eine geeignete QRG, wird aus den vorhandenen Bandinformationen ein automatisches Analyseband hergeleitet und dessen Standardfrequenz verwendet. Manuelle NOT-QRV-Markierungen werden dabei berücksichtigt.
Die tatsächlich verwendete Frequenz steht im Feld **Frequency** der Streckenanalyse. Sie sollte kontrolliert werden, wenn die automatische Zuordnung nicht zur vorgesehenen Funkverbindung passt. Eine Berechnung auf 144 MHz ist für eine geplante Verbindung auf 1296 MHz keine gleichwertige Näherung.
### Link-Budget und Tropo-Spalte
Zusätzlich zur geometrischen Bewertung erstellt KST4Contest eine vereinfachte Link-Budget-Abschätzung. Verwendet werden:
- die konfigurierte eigene Sendeleistung,
- der eigene Antennengewinn,
- die angenommene Sendeleistung der Gegenstation,
- der angenommene Antennengewinn der Gegenstation,
- eine frequenzabhängig geschätzte Speiseleitungsdämpfung,
- die Freiraumdämpfung und
- gegebenenfalls eine grobe zusätzliche Hindernisdämpfung.
Antennengewinne werden in `dBi` eingegeben. Ein in `dBd` bekannter Wert muss deshalb vor der Eingabe um `2,15 dB` erhöht werden.
Die Berechnung betrachtet beide Übertragungsrichtungen. Der daraus abgeleitete ungünstigere SSB-Wert wird als Tropo-Marge für die Station gespeichert und kann anschließend in der **Tropo**-Spalte, beim Sortieren und durch den Filter **Tropo >=0dB** verwendet werden.
Karte und Benutzerliste führen keine voneinander unabhängigen Berechnungen durch. Sie verwenden denselben Reachability-Service und denselben Ergebnisspeicher. Eine über die Karte oder mit **Calc selected** angestoßene Berechnung kann deshalb anschließend auch in der Benutzerliste erscheinen.
Es wird absichtlich keine automatische Online-Geländeabfrage für jeden sichtbaren Chatmember gestartet. Eine Berechnung erfolgt durch eine ausdrückliche Auswahl in der Karte oder über **Calc selected**. Das begrenzt API-Anfragen und verhindert, dass jede Tabellenaktualisierung eine neue Serie von Höhenabfragen auslöst.
### Pfadanalyse ausblenden
Das Geländeprofil und die ausführliche Analyse benötigen einen erheblichen Teil der Fensterhöhe. Werden sie gerade nicht gebraucht, können beide Bereiche gemeinsam mit **Hide path analysis** ausgeblendet werden. Die Karte nutzt den frei werdenden Platz unmittelbar.
![Stationskarte mit ausgeblendeter Pfadanalyse](station_map_compact.png)
Mit **Show path analysis** werden Profil und Detailwerte wieder eingeblendet. Das zuletzt berechnete Ergebnis bleibt erhalten. Die gewählte Sichtbarkeit wird in den Einstellungen gespeichert und beim nächsten Programmstart wiederhergestellt.
Der rechte Detailbereich ist über einen Divider in der Breite verstellbar. Er kann so weit verkleinert werden, dass mehr Platz für die Karte entsteht, ohne ein Rufzeichen mit üblicher Länge vollständig zu verdecken.
### Was sagt die Streckenanalyse nicht aus?
Die Auswertung ist ein geometrisches und rechnerisches Modell. Sie misst weder die tatsächliche Feldstärke noch die momentanen Ausbreitungsbedingungen.
Insbesondere kennt KST4Contest nicht:
- die wirkliche Antennenhöhe und den tatsächlichen Antennengewinn der Gegenstation,
- deren Sendeleistung und Speiseleitungsverluste,
- Gebäude, Bewuchs und andere Hindernisse, die im Höhenmodell nicht enthalten sind,
- lokale Störungen und Empfängereigenschaften,
- den aktuellen atmosphärischen K-Faktor,
- Inversionsschichten oder Ducting und
- die tatsächliche Antennenrichtung der Gegenstation.
Die unter **Mechanisms** genannten Ausbreitungswege sind eine allgemeine Einordnung anhand der Geländegeometrie. Eine Nennung von Aircraft Scatter bedeutet nicht, dass aktuelle Flugzeuge aus AirScout in die Geländeanalyse eingerechnet wurden.
Im Klartext: Ein freier Weg ist ein nützlicher positiver Hinweis. Ein geometrisch versperrter Weg bedeutet im VHF-, UHF- und Mikrowellenbereich aber nicht automatisch „unmöglich“. Ebenso garantiert ein positives Link-Budget kein QSO.
Bedienung: [Stationskarte in der Benutzeroberfläche](de-Benutzeroberflaeche#stationskarte)
Konfiguration: [Streckenanalyse und Link-Budget](de-Konfiguration#streckenanalyse-und-link-budget)
---
## Optimierte Nachrichtenverarbeitung / 30.000-Nachrichten-Limit (ab v1.41)
## Begrenzte Nachrichtenspeicher (ab v1.41)
Chat- und DX-Cluster-Meldungen werden während des Betriebs im Arbeitsspeicher gehalten. Damit ein mehrtägiger Contest nicht zu einem unbegrenzt wachsenden Speicherverbrauch und immer langsameren Tabellen führt, besitzen beide Speicher feste Grenzen:
| Nachrichtenspeicher | Maximale Größe | Größe nach dem automatischen Aufräumen |
|---|---:|---:|
| Chatnachrichten | 30.000 | 25.000 |
| DX-Cluster-Meldungen | 10.000 | 8.000 |
Wird die jeweilige Maximalgröße überschritten, entfernt KST4Contest die ältesten Einträge am Ende der Liste. Neue Nachrichten bleiben erhalten und werden weiterhin zuerst angezeigt.
Die öffentlichen Nachrichten, PMs, Stationsinformationen und **QSO of the other** besitzen keine voneinander getrennten 30.000-Einträge-Speicher. Sie sind gefilterte Ansichten derselben globalen Chatnachrichtenliste. Auch die DX-Cluster-Tabelle im Hauptfenster und die Tabelle im separaten Monitorfenster verwenden denselben Cluster-Speicher.
Die Nachrichten werden nicht dauerhaft gespeichert. Nach einem Neustart beginnen die Ansichten wieder mit leeren Listen.
Die internen Chat- und Nachrichtentabellen sind auf **30.000 Einträge** begrenzt. Ältere Nachrichten werden automatisch verworfen, sobald das Limit erreicht wird. Damit bleiben Speicherverbrauch und Darstellungsperformance auch bei mehrtägigen Contest-Betrieb stabil.
---
+94 -10
View File
@@ -62,6 +62,65 @@ Der Wert sollte zum eigenen Stationsaufbau und zum vorgesehenen Contestbetrieb p
---
### Streckenanalyse und Link-Budget
Die Streckenanalyse der Stationskarte verwendet einige Angaben aus der Stationskonfiguration. Diese Werte beschreiben das eigene Stationssetup und soweit keine individuellen Daten der Gegenstation vorliegen ein angenommenes Setup der Gegenstation.
Folgende Einstellungen werden berücksichtigt:
- **Own antenna height AGL [m]** gibt die Höhe der eigenen Antenne über dem lokalen Gelände an. Die Angabe bezieht sich auf *Above Ground Level* und nicht auf die Höhe über dem Meeresspiegel. KST4Contest addiert diesen Wert zur Geländehöhe am eigenen Standort.
- **Own TX power [W]** gibt die verwendete Sendeleistung der eigenen Station in Watt an.
- **Own ant. gain [dBi]** gibt den Antennengewinn der eigenen Station in dBi an.
- **DX OM TX power [W]** gibt die für die Gegenstation angenommene Sendeleistung in Watt an.
- **DX OM ant. gain [dBi]** gibt den für die Gegenstation angenommenen Antennengewinn in dBi an.
Für die Antennenhöhe der Gegenstation verwendet KST4Contest derzeit einen festen Wert von 10 m über dem lokalen Gelände. Die Leistungs- und Antennendaten der Gegenstation sind globale Annahmen. Sie ersetzen keine individuell bekannten Stationsdaten, ermöglichen aber eine einheitliche Abschätzung, wenn keine genaueren Informationen vorliegen.
Die Antennengewinne müssen in dBi angegeben werden. Falls ein Wert in dBd vorliegt, kann er näherungsweise wie folgt umgerechnet werden:
`dBi = dBd + 2.15`
Auch die aktuelle Antennenrichtung, die konfigurierte Strahlbreite, das maximale QRB und die für die eigene Station aktivierten Bänder beeinflussen die Darstellung oder Auswertung auf der Stationskarte. Die aktuelle QTF und die Strahlbreite bestimmen beispielsweise den eingezeichneten Antennensektor und die Hervorhebung von Stationen innerhalb dieses Bereichs.
Für die Berücksichtigung der Erdkrümmung verwendet die Streckenanalyse einen festen Faktor von `k = 4/3` für den effektiven Erdradius. Das ist eine übliche Näherung für eine durchschnittliche troposphärische Refraktion. Tatsächliche Ausbreitungsbedingungen können davon deutlich abweichen.
Das Link-Budget berücksichtigt unter anderem:
- die Entfernung zwischen beiden Stationen,
- die verwendete Frequenz,
- die konfigurierte Sendeleistung,
- die Antennengewinne,
- geschätzte Speiseleitungsverluste,
- den Freiraumverlust sowie
- eine grobe Zusatzdämpfung durch Hindernisse im Streckenprofil.
Die daraus berechnete Empfangsleistung und SSB- beziehungsweise CW-Marge sind technische Abschätzungen. Sie sollen dabei helfen, mögliche Verbindungen einzuordnen. Sie sind keine vollständige Feldstärkeprognose und können insbesondere aktuelle Wetterbedingungen, lokale Abschattungen, Mehrwegeausbreitung oder andere nicht bekannte Stationsparameter nicht vollständig berücksichtigen.
---
### Streckenanalyse und Link-Budget
Die Stationskarte verwendet mehrere Werte aus dem Reiter **Station**, um das Geländeprofil und das Link-Budget zur ausgewählten Gegenstation zu berechnen.
| Einstellung | Verwendung |
|---|---|
| **Own antenna height AGL** | Höhe der eigenen Antenne über dem lokalen Gelände in Metern |
| **Own TX power W** | Eigene Sendeleistung in Watt |
| **Own ant. gain dBi** | Gewinn der eigenen Antenne in dBi |
| **DX OM TX power W** | Angenommene Sendeleistung der Gegenstation in Watt |
| **DX OM ant. gain dBi** | Angenommener Antennengewinn der Gegenstation in dBi |
**AGL** bedeutet „above ground level“. Trage hier nicht die Höhe über dem Meeresspiegel ein. Die Geländehöhe am eigenen Standort stammt bereits aus dem abgerufenen Höhenprofil; die konfigurierte Antennenhöhe wird zu diesem Wert addiert.
Für die Antennenhöhe der Gegenstation verwendet KST4Contest derzeit einen festen Standardwert von 10 Metern über Grund. Eine stationsbezogene Antennenhöhe wird im ON4KST-Chat nicht übertragen.
Antennengewinne müssen in `dBi` eingetragen werden. Liegt ein Wert in `dBd` vor, gilt:
```text
dBi = dBd + 2,15 dB
## Server-Einstellungen (ab v1.31)
Der Chat-Server-DNS und -Port sind in den Preferences konfigurierbar:
@@ -354,20 +413,45 @@ Weitere Hintergründe: [Automatische Antworten auf Privatnachrichten](de-Funktio
## Win-Test-Netzwerk-Listener (ab v1.31)
Dedizierter Empfänger für Win-Test-spezifische UDP-Pakete. Ermöglicht:
Der Win-Test-Netzwerk-Listener verarbeitet das native Win-Test-UDP-Protokoll. Er ist vom allgemeinen QSO-UDP-Listener auf Port `12060` unabhängig und übernimmt drei Aufgaben:
- **Log-Synchronisation**: Gearbeitete Stationen werden aus Win-Test übernommen und in der Benutzerliste markiert.
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz aus Win-Test befüllt die `MYQRG`-Variable.
- **Sked-Übergabe (SKED Push)**: Skeds aus KST4Contest werden via UDP direkt an Win-Test übergeben. Der UDP-Broadcast-Standardport von Win-Test (9871) wird verwendet.
- QSOs einschließlich Band- und Locatorinformation auswerten,
- STATUS-Pakete für die eigene QRG verarbeiten und
- Skeds an das Win-Test-Netzwerk übergeben.
Einstellungen:
- **Aktivieren/Deaktivieren**: Checkbox in den Preferences (ab v1.40).
- **Port**: Konfigurierbarer UDP-Port für den Win-Test-Listener.
- **Sked-UDP-Adresse und Port**: Zieladresse und Port für die SKED-Übergabe an Win-Test.
### Einstellungen unter Log sync
> **Hinweis**: Der Win-Test-Listener ist ein **zusätzlicher** Listener der Standard-QSO-UDP-Broadcast-Listener auf Port 12060 bleibt davon unabhängig.
| Einstellung | Funktion |
|---|---|
| **Receive Win-Test network based UDP log messages** | Aktiviert den Win-Test-Netzwerk-Listener. Bei aktiviertem Listener wird nach **Create sked** auch die Sked-Übergabe versucht. |
| **UDP-Port for Win-Test listener** | Port des Win-Test-Netzwerks. Standard ist `9871`. Der Port wird auch für die Sked-Übergabe verwendet. |
| **KST station name in Win-Test network (src of SKED packets)** | Stationsname, unter dem KST4Contest die Sked-Pakete sendet. In einem Netzwerk mit mehreren Clients sollte ein eindeutiger Name verwendet werden. |
| **Win-Test network broadcast address** | Zieladresse für ausgehende Win-Test-Netzwerkpakete. Bei lokalem Netzwerkbetrieb muss hier eine vom Win-Test-Rechner erreichbare Broadcast-Adresse eingetragen sein. |
Die Broadcast-Adresse ist konfigurierbar, weil `255.255.255.255` nicht in jedem Stationsnetz und nicht über jede Netzwerkschnittstelle zuverlässig weitergeleitet wird. Bei mehreren Rechnern kann stattdessen die zum Stationsnetz gehörende gerichtete Broadcast-Adresse erforderlich sein.
### Einstellungen unter TRX sync
| Einstellung | Funktion |
|---|---|
| **Win-Test STATUS QRG Sync** | Übernimmt die aktuelle Frequenz aus Win-Test-STATUS-Paketen als eigene QRG. |
| **Use pass frequency from Win-Test STATUS** | Verwendet die übertragene Pass-Frequenz anstelle der normalen TRX-QRG. |
| **Win-Test station name filter** | Verarbeitet nur STATUS-Pakete der angegebenen Win-Test-Station. Ein leeres Feld akzeptiert alle Stationsnamen. |
Der Stationsfilter ist insbesondere bei mehreren Win-Test-Clients sinnvoll. Ohne Filter kann die zuletzt eingegangene STATUS-Meldung eines anderen Arbeitsplatzes die eigene QRG in KST4Contest überschreiben.
### Sked-Übergabe
Für die Sked-Übergabe gibt es keinen davon getrennten internen Sked-Modus. Ist der Listener aktiviert, versucht **Create sked** zusätzlich zur internen Anlage die Übertragung an Win-Test.
KST4Contest sendet nur dann ein `ADDSKED`-Paket, wenn eine QRG ermittelt wurde, die zum ausdrücklich ausgewählten Band gehört. Kann keine passende QRG gefunden werden, bleibt der interne Sked bestehen und die Win-Test-Übergabe wird ausgelassen.
Die Auswahl `SSB` oder `CW` erfolgt direkt im Further-Info-Bereich beim Anlegen des Skeds. Eine automatische Mode-Ableitung wird nicht verwendet.
Nach Änderungen **Save Settings** verwenden, damit Port, Stationsname, Broadcast-Adresse und TRX-Optionen beim nächsten Programmstart wiederhergestellt werden.
Datenbehandlung und QRG-Auswahl: [Log-Synchronisation Win-Test](de-Log-Synchronisation#win-test)
---
## PSTRotator-Einstellungen (ab v1.31)
+64 -23
View File
@@ -86,35 +86,76 @@ Beim Broadcast des vollständigen Logbuchs verwendet DXLog.net `contactreplace`
### Win-Test
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
Win-Test wird über einen eigenen UDP-Listener für das native Win-Test-Netzwerkprotokoll angebunden. Dieser Listener ist vom allgemeinen QSO-UDP-Listener auf Port `12060` unabhängig.
Bei einem neuen QSO übernimmt KST4Contest das Rufzeichen und löst die native Win-Test-Band-ID auf. Dabei werden auch 50 und 70 MHz verarbeitet. Ist im Paket ein gültiger Locator enthalten, wird zusätzlich das gearbeitete Großfeld für das erkannte Band gespeichert.
#### QSO- und Worked-Synchronisation
**Vorteile der Win-Test Integration:**
- **Bandbezogene Worked-Daten:** Neue QSOs setzen die Worked-Markierung des von Win-Test gemeldeten Bandes und aktualisieren sofern vorhanden den Großfeldstatus.
- Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen.
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieser auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet* automatisch, sobald der Listener aktiv ist.
- Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden.
- **Automatische QRG-Auflösung für SKEDs:** KST4Contest wählt die Sked-Frequenz intelligent:
1. Hat die Gegenstation in einer Chat-Nachricht ihre QRG genannt, wird diese verwendet.
2. Sonst wird die eigene aktuelle QRG verwendet (aus Win-Test STATUS oder manueller Eingabe).
Bei einem neuen QSO übernimmt KST4Contest:
**Einstellungen im Reiter „Log-Synchronisation":**
- `Receive Win-Test network based UDP log messages` aktivieren.
- `UDP-Port for Win-Test listener` (Standard: 9871).
- `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST").
- `Win-Test network broadcast address`: Wird i.d.R. automatisch erkannt; erforderlich für das Senden von Sked-Paketen.
- das geloggte Rufzeichen,
- die native Win-Test-Band-ID und
- einen gültigen Locator, sofern er im Paket enthalten ist.
**Einstellungen im Reiter „TRX-Synchronisation":**
- `Win-Test STATUS QRG Sync`: Wenn aktiviert, übernimmt KST4Contest die aktuelle Transceiverfrequenz aus dem Win-Test STATUS-Paket als eigene QRG (MYQRG).
- `Use pass frequency from Win-Test STATUS`: Statt der eigenen TRX-QRG wird die im STATUS-Paket enthaltene Pass-Frequenz als MYQRG verwendet (für Multi-Op-Setups, bei denen mit einer Pass-QRG gearbeitet wird).
- `Win-Test station name filter`: Wird hier ein Name eingetragen (z.B. "STN1"), verarbeitet KST4Contest nur Pakete dieser Win-Test-Instanz. Leer lassen, um alle zu akzeptieren.
Die Band-IDs für 50 und 70 MHz werden ebenso verarbeitet wie die VHF-, UHF- und SHF-Bänder. Das Rufzeichen wird global und auf dem erkannten Band als gearbeitet markiert. Liegt zusätzlich ein Locator vor, wird dessen vierstelliges Großfeld für dieses Band gespeichert.
**Einstellungen in Win-Test:**
- Das Netzwerk in Win-Test muss aktiv sein.
- Win-Test muss so konfiguriert sein, dass es seine Broadcasts an den entsprechenden Port (Standard 9871) sendet bzw. empfängt.
Die Daten werden in derselben internen Datenbank abgelegt wie Worked-Informationen aus den übrigen QSO-UDP-Schnittstellen und nach einem Neustart wiederhergestellt.
#### Skeds an Win-Test übergeben
Mit **Create sked** wird zunächst ein interner KST4Contest-Sked angelegt. Ist der Win-Test-Netzwerk-Listener aktiviert, versucht KST4Contest anschließend automatisch, den Sked als `ADDSKED` an das Win-Test-Netzwerk zu übertragen.
Die QRG wird in folgender Reihenfolge bestimmt:
1. KST4Contest sucht die neueste, höchstens 30 Minuten alte QRG der Gegenstation auf dem ausdrücklich ausgewählten Band. Dabei werden aktive Varianten desselben Basisrufzeichens gemeinsam ausgewertet.
2. Fehlt eine solche QRG, wird die eigene QRG der Chat-Kategorie geprüft, in der der Sked angelegt wurde. Sie wird nur verwendet, wenn sie sich auswerten lässt und tatsächlich zum ausgewählten Band gehört.
3. Kann auf keinem dieser Wege eine passende QRG ermittelt werden, wird kein `ADDSKED` gesendet.
Eine feste Ersatzfrequenz wie `144.300` wird bewusst nicht verwendet. Eine technisch erfolgreiche Übergabe mit falschem Band oder falscher QRG wäre im Contestbetrieb schlechter als eine sichtbar ausgelassene Übergabe.
Der interne Sked bleibt in jedem Fall erhalten. Das gilt auch bei einer ungültigen Broadcast-Adresse, einem Netzwerkfehler oder einem nicht erreichbaren Win-Test-Client.
#### Behandlung von KST-Rufzeichensuffixen
KST-Suffixe kennzeichnen häufig den verwendeten Chat-Login oder ein Band. Sie gehören nicht in jedem Fall zum Logrufzeichen. Für Win-Test entfernt KST4Contest deshalb einen mit `-` abgetrennten KST-Suffix, erhält aber portable und internationale Rufzeichenbestandteile:
| Rufzeichen im KST-Chat | Übergabe an Win-Test |
|---|---|
| `DN9APW-2` | `DN9APW` |
| `9A0BB-70` | `9A0BB` |
| `EA5/G8MBI/P-70` | `EA5/G8MBI/P` |
| `DN9APW-2/P` | `DN9APW/P` |
Innerhalb von KST4Contest bleibt das vollständige Rufzeichen erhalten. Timeline, Reminder-PMs und Chat-Kategorie beziehen sich weiterhin auf den konkret ausgewählten Login.
#### Mode, Zeitpunkt und Notizen
Der Mode wird beim Anlegen des Skeds ausdrücklich als `SSB` oder `CW` gewählt. Eine automatische Ableitung aus der QRG findet nicht statt, weil eine begrenzte Liste angenommener Bandsegmente nicht alle unterstützten VHF-, UHF- und SHF-Bänder zuverlässig abbilden kann.
KST4Contest überträgt den tatsächlichen Sked-Zeitpunkt ohne einen zusätzlichen Minutenversatz. Die Notizen enthalten soweit bekannt Locator und QTF sowie den Hinweis, dass der Sked über KST4Contest angelegt wurde.
Für die Übergabe sendet KST4Contest die Win-Test-Pakete `LOCKSKED`, `ADDSKED` und `UNLOCKSKED`.
![Von KST4Contest an Win-Test übergebener Sked](wintest_sked_handover.png)
#### Einstellungen
Im Reiter **Log sync**:
- `Receive Win-Test network based UDP log messages`
- `UDP-Port for Win-Test listener`, standardmäßig `9871`
- `KST station name in Win-Test network (src of SKED packets)`
- `Win-Test network broadcast address`
Im Reiter **TRX sync**:
- `Win-Test STATUS QRG Sync`
- `Use pass frequency from Win-Test STATUS`
- `Win-Test station name filter`
Das Win-Test-Netzwerk muss in Win-Test aktiviert sein. Bei mehreren Computern muss die Broadcast-Adresse das betreffende lokale Netzwerk erreichen. Der Stationsname sollte die sendende KST4Contest-Instanz innerhalb des Win-Test-Netzwerks eindeutig erkennen lassen.
Ausführliche Beschreibung der Einstellungen: [Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener-ab-v131)
---
## TRX-Frequenz-Synchronisation
+35 -9
View File
@@ -205,18 +205,44 @@ New settings section with the following options:
## Win-Test Network Listener (from v1.31)
A dedicated listener for Win-Test-specific UDP packets. Enables:
The Win-Test network listener processes the native Win-Test UDP protocol. It is independent of the general QSO UDP listener on port `12060` and has three separate tasks:
- **Log synchronisation**: Worked stations are retrieved from Win-Test and marked in the user list.
- **Frequency parsing**: The current TRX frequency from Win-Test populates the `MYQRG` variable.
- **Sked handover (SKED push)**: Skeds from KST4Contest are passed directly to Win-Test via UDP. Win-Test's default UDP broadcast port (9871) is used.
- processing QSOs including band and locator information,
- processing STATUS packets for the local QRG, and
- handing skeds over to the Win-Test network.
Settings:
- **Enable/Disable**: Checkbox in Preferences (from v1.40).
- **Port**: Configurable UDP port for the Win-Test listener.
- **Sked UDP address and port**: Target address and port for SKED handover to Win-Test.
### Log sync settings
> **Note**: The Win-Test listener is an **additional** listener the standard QSO UDP broadcast listener on port 12060 remains independent.
| Setting | Function |
|---|---|
| **Receive Win-Test network based UDP log messages** | Enables the Win-Test network listener. When the listener is enabled, pressing **Create sked** also attempts the Win-Test handover. |
| **UDP-Port for Win-Test listener** | Port used by the Win-Test network. The default is `9871`. The same port is used for the sked handover. |
| **KST station name in Win-Test network (src of SKED packets)** | Station name used by KST4Contest when sending sked packets. A unique name should be used in a network containing several clients. |
| **Win-Test network broadcast address** | Destination address for outgoing Win-Test network packets. In a local network, the address must be reachable by the Win-Test computer. |
The broadcast address is configurable because `255.255.255.255` is not forwarded reliably through every station network or network interface. In a multi-computer setup, the directed broadcast address belonging to the station network may be required instead.
### TRX sync settings
| Setting | Function |
|---|---|
| **Win-Test STATUS QRG Sync** | Takes the current frequency from Win-Test STATUS packets and uses it as the local QRG. |
| **Use pass frequency from Win-Test STATUS** | Uses the transmitted pass frequency instead of the normal TRX QRG. |
| **Win-Test station name filter** | Only processes STATUS packets from the specified Win-Test station. An empty field accepts every station name. |
The station filter is particularly useful when several Win-Test clients are active. Without a filter, the most recently received STATUS packet from another operating position can overwrite the local QRG in KST4Contest.
### Sked handover
There is no separate internal sked mode for the Win-Test handover. When the listener is enabled, **Create sked** attempts the Win-Test transfer in addition to creating the internal sked.
KST4Contest only sends an `ADDSKED` packet when it can determine a QRG which belongs to the explicitly selected band. If no matching QRG is available, the internal sked remains intact and the Win-Test handover is omitted.
`SSB` or `CW` is selected directly in the Further Info section when the sked is created. No automatic mode inference is used.
Click **Save Settings** after making changes so that the port, station name, broadcast address and TRX options are restored at the next start.
Data handling and QRG selection: [Log Synchronisation Win-Test](en-Log-Sync#win-test)
---
+219 -27
View File
@@ -211,19 +211,86 @@ For selected stations in the user list, there are direct buttons to open the **Q
---
## Sked Reminders with ALERT (from v1.40)
## Skeds and Sked Reminders
A sked reminder service with automatic messages can be activated for each chat member. Configurable interval patterns:
> Available from v1.40; band, callsign and Win-Test handling extended in Nightly / v1.42.
- **2+1 minutes**: Messages at 2 min and 1 min before the sked.
- **5+2+1 minutes**: Messages at 5, 2 and 1 min before the sked.
- **10+5+2+1 minutes**: Messages at 10, 5, 2 and 1 min before the sked.
A sked is more than a reminder tied to a particular time. During a contest, it must become visible early enough, move the agreed station up the priority list and if required remind the remote station as well.
In addition to the automated messages to the remote station, there is an **acoustic and visual notification** for your own operator so no sked is ever missed.
KST4Contest therefore treats three tasks separately:
Activate from the FurtherInfo panel of the corresponding station.
1. The sked is stored internally and included in the priority calculation.
2. The scheduled contact appears in the AP and sked timeline.
3. Automatic private reminder messages can optionally be sent before the agreed time.
When the Win-Test network listener is enabled, KST4Contest also attempts to hand the sked over to Win-Test. A failed handover neither removes nor prevents the internal sked.
### Creating a sked
First select the required station in the user list. The sked controls then appear at the bottom of the **Further Info** section.
| Control | Function |
|---|---|
| **Sked in** | Sets the number of minutes until the sked. Available values are 2 through 15 and 20 minutes. |
| **Band** | Selects the sked band. The dropdown contains the local bands enabled under **Station → my station uses …**. |
| **Mode** | Sets the mode passed to Win-Test. Available values are `SSB` and `CW`. This selection does not affect the internal sked or reminder PMs. |
| **Create sked** | Creates the internal sked and, if the Win-Test network listener is enabled, also attempts the Win-Test handover. |
| **Remind-PM in** | Enables automatic private reminder messages before the sked. |
| **2+1**, **5+2+1**, **10+5+2+1** | Selects how many minutes before the sked the reminder PMs are sent. |
![Sked controls in the Further Info section](sked_controls.png)
KST4Contest attempts to preselect a useful band. It checks the following information in this order:
1. a QRG of the selected station which is no more than 30 minutes old and belongs to a locally enabled band,
2. an unambiguous band designator in the station's name field, and
3. the first locally enabled band.
Active callsign variants belonging to the same base callsign are evaluated together when looking for recent band information. A manual NOT-QRV mark is taken into account by the automatic selection. The operator can still select another band explicitly when a different arrangement has been made.
### Effect on the Priority Score
A stored sked raises the score of the normalised base callsign:
| Time relative to the sked | Contribution to the score |
|---|---:|
| more than 15 minutes before the sked | `+40` |
| 15 to 3 minutes before the sked | continuous increase from `+300` towards `+1200` |
| less than 3 minutes before until 1 minute after the sked | `+5000` |
| more than 1 minute after the sked | no remaining sked boost |
The strong weighting immediately around the scheduled time is intentional. An agreed sked should not disappear from the priority list merely because another station is currently very active but has no fixed appointment.
The score is calculated for the base callsign. A sked created for `DN9APW-2` therefore also affects the shared score of other active `DN9APW` variants. The actual message target nevertheless remains `DN9APW-2` in the chat category selected when the sked was created.
The sked is removed from the internal list five minutes after its scheduled time.
### Reminder PMs
Reminder PMs are only scheduled when **Remind-PM in** is enabled. Depending on the selected pattern, KST4Contest sends a private message such as the following two and one minute before the sked:
```text
[KST4C Autoreminder] sked in 2 min
```
The message is sent to the complete visible KST callsign in the chat category in which the sked was created. A sked for `DN9APW-2` is therefore not accidentally sent to `DN9APW`, `DN9APW-70` or a similarly named station in another category.
When a reminder is actually triggered, KST4Contest also displays the visual **SKED** indication. If simple notification sounds are enabled, a short sound is played as well. Merely arming a reminder does not start the blinking indication.
Creating a new set of reminders for the same complete callsign replaces the previously scheduled reminders for that callsign.
### Storage and limitations
Skeds and reminder schedules are kept in memory only. Any skeds which are still required must be recreated after restarting KST4Contest.
The automatic band selection is derived from available chat information. It cannot prove that the station is still operating on the most recently mentioned QRG. Check the band, time and mode before pressing **Create sked**.
Operation: [Station Info Panel](en-User-Interface#station-info-panel-further-info)
Display: [AP and Sked Timeline](#ap-and-sked-timeline)
Win-Test handover: [Log Synchronisation Win-Test](en-Log-Sync#win-test)
---
## QSO Sniffer (from v1.31)
@@ -233,17 +300,22 @@ Configuration: [Configuration Sniffer Settings](en-Configuration#sniffer-set
---
## Win-Test Integration (from v1.31, fully configurable from v1.40)
## Win-Test Integration
KST4Contest fully supports [Win-Test](https://www.win-test.com/) as a logging programme:
KST4Contest uses a dedicated listener for the native Win-Test network protocol. It provides three separate functions:
- **Log synchronisation**: Worked stations are automatically retrieved from Win-Test and marked in the user list.
- **Frequency parsing**: The current TRX frequency is read from Win-Test UDP packets and populates the `MYQRG` variable.
- **Sked handover (SKED push via UDP)**: Agreed skeds from KST4Contest can be pushed directly to Win-Test, so the remote callsign appears in Win-Test's sked window.
- importing new QSOs including band and, where available, locator information,
- reading the current QRG from Win-Test STATUS packets, and
- handing internally created skeds over to the Win-Test network as `ADDSKED` packets.
Details: [Configuration Win-Test Network Listener](en-Configuration#win-test-network-listener)
The sked handover does not replace a missing QRG with a fixed default frequency. KST4Contest only sends a Win-Test sked when it can determine a QRG which belongs to the selected band. The internal sked, timeline and reminder PMs continue to work independently.
A visible KST suffix such as `-2`, `-70` or `-144` is retained inside KST4Contest but removed from the callsign passed to the Win-Test log. Portable components such as `/P`, `/M` and country prefixes are preserved.
Setup and data handling: [Log Synchronisation Win-Test](en-Log-Sync#win-test)
Settings: [Win-Test Network Listener](en-Configuration#win-test-network-listener-from-v131)
---
## PSTRotator Interface (from v1.31, fully configurable from v1.40)
@@ -379,16 +451,55 @@ Related settings:
---
## AP Timeline (from v1.40)
## AP and Sked Timeline
A visual timeline shows up to 4 highly-scored stations per minute slot that should be workable via aircraft scatter. Prioritisation criteria:
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:
- **Highest reflection potential** is preferred (not necessarily the fastest arrival).
- Stations towards which your antenna is not pointing are shown **transparently**.
- When is an interesting AP opportunity expected?
- Which previously agreed sked is approaching independently of that opportunity?
Events further in the future appear on the right. As time passes, they move left towards the current time.
![AP candidates and skeds in the timeline](sked_timeline.png)
### AP candidates
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.
The colour of an AP marker represents the reflection potential:
| Colour | Reflection potential |
|---|---:|
| Magenta | at least 95% |
| Red | at least 75% |
| Yellow | at least 50% |
| Blue | below 50% |
The colour is not a QSO probability. It represents the AirScout value for the calculated reflection geometry.
Clicking an AP candidate selects the corresponding active chat member, including its callsign suffix and chat category. A suitable message can then be prepared immediately.
### Skeds
Skeds appear as diamonds in the lower lane. Their labels use the complete KST callsign, for example `SKED: DN9APW-2`. This makes it clear which particular login was selected for the scheduled contact.
A sked tooltip shows at least:
- the complete KST callsign,
- the agreed band, and
- the QTF towards the remote station.
Where suitable AirScout data is available, the tooltip also includes current AP reachability and the next calculated AP opportunity.
### Antenna direction
When the QTF of an event is clearly outside the current antenna direction, its marker becomes more transparent. The label remains readable. A target close to the centre of the configured antenna beam is highlighted.
This visual effect changes neither the sked nor the Priority Score. It is simply a quick way of identifying candidates which fit the current antenna direction.
The timeline is a preview. AirScout data can change, and a stored sked guarantees neither a clear frequency nor an actual propagation path.
This gives the contest operator a quick overview of which stations will be reachable via which aircraft and at what time.
---
## Interval Beacon
@@ -398,13 +509,83 @@ Automatic CQ messages in the public channel at a configurable interval. Recommen
## Simplelogfile
File-based log evaluation using regex. Details: [Log Synchronisation](Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
File-based log evaluation using regex. Details: [Log Synchronisation](en-Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Cluster & QSO of Others
## Global Message Views
A separate window showing the QSO flow between other stations. Particularly interesting during quieter night-time hours of a contest. This window can be minimised when not needed. Future plan: filtering to stations in your selected QTF.
Most message tables in KST4Contest are deliberately tied either to the local station or to the station currently selected in the user list. Some message streams must, however, remain visible independently of that selection.
KST4Contest therefore provides three global message tabs below the main user list:
| Tab | Content |
|---|---|
| **Public messages** | Public chat messages, including CQ calls and beacon messages |
| **DXCluster messages** | DX cluster messages delivered by the ON4KST server |
| **QSO of the other** | Directed chat messages between chat logins other than the local station |
**Public messages** is selected by default. Changing the selected station does not affect any of these three views.
![Global message tabs below the main user list](global_message_tabs.png)
### DXCluster messages
The DX cluster table shows cluster messages received through the ON4KST connection. Depending on the information contained in the source message, the table displays:
- the time,
- the reporting station and its locator,
- the reported station and its locator,
- the QRG,
- the message text, and
- the global Worked state of the reported station.
An empty locator or another empty field does not necessarily indicate a processing error. The corresponding information may simply be absent from the source message.
This view must not be confused with the [built-in DX Cluster server](en-DX-Cluster-Server). The built-in server sends derived direction spots to connected logging software. The **DXCluster messages** tab displays cluster traffic received from ON4KST.
### QSO of the other
The **QSO of the other** table displays directed chat messages for which neither the sender nor the receiver is the local station. Messages addressed to `ALL` are not included.
The table contains the following columns:
| Column | Meaning |
|---|---|
| **Time** | Time of the chat message |
| **Call TX** | Complete callsign of the sender |
| **Last QRG TX** | Most recently detected QRG assigned to the sender |
| **wkd TX?** | Global Worked state of the sender |
| **Call RX** | Complete callsign of the receiver |
| **Last QRG RX** | Most recently detected QRG assigned to the receiver |
| **wkd RX?** | Global Worked state of the receiver |
| **Message** | Message text |
| **Category** | Chat category in which the message was received |
The QRG columns are not a historical record of the frequency used for the displayed message. They show the latest QRG currently known for the respective chat member. The value may originate from another message and may change when a newer QRG is detected.
The two Worked columns show the global callsign state. They do not indicate whether the station has already been worked on the QRG or band shown next to it.
The expression “QSO of the other” is used as a compact user-interface label. A directed chat message does not prove that an actual radio QSO has taken place. It may equally be a sked request, a frequency exchange or another private message between two chat logins.
### Separate monitor window
The DX cluster and QSO-of-the-other tables are additionally available in a separate monitor window. It places the DX cluster table above the directed messages between other stations.
![Separate monitor window for DX cluster traffic and directed messages between other stations](cluster_qso_monitor.png)
The tabs and the monitor window use the same underlying message stores. Opening the separate window does not create another connection, receive the messages a second time or maintain an independent history.
The window can be hidden or restored through:
**Windows → Hide cluster / stranger QSOs**
or:
**Windows → Show cluster / stranger QSOs**
The additional window is useful when these message streams should remain visible on a second monitor or while another part of the main window is being used. During periods with heavy chat traffic, the global tabs are usually more compact.
When a table cell cannot display its complete message, moving the mouse over the cell shows the full text in a tooltip. Web links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
---
@@ -426,9 +607,20 @@ The map works in packaged environments (AppImage, Flatpak) without internet acce
---
## Optimised Message Handling / 30,000 Message Limit (from v1.41)
## Bounded Message Stores (from v1.41)
The internal chat and message tables are capped at **30,000 entries**. Older messages are automatically discarded when the limit is reached. This keeps memory usage and rendering performance stable during multi-day contest operations.
KST4Contest keeps the received chat and DX cluster messages in two separate bounded memory stores. The limits apply to the stored messages, not independently to every table displaying them.
| Message store | Maximum size | Size after automatic cleanup |
|---|---:|---:|
| Chat messages | 30,000 | 25,000 |
| DX cluster messages | 10,000 | 8,000 |
When a store exceeds its maximum size, the oldest entries are removed until the cleanup size is reached. Newer messages remain available and are displayed first.
The public-message table, private-message views, station-related message views and **QSO of the other** table are filtered views of the same chat-message store. They do not each retain another 30,000 messages. The DX cluster tab and the separate monitor window likewise share the same DX cluster store.
These message stores are held in memory and are not written to the internal database. After restarting KST4Contest, they are rebuilt from messages received during the new session.
---
+65 -23
View File
@@ -86,35 +86,77 @@ When broadcasting the complete logbook, DXLog.net uses `contactreplace` instead
### 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`.
For a new QSO, KST4Contest imports the callsign and resolves the native Win-Test band ID. This includes 50 and 70 MHz. If the packet contains a valid locator, the worked grid square is also stored for the detected band.
#### QSO and Worked synchronisation
**Advantages of Win-Test Integration:**
- **Per-band Worked data:** New QSOs set the Worked mark for the band reported by Win-Test and update the grid-square status where a locator is available.
- 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).
For a new QSO, KST4Contest imports:
**Settings in the "Log Synchronisation" tab:**
- Enable `Receive Win-Test network based UDP log messages`.
- `UDP-Port for Win-Test listener` (default: 9871).
- `KST station name in Win-Test network (src of SKED packets)`: Defines the station name KST4Contest uses in the WT network (e.g. "KST").
- `Win-Test network broadcast address`: Usually detected automatically; required to send sked packets to the network.
- the logged callsign,
- the native Win-Test band ID, and
- a valid locator where one is included in the packet.
**Settings in the "TRX Synchronisation" tab:**
- `Win-Test STATUS QRG Sync`: When enabled, KST4Contest takes the current transceiver frequency from the Win-Test STATUS packet and uses it as your own QRG (MYQRG).
- `Use pass frequency from Win-Test STATUS`: Instead of the main TRX frequency, the pass frequency contained in the STATUS packet is used as MYQRG (useful for multi-op setups that operate with a dedicated pass QRG).
- `Win-Test station name filter`: If a name is entered here (e.g. "STN1"), KST4Contest only processes packets from that specific Win-Test instance. Leave empty to accept all.
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
+64 -4
View File
@@ -111,7 +111,24 @@ The current **Priority score** of the selected station is displayed in the same
**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.
A sked and the corresponding **sked reminders** can be created underneath these controls. An approaching sked raises the Priority Score over time and receives very high priority immediately before the scheduled contact.
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).
---
@@ -141,15 +158,58 @@ Calculation and limitations: [Priority Score and Priority List](en-Features#prio
## Cluster & QSO of Others
Separate window (can be minimised). Shows the communication flow between other stations interesting during quieter contest periods.
## Global Message Tabs and Monitor Window
Three global message tabs are located below the main user list. Unlike the **Further Info** panel, their contents do not depend on the station currently selected.
| Tab | Displayed messages |
|---|---|
| **Public messages** | All public chat messages, including CQ calls and beacons |
| **DXCluster messages** | DX cluster messages received from the ON4KST server |
| **QSO of the other** | Directed messages between chat logins other than the local station |
The **Public messages** tab is selected by default.
![Global message tabs below the main user list](global_message_tabs.png)
The **DXCluster messages** table contains the time, reporting and reported stations, locators, QRG, message text and global Worked state where these values are available in the received message.
The **QSO of the other** table contains:
- the complete sender and receiver callsigns,
- the latest QRG currently known for each station,
- the global Worked state of each station,
- the message text, and
- the chat category.
The displayed QRG is not necessarily the frequency on which the stations intend to make a contact. It is the latest QRG currently associated with the respective chat member. The Worked state is global and not specific to the displayed QRG or band.
A directed chat message in this table does not prove that a radio QSO has taken place. The table also contains sked requests, frequency exchanges and other directed messages between third-party chat logins.
### Separate monitor window
The DX cluster and QSO-of-the-other tables can also be displayed together in a separate window.
![Separate monitor window for DX cluster traffic and directed messages between other stations](cluster_qso_monitor.png)
The separate window and the tabs use the same underlying messages. Hiding the window does not stop message processing or remove messages from the tabs.
Use **Windows → Hide cluster / stranger QSOs** to hide the window and **Windows → Show cluster / stranger QSOs** to restore it.
If a message is too long for its table cell, moving the mouse over the cell displays the complete text in a tooltip. Links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
---
## Menu
### Window
- **Use Dark Mode** (from v1.26): Toggle dark colour scheme on/off.
### Windows
- **Hide cluster / stranger QSOs** hides the separate monitor window for DX cluster messages and directed messages between other stations.
- **Show cluster / stranger QSOs** restores the monitor window.
- **hide options** hides the settings window.
- **show options** restores the settings window.
- **Use dark mode design** activates the dark colour scheme.
- **Use default mode design** restores the default colour scheme.
---
## Window Sizes and Dividers
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 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: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

@@ -5,13 +5,19 @@ import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import kst4contest.model.Band;
import kst4contest.model.ChatMember;
/**
* Sends periodical path requests and an AirScout watchlist for the currently
* active ON4KST stations.
@@ -24,8 +30,17 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
private static final String BROADCAST_ADDRESS = "255.255.255.255";
private final ChatController client;
/*
* ASWATCHLIST is sent as one common list. Remember one syntactically valid
* AirScout band value so an empty list can still be sent on a later cycle
* to remove stations which are no longer active.
*/
private String lastWatchListBandValue;
public AirScoutPeriodicalAPReflectionInquirerTask(
ChatController client
) {
@@ -50,9 +65,6 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
client.getChatPreferences().getAirScout_asClientNameString();
String serverIdentifier =
client.getChatPreferences().getAirScout_asServerNameString();
String bandValue =
client.getChatPreferences().getAirScout_asBandString();
String ownCallSign = normalizeOwnCallSign(
client.getChatPreferences().getStn_loginCallSign()
);
@@ -79,14 +91,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
+ "\" \"" + serverIdentifier + "\" ";
String ownStation = ownCallSign + "," + ownLocator;
StringBuilder watchListMessage = new StringBuilder(
watchListPrefix
+ bandValue
+ ","
+ ownStation
);
List<ChatMember> activeMembers = client.snapshotChatMembers();
List<String> watchListTargets = new ArrayList<>();
Set<String> processedCallsigns = new LinkedHashSet<>();
String watchListBandValue = null;
int port = client.getChatPreferences()
.getAirScout_asCommunicationPort();
@@ -102,11 +110,38 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
continue;
}
if (member.getQrb()
if (member.getQrb() == null
|| member.getQrb()
>= client.getChatPreferences().getStn_maxQRBDefault()) {
continue;
}
String callsignKey = member.getCallSignRaw();
if (callsignKey == null || callsignKey.isBlank()) {
callsignKey = member.getCallSign();
}
if (callsignKey == null
|| !processedCallsigns.add(
callsignKey.trim().toUpperCase(Locale.ROOT)
)) {
continue;
}
/*
* The resolver may deliberately return an exact QRG. AirScout must
* only see a canonical protocol band value such as 4320000.
*/
String bandValue = canonicalizeAirScoutBandValue(
client.resolveAirScoutBandValue(member)
);
if (bandValue == null) {
continue;
}
if (watchListBandValue == null) {
watchListBandValue = bandValue;
}
String targetStation =
member.getCallSign() + "," + member.getQra();
@@ -126,6 +161,30 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
pathQuery
);
watchListTargets.add(targetStation);
}
/*
* AirScout keeps one watchlist per client/server pair. Do not send
* separate lists for the individual station bands because a later
* list would replace stations from an earlier one.
*
* If there are no targets in this cycle, reuse the last valid band
* token and send an empty list so AirScout can clear stale entries.
*/
if (watchListBandValue == null) {
watchListBandValue = lastWatchListBandValue;
}
if (watchListBandValue != null) {
StringBuilder watchListMessage = new StringBuilder(
watchListPrefix
+ watchListBandValue
+ ","
+ ownStation
);
for (String targetStation : watchListTargets) {
watchListMessage
.append(",")
.append(targetStation);
@@ -139,6 +198,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
port,
watchListMessage.toString()
);
lastWatchListBandValue = watchListBandValue;
}
} catch (IOException exception) {
LOGGER.log(
Level.WARNING,
@@ -148,6 +211,60 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
}
}
/**
* Converts a frequency-like value returned by the station resolver into the
* canonical band token expected by the AirScout UDP protocol.
*
* <p>The internal resolver may keep an exact working frequency for path
* analysis. This method removes that precision only at the AirScout protocol
* boundary. For example, {@code 4321740} is sent to AirScout as
* {@code 4320000}.</p>
*
* @param resolvedValue frequency-like AirScout value produced by the resolver
* @return canonical AirScout band value, or {@code null} if unsupported
*/
private String canonicalizeAirScoutBandValue(String resolvedValue) {
if (resolvedValue == null || resolvedValue.isBlank()) {
return null;
}
String normalizedValue = resolvedValue.trim();
if ("off".equalsIgnoreCase(normalizedValue)
|| "auto".equalsIgnoreCase(normalizedValue)) {
return null;
}
final long numericValue;
try {
numericValue = Long.parseLong(normalizedValue);
} catch (NumberFormatException exception) {
LOGGER.log(
Level.WARNING,
"Unsupported AirScout band value: " + resolvedValue,
exception
);
return null;
}
double frequencyMHz = numericValue / 10_000.0;
Band band = Band.fromFrequency(frequencyMHz);
if (band == null) {
LOGGER.warning(
"AirScout query skipped because frequency "
+ frequencyMHz
+ " MHz does not belong to a supported band."
);
return null;
}
return band.getPrefix() + "0000";
}
/**
* Removes the ON4KST login suffix because AirScout expects the actual
* station callsign, for example 9A1W instead of 9A1W-2.
@@ -318,7 +318,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
}
public void stopRotator() {
if (rotatorClient != null) {
rotatorClient.stop();
@@ -451,8 +450,15 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
chatPreferences.getAirScout_asClientNameString();
String serverIdentifier =
chatPreferences.getAirScout_asServerNameString();
String bandValue =
chatPreferences.getAirScout_asBandString();
String bandValue = resolveAirScoutBandValue(remoteChatMember);
if (bandValue == null) {
System.out.println(
"[AirScout, info]: Show-path request ignored because no "
+ "usable propagation frequency could be resolved."
);
return;
}
int port =
chatPreferences.getAirScout_asCommunicationPort();
@@ -499,6 +505,30 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
}
}
/**
* Resolves the AirScout protocol frequency for one station. In automatic mode
* this uses the same station-specific resolution as the internal path analysis;
* manual mode keeps the configured forced value for compatibility.
*
* @param remoteChatMember target station
* @return AirScout frequency in 100-Hz units, or {@code null} if auto mode has
* no safe result
*/
public String resolveAirScoutBandValue(ChatMember remoteChatMember) {
if (!chatPreferences.isAirScout_autoBandSelectionEnabled()) {
return chatPreferences.getAirScout_asBandString();
}
if (reachabilityService == null) {
return null;
}
var resolution = reachabilityService
.resolveAutomaticPropagationFrequency(remoteChatMember);
return resolution == null ? null : resolution.getAirScoutBandValue();
}
/**
* starts the calculation scheduler for scores / priorities of skeds to be made
*/
@@ -773,24 +803,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
new java.util.concurrent.ConcurrentHashMap<>();
/** Tracks the last time WE sent a message containing a QRG to a specific callsign (UPPERCASE).
* Compared against knownActiveBands.timestampEpoch to decide whose QRG to use in a SKED. */
private final Map<String, Long> lastSentQRGToCallsign =
new java.util.concurrent.ConcurrentHashMap<>();
/** Call this whenever we send a PM to {@code receiverCallsign} that contains our QRG. */
public void recordOutboundQRG(String receiverCallsign) {
if (receiverCallsign == null) return;
lastSentQRGToCallsign.put(receiverCallsign.trim().toUpperCase(), System.currentTimeMillis());
System.out.println("[ChatController] Recorded outbound QRG to: " + receiverCallsign);
}
/** Returns epoch-ms of when we last sent our QRG to this callsign, or 0 if never. */
public long getLastSentQRGTimestamp(String callsign) {
if (callsign == null) return 0L;
return lastSentQRGToCallsign.getOrDefault(callsign.trim().toUpperCase(), 0L);
}
private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
private ScheduledExecutorService scoreScheduler;
private final StationMetricsService stationMetricsService = new StationMetricsService();
@@ -814,159 +826,421 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
}
/**
* Pushes a sked to Win-Test via UDP broadcast (LOCKSKED / ADDSKED / UNLOCKSKED).
* Runs on a background thread to avoid blocking the UI.
* Pushes a sked to Win-Test via UDP broadcast.
*
* <p>The internal sked is independent from this handover. If no frequency can
* be resolved safely for the selected band, the internal sked remains active,
* but no misleading Win-Test entry is created.</p>
*/
private void pushSkedToWinTest(ContestSked sked) {
new Thread(() -> {
try {
InetAddress broadcastAddr = InetAddress.getByName(
chatPreferences.getLogsynch_wintestNetworkBroadcastAddress());
int port = chatPreferences.getLogsynch_wintestNetworkPort();
String stationName = chatPreferences.getLogsynch_wintestNetworkStationNameOfKST();
Double frequencyKHz = resolveSkedFrequencyKHz(sked);
WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this);
// Frequency resolution:
// Compare WHO sent a QRG most recently in the PM conversation:
// - OM sent their QRG last use OM's Last Known QRG (ChatMember.frequency)
// - WE sent our QRG last use our own Win-Test QRG (MYQRG)
// Fallback chain if no timestamps exist: OM's Last Known QRG hardcoded default
double freqKHz = -1.0;
final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes
ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign());
// Collect timestamps: when did the OM last mention their QRG? When did WE last send ours?
long omLastQRGTimestamp = 0L;
double omLastQRGMhz = 0.0;
if (targetMember != null && sked.getBand() != null) {
ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand());
if (fi != null && fi.frequency > 0
&& (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) {
omLastQRGTimestamp = fi.timestampEpoch;
omLastQRGMhz = fi.frequency;
}
}
long ourLastQRGTimestamp = getLastSentQRGTimestamp(sked.getTargetCallsign());
// Decision: who was more recent?
if (omLastQRGTimestamp > 0 && omLastQRGTimestamp >= ourLastQRGTimestamp) {
// OM mentioned their QRG MORE RECENTLY (or at same time) use their QRG
freqKHz = omLastQRGMhz * 1000.0;
System.out.println("[ChatController] SKED freq: OM sent last → "
+ omLastQRGMhz + " MHz → " + freqKHz + " kHz");
} else if (ourLastQRGTimestamp > 0) {
// WE sent our QRG more recently use our Win-Test QRG
try {
String qrgStr = chatPreferences.getMYQRGFirstCat().get();
if (qrgStr != null && !qrgStr.isBlank()) {
String cleaned = qrgStr.trim().replace(".", "");
double parsed = Double.parseDouble(cleaned) / 100.0;
if (parsed > 50000) {
freqKHz = parsed;
System.out.println("[ChatController] SKED freq: WE sent last → "
+ freqKHz + " kHz (raw: " + qrgStr + ")");
}
}
} catch (NumberFormatException ignored) { }
if (frequencyKHz == null) {
reportSkippedWinTestSked(
sked,
"no recent or configured QRG matches "
+ sked.getBand().getDisplayLabel()
);
return;
}
// Fallback A: OM's Last Known QRG from KST field (if no PM QRG exchange found at all)
if (freqKHz < 0 && targetMember != null) {
try {
String memberQrg = targetMember.getFrequency().get();
if (memberQrg != null && !memberQrg.isBlank()) {
double mhz = Double.parseDouble(memberQrg.trim());
freqKHz = mhz * 1000.0;
System.out.println("[ChatController] SKED freq: fallback Last Known QRG → "
+ mhz + " MHz → " + freqKHz + " kHz");
}
} catch (NumberFormatException ignored) { }
String winTestCallsign =
toWinTestSkedCallsign(
sked.getTargetChatCallsign()
);
if (winTestCallsign == null || winTestCallsign.isBlank()) {
reportSkippedWinTestSked(
sked,
"the target callsign could not be converted"
);
return;
}
// Fallback B: hardcoded default
if (freqKHz < 0) {
freqKHz = 144300.0;
}
InetAddress broadcastAddress = InetAddress.getByName(
chatPreferences
.getLogsynch_wintestNetworkBroadcastAddress()
);
int port =
chatPreferences.getLogsynch_wintestNetworkPort();
String stationName =
chatPreferences
.getLogsynch_wintestNetworkStationNameOfKST();
WinTestSkedSender sender = new WinTestSkedSender(
stationName,
broadcastAddress,
port,
this
);
String targetLocator =
resolveSkedTargetLocator(
sked.getTargetCallsign()
);
// Build notes string with target locator/azimuth info like reference: [JO02OB - 279°]
String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign());
String notes = "sked via KST4Contest";
if (targetLocator != null && !targetLocator.isBlank() && sked.getTargetAzimuth() > 0) {
notes = String.format("[%s - %.0f°] %s", targetLocator, sked.getTargetAzimuth(), notes);
} else if (targetLocator != null && !targetLocator.isBlank()) {
notes = String.format("[%s] %s", targetLocator, notes);
if (targetLocator != null
&& !targetLocator.isBlank()
&& sked.getTargetAzimuth() > 0) {
notes = String.format(
"[%s - %.0f°] %s",
targetLocator,
sked.getTargetAzimuth(),
notes
);
} else if (targetLocator != null
&& !targetLocator.isBlank()) {
notes = String.format(
"[%s] %s",
targetLocator,
notes
);
} else if (sked.getTargetAzimuth() > 0) {
notes = String.format("[%.0f°] %s", sked.getTargetAzimuth(), notes);
notes = String.format(
"[%.0f°] %s",
sked.getTargetAzimuth(),
notes
);
}
// Determine mode: -1 = auto-detect, 0 = CW, 1 = SSB
String modeStr = chatPreferences.getLogsynch_wintestSkedMode();
int modeOverride = -1; // AUTO
if ("CW".equalsIgnoreCase(modeStr)) modeOverride = 0;
else if ("SSB".equalsIgnoreCase(modeStr)) modeOverride = 1;
String configuredMode =
chatPreferences.getLogsynch_wintestSkedMode();
sender.pushSkedToWinTest(sked, freqKHz, notes, modeOverride);
} catch (Exception e) {
System.out.println("[ChatController] Error pushing sked to Win-Test: " + e.getMessage());
e.printStackTrace();
/*
* Win-Test mode IDs:
* 0 = CW
* 1 = SSB
*
* SSB is the safe fallback for missing or obsolete settings.
* The former AUTO mode was frequency-based and could not produce
* reliable results on every supported VHF, UHF and SHF band.
*/
int winTestMode =
"CW".equalsIgnoreCase(configuredMode)
? 0
: 1;
sender.pushSkedToWinTest(
sked,
winTestCallsign,
frequencyKHz,
notes,
winTestMode
);
} catch (Exception exception) {
String message =
"Error pushing sked to Win-Test: "
+ exception.getMessage();
System.out.println(
"[ChatController] " + message
);
onThreadStatus(
"WT-SkedSend",
new ThreadStateMessage(
"WT-SkedSend",
false,
message,
true
)
);
exception.printStackTrace();
}
}, "WinTestSkedPush").start();
}
private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
/**
* Resolves the frequency used for the Win-Test sked.
*
* <ol>
* <li>Newest QRG detected for the target station on the selected band</li>
* <li>Own QRG belonging to the target's chat category</li>
* <li>No result: do not send the Win-Test sked</li>
* </ol>
*/
private Double resolveSkedFrequencyKHz(ContestSked sked) {
if (sked == null || sked.getBand() == null) {
return null;
}
List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
Double targetFrequencyKHz =
resolveRecentTargetFrequencyKHz(sked);
if (targetFrequencyKHz != null) {
System.out.println(
"[ChatController] SKED frequency from target: "
+ targetFrequencyKHz
+ " kHz on "
+ sked.getBand()
);
return targetFrequencyKHz;
}
String ownQrg =
resolveOwnQrgForSkedCategory(
sked.getTargetChatCategory()
);
Double ownFrequencyKHz =
parseSkedFrequencyKHz(
ownQrg,
sked.getBand()
);
if (ownFrequencyKHz != null) {
System.out.println(
"[ChatController] SKED frequency from own category QRG: "
+ ownFrequencyKHz
+ " kHz on "
+ sked.getBand()
);
return ownFrequencyKHz;
}
private String resolveSkedTargetLocator(String targetCallsignRaw) {
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
return null;
}
for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
/**
* Returns the newest QRG detected on the selected band across all active
* suffix and category variants of the base callsign.
*/
private Double resolveRecentTargetFrequencyKHz(ContestSked sked) {
List<ChatMember> variants =
findActiveChatMembersByRawCall(
sked.getTargetCallsign()
);
long now = System.currentTimeMillis();
long newestTimestamp = Long.MIN_VALUE;
Double newestFrequencyKHz = null;
for (ChatMember member : variants) {
if (member == null || member.getKnownActiveBands() == null) {
continue;
}
ChatMember.ActiveFrequencyInfo frequencyInfo =
member.getKnownActiveBands().get(
sked.getBand()
);
if (frequencyInfo == null
|| frequencyInfo.frequency <= 0.0
|| !sked.getBand().isPlausible(
frequencyInfo.frequency
)) {
continue;
}
long ageMs = now - frequencyInfo.timestampEpoch;
if (ageMs < 0L
|| ageMs > BandOpportunityResolver
.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS) {
continue;
}
if (frequencyInfo.timestampEpoch > newestTimestamp) {
newestTimestamp = frequencyInfo.timestampEpoch;
newestFrequencyKHz =
frequencyInfo.frequency * 1000.0;
}
}
return newestFrequencyKHz;
}
/**
* Returns the own QRG belonging to the chat category in which the target
* station was selected.
*/
private String resolveOwnQrgForSkedCategory(
ChatCategory targetCategory) {
if (sameChatCategory(
targetCategory,
getChatCategorySecondChat())) {
return chatPreferences
.getMYQRGSecondCat()
.get();
}
return chatPreferences
.getMYQRGFirstCat()
.get();
}
private boolean sameChatCategory(ChatCategory first,
ChatCategory second) {
return first != null
&& second != null
&& first.getCategoryNumber()
== second.getCategoryNumber();
}
/**
* Parses the QRG formats used by KST4Contest and Win-Test.
*
* <p>Examples: 144.300, 144.300.03, 144300 and 144300.0.</p>
*/
private Double parseSkedFrequencyKHz(String value,
Band expectedBand) {
if (value == null
|| value.isBlank()
|| expectedBand == null) {
return null;
}
String normalized = value
.trim()
.replace(',', '.')
.replaceAll("\\s+", "");
try {
java.util.regex.Matcher groupedFrequency =
java.util.regex.Pattern.compile(
"^(\\d{2,5})\\.(\\d{3})(?:\\.(\\d{1,2}))?$"
).matcher(normalized);
if (groupedFrequency.matches()) {
double frequencyKHz =
Integer.parseInt(groupedFrequency.group(1))
* 1000.0
+ Integer.parseInt(
groupedFrequency.group(2)
);
String subKHzPart =
groupedFrequency.group(3);
if (subKHzPart != null) {
double subKHz =
Integer.parseInt(subKHzPart);
frequencyKHz +=
subKHzPart.length() == 1
? subKHz / 10.0
: subKHz / 100.0;
}
return expectedBand.isPlausible(
frequencyKHz / 1000.0
) ? frequencyKHz : null;
}
double numericValue =
Double.parseDouble(normalized);
if (expectedBand.isPlausible(numericValue)) {
return numericValue * 1000.0;
}
if (expectedBand.isPlausible(
numericValue / 1000.0)) {
return numericValue;
}
} catch (NumberFormatException ignored) {
// Invalid or unsupported QRG format.
}
return null;
}
/**
* Removes KST dash suffixes while retaining ordinary amateur-radio slash
* notation such as /P, /M or a country prefix.
*
* <p>Examples:
* DN9APW-2 -> DN9APW
* EA5/G8MBI/P-70 -> EA5/G8MBI/P
* DN9APW-2/P -> DN9APW/P</p>
*/
private String toWinTestSkedCallsign(String chatCallsign) {
if (chatCallsign == null || chatCallsign.isBlank()) {
return null;
}
String normalized =
chatCallsign.trim().toUpperCase(Locale.ROOT);
return normalized.replaceAll(
"-[^/]*(?=/|$)",
""
);
}
private void reportSkippedWinTestSked(ContestSked sked,
String reason) {
String target =
sked == null
? "unknown station"
: sked.getTargetChatCallsign();
String message =
"Win-Test sked not sent for "
+ target
+ ": "
+ reason
+ ". The internal KST4Contest sked remains active.";
System.out.println(
"[ChatController] " + message
);
onThreadStatus(
"WT-SkedSend",
new ThreadStateMessage(
"WT-SkedSend",
false,
message,
true
)
);
}
private String resolveSkedTargetLocator(
String targetCallsignRaw) {
if (targetCallsignRaw == null
|| targetCallsignRaw.isBlank()) {
return null;
}
for (ChatMember member
: findActiveChatMembersByRawCall(
targetCallsignRaw)) {
String locator = member.getQra();
if (locator != null && !locator.isBlank()) {
return locator.trim().toUpperCase(Locale.ROOT);
return locator
.trim()
.toUpperCase(Locale.ROOT);
}
}
return null;
}
// private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
// return null;
// }
//
// List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
// return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
//
// }
//
// private String resolveSkedTargetLocator(String targetCallsignRaw) {
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
// return null;
// }
//
// String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
//
// for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
// String locator = member.getQra();
// if (locator != null && !locator.isBlank()) {
// return locator.trim().toUpperCase(Locale.ROOT);
// }
// }
//
// return null;
// }
public StationMetricsService getStationMetricsService() {
return stationMetricsService;
}
@@ -1373,9 +1373,12 @@ public class MessageBusManagementThread extends Thread {
newDXCListSender3.setQra(splittedMessageLine[5]);
ChatMember newDXCListReceiver3 = new ChatMember();
// newDXCListReceiver3.setFrequency(splittedMessageLine[4]);
newDXCListReceiver3.setCallSign(splittedMessageLine[4]);
newDXCListReceiver3.setQra(splittedMessageLine[5]);
/*
* MA format:
* MA|0|epoch|sender|receiver|sender locator|receiver locator|
*/
newDXCListReceiver3.setQra(splittedMessageLine[6]);
dxcMsg3.setSender(newDXCListSender3);
dxcMsg3.setReceiver(newDXCListReceiver3);
@@ -1,6 +1,7 @@
package kst4contest.controller;
import kst4contest.logic.BandOpportunityResolver;
import kst4contest.view.map.MapCallsignRawSnapshot;
import kst4contest.logic.PropagationFrequencyResolver;
import java.util.ArrayList;
import java.util.Collections;
@@ -12,9 +13,7 @@ import java.util.function.Consumer;
import javafx.application.Platform;
import kst4contest.locatorUtils.Location;
import kst4contest.model.Band;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.view.map.GeometryOnlyPathAnalysisService;
import kst4contest.view.map.OpenMeteoTerrainProfileProvider;
@@ -22,7 +21,6 @@ import kst4contest.view.map.PathAnalysisRequest;
import kst4contest.view.map.PathAnalysisResult;
import kst4contest.view.map.PathAnalysisService;
import kst4contest.view.map.PathGeometryUtils;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.Objects;
import java.util.Set;
@@ -139,9 +137,11 @@ public final class ReachabilityService {
* @param member best matching ChatMember, may be null when only a map snapshot exists
* @param selectedSnapshot selected map snapshot
* @param fxCallback callback executed on the JavaFX thread
* @param requestedBandOverride operator-selected band, or null for automatic resolution
*/
public void requestPathAnalysisForMap(ChatMember member,
MapCallsignRawSnapshot selectedSnapshot,
Band requestedBandOverride,
Consumer<PathAnalysisResult> fxCallback) {
String ownLocator6 = normalizeLocator6(chatController.getChatPreferences().getStn_loginLocatorMainCat());
@@ -165,22 +165,23 @@ public final class ReachabilityService {
return;
}
double analysisFrequencyMHz = PathGeometryUtils.resolveAnalysisFrequencyMHz(
selectedSnapshot.lastKnownFrequenciesByBand()
);
Band analysisBand;
double analysisFrequencyMHz;
Band analysisBand = Band.fromFrequency(analysisFrequencyMHz);
if (analysisBand != null && !isUsableAutomaticBand(member, analysisBand)) {
analysisFrequencyMHz = Double.NaN;
analysisBand = null;
}
if (requestedBandOverride != null) {
/*
* An explicit operator selection has priority over automatic propagation
* resolution. Exact recent QRG information on that band is still used
* when available; otherwise the band's default analysis frequency is used.
*/
analysisBand = requestedBandOverride;
analysisFrequencyMHz =
resolveAnalysisFrequencyForBand(member, analysisBand);
} else {
PropagationFrequencyResolver.Resolution frequencyResolution =
resolveAutomaticPropagationFrequency(member);
if (!Double.isFinite(analysisFrequencyMHz)
|| analysisFrequencyMHz <= 0.0
|| analysisBand == null) {
Band fallbackBand = resolveAutoBand(member);
if (fallbackBand == null) {
if (frequencyResolution == null) {
dispatchFxCallback(
fxCallback,
PathAnalysisResult.waitingForUsableBand(
@@ -192,8 +193,9 @@ public final class ReachabilityService {
return;
}
analysisBand = fallbackBand;
analysisFrequencyMHz = resolveAnalysisFrequencyForBand(member, fallbackBand);
analysisBand = frequencyResolution.getBand();
analysisFrequencyMHz =
frequencyResolution.getAnalysisFrequencyMHz();
}
PathAnalysisRequest request = buildRequest(
@@ -236,72 +238,32 @@ public final class ReachabilityService {
}
/**
* Resolves the auto reachability band.
*
* <ol>
* <li>Use the lowest band detected in this session.</li>
* <li>If no session band exists and the station is in the microwave category, use 1296 MHz.</li>
* <li>Otherwise use 144 MHz.</li>
* </ol>
* Resolves the auto reachability band through the shared propagation
* frequency selection used by AirScout and path analysis.
*
* @param member member to inspect
* @return resolved band
*/
public Band resolveAutoBand(ChatMember member) {
EnumSet<Band> enabledBands = getEnabledStationBands();
if (enabledBands.isEmpty()) {
return null;
PropagationFrequencyResolver.Resolution resolution =
resolveAutomaticPropagationFrequency(member);
return resolution == null ? null : resolution.getBand();
}
List<ChatMember> variants = resolveCallsignVariants(member);
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(variants, System.currentTimeMillis());
EnumSet<Band> availableOfferedBands = resolution.getAvailableBands();
availableOfferedBands.retainAll(enabledBands);
if (!availableOfferedBands.isEmpty()) {
return availableOfferedBands.stream()
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(null);
}
// Known evidence exists, but every matching band is disabled or NOT QRV.
if (resolution.hasBandEvidence()) {
return null;
}
EnumSet<Band> fallbackBands = EnumSet.copyOf(enabledBands);
fallbackBands.removeAll(resolution.getNotQrvBands());
if (fallbackBands.isEmpty()) {
return null;
}
if (member != null
&& member.getChatCategory() != null
&& member.getChatCategory().getCategoryNumber() == ChatCategory.MICROWAVE
&& fallbackBands.contains(Band.B_1296)) {
return Band.B_1296;
}
if (member != null
&& member.getChatCategory() != null
&& member.getChatCategory().getCategoryNumber() == ChatCategory.FIFTYSEVENTYMHz) {
if (fallbackBands.contains(Band.B_50)) {
return Band.B_50;
}
if (fallbackBands.contains(Band.B_70)) {
return Band.B_70;
}
}
if (fallbackBands.contains(Band.B_144)) {
return Band.B_144;
}
return fallbackBands.stream()
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(null);
/**
* Resolves one automatic band and exact analysis frequency for a station.
*
* @param member any active category variant of the target station
* @return shared propagation resolution, or {@code null} for unsupported data
*/
public PropagationFrequencyResolver.Resolution resolveAutomaticPropagationFrequency(
ChatMember member
) {
return PropagationFrequencyResolver.resolve(
resolveCallsignVariants(member),
getEnabledStationBands(),
System.currentTimeMillis()
);
}
/**
@@ -329,27 +291,7 @@ public final class ReachabilityService {
return variants.isEmpty() ? List.of(member) : variants;
}
/**
* Verifies that an automatically selected map/snapshot frequency belongs to a
* locally enabled band that is still available after NOT-QRV resolution.
* Manual UI band overrides are handled separately and are not changed here.
*/
private boolean isUsableAutomaticBand(ChatMember member, Band band) {
if (band == null || !getEnabledStationBands().contains(band)) {
return false;
}
if (member == null) {
return true;
}
BandOpportunityResolver.Resolution resolution = BandOpportunityResolver.resolve(
resolveCallsignVariants(member),
System.currentTimeMillis()
);
return resolution.getAvailableBands().contains(band);
}
/**
* Stops the background executor.
@@ -541,28 +483,7 @@ public final class ReachabilityService {
}
}
/**
* Resolves the analysis frequency from a map snapshot first, because the map
* aggregates all visible ChatMember variants and often knows the best current
* frequency per band.
*
* @param member fallback member
* @param selectedSnapshot selected map snapshot
* @return analysis frequency in MHz
*/
private double resolveAnalysisFrequencyForSnapshot(ChatMember member, MapCallsignRawSnapshot selectedSnapshot) {
if (selectedSnapshot != null) {
double snapshotFrequencyMHz =
PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
if (Double.isFinite(snapshotFrequencyMHz) && snapshotFrequencyMHz > 0.0) {
return snapshotFrequencyMHz;
}
}
Band fallbackBand = member == null ? Band.B_144 : resolveAutoBand(member);
return resolveAnalysisFrequencyForBand(member, fallbackBand);
}
/**
* Resolves the analysis frequency for one member/band pair.
@@ -575,15 +496,28 @@ public final class ReachabilityService {
* </ol>
*/
private double resolveAnalysisFrequencyForBand(ChatMember member, Band band) {
if (member != null && member.getKnownActiveBands() != null) {
ChatMember.ActiveFrequencyInfo activeFrequencyInfo = member.getKnownActiveBands().get(band);
if (band == null) {
return Double.NaN;
}
ChatMember.ActiveFrequencyInfo latestFrequencyInfo = null;
for (ChatMember variant : resolveCallsignVariants(member)) {
ChatMember.ActiveFrequencyInfo activeFrequencyInfo =
variant.getKnownActiveBands().get(band);
if (activeFrequencyInfo != null
&& Double.isFinite(activeFrequencyInfo.frequency)
&& activeFrequencyInfo.frequency > 0.0) {
return activeFrequencyInfo.frequency;
&& activeFrequencyInfo.frequency > 0.0
&& (latestFrequencyInfo == null
|| activeFrequencyInfo.timestampEpoch > latestFrequencyInfo.timestampEpoch)) {
latestFrequencyInfo = activeFrequencyInfo;
}
}
if (latestFrequencyInfo != null) {
return latestFrequencyInfo.frequency;
}
if (member != null && member.getFrequency() != null && member.getFrequency().getValue() != null) {
double parsedFrequencyMHz = PathGeometryUtils.tryParseFrequencyMHz(member.getFrequency().getValue());
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
@@ -39,26 +39,63 @@ public class WinTestSkedSender {
}
/**
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
* sequence via UDP broadcast.
* Pushes a ContestSked into Win-Test by sending the
* LOCKSKED / ADDSKED / UNLOCKSKED sequence.
*
* @param sked the sked to push
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0)
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST")
* @param sked sked to push
* @param targetCallsign callsign prepared for Win-Test
* @param frequencyKHz operating frequency in kHz
* @param notes optional notes
* @param mode Win-Test mode ID: 0 for CW, 1 for SSB
*/
public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) {
public void pushSkedToWinTest(ContestSked sked,
String targetCallsign,
double frequencyKHz,
String notes,
int mode) {
try {
sendLockSked();
sendAddSked(sked, frequencyKHz, notes, modeOverride);
sendAddSked(
sked,
targetCallsign,
frequencyKHz,
notes,
mode
);
sendUnlockSked();
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false);
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign()
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand());
} catch (Exception e) {
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage());
e.printStackTrace();
reportStatus(
"Sked pushed to WT: " + targetCallsign,
false
);
System.out.println(
"[WinTestSkedSender] Sked pushed: "
+ targetCallsign
+ " at "
+ frequencyKHz
+ " kHz, band="
+ sked.getBand()
+ ", mode="
+ mode
);
} catch (Exception exception) {
reportStatus(
"ERROR pushing sked: "
+ exception.getMessage(),
true
);
System.out.println(
"[WinTestSkedSender] Error pushing sked: "
+ exception.getMessage()
);
exception.printStackTrace();
}
}
@@ -86,46 +123,53 @@ public class WinTestSkedSender {
/**
* Sends an ADDSKED message with the sked details.
* <p>
* Win-Test ADDSKED data format (from wtKST):
* <pre>
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
* </pre>
* <p>
* Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch).
* The C# code adds 60 seconds to compensate.
*
* <p>The wtKST implementation subtracts a reference time of
* 1970-01-01 00:01:00 UTC and subsequently adds 60 seconds. Both
* operations cancel each other out. The transmitted value is therefore
* an ordinary Unix timestamp and must not receive another offset here.</p>
*/
private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception {
// Win-Test timestamp: epoch seconds with 60s offset
long epochSeconds = sked.getSkedTimeEpoch() / 1000;
long wtTimestamp = epochSeconds + 60;
private void sendAddSked(ContestSked sked,
String targetCallsign,
double frequencyKHz,
String notes,
int mode) throws Exception {
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
long wtTimestamp =
sked.getSkedTimeEpoch() / 1000L;
// Win-Test band ID
int bandId = toWinTestBandId(sked.getBand());
// Frequency in 0.1 kHz units.
long frequencyTenthKHz =
Math.round(frequencyKHz * 10.0);
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
int mode;
if (modeOverride >= 0) {
mode = modeOverride;
} else {
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
}
int bandId =
toWinTestBandId(sked.getBand());
String data = wtTimestamp
+ " " + freqTenthKHz
/*
* 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
+ " " + mode
+ " \"" + sked.getTargetCallsign() + "\""
+ " " + winTestMode
+ " \"" + targetCallsign + "\""
+ " \"" + (notes != null ? notes : "") + "\"";
WinTestMessage msg = new WinTestMessage(
WinTestMessage message = new WinTestMessage(
WinTestMessage.MessageType.ADDSKED,
stationName, "",
data);
sendUdp(msg);
stationName,
"",
data
);
sendUdp(message);
}
/**
@@ -168,18 +212,6 @@ public class WinTestSkedSender {
};
}
/**
* Very simple SSB segment heuristic.
* A more complete implementation would check actual mode from Win-Test STATUS.
*/
private boolean isInSsbSegment(double frequencyKHz) {
// SSB segments (kHz ranges)
if (frequencyKHz >= 144300 && frequencyKHz <= 144399) return true; // 2m SSB
if (frequencyKHz >= 432200 && frequencyKHz <= 432399) return true; // 70cm SSB
if (frequencyKHz >= 1296200 && frequencyKHz <= 1296399) return true; // 23cm SSB
return false;
}
private void reportStatus(String text, boolean isError) {
if (callback != null) {
callback.onThreadStatus(THREAD_NICKNAME,
@@ -0,0 +1,299 @@
package kst4contest.logic;
import kst4contest.model.Band;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import java.util.Collection;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.List;
/**
* Selects one realistic propagation frequency for a station.
*
* <p>The same resolution is used by AirScout and by the internal path analysis.
* Only the chat categories supported by these features participate. This keeps
* unrelated KST categories from silently falling back to 144 MHz.</p>
*/
public final class PropagationFrequencyResolver {
// private static final double DUAL_VUHF_MICROWAVE_FALLBACK_MHZ = 430.0;
private PropagationFrequencyResolver() {
}
/** Explains why a frequency was selected. */
public enum Source {
CURRENT_QRG,
STATION_NAME,
DUAL_CATEGORY_FALLBACK,
CHAT_CATEGORY
}
/**
* Resolves the frequency from all active category variants of one base
* callsign.
*
* <ol>
* <li>Most recently detected QRG</li>
* <li>Lowest band explicitly named by the station</li>
* <li>432 MHz if the station is present in VUHF and Microwave</li>
* <li>Lowest usable fallback band of the supported chat category</li>
* </ol>
*
* <p>Locally disabled and manually excluded bands are never selected.</p>
*
* @param variants active category variants of one callsign
* @param enabledBands bands enabled for the local station
* @param nowEpochMs current time used for the QRG age check
* @return one resolution, or {@code null} if no safe choice exists
*/
public static Resolution resolve(Collection<ChatMember> variants,
EnumSet<Band> enabledBands,
long nowEpochMs) {
if (variants == null || variants.isEmpty()
|| enabledBands == null || enabledBands.isEmpty()) {
return null;
}
List<ChatMember> supportedVariants = variants.stream()
.filter(PropagationFrequencyResolver::isSupportedVariant)
.toList();
if (supportedVariants.isEmpty()) {
return null;
}
BandOpportunityResolver.Resolution opportunityResolution =
BandOpportunityResolver.resolve(supportedVariants, nowEpochMs);
EnumSet<Band> usableBands = EnumSet.copyOf(enabledBands);
usableBands.removeAll(opportunityResolution.getNotQrvBands());
if (usableBands.isEmpty()) {
return null;
}
FrequencyCandidate latestQrg = findLatestQrg(
supportedVariants,
usableBands,
nowEpochMs
);
if (latestQrg != null) {
return new Resolution(
latestQrg.band,
latestQrg.frequencyMHz,
Source.CURRENT_QRG
);
}
EnumSet<Band> nameBands = EnumSet.noneOf(Band.class);
for (ChatMember variant : supportedVariants) {
nameBands.addAll(
BandOpportunityResolver.detectBandsFromStationName(variant.getName())
);
}
nameBands.retainAll(usableBands);
Band nameBand = lowestBand(nameBands);
if (nameBand != null) {
return new Resolution(
nameBand,
nameBand.getDefaultAnalysisFrequencyMHz(),
Source.STATION_NAME
);
}
EnumSet<SupportedCategory> categories = collectSupportedCategories(supportedVariants);
if (categories.contains(SupportedCategory.VUHF)
&& categories.contains(SupportedCategory.MICROWAVE)
&& usableBands.contains(Band.B_432)) {
return new Resolution(
Band.B_432,
Band.B_432.getDefaultAnalysisFrequencyMHz(),
Source.DUAL_CATEGORY_FALLBACK
);
}
EnumSet<Band> categoryBands = EnumSet.noneOf(Band.class);
for (SupportedCategory category : categories) {
categoryBands.addAll(category.fallbackBands);
}
categoryBands.retainAll(usableBands);
Band categoryBand = lowestBand(categoryBands);
if (categoryBand == null) {
return null;
}
return new Resolution(
categoryBand,
categoryBand.getDefaultAnalysisFrequencyMHz(),
Source.CHAT_CATEGORY
);
}
private static FrequencyCandidate findLatestQrg(List<ChatMember> variants,
EnumSet<Band> usableBands,
long nowEpochMs) {
FrequencyCandidate latest = null;
for (ChatMember variant : variants) {
for (var entry : variant.getKnownActiveBands().entrySet()) {
Band band = entry.getKey();
ChatMember.ActiveFrequencyInfo info = entry.getValue();
if (band == null || info == null || !usableBands.contains(band)) {
continue;
}
long ageMs = nowEpochMs - info.timestampEpoch;
if (ageMs < 0L
|| ageMs > BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS
|| !Double.isFinite(info.frequency)
|| !band.isPlausible(info.frequency)) {
continue;
}
if (latest == null || info.timestampEpoch > latest.timestampEpochMs) {
latest = new FrequencyCandidate(
band,
info.frequency,
info.timestampEpoch
);
}
}
}
return latest;
}
private static boolean isSupportedVariant(ChatMember member) {
if (member == null || member.getChatCategory() == null) {
return false;
}
int categoryNumber = member.getChatCategory().getCategoryNumber();
return categoryNumber == ChatCategory.FIFTYSEVENTYMHz
|| categoryNumber == ChatCategory.VUHF
|| categoryNumber == ChatCategory.MICROWAVE
|| categoryNumber == ChatCategory.EMEJT65;
}
private static EnumSet<SupportedCategory> collectSupportedCategories(
List<ChatMember> variants
) {
EnumSet<SupportedCategory> categories = EnumSet.noneOf(SupportedCategory.class);
for (ChatMember variant : variants) {
int categoryNumber = variant.getChatCategory().getCategoryNumber();
if (categoryNumber == ChatCategory.FIFTYSEVENTYMHz) {
categories.add(SupportedCategory.FIFTY_SEVENTY);
} else if (categoryNumber == ChatCategory.VUHF) {
categories.add(SupportedCategory.VUHF);
} else if (categoryNumber == ChatCategory.MICROWAVE) {
categories.add(SupportedCategory.MICROWAVE);
} else if (categoryNumber == ChatCategory.EMEJT65) {
categories.add(SupportedCategory.EME);
}
}
return categories;
}
private static Band lowestBand(Collection<Band> bands) {
if (bands == null || bands.isEmpty()) {
return null;
}
return bands.stream()
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
.orElse(null);
}
private enum SupportedCategory {
FIFTY_SEVENTY(EnumSet.of(Band.B_50, Band.B_70)),
VUHF(EnumSet.of(Band.B_144, Band.B_432)),
MICROWAVE(EnumSet.of(
Band.B_1296,
Band.B_2320,
Band.B_3400,
Band.B_5760,
Band.B_10G,
Band.B_24G
)),
EME(EnumSet.of(
Band.B_144,
Band.B_432,
Band.B_1296,
Band.B_2320,
Band.B_3400,
Band.B_5760,
Band.B_10G,
Band.B_24G
));
private final EnumSet<Band> fallbackBands;
SupportedCategory(EnumSet<Band> fallbackBands) {
this.fallbackBands = fallbackBands;
}
}
private static final class FrequencyCandidate {
private final Band band;
private final double frequencyMHz;
private final long timestampEpochMs;
private FrequencyCandidate(Band band,
double frequencyMHz,
long timestampEpochMs) {
this.band = band;
this.frequencyMHz = frequencyMHz;
this.timestampEpochMs = timestampEpochMs;
}
}
/** Immutable selected band/frequency pair. */
public static final class Resolution {
private final Band band;
private final double analysisFrequencyMHz;
private final Source source;
private Resolution(Band band,
double analysisFrequencyMHz,
Source source) {
this.band = band;
this.analysisFrequencyMHz = analysisFrequencyMHz;
this.source = source;
}
public Band getBand() {
return band;
}
public double getAnalysisFrequencyMHz() {
return analysisFrequencyMHz;
}
public Source getSource() {
return source;
}
/**
* Converts MHz to AirScout's 100-Hz protocol unit.
*
* @return integer protocol value, for example 1442100 for 144.210 MHz
*/
public String getAirScoutBandValue() {
return Long.toString(Math.round(analysisFrequencyMHz * 10_000.0));
}
}
}
@@ -224,7 +224,7 @@ public class ChatPreferences {
boolean logsynch_wintestNetworkListenerEnabled = true; // default true = bisheriges Verhalten
String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test
boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP
String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO
String logsynch_wintestSkedMode = "SSB"; // Supported values: SSB or CW
boolean logsynch_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet
boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet
@@ -245,6 +245,7 @@ public class ChatPreferences {
boolean AirScout_asUDPListenerEnabled = true;
String AirScout_asServerNameString = "AS";
String AirScout_asClientNameString = "KST";
boolean AirScout_autoBandSelectionEnabled = true;
String AirScout_asBandString = "1440000";
int AirScout_asCommunicationPort = 9872;
@@ -1004,6 +1005,16 @@ public class ChatPreferences {
return AirScout_asBandString;
}
public boolean isAirScout_autoBandSelectionEnabled() {
return AirScout_autoBandSelectionEnabled;
}
public void setAirScout_autoBandSelectionEnabled(
boolean airScoutAutoBandSelectionEnabled
) {
AirScout_autoBandSelectionEnabled = airScoutAutoBandSelectionEnabled;
}
public void setAirScout_asBandString(String airScout_asBandString) {
if (airScout_asBandString == null) {
AirScout_asBandString = "1440000";
@@ -1695,6 +1706,13 @@ public class ChatPreferences {
asQry_airScoutUDPPort.setTextContent(this.getAirScout_asCommunicationPort()+"");
AirScoutQuerier.appendChild(asQry_airScoutUDPPort);
Element asQry_airScoutAutoBandSelectionEnabled =
doc.createElement("asQry_airScoutAutoBandSelectionEnabled");
asQry_airScoutAutoBandSelectionEnabled.setTextContent(
Boolean.toString(this.isAirScout_autoBandSelectionEnabled())
);
AirScoutQuerier.appendChild(asQry_airScoutAutoBandSelectionEnabled);
Element asQry_airScoutBandValue = doc.createElement("asQry_airScoutBandValue");
asQry_airScoutBandValue.setTextContent(this.getAirScout_asBandString());
AirScoutQuerier.appendChild(asQry_airScoutBandValue);
@@ -2508,6 +2526,14 @@ public class ChatPreferences {
)
);
setAirScout_autoBandSelectionEnabled(
getBoolean(
airScoutEl,
AirScout_autoBandSelectionEnabled,
"asQry_airScoutAutoBandSelectionEnabled"
)
);
setAirScout_asBandString(
getText(
airScoutEl,
@@ -2525,6 +2551,8 @@ public class ChatPreferences {
+ AirScout_asClientNameString
+ ", port="
+ AirScout_asCommunicationPort
+ ", automatic band selection="
+ AirScout_autoBandSelectionEnabled
+ ", band="
+ AirScout_asBandString
);
@@ -3,25 +3,58 @@ package kst4contest.model;
/**
* Represents a scheduled event or an AirScout opportunity in the future.
* Used for the Timeline View and Priority Calculation.
*
* <p>The base callsign remains the grouping key for scoring and worked-state
* handling. The exact KST login and its chat category are stored separately
* because reminders and external logger handover refer to the selected
* ChatMember entity.</p>
*/
public class ContestSked {
private String targetCallsign;
private double targetAzimuth; // Required for Antenna-Visuals
private long skedTimeEpoch; // The peak time (e.g., AP)
private String targetChatCallsign;
private ChatCategory targetChatCategory;
private double targetAzimuth;
private long skedTimeEpoch;
private Band band;
// Opportunity potential (0..100). -1 means "unknown".
int opportunityPotentialPercent = -1;
// Status flags to prevent spamming alarms
// Status flags to prevent spamming alarms.
private boolean warning3MinSent = false;
private boolean warningNowSent = false;
public ContestSked(String call, double azimuth, long time, Band b) {
this.targetCallsign = call;
/**
* Backward-compatible constructor.
*/
public ContestSked(String call, double azimuth, long time, Band band) {
this(call, call, null, azimuth, time, band);
}
/**
* Creates a sked for one exact KST login.
*
* @param callRaw base callsign used for scoring and worked states
* @param chatCallsign exact KST login, including an optional dash suffix
* @param chatCategory category in which the selected login is active
* @param azimuth target azimuth
* @param time sked time in epoch milliseconds
* @param band selected amateur-radio band
*/
public ContestSked(String callRaw,
String chatCallsign,
ChatCategory chatCategory,
double azimuth,
long time,
Band band) {
this.targetCallsign = callRaw;
this.targetChatCallsign = chatCallsign;
this.targetChatCategory = chatCategory;
this.targetAzimuth = azimuth;
this.skedTimeEpoch = time;
this.band = b;
this.band = band;
}
/**
@@ -32,15 +65,54 @@ public class ContestSked {
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
}
// Getters and Setters...
public String getTargetCallsign() { return targetCallsign; }
public double getTargetAzimuth() { return targetAzimuth; }
public long getSkedTimeEpoch() { return skedTimeEpoch; }
public Band getBand() { return band; }
public boolean isWarning3MinSent() { return warning3MinSent; }
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
public boolean isWarningNowSent() { return warningNowSent; }
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
/**
* Returns the base callsign used for scoring and worked-state grouping.
*/
public String getTargetCallsign() {
return targetCallsign;
}
/**
* Returns the exact KST login selected when the sked was created.
*/
public String getTargetChatCallsign() {
if (targetChatCallsign == null || targetChatCallsign.isBlank()) {
return targetCallsign;
}
return targetChatCallsign;
}
public ChatCategory getTargetChatCategory() {
return targetChatCategory;
}
public double getTargetAzimuth() {
return targetAzimuth;
}
public long getSkedTimeEpoch() {
return skedTimeEpoch;
}
public Band getBand() {
return band;
}
public boolean isWarning3MinSent() {
return warning3MinSent;
}
public void setWarning3MinSent(boolean warning3MinSent) {
this.warning3MinSent = warning3MinSent;
}
public boolean isWarningNowSent() {
return warningNowSent;
}
public void setWarningNowSent(boolean warningNowSent) {
this.warningNowSent = warningNowSent;
}
public int getOpportunityPotentialPercent() {
return opportunityPotentialPercent;
@@ -178,7 +178,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chatcontroller,
tbl_chatMember,
stationMapView,
this::focusChatMemberAndPrepareCq
this::focusChatMemberAndPrepareCq,
() -> selectedReachabilityBandOverride
);
stationMapBridge.install();
}
@@ -583,6 +584,84 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
};
}
/**
* Chooses a useful initial band for a new sked.
*
* <p>Recent frequency evidence has priority over station-name information.
* Manual NOT-QRV exclusions are respected. The operator can still select
* another locally enabled band from the dropdown.</p>
*/
private Band resolveDefaultSkedBand(ChatMember selectedMember,
EnumSet<Band> enabledBands) {
if (selectedMember == null || enabledBands == null || enabledBands.isEmpty()) {
return null;
}
List<ChatMember> variants =
chatcontroller.findActiveChatMembersByRawCall(
selectedMember.getCallSignRaw()
);
if (variants.isEmpty()) {
variants = List.of(selectedMember);
}
BandOpportunityResolver.Resolution resolution =
BandOpportunityResolver.resolve(
variants,
System.currentTimeMillis()
);
EnumSet<Band> availableBands = resolution.getAvailableBands();
availableBands.retainAll(enabledBands);
Band newestFrequencyBand = null;
long newestTimestamp = Long.MIN_VALUE;
long now = System.currentTimeMillis();
for (ChatMember member : variants) {
if (member == null || member.getKnownActiveBands() == null) {
continue;
}
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry
: member.getKnownActiveBands().entrySet()) {
Band band = entry.getKey();
ChatMember.ActiveFrequencyInfo info = entry.getValue();
if (band == null
|| info == null
|| !availableBands.contains(band)
|| !band.isPlausible(info.frequency)) {
continue;
}
long ageMs = now - info.timestampEpoch;
if (ageMs < 0L
|| ageMs > BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS) {
continue;
}
if (info.timestampEpoch > newestTimestamp) {
newestTimestamp = info.timestampEpoch;
newestFrequencyBand = band;
}
}
}
if (newestFrequencyBand != null) {
return newestFrequencyBand;
}
if (!availableBands.isEmpty()) {
return availableBands.iterator().next();
}
return enabledBands.iterator().next();
}
/**
* This method generates a BoderPane which shows some additional information about a callsign which had been
@@ -667,56 +746,158 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
selectedCallSignDownerSiteGridPane.add(priorityRow, 0, 5, 1, 1);
ChoiceBox<Integer> cbSkedMinutes = new ChoiceBox<>(FXCollections.observableArrayList(2, 3, 4, 5, 6,7,8,9, 10,11,12,13,14, 15, 20));
ChoiceBox<Integer> cbSkedMinutes = new ChoiceBox<>(
FXCollections.observableArrayList(
2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 20
)
);
cbSkedMinutes.getSelectionModel().select(Integer.valueOf(5));
ChoiceBox<String> cbSkedMode = new ChoiceBox<>(FXCollections.observableArrayList("AUTO", "SSB", "CW"));
String configuredSkedMode = this.chatcontroller.getChatPreferences().getLogsynch_wintestSkedMode();
if (configuredSkedMode == null || configuredSkedMode.isBlank()) {
configuredSkedMode = "AUTO";
}
String configuredSkedModeUpper = configuredSkedMode.trim().toUpperCase(java.util.Locale.ROOT);
if (!"AUTO".equals(configuredSkedModeUpper)
&& !"SSB".equals(configuredSkedModeUpper)
&& !"CW".equals(configuredSkedModeUpper)) {
configuredSkedModeUpper = "AUTO";
}
cbSkedMode.setValue(configuredSkedModeUpper);
cbSkedMode.setTooltip(new Tooltip("Mode for Win-Test ADDSKED packets"));
cbSkedMode.setOnAction(e ->
chatcontroller.getChatPreferences().setLogsynch_wintestSkedMode(cbSkedMode.getValue()));
EnumSet<Band> enabledSkedBands =
BandOpportunityResolver.getEnabledStationBands(
chatcontroller.getChatPreferences()
);
ChoiceBox<String> cbReminderOffsets = new ChoiceBox<>(FXCollections.observableArrayList("2+1", "5+2+1", "10+5+2+1"));
ChoiceBox<Band> cbSkedBand = new ChoiceBox<>(
FXCollections.observableArrayList(enabledSkedBands)
);
cbSkedBand.setConverter(new StringConverter<>() {
@Override
public String toString(Band band) {
return band == null ? "" : bandToHumanLabel(band);
}
@Override
public Band fromString(String value) {
return null;
}
});
cbSkedBand.setPrefWidth(75);
cbSkedBand.setTooltip(new Tooltip(
"Band for this sked. The initial value is derived from recent "
+ "QRG information or the station name. Only bands enabled "
+ "for your own station are offered."
));
Band defaultSkedBand =
resolveDefaultSkedBand(
selectedCallSignInfoStageChatMember,
enabledSkedBands
);
if (defaultSkedBand != null) {
cbSkedBand.setValue(defaultSkedBand);
}
ChoiceBox<String> cbSkedMode = new ChoiceBox<>(
FXCollections.observableArrayList("SSB", "CW")
);
String configuredSkedMode =
chatcontroller.getChatPreferences()
.getLogsynch_wintestSkedMode();
if (configuredSkedMode == null
|| (!"SSB".equalsIgnoreCase(configuredSkedMode)
&& !"CW".equalsIgnoreCase(configuredSkedMode))) {
configuredSkedMode = "SSB";
}
cbSkedMode.setValue(
configuredSkedMode.trim().toUpperCase(Locale.ROOT)
);
cbSkedMode.setTooltip(new Tooltip(
"Mode transferred to Win-Test with the ADDSKED packet"
));
cbSkedMode.setOnAction(e ->
chatcontroller.getChatPreferences()
.setLogsynch_wintestSkedMode(
cbSkedMode.getValue()
)
);
ChoiceBox<String> cbReminderOffsets = new ChoiceBox<>(
FXCollections.observableArrayList(
"2+1",
"5+2+1",
"10+5+2+1"
)
);
cbReminderOffsets.getSelectionModel().select("2+1");
CheckBox chkPmReminders = new CheckBox("Remind-PM in ");
Button btnCreateSked = new Button("Create sked");
btnCreateSked.setTooltip(new Tooltip("Creates a sked entry and boosts priority (ramp-up)."));
btnCreateSked.setTooltip(new Tooltip(
"Creates a sked entry and boosts priority during the approach."
));
btnCreateSked.setOnAction(e -> {
ChatMember sel = chatcontroller.getScoreService().selectedChatMemberProperty().get();
if (sel == null) return;
ChatMember selectedMember =
chatcontroller.getScoreService()
.selectedChatMemberProperty()
.get();
if (cbSkedMode.getValue() != null) {
chatcontroller.getChatPreferences().setLogsynch_wintestSkedMode(cbSkedMode.getValue());
if (selectedMember == null) {
return;
}
int minutes = cbSkedMinutes.getValue() == null ? 5 : cbSkedMinutes.getValue();
long skedTime = System.currentTimeMillis() + minutes * 60_000L;
Band selectedBand = cbSkedBand.getValue();
if (selectedBand == null) {
showUserInputErrorWindow(
"No sked band is available. Enable at least one band "
+ "under \"My station uses ...\" before creating a sked."
);
return;
}
double az = sel.getQTFdirection() != null ? sel.getQTFdirection() : 0.0;
if (cbSkedMode.getValue() != null) {
chatcontroller.getChatPreferences()
.setLogsynch_wintestSkedMode(cbSkedMode.getValue());
}
// band is not strictly required for scoring; keep current category context
Band band = Band.B_144; // if you want, replace with a real dropdown later
ContestSked sked = new ContestSked(sel.getCallSignRaw(), az, skedTime, band);
int minutes =
cbSkedMinutes.getValue() == null
? 5
: cbSkedMinutes.getValue();
long skedTime =
System.currentTimeMillis() + minutes * 60_000L;
double azimuth =
selectedMember.getQTFdirection() != null
? selectedMember.getQTFdirection()
: 0.0;
ContestSked sked = new ContestSked(
selectedMember.getCallSignRaw(),
selectedMember.getCallSign(),
selectedMember.getChatCategory(),
azimuth,
skedTime,
selectedBand
);
chatcontroller.addSked(sked);
chatcontroller.getScoreService().requestRecompute("sked-created");
chatcontroller.getScoreService()
.requestRecompute("sked-created");
if (chkPmReminders.isSelected()) {
List<Integer> offsets = parseMinuteOffsets(cbReminderOffsets.getValue());
chatcontroller.getSkedReminderService().armReminders(sel.getCallSignRaw(), sel.getChatCategory(), skedTime, offsets);
List<Integer> offsets =
parseMinuteOffsets(cbReminderOffsets.getValue());
chatcontroller.getSkedReminderService().armReminders(
sked.getTargetChatCallsign(),
sked.getTargetChatCategory(),
skedTime,
offsets
);
}
});
@@ -728,6 +909,13 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
);
skedTimeGroup.setAlignment(Pos.CENTER_LEFT);
HBox skedBandGroup = new HBox(
4,
new Label("Band"),
cbSkedBand
);
skedBandGroup.setAlignment(Pos.CENTER_LEFT);
HBox skedModeGroup = new HBox(
4,
new Label("Mode"),
@@ -750,12 +938,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
skedRow.setAlignment(Pos.CENTER_LEFT);
skedRow.getChildren().addAll(
skedTimeGroup,
skedBandGroup,
skedModeGroup,
btnCreateSked,
skedReminderGroup
);
selectedCallSignDownerSiteGridPane.add(skedRow, 0, 6, 2, 1);
selectedCallSignDownerSiteGridPane.add(
skedRow,
0,
6,
2,
1
);
GridPane.setHgrow(skedRow, Priority.ALWAYS);
@@ -3699,12 +3894,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
public ObservableValue<String> call(CellDataFeatures<ClusterMessage, String> cellDataFeatures) {
SimpleStringProperty locRX = new SimpleStringProperty();
if (cellDataFeatures.getValue().getSender() != null) {
if (cellDataFeatures.getValue().getReceiver() != null) {
locRX.setValue(cellDataFeatures.getValue().getReceiver().getQra());
} else {
locRX.setValue("");// TODO: Prevents a bug of not setting all values as a default
locRX.setValue("");
}
return locRX;
}
@@ -3938,7 +4133,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
});
TableColumn<ChatMessage, String> workedTXCol = new TableColumn<ChatMessage, String>("wkd TX?");
workedRXCol.setCellValueFactory(new Callback<CellDataFeatures<ChatMessage, String>, ObservableValue<String>>() {
workedTXCol.setCellValueFactory(new Callback<CellDataFeatures<ChatMessage, String>, ObservableValue<String>>() {
@Override
public ObservableValue<String> call(CellDataFeatures<ChatMessage, String> cellDataFeatures) {
@@ -5453,23 +5648,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
btnSkedWarnIndicator.setTooltip(tipSkedWarnIndicator);
}
private void maybeShowSkedWarnIndicator(String key, ThreadStateMessage msg) {
if (msg == null) return;
String text = msg.getRunningInformationTextDescription();
if (text == null || text.isBlank()) text = msg.getRunningInformation();
if (text == null || text.isBlank()) return;
String nick = msg.getThreadNickName() == null ? "" : msg.getThreadNickName().toLowerCase(Locale.ROOT);
String k = key == null ? "" : key.toLowerCase(Locale.ROOT);
String t = text.toLowerCase(Locale.ROOT);
boolean isSkedRelated = k.contains("sked") || nick.contains("sked") || t.contains("reminder");
if (!isSkedRelated) return;
final String finalText = text;
Platform.runLater(() -> showBlinkingSkedWarnIndicator(finalText + " SKED!"));
}
private void showBlinkingSkedWarnIndicator(String text) {
// short text for the button; full text in tooltip
@@ -6417,8 +6596,28 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
bPaneChatWindow.setTop(flwpne_StatusBar);
initSkedWarnIndicatorButton();
chatcontroller.lastUiReminderEventProperty().addListener(
(observable, oldValue, reminderEvent) -> {
if (reminderEvent == null) {
return;
}
String text = "REMINDER: "
+ reminderEvent.getCallSignRaw()
+ " T-"
+ reminderEvent.getMinutesBefore()
+ "m";
Platform.runLater(
() -> showBlinkingSkedWarnIndicator(text)
);
}
);
flwpne_StatusBar.getChildren().add(btnSkedWarnIndicator);
initBandUpgradeIndicatorButton();
flwpne_StatusBar.getChildren().add(btnBandUpgradeIndicator);
@@ -7233,6 +7432,17 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
// chatMemberTableFilterQTFAndQRBHbox.set
VBox chatMemberTableFilterVBoxForAllFilters= new VBox();
Button btnResetChatMemberFilters = new Button("Reset filters");
btnResetChatMemberFilters.getStyleClass().clear();
btnResetChatMemberFilters.getStyleClass().addAll(
"button",
"buttonMyQrg1"
);
btnResetChatMemberFilters.setTooltip(
new Tooltip("Disable all station-list filters and show every user")
);
chatMemberTableFilterVBoxForAllFilters.setSpacing(1);
chatMemberTableFilterVBoxForAllFilters.setMinWidth(0);
@@ -7416,20 +7626,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
"-fx-border-radius: 1;" +
"-fx-border-color: lightgrey;");
// chatMemberTableFilterQTFAndQRBHbox.setFillHeight(true);
// chatMemberTableFilterQTFAndQRBHbox.setAlignment(Pos.CENTER_LEFT);
// chatMemberTableFilterQTFAndQRBHbox.getChildren().add(chatMemberTableFilterQRBHBox);
chatMemberTableFilterQTFAndQRBHbox
.getChildren()
.add(chatMemberTableFilterQRBHBox);
chatMemberTableFilterQTFAndQRBHbox.getChildren().addAll(
btnResetChatMemberFilters,
chatMemberTableFilterQRBHBox
);
// HBox chatMemberTableFilterQTFHBox = new HBox();
// FlowPane chatMemberTableFilterQTFHBox = new FlowPane();
// chatMemberTableFilterQTFHBox.setAlignment(Pos.CENTER_LEFT);
// chatMemberTableFilterQTFHBox.setPrefWidth(525);
// chatMemberTableFilterQTFHBox.setHgap(2);
FlowPane chatMemberTableFilterQTFHBox = new FlowPane(
Orientation.HORIZONTAL,
2,
@@ -7671,52 +7873,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chatMemberTableFilterTextField.setPromptText("Find...");
chatMemberTableFilterTextField.setFocusTraversable(false);
// chatMemberTableFilterTextField.focusedProperty().addListener(new ChangeListener<Boolean>() {
// @Override
// public void changed(ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean t1) {
// if (chatMemberTableFilterTextField.focusedProperty().getValue()) {
//
// chatMemberTableFilterTextField.clear();
// } else {
// if (!chatMemberTableFilterTextField.focusedProperty().getValue() && chatMemberTableFilterTextField.textProperty().equals("")) {
//
// chatMemberTableFilterTextField.setText("Find...");
// }
// }
//// System.out.println(chatMemberTableFilterTextField.focusedProperty().getValue());
// }
// });
// chatMemberTableFilterTextField.textProperty().addListener(new ChangeListener<String>() {
//
// Predicate<ChatMember> searchTextPredicate = new Predicate<ChatMember>() {
// @Override
// public boolean test(ChatMember chatMember) {
// if (chatMember.getCallSign().toUpperCase().contains(chatMemberTableFilterTextField.getText().toUpperCase()) ||
// chatMember.getCallSign().toUpperCase().contains(chatMemberTableFilterTextField.getText().toLowerCase())) {
// return true;
// } else
//
// return false;
// }
//
// };
//
// @Override
// public void changed(ObservableValue<? extends String> observableValue, String s, String t1) {
//
// if (chatMemberTableFilterTextField.textProperty().getValue().equals("") && !chatMemberTableFilterTextField.focusedProperty().getValue()) {
// chatMemberTableFilterTextField.setText("Find...");
// chatcontroller.getLst_chatMemberListFilterPredicates().remove(searchTextPredicate);
// }
// else {
// if (!chatcontroller.getLst_chatMemberListFilterPredicates().contains(searchTextPredicate)) {
// chatcontroller.getLst_chatMemberListFilterPredicates().add(searchTextPredicate);
// }
// }
//
// System.out.println("KST4CApp " + chatMemberTableFilterTextField.textProperty().getValue().equals("") + " / " + !chatMemberTableFilterTextField.focusedProperty().getValue());
// }
// });
chatMemberTableFilterTextField.textProperty().addListener(new ChangeListener<String>() {
@@ -7757,13 +7913,26 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}
});
// HBox chatMemberTableFilterWorkedBandFiltersHbx = new HBox();
FlowPane chatMemberTableFilterWorkedBandFiltersHbx = new FlowPane(
Orientation.HORIZONTAL,
2,
3
);
HBox chatMemberTableReachabilityBox = new HBox(4);
chatMemberTableReachabilityBox.setAlignment(Pos.CENTER_LEFT);
chatMemberTableReachabilityBox.setMinWidth(0);
chatMemberTableReachabilityBox.setStyle(
"-fx-padding: 1;" +
"-fx-border-style: solid inside;" +
"-fx-border-width: 1;" +
"-fx-border-insets: 1;" +
"-fx-border-radius: 1;" +
"-fx-border-color: lightgrey;"
);
chatMemberTableFilterWorkedBandFiltersHbx.setAlignment(Pos.CENTER_LEFT);
chatMemberTableFilterWorkedBandFiltersHbx.setRowValignment(VPos.CENTER);
chatMemberTableFilterWorkedBandFiltersHbx.setMinWidth(0);
@@ -7790,6 +7959,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
Band selectedBand = resolveReachabilityBandForUi(selectedMember);
chatcontroller.getReachabilityService().calculateSelectedStationOnDemand(selectedMember, selectedBand);
/*
* If the map is already initialized, make it request the same operator-selected
* band. ReachabilityService deduplicates the identical calculation key, so this
* attaches the map callback without causing a second terrain API request.
*/
if (stationMapBridge != null) {
stationMapBridge.requestSelectedPathAnalysisRefresh();
}
}
});
@@ -7802,7 +7980,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
cmbReachabilityBand.getItems().add(band.getDisplayLabel());
}
cmbReachabilityBand.getSelectionModel().select("Auto");
cmbReachabilityBand.setTooltip(new Tooltip("Reachability band for Tropo column/filter. Auto uses the station's lowest session band."));
cmbReachabilityBand.setTooltip(new Tooltip(
"Reachability band for Tropo column/filter. Auto uses the current "
+ "QRG, station-name hints and the supported chat category."
));
cmbReachabilityBand.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
@@ -7815,9 +7996,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}
});
chatMemberTableFilterWorkedBandFiltersHbx.getChildren().add(new Label("Reachability:"));
chatMemberTableFilterWorkedBandFiltersHbx.getChildren().add(cmbReachabilityBand);
chatMemberTableFilterWorkedBandFiltersHbx.getChildren().add(btnCalculateSelectedTropo);
chatMemberTableReachabilityBox.getChildren().addAll(
new Label("Reachability:"),
cmbReachabilityBand,
btnCalculateSelectedTropo
);
/**
* In order to work the filters needs the proper band settings, which should be worked
@@ -8159,8 +8342,44 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
btnTglAsNext5Min
);
// chatMemberTableFilterWorkedBandFilters
btnResetChatMemberFilters.setOnAction(event -> {
/*
* Reset the visible state of every station-list filter.
*
* Grid coloring and the selected reachability band are intentionally
* preserved because they are display/calculation settings, not filters.
*/
List.of(
btnTglNewLocator,
btnTglReachableTropo,
btnTglNewBands,
btnTglAsNext5Min,
tglBtnQRBEnable,
btnTglwkd,
btnTglwkd50,
btnTglwkd70,
btnTglwkd144,
btnTglwkd432,
btnTglwkd23,
btnTglwkd13,
btnTglwkd9,
btnTglwkd6,
btnTglwkd3,
btnTglInactive
).forEach(toggleButton -> toggleButton.setSelected(false));
tglGrpQTF.selectToggle(null);
chatMemberTableFilterQtfEnableChkbx.setSelected(false);
chatMemberTableFilterTextField.clear();
/*
* Programmatically changing a ToggleButton does not invoke its action
* handler. Clear the predicate list explicitly so no stale predicate
* can remain active.
*/
chatcontroller.getLst_chatMemberListFilterPredicates().clear();
});
chatMemberTableFilterTextFieldBox.getChildren().addAll(chatMemberTableFilterTextField);
@@ -8176,6 +8395,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chatMemberTableFilterTextFieldAndWorkedBandsHbx.getChildren().addAll(
chatMemberTableFilterTextFieldBox,
chatMemberTableReachabilityBox,
chatMemberTableFilterWorkedBandFiltersHbx
);
@@ -8831,6 +9051,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
TextField txtFldstn_pathAnalysisDemRootDirectory =
new TextField(this.chatcontroller.getChatPreferences().getStn_pathAnalysisDemRootDirectory());
txtFldstn_pathAnalysisDemRootDirectory.setDisable(true);
txtFldstn_pathAnalysisDemRootDirectory.setFocusTraversable(false);
txtFldstn_pathAnalysisDemRootDirectory.setTooltip(new Tooltip(
"Root directory that contains locally extracted Copernicus GLO-30 DEM tiles.\n" +
@@ -8851,6 +9072,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
OfflineDemImportService offlineDemImportService = new OfflineDemImportService();
Button btnUseDefaultDemDirectory = new Button("Default");
btnUseDefaultDemDirectory.setDisable(true);
btnUseDefaultDemDirectory.setFocusTraversable(false);
btnUseDefaultDemDirectory.setTooltip(new Tooltip(
"Creates and uses the default local Copernicus DEM directory below .praktiKST.\n" +
@@ -8880,6 +9102,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
});
Button btnImportDemTiles = new Button("Import tiles...");
btnImportDemTiles.setDisable(true);
btnImportDemTiles.setFocusTraversable(false);
btnImportDemTiles.setTooltip(new Tooltip(
"Copies manually selected Copernicus *_DEM.tif files into the configured DEM root directory.\n" +
@@ -9731,8 +9954,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
Label lblASUdpPort =
new Label("AirScout UDP port [9872] — reconnect after changing:");
Label lblASAutoBand =
new Label("Select AirScout frequency automatically per station:");
Label lblASBandName =
new Label("AirScout band value [1440000 = 144 MHz]:");
new Label("Forced AirScout band value [1440000 = 144 MHz]:");
CheckBox chkBxEnableUDPMsgbyAS = new CheckBox();
chkBxEnableUDPMsgbyAS.setSelected(
@@ -9885,13 +10111,38 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chatcontroller.getChatPreferences()
.getAirScout_asBandString()
);
CheckBox chkBxAutoAirScoutBand = new CheckBox("Auto per station");
chkBxAutoAirScoutBand.setSelected(
chatcontroller.getChatPreferences()
.isAirScout_autoBandSelectionEnabled()
);
chkBxAutoAirScoutBand.setTooltip(
new Tooltip(
"Uses the station's current QRG first, then station-name and "
+ "chat-category evidence. Disable this option only to force "
+ "one protocol value for every station."
)
);
txtFld_asQRGInt.setDisable(chkBxAutoAirScoutBand.isSelected());
chkBxAutoAirScoutBand.selectedProperty().addListener(
(observable, oldValue, newValue) -> {
chatcontroller.getChatPreferences()
.setAirScout_autoBandSelectionEnabled(newValue);
txtFld_asQRGInt.setDisable(newValue);
}
);
txtFld_asQRGInt.setFocusTraversable(false);
txtFld_asQRGInt.setTooltip(
new Tooltip(
"AirScout protocol band value, for example 1440000 for "
+ "144 MHz or 4320000 for 432 MHz."
"Fallback used only when automatic per-station selection is "
+ "disabled. Examples: 1440000 for 144 MHz or 4320000 "
+ "for 432 MHz."
)
);
txtFld_asQRGInt.focusedProperty().addListener(
(observable, oldValue, newValue) -> {
if (newValue) {
@@ -9926,7 +10177,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
);
Label lblASChangeNote = new Label(
"Server identifier, client identifier and band are applied "
"Server identifier, client identifier and frequency mode are applied "
+ "immediately. Reconnect after changing the UDP port."
);
lblASChangeNote.setWrapText(true);
@@ -9949,9 +10200,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
grdPnlAirScout.add(txtFld_asClientNameString, 1, 3);
grdPnlAirScout.add(lblASUdpPort, 0, 4);
grdPnlAirScout.add(txtFld_asUDPPortInt, 1, 4);
grdPnlAirScout.add(lblASBandName, 0, 5);
grdPnlAirScout.add(txtFld_asQRGInt, 1, 5);
grdPnlAirScout.add(lblASChangeNote, 0, 6, 2, 1);
grdPnlAirScout.add(lblASAutoBand, 0, 5);
grdPnlAirScout.add(chkBxAutoAirScoutBand, 1, 5);
grdPnlAirScout.add(lblASBandName, 0, 6);
grdPnlAirScout.add(txtFld_asQRGInt, 1, 6);
grdPnlAirScout.add(lblASChangeNote, 0, 7, 2, 1);
VBox vbxAirScout = new VBox();
vbxAirScout.setPadding(new Insets(10, 10, 10, 10));
@@ -11384,13 +11637,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
settingsStage.show();
chatcontroller.lastUiReminderEventProperty().addListener((obs, oldVal, ev) -> {
if (ev == null) return;
String text = "REMINDER: " + ev.getCallSignRaw() + " T-" + ev.getMinutesBefore() + "m";
Platform.runLater(() -> showBlinkingSkedWarnIndicator(text));
});
//initialize the timeline
Platform.runLater(this::updateTimelineVisuals);
@@ -11651,10 +11898,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
updateStatusButton(key, threadStateMessage);
});
maybeShowSkedWarnIndicator(key, threadStateMessage);
maybeShowBandUpgradeIndicator(key, threadStateMessage);
//if we receive a threadstatemessage for sked warning, enable the sked warning
@@ -208,7 +208,12 @@ public class TimelineView extends Pane {
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent()));
String baseToolTipFallBack = sked.getTargetCallsign() + " (" + sked.getBand() + ")\nAz: " + sked.getTargetAzimuth();
String baseToolTipFallBack =
sked.getTargetChatCallsign()
+ " ("
+ sked.getBand()
+ ")\nAz: "
+ sked.getTargetAzimuth();
if (skedTooltipExtraTextProvider != null) {
String extra = skedTooltipExtraTextProvider.apply(sked);
@@ -220,7 +225,9 @@ public class TimelineView extends Pane {
Tooltip t = new Tooltip(baseToolTipFallBack);
Tooltip.install(diamond, t);
Label lbl = new Label("SKED: " + sked.getTargetCallsign());
Label lbl = new Label(
"SKED: " + sked.getTargetChatCallsign()
);
// lbl.setFont(new Font(9));
// lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(14);
@@ -1,6 +1,5 @@
package kst4contest.view.map;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -496,37 +495,7 @@ public final class PathGeometryUtils {
}
}
/**
* Resolves one usable analysis frequency from aggregated marker frequency data.
*
* <p>Strategy:
* <ol>
* <li>Prefer 144 MHz data if available</li>
* <li>Otherwise use the first parsable known station frequency</li>
* <li>Otherwise use the central default frequency</li>
* </ol>
*
* @param frequenciesByBand known frequencies grouped by band
* @return resolved analysis frequency in MHz
*/
public static double resolveAnalysisFrequencyMHz(Map<String, String> frequenciesByBand) {
if (frequenciesByBand != null && !frequenciesByBand.isEmpty()) {
String band144Text = frequenciesByBand.get("144");
double parsed144 = tryParseFrequencyMHz(band144Text);
if (Double.isFinite(parsed144) && parsed144 > 0.0) {
return parsed144;
}
for (String value : frequenciesByBand.values()) {
double parsedFrequencyMHz = tryParseFrequencyMHz(value);
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
return parsedFrequencyMHz;
}
}
}
return DEFAULT_ANALYSIS_FREQUENCY_MHZ;
}
/**
* Small immutable geographic point used by great-circle interpolation.
@@ -18,6 +18,7 @@ import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
import kst4contest.model.Band;
import java.util.function.Predicate;
@@ -40,6 +41,7 @@ public final class StationMapBridge {
private final TableView<ChatMember> chatMemberTable;
private final StationMapView stationMapView;
private final Consumer<ChatMember> focusChatMemberConsumer;
private final Supplier<Band> reachabilityBandOverrideSupplier;
@@ -55,16 +57,22 @@ public final class StationMapBridge {
public StationMapBridge(ChatController chatController,
TableView<ChatMember> chatMemberTable,
StationMapView stationMapView,
Consumer<ChatMember> focusChatMemberConsumer) {
Consumer<ChatMember> focusChatMemberConsumer,
Supplier<Band> reachabilityBandOverrideSupplier) {
this.chatController = Objects.requireNonNull(chatController, "chatController");
this.chatMemberTable = Objects.requireNonNull(chatMemberTable, "chatMemberTable");
this.stationMapView = Objects.requireNonNull(stationMapView, "stationMapView");
this.focusChatMemberConsumer = Objects.requireNonNull(focusChatMemberConsumer, "focusChatMemberConsumer");
this.focusChatMemberConsumer = Objects.requireNonNull(
focusChatMemberConsumer,
"focusChatMemberConsumer"
);
this.reachabilityBandOverrideSupplier = Objects.requireNonNull(
reachabilityBandOverrideSupplier,
"reachabilityBandOverrideSupplier"
);
this.refreshCoalescer.setOnFinished(event -> refreshNow());
}
public void install() {
@@ -115,6 +123,18 @@ public final class StationMapBridge {
}
}
/**
* Forces the currently selected map path to be requested again.
*
* <p>This is used by the explicit "Calc selected" action after the operator
* changed the reachability band. Merely changing the ComboBox still does not
* trigger terrain analysis.</p>
*/
public void requestSelectedPathAnalysisRefresh() {
lastPathAnalysisRequestSignature = "";
requestImmediateRefresh();
}
public void focusSelectedCallsign() {
showWindow();
@@ -222,9 +242,12 @@ public final class StationMapBridge {
ChatMember selectedMember = resolveBestChatMember(targetCallsignRaw);
Band requestedBandOverride = reachabilityBandOverrideSupplier.get();
chatController.getReachabilityService().requestPathAnalysisForMap(
selectedMember,
selectedSnapshot,
requestedBandOverride,
result -> {
if (generation != pathAnalysisGeneration.get()) {
return;
@@ -232,6 +255,7 @@ public final class StationMapBridge {
stationMapView.setPathAnalysisResult(result);
}
);
}
/**
@@ -259,6 +283,13 @@ public final class StationMapBridge {
chatMemberTable.scrollTo(resolved);
focusChatMemberConsumer.accept(resolved);
/*
* A map click is an explicit operator action. Clear the signature so a
* newly selected reachability band is honored even when the same station
* is clicked again.
*/
lastPathAnalysisRequestSignature = "";
requestImmediateRefresh();
});
}
@@ -315,10 +346,16 @@ public final class StationMapBridge {
private double resolveAnalysisFrequencyMHz(MapCallsignRawSnapshot selectedSnapshot) {
if (selectedSnapshot == null) {
return PathGeometryUtils.DEFAULT_ANALYSIS_FREQUENCY_MHZ;
return Double.NaN;
}
return PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
ChatMember selectedMember = resolveBestChatMember(selectedSnapshot.callSignRaw());
var resolution = chatController.getReachabilityService()
.resolveAutomaticPropagationFrequency(selectedMember);
return resolution == null
? Double.NaN
: resolution.getAnalysisFrequencyMHz();
}
@@ -0,0 +1,184 @@
package kst4contest.test;
import kst4contest.logic.PropagationFrequencyResolver;
import kst4contest.model.Band;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import org.junit.jupiter.api.Test;
import java.util.EnumSet;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class PropagationFrequencyResolverTest {
private static final long NOW = 10_000_000L;
@Test
void currentQrgWinsOverNameAndCategoryFallback() {
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
addCurrentQrg(station, Band.B_2320, 2320.175, NOW - 1_000L);
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_1296, Band.B_2320, Band.B_10G)
);
assertEquals(Band.B_2320, resolution.getBand());
assertEquals(2320.175, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(PropagationFrequencyResolver.Source.CURRENT_QRG, resolution.getSource());
assertEquals("23201750", resolution.getAirScoutBandValue());
}
@Test
void mostRecentlyDetectedQrgWinsAcrossCategoryVariants() {
ChatMember vhf = station(ChatCategory.VUHF, "");
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
addCurrentQrg(vhf, Band.B_144, 144.210, NOW - 10_000L);
addCurrentQrg(microwave, Band.B_1296, 1296.210, NOW - 1_000L);
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(vhf, microwave),
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
);
assertEquals(Band.B_1296, resolution.getBand());
assertEquals(1296.210, resolution.getAnalysisFrequencyMHz(), 0.000_001);
}
@Test
void microwaveFallsBackToLowestEnabledMicrowaveBand() {
ChatMember station = station(ChatCategory.MICROWAVE, "");
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_2320, Band.B_3400)
);
assertEquals(Band.B_2320, resolution.getBand());
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(PropagationFrequencyResolver.Source.CHAT_CATEGORY, resolution.getSource());
}
@Test
void supportedCategoriesUseTheirAgreedLowestFallbackBand() {
assertEquals(
Band.B_50,
resolve(
List.of(station(ChatCategory.FIFTYSEVENTYMHz, "")),
EnumSet.of(Band.B_50, Band.B_70)
).getBand()
);
assertEquals(
Band.B_144,
resolve(
List.of(station(ChatCategory.VUHF, "")),
EnumSet.of(Band.B_144, Band.B_432)
).getBand()
);
assertEquals(
Band.B_1296,
resolve(
List.of(station(ChatCategory.MICROWAVE, "")),
EnumSet.of(Band.B_1296, Band.B_2320)
).getBand()
);
assertEquals(
Band.B_144,
resolve(
List.of(station(ChatCategory.EMEJT65, "")),
EnumSet.of(Band.B_144, Band.B_1296)
).getBand()
);
}
@Test
void vhfAndMicrowaveUse432MhzFallback() {
ChatMember vhf = station(ChatCategory.VUHF, "");
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(vhf, microwave),
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
);
assertEquals(Band.B_432, resolution.getBand());
assertEquals(432.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(
PropagationFrequencyResolver.Source.DUAL_CATEGORY_FALLBACK,
resolution.getSource()
);
assertEquals("4320000", resolution.getAirScoutBandValue());
}
@Test
void unsupportedChatCategoriesDoNotFallBackTo144Mhz() {
for (int categoryNumber = ChatCategory.LOWBAND;
categoryNumber <= ChatCategory.TENMeter;
categoryNumber++) {
ChatMember unsupported = station(categoryNumber, "QRV 2m");
addCurrentQrg(unsupported, Band.B_144, 144.300, NOW - 1_000L);
assertNull(
resolve(
List.of(unsupported),
EnumSet.of(Band.B_144, Band.B_432)
),
"Category " + categoryNumber + " must be ignored"
);
}
}
@Test
void manualNotQrvExclusionForcesNextUsableMicrowaveBand() {
ChatMember station = station(ChatCategory.MICROWAVE, "");
station.setQrv1240(false);
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_1296, Band.B_2320)
);
assertEquals(Band.B_2320, resolution.getBand());
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
}
@Test
void stationNameBandHintWinsOverCategoryFallback() {
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
PropagationFrequencyResolver.Resolution resolution = resolve(
List.of(station),
EnumSet.of(Band.B_1296, Band.B_10G)
);
assertEquals(Band.B_10G, resolution.getBand());
assertEquals(10368.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
assertEquals(PropagationFrequencyResolver.Source.STATION_NAME, resolution.getSource());
}
private PropagationFrequencyResolver.Resolution resolve(
List<ChatMember> variants,
EnumSet<Band> enabledBands
) {
return PropagationFrequencyResolver.resolve(variants, enabledBands, NOW);
}
private ChatMember station(int categoryNumber, String name) {
ChatMember station = new ChatMember();
station.setCallSign("DL1ABC");
station.setChatCategory(new ChatCategory(categoryNumber));
station.setName(name);
return station;
}
private void addCurrentQrg(ChatMember station,
Band band,
double frequencyMHz,
long timestampEpochMs) {
station.addKnownFrequency(band, frequencyMHz);
station.getKnownActiveBands().get(band).timestampEpoch = timestampEpochMs;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

+42 -8
View File
@@ -1,25 +1,59 @@
---
title: Log Synchronization
title: Log Synchronisation
icon: 🔄
category: Logger Integration
since: "1.31"
summary: Import worked stations and current frequencies from supported contest loggers so filters and band information follow the log.
description: KST4Contest receives worked-station and, where supported, frequency data from UCXLog, N1MM+, QARTest, DXLog.net and Win-Test through file and UDP interfaces.
summary: Import callsign, band, locator and QRG information from supported contest loggers at the level provided by each interface.
description: KST4Contest connects chat activity with current log state through file-based evaluation, general QSO UDP packets and the native Win-Test network protocol.
tagsList:
- Win-Test
- UCXLog
- N1MM+
- QARTest
- DXLog.net
- contest logger
related:
- priority-score
- dual-chat
- sked-reminder
---
## Connect chat and logging
## Why connect the logger?
Contest operation is faster when chat information and log state are connected.
The chat may still show a station as an interesting candidate after the QSO has already been logged. Without synchronisation, Worked filters, band columns and priority calculations would continue to use an outdated contest state.
KST4Contest can use logger information to improve workflow awareness.
KST4Contest therefore imports the information provided by the logging application and applies it to the active chat entries of the corresponding base callsign.
## Worked and band awareness
## Three interfaces with different levels of detail
Log synchronization helps the client understand which stations and bands are relevant.
The available information depends on the interface:
| Interface | Callsign | Band | Locator |
|---|---:|---:|---:|
| Simplelogfile interpreter | yes | no | no |
| General QSO UDP listener | yes | when included | when included |
| Win-Test network listener | yes | yes | when included |
The Simplelogfile interpreter is broadly compatible but can only identify worked callsigns. A callsign match in a file does not provide enough information to infer a reliable band or grid square.
The general UDP listener processes packets from UCXLog, N1MM+, QARTest and DXLog.net. Where the packet contains band and locator data, KST4Contest also updates the per-band Worked state and worked grid square.
![Log synchronisation settings](/manual/assets/client_settings_window_logsync.png)
## Native Win-Test integration
Win-Test uses a separate listener for its native network protocol. KST4Contest resolves the Win-Test band ID, including 50 and 70 MHz, and stores the resulting Worked information in the same internal database.
STATUS packets can also update the local QRG. In multi-operator networks, a station-name filter prevents STATUS packets from another operating position from replacing the frequency of the intended radio.
Win-Test can additionally receive skeds created in KST4Contest. The handover only takes place when a QRG matching the selected band can be determined. No fixed fallback frequency is inserted merely to make the packet technically valid.
> The band-aware sked handover and explicit `SSB`/`CW` selection are included in Nightly / v1.42.
## Stored state and limitations
Worked, NOT-QRV and worked-grid information is stored in the internal SQLite database and restored after a restart. Contest-related records expire automatically after three days.
KST4Contest can only use the fields supplied by the selected interface. Missing band or locator data is not reconstructed from guesswork. This makes the result less complete in some cases, but also avoids turning an incomplete log packet into incorrect Worked information.
[Read the complete log synchronisation setup in the manual.](/manual/en/log-sync/)
+53 -7
View File
@@ -3,24 +3,70 @@ title: Sked Reminder
icon: 🔔
category: Sked Management
since: "1.40"
summary: Store planned contacts and issue configurable chat messages plus local alerts before the agreed time.
description: Sked Reminder keeps scheduled contacts visible and provides automatic advance messages together with acoustic and visual operator alerts.
summary: Create a timed contact, raise its priority, show it on the timeline and optionally send reminder PMs before the agreed time.
description: KST4Contest keeps scheduled contacts in the active workflow, increases their priority as the agreed time approaches and can remind both operators.
tagsList:
- sked
- ON4KST
- contest reminder
- Win-Test
related:
- priority-score
- airscout
- timeline
---
## Never lose important skeds
## Why store a sked inside the chat client?
During active contests, it is easy to miss a planned contact while handling chat traffic, logging and band changes.
A contact agreed for five or ten minutes later has to compete with incoming messages, logging, antenna changes and other stations asking for attention. Remembering the time is only part of the problem. The station must also become visible again when the appointment approaches.
Sked Reminder keeps scheduled contacts visible.
KST4Contest therefore treats a sked as an active operating task rather than a simple alarm.
## Built for real contest pressure
## What happens when a sked is created?
The workflow supports time-critical operation where missing a few minutes can mean missing a QSO.
Select the station, the remaining time and one of the locally enabled bands. The mode can be set to `SSB` or `CW` for a possible Win-Test handover.
After pressing **Create sked**, KST4Contest:
1. stores the sked internally,
2. raises the station's Priority Score as the scheduled time approaches,
3. adds the contact to the AP and sked timeline, and
4. optionally schedules private reminder messages.
![Sked controls in the Further Info section](/manual/assets/sked_controls.png)
The internal sked does not depend on Win-Test. If no logger is connected or the network handover fails, the sked remains available in KST4Contest.
## Reminding the remote station
Reminder PMs are optional. The available patterns are:
- two and one minute before the sked,
- five, two and one minute before the sked, or
- ten, five, two and one minute before the sked.
Messages are sent to the complete KST callsign in the selected chat category. A band-specific login such as `CALLSIGN-70` therefore remains a separate message target instead of being silently reduced to the base callsign.
The local operator receives a visual **SKED** indication and, if simple notification sounds are enabled, an acoustic reminder.
## Win-Test handover
When the Win-Test network listener is enabled, KST4Contest also attempts to send the sked to Win-Test.
The frequency is not guessed. KST4Contest first looks for a recent QRG of the remote station on the selected band. If none is available, it checks whether the local QRG of the selected chat category belongs to that band. Without a matching frequency, the Win-Test handover is omitted while the internal sked remains intact.
KST-specific suffixes such as `-2`, `-70` or `-144` are removed from the callsign passed to the log. Portable components such as `/P` and `/M` are preserved.
> Band-aware QRG validation, explicit `SSB`/`CW` selection and the corrected handling of KST suffixes are included in Nightly / v1.42.
![Sked handed over from KST4Contest to Win-Test](/manual/assets/wintest_sked_handover.png)
## What the reminder cannot guarantee
Skeds and reminder schedules are stored in memory. They must be recreated after restarting KST4Contest.
The proposed band is derived from recent chat and station-name information. This is useful context, not proof that the station is still operating on the same QRG. Check the band, time and mode before creating the sked.
In plain terms: the function makes a scheduled contact considerably harder to overlook. It cannot prevent every missed sked.
[Read the complete sked handling and its limitations in the manual.](/manual/en/features/#skeds-and-sked-reminders)
+38 -9
View File
@@ -1,25 +1,54 @@
---
title: Timeline View
title: AP and Sked Timeline
icon: ⏱️
category: Contest Awareness
since: "1.40"
summary: Show AP-based priority candidates and scheduled contacts on a shared timeline.
description: The Timeline View places upcoming aircraft scatter candidates and planned skeds into a common time-based overview.
summary: Show upcoming aircraft-scatter candidates and scheduled contacts together on a 30-minute timeline.
description: The timeline relates AirScout opportunities, priority candidates, antenna direction and internal skeds to their expected time.
tagsList:
- timeline
- AP windows
- airplane scatter
- aircraft scatter
- sked
related:
- airscout
- priority-score
- sked-reminder
---
## Timing matters
## The useful station may only be useful for a minute
Many VHF/UHF/SHF opportunities are short-lived.
Aircraft-scatter opportunities are time-dependent. A candidate which matters in two minutes may be irrelevant now, while an agreed sked must remain visible even when no aircraft is currently available.
The Timeline View helps operators see upcoming timing windows instead of keeping everything in mind manually.
The timeline places both kinds of event into the same 30-minute view.
## Designed for fast awareness
Events further in the future appear on the right. As their time approaches, they move left towards the present.
The timeline supports quick decisions during busy contest operation.
![AP candidates and skeds in the timeline](/manual/assets/sked_timeline.png)
## AP candidates and scheduled contacts remain distinct
AP candidates appear in the upper lanes. Up to four selected candidates can be shown for each arrival minute. Their colours represent the reflection potential reported by AirScout:
- magenta from 95%,
- red from 75%,
- yellow from 50%, and
- blue below 50%.
Skeds appear as diamonds in the lower lane. Their labels use the complete selected KST callsign so that band-specific or otherwise suffixed logins remain identifiable.
> Complete KST callsigns in sked labels are included in Nightly / v1.42.
## Antenna direction remains visible
A marker becomes more transparent when its QTF is clearly outside the current antenna direction. The callsign remains readable. Targets near the centre of the configured antenna beam receive an additional visual highlight.
Clicking an AP candidate selects the corresponding active chat member, including the callsign suffix and chat category.
## A timing aid, not a contact forecast
The timeline shows when an event is expected to matter. It does not guarantee that the frequency is clear, that the remote station is ready or that the calculated aircraft path will produce a workable signal.
AirScout data can change. So can the actual operating situation.
[Read the complete timeline behaviour in the manual.](/manual/en/features/#ap-and-sked-timeline)