1 Commits

Author SHA1 Message Date
Rsclub2_2 a647e7a429 fix Nightly Documentation 2026-03-27 23:14:50 +01:00
28 changed files with 17798 additions and 2241 deletions
-60
View File
@@ -1,60 +0,0 @@
---
name: Bug Report
about: Report a problem with KST4Contest / Ein Problem mit KST4Contest melden
title: "[BUG] "
labels: bug
---
**DE:** Bitte fülle alle Felder so vollständig wie möglich aus. Das hilft, den Fehler schneller zu finden.
**EN:** Please fill in all fields as completely as possible. This helps to find the bug faster.
## Description / Beschreibung
<!-- EN: A clear and concise description of the bug. -->
<!-- DE: Eine klare und präzise Beschreibung des Problems. -->
## Steps to reproduce / Schritte zum Reproduzieren
<!-- EN: Step-by-step instructions to reproduce the bug. -->
<!-- DE: Schritt-für-Schritt-Anleitung, um den Fehler zu reproduzieren. -->
1.
2.
3.
## Expected behaviour / Erwartetes Verhalten
<!-- EN: What did you expect to happen? / DE: Was hätte passieren sollen? -->
## Actual behaviour / Tatsächliches Verhalten
<!-- EN: What actually happened? / DE: Was ist stattdessen passiert? -->
## Log file content / Inhalt der Logdatei
**EN:** Please paste the content of the error log file here. It is written automatically and contains error messages only — no personal data.
**DE:** Bitte füge hier den Inhalt der Fehler-Logdatei ein. Sie wird automatisch geschrieben und enthält nur Fehlermeldungen — keine persönlichen Daten.
| OS | Path / Pfad |
|----|-------------|
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
| Windows | `C:\Users\<YourName>\.praktiKST\kst4contest-errors.log` |
```
Paste log content here / Loginhalt hier einfügen
```
## Version
**KST4Contest version / Version** (e.g. 1.41.0):
**Java version / Java-Version** (`java -version`, e.g. 17.0.9):
**Operating system / Betriebssystem:** <!-- Linux / Windows / macOS -->
**Logging software / Logprogramm** (if applicable / falls relevant, e.g. UCXLog, N1MM+, WinTest):
## Checklist / Checkliste
- [ ] I have attached the log file / Ich habe die Logdatei angehängt
- [ ] I have checked that this issue has not been reported before / Ich habe geprüft, dass dieses Problem noch nicht gemeldet wurde
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
-33
View File
@@ -1,33 +0,0 @@
---
name: Feature Request
about: Suggest a new feature or improvement / Neue Funktion oder Verbesserung vorschlagen
title: "[FEATURE] "
labels: enhancement
---
**DE:** Bitte beschreibe deine Idee so genau wie möglich.
**EN:** Please describe your idea as precisely as possible.
## Summary / Zusammenfassung
<!-- EN: A short summary of the feature you'd like. -->
<!-- DE: Eine kurze Zusammenfassung der gewünschten Funktion. -->
## Motivation / Begründung
<!-- EN: Why would this feature be useful? What problem does it solve? -->
<!-- DE: Warum wäre diese Funktion nützlich? Welches Problem löst sie? -->
## Detailed description / Detaillierte Beschreibung
<!-- EN: Describe the feature in detail. How should it work? -->
<!-- DE: Beschreibe die Funktion im Detail. Wie soll sie funktionieren? -->
## Alternatives considered / Geprüfte Alternativen
<!-- EN: Have you considered any alternative solutions or workarounds? -->
<!-- DE: Hast du alternative Lösungen oder Workarounds in Betracht gezogen? -->
## Checklist / Checkliste
- [ ] I have checked that this feature has not been requested before / Ich habe geprüft, dass diese Funktion noch nicht angefragt wurde
+1 -1
View File
@@ -239,7 +239,7 @@ $endif$
{\fontsize{22}{28}\selectfont\color{white!75!brand-green}pratiKST (ON4KST Chat Client)}\\[2.8cm] {\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] \color{white!40!brand-green}\rule{10cm}{0.6pt}\\[1.8cm]
{\LARGE\bfseries\color{white}$title$}\\[1cm] {\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 \vfill
{\large\color{white}DO5AMF · Marc Fröhlich · DM5M · DN9APW · Philipp Wagner}\\[0.4cm] {\large\color{white}DO5AMF · Marc Fröhlich · DM5M · DN9APW · Philipp Wagner}\\[0.4cm]
{\color{white!70!brand-green}\today}\\[2cm] {\color{white!70!brand-green}\today}\\[2cm]
+17 -396
View File
@@ -11,10 +11,6 @@ on:
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
permissions:
contents: read
packages: write
jobs: jobs:
build-windows-zip: build-windows-zip:
name: Build Windows ZIP name: Build Windows ZIP
@@ -87,7 +83,9 @@ jobs:
run: | run: |
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/') VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
SHORT_SHA="${GITHUB_SHA::7}" SHORT_SHA="${GITHUB_SHA::7}"
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV" 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 - name: Set up Java 17
uses: actions/setup-java@v4.1.0 uses: actions/setup-java@v4.1.0
@@ -108,7 +106,7 @@ jobs:
mkdir -p dist mkdir -p dist
jpackage \ jpackage \
--type app-image \ --type app-image \
--name KST4Contest \ --name praktiKST \
--input target/dist-libs \ --input target/dist-libs \
--main-jar app.jar \ --main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \ --main-class kst4contest.view.Kst4ContestApplication \
@@ -118,416 +116,39 @@ jobs:
- name: Create AppDir metadata - name: Create AppDir metadata
run: | run: |
rm -rf target/KST4Contest.AppDir rm -rf target/praktiKST.AppDir
cp -a dist/KST4Contest target/KST4Contest.AppDir cp -a dist/praktiKST target/praktiKST.AppDir
cat > target/KST4Contest.AppDir/AppRun << 'EOF' cat > target/praktiKST.AppDir/AppRun << 'EOF'
#!/bin/sh #!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")" HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/bin/KST4Contest" "$@" exec "$HERE/bin/praktiKST" "$@"
EOF EOF
chmod +x target/KST4Contest.AppDir/AppRun chmod +x target/praktiKST.AppDir/AppRun
cat > target/KST4Contest.AppDir/KST4Contest.desktop << 'EOF' cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=KST4Contest Name=praktiKST
Exec=KST4Contest Exec=praktiKST
Icon=KST4Contest Icon=praktiKST
Categories=Network;HamRadio; Categories=Network;HamRadio;
Terminal=false Terminal=false
EOF EOF
if [ -f target/KST4Contest.AppDir/lib/KST4Contest.png ]; then if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
cp target/KST4Contest.AppDir/lib/KST4Contest.png target/KST4Contest.AppDir/KST4Contest.png cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
fi fi
- name: Build AppImage - name: Build AppImage
run: | run: |
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x target/appimagetool.AppImage chmod +x target/appimagetool.AppImage
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/KST4Contest.AppDir "dist/${ASSET_BASENAME}-linux-x86_64.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 - name: Upload Linux artifact
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.4
with: with:
name: linux-appimage name: linux-appimage
path: dist/KST4Contest-*-linux-x86_64.AppImage path: dist/praktiKST-*-linux-x86_64.AppImage
retention-days: 14
build-linux-deb:
name: Build Debian package
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 "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends fakeroot
- 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 Debian package
run: |
mkdir -p dist
jpackage \
--type deb \
--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
DEB="$(ls dist/*.deb | head -n 1)"
if [ -z "$DEB" ]; then
echo "No DEB produced by jpackage" && exit 1
fi
mv "$DEB" "dist/${ASSET_BASENAME}-debian-amd64.deb"
- name: Upload Debian artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-debian
path: dist/KST4Contest-*-debian-amd64.deb
retention-days: 14
build-linux-rpm:
name: Build Fedora package
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 "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends rpm
- 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 Fedora package
run: |
mkdir -p dist
jpackage \
--type rpm \
--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
RPM="$(ls dist/*.rpm | head -n 1)"
if [ -z "$RPM" ]; then
echo "No RPM produced by jpackage" && exit 1
fi
mv "$RPM" "dist/${ASSET_BASENAME}-fedora-x86_64.rpm"
- name: Upload Fedora artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-fedora
path: dist/KST4Contest-*-fedora-x86_64.rpm
retention-days: 14
build-linux-arch:
name: Build Arch Linux package
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=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends zstd
- 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 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
- name: Build Arch Linux package artifact
run: |
ARCH=$(uname -m)
PKGVER=$(printf '%s' "${VERSION}-${SHORT_SHA}" | sed 's/[^[:alnum:].+_]/_/g')
PKGROOT="target/archpkg"
rm -rf "$PKGROOT"
mkdir -p "$PKGROOT/usr/lib/KST4Contest" "$PKGROOT/usr/bin"
cp -a dist/KST4Contest/. "$PKGROOT/usr/lib/KST4Contest/"
cat > "$PKGROOT/usr/bin/KST4Contest" << 'EOF'
#!/bin/sh
exec /usr/lib/KST4Contest/bin/KST4Contest "$@"
EOF
chmod 755 "$PKGROOT/usr/bin/KST4Contest"
mkdir -p "$PKGROOT/usr/share/applications" "$PKGROOT/usr/share/icons/hicolor/256x256/apps"
cat > "$PKGROOT/usr/share/applications/KST4Contest.desktop" << 'EOF'
[Desktop Entry]
Type=Application
Name=KST4Contest
Comment=ON4KST Chat Client for VHF/UHF contest operation
Exec=KST4Contest
Icon=KST4Contest
Categories=Network;HamRadio;
Terminal=false
EOF
if [ -f "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" ]; then
cp "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" "$PKGROOT/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
fi
INSTALLED_SIZE=$(du -sk "$PKGROOT" | cut -f1)
cat > "$PKGROOT/.PKGINFO" << EOF
pkgname = kst4contest
pkgbase = kst4contest
pkgver = ${PKGVER}-1
pkgdesc = KST4Contest amateur radio contest logger
url = https://github.com/${{ github.repository }}
builddate = $(date +%s)
packager = GitHub Actions
size = ${INSTALLED_SIZE}
arch = ${ARCH}
license = custom
depend = java-runtime
EOF
tar --zstd -cf "dist/${ASSET_BASENAME}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
- name: Upload Arch Linux artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-arch
path: dist/KST4Contest-*-archlinux-*.pkg.tar.zst
retention-days: 14
build-flatpak:
name: Build Flatpak
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=KST4Contest-${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: Install Flatpak tooling
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
- name: Build app-image with jpackage
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
mkdir -p target/flatpak-src
jpackage \
--type app-image \
--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 target/flatpak-src
- name: Create Flatpak manifest
run: |
mkdir -p dist
cat > target/de.x08.KST4Contest.yml << 'EOF'
app-id: de.x08.KST4Contest
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
command: KST4Contest
finish-args:
- --socket=wayland
- --socket=x11
- --share=network
- --share=ipc
- --device=dri
modules:
- name: kst4contest
buildsystem: simple
build-commands:
- install -d /app/lib/KST4Contest /app/bin /app/share/applications
- cp -a . /app/lib/KST4Contest/
- printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest
- chmod 755 /app/bin/KST4Contest
- echo '[Desktop Entry]' > /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Type=Application' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Name=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Comment=ON4KST Chat Client for VHF/UHF contest operation' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Exec=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Icon=de.x08.KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- printf 'Categories=Network;HamRadio;\n' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Terminal=false' >> /app/share/applications/de.x08.KST4Contest.desktop
- test -f /app/lib/KST4Contest/lib/KST4Contest.png && install -Dm644 /app/lib/KST4Contest/lib/KST4Contest.png /app/share/icons/hicolor/256x256/apps/de.x08.KST4Contest.png || true
sources:
- type: dir
path: flatpak-src/KST4Contest
EOF
- name: Build Flatpak bundle
run: |
flatpak-builder --force-clean target/flatpak-build target/de.x08.KST4Contest.yml
flatpak build-export target/flatpak-repo target/flatpak-build
flatpak build-bundle target/flatpak-repo "dist/${ASSET_BASENAME}-linux-x86_64.flatpak" de.x08.KST4Contest
- name: Upload Flatpak artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-flatpak
path: dist/KST4Contest-*-linux-x86_64.flatpak
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 retention-days: 14
+15 -449
View File
@@ -8,7 +8,6 @@ on:
permissions: permissions:
contents: write contents: write
packages: write
env: env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
@@ -89,7 +88,7 @@ jobs:
mkdir -p dist mkdir -p dist
jpackage \ jpackage \
--type app-image \ --type app-image \
--name KST4Contest \ --name praktiKST \
--input target/dist-libs \ --input target/dist-libs \
--main-jar app.jar \ --main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \ --main-class kst4contest.view.Kst4ContestApplication \
@@ -99,404 +98,41 @@ jobs:
- name: Create AppDir metadata - name: Create AppDir metadata
run: | run: |
rm -rf target/KST4Contest.AppDir rm -rf target/praktiKST.AppDir
cp -a dist/KST4Contest target/KST4Contest.AppDir cp -a dist/praktiKST target/praktiKST.AppDir
cat > target/KST4Contest.AppDir/AppRun << 'EOF' cat > target/praktiKST.AppDir/AppRun << 'EOF'
#!/bin/sh #!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")" HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/bin/KST4Contest" "$@" exec "$HERE/bin/praktiKST" "$@"
EOF EOF
chmod +x target/KST4Contest.AppDir/AppRun chmod +x target/praktiKST.AppDir/AppRun
cat > target/KST4Contest.AppDir/KST4Contest.desktop << 'EOF' cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
[Desktop Entry] [Desktop Entry]
Type=Application Type=Application
Name=KST4Contest Name=praktiKST
Exec=KST4Contest Exec=praktiKST
Icon=KST4Contest Icon=praktiKST
Categories=Network;HamRadio; Categories=Network;HamRadio;
Terminal=false Terminal=false
EOF EOF
if [ -f target/KST4Contest.AppDir/lib/KST4Contest.png ]; then if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
cp target/KST4Contest.AppDir/lib/KST4Contest.png target/KST4Contest.AppDir/KST4Contest.png cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
fi fi
- name: Build AppImage - name: Build AppImage
run: | run: |
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x target/appimagetool.AppImage chmod +x target/appimagetool.AppImage
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/KST4Contest.AppDir dist/KST4Contest-${{ github.ref_name }}-linux-x86_64.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 - name: Upload Linux artifact
uses: actions/upload-artifact@v4.3.4 uses: actions/upload-artifact@v4.3.4
with: with:
name: linux-appimage name: linux-appimage
path: dist/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage path: dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
build-linux-deb:
name: Build Debian package
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: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends fakeroot
- 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 Debian package
run: |
mkdir -p dist
jpackage \
--type deb \
--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
DEB="$(ls dist/*.deb | head -n 1)"
if [ -z "$DEB" ]; then
echo "No DEB produced by jpackage" && exit 1
fi
mv "$DEB" "dist/KST4Contest-${{ github.ref_name }}-debian-amd64.deb"
- name: Upload Debian artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-debian
path: dist/KST4Contest-${{ github.ref_name }}-debian-amd64.deb
build-linux-rpm:
name: Build Fedora package
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: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends rpm
- 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 Fedora package
run: |
mkdir -p dist
jpackage \
--type rpm \
--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
RPM="$(ls dist/*.rpm | head -n 1)"
if [ -z "$RPM" ]; then
echo "No RPM produced by jpackage" && exit 1
fi
mv "$RPM" "dist/KST4Contest-${{ github.ref_name }}-fedora-x86_64.rpm"
- name: Upload Fedora artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-fedora
path: dist/KST4Contest-${{ github.ref_name }}-fedora-x86_64.rpm
build-linux-arch:
name: Build Arch Linux package
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: Install packaging dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends zstd
- 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 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
- name: Build Arch Linux package artifact
run: |
ARCH=$(uname -m)
PKGVER=$(printf '%s' "${{ github.ref_name }}" | sed 's/[^[:alnum:].+_]/_/g')
PKGROOT="target/archpkg"
rm -rf "$PKGROOT"
mkdir -p "$PKGROOT/usr/lib/KST4Contest" "$PKGROOT/usr/bin"
cp -a dist/KST4Contest/. "$PKGROOT/usr/lib/KST4Contest/"
cat > "$PKGROOT/usr/bin/KST4Contest" << 'EOF'
#!/bin/sh
exec /usr/lib/KST4Contest/bin/KST4Contest "$@"
EOF
chmod 755 "$PKGROOT/usr/bin/KST4Contest"
mkdir -p "$PKGROOT/usr/share/applications" "$PKGROOT/usr/share/icons/hicolor/256x256/apps"
cat > "$PKGROOT/usr/share/applications/KST4Contest.desktop" << 'EOF'
[Desktop Entry]
Type=Application
Name=KST4Contest
Comment=ON4KST Chat Client for VHF/UHF contest operation
Exec=KST4Contest
Icon=KST4Contest
Categories=Network;HamRadio;
Terminal=false
EOF
if [ -f "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" ]; then
cp "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" "$PKGROOT/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
fi
INSTALLED_SIZE=$(du -sk "$PKGROOT" | cut -f1)
cat > "$PKGROOT/.PKGINFO" << EOF
pkgname = kst4contest
pkgbase = kst4contest
pkgver = ${PKGVER}-1
pkgdesc = KST4Contest amateur radio contest logger
url = https://github.com/${{ github.repository }}
builddate = $(date +%s)
packager = GitHub Actions
size = ${INSTALLED_SIZE}
arch = ${ARCH}
license = custom
depend = java-runtime
EOF
tar --zstd -cf "dist/KST4Contest-${{ github.ref_name }}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
- name: Upload Arch Linux artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-arch
path: dist/KST4Contest-${{ github.ref_name }}-archlinux-*.pkg.tar.zst
build-flatpak:
name: Build Flatpak
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: Install Flatpak tooling
run: |
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak --user install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
- name: Build app-image with jpackage
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
mkdir -p target/flatpak-src
jpackage \
--type app-image \
--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 target/flatpak-src
- name: Create Flatpak manifest
run: |
mkdir -p dist
cat > target/de.x08.KST4Contest.yml << 'EOF'
app-id: de.x08.KST4Contest
runtime: org.freedesktop.Platform
runtime-version: "24.08"
sdk: org.freedesktop.Sdk
command: KST4Contest
finish-args:
- --socket=wayland
- --socket=x11
- --share=network
- --share=ipc
- --device=dri
modules:
- name: kst4contest
buildsystem: simple
build-commands:
- install -d /app/lib/KST4Contest /app/bin /app/share/applications
- cp -a . /app/lib/KST4Contest/
- printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest
- chmod 755 /app/bin/KST4Contest
- echo '[Desktop Entry]' > /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Type=Application' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Name=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Comment=ON4KST Chat Client for VHF/UHF contest operation' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Exec=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Icon=de.x08.KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
- printf 'Categories=Network;HamRadio;\n' >> /app/share/applications/de.x08.KST4Contest.desktop
- echo 'Terminal=false' >> /app/share/applications/de.x08.KST4Contest.desktop
- test -f /app/lib/KST4Contest/lib/KST4Contest.png && install -Dm644 /app/lib/KST4Contest/lib/KST4Contest.png /app/share/icons/hicolor/256x256/apps/de.x08.KST4Contest.png || true
sources:
- type: dir
path: flatpak-src/KST4Contest
EOF
- name: Import Flatpak signing key
run: |
echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import
FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')
echo "FLATPAK_GPG_KEY_ID=$FLATPAK_GPG_KEY_ID" >> "$GITHUB_ENV"
- name: Build Flatpak repo
run: |
flatpak-builder --force-clean target/flatpak-build target/de.x08.KST4Contest.yml
flatpak build-export --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo target/flatpak-build stable
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo
- name: Create flatpakref
run: |
REPO_NAME="${GITHUB_REPOSITORY#*/}"
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
cat > "dist/de.x08.KST4Contest.flatpakref" << EOF
[Flatpak Ref]
Name=de.x08.KST4Contest
Branch=stable
Title=KST4Contest ON4KST Chat Client
Url=${PAGES_URL}
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
GPGKey=${GPG_KEY_B64}
IsRuntime=false
EOF
- name: Upload flatpakref
uses: actions/upload-artifact@v4.3.4
with:
name: flatpakref
path: dist/de.x08.KST4Contest.flatpakref
- name: Upload Flatpak OSTree repo
uses: actions/upload-artifact@v4.3.4
with:
name: flatpak-ostree-repo
path: target/flatpak-repo/
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: build-docs-pdf:
name: Build Documentation PDF name: Build Documentation PDF
@@ -575,47 +211,13 @@ jobs:
name: docs-pdf name: docs-pdf
path: dist/KST4Contest-${{ github.ref_name }}-manual-*.pdf path: dist/KST4Contest-${{ github.ref_name }}-manual-*.pdf
publish-flatpak-repo:
name: Publish Flatpak OSTree Repo to GitHub Pages
runs-on: ubuntu-latest
needs: build-flatpak
steps:
- name: Download OSTree repo artifact
uses: actions/download-artifact@v4.1.3
with:
name: flatpak-ostree-repo
path: flatpak-ostree-repo/
- name: Download flatpakref
uses: actions/download-artifact@v4.1.3
with:
name: flatpakref
path: flatpak-ostree-repo/
- name: Push to flatpak-repo branch
run: |
cd flatpak-ostree-repo
git init
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git add -A
git commit -m "Flatpak repo: ${{ github.ref_name }}"
git push --force https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:flatpak-repo
release-tag: release-tag:
name: Publish Tagged Release name: Publish Tagged Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs:
- build-windows-zip - build-windows-zip
- build-linux-appimage - build-linux-appimage
- build-linux-deb
- build-linux-rpm
- build-linux-arch
- build-macos-dmg
- build-flatpak
- build-docs-pdf - build-docs-pdf
- publish-flatpak-repo
steps: steps:
- name: Download Windows artifact - name: Download Windows artifact
@@ -630,37 +232,6 @@ jobs:
name: linux-appimage name: linux-appimage
path: release-assets/linux 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 Debian artifact
uses: actions/download-artifact@v4.1.3
with:
name: linux-debian
path: release-assets/debian
- name: Download Fedora artifact
uses: actions/download-artifact@v4.1.3
with:
name: linux-fedora
path: release-assets/fedora
- name: Download Arch Linux artifact
uses: actions/download-artifact@v4.1.3
with:
name: linux-arch
path: release-assets/archlinux
- name: Download flatpakref
uses: actions/download-artifact@v4.1.3
with:
name: flatpakref
path: release-assets/flatpakref
- name: Download PDF manuals - name: Download PDF manuals
uses: actions/download-artifact@v4.1.3 uses: actions/download-artifact@v4.1.3
with: with:
@@ -680,11 +251,6 @@ jobs:
generateReleaseNotes: true generateReleaseNotes: true
artifacts: >- artifacts: >-
release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip, release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,
release-assets/linux/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage, release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage,
release-assets/debian/KST4Contest-${{ github.ref_name }}-debian-amd64.deb,
release-assets/fedora/KST4Contest-${{ github.ref_name }}-fedora-x86_64.rpm,
release-assets/archlinux/KST4Contest-${{ github.ref_name }}-archlinux-*.pkg.tar.zst,
release-assets/flatpakref/de.x08.KST4Contest.flatpakref,
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-en.pdf,
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
+2
View File
@@ -0,0 +1,2 @@
dr2x
oe3cin
+15832
View File
File diff suppressed because it is too large Load Diff
-11
View File
@@ -44,17 +44,6 @@ Der ON4KST-Chat ist der De-facto-Standard für Skeds auf den 144-MHz-und-höher-
- **GitHub**: https://github.com/praktimarc/kst4contest - **GitHub**: https://github.com/praktimarc/kst4contest
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest - **Download**: https://github.com/praktimarc/kst4contest/releases/latest
### Fehler melden (Issue erstellen)
Beim Melden eines Fehlers bitte **immer die Logdatei anhängen**. KST4Contest schreibt automatisch eine Fehler-Logdatei (nur Fehlermeldungen, keine persönlichen Daten):
| Betriebssystem | Pfad zur Logdatei |
|---|---|
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
| Windows | `C:\Users\<Benutzername>\.praktiKST\kst4contest-errors.log` |
Beim Erstellen eines Issues auf GitHub steht eine Vorlage bereit, die alle wichtigen Felder abfragt.
--- ---
## Danksagungen ## Danksagungen
+9 -80
View File
@@ -42,29 +42,11 @@ Der Dateiname hat das Format `praktiKST-v<Versionsnummer>-windows-x64.zip `.
### Linux ### Linux
Mehrere Paketformate stehen auf der Releases-Seite zur Verfügung: Die aktuelle Version kann als AppImage heruntergeladen werden:
**https://github.com/praktimarc/kst4contest/releases/latest** **https://github.com/praktimarc/kst4contest/releases/latest**
| Format | Dateiname | Geeignet für | Der Dateiname hat das Format `praktiKST-v<Versionsnummer>-linux-x86_64.AppImage`.
|---|---|---|
| AppImage | `KST4Contest-v<Version>-linux-x86_64.AppImage` | Alle Distributionen |
| Debian-Paket | `KST4Contest-v<Version>-debian-amd64.deb` | Debian, Ubuntu, Linux Mint, … |
| RPM-Paket | `KST4Contest-v<Version>-fedora-x86_64.rpm` | Fedora, RHEL, openSUSE, … |
| Arch-Paket | `KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst` | Arch Linux, Manjaro, … |
| Flatpak | `de.x08.KST4Contest.flatpakref` | Alle Distributionen mit Flatpak |
> **Empfehlung für Linux:** Die Flatpak-Installation ist der einfachste Weg, immer aktuell zu bleiben `flatpak update` erledigt alle zukünftigen Updates automatisch. Das Repository ist GPG-signiert.
### 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.
--- ---
@@ -80,56 +62,10 @@ Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-macos-<Architektur>.
Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` gespeichert. Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` gespeichert.
### Linux ### Linux
Die Einstellungen werden immer unter `~/.praktikst/preferences.xml` gespeichert.
#### AppImage
1. AppImage herunterladen. 1. AppImage herunterladen.
2. Ausführbar machen: `chmod +x KST4Contest-v<Version>-linux-x86_64.AppImage` 2. AppImage in gewünschten Ordner entpacken.
3. Starten. 3. AppImage ausführbar machen (geht im Terminal mit `chmod +x praktiKST-v<Versionsnummer>-linux-x86_64.AppImage`)
4. AppImage ausführen.
#### Debian / Ubuntu
```bash
sudo apt install ./KST4Contest-v<Version>-debian-amd64.deb
```
Oder die `.deb`-Datei im Dateimanager doppelklicken.
#### Fedora / RHEL
```bash
sudo dnf install ./KST4Contest-v<Version>-fedora-x86_64.rpm
```
#### Arch Linux
```bash
sudo pacman -U KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst
```
#### Flatpak
Die Datei `de.x08.KST4Contest.flatpakref` aus dem [aktuellen Release](https://github.com/praktimarc/kst4contest/releases/latest) herunterladen und öffnen, oder:
```bash
flatpak install de.x08.KST4Contest.flatpakref
```
Oder den Remote manuell hinzufügen:
```bash
flatpak remote-add kst4contest https://praktimarc.github.io/kst4contest/
flatpak install kst4contest de.x08.KST4Contest
```
### 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. Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
@@ -157,17 +93,10 @@ Die Einstellungsdatei (`preferences.xml`) bleibt erhalten, da sie im Benutzerord
#### Linux #### Linux
- **AppImage**: Neues AppImage herunterladen, ausführbar machen (`chmod +x`), altes optional löschen. Derzeit folgendermaßen:
- **Debian/Ubuntu**: `sudo apt install ./KST4Contest-v<Version>-debian-amd64.deb` 1. neues AppImage herunterladen
- **Fedora/RHEL**: `sudo dnf upgrade ./KST4Contest-v<Version>-fedora-x86_64.rpm` 2. neues AppImage ausführbar makieren
- **Arch Linux**: `sudo pacman -U KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst` 3. (optional) altes AppImage löschen.
- **Flatpak (Repository)**: `flatpak update` aktualisiert alle Flatpak-Apps einschließlich KST4Contest.
#### macOS
1. Neue DMG-Datei herunterladen.
2. DMG öffnen.
3. Die neue `KST4Contest.app` in den **Programme**-Ordner ziehen und die alte Version ersetzen.
--- ---
+6 -17
View File
@@ -87,22 +87,16 @@ Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das
**Vorteile der Win-Test Integration:** **Vorteile der Win-Test Integration:**
- Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen. - Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen.
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieser auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet* automatisch, sobald der Listener aktiv ist. - **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*.
- Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden. - Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden.
- **Automatische QRG-Auflösung für SKEDs:** KST4Contest wählt die Sked-Frequenz intelligent:
1. Hat die Gegenstation in einer Chat-Nachricht ihre QRG genannt, wird diese verwendet.
2. Sonst wird die eigene aktuelle QRG verwendet (aus Win-Test STATUS oder manueller Eingabe).
**Einstellungen im Reiter „Log-Synchronisation":** **Notwendige Einstellungen in KST4Contest:**
- `Receive Win-Test network based UDP log messages` aktivieren.
- `UDP-Port for Win-Test listener` (Standard: 9871). - `UDP-Port for Win-Test listener` (Standard: 9871).
- `Receive Win-Test network based UDP log messages` aktivieren.
- `Win-Test sked transmission (push via ADDSKED to Win-Test network)` aktivieren.
- `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST"). - `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST").
- `Win-Test network broadcast address`: Wird i.d.R. automatisch erkannt; erforderlich für das Senden von Sked-Paketen. - `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.
**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:** **Einstellungen in Win-Test:**
- Das Netzwerk in Win-Test muss aktiv sein. - Das Netzwerk in Win-Test muss aktiv sein.
@@ -118,11 +112,6 @@ 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. **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. > **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.
--- ---
-11
View File
@@ -44,17 +44,6 @@ The ON4KST Chat is the de-facto standard for skeds on the 144 MHz and higher ban
- **GitHub**: https://github.com/praktimarc/kst4contest - **GitHub**: https://github.com/praktimarc/kst4contest
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest - **Download**: https://github.com/praktimarc/kst4contest/releases/latest
### Reporting a bug (creating an issue)
When reporting a bug, please **always attach the log file**. KST4Contest automatically writes an error log (error messages only, no personal data):
| Operating system | Log file location |
|---|---|
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
| Windows | `C:\Users\<YourName>\.praktiKST\kst4contest-errors.log` |
When opening an issue on GitHub, a template is provided that guides you through all relevant fields.
--- ---
## Acknowledgements ## Acknowledgements
+9 -80
View File
@@ -42,29 +42,11 @@ The filename has the format `praktiKST-v<version_number>-windows-x64.zip`.
### Linux ### Linux
Multiple package formats are available from the releases page: The latest version can be downloaded as an AppImage:
**https://github.com/praktimarc/kst4contest/releases/latest** **https://github.com/praktimarc/kst4contest/releases/latest**
| Format | Filename | Suitable for | The filename has the format `praktiKST-v<version_number>-linux-x86_64.AppImage`.
|---|---|---|
| AppImage | `KST4Contest-v<version>-linux-x86_64.AppImage` | All distributions |
| Debian package | `KST4Contest-v<version>-debian-amd64.deb` | Debian, Ubuntu, Linux Mint, … |
| RPM package | `KST4Contest-v<version>-fedora-x86_64.rpm` | Fedora, RHEL, openSUSE, … |
| Arch package | `KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst` | Arch Linux, Manjaro, … |
| Flatpak | `de.x08.KST4Contest.flatpakref` | All distributions with Flatpak |
> **Recommended for Linux:** The Flatpak installation is the easiest way to stay up to date — `flatpak update` handles all future updates automatically. The repository is GPG-signed for security.
### 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).
--- ---
@@ -80,56 +62,10 @@ The filename has the format `KST4Contest-v<version_number>-macos-<arch>.dmg`, wh
Settings are stored at `%USERPROFILE%\.praktikst\preferences.xml`. Settings are stored at `%USERPROFILE%\.praktikst\preferences.xml`.
### Linux ### Linux
Settings are always stored at `~/.praktikst/preferences.xml`.
#### AppImage
1. Download the AppImage. 1. Download the AppImage.
2. Make it executable: `chmod +x KST4Contest-v<version>-linux-x86_64.AppImage` 2. Unzip the AppImage into a folder of your choice.
3. Run it. 3. Make the AppImage executable (in the terminal with `chmod +x praktiKST-v<version_number>-linux-x86_64.AppImage`)
4. Run the AppImage.
#### Debian / Ubuntu
```bash
sudo apt install ./KST4Contest-v<version>-debian-amd64.deb
```
Or double-click the `.deb` file in your file manager.
#### Fedora / RHEL
```bash
sudo dnf install ./KST4Contest-v<version>-fedora-x86_64.rpm
```
#### Arch Linux
```bash
sudo pacman -U KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst
```
#### Flatpak
Download `de.x08.KST4Contest.flatpakref` from the [latest release](https://github.com/praktimarc/kst4contest/releases/latest) and open it, or run:
```bash
flatpak install de.x08.KST4Contest.flatpakref
```
Or add the remote manually:
```bash
flatpak remote-add kst4contest https://praktimarc.github.io/kst4contest/
flatpak install kst4contest de.x08.KST4Contest
```
### 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`. Settings are stored at `~/.praktikst/preferences.xml`.
@@ -157,17 +93,10 @@ The settings file (`preferences.xml`) is preserved because it is stored in the u
#### Linux #### Linux
- **AppImage**: Download the new AppImage, make it executable (`chmod +x`), optionally delete the old one. Currently as follows:
- **Debian/Ubuntu**: `sudo apt install ./KST4Contest-v<version>-debian-amd64.deb` 1. Download the new AppImage
- **Fedora/RHEL**: `sudo dnf upgrade ./KST4Contest-v<version>-fedora-x86_64.rpm` 2. Mark the new AppImage as executable
- **Arch Linux**: `sudo pacman -U KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst` 3. (optional) Delete the old AppImage.
- **Flatpak (repository)**: `flatpak update` updates all Flatpak apps including KST4Contest.
#### 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.
--- ---
+7 -18
View File
@@ -87,22 +87,16 @@ Win-Test is supported with a dedicated UDP network listener that understands the
**Advantages of Win-Test Integration:** **Advantages of Win-Test Integration:**
- Automatic QSO synchronization to mark worked stations. - Automatic QSO synchronization to mark worked stations.
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet* automatically, as soon as the listener is active. No separate toggle is needed. - **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*.
- You can choose between "AUTO", "SSB", or "CW" sked modes. - You can choose between "AUTO", "SSB", or "CW" sked modes.
- **Automatic QRG resolution for SKEDs:** KST4Contest selects the sked frequency intelligently:
1. If the other station mentioned their QRG in a recent chat message, that frequency is used.
2. Otherwise, your own current QRG is used (from Win-Test STATUS or manual entry).
**Settings in the "Log Synchronisation" tab:** **Required Settings in KST4Contest:**
- `UDP-Port for Win-Test listener` (Default: 9871).
- Enable `Receive Win-Test network based UDP log messages`. - Enable `Receive Win-Test network based UDP log messages`.
- `UDP-Port for Win-Test listener` (default: 9871). - 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"). - `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. - `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.
**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:** **Settings in Win-Test:**
- The network in Win-Test must be active. - The network in Win-Test must be active.
@@ -118,11 +112,6 @@ 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. **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. > **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
View File
@@ -1,528 +0,0 @@
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()
@@ -9,8 +9,6 @@ import java.net.NoRouteToHostException;
import java.net.SocketException; import java.net.SocketException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import kst4contest.locatorUtils.Location; import kst4contest.locatorUtils.Location;
@@ -19,7 +17,6 @@ import kst4contest.model.ChatMember;
public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask { public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
private static final Logger LOGGER = Logger.getLogger(AirScoutPeriodicalAPReflectionInquirerTask.class.getName());
private ChatController client; private ChatController client;
public AirScoutPeriodicalAPReflectionInquirerTask(ChatController client) { public AirScoutPeriodicalAPReflectionInquirerTask(ChatController client) {
@@ -58,7 +55,7 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign(); ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
} }
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Error parsing callsign", 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 myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
@@ -108,13 +105,13 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
dsocket.send(packet); dsocket.send(packet);
dsocket.close(); dsocket.close();
} catch (UnknownHostException e1) { } catch (UnknownHostException e1) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Unknown host", e1); e1.printStackTrace();
} catch (NoRouteToHostException e) { } catch (NoRouteToHostException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] No route to host", e); e.printStackTrace();
} catch (SocketException e) { } catch (SocketException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] Socket error", e); e.printStackTrace();
} catch (IOException e) { } catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending query", e); e.printStackTrace();
} }
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout); // System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
@@ -139,7 +136,7 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
dsocket.send(packet); dsocket.send(packet);
dsocket.close(); dsocket.close();
} catch (IOException e) { } catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ASPERIODICAL] IO error sending watchlist", e); e.printStackTrace();
} }
// System.out.println("[ASUDPTask, info:] set watchlist: " + asWatchListStringSuffix); // System.out.println("[ASUDPTask, info:] set watchlist: " + asWatchListStringSuffix);
@@ -23,7 +23,6 @@ import kst4contest.locatorUtils.DirectionUtils;
import kst4contest.logic.PriorityCalculator; import kst4contest.logic.PriorityCalculator;
import kst4contest.model.*; import kst4contest.model.*;
import kst4contest.test.MockKstServer; import kst4contest.test.MockKstServer;
import kst4contest.utils.BoundedDequeObservableList;
import kst4contest.utils.PlayAudioUtils; import kst4contest.utils.PlayAudioUtils;
import kst4contest.view.Kst4ContestApplication; import kst4contest.view.Kst4ContestApplication;
@@ -811,24 +810,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw = private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
new java.util.concurrent.ConcurrentHashMap<>(); 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 final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
private ScheduledExecutorService scoreScheduler; private ScheduledExecutorService scoreScheduler;
private final StationMetricsService stationMetricsService = new StationMetricsService(); private final StationMetricsService stationMetricsService = new StationMetricsService();
@@ -846,7 +827,8 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
}); });
// Push sked to Win-Test via UDP if enabled // Push sked to Win-Test via UDP if enabled
if (chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) { if (chatPreferences.isLogsynch_wintestNetworkSkedPushEnabled()
&& chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
pushSkedToWinTest(sked); pushSkedToWinTest(sked);
} }
} }
@@ -865,69 +847,16 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this); WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this);
// Frequency resolution: // Get current frequency from QRG property (set by Win-Test STATUS or user)
// Compare WHO sent a QRG most recently in the PM conversation: double freqKHz = 144300.0; // fallback default
// - OM sent their QRG last → use OM's Last Known QRG (ChatMember.frequency)
// - WE sent our QRG last → use our own Win-Test QRG (MYQRG)
// Fallback chain if no timestamps exist: OM's Last Known QRG → hardcoded default
double freqKHz = -1.0;
final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes
ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign());
// Collect timestamps: when did the OM last mention their QRG? When did WE last send ours?
long omLastQRGTimestamp = 0L;
double omLastQRGMhz = 0.0;
if (targetMember != null && sked.getBand() != null) {
ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand());
if (fi != null && fi.frequency > 0
&& (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) {
omLastQRGTimestamp = fi.timestampEpoch;
omLastQRGMhz = fi.frequency;
}
}
long ourLastQRGTimestamp = getLastSentQRGTimestamp(sked.getTargetCallsign());
// Decision: who was more recent?
if (omLastQRGTimestamp > 0 && omLastQRGTimestamp >= ourLastQRGTimestamp) {
// OM mentioned their QRG MORE RECENTLY (or at same time) → use their QRG
freqKHz = omLastQRGMhz * 1000.0;
System.out.println("[ChatController] SKED freq: OM sent last → "
+ omLastQRGMhz + " MHz → " + freqKHz + " kHz");
} else if (ourLastQRGTimestamp > 0) {
// WE sent our QRG more recently → use our Win-Test QRG
try { try {
String qrgStr = chatPreferences.getMYQRGFirstCat().get(); String qrgStr = chatPreferences.getMYQRGFirstCat().get();
if (qrgStr != null && !qrgStr.isBlank()) { 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(".", ""); String cleaned = qrgStr.trim().replace(".", "");
double parsed = Double.parseDouble(cleaned) / 100.0; freqKHz = 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) { } } 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°] // Build notes string with target locator/azimuth info like reference: [JO02OB - 279°]
String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign()); String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign());
@@ -954,22 +883,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
}, "WinTestSkedPush").start(); }, "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) { private String resolveSkedTargetLocator(String targetCallsignRaw) {
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) { if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
return null; return null;
@@ -1094,8 +1007,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
// ******All abstract types below here are used by the messageprocessor! // ******All abstract types below here are used by the messageprocessor!
// *************** // ***************
private static final int MAX_CHAT_MESSAGES = 10000; private ObservableList<ChatMessage> lst_globalChatMessageList = FXCollections.observableArrayList(); //All chatmessages will be put in there, later create filtered message lists
private final BoundedDequeObservableList<ChatMessage> lst_globalChatMessageList = new BoundedDequeObservableList<>(MAX_CHAT_MESSAGES); //All chatmessages will be put in there, later create filtered message lists
// private ObservableList<ChatMessage> lst_toAllMessageList = FXCollections.observableArrayList(); // directed to all // private ObservableList<ChatMessage> lst_toAllMessageList = FXCollections.observableArrayList(); // directed to all
// (beacon) // (beacon)
private FilteredList<ChatMessage> lst_toAllMessageList = new FilteredList<>(lst_globalChatMessageList); // directed to all private FilteredList<ChatMessage> lst_toAllMessageList = new FilteredList<>(lst_globalChatMessageList); // directed to all
@@ -1240,14 +1152,13 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
this.lst_selectedCallSignInfofilteredMessageList = lst_selectedCallSignInfofilteredMessageList; this.lst_selectedCallSignInfofilteredMessageList = lst_selectedCallSignInfofilteredMessageList;
} }
public void addChatMessage(ChatMessage message) {
lst_globalChatMessageList.addFirst(message);
}
public ObservableList<ChatMessage> getLst_globalChatMessageList() { public ObservableList<ChatMessage> getLst_globalChatMessageList() {
return lst_globalChatMessageList; return lst_globalChatMessageList;
} }
public void setLst_globalChatMessageList(ObservableList<ChatMessage> lst_globalChatMessageList) {
this.lst_globalChatMessageList = lst_globalChatMessageList;
}
public String getHostname() { public String getHostname() {
return hostname; return hostname;
@@ -10,12 +10,9 @@ import java.net.Socket;
import java.util.*; import java.util.*;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DXClusterThreadPooledServer implements Runnable{ public class DXClusterThreadPooledServer implements Runnable{
private static final Logger LOGGER = Logger.getLogger(DXClusterThreadPooledServer.class.getName());
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
private ThreadStatusCallback callBackToController; private ThreadStatusCallback callBackToController;
@@ -114,7 +111,8 @@ public class DXClusterThreadPooledServer implements Runnable{
System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue()); System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue());
System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " "); System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ");
} catch (Exception e) { } catch (Exception e) {
LOGGER.log(Level.SEVERE, "DXCThPooledServer: Error accessing value in chatmember object", e); System.out.println("DXCThPooledServer: Error accessing value in chatmember object: " + e.getMessage());
// e.printStackTrace();
} }
for (Socket socket : clientSockets) { for (Socket socket : clientSockets) {
@@ -148,7 +146,8 @@ public class DXClusterThreadPooledServer implements Runnable{
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage); callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (IOException e) { } catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] broadcasting DXC-message to clients went wrong", e); e.printStackTrace();
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
return false; return false;
} }
} }
@@ -160,7 +159,6 @@ public class DXClusterThreadPooledServer implements Runnable{
class DXClusterServerWorkerRunnable implements Runnable{ class DXClusterServerWorkerRunnable implements Runnable{
private static final Logger LOGGER = Logger.getLogger(DXClusterServerWorkerRunnable.class.getName());
protected Socket clientSocket = null; protected Socket clientSocket = null;
protected String serverText = null; protected String serverText = null;
private ChatController client = null; private ChatController client = null;
@@ -199,13 +197,14 @@ class DXClusterServerWorkerRunnable implements Runnable{
output.write(("\r\n").getBytes()); output.write(("\r\n").getBytes());
} catch (IOException e) { } catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] keep-alive broadcast to client failed", e); e.printStackTrace();
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
dXCkeepAliveTimer.purge(); dXCkeepAliveTimer.purge();
try { try {
socket.close(); socket.close();
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error closing client socket", ex); ex.printStackTrace();
} }
finally { finally {
this.cancel(); this.cancel();
@@ -225,7 +224,7 @@ class DXClusterServerWorkerRunnable implements Runnable{
System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification
} catch (IOException e) { } catch (IOException e) {
LOGGER.log(Level.SEVERE, "[DXClusterSrvr] error in worker runnable", e); e.printStackTrace();
} finally { } finally {
synchronized(dxClusterClientSocketsConnectedList) { synchronized(dxClusterClientSocketsConnectedList) {
dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung
@@ -2,8 +2,6 @@ package kst4contest.controller;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import kst4contest.model.ChatMessage; import kst4contest.model.ChatMessage;
@@ -15,7 +13,6 @@ import kst4contest.model.ChatMessage;
* No need for it as it´s not longer a console application * No need for it as it´s not longer a console application
*/ */
public class InputReaderThread extends Thread { public class InputReaderThread extends Thread {
private static final Logger LOGGER = Logger.getLogger(InputReaderThread.class.getName());
private PrintWriter writer; private PrintWriter writer;
private Socket socket; private Socket socket;
private ChatController client; private ChatController client;
@@ -42,7 +39,8 @@ public class InputReaderThread extends Thread {
try { try {
sendThisMessage23001 = reader.readLine(); sendThisMessage23001 = reader.readLine();
} catch (IOException e) { } catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error reading from stdin", e); // TODO Auto-generated catch block
e.printStackTrace();
} }
ownMSG.setMessageText("MSG|" + this.client.getChatCategoryMain().getCategoryNumber() + "|0|" + sendThisMessage23001 + "|0|"); ownMSG.setMessageText("MSG|" + this.client.getChatCategoryMain().getCategoryNumber() + "|0|" + sendThisMessage23001 + "|0|");
@@ -55,8 +53,8 @@ public class InputReaderThread extends Thread {
try { try {
this.sleep(500); this.sleep(500);
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOGGER.log(Level.SEVERE, "InputReaderThread interrupted", e); // TODO Auto-generated catch block
Thread.currentThread().interrupt(); e.printStackTrace();
} }
} }
@@ -772,7 +772,7 @@ public class MessageBusManagementThread extends Thread {
dummy.setCallSign("ALL"); dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy); newMessageArrived.setReceiver(dummy);
this.client.addChatMessage(newMessageArrived); // sdtout to all message-List this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
} else { } else {
//message is directed to another chatmember, process as such! //message is directed to another chatmember, process as such!
@@ -817,7 +817,7 @@ public class MessageBusManagementThread extends Thread {
if (newMessageArrived.getReceiver().getCallSign() if (newMessageArrived.getReceiver().getCallSign()
.equals(this.client.getChatPreferences().getStn_loginCallSign())) { .equals(this.client.getChatPreferences().getStn_loginCallSign())) {
this.client.addChatMessage(newMessageArrived); this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
if (this.client.getChatPreferences().isNotify_playSimpleSounds()) { if (this.client.getChatPreferences().isNotify_playSimpleSounds()) {
this.client.getPlayAudioUtils().playNoiseLauncher('P'); this.client.getPlayAudioUtils().playNoiseLauncher('P');
@@ -960,14 +960,7 @@ public class MessageBusManagementThread extends Thread {
String originalMessage = newMessageArrived.getMessageText(); String originalMessage = newMessageArrived.getMessageText();
newMessageArrived newMessageArrived
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage); .setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
this.client.addChatMessage(newMessageArrived); 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 // 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 // the "to me message list" with modified messagetext, added rxers callsign
@@ -1031,7 +1024,7 @@ public class MessageBusManagementThread extends Thread {
newMessageArrived.getSender().setInAngleAndRange(false); newMessageArrived.getSender().setInAngleAndRange(false);
} }
this.client.addChatMessage(newMessageArrived); this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign()); // System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
} }
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) { } catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
@@ -1371,7 +1364,7 @@ public class MessageBusManagementThread extends Thread {
dummy.setCallSign("ALL"); dummy.setCallSign("ALL");
newMessageArrived.setReceiver(dummy); newMessageArrived.setReceiver(dummy);
this.client.addChatMessage(newMessageArrived); // sdtout to all message-List this.client.getLst_globalChatMessageList().add(0, newMessageArrived); // sdtout to all message-List
} else { } else {
//message is directed to another chatmember, process as such! //message is directed to another chatmember, process as such!
@@ -1415,7 +1408,7 @@ public class MessageBusManagementThread extends Thread {
if (newMessageArrived.getReceiver().getCallSign() if (newMessageArrived.getReceiver().getCallSign()
.equals(this.client.getChatPreferences().getStn_loginCallSign())) { .equals(this.client.getChatPreferences().getStn_loginCallSign())) {
this.client.addChatMessage(newMessageArrived); this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
System.out.println("Historic message directed to me: " + newMessageArrived.getReceiver().getCallSign() + "."); System.out.println("Historic message directed to me: " + newMessageArrived.getReceiver().getCallSign() + ".");
@@ -1428,7 +1421,7 @@ public class MessageBusManagementThread extends Thread {
String originalMessage = newMessageArrived.getMessageText(); String originalMessage = newMessageArrived.getMessageText();
newMessageArrived newMessageArrived
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage); .setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
this.client.addChatMessage(newMessageArrived); this.client.getLst_globalChatMessageList().add(0,newMessageArrived);
// if you sent the message to another station, it will be sorted in to // 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 // the "to me message list" with modified messagetext, added rxers callsign
@@ -1448,7 +1441,7 @@ public class MessageBusManagementThread extends Thread {
newMessageArrived.getSender().setInAngleAndRange(false); newMessageArrived.getSender().setInAngleAndRange(false);
} }
this.client.addChatMessage(newMessageArrived); this.client.getLst_globalChatMessageList().add(0, newMessageArrived);
// System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign()); // System.out.println("MSGBS bgfx: tx call = " + newMessageArrived.getSender().getCallSign() + " / rx call = " + newMessageArrived.getReceiver().getCallSign());
} }
} catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) { } catch (NullPointerException referenceDeletedByUserLeftChatDuringMessageprocessing) {
@@ -1521,7 +1514,7 @@ public class MessageBusManagementThread extends Thread {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
client.addChatMessage(pwErrorMsg); client.getLst_globalChatMessageList().add(pwErrorMsg);
// client.getLst_toMeMessageList().add(pwErrorMsg); // client.getLst_toMeMessageList().add(pwErrorMsg);
// client.getLst_toAllMessageList().add(pwErrorMsg); // client.getLst_toAllMessageList().add(pwErrorMsg);
} }
@@ -3,8 +3,6 @@ package kst4contest.controller;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
import kst4contest.model.ChatMessage; import kst4contest.model.ChatMessage;
@@ -16,7 +14,6 @@ import kst4contest.model.ChatMessage;
* @author www.codejava.net * @author www.codejava.net
*/ */
public class ReadThread extends Thread { public class ReadThread extends Thread {
private static final Logger LOGGER = Logger.getLogger(ReadThread.class.getName());
private BufferedReader reader; private BufferedReader reader;
private Socket socket; private Socket socket;
private ChatController client; private ChatController client;
@@ -46,7 +43,8 @@ public class ReadThread extends Thread {
reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8)); reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Error getting input stream", ex); System.out.println("Error getting input stream: " + ex.getMessage());
ex.printStackTrace();
} }
} }
@@ -84,14 +82,15 @@ public class ReadThread extends Thread {
} }
catch (Exception sexc) { catch (Exception sexc) {
LOGGER.log(Level.SEVERE, "[ReadThread] Socket closed unexpectedly", sexc); System.out.println("[ReadThread, CRITICAL: ] Socket geschlossen: " + sexc.getMessage());
try { try {
this.client.getSocket().close(); this.client.getSocket().close();
this.interrupt(); this.interrupt();
break; break;
} catch (IOException e) { } catch (IOException e) {
LOGGER.log(Level.SEVERE, "[ReadThread] Error closing socket", e); // TODO Auto-generated catch block
e.printStackTrace();
} }
} }
@@ -1,6 +1,5 @@
package kst4contest.controller; package kst4contest.controller;
import javafx.application.Platform;
import kst4contest.ApplicationConstants; import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMember; import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage; import kst4contest.model.ThreadStateMessage;
@@ -76,10 +75,9 @@ public class ReadUDPByWintestThread extends Thread {
socket = new DatagramSocket(null); //first init with null, then make ready for reuse socket = new DatagramSocket(null); //first init with null, then make ready for reuse
socket.setReuseAddress(true); socket.setReuseAddress(true);
// socket = new DatagramSocket(PORT); // socket = new DatagramSocket(PORT);
int boundPort = client.getChatPreferences().getLogsynch_wintestNetworkPort(); socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_wintestNetworkPort()));
socket.bind(new InetSocketAddress(boundPort));
socket.setSoTimeout(3000); socket.setSoTimeout(3000);
System.out.println("[WinTest UDP listener] started at port: " + boundPort); System.out.println("[WinTest UDP listener] started at port: " + PORT);
} catch (SocketException e) { } catch (SocketException e) {
e.printStackTrace(); e.printStackTrace();
return; return;
@@ -226,43 +224,9 @@ public class ReadUDPByWintestThread extends Thread {
} else { } else {
formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback
} }
// Parse pass frequency from parts[11] if available (WT STATUS format) this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
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
}
}
if (this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) { System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG);
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) { } catch (Exception e) {
System.out.println("[WinTest] STATUS parsing error: " + e.getMessage()); System.out.println("[WinTest] STATUS parsing error: " + e.getMessage());
} }
@@ -204,8 +204,6 @@ public class ChatPreferences {
String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test
boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP
String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO String logsynch_wintestSkedMode = "SSB"; // 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
@@ -483,22 +481,6 @@ public class ChatPreferences {
this.logsynch_wintestSkedMode = logsynch_wintestSkedMode; 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() { public String getStn_loginLocatorSecondCat() {
return stn_loginLocatorSecondCat; return stn_loginLocatorSecondCat;
} }
@@ -1356,14 +1338,6 @@ public class ChatPreferences {
logsynch_wintestSkedMode.setTextContent(this.logsynch_wintestSkedMode); logsynch_wintestSkedMode.setTextContent(this.logsynch_wintestSkedMode);
logsynch.appendChild(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 * trxSynchUCX
@@ -1938,16 +1912,6 @@ public class ChatPreferences {
logsynch_wintestSkedMode, logsynch_wintestSkedMode,
"logsynch_wintestSkedMode"); "logsynch_wintestSkedMode");
logsynch_wintestQrgSyncEnabled = getBoolean(
logsynchEl,
logsynch_wintestQrgSyncEnabled,
"logsynch_wintestQrgSyncEnabled");
logsynch_wintestUsePassQrg = getBoolean(
logsynchEl,
logsynch_wintestUsePassQrg,
"logsynch_wintestUsePassQrg");
System.out.println( System.out.println(
"[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled); "[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled);
System.out.println( System.out.println(
@@ -7,16 +7,12 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;
/** /**
* This class has utility methods to handle application files inside the home directory. * This class has utility methods to handle application files inside the home directory.
*/ */
public class ApplicationFileUtils { public class ApplicationFileUtils {
private static final Logger LOGGER = Logger.getLogger(ApplicationFileUtils.class.getName());
/** /**
* Gets the path of a file inside the home directory of the user. * Gets the path of a file inside the home directory of the user.
* @param applicationName Name off the application which is used for the hidden directory * @param applicationName Name off the application which is used for the hidden directory
@@ -65,7 +61,8 @@ public class ApplicationFileUtils {
resourceStream.transferTo(fileOutputStream); resourceStream.transferTo(fileOutputStream);
} catch (IOException ex) { } catch (IOException ex) {
LOGGER.log(Level.SEVERE, "Exception when copying Application file: " + ex.getMessage(), ex); System.err.println("Exception when copying Application file: " + ex.getMessage());
ex.printStackTrace(System.err);
} }
} }
@@ -1,168 +0,0 @@
package kst4contest.utils;
import javafx.collections.ObservableListBase;
import java.util.Arrays;
/**
* A bounded ObservableList backed by a circular buffer (ring buffer).
* <p>
* Provides O(1) {@link #addFirst} and {@link #addLast} as well as O(1)
* random access via {@link #get}. When the list reaches {@code maxCapacity},
* adding a new element at the front automatically evicts the oldest element
* at the back — and vice versa.
* <p>
* This is a drop-in replacement for {@code FXCollections.observableArrayList()}
* wherever elements are prepended frequently, e.g. chat message lists.
*/
public class BoundedDequeObservableList<E> extends ObservableListBase<E> {
private final int maxCapacity;
private final Object[] elements;
private int head = 0;
private int size = 0;
public BoundedDequeObservableList(int maxCapacity) {
if (maxCapacity <= 0) throw new IllegalArgumentException("maxCapacity must be > 0");
this.maxCapacity = maxCapacity;
this.elements = new Object[maxCapacity];
}
// ── read access ──────────────────────────────────────────────────────────
@Override
public int size() {
return size;
}
@Override
@SuppressWarnings("unchecked")
public E get(int index) {
checkIndex(index);
return (E) elements[physicalIndex(index)];
}
// ── O(1) deque operations ─────────────────────────────────────────────────
/**
* Inserts {@code element} at index 0 (newest-first order).
* If the list is already at capacity the oldest element (last index) is
* removed first — both changes are reported as a single compound change.
*/
public void addFirst(E element) {
beginChange();
if (size == maxCapacity) {
// evict last element
int lastPhysical = physicalIndex(size - 1);
@SuppressWarnings("unchecked")
E evicted = (E) elements[lastPhysical];
elements[lastPhysical] = null;
size--;
nextRemove(size, evicted); // index after decrement == old last index
}
head = (head - 1 + maxCapacity) % maxCapacity;
elements[head] = element;
size++;
nextAdd(0, 1);
endChange();
}
/**
* Appends {@code element} at the last index (oldest-first order).
* If the list is already at capacity the newest element (index 0) is
* removed first.
*/
public void addLast(E element) {
beginChange();
if (size == maxCapacity) {
// evict first element
@SuppressWarnings("unchecked")
E evicted = (E) elements[head];
elements[head] = null;
head = (head + 1) % maxCapacity;
size--;
nextRemove(0, evicted);
}
elements[physicalIndex(size)] = element;
size++;
nextAdd(size - 1, size);
endChange();
}
// ── standard List mutation (O(n) — use addFirst/addLast for hot path) ─────
@Override
public void add(int index, E element) {
if (index == 0) {
addFirst(element);
return;
}
if (index == size) {
addLast(element);
return;
}
checkIndexForAdd(index);
beginChange();
if (size == maxCapacity) {
int lastPhysical = physicalIndex(size - 1);
@SuppressWarnings("unchecked")
E evicted = (E) elements[lastPhysical];
elements[lastPhysical] = null;
size--;
nextRemove(size, evicted);
}
// shift elements [index .. size-1] one position towards the end
for (int i = size; i > index; i--) {
elements[physicalIndex(i)] = elements[physicalIndex(i - 1)];
}
elements[physicalIndex(index)] = element;
size++;
nextAdd(index, index + 1);
endChange();
}
@Override
public E remove(int index) {
checkIndex(index);
beginChange();
@SuppressWarnings("unchecked")
E removed = (E) elements[physicalIndex(index)];
// shift elements [index+1 .. size-1] one position towards the front
for (int i = index; i < size - 1; i++) {
elements[physicalIndex(i)] = elements[physicalIndex(i + 1)];
}
elements[physicalIndex(size - 1)] = null;
size--;
nextRemove(index, removed);
endChange();
return removed;
}
@Override
public E set(int index, E element) {
checkIndex(index);
beginChange();
@SuppressWarnings("unchecked")
E old = (E) elements[physicalIndex(index)];
elements[physicalIndex(index)] = element;
nextSet(index, old);
endChange();
return old;
}
// ── helpers ───────────────────────────────────────────────────────────────
private int physicalIndex(int virtualIndex) {
return (head + virtualIndex) % maxCapacity;
}
private void checkIndex(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
private void checkIndexForAdd(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size);
}
}
@@ -3,12 +3,7 @@ package kst4contest.view;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.logging.FileHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -3587,57 +3582,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
Menu fileMenu = new Menu("File"); Menu fileMenu = new Menu("File");
// build "Connect to <configured chat>" label from saved preferences // create menuitems
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 = new MenuItem("Disconnect");
menuItemFileDisconnect.setDisable(true); menuItemFileDisconnect.setDisable(true);
@@ -3650,7 +3595,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
public void handle(ActionEvent event) { public void handle(ActionEvent event) {
chatcontroller.disconnect(ApplicationConstants.DISCSTRING_DISCONNECTONLY); chatcontroller.disconnect(ApplicationConstants.DISCSTRING_DISCONNECTONLY);
menuItemFileDisconnect.setDisable(true); menuItemFileDisconnect.setDisable(true);
menuItemFileConnect.setDisable(false);
} }
}); });
@@ -3663,7 +3607,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
}); });
// add menu items to menu // add menu items to menu
fileMenu.getItems().add(menuItemFileConnect);
fileMenu.getItems().add(menuItemFileDisconnect); fileMenu.getItems().add(menuItemFileDisconnect);
fileMenu.getItems().add(m10); fileMenu.getItems().add(m10);
@@ -4067,7 +4010,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
Scene clusterAndQSOMonScene; Scene clusterAndQSOMonScene;
Scene settingsScene; Scene settingsScene;
MenuItem menuItemFileConnect;
MenuItem menuItemFileDisconnect; MenuItem menuItemFileDisconnect;
MenuItem menuItemOptionsAwayBack; MenuItem menuItemOptionsAwayBack;
@@ -4228,15 +4170,10 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
timer_updatePrivatemessageTable.purge(); timer_updatePrivatemessageTable.purge();
timer_updatePrivatemessageTable.cancel(); timer_updatePrivatemessageTable.cancel();
try {
chatcontroller.disconnect("CLOSEALL"); chatcontroller.disconnect("CLOSEALL");
} catch (Exception e) {
System.out.println("[Main.java, Warning:] Exception during disconnect: " + e.getMessage());
}
// Platform.exit(); // Platform.exit();
System.exit(0);
} }
private Queue<Media> musicList = new LinkedList<Media>(); private Queue<Media> musicList = new LinkedList<Media>();
@@ -5445,7 +5382,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
FlowPane chatMemberTableFilterQRBHBox = new FlowPane(); FlowPane chatMemberTableFilterQRBHBox = new FlowPane();
chatMemberTableFilterQRBHBox.setAlignment(Pos.CENTER_LEFT); chatMemberTableFilterQRBHBox.setAlignment(Pos.CENTER_LEFT);
chatMemberTableFilterQRBHBox.setHgap(2); chatMemberTableFilterQRBHBox.setHgap(2);
chatMemberTableFilterQRBHBox.setPrefWidth(225); chatMemberTableFilterQRBHBox.setPrefWidth(210);
TextField chatMemberTableFilterMaxQrbTF = new TextField(chatcontroller.getChatPreferences().getStn_maxQRBDefault() + ""); TextField chatMemberTableFilterMaxQrbTF = new TextField(chatcontroller.getChatPreferences().getStn_maxQRBDefault() + "");
chatMemberTableFilterMaxQrbTF.setFocusTraversable(false); chatMemberTableFilterMaxQrbTF.setFocusTraversable(false);
@@ -5494,7 +5431,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
// HBox chatMemberTableFilterQTFHBox = new HBox(); // HBox chatMemberTableFilterQTFHBox = new HBox();
FlowPane chatMemberTableFilterQTFHBox = new FlowPane(); FlowPane chatMemberTableFilterQTFHBox = new FlowPane();
chatMemberTableFilterQTFHBox.setAlignment(Pos.CENTER_LEFT); chatMemberTableFilterQTFHBox.setAlignment(Pos.CENTER_LEFT);
chatMemberTableFilterQTFHBox.setPrefWidth(525); chatMemberTableFilterQTFHBox.setPrefWidth(490);
chatMemberTableFilterQTFHBox.setHgap(2); chatMemberTableFilterQTFHBox.setHgap(2);
CheckBox chatMemberTableFilterQtfEnableChkbx = new CheckBox("Show only QTF:"); CheckBox chatMemberTableFilterQtfEnableChkbx = new CheckBox("Show only QTF:");
@@ -6275,7 +6212,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
* *
****************************************************************************/ ****************************************************************************/
settingsStage = new Stage(); settingsStage = new Stage();
settingsStage.setTitle("Change Client Settings"); settingsStage.setTitle("Change Client seetings");
BorderPane optionsPanel = new BorderPane(); BorderPane optionsPanel = new BorderPane();
@@ -6336,14 +6273,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
} }
}); });
boolean isSecondChatEnabled = this.chatcontroller.getChatPreferences().isLoginToSecondChatEnabled();
Label lblNameSecondCat = new Label("Name in Chat 2:"); Label lblNameSecondCat = new Label("Name in Chat 2:");
lblNameSecondCat.setVisible(isSecondChatEnabled); lblNameSecondCat.setVisible(false);
lblNameSecondCat.setDisable(!isSecondChatEnabled);
TextField txtFldNameInChatSecondCat = new TextField(this.chatcontroller.getChatPreferences().getStn_loginNameSecondCat()); TextField txtFldNameInChatSecondCat = new TextField(this.chatcontroller.getChatPreferences().getStn_loginNameSecondCat());
txtFldNameInChatSecondCat.setFocusTraversable(false); txtFldNameInChatSecondCat.setFocusTraversable(false);
txtFldNameInChatSecondCat.setVisible(isSecondChatEnabled); txtFldNameInChatSecondCat.setVisible(false);
txtFldNameInChatSecondCat.setDisable(!isSecondChatEnabled);
txtFldNameInChatSecondCat.textProperty().addListener(new ChangeListener<String>() { txtFldNameInChatSecondCat.textProperty().addListener(new ChangeListener<String>() {
@@ -6463,12 +6397,11 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
CheckBox station_chkBxEnableSecondChat = new CheckBox("2nd Chat: "); CheckBox station_chkBxEnableSecondChat = new CheckBox("2nd Chat: ");
boolean isSecondChatEnabledForCheckbox = chatcontroller.getChatPreferences().isLoginToSecondChatEnabled(); station_chkBxEnableSecondChat.setSelected(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
station_chkBxEnableSecondChat.setSelected(isSecondChatEnabledForCheckbox);
stn_choiceBxChatChategorySecond.setDisable(!isSecondChatEnabledForCheckbox); stn_choiceBxChatChategorySecond.setDisable(true);
station_chkBxEnableSecondChat.selectedProperty().addListener(new ChangeListener<Boolean>() { station_chkBxEnableSecondChat.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override @Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
@@ -6498,7 +6431,12 @@ 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() + ""); TextField txtFldstn_antennaBeamWidthDeg = new TextField(this.chatcontroller.getChatPreferences().getStn_antennaBeamWidthDeg() + "");
txtFldstn_antennaBeamWidthDeg.setFocusTraversable(false); txtFldstn_antennaBeamWidthDeg.setFocusTraversable(false);
@@ -6729,6 +6667,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
grdPnlStation_bands.add(settings_chkbx_QRV3400, 1, 2); grdPnlStation_bands.add(settings_chkbx_QRV3400, 1, 2);
grdPnlStation_bands.add(settings_chkbx_QRV5600, 2, 2); grdPnlStation_bands.add(settings_chkbx_QRV5600, 2, 2);
grdPnlStation_bands.add(settings_chkbx_QRV10G, 0, 3); grdPnlStation_bands.add(settings_chkbx_QRV10G, 0, 3);
grdPnlStation_bands.setMaxWidth(555.0);
grdPnlStation_bands.setStyle(" -fx-border-color: lightgray;\n" + grdPnlStation_bands.setStyle(" -fx-border-color: lightgray;\n" +
" -fx-vgap: 5;\n" + " -fx-vgap: 5;\n" +
@@ -6943,32 +6882,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
grdPnlLog.add(lblUDPByWintest, 0, 8); grdPnlLog.add(lblUDPByWintest, 0, 8);
grdPnlLog.add(txtFldUDPPortforWintest, 1, 8); grdPnlLog.add(txtFldUDPPortforWintest, 1, 8);
// --- QRG sync from Win-Test STATUS --- // --- Win-Test SKED push settings ---
Label lblWtQrgSync = new Label("Win-Test STATUS QRG Sync (updates own QRG from Win-Test transceiver frequency)"); Label lblEnableSkedPush = new Label("Push SKEDs to Win-Test via UDP (ADDSKED)");
CheckBox chkBxWtQrgSync = new CheckBox(); CheckBox chkBxEnableSkedPush = new CheckBox();
chkBxWtQrgSync.setSelected( chkBxEnableSkedPush.setSelected(
this.chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled() this.chatcontroller.getChatPreferences().isLogsynch_wintestNetworkSkedPushEnabled()
); );
chkBxWtQrgSync.selectedProperty().addListener((obs, oldVal, newVal) -> { chkBxEnableSkedPush.selectedProperty().addListener((obs, oldVal, newVal) -> {
chatcontroller.getChatPreferences().setLogsynch_wintestQrgSyncEnabled(newVal); chatcontroller.getChatPreferences().setLogsynch_wintestNetworkSkedPushEnabled(newVal);
System.out.println("[Main.java, Info]: Win-Test QRG sync enabled: " + newVal); System.out.println("[Main.java, Info]: Win-Test SKED push 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)"); Label lblWtStationName = new Label("KST station name in Win-Test network (src of SKED packets)");
@@ -7013,8 +6935,13 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
} }
}); });
grdPnlLog.add(lblWtStationName, 0, 9); grdPnlLog.add(lblEnableSkedPush, 0, 9);
grdPnlLog.add(txtFldWtStationName, 1, 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);
// Auto-detect subnet broadcast if preference is still the default // Auto-detect subnet broadcast if preference is still the default
String currentBroadcast = this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress(); String currentBroadcast = this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress();
@@ -7032,8 +6959,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
// Re-read (may have been auto-detected) // Re-read (may have been auto-detected)
txtFldWtBroadcastAddr.setText(this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress()); txtFldWtBroadcastAddr.setText(this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress());
grdPnlLog.add(lblWtBroadcastAddr, 0, 10); grdPnlLog.add(lblWtBroadcastAddr, 0, 13);
grdPnlLog.add(txtFldWtBroadcastAddr, 1, 10); grdPnlLog.add(txtFldWtBroadcastAddr, 1, 13);
VBox vbxLog = new VBox(); VBox vbxLog = new VBox();
vbxLog.setPadding(new Insets(10, 10, 10, 10)); vbxLog.setPadding(new Insets(10, 10, 10, 10));
@@ -7068,45 +6995,51 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
chkBxEnableTRXMsgbyUCX.selectedProperty().addListener(new ChangeListener<Boolean>() { chkBxEnableTRXMsgbyUCX.selectedProperty().addListener(new ChangeListener<Boolean>() {
@Override @Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
chatcontroller.getChatPreferences().setTrxSynch_ucxLogUDPListenerEnabled(newValue); // chk2.setSelected(!newValue);
boolean anyActive = newValue || chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled(); if (!newValue) {
if (!anyActive) { chatcontroller.getChatPreferences()
.setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected());
txt_ownqrgMainCategory.textProperty().unbind(); txt_ownqrgMainCategory.textProperty().unbind();
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by hand (watch prefs!)")); 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]: 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 { } else {
chatcontroller.getChatPreferences()
.setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected());
txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat()); txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat());
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)")); 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());
} }
} }
}); });
// Unconditionally add listener to manually sync the textfield input to the button // Thats the default behaviour of the myqrg textfield
// (this listener also fires correctly when the value is updated by the binding) if (this.chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()) {
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");
} 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) -> { 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); 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());
} else {
txt_ownqrgMainCategory.setTooltip(new Tooltip("enter your cq qrg here"));
} }
grdPnltrx.add(generateLabeledSeparator(100, "Receive UCXLog TRX info"), 0, 0, 2, 1); grdPnltrx.add(generateLabeledSeparator(100, "Receive UCXLog TRX info"), 0, 0, 2, 1);
grdPnltrx.add(lblEnableTRXMsgbyUCX, 0, 1); grdPnltrx.add(lblEnableTRXMsgbyUCX, 0, 1);
grdPnltrx.add(chkBxEnableTRXMsgbyUCX, 1, 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(); VBox vbxTRXSynch = new VBox();
vbxTRXSynch.setPadding(new Insets(10, 10, 10, 10)); vbxTRXSynch.setPadding(new Insets(10, 10, 10, 10));
vbxTRXSynch.getChildren().addAll(grdPnltrx); vbxTRXSynch.getChildren().addAll(grdPnltrx);
@@ -8191,7 +8124,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
else if (chatcontroller.isConnectedAndLoggedIn()) { else if (chatcontroller.isConnectedAndLoggedIn()) {
btnOptionspnlDisconnectOnly.setDisable(false); btnOptionspnlDisconnectOnly.setDisable(false);
menuItemFileDisconnect.setDisable(false); menuItemFileDisconnect.setDisable(false);
menuItemFileConnect.setDisable(true);
menuItemOptionsAwayBack.setDisable(false); menuItemOptionsAwayBack.setDisable(false);
} }
@@ -8215,19 +8147,13 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
txtFldstn_maxQRBDefault.setDisable(false); txtFldstn_maxQRBDefault.setDisable(false);
menuItemOptionsSetFrequencyAsName.setDisable(true); menuItemOptionsSetFrequencyAsName.setDisable(true);
menuItemOptionsAwayBack.setDisable(true); menuItemOptionsAwayBack.setDisable(true);
menuItemFileConnect.setDisable(false);
station_chkBxEnableSecondChat.setDisable(false); station_chkBxEnableSecondChat.setDisable(false);
stn_choiceBxChatChategorySecond.setDisable(false); stn_choiceBxChatChategorySecond.setDisable(false);
} }
}); });
String btnText = "Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain() btnOptionspnlConnect = new Button("Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain()
.getChatCategoryName(choiceBxChatChategory.getSelectionModel().getSelectedItem().getCategoryNumber()); .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>() { btnOptionspnlConnect.setOnAction(new EventHandler<ActionEvent>() {
@Override @Override
public void handle(ActionEvent event) { public void handle(ActionEvent event) {
@@ -8259,7 +8185,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
btnOptionspnlDisconnectOnly.setDisable(false); btnOptionspnlDisconnectOnly.setDisable(false);
menuItemFileDisconnect.setDisable(false); menuItemFileDisconnect.setDisable(false);
menuItemFileConnect.setDisable(true);
menuItemOptionsAwayBack.setDisable(false); menuItemOptionsAwayBack.setDisable(false);
menuItemOptionsSetFrequencyAsName.setDisable(false); menuItemOptionsSetFrequencyAsName.setDisable(false);
@@ -8509,24 +8434,9 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
public static void main(String[] args) { public static void main(String[] args) {
setupFileLogging();
launch(args); launch(args);
} }
private static void setupFileLogging() {
try {
String logDir = Path.of(System.getProperty("user.home"), ".praktiKST").toString();
new File(logDir).mkdirs();
FileHandler fileHandler = new FileHandler(logDir + "/kst4contest-errors.log", true);
fileHandler.setLevel(Level.SEVERE);
fileHandler.setFormatter(new SimpleFormatter());
Logger rootLogger = Logger.getLogger("");
rootLogger.addHandler(fileHandler);
} catch (IOException e) {
System.err.println("Could not set up file logging: " + e.getMessage());
}
}
@Override @Override
public void onThreadStatusChanged(String key, ThreadStateMessage threadStateMessage) { public void onThreadStatusChanged(String key, ThreadStateMessage threadStateMessage) {
-3
View File
@@ -1,11 +1,8 @@
module praktiKST { module praktiKST {
requires javafx.controls; requires javafx.controls;
requires javafx.fxml;
requires javafx.web;
requires jdk.xml.dom; requires jdk.xml.dom;
requires java.sql; requires java.sql;
requires javafx.media; requires javafx.media;
requires java.logging;
exports kst4contest.controller.interfaces; exports kst4contest.controller.interfaces;
exports kst4contest.controller; exports kst4contest.controller;
exports kst4contest.locatorUtils; exports kst4contest.locatorUtils;
+1783
View File
File diff suppressed because it is too large Load Diff