21 Commits

Author SHA1 Message Date
praktimarc
b58ceaf732 Merge branch 'main' into Jul25 2025-07-22 00:03:07 +02:00
praktimarc
9169afbbc0 Update ChatPreferences.java 2024-05-03 00:06:42 +02:00
praktimarc
a8b58d6eb8 Update AirPlane.java 2024-05-03 00:05:13 +02:00
praktimarc
7adc3d54a8 Update AirPlane.java 2024-05-03 00:01:09 +02:00
praktimarc
6aeebfc21b Update MessageBusManagementThread.java 2024-05-02 23:56:10 +02:00
praktimarc
61877e0886 Update DBController.java 2024-05-02 23:54:48 +02:00
praktimarc
eac02aecc9 Update DirectionUtils.java 2024-05-02 23:42:23 +02:00
praktimarc
a51cb1afe6 Update Kst4ContestApplication.java 2024-05-02 23:41:04 +02:00
praktimarc
a898680149 Merge branch 'featureMessagefilter' into main 2024-05-02 23:16:05 +02:00
Marc Froehlich
e7d13401be implemented all filters to the chatmemberlist, activity-displays in chatmember table and userinfopanel, linked selected messages to the userinfopanel for better UI feeling, begin of AS-Showpath-function (not yet ready) 2024-02-27 23:35:18 +01:00
Marc Froehlich
d13b7785af implemented some of the new filters to the chatmemberlist, changed list-subtype to make it sortable again 2024-02-27 23:35:18 +01:00
Marc Froehlich
c369888c37 Changed lists mechanic: not 3 messagelists any more but one oversable messagelist for all messages. The 3 categories of messages are now filteredlists, derived from this global messagelist.
Added a new panel down of the userlist which will be dynamically generated and shows filtered messages to a selected callsign
2024-02-27 23:35:18 +01:00
praktimarc
7fe2930ee2 Merge pull request #5 from praktimarc/featureAudio_jan
Feature audio jan
2024-02-27 23:34:22 +01:00
Marc Froehlich
416ce5b82f Update information service mechanic implemented 2024-02-27 23:31:52 +01:00
Marc Froehlich
de87c217f6 Some bugfixes to make the client robust against crashes after deconnects 2024-02-27 23:31:52 +01:00
Marc Froehlich
be99925b62 Chat is now disconnectable and reconnectable without closing. Made some changes in the thread management to make that possible 2024-02-27 23:31:52 +01:00
Marc Froehlich
3602a252b4 added contextmenu to cq-message-table 2024-02-27 23:31:52 +01:00
Marc Froehlich
7f48698278 added audio support 2024-02-27 23:31:52 +01:00
Marc Froehlich
4549314446 added audio support 2024-02-27 23:31:52 +01:00
praktimarc
686c277ac0 Merge pull request #3 from praktimarc/bugfix_nov
Bugfix nov
2023-11-21 23:34:06 +01:00
praktimarc
eca0dfdf61 Merge pull request #2 from praktimarc/bugfix_nov
bugfixes in view and msgbus
2023-11-14 00:56:28 +01:00
69 changed files with 2234 additions and 12294 deletions

View File

