mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-04-21 16:02:37 +02:00
Compare commits
6 Commits
68d171e793
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8d83f797df | |||
| e35dee55d1 | |||
|
|
071ea800ae | ||
| e01cc3ca11 | |||
| aaa5c1088a | |||
| 178783aa8c |
2
.github/latex-manual/manual-template.tex
vendored
2
.github/latex-manual/manual-template.tex
vendored
@@ -239,7 +239,7 @@ $endif$
|
||||
{\fontsize{22}{28}\selectfont\color{white!75!brand-green}pratiKST (ON4KST Chat Client)}\\[2.8cm]
|
||||
\color{white!40!brand-green}\rule{10cm}{0.6pt}\\[1.8cm]
|
||||
{\LARGE\bfseries\color{white}$title$}\\[1cm]
|
||||
$if(version)${\large\color{white!80!brand-green}Version:\space$version$}\\[0.6cm]$endif$
|
||||
$if(version)${\large\color{white!80!brand-green}Version:\space{}$version$}\\[0.6cm]$endif$
|
||||
\vfill
|
||||
{\large\color{white}DO5AMF · Marc Fröhlich · DM5M · DN9APW · Philipp Wagner}\\[0.4cm]
|
||||
{\color{white!70!brand-green}\today}\\[2cm]
|
||||
|
||||
96
.github/workflows/nightly-artifacts.yml
vendored
96
.github/workflows/nightly-artifacts.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
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"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name praktiKST \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
@@ -116,39 +116,105 @@ jobs:
|
||||
|
||||
- name: Create AppDir metadata
|
||||
run: |
|
||||
rm -rf target/praktiKST.AppDir
|
||||
cp -a dist/praktiKST target/praktiKST.AppDir
|
||||
rm -rf target/KST4Contest.AppDir
|
||||
cp -a dist/KST4Contest target/KST4Contest.AppDir
|
||||
|
||||
cat > target/praktiKST.AppDir/AppRun << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/AppRun << 'EOF'
|
||||
#!/bin/sh
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/bin/praktiKST" "$@"
|
||||
exec "$HERE/bin/KST4Contest" "$@"
|
||||
EOF
|
||||
chmod +x target/praktiKST.AppDir/AppRun
|
||||
chmod +x target/KST4Contest.AppDir/AppRun
|
||||
|
||||
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/KST4Contest.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=praktiKST
|
||||
Exec=praktiKST
|
||||
Icon=praktiKST
|
||||
Name=KST4Contest
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
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
|
||||
if [ -f target/KST4Contest.AppDir/lib/KST4Contest.png ]; then
|
||||
cp target/KST4Contest.AppDir/lib/KST4Contest.png target/KST4Contest.AppDir/KST4Contest.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"
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/KST4Contest.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
|
||||
path: dist/KST4Contest-*-linux-x86_64.AppImage
|
||||
retention-days: 14
|
||||
|
||||
build-macos-dmg:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, macos-15-intel]
|
||||
|
||||
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}"
|
||||
ARCH=$(uname -m)
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
echo "ARCH=$ARCH" >> "$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 macOS DMG with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type dmg \
|
||||
--name KST4Contest \
|
||||
--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
|
||||
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
|
||||
- name: Rename DMG artifact
|
||||
run: |
|
||||
DMG=$(ls dist/*.dmg | head -n 1)
|
||||
if [ -z "$DMG" ]; then
|
||||
echo "No DMG produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$DMG" "dist/${ASSET_BASENAME}-macos-${ARCH}.dmg"
|
||||
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.os }}
|
||||
path: dist/KST4Contest-*-macos-*.dmg
|
||||
retention-days: 14
|
||||
|
||||
95
.github/workflows/tagged-release.yml
vendored
95
.github/workflows/tagged-release.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name praktiKST \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
@@ -98,41 +98,97 @@ jobs:
|
||||
|
||||
- name: Create AppDir metadata
|
||||
run: |
|
||||
rm -rf target/praktiKST.AppDir
|
||||
cp -a dist/praktiKST target/praktiKST.AppDir
|
||||
rm -rf target/KST4Contest.AppDir
|
||||
cp -a dist/KST4Contest target/KST4Contest.AppDir
|
||||
|
||||
cat > target/praktiKST.AppDir/AppRun << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/AppRun << 'EOF'
|
||||
#!/bin/sh
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/bin/praktiKST" "$@"
|
||||
exec "$HERE/bin/KST4Contest" "$@"
|
||||
EOF
|
||||
chmod +x target/praktiKST.AppDir/AppRun
|
||||
chmod +x target/KST4Contest.AppDir/AppRun
|
||||
|
||||
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/KST4Contest.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=praktiKST
|
||||
Exec=praktiKST
|
||||
Icon=praktiKST
|
||||
Name=KST4Contest
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
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
|
||||
if [ -f target/KST4Contest.AppDir/lib/KST4Contest.png ]; then
|
||||
cp target/KST4Contest.AppDir/lib/KST4Contest.png target/KST4Contest.AppDir/KST4Contest.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
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/KST4Contest.AppDir dist/KST4Contest-${{ 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
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
|
||||
build-macos-dmg:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, macos-15-intel]
|
||||
|
||||
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 macOS DMG with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type dmg \
|
||||
--name KST4Contest \
|
||||
--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
|
||||
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
|
||||
- name: Rename DMG artifact
|
||||
run: |
|
||||
ARCH=$(uname -m)
|
||||
DMG=$(ls dist/*.dmg | head -n 1)
|
||||
if [ -z "$DMG" ]; then
|
||||
echo "No DMG produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$DMG" "dist/KST4Contest-${{ github.ref_name }}-macos-${ARCH}.dmg"
|
||||
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.os }}
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-macos-*.dmg
|
||||
|
||||
build-docs-pdf:
|
||||
name: Build Documentation PDF
|
||||
@@ -217,6 +273,7 @@ jobs:
|
||||
needs:
|
||||
- build-windows-zip
|
||||
- build-linux-appimage
|
||||
- build-macos-dmg
|
||||
- build-docs-pdf
|
||||
|
||||
steps:
|
||||
@@ -232,6 +289,13 @@ jobs:
|
||||
name: linux-appimage
|
||||
path: release-assets/linux
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
pattern: macos-dmg-*
|
||||
merge-multiple: true
|
||||
path: release-assets/macos
|
||||
|
||||
- name: Download PDF manuals
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
@@ -251,6 +315,7 @@ jobs:
|
||||
generateReleaseNotes: true
|
||||
artifacts: >-
|
||||
release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,
|
||||
release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage,
|
||||
release-assets/linux/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage,
|
||||
release-assets/macos/KST4Contest-${{ github.ref_name }}-macos-*.dmg,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
|
||||
|
||||
@@ -46,7 +46,17 @@ Die aktuelle Version kann als AppImage heruntergeladen werden:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
Der Dateiname hat das Format `praktiKST-v<Versionsnummer>-linux-x86_64.AppImage`.
|
||||
Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-linux-x86_64.AppImage`.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort-Support:** macOS-Builds werden als zusätzliche Option bereitgestellt, sind aber **nicht umfassend getestet**. Wir bauen und veröffentlichen macOS-Binaries mit jedem Release, können allerdings nicht alle Szenarien unter macOS testen. Bei Problemen freuen wir uns über eine Rückmeldung – wir versuchen unser Bestes, können aber nicht den gleichen Support-Umfang wie für Windows und Linux garantieren.
|
||||
|
||||
Die aktuelle Version kann als DMG-Disk-Image heruntergeladen werden (für Apple-Silicon- und Intel-Macs verfügbar):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-macos-<Architektur>.dmg`, wobei `<Architektur>` entweder `arm64` (Apple Silicon) oder `x86_64` (Intel) ist.
|
||||
|
||||
|
||||
---
|
||||
@@ -64,11 +74,22 @@ Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` gespei
|
||||
### Linux
|
||||
1. AppImage herunterladen.
|
||||
2. AppImage in gewünschten Ordner entpacken.
|
||||
3. AppImage ausführbar machen (geht im Terminal mit `chmod +x praktiKST-v<Versionsnummer>-linux-x86_64.AppImage`)
|
||||
3. AppImage ausführbar machen (geht im Terminal mit `chmod +x KST4Contest-v<Versionsnummer>-linux-x86_64.AppImage`)
|
||||
4. AppImage ausführen.
|
||||
|
||||
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
|
||||
### macOS
|
||||
1. DMG-Datei für die eigene Architektur herunterladen (Apple Silicon oder Intel).
|
||||
2. DMG-Datei öffnen.
|
||||
3. `KST4Contest.app` in den **Programme**-Ordner ziehen.
|
||||
4. Beim ersten Start zeigt macOS ggf. eine Warnung, da die App nicht notarisiert ist. Zum Öffnen:
|
||||
- Rechtsklick (oder Ctrl-Klick) auf `KST4Contest.app` im Finder → **Öffnen** wählen.
|
||||
- Alternativ: **Systemeinstellungen → Datenschutz & Sicherheit** → **Trotzdem öffnen** klicken.
|
||||
5. KST4Contest aus dem Programme-Ordner oder dem Launchpad starten.
|
||||
|
||||
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## Update
|
||||
@@ -98,6 +119,12 @@ Derzeit folgendermaßen:
|
||||
2. neues AppImage ausführbar makieren
|
||||
3. (optional) altes AppImage löschen.
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Neue DMG-Datei herunterladen.
|
||||
2. DMG öffnen.
|
||||
3. Die neue `KST4Contest.app` in den **Programme**-Ordner ziehen und die alte Version ersetzen.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -83,20 +83,26 @@ Für den integrierten DX-Cluster-Server: N1MM+ als DX-Cluster-Client konfigurier
|
||||
|
||||
### Win-Test
|
||||
|
||||
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
|
||||
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
|
||||
|
||||
**Vorteile der Win-Test Integration:**
|
||||
- Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen.
|
||||
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieses auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet*.
|
||||
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieser auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet* – automatisch, sobald der Listener aktiv ist.
|
||||
- Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden.
|
||||
- **Automatische QRG-Auflösung für SKEDs:** KST4Contest wählt die Sked-Frequenz intelligent:
|
||||
1. Hat die Gegenstation in einer Chat-Nachricht ihre QRG genannt, wird diese verwendet.
|
||||
2. Sonst wird die eigene aktuelle QRG verwendet (aus Win-Test STATUS oder manueller Eingabe).
|
||||
|
||||
**Notwendige Einstellungen in KST4Contest:**
|
||||
- `UDP-Port for Win-Test listener` (Standard: 9871).
|
||||
**Einstellungen im Reiter „Log-Synchronisation":**
|
||||
- `Receive Win-Test network based UDP log messages` aktivieren.
|
||||
- `Win-Test sked transmission (push via ADDSKED to Win-Test network)` aktivieren.
|
||||
- `UDP-Port for Win-Test listener` (Standard: 9871).
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST").
|
||||
- `Win-Test station name filter`: Wenn hier ein Name eingetragen wird (z.B. "STN1"), werden nur QSOs von dieser bestimmten Win-Test Instanz verarbeitet. Leer lassen, um alle zu akzeptieren.
|
||||
- `Win-Test network broadcast address`: Wird idR automatisch erkannt und ist erforderlich, um die Sked-Pakete ins Netzwerk zu senden.
|
||||
- `Win-Test network broadcast address`: Wird i.d.R. automatisch erkannt; erforderlich für das Senden von Sked-Paketen.
|
||||
|
||||
**Einstellungen im Reiter „TRX-Synchronisation":**
|
||||
- `Win-Test STATUS QRG Sync`: Wenn aktiviert, übernimmt KST4Contest die aktuelle Transceiverfrequenz aus dem Win-Test STATUS-Paket als eigene QRG (MYQRG).
|
||||
- `Use pass frequency from Win-Test STATUS`: Statt der eigenen TRX-QRG wird die im STATUS-Paket enthaltene Pass-Frequenz als MYQRG verwendet (für Multi-Op-Setups, bei denen mit einer Pass-QRG gearbeitet wird).
|
||||
- `Win-Test station name filter`: Wird hier ein Name eingetragen (z.B. "STN1"), verarbeitet KST4Contest nur Pakete dieser Win-Test-Instanz. Leer lassen, um alle zu akzeptieren.
|
||||
|
||||
**Einstellungen in Win-Test:**
|
||||
- Das Netzwerk in Win-Test muss aktiv sein.
|
||||
@@ -112,6 +118,11 @@ Neben der QSO-Synchronisation übertragen UCXLog und andere Programme auch die *
|
||||
|
||||
**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.
|
||||
|
||||
**Quellen für die eigene QRG (MYQRG):**
|
||||
- UCXLog, N1MM+, DXLog.net, QARTest via UDP-Port 12060
|
||||
- Win-Test STATUS-Paket (optional, konfigurierbar im Reiter „TRX-Synchronisation" unter „Win-Test STATUS QRG Sync")
|
||||
- Manuelle Eingabe im QRG-Feld
|
||||
|
||||
> **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -46,7 +46,17 @@ The latest version can be downloaded as an AppImage:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
The filename has the format `praktiKST-v<version_number>-linux-x86_64.AppImage`.
|
||||
The filename has the format `KST4Contest-v<version_number>-linux-x86_64.AppImage`.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort Support:** macOS builds are provided as a convenience but are **not fully tested**. We build and release macOS binaries with every release, but we cannot test every scenario on macOS. If you encounter issues, please report them – we will do our best to address them, but cannot guarantee the same level of support as for Windows and Linux.
|
||||
|
||||
The latest version can be downloaded as a DMG disk image (available for both Apple Silicon and Intel Macs):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
The filename has the format `KST4Contest-v<version_number>-macos-<arch>.dmg`, where `<arch>` is `arm64` (Apple Silicon) or `x86_64` (Intel).
|
||||
|
||||
|
||||
---
|
||||
@@ -64,11 +74,22 @@ Settings are stored at `%USERPROFILE%\.praktikst\preferences.xml`.
|
||||
### Linux
|
||||
1. Download the AppImage.
|
||||
2. Unzip the AppImage into a folder of your choice.
|
||||
3. Make the AppImage executable (in the terminal with `chmod +x praktiKST-v<version_number>-linux-x86_64.AppImage`)
|
||||
3. Make the AppImage executable (in the terminal with `chmod +x KST4Contest-v<version_number>-linux-x86_64.AppImage`)
|
||||
4. Run the AppImage.
|
||||
|
||||
Settings are stored at `~/.praktikst/preferences.xml`.
|
||||
|
||||
### macOS
|
||||
1. Download the DMG file for your architecture (Apple Silicon or Intel).
|
||||
2. Open the DMG file.
|
||||
3. Drag `KST4Contest.app` into your **Applications** folder.
|
||||
4. On first launch, macOS may show a warning because the app is not notarised. To open it:
|
||||
- Right-click (or Control-click) on `KST4Contest.app` in Finder and choose **Open**.
|
||||
- Alternatively, go to **System Settings → Privacy & Security** and click **Open Anyway**.
|
||||
5. Run KST4Contest from your Applications folder or Launchpad.
|
||||
|
||||
Settings are stored at `~/.praktikst/preferences.xml`.
|
||||
|
||||
---
|
||||
|
||||
## Updating
|
||||
@@ -98,6 +119,12 @@ Currently as follows:
|
||||
2. Mark the new AppImage as executable
|
||||
3. (optional) Delete the old AppImage.
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Download the new DMG file.
|
||||
2. Open the DMG.
|
||||
3. Drag the new `KST4Contest.app` into your **Applications** folder, replacing the old version.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -87,16 +87,22 @@ Win-Test is supported with a dedicated UDP network listener that understands the
|
||||
|
||||
**Advantages of Win-Test Integration:**
|
||||
- Automatic QSO synchronization to mark worked stations.
|
||||
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet*.
|
||||
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet* – automatically, as soon as the listener is active. No separate toggle is needed.
|
||||
- You can choose between "AUTO", "SSB", or "CW" sked modes.
|
||||
- **Automatic QRG resolution for SKEDs:** KST4Contest selects the sked frequency intelligently:
|
||||
1. If the other station mentioned their QRG in a recent chat message, that frequency is used.
|
||||
2. Otherwise, your own current QRG is used (from Win-Test STATUS or manual entry).
|
||||
|
||||
**Required Settings in KST4Contest:**
|
||||
- `UDP-Port for Win-Test listener` (Default: 9871).
|
||||
**Settings in the "Log Synchronisation" tab:**
|
||||
- Enable `Receive Win-Test network based UDP log messages`.
|
||||
- Enable `Win-Test sked transmission (push via ADDSKED to Win-Test network)`.
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Defines the station name KST4Contest uses in the WT network (e.g., "KST").
|
||||
- `Win-Test station name filter`: If a name is entered here (e.g., "STN1"), only QSOs from that specific Win-Test instance will be processed. Leave empty to accept all.
|
||||
- `Win-Test network broadcast address`: Is usually detected automatically and is required to send sked packets to the network.
|
||||
- `UDP-Port for Win-Test listener` (default: 9871).
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Defines the station name KST4Contest uses in the WT network (e.g. "KST").
|
||||
- `Win-Test network broadcast address`: Usually detected automatically; required to send sked packets to the network.
|
||||
|
||||
**Settings in the "TRX Synchronisation" tab:**
|
||||
- `Win-Test STATUS QRG Sync`: When enabled, KST4Contest takes the current transceiver frequency from the Win-Test STATUS packet and uses it as your own QRG (MYQRG).
|
||||
- `Use pass frequency from Win-Test STATUS`: Instead of the main TRX frequency, the pass frequency contained in the STATUS packet is used as MYQRG (useful for multi-op setups that operate with a dedicated pass QRG).
|
||||
- `Win-Test station name filter`: If a name is entered here (e.g. "STN1"), KST4Contest only processes packets from that specific Win-Test instance. Leave empty to accept all.
|
||||
|
||||
**Settings in Win-Test:**
|
||||
- The network in Win-Test must be active.
|
||||
@@ -112,6 +118,11 @@ In addition to QSO synchronisation, UCXLog and other programs also transmit the
|
||||
|
||||
**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.
|
||||
|
||||
**Sources for your own QRG (MYQRG):**
|
||||
- UCXLog, N1MM+, DXLog.net, QARTest via UDP port 12060
|
||||
- Win-Test STATUS packet (optional, configurable in the "TRX Synchronisation" tab under "Win-Test STATUS QRG Sync")
|
||||
- Manual entry in the QRG field
|
||||
|
||||
> **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.
|
||||
|
||||
---
|
||||
|
||||
528
src/kstsimulator.py
Normal file
528
src/kstsimulator.py
Normal file
@@ -0,0 +1,528 @@
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# =====================================
|
||||
# KST-Server-Simulator / DO5AMF
|
||||
# Usage: change configuration below and
|
||||
# run. Enter 127.0.0.1 : 23001 as a
|
||||
# target in KST4Contest or another
|
||||
# KST chat client.
|
||||
# =====================================
|
||||
|
||||
# ==========================================
|
||||
# KONFIGURATION
|
||||
# ==========================================
|
||||
|
||||
PORT = 23001
|
||||
HOST = '127.0.0.1'
|
||||
|
||||
MSG_TO_USER_INTERVAL = 300.0
|
||||
LOGIN_LOGOUT_INTERVAL = 60.0
|
||||
KEEP_ALIVE_INTERVAL = 10.0
|
||||
CLIENT_WARMUP_TIME = 5.0
|
||||
|
||||
PROB_INACTIVE = 0.10
|
||||
PROB_REACTIVE = 0.20
|
||||
|
||||
# QSY Wahrscheinlichkeit (Wie oft wechselt ein User seine Frequenz?)
|
||||
# 0.05 = 5% Chance pro Nachricht, dass er die Frequenz ändert. Sonst bleibt er stabil.
|
||||
PROB_QSY = 0.05
|
||||
|
||||
BANDS_VHF = { "2m": (144.150, 144.400), "70cm": (432.100, 432.300) }
|
||||
BANDS_UHF = { "23cm": (1296.100, 1296.300), "3cm": (10368.100, 10368.250) }
|
||||
|
||||
CHANNELS_SETUP = {
|
||||
"2": {
|
||||
"NAME": "144/432 MHz",
|
||||
"NUM_USERS": 777,
|
||||
"BANDS": BANDS_VHF,
|
||||
"RATES": {"PUBLIC": 0.5, "DIRECTED": 3.0},
|
||||
"PERMANENT": [
|
||||
{"call": "DK5EW", "name": "Erwin", "loc": "JN47NX"},
|
||||
{"call": "DL1TEST", "name": "TestOp", "loc": "JO50XX"}
|
||||
]
|
||||
},
|
||||
"3": {
|
||||
"NAME": "Microwave",
|
||||
"NUM_USERS": 333,
|
||||
"BANDS": BANDS_UHF,
|
||||
"RATES": {"PUBLIC": 0.2, "DIRECTED": 0.5},
|
||||
"PERMANENT": [
|
||||
{"call": "ON4KST", "name": "Alain", "loc": "JO20HI"},
|
||||
{"call": "G4CBW", "name": "MwTest", "loc": "IO83AA"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
COUNTRY_MAPPING = {
|
||||
"DL": ["JO", "JN"], "DA": ["JO", "JN"], "DF": ["JO", "JN"], "DJ": ["JO", "JN"], "DK": ["JO", "JN"], "DO": ["JO", "JN"],
|
||||
"F": ["JN", "IN", "JO"], "G": ["IO", "JO"], "M": ["IO", "JO"], "2E": ["IO", "JO"],
|
||||
"PA": ["JO"], "ON": ["JO"], "OZ": ["JO"], "SM": ["JO", "JP"], "LA": ["JO", "JP"],
|
||||
"OH": ["KP"], "SP": ["JO", "KO"], "OK": ["JO", "JN"], "OM": ["JN", "KN"],
|
||||
"HA": ["JN", "KN"], "S5": ["JN"], "9A": ["JN"], "HB9": ["JN"], "OE": ["JN"],
|
||||
"I": ["JN", "JM"], "IK": ["JN", "JM"], "IU": ["JN", "JM"], "EA": ["IN", "IM"],
|
||||
"CT": ["IM"], "EI": ["IO"], "GM": ["IO"], "GW": ["IO"], "YO": ["KN"],
|
||||
"YU": ["KN"], "LZ": ["KN"], "SV": ["KM", "KN"], "UR": ["KO", "KN"],
|
||||
"LY": ["KO"], "YL": ["KO"], "ES": ["KO"]
|
||||
}
|
||||
|
||||
NAMES = ["Hans", "Peter", "Jo", "Alain", "Mike", "Sven", "Ole", "Jean", "Bob", "Tom", "Giovanni", "Mario", "Frank", "Steve", "Dave"]
|
||||
|
||||
MSG_TEMPLATES_WITH_FREQ = [
|
||||
"QSY {freq}", "PSE QSY {freq}", "Calling CQ on {freq}", "I am QRV on {freq}",
|
||||
"Listening on {freq}", "Can you try {freq}?", "Signals strong on {freq}",
|
||||
"Scattering on {freq}", "Please go to {freq}", "Running test on {freq}",
|
||||
"Any takers for {freq}?", "Back to {freq}", "QRG {freq}?", "Aircraft scatter {freq}"
|
||||
]
|
||||
|
||||
MSG_TEMPLATES_TEXT_ONLY = [
|
||||
"TNX for QSO", "73 all", "Anyone for sked?", "Good conditions",
|
||||
"Nothing heard", "Rain scatter?", "Waiting for moonrise", "CQ Contest",
|
||||
"QRZ?", "My locator is {loc}", "Band is open"
|
||||
]
|
||||
|
||||
REPLY_TEMPLATES = [
|
||||
"Hello {user}, 599 here", "Rgr {user}, tnx for report", "Yes {user}, QSY?",
|
||||
"Sorry {user}, no copy", "Pse wait 5 min {user}", "Ok {user}, 73",
|
||||
"Locator is {loc}", "Go to {freq} please", "Rgr {user}, gl"
|
||||
]
|
||||
|
||||
# ==========================================
|
||||
# CLIENT WRAPPER
|
||||
# ==========================================
|
||||
|
||||
class ConnectedClient:
|
||||
def __init__(self, sock, addr):
|
||||
self.sock = sock
|
||||
self.addr = addr
|
||||
self.call = f"GUEST_{random.randint(1000,9999)}"
|
||||
self.channels = {"2"}
|
||||
self.login_time = time.time()
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def send_safe(self, data_str):
|
||||
if not data_str: return True
|
||||
with self.lock:
|
||||
try:
|
||||
self.sock.sendall(data_str.encode('latin-1', errors='replace'))
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
try: self.sock.close()
|
||||
except: pass
|
||||
|
||||
# ==========================================
|
||||
# LOGIK KLASSEN
|
||||
# ==========================================
|
||||
|
||||
class MessageFactory:
|
||||
@staticmethod
|
||||
def get_stable_frequency(user, band_name, min_f, max_f):
|
||||
"""Liefert eine stabile Frequenz für diesen User auf diesem Band"""
|
||||
# Wenn noch keine Frequenz da ist ODER Zufall zuschlägt (QSY)
|
||||
if band_name not in user['freqs'] or random.random() < PROB_QSY:
|
||||
freq_val = round(random.uniform(min_f, max_f), 3)
|
||||
user['freqs'][band_name] = f"{freq_val:.3f}"
|
||||
|
||||
return user['freqs'][band_name]
|
||||
|
||||
@staticmethod
|
||||
def get_chat_message(bands_config, user):
|
||||
try:
|
||||
# Entscheidung: Text mit Frequenz oder ohne?
|
||||
if random.random() < 0.7:
|
||||
# Wähle zufälliges Band aus den verfügbaren
|
||||
band_name = random.choice(list(bands_config.keys()))
|
||||
min_f, max_f = bands_config[band_name]
|
||||
|
||||
# Hole STABILE Frequenz für diesen User
|
||||
freq_str = MessageFactory.get_stable_frequency(user, band_name, min_f, max_f)
|
||||
|
||||
return random.choice(MSG_TEMPLATES_WITH_FREQ).format(freq=freq_str)
|
||||
else:
|
||||
return random.choice(MSG_TEMPLATES_TEXT_ONLY).format(loc=user['loc'])
|
||||
except: return "TNX 73"
|
||||
|
||||
@staticmethod
|
||||
def get_reply_msg(bands, target_call, my_loc):
|
||||
try:
|
||||
tmpl = random.choice(REPLY_TEMPLATES)
|
||||
freq_str = "QSY?"
|
||||
# Bei Replies simulieren wir oft nur "QSY?" ohne konkrete Frequenz,
|
||||
# oder nutzen eine zufällige, da der Kontext fehlt.
|
||||
if "{freq}" in tmpl and bands:
|
||||
band_name = random.choice(list(bands.keys()))
|
||||
min_f, max_f = bands[band_name]
|
||||
freq_str = f"{round(random.uniform(min_f, max_f), 3):.3f}"
|
||||
return tmpl.format(user=target_call, loc=my_loc, freq=freq_str)
|
||||
except: return "TNX 73"
|
||||
|
||||
class UserFactory:
|
||||
registry = {}
|
||||
|
||||
@classmethod
|
||||
def get_or_create_user(cls, channel_id, current_channel_users):
|
||||
# 1. Reuse existing
|
||||
candidates = [u for call, u in cls.registry.items() if call not in current_channel_users]
|
||||
if candidates and random.random() < 0.5:
|
||||
return random.choice(candidates)
|
||||
|
||||
# 2. Create new
|
||||
return cls._create_new_unique_user(channel_id, current_channel_users)
|
||||
|
||||
@classmethod
|
||||
def _create_new_unique_user(cls, channel_id, current_channel_users):
|
||||
while True:
|
||||
prefix = random.choice(list(COUNTRY_MAPPING.keys()))
|
||||
num = random.randint(0, 9)
|
||||
suffix = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=random.randint(1,3)))
|
||||
call = f"{prefix}{num}{suffix}"
|
||||
|
||||
if call in current_channel_users: continue
|
||||
if call in cls.registry: return cls.registry[call]
|
||||
|
||||
valid_grids = COUNTRY_MAPPING[prefix]
|
||||
grid_prefix = random.choice(valid_grids)
|
||||
sq_num = f"{random.randint(0,99):02d}"
|
||||
sub = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=2))
|
||||
loc = f"{grid_prefix}{sq_num}{sub}"
|
||||
|
||||
name = random.choice(NAMES)
|
||||
rand = random.random()
|
||||
if rand < PROB_INACTIVE: role = "INACTIVE"
|
||||
elif rand < (PROB_INACTIVE + PROB_REACTIVE): role = "REACTIVE"
|
||||
else: role = "ACTIVE"
|
||||
|
||||
# Neu V31: Frequenz-Gedächtnis
|
||||
user_data = {
|
||||
"call": call,
|
||||
"name": name,
|
||||
"loc": loc,
|
||||
"role": role,
|
||||
"freqs": {} # Speicher für { '2m': '144.300' }
|
||||
}
|
||||
|
||||
cls.registry[call] = user_data
|
||||
return user_data
|
||||
|
||||
@classmethod
|
||||
def register_permanent(cls, user_data):
|
||||
# Sicherstellen, dass auch Permanent User Freq-Memory haben
|
||||
if "freqs" not in user_data:
|
||||
user_data["freqs"] = {}
|
||||
cls.registry[user_data['call']] = user_data
|
||||
|
||||
# ==========================================
|
||||
# CHANNEL INSTANCE
|
||||
# ==========================================
|
||||
|
||||
class ChannelInstance:
|
||||
def __init__(self, cid, config, server):
|
||||
self.id = cid
|
||||
self.config = config
|
||||
self.server = server
|
||||
|
||||
self.users_pool = []
|
||||
self.online_users = {}
|
||||
self.history_chat = []
|
||||
|
||||
self.last_pub = time.time()
|
||||
self.last_dir = time.time()
|
||||
self.last_me = time.time()
|
||||
self.last_login = time.time()
|
||||
|
||||
self.rate_pub = 1.0 / config["RATES"]["PUBLIC"]
|
||||
self.rate_dir = 1.0 / config["RATES"]["DIRECTED"]
|
||||
|
||||
self._init_data()
|
||||
|
||||
def _init_data(self):
|
||||
print(f"[*] Init Channel {self.id} ({self.config['NAME']})...")
|
||||
|
||||
for u in self.config["PERMANENT"]:
|
||||
u_full = u.copy()
|
||||
u_full["role"] = "ACTIVE"
|
||||
UserFactory.register_permanent(u_full)
|
||||
self.online_users[u['call']] = u_full
|
||||
|
||||
for _ in range(self.config["NUM_USERS"]):
|
||||
new_u = UserFactory.get_or_create_user(self.id, self.online_users.keys())
|
||||
self.users_pool.append(new_u)
|
||||
|
||||
fill = int(self.config["NUM_USERS"] * 0.9)
|
||||
for i in range(fill):
|
||||
u = self.users_pool[i]
|
||||
if u['call'] not in self.online_users:
|
||||
self.online_users[u['call']] = u
|
||||
|
||||
print(f"[*] Channel {self.id} ready: {len(self.online_users)} Users.")
|
||||
self._prefill_history()
|
||||
|
||||
def _prefill_history(self):
|
||||
actives = [u for u in self.online_users.values() if u['role'] == "ACTIVE"]
|
||||
if not actives: return
|
||||
start = datetime.now() - timedelta(minutes=15)
|
||||
for i in range(30):
|
||||
msg_time = start + timedelta(seconds=i*30)
|
||||
ts = str(int(msg_time.timestamp()))
|
||||
sender = random.choice(actives)
|
||||
if i % 2 == 0:
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], sender)
|
||||
frame = f"CH|{self.id}|{ts}|{sender['call']}|{sender['name']}|0|{text}|0|\r\n"
|
||||
else:
|
||||
target = random.choice(list(self.online_users.values()))
|
||||
text = MessageFactory.get_reply_msg(self.config["BANDS"], target['call'], sender['loc'])
|
||||
frame = f"CH|{self.id}|{ts}|{sender['call']}|{sender['name']}|0|{text}|{target['call']}|\r\n"
|
||||
self.history_chat.append(frame)
|
||||
|
||||
def tick(self, now):
|
||||
actives = [u for u in self.online_users.values() if u['role'] == "ACTIVE"]
|
||||
if not actives: return
|
||||
|
||||
# PUBLIC
|
||||
if now - self.last_pub > self.rate_pub:
|
||||
self.last_pub = now
|
||||
u = random.choice(actives)
|
||||
# V31: Nutzt jetzt get_chat_message, das das Freq-Memory abfragt
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], u)
|
||||
ts = str(int(now))
|
||||
frame = f"CH|{self.id}|{ts}|{u['call']}|{u['name']}|0|{text}|0|\r\n"
|
||||
self._add_hist(frame)
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
|
||||
# DIRECTED
|
||||
if now - self.last_dir > self.rate_dir:
|
||||
self.last_dir = now
|
||||
if len(actives) > 5:
|
||||
u1 = random.choice(actives)
|
||||
u2 = random.choice(list(self.online_users.values()))
|
||||
if u1 != u2:
|
||||
if random.random() < 0.5:
|
||||
# Auch hier Frequenzstabilität beachten
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], u1)
|
||||
else:
|
||||
text = MessageFactory.get_reply_msg(self.config["BANDS"], u2['call'], u1['loc'])
|
||||
ts = str(int(now))
|
||||
frame = f"CH|{self.id}|{ts}|{u1['call']}|{u1['name']}|0|{text}|{u2['call']}|\r\n"
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
if u2['role'] != "INACTIVE":
|
||||
threading.Thread(target=self._schedule_reply, args=(u2['call'], u1['call']), daemon=True).start()
|
||||
|
||||
# MSG TO YOU
|
||||
if now - self.last_me > MSG_TO_USER_INTERVAL:
|
||||
self.last_me = now
|
||||
target_client = self.server.get_random_subscriber(self.id)
|
||||
if target_client and actives:
|
||||
if not target_client.call.startswith("GUEST"):
|
||||
sender = random.choice(actives)
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], sender)
|
||||
print(f"[SIM Ch{self.id}] MSG TO YOU ({target_client.call})")
|
||||
self.process_msg(sender['call'], sender['name'], text, target_client.call)
|
||||
|
||||
# LOGIN/LOGOUT
|
||||
if now - self.last_login > LOGIN_LOGOUT_INTERVAL:
|
||||
self.last_login = now
|
||||
if random.choice(['IN', 'OUT']) == 'OUT' and len(self.online_users) > 20:
|
||||
cands = [c for c in self.online_users if c not in [p['call'] for p in self.config["PERMANENT"]]]
|
||||
if cands:
|
||||
l = random.choice(cands)
|
||||
del self.online_users[l]
|
||||
self.server.broadcast_to_channel(self.id, f"UR6|{self.id}|{l}|\r\n")
|
||||
else:
|
||||
candidates = [u for u in self.users_pool if u['call'] not in self.online_users]
|
||||
if candidates:
|
||||
n = random.choice(candidates)
|
||||
self.online_users[n['call']] = n
|
||||
self.server.broadcast_to_channel(self.id, f"UA5|{self.id}|{n['call']}|{n['name']}|{n['loc']}|2|\r\n")
|
||||
|
||||
def process_msg(self, sender, name, text, target):
|
||||
ts = str(int(time.time()))
|
||||
frame = f"CH|{self.id}|{ts}|{sender}|{name}|0|{text}|{target}|\r\n"
|
||||
if target == "0": self._add_hist(frame)
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
if target in self.online_users:
|
||||
threading.Thread(target=self._schedule_reply, args=(target, sender), daemon=True).start()
|
||||
|
||||
def _schedule_reply(self, sim_sender, real_target):
|
||||
if sim_sender not in self.online_users: return
|
||||
u = self.online_users[sim_sender]
|
||||
if u['role'] == "INACTIVE": return
|
||||
|
||||
time.sleep(random.uniform(2.0, 5.0))
|
||||
if sim_sender in self.online_users:
|
||||
text = MessageFactory.get_reply_msg(self.config["BANDS"], real_target, u['loc'])
|
||||
ts = str(int(time.time()))
|
||||
|
||||
if self.server.is_real_user(real_target):
|
||||
print(f"[REPLY Ch{self.id}] {sim_sender} -> {real_target}")
|
||||
|
||||
frame = f"CH|{self.id}|{ts}|{sim_sender}|{u['name']}|0|{text}|{real_target}|\r\n"
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
|
||||
def _add_hist(self, frame):
|
||||
self.history_chat.append(frame)
|
||||
if len(self.history_chat) > 50: self.history_chat.pop(0)
|
||||
|
||||
def get_full_init_blob(self):
|
||||
blob = ""
|
||||
for u in self.online_users.values():
|
||||
blob += f"UA0|{self.id}|{u['call']}|{u['name']}|{u['loc']}|0|\r\n"
|
||||
for h in self.history_chat: blob += h
|
||||
blob += f"UE|{self.id}|{len(self.online_users)}|\r\n"
|
||||
return blob.encode('latin-1', errors='replace')
|
||||
|
||||
# ==========================================
|
||||
# SERVER
|
||||
# ==========================================
|
||||
|
||||
class KSTServerV31:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.running = True
|
||||
self.clients = {}
|
||||
self.channels = {}
|
||||
|
||||
for cid, cfg in CHANNELS_SETUP.items():
|
||||
self.channels[cid] = ChannelInstance(cid, cfg, self)
|
||||
|
||||
def start(self):
|
||||
threading.Thread(target=self._sim_loop, daemon=True).start()
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
s.bind((HOST, PORT))
|
||||
s.listen(5)
|
||||
s.settimeout(1.0)
|
||||
print(f"[*] ON4KST V31 (Stable Frequencies) running on {HOST}:{PORT}")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
sock, addr = s.accept()
|
||||
print(f"[*] CONNECT: {addr}")
|
||||
threading.Thread(target=self._handle_client, args=(sock,), daemon=True).start()
|
||||
except socket.timeout: continue
|
||||
except OSError: break
|
||||
except KeyboardInterrupt:
|
||||
print("\n[!] Stop.")
|
||||
finally:
|
||||
self.running = False
|
||||
try: s.close()
|
||||
except: pass
|
||||
|
||||
def _handle_client(self, sock):
|
||||
client_obj = ConnectedClient(sock, None)
|
||||
with self.lock:
|
||||
self.clients[sock] = client_obj
|
||||
|
||||
buffer = ""
|
||||
try:
|
||||
while self.running:
|
||||
try: data = sock.recv(2048)
|
||||
except: break
|
||||
if not data: break
|
||||
|
||||
buffer += data.decode('latin-1', errors='replace')
|
||||
while '\n' in buffer:
|
||||
line, buffer = buffer.split('\n', 1)
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
|
||||
parts = line.split('|')
|
||||
cmd = parts[0]
|
||||
|
||||
if cmd == 'LOGIN' or cmd == 'LOGINC':
|
||||
if len(parts) > 1:
|
||||
client_obj.call = parts[1].strip().upper()
|
||||
print(f"[LOGIN] {client_obj.call} (Ch 2)")
|
||||
|
||||
client_obj.send_safe(f"LOGSTAT|100|2|PySimV31|KEY|Conf|3|\r\n")
|
||||
if cmd == 'LOGIN':
|
||||
self._send_channel_init(client_obj, "2")
|
||||
|
||||
elif cmd == 'SDONE':
|
||||
self._send_channel_init(client_obj, "2")
|
||||
|
||||
elif cmd.startswith('ACHAT'):
|
||||
if len(parts) >= 2:
|
||||
new_chan = parts[1]
|
||||
if new_chan in self.channels:
|
||||
client_obj.channels.add(new_chan)
|
||||
print(f"[ACHAT] {client_obj.call} -> Ch {new_chan}")
|
||||
self._send_channel_init(client_obj, new_chan)
|
||||
|
||||
elif cmd == 'MSG':
|
||||
if len(parts) >= 4:
|
||||
cid = parts[1]
|
||||
target = parts[2]
|
||||
text = parts[3]
|
||||
if text.lower().startswith("/cq"):
|
||||
spl = text.split(' ', 2)
|
||||
if len(spl) >= 3:
|
||||
target = spl[1]; text = spl[2]
|
||||
if cid in self.channels:
|
||||
self.channels[cid].process_msg(client_obj.call, "Me", text, target)
|
||||
|
||||
elif cmd == 'CK': pass
|
||||
except Exception as e:
|
||||
print(f"[!] Err: {e}")
|
||||
finally:
|
||||
with self.lock:
|
||||
if sock in self.clients: del self.clients[sock]
|
||||
client_obj.close()
|
||||
|
||||
def _send_channel_init(self, client_obj, cid):
|
||||
if cid in self.channels:
|
||||
full_blob = self.channels[cid].get_full_init_blob()
|
||||
client_obj.send_safe(full_blob.decode('latin-1'))
|
||||
|
||||
def broadcast_to_channel(self, cid, frame):
|
||||
now = time.time()
|
||||
with self.lock:
|
||||
targets = list(self.clients.values())
|
||||
|
||||
for c in targets:
|
||||
if cid in c.channels:
|
||||
if now - c.login_time > CLIENT_WARMUP_TIME:
|
||||
c.send_safe(frame)
|
||||
|
||||
def get_random_subscriber(self, cid):
|
||||
with self.lock:
|
||||
subs = [c for c in self.clients.values() if cid in c.channels and not c.call.startswith("GUEST")]
|
||||
return random.choice(subs) if subs else None
|
||||
|
||||
def is_real_user(self, call):
|
||||
with self.lock:
|
||||
for c in self.clients.values():
|
||||
if c.call.upper() == call.upper() and not c.call.startswith("GUEST"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _sim_loop(self):
|
||||
print("[*] Sim Loop running...")
|
||||
last_ka = time.time()
|
||||
while self.running:
|
||||
now = time.time()
|
||||
time.sleep(0.02)
|
||||
|
||||
for c in self.channels.values():
|
||||
c.tick(now)
|
||||
|
||||
if now - last_ka > KEEP_ALIVE_INTERVAL:
|
||||
last_ka = now
|
||||
self.broadcast_global("CK|\r\n")
|
||||
|
||||
def broadcast_global(self, frame):
|
||||
with self.lock:
|
||||
targets = list(self.clients.values())
|
||||
for c in targets:
|
||||
c.send_safe(frame)
|
||||
|
||||
if __name__ == '__main__':
|
||||
KSTServerV31().start()
|
||||
@@ -810,6 +810,24 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** Tracks the last time WE sent a message containing a QRG to a specific callsign (UPPERCASE).
|
||||
* Compared against knownActiveBands.timestampEpoch to decide whose QRG to use in a SKED. */
|
||||
private final Map<String, Long> lastSentQRGToCallsign =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** Call this whenever we send a PM to {@code receiverCallsign} that contains our QRG. */
|
||||
public void recordOutboundQRG(String receiverCallsign) {
|
||||
if (receiverCallsign == null) return;
|
||||
lastSentQRGToCallsign.put(receiverCallsign.trim().toUpperCase(), System.currentTimeMillis());
|
||||
System.out.println("[ChatController] Recorded outbound QRG to: " + receiverCallsign);
|
||||
}
|
||||
|
||||
/** Returns epoch-ms of when we last sent our QRG to this callsign, or 0 if never. */
|
||||
public long getLastSentQRGTimestamp(String callsign) {
|
||||
if (callsign == null) return 0L;
|
||||
return lastSentQRGToCallsign.getOrDefault(callsign.trim().toUpperCase(), 0L);
|
||||
}
|
||||
|
||||
private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
|
||||
private ScheduledExecutorService scoreScheduler;
|
||||
private final StationMetricsService stationMetricsService = new StationMetricsService();
|
||||
@@ -827,8 +845,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
});
|
||||
|
||||
// Push sked to Win-Test via UDP if enabled
|
||||
if (chatPreferences.isLogsynch_wintestNetworkSkedPushEnabled()
|
||||
&& chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
||||
if (chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
||||
pushSkedToWinTest(sked);
|
||||
}
|
||||
}
|
||||
@@ -847,16 +864,69 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this);
|
||||
|
||||
// Get current frequency from QRG property (set by Win-Test STATUS or user)
|
||||
double freqKHz = 144300.0; // fallback default
|
||||
try {
|
||||
String qrgStr = chatPreferences.getMYQRGFirstCat().get();
|
||||
if (qrgStr != null && !qrgStr.isBlank()) {
|
||||
// QRG is in display format like "144.300.00" – strip dots → "14430000" → / 100 → 144300.0 kHz
|
||||
String cleaned = qrgStr.trim().replace(".", "");
|
||||
freqKHz = Double.parseDouble(cleaned) / 100.0;
|
||||
// Frequency resolution:
|
||||
// Compare WHO sent a QRG most recently in the PM conversation:
|
||||
// - OM sent their QRG last → use OM's Last Known QRG (ChatMember.frequency)
|
||||
// - WE sent our QRG last → use our own Win-Test QRG (MYQRG)
|
||||
// Fallback chain if no timestamps exist: OM's Last Known QRG → hardcoded default
|
||||
double freqKHz = -1.0;
|
||||
final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes
|
||||
|
||||
ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign());
|
||||
|
||||
// Collect timestamps: when did the OM last mention their QRG? When did WE last send ours?
|
||||
long omLastQRGTimestamp = 0L;
|
||||
double omLastQRGMhz = 0.0;
|
||||
if (targetMember != null && sked.getBand() != null) {
|
||||
ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand());
|
||||
if (fi != null && fi.frequency > 0
|
||||
&& (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) {
|
||||
omLastQRGTimestamp = fi.timestampEpoch;
|
||||
omLastQRGMhz = fi.frequency;
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
long ourLastQRGTimestamp = getLastSentQRGTimestamp(sked.getTargetCallsign());
|
||||
|
||||
// Decision: who was more recent?
|
||||
if (omLastQRGTimestamp > 0 && omLastQRGTimestamp >= ourLastQRGTimestamp) {
|
||||
// OM mentioned their QRG MORE RECENTLY (or at same time) → use their QRG
|
||||
freqKHz = omLastQRGMhz * 1000.0;
|
||||
System.out.println("[ChatController] SKED freq: OM sent last → "
|
||||
+ omLastQRGMhz + " MHz → " + freqKHz + " kHz");
|
||||
|
||||
} else if (ourLastQRGTimestamp > 0) {
|
||||
// WE sent our QRG more recently → use our Win-Test QRG
|
||||
try {
|
||||
String qrgStr = chatPreferences.getMYQRGFirstCat().get();
|
||||
if (qrgStr != null && !qrgStr.isBlank()) {
|
||||
String cleaned = qrgStr.trim().replace(".", "");
|
||||
double parsed = Double.parseDouble(cleaned) / 100.0;
|
||||
if (parsed > 50000) {
|
||||
freqKHz = parsed;
|
||||
System.out.println("[ChatController] SKED freq: WE sent last → "
|
||||
+ freqKHz + " kHz (raw: " + qrgStr + ")");
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
|
||||
// Fallback A: OM's Last Known QRG from KST field (if no PM QRG exchange found at all)
|
||||
if (freqKHz < 0 && targetMember != null) {
|
||||
try {
|
||||
String memberQrg = targetMember.getFrequency().get();
|
||||
if (memberQrg != null && !memberQrg.isBlank()) {
|
||||
double mhz = Double.parseDouble(memberQrg.trim());
|
||||
freqKHz = mhz * 1000.0;
|
||||
System.out.println("[ChatController] SKED freq: fallback Last Known QRG → "
|
||||
+ mhz + " MHz → " + freqKHz + " kHz");
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
|
||||
// Fallback B: hardcoded default
|
||||
if (freqKHz < 0) {
|
||||
freqKHz = 144300.0;
|
||||
}
|
||||
|
||||
// Build notes string with target locator/azimuth info like reference: [JO02OB - 279°]
|
||||
String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign());
|
||||
@@ -883,6 +953,22 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
}, "WinTestSkedPush").start();
|
||||
}
|
||||
|
||||
private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
||||
synchronized (getLst_chatMemberList()) {
|
||||
for (ChatMember member : getLst_chatMemberList()) {
|
||||
if (member == null || member.getCallSignRaw() == null) continue;
|
||||
if (normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
return null;
|
||||
|
||||
@@ -962,6 +962,13 @@ public class MessageBusManagementThread extends Thread {
|
||||
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
|
||||
this.client.getLst_globalChatMessageList().add(0,newMessageArrived);
|
||||
|
||||
// If our message contained a frequency (e.g. "QRG is: 144.375"), record that
|
||||
// WE sent our QRG to this OM – used by SKED frequency resolution.
|
||||
if (originalMessage != null && newMessageArrived.getReceiver() != null
|
||||
&& originalMessage.matches(".*\\b\\d{3,5}[.,]\\d{1,3}.*")) {
|
||||
this.client.recordOutboundQRG(newMessageArrived.getReceiver().getCallSign());
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
@@ -75,9 +76,10 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
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()));
|
||||
int boundPort = client.getChatPreferences().getLogsynch_wintestNetworkPort();
|
||||
socket.bind(new InetSocketAddress(boundPort));
|
||||
socket.setSoTimeout(3000);
|
||||
System.out.println("[WinTest UDP listener] started at port: " + PORT);
|
||||
System.out.println("[WinTest UDP listener] started at port: " + boundPort);
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
@@ -224,9 +226,43 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
} else {
|
||||
formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback
|
||||
}
|
||||
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
|
||||
// Parse pass frequency from parts[11] if available (WT STATUS format)
|
||||
String formattedPassQRG = null;
|
||||
if (parts.size() > 11) {
|
||||
try {
|
||||
String passFreqRaw = parts.get(11);
|
||||
double passFreqFloat = Integer.parseInt(passFreqRaw) / 10.0;
|
||||
if (passFreqFloat > 100) { // Must be a valid radio frequency (> 100 kHz), protects against parsing boolean flag tokens
|
||||
long passFreqHzTimes100 = Math.round(passFreqFloat * 100.0);
|
||||
String passHzStr = String.valueOf(passFreqHzTimes100);
|
||||
if (passHzStr.length() == 8) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 3), passHzStr.substring(3, 6), passHzStr.substring(6, 8));
|
||||
} else if (passHzStr.length() == 9) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 4), passHzStr.substring(4, 7), passHzStr.substring(7, 9));
|
||||
} else if (passHzStr.length() == 7) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 2), passHzStr.substring(2, 5), passHzStr.substring(5, 7));
|
||||
} else if (passHzStr.length() == 6) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 1), passHzStr.substring(1, 4), passHzStr.substring(4, 6));
|
||||
} else {
|
||||
formattedPassQRG = String.format(Locale.US, "%.1f", passFreqFloat);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// parts[11] not a valid frequency, leave formattedPassQRG as null
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG);
|
||||
if (this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) {
|
||||
final String qrgToSet = (this.client.getChatPreferences().isLogsynch_wintestUsePassQrg() && formattedPassQRG != null)
|
||||
? formattedPassQRG
|
||||
: formattedQRG;
|
||||
// JavaFX StringProperty must be updated on the FX Application Thread
|
||||
Platform.runLater(() -> this.client.getChatPreferences().getMYQRGFirstCat().set(qrgToSet));
|
||||
}
|
||||
|
||||
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG
|
||||
+ (formattedPassQRG != null ? ", passQrg=" + formattedPassQRG : "")
|
||||
+ ", syncActive=" + this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled());
|
||||
} catch (Exception e) {
|
||||
System.out.println("[WinTest] STATUS parsing error: " + e.getMessage());
|
||||
}
|
||||
|
||||
@@ -204,6 +204,8 @@ public class ChatPreferences {
|
||||
String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test
|
||||
boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP
|
||||
String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO
|
||||
boolean logsynch_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet
|
||||
boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet
|
||||
|
||||
|
||||
|
||||
@@ -481,6 +483,22 @@ public class ChatPreferences {
|
||||
this.logsynch_wintestSkedMode = logsynch_wintestSkedMode;
|
||||
}
|
||||
|
||||
public boolean isLogsynch_wintestQrgSyncEnabled() {
|
||||
return logsynch_wintestQrgSyncEnabled;
|
||||
}
|
||||
|
||||
public void setLogsynch_wintestQrgSyncEnabled(boolean logsynch_wintestQrgSyncEnabled) {
|
||||
this.logsynch_wintestQrgSyncEnabled = logsynch_wintestQrgSyncEnabled;
|
||||
}
|
||||
|
||||
public boolean isLogsynch_wintestUsePassQrg() {
|
||||
return logsynch_wintestUsePassQrg;
|
||||
}
|
||||
|
||||
public void setLogsynch_wintestUsePassQrg(boolean logsynch_wintestUsePassQrg) {
|
||||
this.logsynch_wintestUsePassQrg = logsynch_wintestUsePassQrg;
|
||||
}
|
||||
|
||||
public String getStn_loginLocatorSecondCat() {
|
||||
return stn_loginLocatorSecondCat;
|
||||
}
|
||||
@@ -1338,6 +1356,14 @@ public class ChatPreferences {
|
||||
logsynch_wintestSkedMode.setTextContent(this.logsynch_wintestSkedMode);
|
||||
logsynch.appendChild(logsynch_wintestSkedMode);
|
||||
|
||||
Element logsynch_wintestQrgSyncEnabled = doc.createElement("logsynch_wintestQrgSyncEnabled");
|
||||
logsynch_wintestQrgSyncEnabled.setTextContent(this.logsynch_wintestQrgSyncEnabled + "");
|
||||
logsynch.appendChild(logsynch_wintestQrgSyncEnabled);
|
||||
|
||||
Element logsynch_wintestUsePassQrg = doc.createElement("logsynch_wintestUsePassQrg");
|
||||
logsynch_wintestUsePassQrg.setTextContent(this.logsynch_wintestUsePassQrg + "");
|
||||
logsynch.appendChild(logsynch_wintestUsePassQrg);
|
||||
|
||||
|
||||
/**
|
||||
* trxSynchUCX
|
||||
@@ -1912,6 +1938,16 @@ public class ChatPreferences {
|
||||
logsynch_wintestSkedMode,
|
||||
"logsynch_wintestSkedMode");
|
||||
|
||||
logsynch_wintestQrgSyncEnabled = getBoolean(
|
||||
logsynchEl,
|
||||
logsynch_wintestQrgSyncEnabled,
|
||||
"logsynch_wintestQrgSyncEnabled");
|
||||
|
||||
logsynch_wintestUsePassQrg = getBoolean(
|
||||
logsynchEl,
|
||||
logsynch_wintestUsePassQrg,
|
||||
"logsynch_wintestUsePassQrg");
|
||||
|
||||
System.out.println(
|
||||
"[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled);
|
||||
System.out.println(
|
||||
|
||||
@@ -3582,7 +3582,57 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
Menu fileMenu = new Menu("File");
|
||||
|
||||
// create menuitems
|
||||
// build "Connect to <configured chat>" label from saved preferences
|
||||
ChatCategory mainCat = chatcontroller.getChatPreferences().getLoginChatCategoryMain();
|
||||
String connectLabel = "Connect to " + mainCat.getChatCategoryName(mainCat.getCategoryNumber());
|
||||
if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()) {
|
||||
ChatCategory secCat = chatcontroller.getChatPreferences().getLoginChatCategorySecond();
|
||||
if (secCat != null) {
|
||||
connectLabel += " & " + secCat.getChatCategoryName(secCat.getCategoryNumber());
|
||||
}
|
||||
}
|
||||
menuItemFileConnect = new MenuItem(connectLabel);
|
||||
menuItemFileConnect.setDisable(false);
|
||||
|
||||
if (chatcontroller.isConnectedAndLoggedIn() || chatcontroller.isConnectedAndNOTLoggedIn()) {
|
||||
menuItemFileConnect.setDisable(true);
|
||||
}
|
||||
|
||||
menuItemFileConnect.setOnAction(event -> {
|
||||
System.out.println("[Info] File menu: Connect clicked, using saved preferences");
|
||||
|
||||
String call = chatcontroller.getChatPreferences().getStn_loginCallSign();
|
||||
String pass = chatcontroller.getChatPreferences().getStn_loginPassword();
|
||||
|
||||
if (call == null || call.isBlank() || pass == null || pass.isBlank()) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Cannot connect");
|
||||
alert.setHeaderText("Login credentials missing");
|
||||
alert.setContentText("Please configure your callsign and password in Settings first.");
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
chatcontroller.execute();
|
||||
|
||||
menuItemFileConnect.setDisable(true);
|
||||
menuItemFileDisconnect.setDisable(false);
|
||||
menuItemOptionsAwayBack.setDisable(false);
|
||||
menuItemOptionsSetFrequencyAsName.setDisable(false);
|
||||
|
||||
chatcontroller.setConnectedAndLoggedIn(true);
|
||||
chatcontroller.setDisconnected(false);
|
||||
|
||||
} catch (InterruptedException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Connection failed");
|
||||
alert.setContentText("Could not connect: " + e.getMessage());
|
||||
alert.show();
|
||||
}
|
||||
});
|
||||
|
||||
menuItemFileDisconnect = new MenuItem("Disconnect");
|
||||
menuItemFileDisconnect.setDisable(true);
|
||||
|
||||
@@ -3595,6 +3645,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
public void handle(ActionEvent event) {
|
||||
chatcontroller.disconnect(ApplicationConstants.DISCSTRING_DISCONNECTONLY);
|
||||
menuItemFileDisconnect.setDisable(true);
|
||||
menuItemFileConnect.setDisable(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3607,6 +3658,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
});
|
||||
|
||||
// add menu items to menu
|
||||
fileMenu.getItems().add(menuItemFileConnect);
|
||||
fileMenu.getItems().add(menuItemFileDisconnect);
|
||||
fileMenu.getItems().add(m10);
|
||||
|
||||
@@ -4010,6 +4062,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
Scene clusterAndQSOMonScene;
|
||||
Scene settingsScene;
|
||||
|
||||
MenuItem menuItemFileConnect;
|
||||
MenuItem menuItemFileDisconnect;
|
||||
MenuItem menuItemOptionsAwayBack;
|
||||
|
||||
@@ -4170,10 +4223,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
timer_updatePrivatemessageTable.purge();
|
||||
timer_updatePrivatemessageTable.cancel();
|
||||
chatcontroller.disconnect("CLOSEALL");
|
||||
|
||||
try {
|
||||
chatcontroller.disconnect("CLOSEALL");
|
||||
} catch (Exception e) {
|
||||
System.out.println("[Main.java, Warning:] Exception during disconnect: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Platform.exit();
|
||||
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private Queue<Media> musicList = new LinkedList<Media>();
|
||||
@@ -5382,7 +5440,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
FlowPane chatMemberTableFilterQRBHBox = new FlowPane();
|
||||
chatMemberTableFilterQRBHBox.setAlignment(Pos.CENTER_LEFT);
|
||||
chatMemberTableFilterQRBHBox.setHgap(2);
|
||||
chatMemberTableFilterQRBHBox.setPrefWidth(210);
|
||||
chatMemberTableFilterQRBHBox.setPrefWidth(225);
|
||||
|
||||
TextField chatMemberTableFilterMaxQrbTF = new TextField(chatcontroller.getChatPreferences().getStn_maxQRBDefault() + "");
|
||||
chatMemberTableFilterMaxQrbTF.setFocusTraversable(false);
|
||||
@@ -5431,7 +5489,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
// HBox chatMemberTableFilterQTFHBox = new HBox();
|
||||
FlowPane chatMemberTableFilterQTFHBox = new FlowPane();
|
||||
chatMemberTableFilterQTFHBox.setAlignment(Pos.CENTER_LEFT);
|
||||
chatMemberTableFilterQTFHBox.setPrefWidth(490);
|
||||
chatMemberTableFilterQTFHBox.setPrefWidth(525);
|
||||
chatMemberTableFilterQTFHBox.setHgap(2);
|
||||
|
||||
CheckBox chatMemberTableFilterQtfEnableChkbx = new CheckBox("Show only QTF:");
|
||||
@@ -6212,7 +6270,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
*
|
||||
****************************************************************************/
|
||||
settingsStage = new Stage();
|
||||
settingsStage.setTitle("Change Client seetings");
|
||||
settingsStage.setTitle("Change Client Settings");
|
||||
|
||||
BorderPane optionsPanel = new BorderPane();
|
||||
|
||||
@@ -6273,11 +6331,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
});
|
||||
|
||||
boolean isSecondChatEnabled = this.chatcontroller.getChatPreferences().isLoginToSecondChatEnabled();
|
||||
Label lblNameSecondCat = new Label("Name in Chat 2:");
|
||||
lblNameSecondCat.setVisible(false);
|
||||
lblNameSecondCat.setVisible(isSecondChatEnabled);
|
||||
lblNameSecondCat.setDisable(!isSecondChatEnabled);
|
||||
TextField txtFldNameInChatSecondCat = new TextField(this.chatcontroller.getChatPreferences().getStn_loginNameSecondCat());
|
||||
txtFldNameInChatSecondCat.setFocusTraversable(false);
|
||||
txtFldNameInChatSecondCat.setVisible(false);
|
||||
txtFldNameInChatSecondCat.setVisible(isSecondChatEnabled);
|
||||
txtFldNameInChatSecondCat.setDisable(!isSecondChatEnabled);
|
||||
|
||||
txtFldNameInChatSecondCat.textProperty().addListener(new ChangeListener<String>() {
|
||||
|
||||
@@ -6397,11 +6458,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
|
||||
CheckBox station_chkBxEnableSecondChat = new CheckBox("2nd Chat: ");
|
||||
station_chkBxEnableSecondChat.setSelected(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
boolean isSecondChatEnabledForCheckbox = chatcontroller.getChatPreferences().isLoginToSecondChatEnabled();
|
||||
station_chkBxEnableSecondChat.setSelected(isSecondChatEnabledForCheckbox);
|
||||
|
||||
|
||||
|
||||
stn_choiceBxChatChategorySecond.setDisable(true);
|
||||
stn_choiceBxChatChategorySecond.setDisable(!isSecondChatEnabledForCheckbox);
|
||||
station_chkBxEnableSecondChat.selectedProperty().addListener(new ChangeListener<Boolean>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
@@ -6431,12 +6493,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
});
|
||||
|
||||
if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()) {
|
||||
stn_choiceBxChatChategorySecond.setVisible(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
stn_choiceBxChatChategorySecond.setDisable(!chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
txtFldNameInChatSecondCat.setVisible(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
|
||||
}
|
||||
|
||||
TextField txtFldstn_antennaBeamWidthDeg = new TextField(this.chatcontroller.getChatPreferences().getStn_antennaBeamWidthDeg() + "");
|
||||
txtFldstn_antennaBeamWidthDeg.setFocusTraversable(false);
|
||||
@@ -6667,7 +6724,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
grdPnlStation_bands.add(settings_chkbx_QRV3400, 1, 2);
|
||||
grdPnlStation_bands.add(settings_chkbx_QRV5600, 2, 2);
|
||||
grdPnlStation_bands.add(settings_chkbx_QRV10G, 0, 3);
|
||||
grdPnlStation_bands.setMaxWidth(555.0);
|
||||
|
||||
grdPnlStation_bands.setStyle(" -fx-border-color: lightgray;\n" +
|
||||
" -fx-vgap: 5;\n" +
|
||||
@@ -6882,15 +6938,32 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
grdPnlLog.add(lblUDPByWintest, 0, 8);
|
||||
grdPnlLog.add(txtFldUDPPortforWintest, 1, 8);
|
||||
|
||||
// --- Win-Test SKED push settings ---
|
||||
Label lblEnableSkedPush = new Label("Push SKEDs to Win-Test via UDP (ADDSKED)");
|
||||
CheckBox chkBxEnableSkedPush = new CheckBox();
|
||||
chkBxEnableSkedPush.setSelected(
|
||||
this.chatcontroller.getChatPreferences().isLogsynch_wintestNetworkSkedPushEnabled()
|
||||
// --- QRG sync from Win-Test STATUS ---
|
||||
Label lblWtQrgSync = new Label("Win-Test STATUS QRG Sync (updates own QRG from Win-Test transceiver frequency)");
|
||||
CheckBox chkBxWtQrgSync = new CheckBox();
|
||||
chkBxWtQrgSync.setSelected(
|
||||
this.chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()
|
||||
);
|
||||
chkBxEnableSkedPush.selectedProperty().addListener((obs, oldVal, newVal) -> {
|
||||
chatcontroller.getChatPreferences().setLogsynch_wintestNetworkSkedPushEnabled(newVal);
|
||||
System.out.println("[Main.java, Info]: Win-Test SKED push enabled: " + newVal);
|
||||
chkBxWtQrgSync.selectedProperty().addListener((obs, oldVal, newVal) -> {
|
||||
chatcontroller.getChatPreferences().setLogsynch_wintestQrgSyncEnabled(newVal);
|
||||
System.out.println("[Main.java, Info]: Win-Test QRG sync enabled: " + newVal);
|
||||
boolean anyActive = chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled() || newVal;
|
||||
if (!anyActive) {
|
||||
txt_ownqrgMainCategory.textProperty().unbind();
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by hand (watch prefs!)"));
|
||||
} else {
|
||||
txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat());
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)"));
|
||||
}
|
||||
});
|
||||
Label lblWtUsePassQrg = new Label("Use pass frequency from Win-Test STATUS (instead of own QRG)");
|
||||
CheckBox chkBxWtUsePassQrg = new CheckBox();
|
||||
chkBxWtUsePassQrg.setSelected(
|
||||
this.chatcontroller.getChatPreferences().isLogsynch_wintestUsePassQrg()
|
||||
);
|
||||
chkBxWtUsePassQrg.selectedProperty().addListener((obs, oldVal, newVal) -> {
|
||||
chatcontroller.getChatPreferences().setLogsynch_wintestUsePassQrg(newVal);
|
||||
System.out.println("[Main.java, Info]: Win-Test use pass QRG: " + newVal);
|
||||
});
|
||||
|
||||
Label lblWtStationName = new Label("KST station name in Win-Test network (src of SKED packets)");
|
||||
@@ -6935,13 +7008,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
});
|
||||
|
||||
grdPnlLog.add(lblEnableSkedPush, 0, 9);
|
||||
grdPnlLog.add(chkBxEnableSkedPush, 1, 9);
|
||||
|
||||
grdPnlLog.add(lblWtStationName, 0, 11);
|
||||
grdPnlLog.add(txtFldWtStationName, 1, 11);
|
||||
grdPnlLog.add(lblWtStationFilter, 0, 12);
|
||||
grdPnlLog.add(txtFldWtStationFilter, 1, 12);
|
||||
grdPnlLog.add(lblWtStationName, 0, 9);
|
||||
grdPnlLog.add(txtFldWtStationName, 1, 9);
|
||||
|
||||
// Auto-detect subnet broadcast if preference is still the default
|
||||
String currentBroadcast = this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress();
|
||||
@@ -6959,8 +7027,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
// Re-read (may have been auto-detected)
|
||||
txtFldWtBroadcastAddr.setText(this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress());
|
||||
|
||||
grdPnlLog.add(lblWtBroadcastAddr, 0, 13);
|
||||
grdPnlLog.add(txtFldWtBroadcastAddr, 1, 13);
|
||||
grdPnlLog.add(lblWtBroadcastAddr, 0, 10);
|
||||
grdPnlLog.add(txtFldWtBroadcastAddr, 1, 10);
|
||||
|
||||
VBox vbxLog = new VBox();
|
||||
vbxLog.setPadding(new Insets(10, 10, 10, 10));
|
||||
@@ -6995,51 +7063,45 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
chkBxEnableTRXMsgbyUCX.selectedProperty().addListener(new ChangeListener<Boolean>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
// chk2.setSelected(!newValue);
|
||||
if (!newValue) {
|
||||
chatcontroller.getChatPreferences()
|
||||
.setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected());
|
||||
chatcontroller.getChatPreferences().setTrxSynch_ucxLogUDPListenerEnabled(newValue);
|
||||
boolean anyActive = newValue || chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled();
|
||||
if (!anyActive) {
|
||||
txt_ownqrgMainCategory.textProperty().unbind();
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by hand (watch prefs!)"));
|
||||
System.out.println("[Main.java, Info]: MYQRG will be changed only by User input");
|
||||
System.out.println("[Main.java, Info]: setted the trx-frequency updated by ucxlog to: "
|
||||
+ chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled());
|
||||
|
||||
} else {
|
||||
chatcontroller.getChatPreferences()
|
||||
.setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected());
|
||||
txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat());
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)"));
|
||||
System.out.println("[Main.java, Info]: setted the trx-frequency updated by ucxlog to: "
|
||||
+ chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Thats the default behaviour of the myqrg textfield
|
||||
if (this.chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()) {
|
||||
// Unconditionally add listener to manually sync the textfield input to the button
|
||||
// (this listener also fires correctly when the value is updated by the binding)
|
||||
txt_ownqrgMainCategory.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
MYQRGButton.textProperty().set(newValue);
|
||||
});
|
||||
|
||||
// That's the default behaviour of the myqrg textfield
|
||||
if (this.chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled() || this.chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) {
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)"));
|
||||
txt_ownqrgMainCategory.textProperty().bind(this.chatcontroller.getChatPreferences().getMYQRGFirstCat());// TODO: Bind darf nur
|
||||
// gemacht werden, wenn
|
||||
// ucxlog-Frequenznachrichten
|
||||
// ausgewerttet werden!
|
||||
// System.out.println("[Main.java, Info]: MYQRG will be changed only by UCXListener");
|
||||
txt_ownqrgMainCategory.textProperty().bind(this.chatcontroller.getChatPreferences().getMYQRGFirstCat());
|
||||
} else {
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("enter your cq qrg here"));
|
||||
// System.out.println("[Main.java, Info]: MYQRG will be changed only by User input");
|
||||
txt_ownqrgMainCategory.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
System.out.println(
|
||||
"[Main.java, Info]: MYQRG Text changed from " + oldValue + " to " + newValue + " by hand");
|
||||
MYQRGButton.textProperty().set(newValue);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
grdPnltrx.add(generateLabeledSeparator(100, "Receive UCXLog TRX info"), 0, 0, 2, 1);
|
||||
grdPnltrx.add(lblEnableTRXMsgbyUCX, 0, 1);
|
||||
grdPnltrx.add(chkBxEnableTRXMsgbyUCX, 1, 1);
|
||||
|
||||
grdPnltrx.add(generateLabeledSeparator(100, "Win-Test TRX sync"), 0, 2, 2, 1);
|
||||
grdPnltrx.add(lblWtQrgSync, 0, 3);
|
||||
grdPnltrx.add(chkBxWtQrgSync, 1, 3);
|
||||
grdPnltrx.add(lblWtUsePassQrg, 0, 4);
|
||||
grdPnltrx.add(chkBxWtUsePassQrg, 1, 4);
|
||||
grdPnltrx.add(lblWtStationFilter, 0, 5);
|
||||
grdPnltrx.add(txtFldWtStationFilter, 1, 5);
|
||||
|
||||
VBox vbxTRXSynch = new VBox();
|
||||
vbxTRXSynch.setPadding(new Insets(10, 10, 10, 10));
|
||||
vbxTRXSynch.getChildren().addAll(grdPnltrx);
|
||||
@@ -8124,6 +8186,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
else if (chatcontroller.isConnectedAndLoggedIn()) {
|
||||
btnOptionspnlDisconnectOnly.setDisable(false);
|
||||
menuItemFileDisconnect.setDisable(false);
|
||||
menuItemFileConnect.setDisable(true);
|
||||
menuItemOptionsAwayBack.setDisable(false);
|
||||
}
|
||||
|
||||
@@ -8147,13 +8210,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
txtFldstn_maxQRBDefault.setDisable(false);
|
||||
menuItemOptionsSetFrequencyAsName.setDisable(true);
|
||||
menuItemOptionsAwayBack.setDisable(true);
|
||||
menuItemFileConnect.setDisable(false);
|
||||
station_chkBxEnableSecondChat.setDisable(false);
|
||||
stn_choiceBxChatChategorySecond.setDisable(false);
|
||||
}
|
||||
});
|
||||
|
||||
btnOptionspnlConnect = new Button("Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain()
|
||||
.getChatCategoryName(choiceBxChatChategory.getSelectionModel().getSelectedItem().getCategoryNumber()));
|
||||
String btnText = "Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain()
|
||||
.getChatCategoryName(choiceBxChatChategory.getSelectionModel().getSelectedItem().getCategoryNumber());
|
||||
ChatCategory secCat = chatcontroller.getChatPreferences().getLoginChatCategorySecond();
|
||||
if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled() && secCat != null) {
|
||||
btnText += " & " + secCat.getChatCategoryName(secCat.getCategoryNumber());
|
||||
}
|
||||
btnOptionspnlConnect = new Button(btnText);
|
||||
btnOptionspnlConnect.setOnAction(new EventHandler<ActionEvent>() {
|
||||
@Override
|
||||
public void handle(ActionEvent event) {
|
||||
@@ -8185,6 +8254,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
btnOptionspnlDisconnectOnly.setDisable(false);
|
||||
menuItemFileDisconnect.setDisable(false);
|
||||
menuItemFileConnect.setDisable(true);
|
||||
menuItemOptionsAwayBack.setDisable(false);
|
||||
menuItemOptionsSetFrequencyAsName.setDisable(false);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module praktiKST {
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires javafx.web;
|
||||
requires jdk.xml.dom;
|
||||
requires java.sql;
|
||||
requires javafx.media;
|
||||
|
||||
Reference in New Issue
Block a user