@@ -1,21 +0,0 @@
name: Publish wiki
on:
push:
branches: [main]
paths:
- github_docs/**
- .github/workflows/github-wiki.yml
concurrency:
group: publish-wiki
cancel-in-progress: true
permissions:
contents: write
jobs:
publish-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: github_docs
disable-empty-commits: true

View File

@@ -1,154 +0,0 @@
name: Nightly Runtime Artifacts
on:
push:
branches:
- main
schedule:
- cron: "20 2 * * *"
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build-windows-zip:
name: Build Windows ZIP
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Resolve nightly version info
shell: pwsh
run: |
$xml = [xml](Get-Content pom.xml)
$version = $xml.project.version
$shortSha = "${{ github.sha }}".Substring(0, 7)
Add-Content -Path $env:GITHUB_ENV -Value "VERSION=$version"
Add-Content -Path $env:GITHUB_ENV -Value "SHORT_SHA=$shortSha"
Add-Content -Path $env:GITHUB_ENV -Value "ASSET_BASENAME=praktiKST-$version-$shortSha"
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Install WiX Toolset
shell: pwsh
run: choco install wixtoolset --no-progress -y
- name: Build JAR and copy runtime dependencies
shell: pwsh
run: |
.\mvnw.cmd -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
$jar = Get-ChildItem -Path target -Filter 'praktiKST-*.jar' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $jar) {
throw "No project JAR produced"
}
Copy-Item $jar.FullName target/dist-libs/app.jar
- name: Build app-image with jpackage
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
- name: Create Windows ZIP
shell: pwsh
run: |
if (-not (Test-Path dist/praktiKST)) {
throw "No Windows app-image produced by jpackage"
}
Compress-Archive -Path dist/praktiKST -DestinationPath "dist/$env:ASSET_BASENAME-windows-x64.zip" -Force
- name: Upload Windows artifact
uses: actions/upload-artifact@v4.3.4
with:
name: windows-zip
path: dist/praktiKST-*-windows-x64.zip
retention-days: 14
build-linux-appimage:
name: Build Linux AppImage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
SHORT_SHA="${GITHUB_SHA::7}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
echo "ASSET_BASENAME=praktiKST-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Ensure mvnw is executable
run: chmod +x mvnw
- name: Build JAR and copy runtime dependencies
run: |
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
- name: Build app-image with jpackage
run: |
mkdir -p dist
jpackage \
--type app-image \
--name praktiKST \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
--module-path target/dist-libs \
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
--dest dist
- name: Create AppDir metadata
run: |
rm -rf target/praktiKST.AppDir
cp -a dist/praktiKST target/praktiKST.AppDir
cat > target/praktiKST.AppDir/AppRun << 'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/bin/praktiKST" "$@"
EOF
chmod +x target/praktiKST.AppDir/AppRun
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=praktiKST
Exec=praktiKST
Icon=praktiKST
Categories=Network;HamRadio;
Terminal=false
EOF
if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
fi
- name: Build AppImage
run: |
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x target/appimagetool.AppImage
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/praktiKST.AppDir "dist/${ASSET_BASENAME}-linux-x86_64.AppImage"
- name: Upload Linux artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-appimage
path: dist/praktiKST-*-linux-x86_64.AppImage
retention-days: 14

View File

@@ -1,31 +0,0 @@
name: PR Compile Check
on:
pull_request:
branches:
- main
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
compile:
name: Compile (Java 17)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Ensure mvnw is executable
run: chmod +x mvnw
- name: Compile
run: ./mvnw -B -DskipTests compile

View File

@@ -1,168 +0,0 @@
name: Tagged Release Build
on:
push:
tags:
- "*"
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build-windows-zip:
name: Build Windows ZIP
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Install WiX Toolset
shell: pwsh
run: choco install wixtoolset --no-progress -y
- name: Build JAR and copy runtime dependencies
shell: pwsh
run: |
.\mvnw.cmd -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
$jar = Get-ChildItem -Path target -Filter 'praktiKST-*.jar' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $jar) {
throw "No project JAR produced"
}
Copy-Item $jar.FullName target/dist-libs/app.jar
- name: Build app-image with jpackage
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
- name: Create Windows ZIP
shell: pwsh
run: |
if (-not (Test-Path dist/praktiKST)) {
throw "No Windows app-image produced by jpackage"
}
Compress-Archive -Path dist/praktiKST -DestinationPath dist/praktiKST-${{ github.ref_name }}-windows-x64.zip -Force
- name: Upload Windows artifact
uses: actions/upload-artifact@v4.3.4
with:
name: windows-zip
path: dist/praktiKST-${{ github.ref_name }}-windows-x64.zip
build-linux-appimage:
name: Build Linux AppImage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Ensure mvnw is executable
run: chmod +x mvnw
- name: Build JAR and copy runtime dependencies
run: |
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
- name: Build app-image with jpackage
run: |
mkdir -p dist
jpackage \
--type app-image \
--name praktiKST \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
--module-path target/dist-libs \
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
--dest dist
- name: Create AppDir metadata
run: |
rm -rf target/praktiKST.AppDir
cp -a dist/praktiKST target/praktiKST.AppDir
cat > target/praktiKST.AppDir/AppRun << 'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/bin/praktiKST" "$@"
EOF
chmod +x target/praktiKST.AppDir/AppRun
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=praktiKST
Exec=praktiKST
Icon=praktiKST
Categories=Network;HamRadio;
Terminal=false
EOF
if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
fi
- name: Build AppImage
run: |
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x target/appimagetool.AppImage
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/praktiKST.AppDir dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
- name: Upload Linux artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-appimage
path: dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
release-tag:
name: Publish Tagged Release
runs-on: ubuntu-latest
needs:
- build-windows-zip
- build-linux-appimage
steps:
- name: Download Windows artifact
uses: actions/download-artifact@v4.1.1
with:
name: windows-zip
path: release-assets/windows
- name: Download Linux artifact
uses: actions/download-artifact@v4.1.1
with:
name: linux-appimage
path: release-assets/linux
- name: Create tagged release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
name: ${{ startsWith(github.ref_name, 'beta-') && format('Beta {0}', github.ref_name) || format('Release {0}', github.ref_name) }}
prerelease: ${{ startsWith(github.ref_name, 'beta-') }}
allowUpdates: false
replacesArtifacts: false
makeLatest: ${{ !startsWith(github.ref_name, 'beta-') }}
generateReleaseNotes: true
artifacts: release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage

16
.gitignore vendored
View File

@@ -16,19 +16,3 @@ target
debug.out
.DS_Store
#Logfiles
SimpleLogFile.txt
udpReaderBackup.txt
#tempfiles
.idea/
out/
#targetfiles - mvn wrapper
target/
#builds
build/
#zip files for local backups
*.zip

View File

@@ -1,53 +0,0 @@
# KST4Contest Wiki
**KST4Contest** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), speziell entwickelt für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern.
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
---
## 🌐 Sprache / Language
| 🇩🇪 Deutsch | 🇬🇧 English |
|---|---|
| [Startseite (Deutsch)](de-Home) | [Home (English)](en-Home) |
---
## 🇩🇪 Inhalt (Deutsch)
| Seite | Inhalt |
|---|---|
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
| [Changelog](de-Changelog) | Versionsgeschichte |
---
## 🇬🇧 Contents (English)
| Page | Contents |
|---|---|
| [Installation](en-Installation) | Download, Java requirements, updates |
| [Configuration](en-Configuration) | All settings in detail |
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
| [Features](en-Features) | All features at a glance |
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
| [User Interface](en-User-Interface) | UI explained and how to operate it |
| [Changelog](en-Changelog) | Version history |
---
## Schnellinfo / Quick Info
- **Download**: https://do5amf.funkerportal.de/
- **GitHub**: https://github.com/praktimarc/kst4contest
- **Kontakt / Contact**: praktimarc+kst4contest@gmail.com

View File

@@ -1,108 +0,0 @@
# AirScout-Integration
> 🇬🇧 [English version](en-AirScout-Integration) | 🇩🇪 Du liest gerade die deutsche Version
AirScout (von DL2ALF) ist ein Programm zur Erkennung von Flugzeugen für den Aircraft-Scatter-Betrieb. KST4Contest ist eng mit AirScout integriert und zeigt reflektierbare Flugzeuge direkt in der Benutzerliste an.
> **Aircraft Scatter** ermöglicht sehr weitreichende Verbindungen auf VHF und höher auch für Stationen mit geringer Höhe über NN oder ungünstigen topografischen Verhältnissen.
---
## AirScout herunterladen
Download von AirScout:
- http://airscout.eu/index.php/download
---
## Flugzeugdaten-Feeds (ADSB)
Öffentliche Flugzeugdaten-Feeds im Internet sind oft unzuverlässig und begrenzt nutzbar. Eine empfohlene Alternative bietet **OV3T (Thomas)** mit einem dedizierten ADSB-Feed-Dienst:
- https://airscatter.dk/
- https://www.facebook.com/groups/825093981868542
Für diesen Dienst ist ein Account erforderlich. Bitte eine Spende für Thomas in Betracht ziehen der Server-Betrieb ist nicht kostenlos!
---
## AirScout einrichten
### Schritt 1: ADSB-Feed in AirScout konfigurieren
1. AirScout starten.
2. In den AirScout-Einstellungen den OV3T-Feed-Account eintragen (Benutzername, Passwort, URL).
3. Verbindung testen.
### Schritt 2: UDP-Kommunikation für KST4Contest aktivieren
In AirScout die UDP-Schnittstelle aktivieren:
- In den AirScout-Einstellungen die entsprechende Checkbox aktivieren (nur eine Checkbox notwendig).
- Standard-Ports nicht ändern, wenn kein besonderer Grund vorliegt.
### Schritt 3: KST4Contest-Einstellungen
In den KST4Contest-Preferences → **AirScout Settings**:
- AirScout-Kommunikation aktivieren
- IP und Port auf Standardwerte lassen (sofern nicht geändert)
---
## Kommunikation zwischen KST4Contest und AirScout (ab v1.263)
**Verbesserung in v1.263**: KST4Contest sendet nur noch Stationen an AirScout, deren QRB (Entfernung) kleiner als das eingestellte **Maximum-QRB** ist. Das Abfrageintervall wurde von 12 Sekunden auf **60 Sekunden** verlängert.
**Vorteile:**
- Deutlich weniger Berechnungsaufwand für AirScout
- Deutlich weniger Nachrichtenverkehr
- Das Tracking-Problem mit dem „Show Path in AirScout"-Button wurde dadurch deutlich verbessert
- Weniger Rechenleistung insgesamt
Außerdem: Der Name des KST4Contest-Clients und des AirScout-Servers war früher hartcodiert (`KST` und `AS`). Ab v1.263 werden die in den Preferences eingetragenen Namen verwendet.
---
## Mehrere KST4Contest-Instanzen und AirScout
> **Achtung**: Wenn mehrere KST4Contest-Instanzen gleichzeitig betrieben werden und bei beiden die AirScout-Kommunikation aktiviert ist, antwortet AirScout **an beide Instanzen**.
Das ist unproblematisch, wenn:
- Beide Instanzen denselben Locator verwenden, **oder**
- Beide Instanzen unterschiedliche Login-Rufzeichen haben.
Andernfalls kann es zu fehlerhaften AP-Daten kommen.
---
## AP-Spalte in der Benutzerliste
Nach der Einrichtung erscheint in der Benutzerliste eine **AP-Spalte** mit bis zu zwei reflektierbaren Flugzeugen pro Station.
Beispiel-Darstellung:
| Station | AP-Info |
|---|---|
| DF9QX | 2 Planes: 0 min / 0 min, je 100% |
| F5DYD | 2 Planes: 14 min / 31 min, je 50% |
Die AP-Informationen sind auch im **Privatnachrichten-Fenster** verfügbar.
Die Prozentzahl gibt das Reflexionspotenzial an (Größe des Flugzeugs, Höhe, Entfernung).
---
## AP-Variablen in Nachrichten
Die Flugzeugdaten können direkt in Nachrichten eingefügt werden:
- `FIRSTAP` → z. B. `a very big AP in 1 min`
- `SECONDAP` → z. B. `Next big AP in 9 min`
Details: [Makros und Variablen](Makros-und-Variablen#variablen)
---
## „Show Path in AirScout"-Button
In der Benutzerliste gibt es einen Button mit einem Pfeil, der die Richtung (QTF) zur ausgewählten Station anzeigt. Ein Klick maximiert AirScout und zeigt den Pfad mit reflektierbaren Flugzeugen zum ausgewählten Gesprächspartner.

View File

@@ -1,105 +0,0 @@
# Benutzeroberfläche
> 🇬🇧 [English version](en-User-Interface) | 🇩🇪 Du liest gerade die deutsche Version
## Verbinden mit dem Chat
1. Im Einstellungsfenster eine **Chat-Kategorie** auswählen (z. B. 144 MHz VHF, 432 MHz UHF, …).
2. **Connect**-Button klicken.
3. Warten bis die Verbindung aufgebaut ist.
> Trennen und Neu-Verbinden ist nur über das Einstellungsfenster möglich. Es empfiehlt sich daher, das Einstellungsfenster geöffnet zu lassen.
---
## Hauptfenster-Überblick
Das Hauptfenster besteht aus mehreren Bereichen:
### PM-Fenster (oben links)
Zeigt alle empfangenen **Privatnachrichten** sowie abgefangene öffentliche Nachrichten, die das eigene Rufzeichen enthalten. Neue Nachrichten erscheinen in **Rot** und faden alle 30 Sekunden über Gelb bis Weiß ab.
### Benutzerliste (Chat Members)
Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfiguration):
| Spalte | Inhalt |
|---|---|
| Call | Rufzeichen der Station |
| Name | Name aus dem Chat-Namenfeld |
| Loc | Maidenhead-Locator |
| QRB | Entfernung in km |
| QTF | Richtung in Grad |
| QRG | Automatisch erkannte Frequenz |
| AP | AirScout-Flugzeugdaten (wenn aktiv) |
| Band-Farben | Worked/NOT-QRV-Status pro Band |
**Sortierung**: Klick auf Spaltenköpfe. QRB-Sortierung arbeitet numerisch (ab v1.22 korrigiert).
### Sendfeld
Texteingabe für ausgehende Nachrichten. Nach Klick auf ein Rufzeichen in der Benutzerliste erhält das Sendfeld automatisch den Fokus sofort tippen ohne Doppelklick (ab v1.22).
### MYQRG-Feld
Rechts neben dem Sendbutton. Zeigt die aktuelle eigene QRG an, kann auch manuell eingetragen werden.
### MYQTF-Feld *(für v1.3)*
Eingabefeld für die aktuelle Antennenrichtung. Wird für die geplante `MYQTF`-Variable verwendet.
---
## Filter
Die Filter-Leiste (ab v1.21 als Flowpane für kleine Bildschirme):
- **Show only QTF**: Richtungsfilter aktivieren (Buttons N/NE/E/… oder Grad-Eingabe)
- **Show only QRB [km] <=**: Entfernungsfilter aktivieren (Toggle-Button)
- **Hide Worked [Band]**: Gearbeitete Stationen pro Band ausblenden (je ein Toggle pro Band)
- **Hide NOT-QRV [Band]**: NOT-QRV-markierte Stationen pro Band ausblenden
---
## Stationsinfo-Panel (Further Info)
Rechts unten: Zeigt alle Nachrichten einer ausgewählten Station (CQ-Nachrichten und PMs in einem Panel). Ein Nachrichtenfilter lässt sich über den Standard-Filter in den Preferences vorbelegen.
Hier können auch **Sked-Erinnerungen** aktiviert werden.
---
## Prioritätsliste
Zeigt die vom Score-Service berechneten Top-Kandidaten. Aktualisiert sich automatisch im Hintergrund basierend auf Richtung, Entfernung und AP-Verfügbarkeit.
---
## Cluster & QSO der anderen
Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen interessant in ruhigeren Phasen.
---
## Menü
### Window
- **Use Dark Mode** (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.
---
## Fenstergrößen und Divider
Ab **v1.21** werden beim Klick auf **„Save Settings"** auch Fenstergrößen und Divider-Positionen aller Panels in der Konfigurationsdatei gespeichert und beim nächsten Start wiederhergestellt.
Bei Problemen mit der Darstellung: Konfigurationsdatei löschen → KST4Contest erstellt neue Standardwerte.
---
## Tipps zur Bedienung
- **Einstellungsfenster geöffnet lassen**: Schneller Zugriff auf Beacon-Aktivierung/Deaktivierung.
- **Rechtsklick in der Benutzerliste**: Öffnet das Snippet-Menü und weitere Aktionen (QRZ.com-Profil, NOT-QRV-Tags setzen).
- **Enter aus dem Chat heraus**: Wenn im Sendfeld Text steht, sendet Enter direkt auch wenn der Fokus woanders liegt.
- **Beacon stoppen**: Beim Scannen von Frequenzen den Beacon ausschalten, damit der Chat nicht mit Meldungen überflutet wird.

View File

@@ -1,156 +0,0 @@
# Changelog
> 🇬🇧 [English version](en-Changelog) | 🇩🇪 Du liest gerade die deutsche Version
Versionsverlauf von KST4Contest / PraktiKST.
---
## v1.263 (2025-06-08)
**AirScout-Kommunikation und Login-Name**
**Geändert:**
- AirScout-Kommunikation grundlegend überarbeitet: Nur noch Stationen mit QRB < max-QRB werden an AirScout gesendet.
- Abfrage-Intervall von 12 Sekunden auf **60 Sekunden** erhöht.
- Deutlich weniger Berechnungsaufwand und Nachrichtenverkehr → Stabileres AirScout-Tracking.
- Name des AS-Clients und AS-Servers ist jetzt aus den Preferences konfigurierbar (war vorher hartcodiert auf „KST" / „AS").
**Behoben:**
- „Track in AirScout"-Button war sehr träge → durch neue Kommunikationslogik deutlich verbessert.
- Name im Chat ist jetzt speicherbar (Fehler behoben).
- Visuelle Korrekturen vor und nach dem Login.
- Fehler behoben, der von 9A2HM (Kreso) gemeldet wurde.
---
## v1.262 (2025-05-21)
**Freeze-Fix bei vorzeitiger Nachrichtenlieferung**
**Behoben:**
- ON4KST liefert manchmal Nachrichten, bevor der Login abgeschlossen ist. Das verursachte Fehler in der Nachrichtenverarbeitung → jetzt behoben.
---
## v1.26 (2025-05)
**Multi-Channel-Login und Dark Mode**
**Neu:**
- **Dark Mode**: Umschaltbar über `Window → Use Dark Mode`.
- **Multi-Channel-Login**: Gleichzeitiger Login in zwei Chat-Kategorien.
- **Opposite Station Multi-Callsign Login-Tagging**: Unterstützung für Stationen mit mehreren Rufzeichen.
**Geändert:**
- Farbgebungs-Mechanismus überarbeitet: Farben können jetzt über CSS angepasst werden.
**Behoben:**
- Stationsmarkierung komplett überarbeitet und korrekt gestellt.
---
## v1.251 (2025-02)
**Bugfix für UDP-Broadcast-Spot-Info**
**Behoben:**
- Problem beim Lesen von UDP-Broadcast-Spot-Informationen behoben (gemeldet von Steve Clements danke!).
- Stationsmarkierung (erneut verbessert).
---
## v1.25 (2025-02)
**Wunschliste umgesetzt**
**Neu:**
- **Neuer Einstellungs-Tab: Messagehandling**
- Auto-Antwort auf eingehende Nachrichten konfigurierbar.
- Automatische Antwort mit eigener CQ-QRG, wenn jemand danach fragt.
- Konfigurierbarer Standard-Filter für das Userinfo-Fenster *(für Gianluca :-) )*.
- **Farbige PM-Zeilen**: Neue Privatnachrichten erscheinen rot und faden alle 30 Sekunden über Gelb bis Weiß ab *(Idee von IU3OAR, Gianluca)*.
**Behoben:**
- Stationen mit Suffixen wie „-2" und „-70" wurden nicht als gearbeitet markiert → werden jetzt ignoriert, Station wird korrekt markiert.
---
## v1.24 (2024-11)
**Wunschliste + DX-Cluster-Spots**
**Neu:**
- Button zum Öffnen des **QRZ.com-Profils** der ausgewählten Station.
- Button zum Öffnen des **QRZ-CQ-Profils** der ausgewählten Station.
- **DX-Cluster-Server-Integration**: Richtungs-Warnungen werden als Spots an das Logprogramm gesendet (wenn QRG bekannt).
*(Zusätzlich wurden Farbgebungen der PM-Zeilen hinzugefügt tnx Gianluca)*
---
## v1.23 (2024-10)
**Integrierter DX-Cluster-Server**
**Neu:**
- KST4Contest enthält jetzt einen **integrierten DX-Cluster-Server**.
- Generiert DX-Cluster-Spots und sendet sie an das Logprogramm, wenn eine Richtungs-Warnung ausgelöst und eine QRG bekannt ist.
- Spotter-Rufzeichen muss sich vom Contest-Rufzeichen unterscheiden (für korrekte Filterung im Logprogramm).
*(Idee von OM0AAO, Viliam Petrik danke!)*
---
## v1.22 (2024-05)
**Usability-Verbesserungen und AirScout-Button-Fix**
**Neu:**
- Neue Variablen (tnx OM0AAO, Viliam Petrik):
- `MYLOCATORSHORT`
- `MYQRGSHORT`
- `QRZNAME`
**Geändert:**
- Sendfeld-Fokus: Nach Klick auf Rufzeichen in der Benutzerliste erhält das Sendfeld sofort den Fokus kein Doppelklick notwendig *(tnx Gianluca)*.
**Behoben:**
- Worked-Station-Filter ist jetzt live-aktiv: Gearbeitete Stationen verschwinden sofort nach Aktivierung des Filters *(tnx Gianluca)*.
- QRB-Sortierung war lexikografisch → jetzt numerisch *(tnx Alessandro Murador)*.
- AirScout-„Show Path"-Button: Klick maximiert AirScout und zeigt den Pfad korrekt an.
---
## v1.21 (2024-04)
**Usability-Verbesserungen**
**Geändert:**
- Fenstergrößen und Divider-Positionen werden beim Klick auf „Save Settings" in der Konfigurationsdatei gespeichert und beim Start wiederhergestellt.
- Filter-Bereich als Flowpane → bessere Darstellung auf kleineren Bildschirmen.
---
## v1.2 (2024-04)
**Bandselektion und NOT-QRV-Tags**
**Neu:**
- **Bandselektion**: In den Preferences auswählbar, welche Bänder aktiv sind. Nur für gewählte Bänder erscheinen Buttons und Felder in der UI. Speichern und Neustart erforderlich.
- **NOT-QRV-Tags pro Station und Band**: Stationen können für jedes Band als „nicht QRV" markiert werden. Kombinierbar mit dem Userlist-Filter.
- **QTF-Pfeil**: Der „Show path in AS"-Button zeigt jetzt einen Pfeil mit dem QTF der ausgewählten Station an.
---
## Frühere Versionen
### v1.1
Erste öffentlich veröffentlichte Version. Grundfunktionen:
- Worked-Markierung via Simplelogfile und UDP
- Sked-Richtungs-Hervorhebung
- QRG-Erkennung
- Text-Snippets und Shortcuts
- AirScout-Interface (erste Version)
- Intervall-Beacon
- PM-Abfang für öffentliche Nachrichten mit eigenem Rufzeichen
- Update-Hinweis-Dienst
---
## Geplante Features
- `MYQTF`-Variable (eigene Antennenrichtung als Text)
- Lebensdauer für den Worked-Status (automatisches Zurücksetzen)
- Filterung des „Cluster & QSO der anderen"-Fensters auf eigenes QTF
- Weitere Topografie-basierte Berechnungen für die Richtungswarnung

View File

@@ -1,76 +0,0 @@
# Integrierter DX-Cluster-Server
> 🇬🇧 [English version](en-DX-Cluster-Server) | 🇩🇪 Du liest gerade die deutsche Version
Ab **Version 1.23** enthält KST4Contest einen integrierten DX-Cluster-Server. Dieser sendet Spots direkt an das Logprogramm, wenn eine Richtungs-Warnung ausgelöst wird.
*(Idee von OM0AAO, Viliam Petrik danke!)*
---
## Wozu dient der integrierte DX-Cluster-Server?
Wenn KST4Contest erkennt, dass eine Station aus der eigenen Richtung ein Sked anfragt und gleichzeitig eine QRG bekannt ist, wird **automatisch ein DX-Cluster-Spot generiert** und an den Cluster-Client des Logprogramms gesendet.
Das Logprogramm zeigt den Spot in der Bandkarte an. Ein Klick auf den Spot stellt Frequenz und Mode des Transceivers direkt ein ohne manuelles Eintippen.
---
## Einrichtung
### In KST4Contest
In den Preferences → **DX-Cluster-Server-Einstellungen**:
1. **Port** des internen Servers eintragen (z. B. 7300 oder 8000 muss mit dem Logprogramm übereinstimmen).
2. **Spotter-Rufzeichen** eintragen **unbedingt ein anderes Rufzeichen als das Contest-Rufzeichen verwenden!**
- Grund: Logprogramme filtern Spots, die vom eigenen Rufzeichen stammen, als „gearbeitet" heraus. Wenn der Spotter dasselbe Rufzeichen hat, werden die Spots nicht angezeigt.
3. **Angenommene MHz** eintragen: Bei Frequenzangaben wie „.205" im Chat muss KST4Contest entscheiden, ob 144.205, 432.205 oder 1296.205 gemeint ist. Bei Einband-Contests einfach die entsprechende Bandmitte eintragen. Vollständige Frequenzangaben wie „144.205" oder „1296.338" im Chat werden immer korrekt erkannt.
### In UCXLog
- Verbindung zu einem DX-Cluster-Server konfigurieren:
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
- Port: Wie in KST4Contest konfiguriert
- Passwort: kann leer bleiben
- Über die Schaltfläche **„Send a test message to your log"** kann die Verbindung getestet werden.
### In N1MM+
Ähnliche Einstellungen:
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
- Port: Wie in KST4Contest konfiguriert
---
## Funktionsweise
Ein Spot wird generiert, wenn **beide** Bedingungen erfüllt sind:
1. Eine **Richtungs-Warnung** wurde ausgelöst (Station macht ein Sked in die eigene Richtung).
2. **QRG der Station ist bekannt** (aus dem Chat ausgelesen oder manuell eingetragen).
Der generierte Spot enthält:
- Rufzeichen der Station
- Frequenz
- Spotterzeit
Das Logprogramm kann den Spot dann in der Bandkarte anzeigen und den TRX per Mausklick auf die Frequenz abstimmen.
---
## Multi-Computer-Setup
Wenn KST4Contest auf einem separaten Computer läuft (nicht auf dem Log-Computer):
- Host im Logprogramm: IP des KST4Contest-Computers (nicht `127.0.0.1`)
- Entspricht der Konfiguration der QSO-UDP-Broadcast-Pakete (siehe [Log-Synchronisation](de-Log-Synchronisation))
---
## Getestete Logprogramme
- **UCXLog** ✓
- **N1MM+** ✓
Weitere Testergebnisse sind willkommen bitte per E-Mail an DO5AMF melden.

View File

@@ -1,173 +0,0 @@
# Funktionen
> 🇬🇧 [English version](en-Features) | 🇩🇪 Du liest gerade die deutsche Version
Übersicht aller Hauptfunktionen von KST4Contest.
---
## Sked-Richtungs-Hervorhebung
Eine der Kernfunktionen: Wenn eine Station ein Sked in die **eigene Richtung** sendet, wird sie in der Benutzerliste **grün und fett** hervorgehoben.
### Wie funktioniert das?
Die Berechnung basiert auf folgender Logik:
- Wenn Station A eine Sked-Anfrage an Station B sendet, wird angenommen, dass A ihre Antenne auf B ausrichtet.
- Wenn die daraus resultierende Richtung von A zur eigenen Station innerhalb des halben Öffnungswinkels der eigenen Antenne liegt, wird A hervorgehoben.
**Beispiel** (Öffnungswinkel 69°, Halbwinkel 34,5°):
| Situation | Ergebnis für DO5AMF in JN49 |
|---|---|
| Sked von F5FEN → DM5M | ✅ Hervorhebung (F5FEN zeigt Richtung DM5M, das liegt nahe JN49) |
| Sked von DM5M → F5FEN | ✅ Hervorhebung (DM5M antwortet in Richtung F5FEN) |
| F1DBN ist unbeteiligt | ❌ Keine Hervorhebung |
| DO5AMF/P (anderer Standort) | ❌ Keine Hervorhebung für Sked-Antwort |
Die Berechnung berücksichtigt keine topografischen Wegberechnungen das ist eine bewusste Vereinfachung. Möglicherweise wird das in einer späteren Version ergänzt.
> Konfiguration: [Konfiguration Antennen-Öffnungswinkel](Konfiguration#antennen-öffnungswinkel-antenna-beamwidth)
---
## Sked-Richtungs-Spots (Integrierter DX-Cluster)
Ab **v1.23**: Richtungs-Warnungen werden als DX-Cluster-Spots an das Logprogramm weitergeleitet, wenn eine QRG bekannt ist. Details: [DX-Cluster-Server](de-DX-Cluster-Server).
---
## QRG-Erkennung (QRG Reading)
KST4Contest verarbeitet jede Chat-Nachricht und extrahiert automatisch **Frequenzangaben**. Diese werden in der Benutzerliste in der **QRG-Spalte** angezeigt.
Erkannte Formate: `144.205`, `432.088`, `.205` (mit konfigurierter Bandannahme), etc.
**Nutzen**: Ohne nachzufragen kann man direkt auf die QRG einer Station schauen und entscheiden, ob eine Verbindung möglich ist.
---
## Worked-Markierung
Gearbeitete Stationen werden in der Benutzerliste visuell markiert pro Band. Grundlage ist die [Log-Synchronisation](de-Log-Synchronisation) via UDP oder Simplelogfile.
Vor jedem Contest die Datenbank zurücksetzen: [Konfiguration Worked Station Database Settings](Konfiguration#worked-station-database-settings).
---
## NOT-QRV-Tags (ab v1.2)
Wenn eine Station mitteilt, dass sie auf einem bestimmten Band nicht QRV ist, kann dies manuell markiert werden:
1. Station in der Benutzerliste auswählen.
2. Rechtsklick → NOT-QRV für das entsprechende Band setzen.
Diese Tags werden in der internen Datenbank gespeichert und bleiben nach einem Neustart von KST4Contest erhalten. Zurücksetzen über die Einstellungen möglich.
**Nutzen**: Verhindert wiederholte Sked-Anfragen auf Bändern, auf denen die Station nicht QRV ist schont sowohl die eigenen Nerven als auch die der Gegenstation.
---
## Richtungsfilter (Direction Filter)
Zeigt in der Benutzerliste nur Stationen an, die sich in einer bestimmten Richtung befinden. Aktivierbar über die Buttons N / NE / E / SE / S / SW / W / NW oder durch manuelle Eingabe von Grad.
Sinnvoll: Während man CQ in eine bestimmte Richtung ruft, nur Stationen in dieser Richtung anzeigen.
---
## Entfernungsfilter (Distance Filter)
Stationen jenseits einer maximalen Entfernung ausblenden. Schaltfläche **„Show only QRB [km] <="** ist ein Toggle-Button.
---
## Worked- und NOT-QRV-Filter
Toggle-Buttons (einer pro Band) zum Ausblenden bereits gearbeiteter Stationen und/oder NOT-QRV-markierter Stationen. Der Filter wirkt **sofort** ohne manuelles Neu-Aktivieren (ab v1.22 live).
---
## Farbige PM-Zeilen (ab v1.25)
Neue Privatnachrichten erscheinen in **Rot**. Die Farbe wechselt alle 30 Sekunden über Gelb bis Weiß wie ein Regenbogen-Fade. So ist auf einen Blick erkennbar, wie aktuell eine Nachricht ist.
*(Idee von IU3OAR, Gianluca Costantino danke!)*
---
## PM-Abfang (Catching Personal Messages)
Manche Nutzer senden Direktnachrichten versehentlich öffentlich, z. B.:
```
(DM5M) pse ur qrg
```
KST4Contest erkennt solche Nachrichten, die das eigene Rufzeichen enthalten, und sortiert sie automatisch in die **Privatnachrichten-Tabelle** ein. So gehen keine Nachrichten verloren.
---
## Multi-Channel-Login (ab v1.26)
Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Beide Chats werden parallel überwacht.
---
## Dark Mode (ab v1.26)
Aktivierbar über: **Window → Use Dark Mode**
Für individuelle Farbanpassungen: CSS-Datei bearbeiten (Pfad in den Programmunterlagen).
---
## Opposite Station Multi-Callsign Login-Tagging (ab v1.26)
Unterstützung für Stationen, die mit mehreren Rufzeichen gleichzeitig im Chat aktiv sind (z. B. Expedition-Setups).
---
## QRZ.com und QRZ-CQ Profil-Buttons (ab v1.24)
Für ausgewählte Stationen in der Benutzerliste gibt es direkte Buttons, um das **QRZ.com-Profil** und das **QRZ-CQ-Profil** im Browser zu öffnen.
---
## Sked-Erinnerungen (Sked Reminder Service)
Für vereinbarte Skeds können automatische Erinnerungs-PMs konfiguriert werden, die X Minuten vor dem vereinbarten Zeitpunkt gesendet werden. Die Erinnerungen werden aus dem FurtherInfo-Panel heraus aktiviert.
---
## Prioritätsliste / Score-Service
KST4Contest berechnet automatisch eine **Prioritätsliste** der interessantesten Gesprächspartner, basierend auf:
- Richtungserkennung
- QRB (Entfernung)
- AP-Verfügbarkeit (AirScout)
- Worked-Status
Die Top-Kandidaten werden in einer eigenen Liste angezeigt und helfen, im Contest-Stress die wichtigsten Stationen nicht zu übersehen.
---
## Intervall-Beacon
Automatische CQ-Meldungen im öffentlichen Kanal in konfigurierbarem Intervall. Empfohlene Verwendung mit der Variable `MYQRG` für aktuelle Frequenzangabe. Details: [Konfiguration Beacon Settings](Konfiguration#beacon-settings-automatischer-beacon).
---
## Simplelogfile
Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Cluster & QSO der anderen
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
Dieses Fenster kann miniaturisiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.

View File

@@ -1,51 +0,0 @@
# KST4Contest Wiki
> 🇬🇧 [English version](en-Home) | 🇩🇪 Du liest gerade die deutsche Version
**KST4Contest** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), der speziell für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern (144 MHz und aufwärts) entwickelt wurde.
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
---
## Schnellnavigation
| Seite | Inhalt |
|---|---|
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
| [Changelog](de-Changelog) | Versionsgeschichte |
---
## Was ist KST4Contest?
Der ON4KST-Chat ist der De-facto-Standard für Skeds auf den 144-MHz-und-höher-Bändern. KST4Contest erweitert die Chat-Nutzung um contest-spezifische Funktionen:
- **Worked-Markierung**: Bereits gearbeitete Stationen werden farblich markiert, direkt aus dem Logprogramm via UDP synchronisiert.
- **Sked-Richtungs-Erkennung**: Wenn eine Station eine andere aus deiner Richtung anruft, wird sie grün und fett hervorgehoben.
- **QRG-Erkennung**: KST4Contest liest Frequenzen automatisch aus dem Chat-Verkehr und zeigt sie in der Benutzerliste an.
- **AirScout-Interface**: Anzeige reflektierbarer Flugzeuge direkt in der Benutzerliste.
- **Integrierter DX-Cluster-Server**: Spots werden direkt an das Logprogramm gesendet.
- **Dark Mode** (ab v1.26): Schont die Augen in der Nacht.
- **Multi-Channel-Login** (ab v1.26): Gleichzeitig in zwei Chat-Kategorien einloggen.
---
## Kontakt & Support
- **E-Mail**: praktimarc+kst4contest@gmail.com *(nur für kst4contest-Themen)*
- **GitHub**: https://github.com/praktimarc/kst4contest
- **Download**: https://do5amf.funkerportal.de/
---
## Danksagungen
Besonderer Dank gilt: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX-Cluster-Idee), DC9DJ (Konrad Neitzel, Projektstruktur), DO5ALF (Andreas, Webmaster funkerportal.de), PE0WGA (Franz van Velzen, Tester) sowie allen weiteren Testern und Ideengebern.

View File

@@ -1,83 +0,0 @@
# Installation
> 🇬🇧 [English version](en-Installation) | 🇩🇪 Du liest gerade die deutsche Version
## Voraussetzungen
### Java
KST4Contest ist eine Java-Anwendung. Es wird eine aktuelle **Java Runtime Environment (JRE)** benötigt. Die empfohlene Version ist Java 17 oder höher.
### ON4KST-Account
Um den Chat zu nutzen, ist ein registrierter Account beim ON4KST-Chat-Dienst erforderlich:
- Registrierung unter: http://www.on4kst.info/chat/register.php
### Verhaltensregeln im Chat
Die offizielle Sprache im ON4KST-Chat ist **Englisch**. Auch bei Kommunikation mit Stationen aus dem eigenen Land bitte Englisch verwenden. Übliche HAM-Abkürzungen (agn, dir, pse, rrr, tnx, 73 …) sind gang und gäbe.
### Persönliche Nachrichten
Um eine Privatnachricht an eine andere Station zu senden, immer folgendes Format verwenden:
```
/CQ RUFZEICHEN Nachrichtentext
```
Beispiel: `/CQ DL5ASG pse sked 144.205?`
Bei starkem Chat-Verkehr (56 Nachrichten pro Sekunde im Contest) gehen öffentliche Nachrichten, die an ein bestimmtes Rufzeichen gerichtet sind, leicht unter. KST4Contest fängt solche Nachrichten aber auch dann ab, wenn sie fälschlicherweise öffentlich gepostet werden (siehe [Funktionen PM-Abfang](Funktionen#catching-personal-messages)).
---
## Download
Die aktuelle Version kann als ZIP-Datei heruntergeladen werden:
**https://do5amf.funkerportal.de/**
Der Dateiname hat das Format `kst4Contest_v<Versionsnummer>.zip`.
---
## Installation
1. ZIP-Datei herunterladen.
2. ZIP-Datei in einen gewünschten Ordner entpacken.
3. `praktiKST.exe` (Windows) bzw. das entsprechende Start-Skript ausführen.
Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` (Windows) gespeichert.
---
## Update
KST4Contest enthält einen **automatischen Update-Hinweis-Dienst**: Sobald eine neue Version verfügbar ist, erscheint beim Start ein Fenster mit:
- der Information, dass eine neue Version vorliegt,
- einem Changelog,
- dem Download-Link zur neuen Version.
### Update-Prozess
Derzeit gibt es nur einen Weg zum Aktualisieren:
1. Den alten Ordner löschen.
2. Das neue ZIP entpacken.
Die Einstellungsdatei (`preferences.xml`) bleibt erhalten, da sie im Benutzerordner gespeichert ist nicht im Programmordner.
---
## Bekannte Probleme beim Start
### Norton 360
Norton 360 stuft `praktiKST.exe` als gefährlich ein (Fehlalarm). Es muss eine Ausnahme für die Datei eingerichtet werden:
1. Norton 360 öffnen.
2. Sicherheit → Verlauf → Das entsprechende Ereignis suchen.
3. „Wiederherstellen & Ausnahme hinzufügen" wählen.
*(Gemeldet von PE0WGA, Franz van Velzen danke!)*

View File

@@ -1,135 +0,0 @@
# Konfiguration
> 🇬🇧 [English version](en-Configuration) | 🇩🇪 Du liest gerade die deutsche Version
Nach dem ersten Start öffnet sich das **Einstellungsfenster** dieses ist der zentrale Ausgangspunkt für alle Konfigurationen. Es empfiehlt sich, das Einstellungsfenster während des Betriebs geöffnet zu lassen (z. B. um den Beacon schnell ein- und auszuschalten).
> **Wichtig**: Nach jeder Änderung unbedingt **„Save Settings"** klicken! Die Einstellungen werden in `~/.praktikst/preferences.xml` gespeichert. Ab v1.21 werden auch Fenstergrößen und Divider-Positionen beim Speichern gesichert.
---
## Station Settings (Stationseinstellungen)
### Rufzeichen und Locator
Eigenes Rufzeichen und Maidenhead-Locator (6-stellig, z. B. `JN49IJ`) eintragen. Diese Werte werden für Distanz- und Richtungsberechnungen benötigt.
### Aktivierte Bänder
Über die **„my station uses band"**-Checkboxen werden die aktiven Bänder ausgewählt. Nur für ausgewählte Bänder erscheinen Schaltflächen und Tabellenzeilen in der Benutzeroberfläche. Nach Änderungen muss die Software neu gestartet werden.
### Antennen-Öffnungswinkel (Antenna Beamwidth)
Einen realistischen Wert für den Öffnungswinkel der eigenen Antenne eintragen (in Grad). Dieser Wert wird für die [Sked-Richtungs-Hervorhebung](Funktionen#sked-richtungs-hervorhebung) verwendet. Ein Testwert von 50° hat sich bewährt; DM5M nutzt Quads mit 69°.
> **Keinesfalls** Fantasy-Werte eintragen die Richtungsberechnungen werden sonst unbrauchbar.
### Standard-Maximum-QRB
Maximale Entfernung (in km), für die Richtungs-Warnungen ausgelöst werden sollen. Realistischer Wert für DM5M: 900 km. Stationen, die weiter entfernt sind, werden für Highlighting-Zwecke ignoriert.
---
## Log-Sync-Einstellungen
Zwei Methoden stehen zur Verfügung, um gearbeitete Stationen automatisch zu markieren. Details: [Log-Synchronisation](de-Log-Synchronisation).
### Universal File Based Callsign Interpreter (Simplelogfile)
Interpretiert beliebige Log-Dateien per Regex nach Rufzeichen-Mustern. Keine Bandinformation möglich. Geeignet als Fallback oder für nicht direkt unterstützte Logprogramme.
### Netzwerk-Listener für QSO-UDP-Broadcast
**Empfohlene Methode.** KST4Contest hört auf UDP-Pakete, die das Logprogramm beim Speichern eines QSOs an die Broadcast-Adresse sendet. Die Stationen werden mit Bandinformation markiert. UDP-Port: Standard **12060**.
---
## TRX-Sync-Einstellungen
Empfängt die aktuelle Frequenz des Transceivers vom Logprogramm via UDP. Ermöglicht die automatische Befüllung der Variable `MYQRG`. Nützlich für:
- Schnelles Einfügen der eigenen QRG in Chat-Nachrichten.
- Automatische CQ-Baken mit aktueller Frequenz.
> **Hinweis für Multi-Setup**: Wenn zwei Logprogramme an zwei Computern betrieben werden, aber nur eine KST4Contest-Instanz, darf nur ein Logprogramm die Frequenzpakete senden. KST4Contest kann nicht zwischen den Quellen unterscheiden.
---
## AirScout-Einstellungen
Konfiguration der Schnittstelle zu AirScout für die Flugzeug-Scatter-Erkennung. Details: [AirScout-Integration](de-AirScout-Integration).
---
## Notification Settings (Benachrichtigungen)
Drei Benachrichtigungstypen stehen zur Wahl:
1. **Einfache Sounds**: TADA-Sound für eingehende Nachrichten, Tick für Sked-Richtungserkennung usw.
2. **CW-Ansage**: Das Rufzeichen einer Station, die eine Privatnachricht sendet, wird als CW-Signal ausgegeben.
3. **Phonetische Ansage**: Das Rufzeichen wird phonetisch ausgesprochen.
---
## Shortcut Settings (Schnellzugriff-Schaltflächen)
Konfiguration von Schnellzugriff-Schaltflächen, die direkt im Hauptfenster erscheinen. Ein Klick auf eine Schaltfläche fügt den konfigurierten Text in das Sendfeld ein. Alle [Variablen](Makros-und-Variablen#variablen) können verwendet werden.
---
## Snippet Settings (Text-Snippets)
Text-Snippets sind über folgende Wege abrufbar:
- **Rechtsklick** auf ein Rufzeichen in der Benutzerliste
- **Rechtsklick** in der CQ-Nachrichtentabelle
- **Rechtsklick** in der PM-Nachrichtentabelle
- **Tastenkombinationen**: `Ctrl+1` bis `Ctrl+0` für die ersten 10 Snippets
Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als Direktnachricht adressiert:
`/CQ RUFZEICHEN <Snippet-Text>`
---
## Beacon Settings (Automatischer Beacon)
Konfiguration eines automatischen Intervall-Beacons im öffentlichen Chat-Kanal. Empfohlen: Variable `MYQRG` im Text verwenden, damit die aktuelle Frequenz immer aktuell ist. Intervall und Text sind frei konfigurierbar.
> **Tipp**: Beacon beim CQ-Rufen aktivieren und im Einstellungsfenster schnell deaktivieren, wenn kein CQ gerufen wird.
---
## Messagehandling Settings (ab v1.25)
Neuer Einstellungsbereich mit folgenden Optionen:
- **Auto-Antwort auf alle eingehenden Nachrichten**: Automatische Antwort auf Privatnachrichten konfigurierbar.
- **Auto-Antwort mit eigener CQ-QRG**: Wenn jemand nach der eigenen QRG fragt, antwortet KST4Contest automatisch mit dem Inhalt der `MYQRG`-Variable.
- **Standard-Filter für das Userinfo-Fenster**: Voreingestellter Nachrichtenfilter für das Stationsinfo-Fenster konfigurierbar *(für Gianluca :-) )*.
---
## Worked Station Database Settings (Gearbeitete-Stationen-Datenbank)
Vor jedem Contest die interne Worked-Datenbank zurücksetzen! Enthält:
- Worked-Status aller Stationen (pro Band)
- NOT-QRV-Tags (seit v1.2)
Schaltfläche **„Reinitialize"** unter der Tabelle verwenden. Eine geplante Funktion ist eine automatische Ablaufzeit für den Worked-Status.
---
## Dark Mode (ab v1.26)
Umschaltbar über das Menü: **Window → Use Dark Mode**. Die Farben können über CSS individuell angepasst werden.
---
## Einstellungen speichern
Nach **jeder** Änderung **„Save Settings"** klicken! Ohne Speichern gehen alle Änderungen beim nächsten Start verloren.
- Speicherort: `~/.praktikst/preferences.xml`
- Ab v1.21: Fenstergrößen und Divider-Positionen werden ebenfalls gespeichert.
- Bei Problemen: Konfigurationsdatei löschen → KST4Contest erstellt eine neue mit Standardwerten.

View File

@@ -1,111 +0,0 @@
# Log-Synchronisation
> 🇬🇧 [English version](en-Log-Sync) | 🇩🇪 Du liest gerade die deutsche Version
KST4Contest markiert gearbeitete Stationen automatisch in der Chat-Benutzerliste. Dafür gibt es zwei grundlegende Methoden:
---
## Methode 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest liest eine Log-Datei und sucht mittels regulärem Ausdruck nach Rufzeichen-Mustern. Dabei werden auch binäre Logdateien unterstützt unlesbarer Binärinhalt wird einfach ignoriert.
**Vorteil**: Funktioniert mit nahezu jedem Logprogramm, das eine Datei schreibt.
**Nachteil**: Keine Bandinformation möglich es wird nur „gearbeitet" markiert, nicht auf welchem Band.
Pfad der Log-Datei in den Preferences eintragen. Die Datei wird nur gelesen, nie verändert (read-only).
> **Tipp**: Die Simplelogfile-Funktion kann auch genutzt werden, um Stationen zu markieren, die definitiv nicht erreichbar sind (z. B. eigene Notizen). Das wird in einer späteren Version durch ein besseres Tagging-System ersetzt.
---
## Methode 2: Netzwerk-Listener (UDP-Broadcast) Empfohlen
Das Logprogramm sendet beim Speichern eines QSOs ein UDP-Paket an die Broadcast-Adresse des Heimnetzwerks. KST4Contest empfängt dieses Paket und markiert die Station inklusive **Bandinformation** in der internen SQLite-Datenbank.
> **Wichtig**: KST4Contest muss **parallel zum Logprogramm laufen**. QSOs, die während einer Abwesenheit von KST4Contest geloggt werden, werden nicht erfasst außer bei QARTest (kann das komplette Log senden).
**Standard UDP-Port**: 12060 (entspricht dem Standard der meisten Logprogramme)
---
## Unterstützte Logprogramme
### UCXLog (DL7UCX)
UCXLog sendet QSO-UDP-Pakete und Transceiver-Frequenzpakete.
**Einstellungen in UCXLog:**
- UDP-Broadcast aktivieren
- IP-Adresse des KST4Contest-Computers eintragen (bei lokalem Betrieb: `127.0.0.1`)
- Port: 12060 (Standard)
Grün markierte Felder in den UCXLog-Einstellungen beachten: IP und Port müssen eingetragen werden.
Hinweis für Multi-Setup (2 Computer, 2 Radios, eine KST4Contest-Instanz): Beide Logprogramme müssen die QSO-Pakete an die IP des KST4Contest-Computers senden. Dann ist mindestens eine IP nicht `127.0.0.1`.
### QARTest (IK3QAR)
**Besonderheit**: QARTest kann das **vollständige Log** an KST4Contest senden (Schaltfläche „Invia log completo" in den QARTest-Einstellungen). Damit werden auch QSOs erfasst, die vor dem Start von KST4Contest geloggt wurden.
**Einstellungen in QARTest:**
- UDP-Broadcast und IP/Port wie UCXLog konfigurieren
- „Invia log completo" für den vollständigen Log-Upload verwenden
*(„Buona funzionalità caro IK3QAR!" DO5AMF)*
### N1MM+
**Einstellungen in N1MM+:**
In N1MM+ unter `Config → Configure Ports, Mode Control, Winkey, etc. → Broadcast Data`:
- `Radio Info` aktivieren (für TRX-Sync/QRG)
- `Contact Info` aktivieren (für QSO-Sync)
- IP: `127.0.0.1` (oder IP des KST4Contest-Computers)
- Port: 12060
Für den integrierten DX-Cluster-Server: N1MM+ als DX-Cluster-Client konfigurieren (Server: `127.0.0.1`, Port wie in KST4Contest eingestellt).
### DXLog.net
**Einstellungen in DXLog.net:**
- UDP-Broadcast aktivieren
- IP des KST4Contest-Computers eintragen (grün markierte Felder)
- Port: 12060
### WinTest
WinTest wird ebenfalls unterstützt. KST4Contest empfängt WinTest-UDP-Pakete über einen dedizierten Listener. Die Konfiguration erfolgt analog zu den anderen Programmen.
---
## TRX-Frequenz-Synchronisation
Neben der QSO-Synchronisation übertragen UCXLog und andere Programme auch die **aktuelle Transceiverfrequenz** via UDP. KST4Contest verarbeitet diese Information und stellt sie als Variable `MYQRG` bereit.
**Ergebnis**: Die eigene QRG muss im Chat nie mehr manuell eingegeben werden ein Klick auf den MYQRG-Button oder die Verwendung der Variable im Beacon genügt.
> **Hinweis für Multi-Setup**: Bei zwei Logprogrammen an zwei Computern sollte nur **eines** die Frequenzpakete senden. KST4Contest kann nicht zwischen den Quellen unterscheiden und verarbeitet alle eingehenden Pakete.
---
## Multi-Setup: 2 Radios, 2 Computer
Für DM5M-typische Setups (2 Radios, 2 Computer, eine KST4Contest-Instanz oder zwei separate):
**Variante A Eine gemeinsame KST4Contest-Instanz:**
- Beide Logprogramme senden QSO-Pakete an die IP des KST4Contest-Computers
- Nur ein Logprogramm sendet Frequenzpakete (empfohlen: das VHF-Logprogramm)
**Variante B Zwei separate KST4Contest-Instanzen (empfohlen):**
- Jedes Logprogramm kommuniziert mit seiner eigenen KST4Contest-Instanz via `127.0.0.1`
- Zwei separate Chat-Logins
- Bessere Trennung und weniger Konflikte
---
## Interne Datenbank
KST4Contest speichert die Worked-Information in einer internen **SQLite-Datenbank**. Diese ist von der Logprogramm-Datenbank unabhängig und wird nur über den UDP-Broadcast befüllt.
Vor jedem neuen Contest: Datenbank zurücksetzen! → [Konfiguration Worked Station Database Settings](Konfiguration#worked-station-database-settings)

View File

@@ -1,162 +0,0 @@
# Makros und Variablen
> 🇬🇧 [English version](en-Macros-and-Variables) | 🇩🇪 Du liest gerade die deutsche Version
KST4Contest bietet ein flexibles System aus Text-Snippets, Shortcuts und eingebauten Variablen, die den Chat-Workflow im Contest erheblich beschleunigen.
---
## Überblick
| Typ | Aufruf | Zweck |
|---|---|---|
| **Shortcuts** | Button in der Toolbar | Schneller Text-Insert ins Sendfeld |
| **Snippets** | Rechtsklick / Ctrl+1..0 | Text-Bausteine, optionaler PM-Versand |
| **Variablen** | In allen Text-Feldern verwendbar | Dynamische Werte (QRG, Locator, AP-Daten) |
---
## Shortcuts (Schnellzugriff-Schaltflächen)
Konfigurierbar in den Preferences → **Shortcut Settings**.
- Jeder konfigurierte Text erzeugt **einen Button** in der Benutzeroberfläche.
- Ein Klick fügt den Text in das **Sendfeld** ein.
- **Alle Variablen** können in Shortcuts verwendet werden und werden beim Einfügen sofort aufgelöst.
- Auch längere Texte möglich.
**Tipp**: Häufig verwendete Abkürzungen wie „pse", „rrr", „tnx", „73" als Shortcuts anlegen.
---
## Snippets (Text-Bausteine)
Konfigurierbar in den Preferences → **Snippet Settings**.
### Aufruf
- **Rechtsklick** auf ein Rufzeichen in der Benutzerliste
- **Rechtsklick** in der CQ-Nachrichtentabelle
- **Rechtsklick** in der PM-Nachrichtentabelle
- **Tastaturkürzel**: `Ctrl+1` bis `Ctrl+0` für die ersten 10 Snippets
### Verhalten mit ausgewähltem Rufzeichen
Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als **Privatnachricht** adressiert:
```
/CQ RUFZEICHEN <Snippet-Text>
```
Anschließend kann mit **Enter** direkt gesendet werden auch wenn das Sendfeld nicht den Fokus hat.
### Hardware-Makro-Tastatur
*(Idee von IU3OAR, Gianluca Costantino)*
Die Tastenkombinationen `Ctrl+1` bis `Ctrl+0` können auf einer programmierbaren Makro-Tastatur belegt werden. Ein weiterer Tastendruck (auf eine „Enter"-Taste) sendet den Text sofort. Im Contest-Betrieb spart das erheblich Zeit.
### Vordefinierte Standard-Snippets
Beim ersten Start werden einige Snippets vorbelegt, z. B.:
- `Hi OM, try sked?`
- `I am calling cq ur dir, pse lsn to me at MYQRG`
- `pse ur qrg?`
- `rrr, I move to your qrg nw, pse ant dir me`
Diese können in den Preferences angepasst oder gelöscht werden.
---
## Variablen
Variablen werden in geschriebenen Texten (Snippets, Shortcuts, Beacon, Sendfeld) durch ihre aktuellen Werte ersetzt. Einfach den Variablennamen **großgeschrieben** in den Text einfügen.
### MYQRG
Wird durch die aktuelle Transceiverfrequenz ersetzt.
- Quelle: TRX-Sync via UDP vom Logprogramm (wenn aktiviert)
- Fallback: Manuell eingetragener Wert im MYQRG-Textfeld rechts neben dem Sendbutton
- Format: `144.388.03`
**Beispiel**: `calling cq at MYQRG``calling cq at 144.388.03`
### MYQRGSHORT
Wie MYQRG, aber nur die ersten 7 Zeichen.
- Format: `144.388`
**Beispiel**: `qrg: MYQRGSHORT``qrg: 144.388`
### MYLOCATOR
Wird durch den eigenen Maidenhead-Locator (6-stellig) ersetzt.
- Format: `JO51IJ`
**Beispiel**: `my loc: MYLOCATOR``my loc: JO51IJ`
### MYLOCATORSHORT
Wie MYLOCATOR, aber nur die ersten 4 Zeichen.
- Format: `JO51`
**Beispiel**: `loc: MYLOCATORSHORT``loc: JO51`
### QRZNAME
Wird durch den **Namen** der aktuell ausgewählten Station aus dem Chat-Namenfeld ersetzt.
**Beispiel**: `Hi QRZNAME, sked?``Hi Gianluca, sked?`
### FIRSTAP
Wird durch Daten des ersten reflektierbaren Flugzeugs zur ausgewählten Station ersetzt (sofern vorhanden).
- Bedingung: AirScout ist aktiv und ein Flugzeug ist verfügbar.
- Format-Beispiel: `a very big AP in 1 min`
**Beispiel**: `AP info: FIRSTAP``AP info: a very big AP in 1 min`
### SECONDAP
Wie FIRSTAP, aber für das zweite verfügbare Flugzeug.
- Format-Beispiel: `Next big AP in 9 min`
**Beispiel**: `also: SECONDAP``also: Next big AP in 9 min`
### MYQTF *(geplant für v1.3)*
Wird durch die aktuelle Antennenrichtung in Worten ersetzt (z. B. `north`, `north east`, `east`, …).
- Quelle: Winkelwert im MYQTF-Eingabefeld (rechts neben dem MYQRG-Feld)
---
## Variablen im Beacon
Alle Variablen können auch im **automatischen Beacon** (Intervall-Nachrichten) verwendet werden. Empfohlene Beacon-Konfiguration:
```
calling cq at MYQRG, loc MYLOCATOR, GL all!
```
Da KST4Contest QRG-Daten automatisch aus Chat-Nachrichten ausliest: Wenn andere Stationen ebenfalls KST4Contest nutzen, sehen sie die eigene QRG sofort in der QRG-Spalte der Benutzerliste.
---
## Beispiel-Workflow mit Makros im Contest
1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt.
2. `Ctrl+1` drücken → Snippet „Hi OM, try sked?" wird als PM adressiert.
3. Enter drücken → Nachricht wird gesendet.
4. Station antwortet mit Frequenz → QRG-Spalte wird automatisch befüllt.
5. `Ctrl+2` → Snippet „I am calling cq ur dir, pse lsn to me at 144.388" (MYQRG aufgelöst).
6. Enter → Gesendet.
Ohne manuelle Tipparbeit, ohne Fehler, ohne Unterbrechung des CQ-Rufens.

View File

@@ -1,108 +0,0 @@
# AirScout Integration
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-AirScout-Integration)
AirScout (by DL2ALF) is a program for detecting aircraft for aircraft scatter operation. KST4Contest is tightly integrated with AirScout and shows reflectable aircraft directly in the user list.
> **Aircraft Scatter** enables very long-distance communication on VHF and higher even for stations with low altitude above sea level or unfavourable topographic conditions.
---
## Downloading AirScout
Download AirScout from:
- http://airscout.eu/index.php/download
---
## Aircraft Data Feeds (ADSB)
Public aircraft data feeds on the internet are often unreliable and limited in use. A recommended alternative is the dedicated ADSB feed service provided by **OV3T (Thomas)**:
- https://airscatter.dk/
- https://www.facebook.com/groups/825093981868542
An account is required for this service. Please consider donating to Thomas the server costs are not free!
---
## Setting Up AirScout
### Step 1: Configure the ADSB Feed in AirScout
1. Start AirScout.
2. Enter your OV3T feed account details (username, password, URL) in the AirScout settings.
3. Test the connection.
### Step 2: Enable UDP Communication for KST4Contest
In AirScout, enable the UDP interface:
- Activate the corresponding checkbox in the AirScout settings (only one checkbox needed).
- Do not change the default ports unless there is a specific reason.
### Step 3: KST4Contest Settings
In KST4Contest Preferences → **AirScout Settings**:
- Enable AirScout communication
- Leave IP and port at their default values (unless changed)
---
## Communication Between KST4Contest and AirScout (from v1.263)
**Improvement in v1.263**: KST4Contest now only sends stations to AirScout whose QRB (distance) is less than the configured **maximum QRB**. The query interval has been extended from 12 seconds to **60 seconds**.
**Benefits:**
- Significantly less computation load for AirScout
- Significantly less message traffic
- The tracking issue with the "Show Path in AirScout" button is greatly improved
- Less overall CPU usage
Additionally: The name of the KST4Contest client and AirScout server was previously hardcoded (`KST` and `AS`). From v1.263, the names configured in the Preferences are used.
---
## Multiple KST4Contest Instances and AirScout
> **Note**: If multiple KST4Contest instances are running simultaneously and AirScout communication is enabled on both, AirScout will respond **to both instances**.
This is not a problem if:
- Both instances use the same locator, **or**
- Both instances have different login callsigns.
Otherwise, it may result in incorrect AP data.
---
## AP Column in the User List
After setup, an **AP column** appears in the user list showing up to two reflectable aircraft per station.
Example display:
| Station | AP Info |
|---|---|
| DF9QX | 2 Planes: 0 min / 0 min, 100% each |
| F5DYD | 2 Planes: 14 min / 31 min, 50% each |
AP information is also available in the **private messages window**.
The percentage indicates the reflection potential (aircraft size, altitude, distance).
---
## AP Variables in Messages
Aircraft data can be inserted directly into messages:
- `FIRSTAP` → e.g. `a very big AP in 1 min`
- `SECONDAP` → e.g. `Next big AP in 9 min`
Details: [Macros and Variables](Macros-and-Variables#variables)
---
## "Show Path in AirScout" Button
In the user list there is a button with an arrow showing the direction (QTF) to the selected station. Clicking it maximises AirScout and shows the path with reflectable aircraft to the selected contact.

View File

@@ -1,156 +0,0 @@
# Changelog
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Changelog)
Version history of KST4Contest / PraktiKST.
---
## v1.263 (2025-06-08)
**AirScout Communication and Login Name**
**Changed:**
- AirScout communication fundamentally revised: Only stations with QRB < max-QRB are now sent to AirScout.
- Query interval extended from 12 seconds to **60 seconds**.
- Significantly less computation load and message traffic → more stable AirScout tracking.
- Name of the AS client and AS server is now configurable from the Preferences (was previously hardcoded to "KST" / "AS").
**Fixed:**
- "Track in AirScout" button was very sluggish → greatly improved by new communication logic.
- Name in chat is now saveable (bug fixed).
- Visual corrections before and after login.
- Bug fixed that was reported by 9A2HM (Kreso).
---
## v1.262 (2025-05-21)
**Freeze Fix for Early Message Delivery**
**Fixed:**
- ON4KST sometimes delivers messages before login is complete. This caused errors in the message processing engine → now fixed.
---
## v1.26 (2025-05)
**Multi-Channel Login and Dark Mode**
**New:**
- **Dark Mode**: Toggle via `Window → Use Dark Mode`.
- **Multi-channel login**: Simultaneous login to two chat categories.
- **Opposite station multi-callsign login tagging**: Support for stations with multiple callsigns.
**Changed:**
- Colouring mechanism revised: Colours can now be customised via CSS.
**Fixed:**
- Station tagging completely revised and corrected.
---
## v1.251 (2025-02)
**Bugfix for UDP Broadcast Spot Info**
**Fixed:**
- Problem reading UDP broadcast spot information fixed (reported by Steve Clements thank you!).
- Station tagging (further improved).
---
## v1.25 (2025-02)
**Wishlist Time**
**New:**
- **New settings tab: Messagehandling**
- Auto-reply to incoming messages configurable.
- Automatic reply with own CQ QRG when someone asks for it.
- Configurable default filter for the userinfo window *(for Gianluca :-) )*.
- **Coloured PM rows**: New private messages appear red and fade every 30 seconds from yellow to white *(idea by IU3OAR, Gianluca)*.
**Fixed:**
- Stations with suffixes like "-2" and "-70" were not being marked as worked → now ignored, station is correctly marked.
---
## v1.24 (2024-11)
**Wishlist + DX Cluster Spots**
**New:**
- Button to open the **QRZ.com profile** of the selected station.
- Button to open the **QRZ-CQ profile** of the selected station.
- **DX Cluster Server integration**: Direction warnings are sent as spots to the logging software (when QRG is known).
*(Coloured PM row feature also added tnx Gianluca)*
---
## v1.23 (2024-10)
**Built-in DX Cluster Server**
**New:**
- KST4Contest now contains a **built-in DX cluster server**.
- Generates DX cluster spots and sends them to the logging software when a direction warning is triggered and a QRG is known.
- Spotter callsign must differ from the contest callsign (for correct filtering in the logging software).
*(Idea by OM0AAO, Viliam Petrik thank you!)*
---
## v1.22 (2024-05)
**Usability Improvements and AirScout Button Fix**
**New:**
- New variables (tnx OM0AAO, Viliam Petrik):
- `MYLOCATORSHORT`
- `MYQRGSHORT`
- `QRZNAME`
**Changed:**
- Send field focus: After clicking a callsign in the user list, the send field immediately receives focus no double-click needed *(tnx Gianluca)*.
**Fixed:**
- Worked-station filter is now live: Worked stations disappear immediately when the filter is activated *(tnx Gianluca)*.
- QRB sorting was lexicographic → now numeric *(tnx Alessandro Murador)*.
- AirScout "Show Path" button: Click now maximises AirScout and correctly shows the path.
---
## v1.21 (2024-04)
**Usability Improvements**
**Changed:**
- Window sizes and divider positions are saved in the configuration file when clicking "Save Settings" and restored on startup.
- Filter section as flowpane → better display on smaller screens.
---
## v1.2 (2024-04)
**Band Selection and NOT-QRV Tags**
**New:**
- **Band selection**: Selectable in Preferences which bands are active. Only buttons and fields for selected bands appear in the UI. Save and restart required.
- **NOT-QRV tags per station and band**: Stations can be marked as "not QRV" for each band. Combinable with the user list filter.
- **QTF arrow**: The "Show path in AS" button now shows an arrow with the QTF of the selected station.
---
## Earlier Versions
### v1.1
First publicly released version. Core features:
- Worked marking via Simplelogfile and UDP
- Sked direction highlighting
- QRG detection
- Text snippets and shortcuts
- AirScout interface (first version)
- Interval beacon
- PM catching for public messages containing your own callsign
- Update notification service
---
## Planned Features
- `MYQTF` variable (own antenna direction as text)
- Lifetime for worked status (automatic reset)
- Filtering the "Cluster & QSO of others" window to own QTF
- Further topography-based calculations for direction warnings

View File

@@ -1,135 +0,0 @@
# Configuration
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Konfiguration)
After the first start, the **settings window** opens this is the central starting point for all configuration. It is recommended to keep the settings window open during operation (e.g. to quickly toggle the beacon on and off).
> **Important**: Always click **"Save Settings"** after any change! Settings are stored in `~/.praktikst/preferences.xml`. From v1.21 onwards, window sizes and divider positions are also saved when you click Save.
---
## Station Settings
### Callsign and Locator
Enter your callsign and Maidenhead locator (6 characters, e.g. `JN49IJ`). These values are used for distance and direction calculations.
### Active Bands
Use the **"my station uses band"** checkboxes to select which bands you are active on. Only selected bands will show buttons and table rows in the user interface. A restart is required after changing these settings.
### Antenna Beamwidth
Enter a realistic value for your antenna's beamwidth (in degrees). This value is used for the [Sked Direction Highlighting](Features#sked-direction-highlighting). A test value of 50° has proven useful; DM5M uses Quads with 69°.
> **Do not** enter fantasy values the direction calculations will become meaningless.
### Default Maximum QRB
Maximum distance (in km) for which direction warnings should be triggered. A realistic value for DM5M is 900 km. Stations beyond this distance are ignored for highlighting purposes.
---
## Log Sync Settings
Two methods are available for automatically marking worked stations. Details: [Log Synchronisation](en-Log-Sync).
### Universal File Based Callsign Interpreter (Simplelogfile)
Interprets any log file using regex to find callsign patterns. No band information available. Suitable as a fallback or for unsupported logging programs.
### Network Listener for Logger's QSO UDP Broadcast
**Recommended method.** KST4Contest listens for UDP packets sent by the logging software when saving a QSO. Stations are marked including band information. UDP port: default **12060**.
---
## TRX Sync Settings
Receives the current transceiver frequency from the logging software via UDP. Makes the `MYQRG` variable available automatically. Useful for:
- Quickly inserting your own QRG into chat messages.
- Automatic CQ beacon with current frequency.
> **Note for multi-setup**: When running two logging programs on two computers but only one KST4Contest instance, only one logging program should send frequency packets. KST4Contest cannot distinguish between sources.
---
## AirScout Settings
Configuration of the interface to AirScout for aircraft scatter detection. Details: [AirScout Integration](en-AirScout-Integration).
---
## Notification Settings
Three notification types are available:
1. **Simple sounds**: TADA sound for incoming messages, tick for sked direction detection, etc.
2. **CW announcement**: The callsign of a station sending a private message is output as a CW signal.
3. **Phonetic announcement**: The callsign is spoken phonetically.
---
## Shortcut Settings
Configure quick-access buttons that appear directly in the main window. Clicking a button inserts the configured text into the send field. All [variables](Macros-and-Variables#variables) can be used.
---
## Snippet Settings
Text snippets are accessible via:
- **Right-click** on a callsign in the user list
- **Right-click** in the CQ message table
- **Right-click** in the PM message table
- **Keyboard shortcuts**: `Ctrl+1` to `Ctrl+0` for the first 10 snippets
If a callsign is selected in the user list, the snippet is addressed as a direct message:
`/CQ CALLSIGN <snippet text>`
---
## Beacon Settings
Configure an automatic interval message in the public chat channel. Recommended: use the `MYQRG` variable in the text so the current frequency is always up to date. Interval and text are freely configurable.
> **Tip**: Enable the beacon while calling CQ and quickly disable it in the settings window when not calling.
---
## Messagehandling Settings (from v1.25)
New settings section with the following options:
- **Auto-reply to all incoming messages**: Configurable automatic reply to private messages.
- **Auto-reply with CQ QRG**: When someone asks for your frequency, KST4Contest automatically replies with the `MYQRG` variable content.
- **Default filter for the userinfo window**: Pre-configured message filter for the station info panel *(for Gianluca :-) )*.
---
## Worked Station Database Settings
Reset the internal worked database before each contest! Contains:
- Worked status for all stations (per band)
- NOT-QRV tags (since v1.2)
Use the **"Reinitialize"** button below the table. A planned feature is an automatic expiry time for the worked status.
---
## Dark Mode (from v1.26)
Toggle via the menu: **Window → Use Dark Mode**. Colours can be individually customised via CSS.
---
## Saving Settings
Click **"Save Settings"** after **every** change! Without saving, all changes are lost on next start.
- Storage location: `~/.praktikst/preferences.xml`
- From v1.21: Window sizes and divider positions are also saved.
- If you encounter problems: delete the configuration file → KST4Contest creates a new one with default values.

View File

@@ -1,76 +0,0 @@
# Built-in DX Cluster Server
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-DX-Cluster-Server)
From **version 1.23**, KST4Contest includes a built-in DX cluster server. It sends spots directly to the logging software whenever a direction warning is triggered.
*(Idea by OM0AAO, Viliam Petrik thank you!)*
---
## What is the Built-in DX Cluster Server For?
When KST4Contest detects that a station is requesting a sked from your direction and a QRG is known, it **automatically generates a DX cluster spot** and feeds it directly to the logging software's cluster client / band map.
The logging software then displays the spot in the band map. Clicking the spot sets the transceiver's frequency and mode directly without any manual typing.
---
## Setup
### In KST4Contest
In Preferences → **DX Cluster Server Settings**:
1. Enter the **port** of the internal server (e.g. 7300 or 8000 must match the logging software).
2. Enter a **spotter callsign** **this must be a different callsign than your contest callsign!**
- Reason: Logging programs filter spots from your own callsign as "already worked". If the spotter uses the same callsign, the spots will not be displayed.
3. Enter the **assumed MHz**: For frequency references like ".205" in the chat, KST4Contest needs to decide whether 144.205, 432.205 or 1296.205 is meant. For single-band contests, simply enter the corresponding band centre. Full frequency references like "144.205" or "1296.338" in the chat are always correctly identified.
### In UCXLog
- Configure a DX cluster server connection:
- Host: `127.0.0.1` (or IP of the KST4Contest computer)
- Port: As configured in KST4Contest
- Password: can be left empty
- Use the **"Send a test message to your log"** button to test the connection.
### In N1MM+
Similar settings:
- Host: `127.0.0.1` (or IP of the KST4Contest computer)
- Port: As configured in KST4Contest
---
## How It Works
A spot is generated when **both** conditions are met:
1. A **direction warning** has been triggered (station is making a sked in your direction).
2. The **station's QRG is known** (read from the chat or manually entered).
The generated spot contains:
- Station's callsign
- Frequency
- Spot time
The logging software can then display the spot in the band map and tune the TRX to that frequency with a mouse click.
---
## Multi-Computer Setup
If KST4Contest runs on a separate computer (not the logging computer):
- Host in the logging software: IP of the KST4Contest computer (not `127.0.0.1`)
- Same configuration as for the QSO UDP broadcast packets (see [Log Synchronisation](en-Log-Sync))
---
## Tested Logging Software
- **UCXLog** ✓
- **N1MM+** ✓
Further test reports are welcome please send by email to DO5AMF.

View File

@@ -1,171 +0,0 @@
# Features
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Funktionen)
Overview of all main features of KST4Contest.
---
## Sked Direction Highlighting
One of the core features: when a station makes a sked request **towards your direction**, it is highlighted **green and bold** in the user list.
### How does it work?
The calculation is based on the following logic:
- When station A sends a sked request to station B, it is assumed that A is pointing its antenna towards B.
- If the resulting direction from A to your own station is within half the beamwidth of your own antenna, A is highlighted.
**Example** (beamwidth 69°, half-angle 34.5°):
| Situation | Result for DO5AMF in JN49 |
|---|---|
| Sked from F5FEN → DM5M | ✅ Highlighted (F5FEN points towards DM5M, close to JN49) |
| Sked from DM5M → F5FEN | ✅ Highlighted (DM5M replies towards F5FEN) |
| F1DBN is uninvolved | ❌ No highlighting |
| DO5AMF/P (different location) | ❌ No highlighting for sked reply |
The calculation does not include topographic path calculations this is a deliberate simplification. It may be added in a future version.
> Configuration: [Configuration Antenna Beamwidth](Configuration#antenna-beamwidth)
---
## Sked Direction Spots (Built-in DX Cluster)
From **v1.23**: Direction warnings are forwarded as DX cluster spots to the logging software when a QRG is known. Details: [DX Cluster Server](en-DX-Cluster-Server).
---
## QRG Detection (QRG Reading)
KST4Contest processes every line of text flowing through the channel and automatically extracts **frequency references**. These are displayed in the user list in the **QRG column**.
Recognised formats: `144.205`, `432.088`, `.205` (with configured band assumption), etc.
**Benefit**: Without asking, you can directly look up a station's calling frequency and decide whether a contact is possible.
---
## Worked Marking
Worked stations are visually marked in the user list per band. Based on [Log Synchronisation](en-Log-Sync) via UDP or Simplelogfile.
Reset the database before each contest: [Configuration Worked Station Database Settings](Configuration#worked-station-database-settings).
---
## NOT-QRV Tags (from v1.2)
When a station indicates it is not QRV on a specific band, this can be manually marked:
1. Select the station in the user list.
2. Right-click → Set NOT-QRV for the appropriate band.
These tags are stored in the internal database and persist after a KST4Contest restart. Can be reset via the settings.
**Benefit**: Prevents repeated sked requests on bands where the station is not active saves time for both sides.
---
## Direction Filter
Shows only stations in the user list that are located in a specific direction. Toggle using the N / NE / E / SE / S / SW / W / NW buttons or by entering degrees manually.
Useful: While calling CQ in a specific direction, only show stations in that direction.
---
## Distance Filter
Hide stations beyond a maximum distance. The **"Show only QRB [km] <="** button is a toggle.
---
## Worked and NOT-QRV Filter
Toggle buttons (one per band) to hide already-worked stations and/or NOT-QRV-tagged stations. The filter takes effect **immediately** without manually reactivating (live since v1.22).
---
## Coloured PM Rows (from v1.25)
New private messages appear in **red**. The colour fades every 30 seconds from yellow to white like a rainbow fade. This makes it immediately clear how recent a message is.
*(Idea by IU3OAR, Gianluca Costantino thank you!)*
---
## PM Catching
Some users accidentally post direct messages publicly, e.g.:
```
(DM5M) pse ur qrg
```
KST4Contest detects such messages that contain your own callsign and automatically sorts them into the **private messages table**. No messages are missed this way.
---
## Multi-Channel Login (from v1.26)
Simultaneous login to **two chat categories** (e.g. 144 MHz and 432 MHz). Both chats are monitored in parallel.
---
## Dark Mode (from v1.26)
Toggle via: **Window → Use Dark Mode**
For individual colour adjustments: edit the CSS file (path in the program settings).
---
## Opposite Station Multi-Callsign Login Tagging (from v1.26)
Support for stations that are active in the chat with multiple callsigns simultaneously (e.g. expedition setups).
---
## QRZ.com and QRZ-CQ Profile Buttons (from v1.24)
For selected stations in the user list, there are direct buttons to open the **QRZ.com profile** and the **QRZ-CQ profile** in the browser.
---
## Sked Reminders (Sked Reminder Service)
For agreed skeds, automatic reminder PMs can be configured, sent X minutes before the agreed time. Reminders are activated from the FurtherInfo panel.
---
## Priority List / Score Service
KST4Contest automatically calculates a **priority list** of the most interesting contacts, based on:
- Direction detection
- QRB (distance)
- AP availability (AirScout)
- Worked status
The top candidates are shown in a separate list, helping you not to miss the most important stations during contest stress.
---
## Interval Beacon
Automatic CQ messages in the public channel at a configurable interval. Recommended: use the `MYQRG` variable so the current frequency is always accurate. Details: [Configuration Beacon Settings](Configuration#beacon-settings).
---
## Simplelogfile
File-based log evaluation using regex. Details: [Log Synchronisation](Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Cluster & QSO of Others
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.

View File

@@ -1,51 +0,0 @@
# KST4Contest Wiki
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Home)
**KST4Contest** (also known as *PraktiKST*) is a Java-based chat client for the [ON4KST Chat](http://www.on4kst.info/chat/), specifically designed for contest operation on the VHF/UHF/SHF bands (144 MHz and above).
Developed by **DO5AMF (Marc Fröhlich)**, operator at DM5M.
---
## Quick Navigation
| Page | Contents |
|---|---|
| [Installation](en-Installation) | Download, Java requirements, updates |
| [Configuration](en-Configuration) | All settings in detail |
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
| [Features](en-Features) | All features at a glance |
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
| [User Interface](en-User-Interface) | UI explained and how to operate it |
| [Changelog](en-Changelog) | Version history |
---
## What is KST4Contest?
The ON4KST Chat is the de-facto standard for skeds on the 144 MHz and higher bands. KST4Contest enhances the chat experience with contest-specific features:
- **Worked marking**: Stations already worked are highlighted visually, synchronised directly from your logging software via UDP.
- **Sked direction detection**: When a station calls another one from your direction, it is highlighted green and bold.
- **QRG detection**: KST4Contest automatically reads frequencies from the chat traffic and shows them in the user list.
- **AirScout interface**: Reflectable aircraft are shown directly in the user list.
- **Built-in DX cluster server**: Spots are sent directly to your logging software.
- **Dark mode** (from v1.26): Easy on the eyes during night-time operation.
- **Multi-channel login** (from v1.26): Simultaneously logged into two chat categories.
---
## Contact & Support
- **Email**: praktimarc+kst4contest@gmail.com *(for kst4contest topics only)*
- **GitHub**: https://github.com/praktimarc/kst4contest
- **Download**: https://do5amf.funkerportal.de/
---
## Acknowledgements
Special thanks to: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX cluster idea), DC9DJ (Konrad Neitzel, project structure), DO5ALF (Andreas, webmaster funkerportal.de), PE0WGA (Franz van Velzen, tester) and all other testers and contributors.

View File

@@ -1,83 +0,0 @@
# Installation
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Installation)
## Prerequisites
### Java
KST4Contest is a Java application. A current **Java Runtime Environment (JRE)** is required. The recommended version is Java 17 or higher.
### ON4KST Account
To use the chat client, you need a registered account with the ON4KST chat service:
- Register at: http://www.on4kst.info/chat/register.php
### Behavioural Etiquette
The official language in the ON4KST Chat is **English**. Please use English even when communicating with stations from your own country. Common HAM abbreviations (agn, dir, pse, rrr, tnx, 73 …) are widely used and understood.
### Sending Personal Messages
To send a private message to another station, always use this format:
```
/CQ CALLSIGN message text
```
Example: `/CQ DL5ASG pse sked 144.205?`
During contest operation (56 messages per second in the public channel), public messages directed at a specific callsign are easily missed. KST4Contest also catches such messages if they are accidentally posted publicly (see [Features PM Catching](Features#pm-catching)).
---
## Download
The latest version can be downloaded as a ZIP file:
**https://do5amf.funkerportal.de/**
The filename follows the pattern `kst4Contest_v<version>.zip`.
---
## Installation
1. Download the ZIP file.
2. Unzip into a folder of your choice.
3. Run `praktiKST.exe` (Windows) or the corresponding start script.
Settings are stored at `%USERPROFILE%\.praktikst\preferences.xml` (Windows).
---
## Updating
KST4Contest includes an **automatic update notification service**: when a new version is available, a window will appear at startup showing:
- A notification that a new version is available
- A changelog
- The download link for the latest package
### Update Process
Currently the only way to update is:
1. Delete the old folder.
2. Unzip the new package.
Your settings file (`preferences.xml`) is preserved since it is stored in your user folder, not the program folder.
---
## Known Issues at Startup
### Norton 360
Norton 360 flags `praktiKST.exe` as dangerous (false positive). You need to add an exception:
1. Open Norton 360.
2. Security → History → Find the relevant event.
3. Select "Restore & Add Exception".
*(Reported by PE0WGA, Franz van Velzen thank you!)*

View File

@@ -1,111 +0,0 @@
# Log Synchronisation
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Log-Synchronisation)
KST4Contest automatically marks worked stations in the chat user list. Two basic methods are available:
---
## Method 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest reads a log file and searches for callsign patterns using a regular expression. Binary log files are also supported unreadable binary content is simply ignored.
**Advantage**: Works with almost any logging program that writes a file.
**Disadvantage**: No band information available stations are only marked as "worked", not on which band.
Enter the path to the log file in the Preferences. The file is only read, never modified (read-only).
> **Tip**: The Simplelogfile function can also be used to mark stations that are definitely unreachable (e.g. personal notes). This will be replaced in a later version by a better tagging system.
---
## Method 2: Network Listener (UDP Broadcast) Recommended
When saving a QSO, the logging software sends a UDP packet to the broadcast address of the home network. KST4Contest receives this packet and marks the station including **band information** in its internal SQLite database.
> **Important**: KST4Contest must be **running in parallel with the logging software**. QSOs logged while KST4Contest is not running will not be captured except with QARTest (which can send the complete log).
**Default UDP port**: 12060 (matches the default of most logging programs)
---
## Supported Logging Software
### UCXLog (DL7UCX)
UCXLog sends QSO UDP packets and transceiver frequency packets.
**Settings in UCXLog:**
- Enable UDP broadcast
- Enter the IP address of the KST4Contest computer (for local operation: `127.0.0.1`)
- Port: 12060 (default)
Note the green-highlighted fields in the UCXLog settings: IP and port must be filled in.
Note for multi-setup (2 computers, 2 radios, one KST4Contest instance): Both logging programs must send QSO packets to the IP of the KST4Contest computer. In this case, at least one IP is not `127.0.0.1`.
### QARTest (IK3QAR)
**Special feature**: QARTest can send the **complete log** to KST4Contest (button "Invia log completo" in the QARTest settings). This means QSOs logged before KST4Contest was started are also captured.
**Settings in QARTest:**
- Configure UDP broadcast and IP/port as with UCXLog
- Use "Invia log completo" for a full log upload
*(„Buona funzionalità caro IK3QAR!" DO5AMF)*
### N1MM+
**Settings in N1MM+:**
In N1MM+ under `Config → Configure Ports, Mode Control, Winkey, etc. → Broadcast Data`:
- Enable `Radio Info` (for TRX sync / QRG)
- Enable `Contact Info` (for QSO sync)
- IP: `127.0.0.1` (or IP of the KST4Contest computer)
- Port: 12060
For the built-in DX cluster server: configure N1MM+ as a DX cluster client (server: `127.0.0.1`, port as set in KST4Contest).
### DXLog.net
**Settings in DXLog.net:**
- Enable UDP broadcast
- Enter the IP of the KST4Contest computer (green-highlighted fields)
- Port: 12060
### WinTest
WinTest is also supported. KST4Contest receives WinTest UDP packets via a dedicated listener. Configuration is analogous to the other programs.
---
## TRX Frequency Synchronisation
In addition to QSO synchronisation, UCXLog and other programs also transmit the **current transceiver frequency** via UDP. KST4Contest processes this information and makes it available as the `MYQRG` variable.
**Result**: Your own QRG never needs to be typed manually in the chat clicking the MYQRG button or using the variable in the beacon is sufficient.
> **Note for multi-setup**: With two logging programs on two computers, only **one** should send frequency packets. KST4Contest cannot distinguish between sources and processes all incoming packets.
---
## Multi-Setup: 2 Radios, 2 Computers
For DM5M-style setups (2 radios, 2 computers, one KST4Contest instance or two separate):
**Option A One shared KST4Contest instance:**
- Both logging programs send QSO packets to the IP of the KST4Contest computer
- Only one logging program sends frequency packets (recommended: the VHF logging program)
**Option B Two separate KST4Contest instances (recommended):**
- Each logging program communicates with its own KST4Contest instance via `127.0.0.1`
- Two separate chat logins
- Better separation and fewer conflicts
---
## Internal Database
KST4Contest stores worked information in an internal **SQLite database**. This is independent of the logging program's database and is only populated via the UDP broadcast.
Before each new contest: reset the database! → [Configuration Worked Station Database Settings](Configuration#worked-station-database-settings)

View File

@@ -1,162 +0,0 @@
# Macros and Variables
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Makros-und-Variablen)
KST4Contest offers a flexible system of text snippets, shortcuts and built-in variables that significantly speed up the chat workflow during contests.
---
## Overview
| Type | Access | Purpose |
|---|---|---|
| **Shortcuts** | Button in the toolbar | Quick text insert into the send field |
| **Snippets** | Right-click / Ctrl+1..0 | Text building blocks, optional PM sending |
| **Variables** | Usable in all text fields | Dynamic values (QRG, locator, AP data) |
---
## Shortcuts (Quick-Access Buttons)
Configurable in Preferences → **Shortcut Settings**.
- Each configured text creates **one button** in the user interface.
- Clicking a button inserts the text into the **send field**.
- **All variables** can be used in shortcuts and are resolved immediately when inserted.
- Longer texts are also possible.
**Tip**: Set up frequently used abbreviations like "pse", "rrr", "tnx", "73" as shortcuts.
---
## Snippets (Text Building Blocks)
Configurable in Preferences → **Snippet Settings**.
### Access
- **Right-click** on a callsign in the user list
- **Right-click** in the CQ message table
- **Right-click** in the PM message table
- **Keyboard shortcuts**: `Ctrl+1` to `Ctrl+0` for the first 10 snippets
### Behaviour with a Selected Callsign
When a callsign is selected in the user list, the snippet is addressed as a **private message**:
```
/CQ CALLSIGN <snippet text>
```
Then **Enter** can be pressed to send directly even if the send field does not have focus.
### Hardware Macro Keyboard
*(Idea by IU3OAR, Gianluca Costantino)*
The key combinations `Ctrl+1` to `Ctrl+0` can be assigned to a programmable macro keyboard. One key press triggers the snippet, another press (mapped to Enter) sends it immediately. In contest operation this saves considerable time.
### Predefined Default Snippets
On first start, some snippets are pre-configured, e.g.:
- `Hi OM, try sked?`
- `I am calling cq ur dir, pse lsn to me at MYQRG`
- `pse ur qrg?`
- `rrr, I move to your qrg nw, pse ant dir me`
These can be customised or deleted in the Preferences.
---
## Variables
Variables in written texts (snippets, shortcuts, beacon, send field) are replaced by their current values at runtime. Simply type the variable name in **uppercase** in the text.
### MYQRG
Replaced by the current transceiver frequency.
- Source: TRX sync via UDP from the logging software (if enabled)
- Fallback: Manually entered value in the MYQRG text field to the right of the send button
- Format: `144.388.03`
**Example**: `calling cq at MYQRG``calling cq at 144.388.03`
### MYQRGSHORT
Like MYQRG, but only the first 7 characters.
- Format: `144.388`
**Example**: `qrg: MYQRGSHORT``qrg: 144.388`
### MYLOCATOR
Replaced by your own Maidenhead locator (6 characters).
- Format: `JO51IJ`
**Example**: `my loc: MYLOCATOR``my loc: JO51IJ`
### MYLOCATORSHORT
Like MYLOCATOR, but only the first 4 characters.
- Format: `JO51`
**Example**: `loc: MYLOCATORSHORT``loc: JO51`
### QRZNAME
Replaced by the **name** of the currently selected station from the chat name field.
**Example**: `Hi QRZNAME, sked?``Hi Gianluca, sked?`
### FIRSTAP
Replaced by data of the first reflectable aircraft to the selected station (if available).
- Condition: AirScout is active and an aircraft is available.
- Example format: `a very big AP in 1 min`
**Example**: `AP info: FIRSTAP``AP info: a very big AP in 1 min`
### SECONDAP
Like FIRSTAP, but for the second available aircraft.
- Example format: `Next big AP in 9 min`
**Example**: `also: SECONDAP``also: Next big AP in 9 min`
### MYQTF *(planned for v1.3)*
Replaced by the current antenna direction in words (e.g. `north`, `north east`, `east`, …).
- Source: Degree value in the MYQTF input field (to the right of the MYQRG field)
---
## Variables in the Beacon
All variables can also be used in the **automatic beacon** (interval messages). Recommended beacon configuration:
```
calling cq at MYQRG, loc MYLOCATOR, GL all!
```
Since KST4Contest automatically reads QRG data from chat messages: if other stations also use KST4Contest, they will immediately see your QRG in the QRG column of their user list.
---
## Example Contest Workflow with Macros
1. Select a station in the user list → callsign is now pre-selected.
2. Press `Ctrl+1` → Snippet "Hi OM, try sked?" is addressed as a PM.
3. Press Enter → Message sent.
4. Station replies with frequency → QRG column is automatically filled.
5. Press `Ctrl+2` → Snippet "I am calling cq ur dir, pse lsn to me at 144.388" (MYQRG resolved).
6. Press Enter → Sent.
No manual typing, no errors, no interruption to CQ calling.

View File

@@ -1,105 +0,0 @@
# User Interface
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Benutzeroberflaeche)
## Connecting to the Chat
1. Select a **chat category** in the settings window (e.g. 144 MHz VHF, 432 MHz UHF, …).
2. Click the **Connect** button.
3. Wait for the connection to be established.
> Disconnecting and reconnecting is only possible via the settings window. It is therefore recommended to keep the settings window open.
---
## Main Window Overview
The main window consists of several areas:
### PM Window (top left)
Shows all received **private messages** as well as intercepted public messages containing your own callsign. New messages appear in **red** and fade every 30 seconds from yellow to white.
### User List (Chat Members)
The central table of all currently active chat users. Columns (depending on configuration):
| Column | Content |
|---|---|
| Call | Station's callsign |
| Name | Name from the chat name field |
| Loc | Maidenhead locator |
| QRB | Distance in km |
| QTF | Direction in degrees |
| QRG | Automatically detected frequency |
| AP | AirScout aircraft data (when active) |
| Band colours | Worked / NOT-QRV status per band |
**Sorting**: Click column headers. QRB sorting is numerical (corrected in v1.22).
### Send Field
Text input for outgoing messages. After clicking a callsign in the user list, the send field automatically receives focus start typing immediately without double-clicking (from v1.22).
### MYQRG Field
To the right of the send button. Shows the current own QRG, can also be entered manually.
### MYQTF Field *(for v1.3)*
Input field for the current antenna direction. Used for the planned `MYQTF` variable.
---
## Filters
The filter bar (from v1.21 as a flowpane for small screens):
- **Show only QTF**: Activate direction filter (N/NE/E/… buttons or degree input)
- **Show only QRB [km] <=**: Activate distance filter (toggle button)
- **Hide Worked [Band]**: Hide worked stations per band (one toggle per band)
- **Hide NOT-QRV [Band]**: Hide NOT-QRV-tagged stations per band
---
## Station Info Panel (Further Info)
Bottom right: Shows all messages of a selected station (CQ messages and PMs in one panel). A message filter can be pre-configured via the default filter in the Preferences.
**Sked reminders** can also be activated here.
---
## Priority List
Shows the top candidates calculated by the Score Service. Updates automatically in the background based on direction, distance and AP availability.
---
## Cluster & QSO of Others
Separate window (can be minimised). Shows the communication flow between other stations interesting during quieter contest periods.
---
## Menu
### Window
- **Use Dark Mode** (from v1.26): Toggle dark colour scheme on/off.
---
## Window Sizes and Dividers
From **v1.21**, clicking **"Save Settings"** also saves window sizes and divider positions of all panels in the configuration file, which are restored on the next start.
If you encounter display problems: delete the configuration file → KST4Contest creates new default values.
---
## Operating Tips
- **Keep the settings window open**: Quick access to enable/disable the beacon.
- **Right-click in the user list**: Opens the snippet menu and further actions (QRZ.com profile, set NOT-QRV tags).
- **Enter from anywhere**: When text is in the send field, Enter sends directly even if the focus is elsewhere.
- **Stop the beacon**: Switch off the beacon while scanning frequencies to avoid flooding the chat with messages.

View File

@@ -6,7 +6,7 @@
<groupId>de.x08</groupId>
<artifactId>praktiKST</artifactId>
<version>1.41.0-nightly</version>
<version>1.0-SNAPSHOT</version>
<name>praktiKST</name>
@@ -32,7 +32,7 @@
<javafx.version>19.0.2.1</javafx.version>
<jetbrains.annotations.version>24.0.1</jetbrains.annotations.version>
<junit.version>5.10.1</junit.version>
<lombok.version>1.18.44</lombok.version>
<lombok.version>1.18.30</lombok.version>
<mockito.version>5.7.0</mockito.version>
<sqlite.version>3.43.2.2</sqlite.version>
@@ -54,8 +54,8 @@
<pmd.version>6.55.0</pmd.version>
<codehaus.version.plugin>2.16.1</codehaus.version.plugin>
<javafx.maven.plugin>0.0.8</javafx.maven.plugin>
<spotbugs.maven.plugin>4.9.8.2</spotbugs.maven.plugin>
<spotbugs.version>4.9.8</spotbugs.version>
<spotbugs.maven.plugin>4.8.1.0</spotbugs.maven.plugin>
<spotbugs.version>4.8.1</spotbugs.version>
<!-- other properties -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@@ -1,17 +1,6 @@
package kst4contest;
import java.util.Random;
public class ApplicationConstants {
/**
* default constructor generates runtime id
*/
ApplicationConstants() {
sessionRuntimeUniqueId = generateRuntimeId();
};
public static int sessionRuntimeUniqueId = generateRuntimeId();
/**
* Name of the Application.
*/
@@ -20,7 +9,9 @@ public class ApplicationConstants {
/**
* Name of file to store preferences in.
*/
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.41;
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.263;
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml";
public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
@@ -32,23 +23,6 @@ public class ApplicationConstants {
public static final String DISCSTRING_DISCONNECT_DUE_PAWWORDERROR = "JUSTDSICCAUSEPWWRONG";
public static final String DISCSTRING_DISCONNECTONLY = "ONLYDISCONNECT";
// public static final String DISCONNECT_RDR_POISONPILL = "POISONPILL_KILLTHREAD: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String DISCONNECT_RDR_POISONPILL = "POISONPILL_KILLTHREAD"; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String DISCONNECT_RDR_POISONPILL = "UNKNOWN: KST4C KILL POISONPILL_KILLTHREAD=: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg] "; // hard-coded marker (user can't remove it)
/**
* generates a unique runtime id per session. Its used to feed the poison pill in order to kill only this one and
* only instance if the program and not multiple instances
* @return
*/
public static int generateRuntimeId() {
Random ran = new Random();
return ran.nextInt(6) + 100;
}
}

View File

@@ -41,25 +41,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String prefix_asWatchList = "ASWATCHLIST: \""+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String bandString = "1440000"; //TODO: this must variable in case of higher bands! ... default: 1440000
// String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //before fix 1.266
String ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
try {
if (this.client.getChatPreferences().getStn_loginCallSign().contains("-")) {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign().split("-")[0];
} else {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
}
} catch (Exception e) {
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
}
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
String prefix_asWatchList = "ASWATCHLIST:\" "+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String bandString = "1440000";
String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat();
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
String asWatchListStringSuffix = asWatchListString;
@@ -85,9 +70,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
for (ChatMember i : ary_threadSafeChatMemberArray) {
if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault())
//Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower!
//this counts for AS request and Aswatchlist
{
suffix = i.getCallSign() + "," + i.getQra() + " ";

View File

@@ -1,10 +1,8 @@
package kst4contest.controller;
import java.util.Arrays;
import java.util.TimerTask;
import kst4contest.model.ChatMessage;
import kst4contest.model.ThreadStateMessage;
/**
* This class is for sending beacons intervalled to the public chat. Gets all
@@ -22,24 +20,17 @@ import kst4contest.model.ThreadStateMessage;
public class BeaconTask extends TimerTask {
private ChatController chatController;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "MyBeacon";
public BeaconTask(ChatController client, ThreadStatusCallback callback) {
this.callBackToController = callback;
public BeaconTask(ChatController client) {
this.chatController = client;
}
@Override
public void run() {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("BeaconTask");
ChatMessage beaconMSG = new ChatMessage();
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
@@ -84,12 +75,8 @@ public class BeaconTask extends TimerTask {
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
//do nothing, CQ is disabled
}
/**
@@ -107,15 +94,14 @@ public class BeaconTask extends TimerTask {
+ " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG2);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
//do nothing, CQ is disabled
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ package kst4contest.controller;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ThreadStateMessage;
import java.io.*;
import java.net.ServerSocket;
@@ -15,8 +14,6 @@ public class DXClusterThreadPooledServer implements Runnable{
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
ChatController chatController = null;
protected int serverPort = 8080;
protected ServerSocket serverSocket = null;
@@ -26,17 +23,13 @@ public class DXClusterThreadPooledServer implements Runnable{
Executors.newFixedThreadPool(10);
Socket clientSocket;
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
public DXClusterThreadPooledServer(int port, ChatController chatController){
this.serverPort = port;
this.chatController = chatController;
this.callBackToController = callback;
}
public void run(){
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
synchronized(this){
this.runningThread = Thread.currentThread();
runningThread.setName("DXCluster-thread-pooled-server");
@@ -60,7 +53,7 @@ public class DXClusterThreadPooledServer implements Runnable{
"Error accepting client connection", e);
}
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController);
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets);
this.threadPool.execute(worker);
@@ -118,7 +111,6 @@ public class DXClusterThreadPooledServer implements Runnable{
for (Socket socket : clientSockets) {
try {
OutputStream output = socket.getOutputStream();
String singleDXClusterMessage = "DX de ";
@@ -142,9 +134,6 @@ public class DXClusterThreadPooledServer implements Runnable{
output.write((singleDXClusterMessage).getBytes());
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last msg to " + clientSockets.size() + " Cluster Clients:\n" + singleDXClusterMessage, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (IOException e) {
e.printStackTrace();
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
@@ -163,16 +152,12 @@ class DXClusterServerWorkerRunnable implements Runnable{
protected String serverText = null;
private ChatController client = null;
private List<Socket> dxClusterClientSocketsConnectedList;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) {
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets) {
this.clientSocket = clientSocket;
this.serverText = serverText;
this.client = chatController;
this.dxClusterClientSocketsConnectedList = clientSockets;
this.callBackToController = callback;
}
public void run() {
@@ -186,12 +171,8 @@ class DXClusterServerWorkerRunnable implements Runnable{
@Override
public void run() {
StringBuilder connectedClients = new StringBuilder(); //only for statistics
for (Socket socket : dxClusterClientSocketsConnectedList) {
connectedClients.append(socket.getInetAddress()).append("\n");
try {
OutputStream output = socket.getOutputStream();
output.write(("\r\n").getBytes());
@@ -213,9 +194,6 @@ class DXClusterServerWorkerRunnable implements Runnable{
}
}
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
}, 30000, 30000);

View File

@@ -13,7 +13,7 @@ public class DXClusterThreadPooledServerTest {
testPreferences.setStn_loginCallSign("DM5M");
client.setChatPreferences(testPreferences);
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client, client);
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client);
new Thread(dxClusterServer).start();

View File

@@ -5,10 +5,8 @@ import java.io.PrintWriter;
import java.sql.SQLException;
//import java.net.Socket;
//import java.util.ArrayList;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,9 +28,6 @@ public class MessageBusManagementThread extends Thread {
int index;
private String ThreadNickName = "MessageBus";
private ThreadStatusCallback callBackToController;
private PrintWriter writer;
// private Socket socket;
private ChatController client;
@@ -45,15 +40,6 @@ public class MessageBusManagementThread extends Thread {
private final String PTRN_USERLISTENTRY = "([a-zA-Z0-9]{2}/{1})?([a-zA-Z0-9]{1,3}[0-9][a-zA-Z0-9]{0,3}[a-zA-Z]{0,3})(/p)? [a-zA-Z]{2}[0-9]{2}[a-zA-Z]{2} [ -~]{1,20}";
private final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
private final String PTRN_QRG_CAT3 = "(([0-9]{3,5}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
// ==== Autoanswer Flood/Pingpong Protection ====
private static final String AUTOANSWER_PREFIX = ApplicationConstants.AUTOANSWER_PREFIX; // hard-coded marker (user can't remove it)
private static final long AUTOANSWER_COOLDOWN_MS = 45_000L; // 45_000L = 45s
// Cooldown per opponent station (and ChatCategory) only setted if this client sends
private final Hashtable<String, Long> lastLocalAutoAnswerPerRemoteMs = new Hashtable<>();
// BufferedWriter bufwrtrDBGMSGOut;
// private String text;
@@ -74,14 +60,10 @@ public class MessageBusManagementThread extends Thread {
this.serverReady = serverReady;
}
public MessageBusManagementThread(ChatController client, ThreadStatusCallback callBack) {
public MessageBusManagementThread(ChatController client) {
this.callBackToController = callBack;
this.client = client;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
/**
@@ -200,184 +182,6 @@ public class MessageBusManagementThread extends Thread {
return stringAggregation;
}
/**
* Smart Frequency Parser (V1.32)
* Replaces the old RegEx logic.
* Features:
* 1. Handles full frequencies (144.210) and short forms (.210, 210).
* 2. Handles extended precision/weird formatting (144.210.10, 144,210,10).
* 3. Prioritizes USER CONTEXT (History) over GLOBAL CONTEXT (Preferences).
*/
private void smartFrequencyExtraction(ChatMessage message, ChatPreferences prefs) {
// Regex Explanation:
// Part 1 (Full): Start (not digit), 3-5 digits, sep, 1-3 digits, OPTIONAL (sep, 1-3 digits)
// Matches: 144.210, 144.210.10, 10368.100
// Part 2 (Short1): Start (not digit), sep, 3 digits, OPTIONAL (sep, 1-3 digits)
// Matches: .210, .210.10, ,210
// Part 3 (Short2): Whitespace/Start, 3 digits, Whitespace/End
// Matches: " 210 ", " 144 "
String smartPattern = "(?<![\\d])(\\d{3,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])|(?<=\\s|^)(\\d{3})(?=\\s|$)";
Pattern pattern = Pattern.compile(smartPattern);
Matcher matcher = pattern.matcher(message.getMessageText());
ChatMember sender = message.getSender();
// Safety check, in case sender is null (e.g., server message)
if (sender == null) return;
while (matcher.find()) {
String foundRaw = matcher.group().trim();
// --- PRE-PROCESSING: Normalize separators ---
// 1. Replace all commas with dots to unify format (144,210,10 -> 144.210.10)
foundRaw = foundRaw.replace(",", ".");
double finalDetectedFrequency = 0.0;
Band finalDetectedBand = null;
boolean isShortForm = false;
// --- STEP 1: Type Determination (Short or Full?) ---
// Check if it starts with a dot (e.g. ".210") OR is just 3 digits ("210")
if (foundRaw.startsWith(".") || foundRaw.length() == 3) {
// It is a short form.
// We strip the leading dot for calculation if present -> "210.10" or "210"
if (foundRaw.startsWith(".")) foundRaw = foundRaw.substring(1);
isShortForm = true;
} else {
// It is a full frequency (e.g., 144.210.10 or 144.210)
try {
// Normalize "144.210.10" to "144.21010" for Double.parseDouble
String normalizedFull = normalizeFrequencyString(foundRaw);
finalDetectedFrequency = Double.parseDouble(normalizedFull);
finalDetectedBand = Band.fromFrequency(finalDetectedFrequency);
} catch (NumberFormatException e) { continue; }
}
// --- STEP 2: Context Resolution (Only needed for Short Forms) ---
if (isShortForm) {
// A) HISTORY CHECK (Priority 1: What did THIS USER do recently?)
// We search for the most recent band where this short form makes physical sense.
long bestTimestamp = 0;
// Iterate over all bands where the user is known
// (Assumption: ChatMember has a getter getKnownActiveBands())
if (sender.getKnownActiveBands() != null) {
for (java.util.Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry : sender.getKnownActiveBands().entrySet()) {
Band candidateBand = entry.getKey();
ChatMember.ActiveFrequencyInfo info = entry.getValue();
// Timeout Check: Info must not be older than 30 mins (1,800,000 ms)
if (System.currentTimeMillis() - info.timestampEpoch > 1800000) continue;
// Try Reconstruction: Band Prefix + ShortForm
// Example: Band 144 (Prefix "144") + "." + "210.10" -> "144.210.10"
try {
String reconstructedStr = candidateBand.getPrefix() + "." + foundRaw;
String normalizedReconstruction = normalizeFrequencyString(reconstructedStr);
double attemptFreq = Double.parseDouble(normalizedReconstruction);
// Does this frequency fit into the candidate band?
if (candidateBand.isPlausible(attemptFreq)) {
// If we have multiple matches, pick the most recent one
if (info.timestampEpoch > bestTimestamp) {
finalDetectedFrequency = attemptFreq;
finalDetectedBand = candidateBand;
bestTimestamp = info.timestampEpoch;
}
}
} catch (Exception e) { /* Ignore parsing errors */ }
}
}
// B) GLOBAL PREFERENCES CHECK (Priority 2: Fallback if history is empty/old)
if (finalDetectedBand == null) {
// Get standard band from prefs (e.g., "144" or "432")
String defaultPrefix = prefs.getNotify_optionalFrequencyPrefix().get();
try {
String reconstructedStr = defaultPrefix + "." + foundRaw;
String normalizedReconstruction = normalizeFrequencyString(reconstructedStr);
double attemptFreq = Double.parseDouble(normalizedReconstruction);
// Check if this results in a valid amateur radio band
Band defaultBandCandidate = Band.fromFrequency(attemptFreq);
if (defaultBandCandidate != null) {
finalDetectedFrequency = attemptFreq;
finalDetectedBand = defaultBandCandidate;
}
} catch (NumberFormatException e) {
// Number was likely not a frequency (e.g., "73" or "599") and didn't fit any band
continue;
}
}
}
// --- STEP 3: Process Result ---
if (finalDetectedBand != null && finalDetectedFrequency > 0) {
// 1. Store in the new Map (for future context/history)
sender.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
//propagate known frequency to all instances of the same callsign (callRaw may exist multiple times)
try {
ArrayList<Integer> sameCallIdx = client.checkListForChatMemberIndexesByCallSign(sender);
for (int idx : sameCallIdx) {
ChatMember cm = client.getLst_chatMemberList().get(idx);
if (cm != null && cm != sender) {
cm.addKnownFrequency(finalDetectedBand, finalDetectedFrequency);
}
}
} catch (Exception e) {
System.out.println("[SmartParser, warning]: failed to propagate known frequency across duplicates: " + e.getMessage());
}
// 2. Set the old String-Property for GUI compatibility
// We assume standard display format (MHz)
sender.setFrequency(new javafx.beans.property.SimpleStringProperty(String.valueOf(finalDetectedFrequency)));
System.out.println("[SmartParser] Detected for " + sender.getCallSign() + ": " +
finalDetectedFrequency + " MHz (" + finalDetectedBand + ") " +
(isShortForm ? "[derived from " + foundRaw + "]" : "[full match]"));
// Optional: Trigger Cluster-Spot here if enabled
}
}
}
/**
* Helper: Normalizes weird frequency formats to valid Double strings.
* Example: "144.210.10" -> "144.21010"
* Example: "144.210" -> "144.210"
*/
private String normalizeFrequencyString(String rawInput) {
// Input is already guaranteed to have only dots as separators (commas replaced earlier)
int firstDotIndex = rawInput.indexOf(".");
if (firstDotIndex != -1) {
// Check if there are more dots after the first one
String decimalPart = rawInput.substring(firstDotIndex + 1);
if (decimalPart.contains(".")) {
// Remove all subsequent dots to make it a valid double
decimalPart = decimalPart.replace(".", "");
return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
}
}
return rawInput;
}
/**
* Builds UserList and gets meta informations out of the chat, as far as it is
* possible. \n This is the only place where the Chatmember-List will be written
@@ -496,14 +300,11 @@ public class MessageBusManagementThread extends Thread {
*/
private void processRXMessage23001(ChatMessage messageToProcess) throws IOException, SQLException {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last message processed:\n" + messageToProcess.getMessageText(), false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
final String INITIALUSERLISTENTRY = "UA0";
final String USERENTEREDCHAT = "UA5";
final String USERENTEREDCHAT2 = "UA2"; // seen at 50MHZ Chat
final String initialChatHistoryEntry = "CR";
final String SERVERMESSAGEHISTORIC = "CR"; //takes messages out of the ON4KST history
final String SERVERMESSAGE = "CR";
final String USERLEFTCHAT = "UR6";
final String USERLEFTCHAT2 = "UR7";
final String CHATCHANNELMESSAGE = "CH";
@@ -534,7 +335,7 @@ public class MessageBusManagementThread extends Thread {
qrgQuestionTexts.add("your qrg?");
qrgQuestionTexts.add("qrg?");
qrgQuestionTexts.add("freq?");
qrgQuestionTexts.add("pse qrg");
qrgQuestionTexts.add("pse QRG");
/**
@@ -542,7 +343,7 @@ public class MessageBusManagementThread extends Thread {
*/
if (messageToProcess.getMessageText().isEmpty()) {
// System.out.println("[MSGBUSMGTT:] no processable data");
System.out.println("[MSGBUSMGTT:] ######################no processable data");
} else {
@@ -563,7 +364,6 @@ public class MessageBusManagementThread extends Thread {
* Initializes the Userlist if entry fits UA0
* UA0|3|DL6SAQ|walter not qrv|JN58CK|1| <- RXed
*
*
*/
if (splittedMessageLine[0].contains(INITIALUSERLISTENTRY)) {
// System.out.println("MSGBUS: User detected");
@@ -584,15 +384,16 @@ public class MessageBusManagementThread extends Thread {
newMember.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());//TODO evt obsolete!
newMember.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// this.client.getChatMemberTable().put(splittedMessageLine[2], newMember); //TODO: map -> List
//the own call will not be in the list
if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) {
this.client.getLst_chatMemberList().add(newMember); //the own call will not be in the list
this.client.getLst_chatMemberList().add(newMember);
}
this.client.getDbHandler().storeChatMember(newMember);
// bufwrtrDBGMSGOut.write(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
// + "[MSGBUSMGT:] User detected and added to list [" + this.client.getChatMemberTable().size()
// + "] :" + newMember.getCallSign() + "\n");
@@ -637,8 +438,6 @@ public class MessageBusManagementThread extends Thread {
}
this.client.fireUserListUpdate("User entered the chat");
// this.client.getChatMemberTable().put(splittedMessageLine[2], newMember);
// System.out.println("[MSGBUSMGT:] New entered User detected and added to list ["
@@ -663,14 +462,29 @@ public class MessageBusManagementThread extends Thread {
this.client.getLst_chatMemberList().remove(
checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), newMember));
//since 1.26 new method design to detect chatcategory, too!
//TODO: since 1.26 new method design to detect chatcategory, too!
} catch (Exception e) {
System.out.println("[MSGBUSMGT, EXC!, Error:] User sent left chat but had not been there ... ["
+ this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign() + "\n"
+ e.getStackTrace());
// e.printStackTrace();
}
// int indexToDelete = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(),
// newMember);
// if (indexToDelete != -1) {
// System.out.println("[MSGBUSMGT:] User left Chat and is removed from list ["
// + this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign());
//
// this.client.getLst_chatMemberList().remove(indexToDelete);
//
// } else {
// System.out.println("[MSGBUSMGT:] Error, user sent left chat but had not been there ... ["
// + this.client.getLst_chatMemberList().size() + "] :" + newMember.getCallSign());
//
// }
} else
/**
@@ -710,28 +524,8 @@ public class MessageBusManagementThread extends Thread {
if (index != -1) {
//user not found in the chatmember list
try {
// newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
// this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
newMessageArrived.setSender(senderObj);
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// Remember last inbound category per callsignRaw (required for correct send-routing later)
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
// Metrics for scoring: momentum, response-time, no-reply, positive signals
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
newMessageArrived.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
);
// Activity/category changes influence priority => request recompute
this.client.getScoreService().requestRecompute("rx-chat-message");
newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
} catch (Exception exc) {
ChatMember aSenderDummy = new ChatMember();
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
@@ -817,7 +611,8 @@ public class MessageBusManagementThread extends Thread {
if (newMessageArrived.getReceiver().getCallSign()
.equals(this.client.getChatPreferences().getStn_loginCallSign())) {
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
// this.client.getLst_toMeMessageList().add(0, newMessageArrived); //TODO: change, moved to globalmessagelist, original
this.client.getLst_globalChatMessageList().add(0, newMessageArrived); //TODO: change, moved to globalmessagelist, original
if (this.client.getChatPreferences().isNotify_playSimpleSounds()) {
this.client.getPlayAudioUtils().playNoiseLauncher('P');
@@ -834,100 +629,8 @@ public class MessageBusManagementThread extends Thread {
this.client.getPlayAudioUtils().playVoiceLauncher("!");
}
}
if (newMessageArrived.getMessageText().toUpperCase().contains("//VER")) {
ChatMessage versionInfo = new ChatMessage();
ChatMember itsMe = new ChatMember();
itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
versionInfo.setSender(itsMe);
versionInfo.setReceiver(newMessageArrived.getSender());
versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + ApplicationConstants.AUTOANSWER_PREFIX + " " + "KST4Contest " + " v" + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER + " by DO5AMF");
this.client.getMessageTXBus().add(versionInfo);
}
// if (this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled()) {
//
// ChatMessage automaticAnswer = new ChatMessage();
// ChatMember itsMe = new ChatMember();
// itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
//
// automaticAnswer.setSender(itsMe);
// automaticAnswer.setReceiver(newMessageArrived.getSender());
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat());
//
// this.client.getMessageTXBus().add(automaticAnswer);
//
// }
/**
* auto reply/answer to QRG requests is here
*/
// if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) {
//
// for (String lookForQRGString : qrgQuestionTexts) {
// if (newMessageArrived.getMessageText().contains(lookForQRGString)) {
//
// ChatMessage automaticAnswer = new ChatMessage();
// ChatMember itsMe = new ChatMember();
// itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
//
// automaticAnswer.setSender(itsMe);
// automaticAnswer.setReceiver(newMessageArrived.getSender());
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
//
// if (this.client.getChatPreferences().isLoginToSecondChatEnabled()) {
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRGs: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue() + " / " + this.client.getChatPreferences().getMYQRGSecondCat().getValue());
// } else {
// automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
// }
//
// this.client.getMessageTXBus().add(automaticAnswer);
//
// }
// }
// }
// ==== Unified Autoanswer (Generic + QRG) with Pingpong-Guard + per-Remote Cooldown ====
final String incomingText = newMessageArrived.getMessageText();
final String incomingLower = (incomingText == null) ? "" : incomingText.toLowerCase(Locale.ROOT);
// 1) Pingpong-security: never ever react to auto generated messages
if (!isAutoMessage(newMessageArrived)) {
boolean qrgRequested = false;
if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) {
for (String lookForQRGString : qrgQuestionTexts) {
if (incomingLower.contains(lookForQRGString)) {
qrgRequested = true;
break;
}
}
}
boolean genericEnabled = this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled();
// 2) Entscheide, ob überhaupt geantwortet wird (QRG hat Vorrang vor Generic)
String payload = null;
if (qrgRequested) {
if (this.client.getChatPreferences().isLoginToSecondChatEnabled()) {
payload = "QRGs: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue()
+ " / " + this.client.getChatPreferences().getMYQRGSecondCat().getValue();
} else {
payload = "QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue();
}
} else if (genericEnabled) {
payload = this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat();
}
// 3) Cooldown pro Gegenstation: nur wenn DIESER Client jetzt wirklich sendet
if (payload != null && isAutoAnswerAllowedNow(newMessageArrived)) {
if (this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled()) {
ChatMessage automaticAnswer = new ChatMessage();
ChatMember itsMe = new ChatMember();
@@ -935,19 +638,39 @@ public class MessageBusManagementThread extends Thread {
automaticAnswer.setSender(itsMe);
automaticAnswer.setReceiver(newMessageArrived.getSender());
// Prefix fest + nicht entfernbar, damit Auto↔Auto nicht pingpongt
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
+ " " + AUTOANSWER_PREFIX + " " + payload);
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " " + this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat());
this.client.getMessageTXBus().add(automaticAnswer);
// Cooldown wird NUR hier gesetzt (nicht bei 'message sent by me' Echo),
// damit nur lokale Auto-Sends zählen.
markLocalAutoAnswerSent(newMessageArrived);
}
}
/**
* auto reply/answer to QRG requests is here
*/
if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) {
for (String lookForQRGString : qrgQuestionTexts) {
if (newMessageArrived.getMessageText().contains(lookForQRGString)) {
ChatMessage automaticAnswer = new ChatMessage();
ChatMember itsMe = new ChatMember();
itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
automaticAnswer.setSender(itsMe);
automaticAnswer.setReceiver(newMessageArrived.getSender());
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
if (this.client.getChatPreferences().isLoginToSecondChatEnabled()) {
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRGs: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue() + " / " + this.client.getChatPreferences().getMYQRGSecondCat().getValue());
} else {
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign() + " KST4Contest Auto: QRG is: " + this.client.getChatPreferences().getMYQRGFirstCat().getValue());
}
this.client.getMessageTXBus().add(automaticAnswer);
}
}
}
System.out.println("message directed to me: " + newMessageArrived.getReceiver().getCallSign() + ".");
@@ -967,6 +690,7 @@ public class MessageBusManagementThread extends Thread {
} else {
//message sent to other user
// this.client.getLst_toOtherMessageList().add(0, newMessageArrived); //TODO: change, moved to globalmessagelist, original
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
newMessageArrived.getSender().getQra(),
newMessageArrived.getReceiver().getQra(),
@@ -985,31 +709,11 @@ public class MessageBusManagementThread extends Thread {
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
try {
if (newMessageArrived.getSender().getFrequency() != null) {
//TODO: testing for next version 3.33: addinitional information will be displayed in cluster if there is such an information
ChatMember onlyForSpottingObject = new ChatMember();
onlyForSpottingObject.setCallSign(newMessageArrived.getSender().getCallSign());
onlyForSpottingObject.setFrequency(newMessageArrived.getSender().getFrequency());
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 0) {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + " , AP: " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getArrivingDurationMinutes() + "min, " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getPotential() + "%");
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 1) {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + "; " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getArrivingDurationMinutes() + "min, " +
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getPotential() + "%");
}
} else {
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra());
}
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject); //tells the DXCluster server to send a DXC message for this member to the logbook software
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(newMessageArrived.getSender()); //tells the DXCluster server to send a DXC message for this member to the logbook software
}
} catch (Exception exception) {
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
// exception.printStackTrace();
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0" + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
exception.printStackTrace();
}
}
@@ -1051,8 +755,50 @@ public class MessageBusManagementThread extends Thread {
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
}
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
smartFrequencyExtraction(newMessageArrived, this.client.getChatPreferences());
String locatedFrequencies = checkIfMessageInhibitsFrequency(newMessageArrived);
SimpleStringProperty qrg = new SimpleStringProperty(locatedFrequencies);
if (!splittedMessageLine[3].equals("SERVER")) {
if (locatedFrequencies.equals("")) {
// no qrg found, nothing to do
} else {
ChatMember temp3 = new ChatMember();
temp3.setCallSign(splittedMessageLine[3]);
temp3.setChatCategory(chategoryForMessageAndMessageSender);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), temp3);
if (index == -1) { // user is not in the userlist but sent message...
/**
* CH|2|1664663240|IK7LMX|Gilberto QRO|0|pse ant to jn80|YT5W| Caused this line
*/
System.out.println("[MSGBUSMGT <<<catched ERROR>>>]:, Frequency for " + splittedMessageLine[3]
+ " is not settable, Callsign is not in the Member-list!");
//create dummy user to display the message but it wont be hit an existing user object
ChatMember newMember = new ChatMember();
newMember.setCallSign(splittedMessageLine[3]);
newMember.setName(splittedMessageLine[4]);
newMember.setFrequency(qrg);
} else {
/**
* User is in the list...
*/
this.client.getLst_chatMemberList().get(index).setFrequency(qrg);
System.out.println("[MSGBUSMGT:] Frequency for " + splittedMessageLine[3] + " setted: "
+ locatedFrequencies);
// this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(this.client.getLst_chatMemberList().get(index)); //tells the DXCluster server to send a DXC message for this member to the logbook software
}
}
}
// TODO: Next: get frequency infos out of name?
} else
@@ -1260,214 +1006,6 @@ public class MessageBusManagementThread extends Thread {
this.client.getLst_chatMemberList().get(index).setState(stateChangeMember.getState());
}
} else
/**
* Handled like normal messages, but historic...will not trigger any functions
*
* Chat history line like:
* CR|6|1771165971|DF0GEB|test|0|ok|0|
* ^^hist
* ^chan
* ^^^^^^^^^^time ...
*/
if (splittedMessageLine[0].contains(SERVERMESSAGEHISTORIC)) {
ChatMessage newMessageArrived = new ChatMessage();
ChatCategory chategoryForMessageAndMessageSender;
newMessageArrived.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
chategoryForMessageAndMessageSender = newMessageArrived.getChatCategory();
newMessageArrived.setMessageGeneratedTime(splittedMessageLine[2]);
if (splittedMessageLine[3].equals("SERVER")) {
ChatMember dummy = new ChatMember();
dummy.setCallSign("SERVER");
dummy.setName("Sysop");
newMessageArrived.setSender(dummy);
newMessageArrived.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
dummy.setChatCategory(util_getChatCategoryByCategoryNrString(splittedMessageLine[1]));
// System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> servers cat " + newMessageArrived.getChatCategory());
} else {
ChatMember sender = new ChatMember();
sender.setCallSign(splittedMessageLine[3]);
sender.setChatCategory(chategoryForMessageAndMessageSender);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), sender);
if (index != -1) {
//user not found in the chatmember list
try {
// newMessageArrived.setSender(this.client.getLst_chatMemberList().get(index)); // set sender to member of
// this.client.getLst_chatMemberList().get(index).setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
ChatMember senderObj = this.client.getLst_chatMemberList().get(index);
newMessageArrived.setSender(senderObj);
senderObj.setActivityTimeLastInEpoch(new Utils4KST().time_generateCurrentEpochTime());
// Remember last inbound category per callsignRaw (required for correct send-routing later)
this.client.rememberLastInboundCategory(senderObj.getCallSignRaw(), senderObj.getChatCategory());
// Metrics for scoring: momentum, response-time, no-reply, positive signals
this.client.getStationMetricsService().onInboundMessage(
senderObj.getCallSignRaw(),
System.currentTimeMillis(),
newMessageArrived.getMessageText(),
this.client.getChatPreferences(),
this.client.getChatPreferences().getStn_loginCallSign()
);
// Activity/category changes influence priority => request recompute
this.client.getScoreService().requestRecompute("rx-chat-message");
} catch (Exception exc) {
ChatMember aSenderDummy = new ChatMember();
aSenderDummy.setCallSign(splittedMessageLine[3] + "[n/a]");
aSenderDummy.setAirPlaneReflectInfo(new AirPlaneReflectionInfo());
newMessageArrived.setSender(aSenderDummy);
System.out.println("MsgBusmgtT: Catched Error! " + exc.getMessage() + " // " + splittedMessageLine[3] + " is not in the list! Faking sender!");
exc.printStackTrace();
}
// b4 init list
} else {
//user not found in chatmember list, mark it, sender can not be set
if (!sender.getCallSign().equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
sender.setCallSign("[n/a]" + sender.getCallSign());
// if someone sent a message without being in the userlist (cause
// on4kst missed implementing....), callsign will be marked
} else {
//that means, message was by own station, broadcasted to all other
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
AirPlaneReflectionInfo preventNullpointerExc = new AirPlaneReflectionInfo();
preventNullpointerExc.setAirPlanesReachableCntr(0);
sender.setAirPlaneReflectInfo(preventNullpointerExc);
newMessageArrived.setSender(sender); //my own call is the sender
}
}
// newMessageArrived.setSender(this.client.getChatMemberTable().get(splittedMessageLine[3]));
}
newMessageArrived.setMessageSenderName(splittedMessageLine[4]);
newMessageArrived.setMessageText(splittedMessageLine[6]);
if (splittedMessageLine[7].equals("0")) {
// message is not directed to anyone, move it to the cq messages!
ChatMember dummy = new ChatMember();
dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy);
this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
} else {
//message is directed to another chatmember, process as such!
ChatMember receiver = new ChatMember();
receiver.setChatCategory(chategoryForMessageAndMessageSender); //got out of message itself
receiver.setCallSign(splittedMessageLine[7]);
int index = checkListForChatMemberIndexByCallSign(this.client.getLst_chatMemberList(), receiver);
if (index != -1) {
newMessageArrived.setReceiver(this.client.getLst_chatMemberList().get(index));// -1: Member left Chat
// before...
} else { //found in active member list
if (receiver.getCallSign().equals(client.getChatPreferences().getStn_loginCallSign())) {
/**
* If mycallsign sent a message to the server, server will publish that message and
* send it to all chatmember including me.
* As mycall is not in the userlist, the message would not been displayed if I handle
* it in the next case (marking left user, just for information). But I want an echo.
*/
receiver.setCallSign(client.getChatPreferences().getStn_loginCallSign());
newMessageArrived.setReceiver(receiver);
} else {
//this are user which left chat but had been adressed by this message
receiver.setCallSign(receiver.getCallSign() + "(left)");
newMessageArrived.setReceiver(receiver);
}
}
// System.out.println("message directed to: " + newMessageArrived.getReceiver().getCallSign() + ". EQ?: " + this.client.getownChatMemberObject().getCallSign() + " sent by: " + newMessageArrived.getSender().getCallSign().toUpperCase() + " -> EQ?: "+ this.client.getChatPreferences().getLoginCallSign().toUpperCase());
try {
/**
* message is directed to me, will be put in the "to me" messagelist
*/
if (newMessageArrived.getReceiver().getCallSign()
.equals(this.client.getChatPreferences().getStn_loginCallSign())) {
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
System.out.println("Historic message directed to me: " + newMessageArrived.getReceiver().getCallSign() + ".");
} else if (newMessageArrived.getSender().getCallSign().toUpperCase()
.equals(this.client.getChatPreferences().getStn_loginCallSign().toUpperCase())) {
/**
* message sent by me!
* message from me will appear in the PM window, too, with (>CALLSIGN) before
*/
String originalMessage = newMessageArrived.getMessageText();
newMessageArrived
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
this.client.getLst_globalChatMessageList().add(0,newMessageArrived);
// if you sent the message to another station, it will be sorted in to
// the "to me message list" with modified messagetext, added rxers callsign
} else {
//message sent to other user
if (DirectionUtils.isInAngleAndRange(client.getChatPreferences().getStn_loginLocatorMainCat(),
newMessageArrived.getSender().getQra(),
newMessageArrived.getReceiver().getQra(),
client.getChatPreferences().getStn_maxQRBDefault(),
client.getChatPreferences().getStn_antennaBeamWidthDeg())) {
newMessageArrived.getSender().setInAngleAndRange(true);
} else {
newMessageArrived.getSender().setInAngleAndRange(false);
}
this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
}
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
System.out.println("MSGBS bgfx, <<<catched error>>>: referenced user left the chat during messageprocessing or message got before user entered chat message: " + referenceDeletedByUserLeftChatDuringMessageprocessing.getStackTrace());
// referenceDeletedByUserLeftChatDuringMessageprocessing.printStackTrace();
}
// sdtout to me message-List
}
try {
System.out.println("[MSGBUSMGT:] processed message: " + newMessageArrived.getChatCategory().getCategoryNumber()
+ " " + newMessageArrived.getSender().getCallSign() + ", " + newMessageArrived.getMessageSenderName() + " -> "
+ newMessageArrived.getReceiver().getCallSign() + ": " + newMessageArrived.getMessageText());
} catch (Exception exceptionOccured) {
System.out.println("[MSGMgtBus: ERROR CHATCHED ON MAYBE NULL ISSUE]: " + exceptionOccured.getMessage() + "\n" + exceptionOccured.getStackTrace());
}
// --- Band/QRG recognition (fills ChatMember.knownActiveBands) ---
smartFrequencyExtraction(newMessageArrived, this.client.getChatPreferences());
} else
/**
@@ -1582,47 +1120,6 @@ public class MessageBusManagementThread extends Thread {
}
/**
* check if message had been auto generated
* @param msg
* @return
*/
private boolean isAutoMessage(ChatMessage msg) {
return msg != null
&& msg.getMessageText() != null
&& msg.getMessageText().contains(AUTOANSWER_PREFIX);
}
private String autoAnswerCooldownKey(ChatMessage incoming) {
String remoteCall = "UNKNOWN";
if (incoming != null && incoming.getSender() != null && incoming.getSender().getCallSign() != null) {
remoteCall = incoming.getSender().getCallSign().toUpperCase();
}
int cat = 0; // fallback
if (incoming != null && incoming.getSender() != null && incoming.getSender().getChatCategory() != null) {
cat = incoming.getSender().getChatCategory().getCategoryNumber();
}
// pro Gegenstation + pro Chat-Kategorie (falls derselbe Call in Cat2/Cat3 PMs macht)
return remoteCall + "|" + cat;
}
private boolean isAutoAnswerAllowedNow(ChatMessage incoming) {
String key = autoAnswerCooldownKey(incoming);
Long last = lastLocalAutoAnswerPerRemoteMs.get(key);
long now = System.currentTimeMillis();
return last == null || (now - last) >= AUTOANSWER_COOLDOWN_MS;
}
private void markLocalAutoAnswerSent(ChatMessage incoming) {
lastLocalAutoAnswerPerRemoteMs.put(autoAnswerCooldownKey(incoming), System.currentTimeMillis());
}
public void run() {
// fileLogRAW = new File(new Utils4KST().time_generateCurrentMMddString() + "_praktiKST_raw.txt");
@@ -1681,7 +1178,7 @@ public class MessageBusManagementThread extends Thread {
try {
messageTextRaw = client.getMessageRXBus().take();
if (messageTextRaw.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL) && messageTextRaw.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
if (messageTextRaw.getMessageText().equals("POISONPILL_KILLTHREAD") && messageTextRaw.getMessageSenderName().equals("POISONPILL_KILLTHREAD")) {
client.getMessageRXBus().clear();
break;
}
@@ -1925,6 +1422,7 @@ public class MessageBusManagementThread extends Thread {
// } //end tx.peek != null
}
// System.out.println("messagebusmgt while performed");
} // while true end
System.out.println("Msgbusmgt: interrupt");

View File

@@ -1,237 +0,0 @@
package kst4contest.controller;
import kst4contest.controller.interfaces.PstRotatorEventListener;
import kst4contest.model.ThreadStateMessage;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PstRotatorClient implements Runnable {
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "PSTRotator";
private static final Logger LOGGER = Logger.getLogger(PstRotatorClient.class.getName());
private static final int BUFFER_SIZE = 1024;
// Konfiguration
private final String host;
private final int remotePort; // Port, auf dem PSTRotator hört (z.B. 12060)
private final int localPort; // Port, auf dem wir hören (z.B. 12061)
private DatagramSocket socket;
private volatile boolean running = false;
private PstRotatorEventListener listener;
// Executor für Polling (Status-Abfrage)
private ScheduledExecutorService poller;
/**
* Konstruktor
* @param host IP Adresse von PSTRotator (meist "127.0.0.1")
* @param remotePort Der Port, der in PSTRotator eingestellt ist (User-Wunsch: 12060)
* @param listener Callback für den Chatcontroller
*/
public PstRotatorClient(String host, int remotePort, PstRotatorEventListener listener, ThreadStatusCallback callBack) {
this.callBackToController = callBack;
this.host = host;
this.remotePort = remotePort;
// Laut Manual antwortet PSTRotator oft auf Port+1.
// Wir binden uns also standardmäßig auf remotePort + 1.
this.localPort = remotePort + 1;
this.listener = listener;
}
/**
* alternative constructor for seting the remote port explicitely
*/
public PstRotatorClient(String host, int remotePort, int localPort, PstRotatorEventListener listener) {
this.host = host;
this.remotePort = remotePort;
this.localPort = localPort;
this.listener = listener;
}
/**
* Startet den Empfangs-Thread und das Polling
*/
public void start() {
try {
// Socket binden
// socket = new DatagramSocket(null);
// socket.setReuseAddress(true);
// socket = new DatagramSocket(localPort);
//
socket = new DatagramSocket(null);
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(localPort)); //bind to port
running = true;
// 1. Empfangs-Thread starten (dieses Runnable)
Thread thread = new Thread(this, "PSTRotator-Listener-" + remotePort);
thread.start();
// 2. Polling starten (z.B. alle 2 Sekunden Status abfragen)
poller = Executors.newSingleThreadScheduledExecutor();
poller.scheduleAtFixedRate(this::pollStatus, 1, 2, TimeUnit.SECONDS);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, running, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
LOGGER.info("PstRotatorClient started. Remote: " + remotePort + ", Local: " + localPort);
} catch (SocketException e) {
LOGGER.log(Level.SEVERE, "Fehler beim Öffnen des UDP Sockets", e);
}
}
/**
* Stopping threads and closing sockets of pstRotator communicator
*/
public void stop() {
running = false;
if (poller != null && !poller.isShutdown()) {
poller.shutdownNow();
}
if (socket != null && !socket.isClosed()) {
socket.close();
}
}
/**
* Main loop in thread which listens fpr PSTrotator packets
*/
@Override
public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
while (running && !socket.isClosed()) {
try {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // Blockiert bis Daten kommen
String received = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + received, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
parseResponse(received);
} catch (IOException e) {
if (running) {
LOGGER.log(Level.WARNING, "Fehler beim Empfangen des Pakets", e);
}
}
}
}
/**
* parses a pst rotatpr message to fit the PST listener interface
* @param msg
*/
private void parseResponse(String msg) {
// Debug
if (listener != null) listener.onMessageReceived(msg);
// Example answer: "AZ:145.0<CR>", "EL:010.0<CR>", "MODE:1<CR>"
msg = msg.replace("<CR>", "").trim();
try {
if (msg.startsWith("AZ:")) {
String val = msg.substring(3);
if (listener != null) listener.onAzimuthUpdate(Double.parseDouble(val));
}
else if (msg.startsWith("EL:")) {
String val = msg.substring(3);
if (listener != null) listener.onElevationUpdate(Double.parseDouble(val));
}
else if (msg.startsWith("MODE:")) {
// MODE:1 = Tracking, MODE:0 = Manual
String val = msg.substring(5);
boolean tracking = "1".equals(val);
if (listener != null) listener.onModeUpdate(tracking);
}
else if (msg.startsWith("OK:")) {
// Bestätigung von Befehlen, z.B. OK:STOP:1
LOGGER.fine("Befehl bestätigt: " + msg);
}
} catch (NumberFormatException e) {
LOGGER.warning("Konnte Wert nicht parsen: " + msg);
}
}
// --- Sende Methoden (API für den Chatcontroller) ---
private void sendUdp(String message) {
if (socket == null || socket.isClosed()) return;
try {
byte[] data = message.getBytes(StandardCharsets.US_ASCII);
InetAddress address = InetAddress.getByName(host);
DatagramPacket packet = new DatagramPacket(data, data.length, address, remotePort);
socket.send(packet);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Fehler beim Senden an PstRotator", e);
}
}
/**
* Sendet den generischen XML Befehl.
* Bsp: <PST><AZIMUTH>85</AZIMUTH></PST>
*/
private void sendCommand(String tag, String value) {
String xml = String.format("<PST><%s>%s</%s></PST>", tag, value, tag);
System.out.println("PSTRotatorClient: sent: " + xml);
sendUdp(xml);
}
// Öffentliche Steuermethoden
public void setAzimuth(double degrees) {
// Formatierung ohne unnötige Nachkommastellen, falls nötig
sendCommand("AZIMUTH", String.valueOf((int) degrees));
}
public void setElevation(double degrees) {
sendCommand("ELEVATION", String.valueOf(degrees));
}
public void stopRotor() {
sendCommand("STOP", "1");
}
public void park() {
sendCommand("PARK", "1");
}
public void setTrackingMode(boolean enable) {
sendCommand("TRACK", enable ? "1" : "0");
}
/**
* Method for polling rotators status via PSTRotator software. Asks only for AZ value!<br/>
* Scheduled in a fixed time by executor
*/
public void pollStatus() {
// PSTRotator Dokumentation:
// <PST>AZ?</PST>
// <PST>EL?</PST>
// <PST>MODE?</PST>
// Man kann mehrere Befehle in einem Paket senden
String query = "<PST><AZ?></AZ?><EL?></EL?><MODE?></MODE?></PST>";
// HINWEIS: Laut Doku ist die Syntax für Abfragen etwas anders: <PST>AZ?</PST>
// Daher bauen wir den String manuell, da sendCommand Tags schließt.
sendUdp("<PST>AZ?</PST>");
// sendUdp("<PST>EL?</PST>");
sendUdp("<PST>MODE?</PST>");
}
}

View File

@@ -1,407 +0,0 @@
package kst4contest.controller;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ReadUDPByWintestThread extends Thread {
private static final Pattern STATUS_TOKEN_PATTERN = Pattern.compile("\"([^\"]*)\"|(\\S+)");
private DatagramSocket socket;
private ChatController client;
private volatile boolean running = true;
private int PORT = 9871; //default
private static final int BUFFER_SIZE = 4096;
private final Map<Integer, String> receivedQsos = new ConcurrentHashMap<>();
private long lastPacketTime = 0;
private String myStation = "DO5AMF";
private String targetStation = "";
private String stationID = "";
private int lastKnownQso = 0;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "Wintest-msg";
public ReadUDPByWintestThread(ChatController client, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.client = client;
this.myStation = client.getChatPreferences().getStn_loginCallSignRaw(); //callsign of the logging stn
this.PORT = client.getChatPreferences().getLogsynch_wintestNetworkPort();
}
@Override
public void interrupt() {
running = false;
if (socket != null && !socket.isClosed()) socket.close();
super.interrupt();
}
@Override
public void run() {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("ReadUDPByWintestThread");
byte[] buffer = new byte[BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
socket = new DatagramSocket(null); //first init with null, then make ready for reuse
socket.setReuseAddress(true);
// socket = new DatagramSocket(PORT);
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_wintestNetworkPort()));
socket.setSoTimeout(3000);
System.out.println("[WinTest UDP listener] started at port: " + PORT);
} catch (SocketException e) {
e.printStackTrace();
return;
}
while (running) {
try {
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
processWinTestMessage(msg);
} catch (SocketTimeoutException e) {
// checkForMissingQsos();
} catch (IOException e) {
//TODO: here is something to catch
}
}
}
private void processWinTestMessage(String msg) {
// System.out.println("Wintest-Message received: " + msg);
lastPacketTime = System.currentTimeMillis();
if (msg.startsWith("HELLO:")) { //Client Signon of wintest
parseHello(msg);
try {
// send_needqso();
}catch (Exception e) {
System.out.println("Error: ");
e.printStackTrace();
}
} else if (msg.startsWith("ADDQSO:")) { //adding qso to wintest log
try {
parseAddQso(msg);
} catch (Exception e) {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Parsing ERROR: " + Arrays.toString(e.getStackTrace()), true);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
} else if (msg.startsWith("STATUS")) {
parseStatus(msg);
} else if (msg.startsWith("IHAVE:")) { //periodical message of wintest, which qsos are in the log
// parseIHave(msg); //TODO
}
else if (msg.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
System.out.println("ReadUdpByWintest, Info: got poison, now dieing....");
socket.close();
running = false;
}
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "message received\n" + msg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
/**
* parsing of the hello message of wintest:
* "HELLO: "STN1" "" 6667 130 "SLAVE" 1 0 1762201985"
* @param msg
*/
private void parseHello(String msg) {
try {
String[] tokens = msg.split("\"");
if (tokens.length >= 2) {
targetStation = tokens[1];
System.out.println("[WinTest rcv: found logger instance: " + targetStation);
}
} catch (Exception e) {
System.out.println("[WinTest] ERROR on HELLO-Parsing: " + e.getMessage());
}
}
private byte util_calculateChecksum(byte[] bytes) {
int sum = 0;
for (byte b : bytes) sum += b;
return (byte) ((sum | 0x80) & 0xFF);
}
/**
* Parse Win-Test STATUS packets and update own QRG from WT station.
*
* Parsing model (tokenized with quotes preserved):
* parts[0] = "STATUS"
* parts[1] = station name (example: "STN1")
* parts[5] = val2 (used to derive mode: 1 => SSB, else CW)
* parts[7] = frequency in 0.1 kHz units (example: 1443210 => 144321.0)
*/
private void parseStatus(String msg) {
try {
ArrayList<String> parts = new ArrayList<>();
Matcher matcher = STATUS_TOKEN_PATTERN.matcher(msg);
while (matcher.find()) {
if (matcher.group(1) != null) {
parts.add(matcher.group(1));
} else {
parts.add(matcher.group(2));
}
}
if (parts.size() < 8) {
System.out.println("[WinTest] STATUS too short: " + msg);
return;
}
String stn = parts.get(1);
String stationFilter = client.getChatPreferences().getLogsynch_wintestNetworkStationNameOfWintestClient1();
if (stationFilter != null && !stationFilter.isBlank() && !stn.equalsIgnoreCase(stationFilter)) {
return;
}
String val2 = parts.get(5);
String freqRaw = parts.get(7);
double freqFloat = Integer.parseInt(freqRaw) / 10.0;
String mode;
if ("1".equals(val2)) {
mode = freqFloat > 10000.0 ? "usb" : "lsb";
} else {
mode = "cw";
}
// Format as MMM.KKK.HH display format (e.g. 144.300.00) consistent with UCX thread
// freqFloat is in kHz (e.g. 144300.0), convert to Hz-string for formatting
long freqHzTimes100 = Math.round(freqFloat * 100.0); // e.g. 14430000
String hzStr = String.valueOf(freqHzTimes100);
String formattedQRG;
if (hzStr.length() == 8) {
// 144MHz range: 14430000 -> 144.300.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 3), hzStr.substring(3, 6), hzStr.substring(6, 8));
} else if (hzStr.length() == 9) {
// 1296MHz range: 129600000 -> 1296.000.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 4), hzStr.substring(4, 7), hzStr.substring(7, 9));
} else if (hzStr.length() == 7) {
// 70MHz range: 7010000 -> 70.100.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 2), hzStr.substring(2, 5), hzStr.substring(5, 7));
} else if (hzStr.length() == 6) {
// 50MHz range: 5030000 but 6 digits: 503000 -> 5.030.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 1), hzStr.substring(1, 4), hzStr.substring(4, 6));
} else {
formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback
}
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG);
} catch (Exception e) {
System.out.println("[WinTest] STATUS parsing error: " + e.getMessage());
}
}
// private void send_needqso() throws IOException {
// String payload = String.format("NEEDQSO:\"%s\" \"%s\" \"%s\" %d %d?\0",
// "DO5AMF", "STN1", stationID, 1, 9999);
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
// }
// private void send_hello() throws IOException {
// String payload = String.format("HELLO:\"%s\" \"%s\" \"%s\" %d %d?\0",
// "DO5AMF", "", stationID, "SLAVE", 1, 14);
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
// }
/**
* Catches add-qso messages of wintest if a new qso gets into the log<br/>
*
* String is like this:<br/><br/>
*ADDQSO: "STN1" "" "STN1" 1762202297 1440000 0 12 0 0 0 2 2 "DM2RN" "599" "599001" "JO51UM" "" "" 0 "" "" "" 44510
*
* ^^^^sentby<br/>
* ^^^^^^^^^^time<br/>
* ^^^^^^qrg<br/>
* ^^band<br/>
* ^^^^^callsign logged<br/>
* stn-id ^^^^
* @param msg
*/
private void parseAddQso(String msg) {
ChatMember modifyThat = null;
try {
// int qsoNumber = extractQsoNumber(msg);
// receivedQsos.put(qsoNumber, msg);
// lastKnownQso = Math.max(lastKnownQso, qsoNumber);
String callSignCatched = msg.split("\"") [7];
ChatMember workedCall = new ChatMember();
workedCall.setCallSign(callSignCatched);
workedCall.setWorked(true); //its worked at this place, for sure!
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
String bandId;
bandId = msg.split("\"")[6].split(" ")[4].trim();
switch (bandId) {
case "10" -> workedCall.setWorked50(true);
case "11" -> workedCall.setWorked70(true);
case "12" -> workedCall.setWorked144(true);
case "14" -> workedCall.setWorked432(true);
case "16" -> workedCall.setWorked1240(true);
case "17" -> workedCall.setWorked2300(true);
case "18" -> workedCall.setWorked3400(true);
case "19" -> workedCall.setWorked5600(true);
case "20" -> workedCall.setWorked10G(true);
case "21" -> workedCall.setWorked24G(true);
case "22" -> workedCall.setWorked47G(true);
case "23" -> workedCall.setWorked76G(true);
default -> System.out.println("[WinTestUDPRcvr: warning] Unbekannte Band-ID: " + bandId);
}
if (!markTheseChattersAsWorked.isEmpty()) {
//Worked call is part of the current chatmember list
for (int index : markTheseChattersAsWorked) {
//iterate through the logged in chatmembers callsigns and set the worked markers
modifyThat = client.getLst_chatMemberList().get(index);
modifyThat.setWorked(true); //worked its for sure
if (workedCall.isWorked50()) {
modifyThat.setWorked50(true);
} else if (workedCall.isWorked70()) {
modifyThat.setWorked70(true);
} else if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
} else if (workedCall.isWorked24G()) {
modifyThat.setWorked24G(true);
} else if (workedCall.isWorked47G()) {
modifyThat.setWorked47G(true);
} else if (workedCall.isWorked76G()) {
modifyThat.setWorked76G(true);
} else {
System.out.println("[WinTestUDPRcvr: warning] found no new worked-flag for this band: " + workedCall.getCallSignRaw() + bandId);
}
}
try {
GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
// trigger band-upgrade hint after log entry (Win-Test)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[WinTestUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
} catch (Exception IllegalStateException) {
//do nothing, as it works...
}
}
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
// This will update the worked info on a worked chatmember. DBHandler will
// check, if an entry at the db had been modified. If not, then the worked
// station had not been stored. DBHandler will store the information then.
if (!isInChat) {
workedCall.setName("unknown");
workedCall.setQra("unknown");
workedCall.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
this.client.getDbHandler().storeChatMember(workedCall);
}
File logUDPMessageToThisFile = new File(this.client.getChatPreferences()
.getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
FileWriter fileWriterPersistUDPToFile = null;
BufferedWriter bufwrtrRawMSGOut;
try {
fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);
} catch (IOException e1) {
e1.printStackTrace();
}
bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile);
if (modifyThat != null) {
bufwrtrRawMSGOut.write("\n" + modifyThat.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
} else {
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
}
System.out.println("[WinTest, Info: Marking Chatmember as worked: " + workedCall.toString());
// markChatMemberAsWorked(call, band); //TODO
} catch (Exception e) {
System.out.println("[WinTest] Fehler beim ADDQSO-Parsing: " + e.getMessage());
}
}
}

View File

@@ -1,18 +0,0 @@
package kst4contest.controller;
import kst4contest.model.ChatPreferences;
public class ReadUDPByWintestThreadTest {
public static void main(String[] args) {
ChatController ctrl1 = new ChatController();
ChatPreferences prefs = new ChatPreferences();
ctrl1.setChatPreferences(prefs);
// ReadUDPByWintestThread test = new ReadUDPByWintestThread(ctrl1);
// test.run();
}
}

View File

@@ -2,7 +2,6 @@ package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Comparator;
import javafx.collections.FXCollections;
@@ -11,7 +10,6 @@ import kst4contest.ApplicationConstants;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
/**
* This thread is responsible for reading server's input and printing it to the
@@ -26,16 +24,15 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
private ChatController client;
private int localPort;
private String ASIdentificator, ChatClientIdentificator;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "AirScout msg";
// public ReadUDPbyAirScoutMessageThread(int localPort) {
// this.localPort = localPort;
// }
public ReadUDPbyAirScoutMessageThread(int localPort) {
this.localPort = localPort;
}
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator,
String ChatClientIdentificator, ThreadStatusCallback callback) {
String ChatClientIdentificator) {
this.callBackToController = callback;
this.localPort = localPort;
this.client = client;
this.ASIdentificator = ASIdentificator;
@@ -57,12 +54,6 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
}
}
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) {
if (callBackToController != null) {
//update the visual control of running thread
callBackToController.onThreadStatus("AirScout", threadStateMessage);
}
}
public void run() {
@@ -137,30 +128,26 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
// do nothing, that is your own message
} else if (received.contains("ASNEAREST:")) { //answer by airscout
processASUDPMessage(received);
// processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
// System.out.println("[ReadUSPASTh, info:] received AS String " + received);
AirPlaneReflectionInfo apReflectInfoForChatMember;
apReflectInfoForChatMember = processASUDPMessage(received);
if (!this.client.getLst_chatMemberList().isEmpty()) {
if (this.client.getLst_chatMemberList().size() != 0) {
try {
// this.client.getLst_chatMemberList()
// .get(this.client.checkListForChatMemberIndexByCallSign(
// apReflectInfoForChatMember.getReceiver()))
// .setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
// // the central instance of
// // chatmember list .... -1 is a
// // problem!
ArrayList<Integer> addApInfoToThese = this.client.checkListForChatMemberIndexesByCallSign(apReflectInfoForChatMember.getReceiver());
addApInfoToThese.forEach((integerIndex) -> {this.client.getLst_chatMemberList().get(integerIndex).setAirPlaneReflectInfo(apReflectInfoForChatMember); });
// AirScout availability strongly affects priority => request recompute the score of the chatmember
this.client.getScoreService().requestRecompute("airscout-update");
// if (this.client.checkListForChatMemberIndexByCallSign(apReflectInfoForChatMember.getReceiver()) != -1) {
this.client.getLst_chatMemberList()
.get(this.client.checkListForChatMemberIndexByCallSign(
apReflectInfoForChatMember.getReceiver()))
.setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
// the central instance of
// chatmember list .... -1 is a
// problem!
/**
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10"
* java.util.ConcurrentModificationException at
@@ -171,7 +158,6 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
*
*/
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
// }
} catch (Exception e) {
@@ -181,13 +167,6 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
// TODO: handle exception
}
// String[] newState = new String[3];
// newState[0] = "On";
// newState[1] = "received line";
// newState[2] = apReflectInfoForChatMember.toString();
// callThreadStateToUi(newState);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + apReflectInfoForChatMember.toString(), false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
}

View File

@@ -4,7 +4,6 @@ import java.io.*;
import java.net.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
@@ -12,7 +11,6 @@ import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import kst4contest.ApplicationConstants;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -34,19 +32,13 @@ public class ReadUDPbyUCXMessageThread extends Thread {
private BufferedReader reader;
private Socket socket;
private ChatController client;
private int udpPortNr = 12060;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "UDP-Log msg";
// public ReadUDPbyUCXMessageThread(int localPort , ThreadStatusCallback callback) {
//
//// this.callBackToController = callback;
// }
public ReadUDPbyUCXMessageThread(int localPort) {
public ReadUDPbyUCXMessageThread(int localPort, ChatController client, ThreadStatusCallback callback) {
this.udpPortNr = localPort;
}
public ReadUDPbyUCXMessageThread(int localPort, ChatController client) {
this.client = client;
this.callBackToController = callback;
}
@Override
@@ -56,7 +48,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
if (this.socket != null) {
System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket");
terminateConnection();
// callBackToController.onThreadStatus("UDPReceiver", new String[]);
}
} catch (Exception e) {
// TODO Auto-generated catch block
@@ -66,22 +57,17 @@ public class ReadUDPbyUCXMessageThread extends Thread {
public void run() {
System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
Thread.currentThread().setName("ReadUDPByUCXLogThread");
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
DatagramSocket socket = null;
boolean running;
byte[] buf = new byte[1777];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
try {
// socket = new DatagramSocket(12060);
socket = new DatagramSocket(udpPortNr);
socket = new DatagramSocket(12060);
socket.setSoTimeout(2000); //TODO try for end properly
}
@@ -113,6 +99,8 @@ public class ReadUDPbyUCXMessageThread extends Thread {
nE.printStackTrace();
System.out.println("ReadUdpByUCXTH: Socket not ready");
try {
socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort());
socket.setSoTimeout(2000);
@@ -148,12 +136,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
System.out.println("ReadUdpByUCX, Info: got poison, now dieing....");
socket.close();
timeOutIndicator = true;
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "stopped";
// threadStatusMessage[1] = "by poisonpill message (disconnect on purpose)";
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "stopped by Poisonpill", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
break;
}
@@ -181,17 +163,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
ChatMember modifyThat = null;
// System.out.println("ReadUDPByUCX, message catched: " + udpMsg);
// String[] threadStatusMessage = new String[2];
// threadStatusMessage = new String[3];
// threadStatusMessage[0] = "on";
// threadStatusMessage[1] = "received message:";
// threadStatusMessage[2] = udpMsg;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
// System.out.println(udpMsg);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
@@ -268,7 +240,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
case "10G": {
workedCall.setWorked10G(true);
break;
}
/**
@@ -338,41 +310,56 @@ public class ReadUDPbyUCXMessageThread extends Thread {
modifyThat = client.getLst_chatMemberList().get(index);
modifyThat.setWorked(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked144(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked432(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked1240(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked2300(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked3400(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked5600(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked10G(true);
}
}
try {
GuiUtils.triggerGUIFilteredChatMemberListChange(this.client);
// BEGIN PATCH: trigger band-upgrade hint after log entry (UCXLog)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[UCXUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
} catch (Exception IllegalStateException) {
//do nothing, as it works...
}
@@ -552,7 +539,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
// System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
}
}
@@ -562,27 +549,9 @@ public class ReadUDPbyUCXMessageThread extends Thread {
e.printStackTrace();
System.out.println(e.getCause());
System.out.println(e.getMessage());
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
}
// System.out.println("[ReadUDPbyUCXTh: ] worked size = " + this.client.getMap_ucxLogInfoWorkedCalls().size());
@@ -592,14 +561,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
}
public boolean terminateConnection() throws IOException {
// String[] threadStatusMessage = new String[2];
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = "Connection terminated for purpose.";
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "terminated", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
this.socket.close();

View File

@@ -1,309 +0,0 @@
package kst4contest.controller;
import javafx.application.Platform;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.SimpleLongProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import kst4contest.logic.PriorityCalculator;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ContestSked;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
/**
* Calculates priority scores off the JavaFX thread and publishes a small UI model.
*
* Design goals:
* - No per-member Platform.runLater flooding.
* - Score is computed once per callsignRaw (e.g. "SM6VTZ"), even if it exists in multiple chat categories.
* - A routing hint (preferred ChatCategory) is kept using "last inbound category" if available.
*/
public final class ScoreService {
public static final int DEFAULT_TOP_N = 15; //how many top places we have?
/** Force a refresh at least every X ms (some scoring inputs are time dependent). */
private static final long MAX_SNAPSHOT_AGE_MS = 10_000L;
private final ChatController controller;
private final PriorityCalculator priorityCalculator;
private final AtomicBoolean recomputeRequested = new AtomicBoolean(true);
private final AtomicReference<ScoreSnapshot> latestSnapshot = new AtomicReference<>(ScoreSnapshot.empty());
// UI outputs
private final ObservableList<TopCandidate> topCandidatesFx = FXCollections.observableArrayList();
private final ReadOnlyDoubleWrapper selectedCallPriorityScore = new ReadOnlyDoubleWrapper(Double.NaN);
private final LongProperty uiPulse = new SimpleLongProperty(0);
private volatile String selectedCallSignRaw;
private volatile long lastComputedEpochMs = 0L;
private final int topN;
private final ObjectProperty<ChatMember> selectedChatMember = new SimpleObjectProperty<>(null);
public ScoreService(ChatController controller, PriorityCalculator priorityCalculator, int topN) {
this.controller = Objects.requireNonNull(controller, "controller");
this.priorityCalculator = Objects.requireNonNull(priorityCalculator, "priorityCalculator");
this.topN = topN > 0 ? topN : DEFAULT_TOP_N;
}
public ObservableList<TopCandidate> getTopCandidatesFx() {
return topCandidatesFx;
}
public ReadOnlyDoubleProperty selectedCallPriorityScoreProperty() {
return selectedCallPriorityScore.getReadOnlyProperty();
}
/**
* A lightweight UI invalidation signal that increments after every published snapshot.
* Consumers can refresh small panels (timeline/toplist), but should avoid refreshing huge tables.
*/
public LongProperty uiPulseProperty() {
return uiPulse;
}
public ScoreSnapshot getLatestSnapshot() {
return latestSnapshot.get();
}
/** Coalesced recompute request (safe to call frequently from other threads). */
public void requestRecompute(String reason) {
recomputeRequested.set(true);
}
/** Called by UI when selection changes. */
public void setSelectedChatMember(ChatMember member) {
// keep a central selection for UI actions (FurtherInfo buttons, timeline clicks, etc.)
if (Platform.isFxApplicationThread()) {
selectedChatMember.set(member);
} else {
Platform.runLater(() -> selectedChatMember.set(member));
}
selectedCallSignRaw = member == null ? null : normalizeCallRaw(member.getCallSignRaw());
// Update score immediately from the latest snapshot
if (Platform.isFxApplicationThread()) {
updateSelectedScoreFromSnapshot(latestSnapshot.get());
} else {
Platform.runLater(() -> updateSelectedScoreFromSnapshot(latestSnapshot.get()));
}
}
/**
* Called periodically by the scheduler thread.
* Recomputes only if explicitly requested or if the snapshot is too old.
*/
public void tick() {
long now = System.currentTimeMillis();
boolean shouldRecompute = recomputeRequested.getAndSet(false) || (now - lastComputedEpochMs) > MAX_SNAPSHOT_AGE_MS;
if (!shouldRecompute) return;
try {
// Apply "no reply" strikes (operator pinged via /cq but no inbound line arrived)
controller.getStationMetricsService().evaluateNoReplyTimeouts(now, controller.getChatPreferences());
recompute(now);
} catch (Exception e) {
System.err.println("[ScoreService] CRITICAL error while recomputing scores");
e.printStackTrace();
}
}
private void recompute(long nowEpochMs) {
// Keep sked list clean (must happen on FX thread)
controller.requestRemoveExpiredSkeds(nowEpochMs);
final List<ChatMember> members = controller.snapshotChatMembers();
final List<ContestSked> activeSkeds = controller.snapshotActiveSkeds();
final ChatPreferences prefs = controller.getChatPreferences();
final Map<String, ChatCategory> lastInbound = controller.snapshotLastInboundCategoryMap();
StationMetricsService.Snapshot metricsSnapshot =
controller.getStationMetricsService().snapshot(nowEpochMs, prefs);
// 1) Choose one representative per callsignRaw
Map<String, ChatMember> representativeByCallRaw = chooseRepresentativeMembers(members, lastInbound);
// 2) Compute score once per callsignRaw
Map<String, Double> scoreByCallRaw = new HashMap<>(representativeByCallRaw.size());
Map<String, ChatCategory> preferredCategoryByCallRaw = new HashMap<>(representativeByCallRaw.size());
List<TopCandidate> topAll = new ArrayList<>(representativeByCallRaw.size());
for (Map.Entry<String, ChatMember> e : representativeByCallRaw.entrySet()) {
String callRaw = e.getKey();
ChatMember representative = e.getValue();
if (representative == null) continue;
double score = priorityCalculator.calculatePriority(
representative,
prefs,
activeSkeds,
metricsSnapshot,
nowEpochMs
);
scoreByCallRaw.put(callRaw, score);
preferredCategoryByCallRaw.put(callRaw, representative.getChatCategory());
topAll.add(new TopCandidate(callRaw, representative.getCallSign(), representative.getChatCategory(), score));
}
// 3) Build Top-N
topAll.sort(Comparator.comparingDouble(TopCandidate::getScore).reversed());
List<TopCandidate> topNList = topAll.size() <= topN ? topAll : new ArrayList<>(topAll.subList(0, topN));
ScoreSnapshot snap = new ScoreSnapshot(
nowEpochMs,
Collections.unmodifiableMap(scoreByCallRaw),
Collections.unmodifiableMap(preferredCategoryByCallRaw),
Collections.unmodifiableList(topNList)
);
latestSnapshot.set(snap);
lastComputedEpochMs = nowEpochMs;
// 4) Publish to UI in ONE batched runLater
Platform.runLater(() -> {
topCandidatesFx.setAll(snap.getTopCandidates());
updateSelectedScoreFromSnapshot(snap);
uiPulse.set(uiPulse.get() + 1);
});
}
/**
* Picks one ChatMember object per callsignRaw.
* Preference order:
* 1) Variant in last inbound chat category (stable reply routing)
* 2) Most recently active variant (fallback)
*/
private Map<String, ChatMember> chooseRepresentativeMembers(
List<ChatMember> members,
Map<String, ChatCategory> lastInboundCategoryByCallRaw
) {
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
for (ChatMember m : members) {
if (m == null) continue;
String callRaw = normalizeCallRaw(m.getCallSignRaw());
if (callRaw == null || callRaw.isEmpty()) continue;
byCallRaw.computeIfAbsent(callRaw, k -> new ArrayList<>()).add(m);
}
Map<String, ChatMember> representative = new HashMap<>(byCallRaw.size());
for (Map.Entry<String, List<ChatMember>> entry : byCallRaw.entrySet()) {
String callRaw = entry.getKey();
List<ChatMember> variants = entry.getValue();
ChatCategory preferredCat = lastInboundCategoryByCallRaw.get(callRaw);
ChatMember chosen = null;
if (preferredCat != null) {
for (ChatMember v : variants) {
if (v != null && v.getChatCategory() == preferredCat) {
chosen = v;
break;
}
}
}
if (chosen == null) {
chosen = variants.stream()
.filter(Objects::nonNull)
.max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
.orElse(null);
}
if (chosen != null) representative.put(callRaw, chosen);
}
return representative;
}
private void updateSelectedScoreFromSnapshot(ScoreSnapshot snap) {
if (snap == null || selectedCallSignRaw == null) {
selectedCallPriorityScore.set(Double.NaN);
return;
}
Double v = snap.getScoreByCallSignRaw().get(selectedCallSignRaw);
selectedCallPriorityScore.set(v == null ? Double.NaN : v);
}
private static String normalizeCallRaw(String callRaw) {
if (callRaw == null) return null;
return callRaw.trim().toUpperCase();
}
// ------------------------- DTOs -------------------------
public static final class TopCandidate {
private final String callSignRaw;
private final String displayCallSign;
private final ChatCategory preferredChatCategory;
private final double score;
public TopCandidate(String callSignRaw, String displayCallSign, ChatCategory preferredChatCategory, double score) {
this.callSignRaw = callSignRaw;
this.displayCallSign = displayCallSign;
this.preferredChatCategory = preferredChatCategory;
this.score = score;
}
public String getCallSignRaw() { return callSignRaw; }
public String getDisplayCallSign() { return displayCallSign; }
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
public double getScore() { return score; }
}
public static final class ScoreSnapshot {
private final long computedAtEpochMs;
private final Map<String, Double> scoreByCallSignRaw;
private final Map<String, ChatCategory> preferredCategoryByCallSignRaw;
private final List<TopCandidate> topCandidates;
public ScoreSnapshot(long computedAtEpochMs,
Map<String, Double> scoreByCallSignRaw,
Map<String, ChatCategory> preferredCategoryByCallSignRaw,
List<TopCandidate> topCandidates) {
this.computedAtEpochMs = computedAtEpochMs;
this.scoreByCallSignRaw = scoreByCallSignRaw;
this.preferredCategoryByCallSignRaw = preferredCategoryByCallSignRaw;
this.topCandidates = topCandidates;
}
public static ScoreSnapshot empty() {
return new ScoreSnapshot(System.currentTimeMillis(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList());
}
public long getComputedAtEpochMs() { return computedAtEpochMs; }
public Map<String, Double> getScoreByCallSignRaw() { return scoreByCallSignRaw; }
public Map<String, ChatCategory> getPreferredCategoryByCallSignRaw() { return preferredCategoryByCallSignRaw; }
public List<TopCandidate> getTopCandidates() { return topCandidates; }
}
public ReadOnlyObjectProperty<ChatMember> selectedChatMemberProperty() {
return selectedChatMember;
}
public ChatMember getSelectedChatMember() {
return selectedChatMember.get();
}
}

View File

@@ -1,124 +0,0 @@
package kst4contest.controller;
import javafx.application.Platform;
import kst4contest.model.ChatCategory;
import kst4contest.model.ThreadStateMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* Schedules PM reminders for a specific sked time.
*
* Requirements:
* - Reminder goes out as PM to the station (via "/cq CALL ...").
* - Reminders are armed manually from FurtherInfo.
*/
public final class SkedReminderService {
private final ChatController controller;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("SkedReminderService");
return t;
});
private final ConcurrentHashMap<String, List<ScheduledFuture<?>>> scheduledByCallRaw = new ConcurrentHashMap<>();
public SkedReminderService(ChatController controller) {
this.controller = controller;
}
/**
* Arms reminders for one sked. Existing reminders for this call are cancelled.
*
* @param callSignRaw target call
* @param preferredCategory where to send (if null, controller resolves via lastInbound category)
* @param skedTimeEpochMs sked time
* @param offsetsMinutes e.g. [5,2,1] => reminders 5,2,1 minutes before
*/
public void armReminders(String callSignRaw,
ChatCategory preferredCategory,
long skedTimeEpochMs,
List<Integer> offsetsMinutes) {
String callRaw = normalize(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
cancelReminders(callRaw);
long now = System.currentTimeMillis();
List<Integer> offsets = (offsetsMinutes == null) ? List.of() : offsetsMinutes;
List<ScheduledFuture<?>> futures = new ArrayList<>();
for (Integer offMin : offsets) {
if (offMin == null) continue;
long fireAt = skedTimeEpochMs - (offMin * 60_000L);
long delayMs = fireAt - now;
if (delayMs <= 0) continue;
ScheduledFuture<?> f = scheduler.schedule(
() -> fireReminder(callRaw, preferredCategory, offMin),
delayMs,
TimeUnit.MILLISECONDS
);
futures.add(f);
}
scheduledByCallRaw.put(callRaw, futures);
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", true,
"Armed for " + callRaw + " (" + offsets + " min before)", false));
}
public void cancelReminders(String callSignRaw) {
String callRaw = normalize(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
List<ScheduledFuture<?>> futures = scheduledByCallRaw.remove(callRaw);
if (futures != null) {
for (ScheduledFuture<?> f : futures) {
if (f != null) f.cancel(false);
}
}
}
private void fireReminder(String callRaw, ChatCategory preferredCategory, int minutesBefore) {
try {
controller.queuePrivateCqMessage(callRaw, preferredCategory, "[KST4C Autoreminder] sked in " + minutesBefore + " min");
controller.fireUiReminderEvent(callRaw, minutesBefore); //triggers some blingbling in the UI
///Local acoustic hint (reuse existing project audio utilities, no AWT, no extra JavaFX modules)
try {
if (controller.getChatPreferences().isNotify_playSimpleSounds()) {
controller.getPlayAudioUtils().playNoiseLauncher('!'); // choose a suitable char you already use
}
// Optional: voice/cw hint (short, not too intrusive)
// controller.getPlayAudioUtils().playCWLauncher(" SKED " + minutesBefore);
} catch (Exception ignore) {
// never block reminder sending because of audio issues
}
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", true,
"PM reminder sent to " + callRaw + " (" + minutesBefore + " min)", false));
} catch (Exception e) {
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", false,
"ERROR sending reminder to " + callRaw + ": " + e.getMessage(), true));
e.printStackTrace();
}
}
private static String normalize(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
}

View File

@@ -1,268 +0,0 @@
package kst4contest.controller;
import kst4contest.logic.SignalDetector;
import kst4contest.model.ChatPreferences;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Thread-safe metrics store keyed by normalized callsignRaw (e.g. "SM6VTZ").
*
* Purpose:
* - Provide inputs for scoring (momentum, reply time, no-reply strikes, manual sked-fail, positive signals).
* - Decouple MessageBus / TX from ScoreService (only data flows, no UI calls here).
*/
public final class StationMetricsService {
/** /cq <CALL> ... */
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/]+)\\b.*");
/** Rolling window timestamps for momentum scoring. */
private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32;
private final ConcurrentHashMap<String, StationMetrics> byCallRaw = new ConcurrentHashMap<>();
/**
* Called when the operator sends a message.
* If it is a "/cq CALL ..." message, this arms a pending ping for response-time / no-reply tracking.
*/
public Optional<String> tryRecordOutboundCq(String messageText, long nowEpochMs) {
if (messageText == null) return Optional.empty();
Matcher m = OUTBOUND_CQ_PATTERN.matcher(messageText.trim());
if (!m.matches()) return Optional.empty();
String callRaw = normalizeCallRaw(m.group(1));
if (callRaw == null || callRaw.isBlank()) return Optional.empty();
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.pendingCqSentAtEpochMs = nowEpochMs;
metrics.lastOutboundCqEpochMs = nowEpochMs;
}
return Optional.of(callRaw);
}
/**
* Called for EVERY inbound line from a station (CH or PM).
* "Any line counts as activity"
*/
public void onInboundMessage(String senderCallSignRaw,
long nowEpochMs,
String messageText,
ChatPreferences prefs,
String ownCallSignRaw) {
String callRaw = normalizeCallRaw(senderCallSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
// ignore own echoed messages
if (ownCallSignRaw != null && callRaw.equalsIgnoreCase(normalizeCallRaw(ownCallSignRaw))) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.lastInboundEpochMs = nowEpochMs;
// rolling timestamps (momentum)
metrics.recentInboundEpochMs.addLast(nowEpochMs);
while (metrics.recentInboundEpochMs.size() > MAX_STORED_INBOUND_TIMESTAMPS) {
metrics.recentInboundEpochMs.removeFirst();
}
// positive signal detection (extendable by prefs)
if (messageText != null && prefs != null) {
if (SignalDetector.containsPositiveSignal(messageText, prefs.getNotify_positiveSignalsPatterns())) {
metrics.lastPositiveSignalEpochMs = nowEpochMs;
}
}
// response time measurement: any inbound line ends a pending ping
if (metrics.pendingCqSentAtEpochMs > 0) {
long rttMs = Math.max(0, nowEpochMs - metrics.pendingCqSentAtEpochMs);
metrics.pendingCqSentAtEpochMs = 0;
// EWMA for response time (stable, no spikes)
final double alpha = 0.25;
if (metrics.avgResponseTimeMs <= 0) {
metrics.avgResponseTimeMs = rttMs;
} else {
metrics.avgResponseTimeMs = alpha * rttMs + (1.0 - alpha) * metrics.avgResponseTimeMs;
}
}
}
}
/**
* Called periodically (e.g. from ScoreService.tick()).
* Applies a "no reply" strike if the pending ping is older than prefs timeout.
*/
public void evaluateNoReplyTimeouts(long nowEpochMs, ChatPreferences prefs) {
if (prefs == null) return;
long timeoutMs = Math.max(1, prefs.getNotify_noReplyPenaltyMinutes()) * 60_000L;
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
StationMetrics metrics = e.getValue();
if (metrics == null) continue;
synchronized (metrics) {
if (metrics.pendingCqSentAtEpochMs <= 0) continue;
long age = nowEpochMs - metrics.pendingCqSentAtEpochMs;
if (age >= timeoutMs) {
metrics.pendingCqSentAtEpochMs = 0;
metrics.noReplyStrikes++;
metrics.lastNoReplyStrikeEpochMs = nowEpochMs;
}
}
}
}
/** Manual sked fail: permanent until reset. */
public void markManualSkedFail(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.manualSkedFailed = true;
metrics.manualSkedFailCount++;
}
}
public void resetManualSkedFail(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.manualSkedFailed = false;
metrics.manualSkedFailCount = 0;
}
}
public boolean isManualSkedFailed(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return false;
StationMetrics metrics = byCallRaw.get(callRaw);
if (metrics == null) return false;
synchronized (metrics) {
return metrics.manualSkedFailed;
}
}
/** Immutable snapshot for scoring */
public Snapshot snapshot(long nowEpochMs, ChatPreferences prefs) {
long momentumWindowMs = (prefs != null ? prefs.getNotify_momentumWindowSeconds() : 180) * 1000L;
Snapshot snap = new Snapshot(nowEpochMs, momentumWindowMs);
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
String callRaw = e.getKey();
StationMetrics m = e.getValue();
if (m == null) continue;
synchronized (m) {
snap.byCallRaw.put(callRaw, new Snapshot.Metrics(
m.lastInboundEpochMs,
countRecent(m.recentInboundEpochMs, nowEpochMs, momentumWindowMs),
m.avgResponseTimeMs,
m.noReplyStrikes,
m.manualSkedFailed,
m.manualSkedFailCount,
m.lastPositiveSignalEpochMs
));
}
}
return snap;
}
private static int countRecent(Deque<Long> timestamps, long nowEpochMs, long windowMs) {
if (timestamps == null || timestamps.isEmpty()) return 0;
int cnt = 0;
for (Long t : timestamps) {
if (t == null) continue;
if (nowEpochMs - t <= windowMs) cnt++;
}
return cnt;
}
private static String normalizeCallRaw(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
private static final class StationMetrics {
long lastInboundEpochMs;
long lastOutboundCqEpochMs;
long pendingCqSentAtEpochMs; // 0 = none
int noReplyStrikes;
long lastNoReplyStrikeEpochMs;
double avgResponseTimeMs; // EWMA
final Deque<Long> recentInboundEpochMs = new ArrayDeque<>();
long lastPositiveSignalEpochMs;
boolean manualSkedFailed;
int manualSkedFailCount;
}
public static final class Snapshot {
private final long snapshotEpochMs;
private final long momentumWindowMs;
private final ConcurrentHashMap<String, Metrics> byCallRaw = new ConcurrentHashMap<>();
private Snapshot(long snapshotEpochMs, long momentumWindowMs) {
this.snapshotEpochMs = snapshotEpochMs;
this.momentumWindowMs = momentumWindowMs;
}
public Metrics get(String callSignRaw) {
if (callSignRaw == null) return null;
return byCallRaw.get(normalizeCallRaw(callSignRaw));
}
public long getSnapshotEpochMs() {
return snapshotEpochMs;
}
public long getMomentumWindowMs() {
return momentumWindowMs;
}
public static final class Metrics {
public final long lastInboundEpochMs;
public final int inboundCountInWindow;
public final double avgResponseTimeMs;
public final int noReplyStrikes;
public final boolean manualSkedFailed;
public final int manualSkedFailCount;
public final long lastPositiveSignalEpochMs;
public Metrics(long lastInboundEpochMs,
int inboundCountInWindow,
double avgResponseTimeMs,
int noReplyStrikes,
boolean manualSkedFailed,
int manualSkedFailCount,
long lastPositiveSignalEpochMs) {
this.lastInboundEpochMs = lastInboundEpochMs;
this.inboundCountInWindow = inboundCountInWindow;
this.avgResponseTimeMs = avgResponseTimeMs;
this.noReplyStrikes = noReplyStrikes;
this.manualSkedFailed = manualSkedFailed;
this.manualSkedFailCount = manualSkedFailCount;
this.lastPositiveSignalEpochMs = lastPositiveSignalEpochMs;
}
}
}
}

View File

@@ -1,20 +0,0 @@
package kst4contest.controller;
import kst4contest.model.ThreadStateMessage;
public interface StatusUpdateListener {
/**
* Thread (key) will send update status (value) to the view via this interface.
*
*/
void onThreadStatusChanged(String key, ThreadStateMessage threadStateMessage);
/**
* Called on change if the userlist to update the UI (sort the chatmembers list)
*/
void onUserListUpdated(String reason);
// new: userlist-update
}

View File

@@ -1,8 +0,0 @@
package kst4contest.controller;
import kst4contest.model.ThreadStateMessage;
public interface ThreadStatusCallback {
void onThreadStatus(String threadName, ThreadStateMessage threadStateMessage);
}

View File

@@ -15,10 +15,7 @@ import kst4contest.model.ChatMember;
public class UCXLogFileToHashsetParser {
public BufferedReader fileReader;
// private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"; //OLD, S51AR for example will not work
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)|([A-Z]\\d{2}[A-Z]{1,3})";
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)";
public UCXLogFileToHashsetParser(String filePathAndName) {

View File

@@ -51,10 +51,21 @@ public class UserActualizationTask extends TimerTask {
UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser(
this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly());
// UCXLogFileToHashsetParser getWorkedCallsignsOfUDPBackupFile = new UCXLogFileToHashsetParser(
// this.client.getChatPreferences().getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
try {
fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse();
// fetchedWorkedSetUdpBckup = getWorkedCallsignsOfUDPBackupFile.parse();
// for (HashMap.Entry entry : fetchedWorkedSet.entrySet()) {
// String key = (String) entry.getKey();
// Object value = entry.getValue();
// System.out.println("key " + key);
// }
System.out.println("USERACT: fetchedWorkedSet size: " + fetchedWorkedSet.size());
// System.out.println("USERACT: fetchedWorkedSetudpbckup size: " + fetchedWorkedSetUdpBckup.size());
} catch (IOException e) {
// TODO Auto-generated catch block

View File

@@ -52,8 +52,7 @@ public class Utils4KST {
// Instant instant = Instant.ofEpochSecond(epoch);
Date date = new Date(epoch * 1000L);
// DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss"); //old value which is too long
DateFormat format = new SimpleDateFormat("H:mm:ss");
DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss");
format.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
String formatted = format.format(date);

View File

@@ -1,80 +0,0 @@
package kst4contest.controller;
import java.nio.charset.StandardCharsets;
/**
* Represents a Win-Test network protocol message.
* <p>
* Ported from the C# wtMessage class in wtKST.
* <p>
* Win-Test uses a simple ASCII-based UDP protocol with a checksum byte.
* Message format (for sending):
* <pre>
* MESSAGETYPE: "src" "dst" data{checksum}\0
* </pre>
* The checksum is calculated over all bytes before the checksum position,
* then OR'd with 0x80.
*/
public class WinTestMessage {
/** Win-Test message types relevant for SKED management. */
public enum MessageType {
LOCKSKED,
UNLOCKSKED,
ADDSKED,
DELETESKED,
UPDATESKED
}
private final MessageType type;
private final String src;
private final String dst;
private final String data;
public WinTestMessage(MessageType type, String src, String dst, String data) {
this.type = type;
this.src = src;
this.dst = dst;
this.data = data;
}
/**
* Serializes this message to bytes for UDP transmission.
* <p>
* Format: {@code MESSAGETYPE: "src" "dst" data{checksum}\0}
* <p>
* The '?' placeholder is replaced by the calculated checksum,
* followed by a NUL terminator.
* Degree signs (°) are escaped as \260 per Win-Test convention.
*/
public byte[] toBytes() {
String escapedData = data.replace("°", "\\260");
// Format: MESSAGETYPE: "src" "dst" data?\0
// The '?' is a placeholder for the checksum byte
String raw = type.name() + ": \"" + src + "\" \"" + dst + "\" " + escapedData + "?\0";
byte[] bytes = raw.getBytes(StandardCharsets.US_ASCII);
// Calculate checksum over everything before the checksum position (length - 2)
int sum = 0;
for (int i = 0; i < bytes.length - 2; i++) {
sum += (bytes[i] & 0xFF);
}
byte checksum = (byte) ((sum | 0x80) & 0xFF);
bytes[bytes.length - 2] = checksum;
return bytes;
}
// Getters for debugging/logging
public MessageType getType() { return type; }
public String getSrc() { return src; }
public String getDst() { return dst; }
public String getData() { return data; }
@Override
public String toString() {
return type.name() + ": src=" + src + " dst=" + dst + " data=" + data;
}
}

View File

@@ -1,187 +0,0 @@
package kst4contest.controller;
import kst4contest.model.Band;
import kst4contest.model.ContestSked;
import kst4contest.model.ThreadStateMessage;
import java.net.*;
import java.nio.charset.StandardCharsets;
/**
* Sends SKED entries to Win-Test via UDP broadcast.
* <p>
* Ported from the C# wtSked class in wtKST.
* <p>
* Win-Test expects a LOCKSKED / ADDSKED / UNLOCKSKED sequence
* to safely insert a new sked into its schedule window.
*/
public class WinTestSkedSender {
private final String stationName;
private final InetAddress broadcastAddress;
private final int port;
private final ThreadStatusCallback callback;
private static final String THREAD_NICKNAME = "WT-SkedSend";
/**
* @param stationName our station name in the Win-Test network (e.g. "KST4Contest")
* @param broadcastAddress UDP broadcast address (e.g. 255.255.255.255 or subnet broadcast)
* @param port Win-Test network port (default 9871)
* @param callback optional callback for status reporting (may be null)
*/
public WinTestSkedSender(String stationName, InetAddress broadcastAddress, int port,
ThreadStatusCallback callback) {
this.stationName = stationName;
this.broadcastAddress = broadcastAddress;
this.port = port;
this.callback = callback;
}
/**
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
* sequence via UDP broadcast.
*
* @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")
*/
public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) {
try {
sendLockSked();
sendAddSked(sked, frequencyKHz, notes, modeOverride);
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();
}
}
/**
* Sends a LOCKSKED message to lock the Win-Test sked window.
*/
private void sendLockSked() throws Exception {
WinTestMessage msg = new WinTestMessage(
WinTestMessage.MessageType.LOCKSKED,
stationName, "",
"\"" + stationName + "\"");
sendUdp(msg);
}
/**
* Sends an UNLOCKSKED message to unlock the Win-Test sked window.
*/
private void sendUnlockSked() throws Exception {
WinTestMessage msg = new WinTestMessage(
WinTestMessage.MessageType.UNLOCKSKED,
stationName, "",
"\"" + stationName + "\"");
sendUdp(msg);
}
/**
* 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.
*/
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;
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
// Win-Test band ID
int bandId = toWinTestBandId(sked.getBand());
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
int mode;
if (modeOverride >= 0) {
mode = modeOverride;
} else {
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
}
String data = wtTimestamp
+ " " + freqTenthKHz
+ " " + bandId
+ " " + mode
+ " \"" + sked.getTargetCallsign() + "\""
+ " \"" + (notes != null ? notes : "") + "\"";
WinTestMessage msg = new WinTestMessage(
WinTestMessage.MessageType.ADDSKED,
stationName, "",
data);
sendUdp(msg);
}
/**
* Sends a WinTestMessage via UDP broadcast.
*/
private void sendUdp(WinTestMessage msg) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
socket.setReuseAddress(true);
byte[] bytes = msg.toBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcastAddress, port);
socket.send(packet);
System.out.println("[WinTestSkedSender] sent: " + msg);
}
}
/**
* Maps the kst4contest Band enum to Win-Test band IDs.
* <p>
* Win-Test band IDs (reverse-engineered from wtKST):
* 10=50MHz, 11=70MHz, 12=144MHz, 14=432MHz, 16=1.2GHz,
* 17=2.3GHz, 18=3.4GHz, 19=5.7GHz, 20=10GHz, 21=24GHz,
* 22=47GHz, 23=76GHz
*/
public static int toWinTestBandId(Band band) {
if (band == null) return 12; // default to 144 MHz
return switch (band) {
case B_144 -> 12;
case B_432 -> 14;
case B_1296 -> 16;
case B_2320 -> 17;
case B_3400 -> 18;
case B_5760 -> 19;
case B_10G -> 20;
case B_24G -> 21;
};
}
/**
* 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,
new ThreadStateMessage(THREAD_NICKNAME, !isError, text, isError));
}
}
}

View File

@@ -5,7 +5,6 @@ import java.net.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMessage;
/**
@@ -170,8 +169,8 @@ public class WriteThread extends Thread {
try {
messageToBeSend = client.getMessageTXBus().take();
if (messageToBeSend.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)
&& messageToBeSend.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
if (messageToBeSend.getMessageText().equals("POISONPILL_KILLTHREAD")
&& messageToBeSend.getMessageSenderName().equals("POISONPILL_KILLTHREAD")) {
client.getMessageRXBus().clear();
this.interrupt();
break;

View File

@@ -1,13 +0,0 @@
package kst4contest.controller.interfaces;
public interface PstRotatorEventListener {
void onAzimuthUpdate(double azimuth);
void onElevationUpdate(double elevation);
void onModeUpdate(boolean isTracking); // true = Tracking, false = Manual
void onMessageReceived(String rawMessage); // Debugging usage
// void setRotorPosition(double azimuth);
}

View File

@@ -1,292 +0,0 @@
package kst4contest.logic;
import kst4contest.controller.StationMetricsService;
import kst4contest.model.*;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Priority score calculation (off FX-thread).
*
* Notes:
* - Score is computed once per callsignRaw by ScoreService.
* - This calculator MUST be pure (no UI calls) and fast.
*/
public class PriorityCalculator {
/** Max age for "known active bands" (derived from chat history). */
private static final long RX_BANDS_MAX_AGE_MS = 30L * 60L * 1000L; // 30 minutes
public double calculatePriority(ChatMember member,
ChatPreferences prefs,
List<ContestSked> activeSkeds,
StationMetricsService.Snapshot metricsSnapshot,
long nowEpochMs) {
if (member == null || prefs == null) return 0.0;
final String callRaw = normalize(member.getCallSignRaw());
if (callRaw == null || callRaw.isBlank()) return 0.0;
// --------------------------------------------------------------------
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
// --------------------------------------------------------------------
// --------------------------------------------------------------------
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
// --------------------------------------------------------------------
EnumSet<Band> myEnabledBands = getMyEnabledBands(prefs);
// "worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
// IMPORTANT: ChatMember.worked is UI-only and NOT used in scoring.
EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
// Remaining bands that are:
// - recently offered by the station (from knownActiveBands history)
// - enabled at our station
// - NOT worked yet (per-band flags)
// If we do not know offered bands (history empty), this remains empty.
EnumSet<Band> unworkedPossible = EnumSet.noneOf(Band.class);
EnumSet<Band> stationOfferedBands = getStationOfferedBandsFromHistory(member, nowEpochMs);
EnumSet<Band> possibleBands = stationOfferedBands.isEmpty()
? EnumSet.noneOf(Band.class) // unknown => don't hard-filter
: EnumSet.copyOf(stationOfferedBands);
if (!possibleBands.isEmpty()) {
possibleBands.retainAll(myEnabledBands);
if (possibleBands.isEmpty()) {
// We know their bands, but none of them are enabled at our station.
return 0.0;
}
unworkedPossible = EnumSet.copyOf(possibleBands);
unworkedPossible.removeAll(workedBandsForScoring);
// If already worked on all possible bands => no priority on them anymore (contest logic).
if (unworkedPossible.isEmpty()) {
return 0.0;
}
}
// --------------------------------------------------------------------
// 2) BASE SCORE
// --------------------------------------------------------------------
double score = 100.0;
// if (!member.isWorked()) {
// score += 200.0;
// }
//"worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
// EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
if (workedBandsForScoring.isEmpty()) {
score += 200.0; // never worked on any supported band -> higher priority
} else {
score -= 150.0; // already worked on at least one band -> lower base priority
}
// Multi-band bonus: if they offer >1 possible band and we worked at least one, prefer them
if (!possibleBands.isEmpty()) {
int bandCount = possibleBands.size();
score += (bandCount - 1) * 80.0;
}
// Optional: band-upgrade visibility boost
// If the station is already worked on at least one band, but is still QRV on other unworked enabled band(s),
// we can optionally add a boost so it remains visible in the list.
if (prefs.isNotify_bandUpgradePriorityBoostEnabled()
&& !workedBandsForScoring.isEmpty()
&& !unworkedPossible.isEmpty()) {
score += 180.0; // tuned visibility boost
}
// --------------------------------------------------------------------
// 3) DISTANCE ("Goldilocks Zone")
// --------------------------------------------------------------------
double distKm = member.getQrb() == null ? 0.0 : member.getQrb();
if (distKm > 0) {
if (distKm < 200) {
score *= 0.7;
} else if (distKm > prefs.getStn_maxQRBDefault()) {
score *= 0.3;
} else {
score *= 1.15;
}
}
// --------------------------------------------------------------------
// 4) AIRSCOUT BOOST
// --------------------------------------------------------------------
AirPlaneReflectionInfo apInfo = member.getAirPlaneReflectInfo();
if (apInfo != null && apInfo.getAirPlanesReachableCntr() > 0) {
score += 200;
int nextMinutes = findNextAirplaneArrivingMinutes(apInfo);
if (nextMinutes == 0) score += 120;
else if (nextMinutes == 1) score += 60;
else if (nextMinutes == 2) score += 30;
}
// --------------------------------------------------------------------
// 5) BOOST IDEA #1: Beam direction match (within beamwidth)
// --------------------------------------------------------------------
if (member.getQTFdirection() != null) {
double myAz = prefs.getActualQTF().getValue();
double targetAz = member.getQTFdirection();
double diff = minimalAngleDiffDeg(myAz, targetAz);
double halfBeam = Math.max(1.0, prefs.getStn_antennaBeamWidthDeg()) / 2.0;
if (diff <= halfBeam) {
double centerFactor = 1.0 - (diff / halfBeam); // 1.0 center -> 0.0 edge
score += 80.0 + (120.0 * centerFactor);
}
}
// --------------------------------------------------------------------
// 6) BOOST IDEA #3: Conversation momentum (recent inbound burst)
// --------------------------------------------------------------------
if (metricsSnapshot != null) {
StationMetricsService.Snapshot.Metrics mx = metricsSnapshot.get(callRaw);
if (mx != null) {
long ageMs = mx.lastInboundEpochMs > 0 ? (nowEpochMs - mx.lastInboundEpochMs) : Long.MAX_VALUE;
// "Active now" bonus
if (ageMs < 60_000) score += 120;
else if (ageMs < 3 * 60_000) score += 60;
// Momentum bonus: multiple lines in the configured window
int cnt = mx.inboundCountInWindow;
if (cnt >= 6) score += 160;
else if (cnt >= 4) score += 110;
else if (cnt >= 2) score += 60;
// Positive signal (configurable)
if (mx.lastPositiveSignalEpochMs > 0 && (nowEpochMs - mx.lastPositiveSignalEpochMs) < 5 * 60_000) {
score += 120;
}
// Reply time: prefer fast responders
if (mx.avgResponseTimeMs > 0) {
if (mx.avgResponseTimeMs < 60_000) score += 80;
else if (mx.avgResponseTimeMs < 3 * 60_000) score += 40;
}
// No-reply penalty (automatic failed attempt)
if (mx.noReplyStrikes > 0) {
score /= (1.0 + (mx.noReplyStrikes * 0.6));
}
// Manual sked fail (path likely bad) => strong, permanent penalty until reset
if (mx.manualSkedFailed) {
score *= 0.15;
}
}
}
// --------------------------------------------------------------------
// 7) BOOST IDEA #4: Sked commitment ramp-up
// --------------------------------------------------------------------
if (activeSkeds != null && !activeSkeds.isEmpty()) {
for (ContestSked sked : activeSkeds) {
if (sked == null) continue;
if (!callRaw.equals(normalize(sked.getTargetCallsign()))) continue;
long seconds = sked.getTimeUntilSkedSeconds();
// Imminent sked: absolute priority (T-3min..T+1min)
if (seconds < 180 && seconds > -60) {
score += 5000;
continue;
}
// Ramp: 0..15 minutes before => up to +1200
if (seconds >= 0 && seconds <= 15 * 60) {
double t = (15 * 60 - seconds) / (15.0 * 60.0); // 0.0..1.0
score += 300 + (900 * t);
} else if (seconds > 15 * 60) {
score += 40;
}
}
}
// --------------------------------------------------------------------
// 8) Legacy penalty: failed attempts in ChatMember
// --------------------------------------------------------------------
if (member.getFailedQSOAttempts() > 0) {
score = score / (member.getFailedQSOAttempts() + 1);
}
return Math.max(0.0, score);
}
private static EnumSet<Band> getMyEnabledBands(ChatPreferences prefs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (prefs.isStn_bandActive144()) out.add(Band.B_144);
if (prefs.isStn_bandActive432()) out.add(Band.B_432);
if (prefs.isStn_bandActive1240()) out.add(Band.B_1296);
if (prefs.isStn_bandActive2300()) out.add(Band.B_2320);
if (prefs.isStn_bandActive3400()) out.add(Band.B_3400);
if (prefs.isStn_bandActive5600()) out.add(Band.B_5760);
if (prefs.isStn_bandActive10G()) out.add(Band.B_10G);
return out;
}
private static EnumSet<Band> getStationOfferedBandsFromHistory(ChatMember member, long nowEpochMs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
Map<Band, ChatMember.ActiveFrequencyInfo> map = member.getKnownActiveBands();
if (map == null || map.isEmpty()) return out;
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> e : map.entrySet()) {
if (e == null || e.getKey() == null || e.getValue() == null) continue;
long age = nowEpochMs - e.getValue().timestampEpoch;
if (age <= RX_BANDS_MAX_AGE_MS) {
out.add(e.getKey());
}
}
return out;
}
private static EnumSet<Band> getWorkedBands(ChatMember member) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (member.isWorked144()) out.add(Band.B_144);
if (member.isWorked432()) out.add(Band.B_432);
if (member.isWorked1240()) out.add(Band.B_1296);
if (member.isWorked2300()) out.add(Band.B_2320);
if (member.isWorked3400()) out.add(Band.B_3400);
if (member.isWorked5600()) out.add(Band.B_5760);
if (member.isWorked10G()) out.add(Band.B_10G);
if (member.isWorked24G()) out.add(Band.B_24G);
return out;
}
private static int findNextAirplaneArrivingMinutes(AirPlaneReflectionInfo apInfo) {
try {
if (apInfo.getRisingAirplanes() == null || apInfo.getRisingAirplanes().isEmpty()) return -1;
int min = Integer.MAX_VALUE;
for (AirPlane ap : apInfo.getRisingAirplanes()) {
if (ap == null) continue;
min = Math.min(min, ap.getArrivingDurationMinutes());
}
return min == Integer.MAX_VALUE ? -1 : min;
} catch (Exception ignore) {
return -1;
}
}
private static double minimalAngleDiffDeg(double a, double b) {
double diff = Math.abs((a - b) % 360.0);
return diff > 180.0 ? 360.0 - diff : diff;
}
private static String normalize(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
}

View File

@@ -1,51 +0,0 @@
package kst4contest.logic;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
/**
* Lightweight positive-signal detector.
*
* Patterns are configured via a single preference string, delimited by ';' or newlines.
* Examples: "QRV;READY;RX OK;RGR;TNX;TU;HRD"
*/
public final class SignalDetector {
private static final AtomicReference<String> lastPatterns = new AtomicReference<>("");
private static final AtomicReference<List<Pattern>> cached = new AtomicReference<>(List.of());
private SignalDetector() {}
public static boolean containsPositiveSignal(String messageText, String patternsDelimited) {
if (messageText == null || messageText.isBlank()) return false;
List<Pattern> patterns = compileIfChanged(patternsDelimited);
String txt = messageText.toUpperCase();
for (Pattern p : patterns) {
if (p.matcher(txt).find()) return true;
}
return false;
}
private static List<Pattern> compileIfChanged(String patternsDelimited) {
String p = patternsDelimited == null ? "" : patternsDelimited.trim();
String prev = lastPatterns.get();
if (p.equals(prev)) return cached.get();
List<Pattern> out = new ArrayList<>();
for (String token : p.split("[;\\n\\r]+")) {
String t = token.trim();
if (t.isEmpty()) continue;
// plain substring match, but regex-safe
String regex = Pattern.quote(t.toUpperCase());
out.add(Pattern.compile(regex));
}
lastPatterns.set(p);
cached.set(List.copyOf(out));
return out;
}
}

View File

@@ -1,49 +0,0 @@
package kst4contest.model;
/**
* Represents Amateur Radio Bands and their physical limits.
* Used for plausibility checks in the Smart Parser.
*/
public enum Band {
B_144(144.000, 146.000, "144"),
B_432(432.000, 434.000, "432"),
B_1296(1296.000, 1298.000, "1296"),
B_2320(2320.000, 2322.000, "2320"),
B_3400(3400.000, 3410.000, "3400"),
B_5760(5760.000, 5762.000, "5760"),
B_10G(10368.000, 10370.000, "10368"),
B_24G(24048.000, 24050.000, "24048");
// more space for future usage
private final double minFreq;
private final double maxFreq;
private final String prefix; // Default prefix for "short value" parsing (e.g., .210)
Band(double min, double max, String prefix) {
this.minFreq = min;
this.maxFreq = max;
this.prefix = prefix;
}
public String getPrefix() {
return prefix;
}
/**
* Checks if a specific frequency falls within this band's limits.
*/
public boolean isPlausible(double freq) {
return freq >= minFreq && freq <= maxFreq;
}
/**
* Helper to find the matching Band enum for a given frequency.
* Returns null if no band matches.
*/
public static Band fromFrequency(double freq) {
for (Band b : values()) {
if (b.isPlausible(freq)) return b;
}
return null;
}
}

View File

@@ -1,10 +1,6 @@
package kst4contest.model;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.BooleanProperty;
@@ -13,10 +9,7 @@ import javafx.beans.property.StringProperty;
public class ChatMember {
long lastFlagsChangeEpochMs; // timestamp of the last worked/not-QRV flag change in the internal DB
// private final BooleanProperty workedInfoChangeFireListEventTrigger = new SimpleBooleanProperty();
// private final BooleanProperty workedInfoChangeFireListEventTrigger = new SimpleBooleanProperty();
AirPlaneReflectionInfo airPlaneReflectInfo;
String callSign;
String qra;
@@ -52,12 +45,6 @@ public class ChatMember {
boolean worked3400;
boolean worked5600;
boolean worked10G;
boolean Worked50;
boolean Worked70;
boolean Worked24G;
boolean Worked47G;
boolean Worked76G;
/**
* Chatmember is qrv at all band except we initialize anything other, depending to user entry
@@ -71,35 +58,9 @@ public class ChatMember {
boolean qrv10G = true;
boolean qrvAny = true;
// Stores the last known frequency per band (Context History)
private final Map<Band, ActiveFrequencyInfo> knownActiveBands = new ConcurrentHashMap<>();
// --- INNER CLASS FOR QRG HISTORY ---
public class ActiveFrequencyInfo {
public double frequency;
public long timestampEpoch;
public ActiveFrequencyInfo(double freq) {
this.frequency = freq;
this.timestampEpoch = System.currentTimeMillis();
}
}
// Counter for failed calls (Penalty Logic)
private int failedQSOAttempts = 0;
// Calculated Score for sorting the user list
private double currentPriorityScore = 0.0;
public long getLastFlagsChangeEpochMs() {
return lastFlagsChangeEpochMs;
}
public void setLastFlagsChangeEpochMs(long lastFlagsChangeEpochMs) {
this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs;
}
public boolean isInAngleAndRange() {
return isInAngleAndRange;
@@ -309,129 +270,8 @@ public class ChatMember {
return callSign;
}
/**
* Sets the original callsign and derives the normalized base callsign which is
* used as the database key. Prefixes like EA5/ and suffixes like /P or -70 are
* ignored for the raw-key handling.
*
* @param callSign callsign as received from chat or database
*/
public void setCallSign(String callSign) {
if (callSign == null) {
this.callSign = null;
this.callSignRaw = null;
return;
}
this.callSign = callSign.trim().toUpperCase(Locale.ROOT);
this.callSignRaw = normalizeCallSignToBaseCallSign(this.callSign);
}
/**
* Normalizes a callsign to the base callsign which is used as the unique key in
* the internal database. The method removes KST suffixes like "-2", portable
* suffixes like "/P" and prefix additions like "EA5/".
*
* @param callSign callsign to normalize
* @return normalized base callsign in upper case
*/
public static String normalizeCallSignToBaseCallSign(String callSign) {
if (callSign == null) {
return null;
}
String normalizedCallSign = callSign.trim().toUpperCase(Locale.ROOT);
if (normalizedCallSign.isBlank()) {
return normalizedCallSign;
}
String callSignWithoutDashSuffix = normalizedCallSign.split("-", 2)[0].trim();
if (!callSignWithoutDashSuffix.contains("/")) {
return callSignWithoutDashSuffix;
}
String[] callSignParts = callSignWithoutDashSuffix.split("/");
String bestMatchingCallsignPart = helper_selectBestCallsignPart(callSignParts);
if (bestMatchingCallsignPart == null || bestMatchingCallsignPart.isBlank()) {
return callSignWithoutDashSuffix;
}
return bestMatchingCallsignPart;
}
/**
* Selects the most plausible base callsign segment from a slash-separated
* callsign. In strings like "EA5/G8MBI/P" the segment "G8MBI" is preferred over
* prefix or portable markers.
*
* @param callSignParts slash-separated callsign parts
* @return best matching base callsign segment
*/
private static String helper_selectBestCallsignPart(String[] callSignParts) {
String bestLikelyBaseCallsignPart = null;
int bestLikelyBaseCallsignLength = -1;
String bestFallbackCallsignPart = null;
int bestFallbackCallsignLength = -1;
for (String rawCallsignPart : callSignParts) {
String currentCallsignPart = rawCallsignPart == null ? "" : rawCallsignPart.trim().toUpperCase(Locale.ROOT);
if (currentCallsignPart.isBlank()) {
continue;
}
if (currentCallsignPart.length() > bestFallbackCallsignLength) {
bestFallbackCallsignPart = currentCallsignPart;
bestFallbackCallsignLength = currentCallsignPart.length();
}
if (helper_isLikelyBaseCallsignSegment(currentCallsignPart)
&& currentCallsignPart.length() > bestLikelyBaseCallsignLength) {
bestLikelyBaseCallsignPart = currentCallsignPart;
bestLikelyBaseCallsignLength = currentCallsignPart.length();
}
}
if (bestLikelyBaseCallsignPart != null) {
return bestLikelyBaseCallsignPart;
}
return bestFallbackCallsignPart;
}
/**
* Checks whether a slash-separated segment looks like a real base callsign. A
* normal amateur-radio callsign typically contains letters and digits and is
* longer than one-character postfix markers.
*
* @param callsignSegment segment to inspect
* @return true if the segment looks like a base callsign
*/
private static boolean helper_isLikelyBaseCallsignSegment(String callsignSegment) {
boolean containsLetter = false;
boolean containsDigit = false;
for (int currentIndex = 0; currentIndex < callsignSegment.length(); currentIndex++) {
char currentCharacter = callsignSegment.charAt(currentIndex);
if (Character.isLetter(currentCharacter)) {
containsLetter = true;
}
if (Character.isDigit(currentCharacter)) {
containsDigit = true;
}
}
return containsLetter && containsDigit && callsignSegment.length() >= 3;
this.callSign = callSign;
}
public String getQra() {
@@ -473,51 +313,9 @@ public class ChatMember {
return worked;
}
public boolean isWorked50() {
return Worked50;
}
public void setWorked50(boolean worked50) {
Worked50 = worked50;
}
public boolean isWorked70() {
return Worked70;
}
public void setWorked70(boolean worked70) {
Worked70 = worked70;
}
public boolean isWorked24G() {
return Worked24G;
}
public void setWorked24G(boolean worked24G) {
Worked24G = worked24G;
}
public boolean isWorked47G() {
return Worked47G;
}
public void setWorked47G(boolean worked47G) {
Worked47G = worked47G;
}
public boolean isWorked76G() {
return Worked76G;
}
public void setWorked76G(boolean worked76G) {
Worked76G = worked76G;
}
public void setWorked(boolean worked) {
this.worked = worked;
}
/**
@@ -526,15 +324,13 @@ public class ChatMember {
*/
public String getCallSignRaw() {
String raw = "";
return callSignRaw;
// String raw = "";
//
// try {
// return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m
// } catch (Exception e) {
// return getCallSign();
// }
try {
return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m
} catch (Exception e) {
return getCallSign();
}
}
@@ -546,19 +342,12 @@ public class ChatMember {
this.setWorked(false);
this.setWorked144(false);
this.setWorked50(false);
this.setWorked70(false);
this.setWorked432(false);
this.setWorked1240(false);
this.setWorked2300(false);
this.setWorked3400(false);
this.setWorked5600(false);
this.setWorked10G(false);
this.setWorked24G(false);
this.setWorked47G(false);
this.setWorked76G(false);
}
/**
@@ -602,56 +391,4 @@ public class ChatMember {
return false;
}
/**
* Adds a new recognized frequency by band to the internal band/qrg map
* @param band
* @param freq
*/
public void addKnownFrequency(Band band, double freq) {
this.knownActiveBands.put(band, new ActiveFrequencyInfo(freq));
}
/**
* represents a map of bands which are known of this chatmember
*
* @return Band
*/
public Map<Band, ActiveFrequencyInfo> getKnownActiveBands() {
return knownActiveBands;
}
/**
* If a sked fails and the user tells this to the client, this counter will be increased to give the station a
* lower score
*/
public void incrementFailedAttempts() {
this.failedQSOAttempts++;
}
public void resetFailedAttempts() {
this.failedQSOAttempts = 0;
}
public int getFailedQSOAttempts() {
return failedQSOAttempts;
}
/**
* Sets the working-priority score of a chatmember for the "Todo-List"
* @param score
*/
public void setCurrentPriorityScore(double score) {
this.currentPriorityScore = score;
}
/**
* Gets the working-priority score of a chatmember for the "Todo-List"
*
*/
public double getCurrentPriorityScore() {
return currentPriorityScore;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,52 +0,0 @@
package kst4contest.model;
/**
* Represents a scheduled event or an AirScout opportunity in the future.
* Used for the Timeline View and Priority Calculation.
*/
public class ContestSked {
private String targetCallsign;
private double targetAzimuth; // Required for Antenna-Visuals
private long skedTimeEpoch; // The peak time (e.g., AP)
private Band band;
// Opportunity potential (0..100). -1 means "unknown".
int opportunityPotentialPercent = -1;
// 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;
this.targetAzimuth = azimuth;
this.skedTimeEpoch = time;
this.band = b;
}
/**
* Returns the seconds remaining until the event.
* Negative values mean the event is in the past.
*/
public long getTimeUntilSkedSeconds() {
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; }
public int getOpportunityPotentialPercent() {
return opportunityPotentialPercent;
}
public void setOpportunityPotentialPercent(int opportunityPotentialPercent) {
this.opportunityPotentialPercent = opportunityPotentialPercent;
}
}

View File

@@ -1,103 +0,0 @@
package kst4contest.model;
/**
* Object for the description of the activity of a Thread to show these information in a View.
* <br/><br/>
* If state is critical, there could be used a further information field for the stacktrace
*/
public class ThreadStateMessage {
String threadNickName;
String threadDescription;
boolean running;
String runningInformationTextDescription;
String runningInformation;
boolean criticalState;
String criticalStateFurtherInfo;
public ThreadStateMessage(String threadNickName, boolean running, String runningInformation, boolean criticalState) {
this.threadNickName = threadNickName;
this.running = running;
this.criticalState = criticalState;
this.runningInformation = runningInformation;
}
/**
* This triggers the message for "Sked armed"
*
* @return
*/
public String getRunningInformationTextDescription() {
// If a custom description was set (e.g. for UI indicator buttons), prefer it.
if (runningInformationTextDescription != null && !runningInformationTextDescription.isBlank()) {
return runningInformationTextDescription;
}
// Fallback (legacy behavior)
if (isRunning()) {
return "on";
} else if (!isRunning() && isCriticalState()) {
return "FAILED";
} else {
return "off";
}
}
public void setRunningInformationTextDescription(String runningInformationTextDescription) {
this.runningInformationTextDescription = runningInformationTextDescription;
}
public String getThreadNickName() {
return threadNickName;
}
public void setThreadNickName(String threadNickName) {
this.threadNickName = threadNickName;
}
public String getThreadDescription() {
return threadDescription;
}
public void setThreadDescription(String threadDescription) {
this.threadDescription = threadDescription;
}
public boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
public String getRunningInformation() {
return runningInformation;
}
public void setRunningInformation(String runningInformation) {
this.runningInformation = runningInformation;
}
public boolean isCriticalState() {
return criticalState;
}
public void setCriticalState(boolean criticalState) {
this.criticalState = criticalState;
}
public String getCriticalStateFurtherInfo() {
return criticalStateFurtherInfo;
}
public void setCriticalStateFurtherInfo(String criticalStateFurtherInfo) {
this.criticalStateFurtherInfo = criticalStateFurtherInfo;
}
}

View File

@@ -1,256 +0,0 @@
package kst4contest.test;
import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class MockKstServer {
private static final int PORT = 23001;
private static final String CHAT_ID = "2"; // 2 = 144/432 MHz
// Thread-sichere Liste aller verbundenen Clients (OutputStreams)
private final List<PrintWriter> clients = new CopyOnWriteArrayList<>();
// Permanente User (Ihre Test-Callsigns)
private final Map<String, User> onlineUsers = new HashMap<>();
// Historien müssen synchronisiert werden
private final List<String> historyChat = Collections.synchronizedList(new ArrayList<>());
private final List<String> historyDx = Collections.synchronizedList(new ArrayList<>());
private boolean running = false;
private ServerSocket serverSocket;
public MockKstServer() {
// Initiale Permanente User
addUser("DK5EW", "Erwin", "JN47NX");
addUser("DL1TEST", "TestOp", "JO50XX");
addUser("ON4KST", "Alain", "JO20HI");
addUser("PA9R-2", "2", "JO20HI");
addUser("PA9R-70", "70", "JO20HI");
addUser("PA9R", "general", "JO20HI");
}
// Startet den Server im Hintergrund (Non-Blocking)
public void start() {
if (running) return;
running = true;
new Thread(() -> {
try {
serverSocket = new ServerSocket(PORT);
System.out.println("[Server] ON4KST Simulation gestartet auf Port " + PORT);
// Startet den Simulator für Zufallstraffic
new Thread(this::simulationLoop).start();
while (running) {
Socket clientSocket = serverSocket.accept();
System.out.println("[Server] Neuer Client verbunden: " + clientSocket.getInetAddress());
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
if (running) e.printStackTrace();
}
}).start();
}
public void stop() {
running = false;
try {
if (serverSocket != null) serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void addUser(String call, String name, String loc) {
onlineUsers.put(call, new User(call, name, loc));
}
private void removeUser(String call) {
onlineUsers.remove(call);
}
// Sendet Nachricht an ALLE verbundenen Clients (inkl. Sender)
private void broadcast(String message) {
if (!message.endsWith("\r\n")) message += "\r\n";
String finalMsg = message;
for (PrintWriter writer : clients) {
try {
writer.print(finalMsg);
writer.flush(); // WICHTIG: Sofort senden!
} catch (Exception e) {
// Client wohl weg, wird beim nächsten Schreibversuch oder im Handler entfernt
}
}
}
// --- Innere Logik: Client Handler ---
private class ClientHandler implements Runnable {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private String myCall = "MYCLIENT"; // Default, wird bei LOGIN überschrieben
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// ISO-8859-1 ist Standard für KST/Telnet Cluster
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ISO-8859-1"));
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"), true);
clients.add(out);
String line;
boolean loginComplete = false;
while ((line = in.readLine()) != null) {
// System.out.println("[RECV] " + line); // Debugging aktivieren falls nötig
String[] parts = line.split("\\|");
String cmd = parts[0];
if (cmd.equals("LOGIN") || cmd.equals("LOGINC")) {
// Protokoll: LOGIN|callsign|password|... [cite: 21]
if (parts.length > 1) myCall = parts[1];
// 1. Login Bestätigung
// Format: LOGSTAT|100|chat id|client software version|session key|config|dx option|
send("LOGSTAT|100|" + CHAT_ID + "|JavaSim|KEY123|Config|3|");
// Bei LOGIN senden wir die Daten sofort
// Bei LOGINC warten wir eigentlich auf SDONE, senden hier aber vereinfacht direkt
if (cmd.equals("LOGIN")) {
sendInitialData();
loginComplete = true;
}
}
else if (cmd.equals("SDONE")) {
// Abschluss der Settings (bei LOGINC) [cite: 34]
sendInitialData();
loginComplete = true;
}
else if (cmd.equals("MSG")) {
// MSG|chat id|destination|command|0| [cite: 42]
if (parts.length >= 4) {
String text = parts[3];
// Nachricht sofort als CH Frame an alle verteilen (Echo)
handleChatMessage(myCall, "Me", text);
}
}
else if (cmd.equals("CK")) {
// Keepalive [cite: 20]
// Server muss nicht zwingend antworten, aber Connection bleibt offen
}
}
} catch (IOException e) {
// System.out.println("Client getrennt");
} finally {
clients.remove(out);
try { socket.close(); } catch (IOException e) {}
}
}
private void send(String msg) {
if (!msg.endsWith("\r\n")) msg += "\r\n";
out.print(msg);
out.flush();
}
private void sendInitialData() {
// 1. User Liste UA0 [cite: 14]
for (User u : onlineUsers.values()) {
send("UA0|" + CHAT_ID + "|" + u.call + "|" + u.name + "|" + u.loc + "|0|");
}
// 2. Chat History CR [cite: 7]
synchronized(historyChat) {
for (String h : historyChat) send(h);
}
// 3. DX History DL [cite: 10]
synchronized(historyDx) {
for (String d : historyDx) send(d);
}
// 4. Ende User Liste UE [cite: 15]
send("UE|" + CHAT_ID + "|" + onlineUsers.size() + "|");
}
}
// --- Hilfsmethoden für Traffic ---
private void handleChatMessage(String call, String name, String text) {
// CH|chat id|date|callsign|firstname|destination|msg|highlight|
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String frame = String.format("CH|%s|%s|%s|%s|0|%s|0|", CHAT_ID, date, call, name, text);
synchronized(historyChat) {
historyChat.add(frame);
if (historyChat.size() > 50) historyChat.remove(0);
}
broadcast(frame);
}
private void handleDxSpot(String spotter, String dx, String freq) {
// DL|Unix time|dx utc|spotter|qrg|dx|info|spotter locator|dx locator| [cite: 10]
long unixTime = System.currentTimeMillis() / 1000;
String utc = new SimpleDateFormat("HHmm").format(new Date());
// Simple Dummy Locators
String frame = String.format("DL|%d|%s|%s|%s|%s|Simulated|JO00|JO99|",
unixTime, utc, spotter, freq, dx);
synchronized(historyDx) {
historyDx.add(frame);
if (historyDx.size() > 20) historyDx.remove(0);
}
broadcast(frame);
}
private void simulationLoop() {
String[] randomCalls = {"PA0GUS", "F6APE", "OH8K", "OZ2M", "G4CBW"};
String[] msgs = {"CQ 144.300", "Tnx for QSO", "Any sked?", "QRV 432.200"};
Random rand = new Random();
while (running) {
try {
Thread.sleep(3000 + rand.nextInt(5000)); // 3-8 Sek Pause
int action = rand.nextInt(10);
String call = randomCalls[rand.nextInt(randomCalls.length)];
if (action < 4) { // 40% Chat
handleChatMessage(call, "SimOp", msgs[rand.nextInt(msgs.length)]);
} else if (action < 7) { // 30% DX Spot
handleDxSpot(call, randomCalls[rand.nextInt(randomCalls.length)], "144." + rand.nextInt(400));
} else if (action == 8) { // Login Simulation UA5
if (!onlineUsers.containsKey(call)) {
addUser(call, "SimOp", "JO11");
broadcast("UA5|" + CHAT_ID + "|" + call + "|SimOp|JO11|2|");
}
} else if (action == 9) { // Logout Simulation UR6
if (onlineUsers.containsKey(call) && !call.equals("DK5EW")) { // DK5EW nicht kicken
removeUser(call);
broadcast("UR6|" + CHAT_ID + "|" + call + "|");
}
}
// Ping ab und zu
if (rand.nextInt(5) == 0) broadcast("CK|");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Kleine Datenklasse
private static class User {
String call, name, loc;
User(String c, String n, String l) { this.call=c; this.name=n; this.loc=l; }
}
}

View File

@@ -44,14 +44,6 @@ public class GuiUtils {
public static void triggerGUIFilteredChatMemberListChange(ChatController chatController) {
if (javafx.application.Platform.isFxApplicationThread()) {
triggerUpdate(chatController);
} else{
javafx.application.Platform.runLater(() -> triggerUpdate(chatController));
}
}
private static void triggerUpdate(ChatController chatController) {
{
//trick to trigger gui changes on property changes of obects

File diff suppressed because it is too large Load Diff

View File

@@ -1,369 +0,0 @@
package kst4contest.view;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.paint.Color;
import javafx.scene.effect.DropShadow;
import javafx.scene.Node;
import javafx.scene.Group;
import javafx.scene.text.Font;
import kst4contest.model.ContestSked;
import kst4contest.model.ChatCategory;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* A custom UI Component that visualizes future events (Skeds/AP).
* It changes opacity based on the current antenna direction.
*
* Extended:
* - Can also render "priority candidates" (ScoreService top list) with time base = next AirScout airplane minute.
* - Clicking a candidate triggers a callback (selection + /cq preparation happens in Kst4ContestApplication).
*/
public class TimelineView extends Pane {
private double currentAntennaAzimuth = 0;
private double beamWidth = 50.0; // TODO: from Prefs (later)
private final long PREVIEW_TIME_MS = 30 * 60 * 1000; // 30 Minutes Preview
double margin = 30; // enough space for the callsign label
private Function<ContestSked, String> skedTooltipExtraTextProvider; //used for further info in sked tooltip
private Consumer<CandidateEvent> onCandidateClicked;
public TimelineView() {
this.setPrefHeight(40);
this.setStyle("-fx-background-color: #2b2b2b;");
// weitere init defaults, falls du welche hattest
}
/**
* Backward compatibility: if some code still calls the old ctor.
* Potential/tooltip are not view-wide properties; they belong to markers/events.
*/
@Deprecated
public TimelineView(int opportunityPotentialPercent, String tooltipText) {
this(); // ignore args on purpose
}
//
// public int getOpportunityPotentialPercent() {
// return opportunityPotentialPercent;
// }
public void setCurrentAntennaAzimuth(double az) {
this.currentAntennaAzimuth = az;
}
public long getPreviewTimeMs() {
return PREVIEW_TIME_MS;
}
public void setOnCandidateClicked(Consumer<CandidateEvent> handler) {
this.onCandidateClicked = handler;
}
/** Backward compatible call (Skeds only) */
public void updateVisuals(List<ContestSked> skeds) {
updateVisuals(skeds, Collections.emptyList());
}
/**
* Redraws the timeline based on the list of active skeds AND priority candidates.
*/
public void updateVisuals(List<ContestSked> skeds, List<CandidateEvent> candidates) {
this.getChildren().clear();
double width = this.getWidth();
if (width <= 5) {
// Layout not ready yet; will be updated by caller later (uiPulse/list change)
return;
}
// Draw Axis
double axisY = 30;
Line axis = new Line(0, axisY, width, axisY);
axis.setStroke(Color.GRAY);
this.getChildren().add(axis);
long now = System.currentTimeMillis();
// 1) Draw Priority Candidates (upper lanes)
for (CandidateEvent ev : candidates) {
long timeDiff = ev.getTimeUntilMs();
if (timeDiff < 0 || timeDiff > PREVIEW_TIME_MS) continue;
double percent = (double) timeDiff / PREVIEW_TIME_MS;
double xPos = percent * width;
xPos = Math.max(margin, Math.min(this.getWidth() - margin, xPos)); //starting point of the diagram
Node marker = createCandidateMarker(ev);
applyAntennaEffect(marker, ev.getTargetAzimuth());
// Upper lanes so they don't overlap skeds
double laneBaseY = 2;
double laneOffsetY = 14.0 * ev.getLaneIndex();
marker.setLayoutX(xPos);
marker.setLayoutY(laneBaseY + laneOffsetY);
this.getChildren().add(marker);
}
// 2) Draw Skeds (lower lane)
for (ContestSked sked : skeds) {
long timeDiff = sked.getSkedTimeEpoch() - now;
// Only draw if within the next 30 mins
if (timeDiff >= 0 && timeDiff <= PREVIEW_TIME_MS) {
double percent = (double) timeDiff / PREVIEW_TIME_MS;
double xPos = percent * width;
xPos = clamp(xPos, 10, width - 10);
Node marker = createSkedMarker(sked);
applyAntennaEffect(marker, sked.getTargetAzimuth());
marker.setLayoutX(xPos);
marker.setLayoutY(axisY - 18); // below candidate lanes, near axis
this.getChildren().add(marker);
}
}
}
private double clamp(double v, double min, double max) {
return Math.max(min, Math.min(max, v));
}
/**
* Logic:
* If Antenna is ON TARGET -> Bright & Glowing.
* If Antenna is OFF TARGET -> Transparent (Ghost).
*/
private void applyAntennaEffect(Node marker, double targetAz) {
// invalid azimuth -> keep readable
if (!Double.isFinite(targetAz) || targetAz < 0) {
return;
}
double delta = Math.abs(currentAntennaAzimuth - targetAz);
if (delta > 180) delta = 360 - delta;
final boolean onTarget = delta <= (beamWidth / 2.0);
final boolean inBeam = delta <= beamWidth;
// Rule: only fade when we are clearly NOT pointing there
final double iconOpacity = inBeam ? 1.0 : 0.30;
if (marker instanceof Group g) {
// Never fade the whole group -> text stays readable
g.setOpacity(1.0);
for (Node child : g.getChildren()) {
if (child instanceof Label) {
child.setOpacity(1.0);
} else {
child.setOpacity(iconOpacity);
}
}
// Add glow only if well centered (optional)
if (onTarget) {
g.setEffect(new DropShadow(10, Color.LIMEGREEN));
g.setScaleX(1.10);
g.setScaleY(1.10);
} else {
g.setEffect(null);
g.setScaleX(1.0);
g.setScaleY(1.0);
}
return;
}
// fallback
marker.setOpacity(iconOpacity);
marker.setEffect(onTarget ? new DropShadow(10, Color.LIMEGREEN) : null);
}
public void setSkedTooltipExtraTextProvider(Function<ContestSked, String> provider) {
this.skedTooltipExtraTextProvider = provider;
}
/** Existing marker for Skeds (diamond + label) */
private Node createSkedMarker(ContestSked sked) {
Polygon diamond = new Polygon(0.0, 0.0, 6.0, 6.0, 0.0, 12.0, -6.0, 6.0);
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent()));
String baseToolTipFallBack = sked.getTargetCallsign() + " (" + sked.getBand() + ")\nAz: " + sked.getTargetAzimuth();
if (skedTooltipExtraTextProvider != null) {
String extra = skedTooltipExtraTextProvider.apply(sked);
if (extra != null && !extra.isBlank()) {
baseToolTipFallBack += "\n" + extra;
}
}
Tooltip t = new Tooltip(baseToolTipFallBack);
Tooltip.install(diamond, t);
Label lbl = new Label("SKED: " + sked.getTargetCallsign());
// lbl.setFont(new Font(9));
// lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(14);
lbl.setLayoutX(-10);
lbl.setStyle(
"-fx-text-fill: white;" +
"-fx-font-weight: bold;" +
"-fx-background-color: rgba(0,0,0,0.65);" +
"-fx-background-radius: 6;" +
"-fx-padding: 1 4 1 4;"
);
lbl.setEffect(new DropShadow(2, Color.BLACK));
return new Group(diamond, lbl);
}
/**
* Give me a color for a given potencial of a AP reflection
*
* @param p AS potencial
* @return
*/
private Color colorForPotential(int p) {
if (p >= 95) return Color.MAGENTA; // ~100%
if (p >= 75) return Color.RED;
if (p >= 50) return Color.YELLOW;
return Color.DEEPSKYBLUE; // low potential
}
/** New marker for Priority Candidates (triangle + label) */
private Node createCandidateMarker(CandidateEvent ev) {
// Color derived from airplane potential (not urgency)
Color markerColor = colorForPotential(ev.getOpportunityPotentialPercent());
// small triangle marker (points downwards)
Polygon tri = new Polygon(-6.0, 0.0, 6.0, 0.0, 0.0, 10.0);
tri.setFill(markerColor);
// Optional: small dot behind triangle (makes it easier to see)
Circle dot = new Circle(4, markerColor);
dot.setLayoutY(5); // center behind triangle
Label lbl = new Label(ev.getDisplayCallSign());
lbl.setFont(new Font(9));
lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(10);
lbl.setLayoutX(-12);
lbl.setStyle(
"-fx-text-fill: white;" +
"-fx-font-weight: bold;" +
"-fx-background-color: rgba(0,0,0,0.65);" +
"-fx-background-radius: 6;" +
"-fx-padding: 1 4 1 4;"
);
lbl.setEffect(new DropShadow(8, Color.BLACK));
// IMPORTANT: include dot + triangle + label in the Group
Group g = new Group(dot, tri, lbl);
if (ev.getTooltipText() != null && !ev.getTooltipText().isBlank()) {
Tooltip.install(g, new Tooltip(ev.getTooltipText()));
}
g.setOnMouseClicked(e -> {
if (onCandidateClicked != null) {
onCandidateClicked.accept(ev);
}
});
return g;
}
/**
* Data object rendered by the Timeline ("priority candidate").
* Created in Kst4ContestApplication from ScoreService TopCandidates + AirScout next-AP minute.
*/
public static class CandidateEvent {
private final String callSignRaw;
private final String displayCallSign;
private final ChatCategory preferredChatCategory;
private final long timeUntilMs;
private final int minuteBucket;
private final int laneIndex;
private final double targetAzimuth;
private final double score;
private final String tooltipText;
private final int opportunityPotentialPercent;
public CandidateEvent(
String callSignRaw,
String displayCallSign,
ChatCategory preferredChatCategory,
long timeUntilMs,
int minuteBucket,
int laneIndex,
double targetAzimuth,
double score,
int opportunityPotentialPercent,
String tooltipText
) {
this.callSignRaw = callSignRaw;
this.displayCallSign = displayCallSign;
this.preferredChatCategory = preferredChatCategory;
this.timeUntilMs = timeUntilMs;
this.minuteBucket = minuteBucket;
this.laneIndex = laneIndex;
this.targetAzimuth = targetAzimuth;
this.score = score;
this.opportunityPotentialPercent = opportunityPotentialPercent;
this.tooltipText = tooltipText;
}
public String getCallSignRaw() { return callSignRaw; }
public String getDisplayCallSign() { return displayCallSign; }
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
public long getTimeUntilMs() { return timeUntilMs; }
public int getMinuteBucket() { return minuteBucket; }
public int getLaneIndex() { return laneIndex; }
public double getTargetAzimuth() { return targetAzimuth; }
public double getScore() { return score; }
public String getTooltipText() { return tooltipText; }
public int getOpportunityPotentialPercent() { return opportunityPotentialPercent; }
}
public void setBeamWidthDeg(double beamWidthDeg) {
if (beamWidthDeg > 0 && Double.isFinite(beamWidthDeg)) {
this.beamWidth = beamWidthDeg;
}
}
}

View File

@@ -3,7 +3,6 @@ module praktiKST {
requires jdk.xml.dom;
requires java.sql;
requires javafx.media;
exports kst4contest.controller.interfaces;
exports kst4contest.controller;
exports kst4contest.locatorUtils;
exports kst4contest.model;

View File

@@ -6,52 +6,6 @@
-fx-border-color: #ff7777;
}
.btn-showstate-enabled-default {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: black;
}
.btn-showstate-enabled-default:hover {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: black;
}
.btn-showstate-enabled-furtherInfo {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: green;
}
.btn-showstate-enabled-furtherInfo:hover {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: green;
}
.btn-showstate-disabled {
-fx-background-color:linear-gradient(#f0ff35, #111111),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: red;
}
.toggle-button:selected {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
@@ -75,7 +29,6 @@
.text-input-MYQRG1 {
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, orange 0%, red 100%);
-fx-font-weight: 300;
-fx-padding: 1,1,1,1;
}
.button{

View File

@@ -30,8 +30,6 @@
.text-input-MYQRG1 {
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/
-fx-font-weight: 300;
-fx-padding: 1,1,1,1;
}
@@ -46,7 +44,6 @@
.button:hover{
-fx-text-fill: white;
-fx-border-color: #ff7777;
}
.separator *.line {
@@ -85,54 +82,6 @@
-fx-text-fill: #395306;
}
.btn-showstate-enabled-default {
-fx-base: #373e43;
-fx-text-fill: lightgray;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-default:hover {
-fx-base: #373e43;
-fx-text-fill: black;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-furtherInfo {
-fx-base: #373e43;
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-furtherInfo:hover {
-fx-base: #373e43;
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-disabled {
-fx-base: #373e43 ;
-fx-font-weight: bold;
-fx-text-fill: red;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.toggle-button:selected {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),

View File

@@ -8,8 +8,8 @@ public class TestReadUDPASListenerThread {
@Test
public static void main(String[] args) {
// ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
// asUDPReader.start();
ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
asUDPReader.start();
String testThis;

View File

@@ -8,8 +8,8 @@ public class TestReadUDPUCXListenerThread {
@Test
public static void main(String[] args) {
// ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
// ucxUDPReader.start();
ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
ucxUDPReader.start();
String testThis;

View File

@@ -1,623 +1,3 @@
9A5R;Zeljko;JN95MM;StringProperty [value: null];true;true;false;false;false;false;false;false
DM5M;Marc;JN49FL;StringProperty [value: 144.243 ];true;true;false;false;false;false;false;false
DM5M;Marc;JO51JL;StringProperty [value: null];true;true;false;false;false;false;false;false
DM5M;Marc;JO51JL;StringProperty [value: null];true;true;true;false;false;false;false;false
DM5M;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0GEB;Marc;JO51IJ;StringProperty [value: 144.174 ];true;true;false;false;false;false;false;false
DF0GEB;Marc;JO51IJ;StringProperty [value: 144.174 ];true;true;true;false;false;false;false;false
DF9QX;Matthias;JO42HD;StringProperty [value: null];true;false;false;false;false;false;false;false
DF9QX;Matthias;JO42HD;StringProperty [value: null];true;true;false;false;false;false;false;false
DF9QX;Matthias;JO42HD;StringProperty [value: null];true;true;true;false;false;false;false;false
9A1AAY;RKNG;JN85PJ;StringProperty [value: null];true;true;false;false;false;false;false;false
DO5AMF;Marc;JO51IJ;StringProperty [value: null];true;false;true;false;false;false;false;false
;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DM2EUN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2ALF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6KDS;Klaus;JO50KQ;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0YY;Berlin 432.240;JO62GD;StringProperty [value: null];true;false;true;false;false;false;false;false
DL2AKT;Jens;JO50NV;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5AAJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0HBS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK0NA;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DD6YR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OE5D;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DM3F;Fred 70/23cm;JO60OM;StringProperty [value: null];true;false;true;false;false;false;false;false
DG3RAP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OL3Z;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DF0YY;Berlin 432.240;JO62GD;StringProperty [value: 432.240 ];true;false;true;false;false;false;false;false
OE5D;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DM3F;Fred 70/23cm;JO60OM;StringProperty [value: null];true;false;true;false;false;false;false;false
OL3Z;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL4NWM/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OL3Z;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DN4DI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0WF;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL5MO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5OA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1X;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DG7NBE;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OL7C;Club 2m;JO60JJ;StringProperty [value: null];true;true;false;false;false;false;false;false
DO3BST;Sven 2x9 /2x16;JO51KW;StringProperty [value: null];true;true;false;false;false;false;false;false
DR2L;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DR7C;3cm up;JO50WB;StringProperty [value: 377 ];true;true;false;false;false;false;false;false
DL6ON;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5DAW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK2LB;Torsten;JO53LQ;StringProperty [value: null];true;true;false;false;false;false;false;false
DK4VW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DG2ON;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5OA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF7NX;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL3LAR;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DG3AWN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2TN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DN5PW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL1YDI;Dirk 2m/9Ele;JO42FA;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2WC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ3QB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO3LGI;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DF2KD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2YCT;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6AA;Sven;JO43JH;StringProperty [value: 165 ];true;false;true;false;false;false;false;false
DL6ZEJ/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2RMC;Tom 70 + 23cm;JO50WB;StringProperty [value: 432.179.4 ];true;false;true;false;false;false;false;false
DK7SG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF1AK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8NAS;Sigi-70cm;JN59LE;StringProperty [value: null];true;false;true;false;false;false;false;false
DJ9FC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG2YIQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3NGN/P;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL9OLI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6MHG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1AXC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2BQC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO4HBK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2ALF;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL0ARN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO6JH;Julian 2 70 3cm;JO51TX;StringProperty [value: null];true;true;false;false;false;false;false;false
DF5EM/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH1NAS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO3UKW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2BK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5AJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO3LGI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO3BST;Sven 2x9 /2x16;JO51KW;StringProperty [value: null];true;true;true;false;false;false;false;false
DJ1OB;Olli - 2m;JN48UG;StringProperty [value: null];true;true;false;false;false;false;false;false
DG6ME;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM5D;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK5EZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1NPF/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL7GA/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1AYJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1RDO;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL2NDL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6UJH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DA2R;Hans-Jürgen;JN69EM;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4HMS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5DWF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8ZT;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5HQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8LR;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL4MA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM2CF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2HTI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH1AKY;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6ABB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK0KTL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF6RI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2L;Team 2m;JN99BN;StringProperty [value: 144.230 ];true;true;false;false;false;false;false;false
OR6T;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DF1ASG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1RLB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1RWO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0HAL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2AKT;Jens;JO50NV;StringProperty [value: null];true;true;true;false;false;false;false;false
DL9AAA/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM5F;Marcel 2/70/23;JO71ES;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5ANS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0NF;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DO4SKH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL9BBD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1HSF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1RMR;Club;JO60QC;StringProperty [value: null];true;true;false;false;false;false;false;false
DG4UF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DR5W;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6CNG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL7ZN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK4RL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM3ZF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1OHL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DB3LO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1XRK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH0HD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2YDS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH7ACI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM2D;2x8 QRO SSB/CW;JO64ND;StringProperty [value: 180 ];true;true;false;false;false;false;false;false
DH1GSD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2LBK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1MJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ3AK;Detlef;JO52GJ;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ3AX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6ZXG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5OU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM2EV;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM6AT;Andreas;JO52JG;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1UF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5XAT;Holger 2m only;JO53CN;StringProperty [value: null];true;true;false;false;false;false;false;false
DO2PSW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OZ6TY;Henning;JO55XE;StringProperty [value: null];true;true;false;false;false;false;false;false
DG4OP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4WK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ6OL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO3VE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH0LS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2JST;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DN5PW;Philipp 2m SSB;JO50LQ;StringProperty [value: null];true;true;true;false;false;false;false;false
DC7EF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3LAR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DC7BK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH8GHH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0BQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8AMB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO8THW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2LSM;Guenter;JO61GH;StringProperty [value: 144.065 ];true;true;false;false;false;false;false;false
DL5ZA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2AKV;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4MW;Ralf 2m;JO50KQ;StringProperty [value: null];true;true;false;false;false;false;false;false
DF8CV;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2NDL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ5NE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0DLE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6NBS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH6DAO;Ray;JO41CN;StringProperty [value: null];true;true;false;false;false;false;false;false
DH0CF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH1PAL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2MDU;Chris;JN58RF;StringProperty [value: null];true;true;false;false;false;false;false;false
DK7AW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG2SER;Carsten;JN58OH;StringProperty [value: 337 ];true;true;false;false;false;false;false;false
DC9UN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4MN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ2FR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2WU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5MO;Thomas 2m/7023;JO50LQ;StringProperty [value: null];true;true;true;false;false;false;false;false
9A1MC;Mladen 144;JN85QJ;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5AJ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL2MHO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6KDS;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DJ2DA;Hans 432;JO61PG;StringProperty [value: null];true;false;true;false;false;false;false;false
DM5GG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1AYJ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL9AAA/P;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL3BUA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK6AC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
SP7VVB;Maciek;JO91VQ;StringProperty [value: 340 ];true;true;false;false;false;false;false;false
SP6CPF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD6OM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG5BRE;Ronny 70/23/13/9;JO62VM;StringProperty [value: 185 ];true;true;false;false;false;false;false;false
DG5BRE;Ronny 70/23/13/9;JO62VM;StringProperty [value: 185 ];true;true;true;false;false;false;false;false
DH5BS;erni 6/2/70;JO63UW;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6EB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH1GSD;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DO1MEW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1HSF;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL3RHN;Rüdiger 2m;JO63PM;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3HXS;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK0FWS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DC5IMM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8OAZ/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2HSX;Heiko 2m/70cm;JO51XC;StringProperty [value: 300 ];true;false;true;false;false;false;false;false
DJ2NR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1VRY;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2RAS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8QS;Heiko;JO43KH;StringProperty [value: 432288 ];true;false;true;false;false;false;false;false
OE3NHW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8SAM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2FFW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL9MKA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OE3FKS/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1AWD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK6R;144 only;JO70HG;StringProperty [value: 144.176 ];true;true;false;false;false;false;false;false
DH9NFM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DB5SM;Klaus-2m;JN59LE;StringProperty [value: 144.200 ];true;true;false;false;false;false;false;false
DL0GM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OL4N;club 2m;JO60VR;StringProperty [value: 144.232.8 ];true;true;false;false;false;false;false;false
DL2NBU;Peter;JN59KQ;StringProperty [value: 144.239 ];true;true;false;false;false;false;false;false
DM5D;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1DSX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1KCB;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
S57O;Frank;JN86DT;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1KKI;OK1KKI 144MHz;JN79NF;StringProperty [value: 144310 ];true;true;false;false;false;false;false;false
OL7M;OL7M;JO80FG;StringProperty [value: 144.341 ];true;true;false;false;false;false;false;false
OK1KQH;Radioclub;JN79GO;StringProperty [value: 144.351 ];true;true;false;false;false;false;false;false
DQ2C;2m only;JN48WM;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1JHR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK7AC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2TX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OM6DN;2x12ele 950asl;JN99FI;StringProperty [value: 144.155 ];true;true;false;false;false;false;false;false
DL5ALW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG3FFM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG3FFM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM1PIO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2C;70cm-76GHz;JN99AJ;StringProperty [value: 432,333 ];true;false;true;false;false;false;false;false
DF0YY;Berlin.240;JO62GD;StringProperty [value: 432.240 ];true;true;true;false;false;false;false;false
DD2ML;Ulli 4x10 QRO;JN68GI;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2CB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1ATI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0LU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2C;70cm-76GHz;JN99AJ;StringProperty [value: 432 333.000 ];true;false;true;false;false;false;false;false
DM5B;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK5T;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OE2M;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL9DX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO5OMH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DR1T;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2UPG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8MEM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1KKP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6MR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5OCD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1HXL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5C;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF8TM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO7WM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF2AJ;Andy 2/4/6m;JN48MW;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2FFW;FRANK 2m;JO50LQ;StringProperty [value: 203 ];true;true;true;false;false;false;false;false
9A1N;Radio klub;JN85LI;StringProperty [value: 216 ];true;false;true;false;false;false;false;false
DL6NEJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2MAJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG7SCB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DP9X;Pom 144SSB;JO42SC;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0OB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5BL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH0LS;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DR6T;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ6QS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO6NI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6FBK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0GL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2PZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DB7MM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8EAY;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG1E;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2PZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
PC2K;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO8HK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK6FE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
G2N;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5ZBS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
PA3FVE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH7FFE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK0PU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1MF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
PA0GSM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1KUB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5CAT;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL8PA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5HQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH8IAB;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL1SUZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1FY;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4MW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DM4KCS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG9FBA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3NCR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4YAJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2OY;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL1AVF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5OCD;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DO2NFS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8SDQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8LR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1X;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3LE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1OIB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1LDZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO4OFR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG0OGJ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL5OAZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ8AK;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
HB9TTY;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF1ASG;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL9FBF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF9LW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL9NDP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5ALW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DF4HA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5IR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
IQ4KD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO6NI;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DB1RUL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG5DJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OM3KOM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
HG7M;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OM5AW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2KRT;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK3TFA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK7PY;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG8LG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2KJU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF1HF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DC8RI;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK9TF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM2FLY;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2R;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG0OGJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF8OI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DN7OMB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM5F;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DQ55DIG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2TN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
SQ1GU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL7LTM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD5DX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0PP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD9FJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5AAJ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
S53O;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2LSM;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL2FQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK3ZQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1PR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8NSB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
HB9IAB/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL8RH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG7NBE;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK6NJ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL7PV;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1RDO;ok1rdo;JN69KL;StringProperty [value: null];true;true;true;false;false;false;false;false
DL4M;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2YL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3SFB;Martin 70cm;JN48WM;StringProperty [value: 432.224 ];true;false;true;false;false;false;false;false
F8KID;Club;JN38AT;StringProperty [value: 144 254 ];true;true;false;false;false;false;false;false
DL2DHM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2IT;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF8XC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4ZBG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH1AKY;Jens 70;JO50LQ;StringProperty [value: null];true;true;true;false;false;false;false;false
OE5LHM/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL7AVZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1NPF;Roman 2/70;JO70UK;StringProperty [value: 144.351 ];true;true;false;false;false;false;false;false
F6KFH;RC 70cm;JN39OC;StringProperty [value: 267 ];true;false;true;false;false;false;false;false
OK2O;club;JN89IW;StringProperty [value: 144,317 ];true;true;false;false;false;false;false;false
DL0WX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
TM5R;Didier;JN19BQ;StringProperty [value: null];true;true;false;false;false;false;false;false
DL9NDP;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
ON8TT/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
HB9GF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1KAD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL7AX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2KAA;Club 2m;JN79QJ;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1DMP;Milan 2m/70cm;JN79IX;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5JTS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1IME;Ota 2m;JO70FB;StringProperty [value: null];true;true;false;false;false;false;false;false
G3XDY;John;JO02OB;StringProperty [value: 144.214 ];true;true;false;false;false;false;false;false
OK1KCR;BIG GUN;JN79VS;StringProperty [value: 144.162 ];true;true;false;false;false;false;false;false
OK1WAV;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2RZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL9NM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OL3Z;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
SP9KDA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
IQ5NN;MonteNerone144;JN63GN;StringProperty [value: 144.100 ];true;true;false;false;false;false;false;false
DL9NM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL7ACN;Jens, 144;JN49JC;StringProperty [value: 284 ];true;true;false;false;false;false;false;false
DL0NF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD7PA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OL7W;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
HB9NE;Contest Team;JN37JC;StringProperty [value: 273.4 ];true;true;false;false;false;false;false;false
DL6GCK;Konrad;JN47OR;StringProperty [value: 338 ];true;true;false;false;false;false;false;false
OK1KCB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK0GFF/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM3D;Club;JO62IH;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0A;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
S59P;Club JN86AO;JN86AO;StringProperty [value: 144108 ];true;true;false;false;false;false;false;false
OL7C;Radio Club;JO60JJ;StringProperty [value: 144.211 ];true;true;false;false;false;false;false;false
OE5D;ARGE Braunau;JN68PC;StringProperty [value: 328 ];true;true;true;false;false;false;false;false
OK2R;70cm;JN89JM;StringProperty [value: 240 ];true;true;false;false;false;false;false;false
9A0V;RC Vukovar;JN95PE;StringProperty [value: 144.155 ];true;true;false;false;false;false;false;false
HG1Z;Team 2m;JN86KU;StringProperty [value: 335 ];true;true;false;false;false;false;false;false
9A8D;radio klub Dalj;JN95LM;StringProperty [value: 144060 ];true;true;false;false;false;false;false;false
OK2KCN;Club, 2m only;JN89OI;StringProperty [value: 144,049 ];true;true;false;false;false;false;false;false
S50L;mt. Slivnica;JN75ES;StringProperty [value: null];true;true;false;false;false;false;false;false
DD5M;franta;JN58VC;StringProperty [value: 144110 ];true;true;false;false;false;false;false;false
DK0A;Club (1140m asl);JN48CO;StringProperty [value: 144236 ];true;true;false;false;false;false;false;false
OK1RW;144 only;JO70HG;StringProperty [value: 144.177 ];true;true;false;false;false;false;false;false
OK5Y;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DR1H;144320 8*12el;JN59OP;StringProperty [value: 144.320 ];true;true;false;false;false;false;false;false
OK1VDJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3AAV;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1TV;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ9MH;Hajo;JO50FA;StringProperty [value: 144.070 ];true;true;false;false;false;false;false;false
DL1QC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0XX;Contest Club;JO52BO;StringProperty [value: null];true;true;false;false;false;false;false;false
DF2BR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF4AJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
ON4KHG;Gaetan 2m/3cm;JO10XO;StringProperty [value: null];true;true;false;false;false;false;false;false
PD4R;dennis;JO32CD;StringProperty [value: null];true;true;false;false;false;false;false;false
DG6YID;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG0ONW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DF6LH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH4JQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
PA1T;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
ON4EI/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL9MKA;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DG0ONW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD0PX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF1QR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0MU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0MI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2HXE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM3JAN;Janek 2m QRO;JO60OM;StringProperty [value: 350 ];true;true;false;false;false;false;false;false
9A3DF;Zeljko;JN86HF;StringProperty [value: 144233 ];true;true;false;false;false;false;false;false
DL2DRG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG0JMB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
SP6FXF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH2UHE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5ME;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0HG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG4VW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4OCF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF1HC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ3WE;Rudolf;JN57WS;StringProperty [value: 432241,3 ];true;false;true;false;false;false;false;false
DL1HTL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD6ULF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5AWE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2PK;Peter 2m 750W;JO31IK;StringProperty [value: null];true;true;false;false;false;false;false;false
G3M;432.237;JO01QD;StringProperty [value: 432.237 ];true;true;false;false;false;false;false;false
DF7JU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
PE1ITR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2ZO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK4VW;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK2BO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM3AW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM2CHK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5WMA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1GPP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2RSF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL0TZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
SP6ZHP/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2HWA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1RS;432.323 only;JO60MM;StringProperty [value: 432.323 ];true;false;true;false;false;false;false;false
PE1OBL;Hans 12 EL ZL;JO21ET;StringProperty [value: 162 ];true;true;false;false;false;false;false;false
OK1DOY;Zdeno 2m;JO60UQ;StringProperty [value: 144.326 ];true;true;false;false;false;false;false;false
DJ8MS;Tor_70cm;JO54VC;StringProperty [value: 282 ];true;false;true;false;false;false;false;false
DO9OM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK4IN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1EIP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG7BBP/P;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK6AO;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OK1HCU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0PW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO1MLH;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL2AWR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2YDS;Stefan;JO42HG;StringProperty [value: null];true;true;true;false;false;false;false;false
DR6R;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF8KVK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1KC/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH8NAS;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DR7B;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4MHT;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG3AWN;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL4NAZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK2KOJ;70 & 23 cm;JN79UG;StringProperty [value: 233 ];true;false;true;false;false;false;false;false
OK2KYJ;2/70 1kW/500W;JN89QQ;StringProperty [value: 305 ];true;true;false;false;false;false;false;false
DO1JKO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM3DG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH1DX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DN5KA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG6ME;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DG1HQK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1OA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG0LFG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1KAD;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DH1AKY;Jens 2m;JO50LQ;StringProperty [value: null];true;true;true;false;false;false;false;false
OK7MH;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
SM7FMX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK0TU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK6M;Martin;JN99CR;StringProperty [value: 177 ];true;false;true;false;false;false;false;false
DL2LMS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF1KA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2L;Volker;JN68DT;StringProperty [value: 312 ];true;true;false;false;false;false;false;false
DO2LNJ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DB7AD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1YEG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5WN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DR7B;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OZ1JMN;Allan;JO46VE;StringProperty [value: 262 250 ];true;true;false;false;false;false;false;false
DB0DH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1DCS;Vaclav;JN78CS;StringProperty [value: null];true;true;false;false;false;false;false;false
SN7L;Team 144.236;JO91QF;StringProperty [value: 144.240 ];true;true;false;false;false;false;false;false
DH6AD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4M;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OK1AUO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD7MH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OP5Y/P;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK9TF;Juergen 23+13;JO31NF;StringProperty [value: 1296.233 ];true;true;true;false;false;false;false;false
DL8DAU;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL8SCD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF6KB;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DC6HG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ1AA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ6JJ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OK1FPQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6MHG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM7KN/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5KK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5WN;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL6ZEJ/P;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL2ZA;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL0BBK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM8MM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5BAW/P;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0GC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG4MH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG6YGE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG1YBN;Harald;JO31VX;StringProperty [value: null];true;true;false;false;false;false;false;false
DF0AP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1PZ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DF2QZ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2PU;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DD5DD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK9ZQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG0PF;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK9AM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OE4WHG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO5HMK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
F6GYH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF7WL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
HB9YBQ;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1WB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
S57M;Bojan 432285;JN76PO;StringProperty [value: 390 ];true;false;true;false;false;false;false;false
DG1HP;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DC2TH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
HB9OOH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF2CD;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL9DBF;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4ASK;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DJ7AQ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DJ3AM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1EHG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DG5YL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL2MAJ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DR1T;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DC6HG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DF2AP;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DB0AI;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL6DBN;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5NUA;Klaus(70cm);JO63PO;StringProperty [value: 190 ];true;false;true;false;false;false;false;false
PI4ADH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1FY;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL5NUA;Klaus(70cm);JO63PO;StringProperty [value: 190 ];true;false;true;false;false;false;false;false
DF3TE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DC6CX/P;Chris 2/70/23;JO31SE;StringProperty [value: null];true;true;false;false;false;false;false;false
DK5WO;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1SE;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL1AG;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DB3LO;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OK1MBT;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DJ1AA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DO4SSH;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH1PS;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL3YCW;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK5ET;Martin LP 9elY;JO70WE;StringProperty [value: 268 ];true;true;false;false;false;false;false;false
OE5JWL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK1PZ;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DK5TI;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DO1ARR;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH8GHH;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL1SE;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OK1VQC;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5AWE;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OK1PMA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DH2PA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DM5CB;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OE5FLM;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
DL4MN;unknown;unknown;StringProperty [value: null];true;false;true;false;false;false;false;false
OE6V;Werner 72 el kW;JN76VT;StringProperty [value: 078 ];true;true;false;false;false;false;false;false
DL6CWM;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL4ASK;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DL5SE;Dan 70cm;JO50XL;StringProperty [value: null];true;false;true;false;false;false;false;false
DJ6VX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OM3W;Club 2m;JN99CH;StringProperty [value: 302 ];true;true;false;false;false;false;false;false
DL2NDL;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
OK1OLA;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false
DK2TX;unknown;unknown;StringProperty [value: null];true;true;false;false;false;false;false;false