Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af30c17a0d | ||
|
|
399e5f34b7
|
||
|
|
2bc7554a52
|
||
|
|
77b7a4729f
|
||
|
|
56b2fae0a3
|
||
|
|
cc991418e8 | ||
|
|
399f7235ef | ||
|
|
720dd2560b | ||
|
|
8fa5360752 | ||
|
|
422e6cf4b7 | ||
|
|
f77e0bd8f9 | ||
|
|
641c9bdbc3 | ||
|
|
265115c9cc | ||
|
|
49c0c1eb37 | ||
|
|
88cb55520d | ||
|
|
f014b4697b
|
||
|
|
209034724f | ||
|
|
f8c04e72e1 | ||
|
|
a4475e6d12 | ||
|
|
e3b2ea725a | ||
|
|
6966edbfff | ||
|
|
42d4b72dd6 | ||
|
|
15f585c938 | ||
|
|
663f724c98 | ||
|
|
9cccdb73e9 | ||
|
|
401f271d56 | ||
|
|
00aefdacd5 | ||
|
|
ea4fc9c008 | ||
|
|
caaeebd00c | ||
|
|
852f76b05a | ||
|
|
d6c1ffbb34 | ||
|
|
cfbb978aee | ||
|
|
8c3ff5f07c | ||
|
|
926bb9daee | ||
|
|
2828e7ef80 | ||
|
|
b0cc5ee9ba | ||
|
|
b00e4c1e3d | ||
|
|
b3ca684f04 | ||
|
|
1de9673d12 | ||
|
|
c119f28b30 | ||
|
|
6a7f07c62e | ||
|
|
4f574ebec6 | ||
|
|
7ce31e110b | ||
|
|
92804a622a |
@@ -95,7 +95,8 @@ $endif$
|
||||
belowskip = 8pt,
|
||||
literate = {→}{{\ensuremath{\rightarrow}}}1
|
||||
{←}{{\ensuremath{\leftarrow}}}1
|
||||
{↔}{{\ensuremath{\leftrightarrow}}}1,
|
||||
{↔}{{\ensuremath{\leftrightarrow}}}1
|
||||
{…}{{\ldots}}1,
|
||||
}
|
||||
|
||||
%% ─── Blockquotes ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -94,6 +94,10 @@ jobs:
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" packaging/aur/kst4contest/PKGBUILD
|
||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA_SRC}')/" packaging/aur/kst4contest/PKGBUILD
|
||||
|
||||
# A new pkgver supersedes any rebuild-only pkgrel bumps made in between.
|
||||
sed -i "s/^pkgrel=.*/pkgrel=1/" packaging/aur/kst4contest-bin/PKGBUILD
|
||||
sed -i "s/^pkgrel=.*/pkgrel=1/" packaging/aur/kst4contest/PKGBUILD
|
||||
|
||||
# kst4contest-git: Basis aus pom.xml, Suffix aus git
|
||||
BASE_VER=$(grep -m1 '<version>' pom.xml \
|
||||
| sed 's/.*<version>\(.*\)<\/version>.*/\1/' | sed 's/[-.]nightly//')
|
||||
|
||||
@@ -6,6 +6,8 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- "src/**"
|
||||
- "packaging/icons/**"
|
||||
- "packaging/macos/**"
|
||||
- "pom.xml"
|
||||
- "mvnw"
|
||||
- "mvnw.cmd"
|
||||
@@ -62,7 +64,18 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec --dest dist
|
||||
$addModules = & java packaging/AddModules.java
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to resolve --add-modules from module-info.java" }
|
||||
jpackage `
|
||||
--type app-image `
|
||||
--name praktiKST `
|
||||
--icon packaging/icons/kst4contest.ico `
|
||||
--input target/dist-libs `
|
||||
--main-jar app.jar `
|
||||
--main-class kst4contest.view.Kst4ContestApplication `
|
||||
--module-path target/dist-libs `
|
||||
--add-modules $addModules `
|
||||
--dest dist
|
||||
|
||||
- name: Create Windows ZIP
|
||||
shell: pwsh
|
||||
@@ -110,14 +123,16 @@ jobs:
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
|
||||
- name: Create AppDir metadata
|
||||
@@ -195,14 +210,16 @@ jobs:
|
||||
- name: Build Debian package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type deb \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--linux-package-deps "libgstreamer1.0-0,libgstreamer-plugins-base1.0-0,gstreamer1.0-plugins-good" \
|
||||
--dest dist
|
||||
DEB="$(ls dist/*.deb | head -n 1)"
|
||||
@@ -254,14 +271,16 @@ jobs:
|
||||
- name: Build Fedora package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type rpm \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--linux-package-deps "gstreamer1,gstreamer1-plugins-base,gstreamer1-plugins-good" \
|
||||
--dest dist
|
||||
RPM="$(ls dist/*.rpm | head -n 1)"
|
||||
@@ -315,14 +334,16 @@ jobs:
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
|
||||
- name: Build Arch Linux package artifact
|
||||
@@ -433,14 +454,16 @@ jobs:
|
||||
./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
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest target/flatpak-src
|
||||
|
||||
- name: Create Flatpak manifest
|
||||
@@ -586,6 +609,7 @@ jobs:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, macos-15-intel]
|
||||
|
||||
@@ -612,32 +636,38 @@ jobs:
|
||||
- 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,java.net.http,jdk.crypto.ec \
|
||||
--dest dist
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
|
||||
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
|
||||
run: ./packaging/macos/ci-import-cert.sh
|
||||
|
||||
# Builds the jar, signs the app bundle and every native library inside it,
|
||||
# wraps it into a DMG and has Apple notarize the result. Same script the
|
||||
# local Mac uses, so the two cannot drift apart.
|
||||
- name: Build signed and notarized DMG
|
||||
env:
|
||||
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
|
||||
NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
|
||||
NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_ISSUER }}
|
||||
run: |
|
||||
printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8"
|
||||
export NOTARY_KEY="$RUNNER_TEMP/notary.p8"
|
||||
./packaging/macos/build-signed-dmg.sh
|
||||
|
||||
- name: Remove signing credentials
|
||||
if: always()
|
||||
run: |
|
||||
rm -f "$RUNNER_TEMP/notary.p8"
|
||||
if [ -n "${SIGNING_KEYCHAIN:-}" ]; then
|
||||
security delete-keychain "$SIGNING_KEYCHAIN" || true
|
||||
fi
|
||||
|
||||
- name: Rename DMG artifact
|
||||
run: |
|
||||
DMG=$(ls dist/*.dmg | head -n 1)
|
||||
if [ -z "$DMG" ]; then
|
||||
echo "No DMG produced by jpackage" && exit 1
|
||||
echo "No DMG produced by the build" && exit 1
|
||||
fi
|
||||
mv "$DMG" "dist/${ASSET_BASENAME}-macos-${ARCH}.dmg"
|
||||
|
||||
@@ -646,4 +676,4 @@ jobs:
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.os }}
|
||||
path: dist/KST4Contest-*-macos-*.dmg
|
||||
retention-days: 14
|
||||
retention-days: 14
|
||||
@@ -27,5 +27,8 @@ jobs:
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Verify packaging module list matches module-info.java
|
||||
run: java packaging/AddModules.java --verify-pom
|
||||
|
||||
- name: Compile
|
||||
run: ./mvnw -B -DskipTests compile
|
||||
|
||||
@@ -7,7 +7,9 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
issues: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
@@ -46,7 +48,18 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec --dest dist
|
||||
$addModules = & java packaging/AddModules.java
|
||||
if ($LASTEXITCODE -ne 0) { throw "Failed to resolve --add-modules from module-info.java" }
|
||||
jpackage `
|
||||
--type app-image `
|
||||
--name praktiKST `
|
||||
--icon packaging/icons/kst4contest.ico `
|
||||
--input target/dist-libs `
|
||||
--main-jar app.jar `
|
||||
--main-class kst4contest.view.Kst4ContestApplication `
|
||||
--module-path target/dist-libs `
|
||||
--add-modules $addModules `
|
||||
--dest dist
|
||||
|
||||
- name: Create Windows ZIP
|
||||
shell: pwsh
|
||||
@@ -87,14 +100,16 @@ jobs:
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
|
||||
- name: Create AppDir metadata
|
||||
@@ -165,14 +180,16 @@ jobs:
|
||||
- name: Build Debian package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type deb \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--linux-package-deps "libgstreamer1.0-0,libgstreamer-plugins-base1.0-0,gstreamer1.0-plugins-good" \
|
||||
--dest dist
|
||||
DEB="$(ls dist/*.deb | head -n 1)"
|
||||
@@ -217,14 +234,16 @@ jobs:
|
||||
- name: Build Fedora package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type rpm \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--linux-package-deps "gstreamer1,gstreamer1-plugins-base,gstreamer1-plugins-good" \
|
||||
--dest dist
|
||||
RPM="$(ls dist/*.rpm | head -n 1)"
|
||||
@@ -269,14 +288,16 @@ jobs:
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
|
||||
- name: Build Arch Linux package artifact
|
||||
@@ -378,14 +399,16 @@ jobs:
|
||||
./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
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest target/flatpak-src
|
||||
|
||||
- name: Create Flatpak manifest
|
||||
@@ -483,6 +506,7 @@ jobs:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, macos-15-intel]
|
||||
|
||||
@@ -499,33 +523,39 @@ jobs:
|
||||
- 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,java.net.http,jdk.crypto.ec \
|
||||
--dest dist
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
|
||||
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
|
||||
run: ./packaging/macos/ci-import-cert.sh
|
||||
|
||||
# Builds the jar, signs the app bundle and every native library inside it,
|
||||
# wraps it into a DMG and has Apple notarize the result. Same script the
|
||||
# local Mac uses, so the two cannot drift apart.
|
||||
- name: Build signed and notarized DMG
|
||||
env:
|
||||
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
|
||||
NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
|
||||
NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_ISSUER }}
|
||||
run: |
|
||||
printf '%s' "$MACOS_NOTARY_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8"
|
||||
export NOTARY_KEY="$RUNNER_TEMP/notary.p8"
|
||||
./packaging/macos/build-signed-dmg.sh
|
||||
|
||||
- name: Remove signing credentials
|
||||
if: always()
|
||||
run: |
|
||||
rm -f "$RUNNER_TEMP/notary.p8"
|
||||
if [ -n "${SIGNING_KEYCHAIN:-}" ]; then
|
||||
security delete-keychain "$SIGNING_KEYCHAIN" || true
|
||||
fi
|
||||
|
||||
- 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
|
||||
echo "No DMG produced by the build" && exit 1
|
||||
fi
|
||||
mv "$DMG" "dist/KST4Contest-${{ github.ref_name }}-macos-${ARCH}.dmg"
|
||||
|
||||
@@ -683,6 +713,9 @@ jobs:
|
||||
- publish-flatpak-repo
|
||||
|
||||
steps:
|
||||
- name: Checkout release source
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Download Windows artifact
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
@@ -753,3 +786,45 @@ jobs:
|
||||
release-assets/macos/KST4Contest-${{ github.ref_name }}-macos-*.dmg,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
|
||||
|
||||
# The update feed is generated only after GitHub has published the
|
||||
# release. Otherwise the Releases API cannot return the release notes
|
||||
# belonging to the tag which triggered this workflow.
|
||||
- name: Set up Node.js for website build
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
cache-dependency-path: website/package-lock.json
|
||||
|
||||
- name: Build and validate website after release publication
|
||||
working-directory: website
|
||||
run: |
|
||||
npm ci
|
||||
npm test
|
||||
npm run build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Verify Stable release in update feed
|
||||
if: ${{ !startsWith(github.ref_name, 'beta-') }}
|
||||
working-directory: website
|
||||
run: npm run validate:version-info
|
||||
env:
|
||||
EXPECTED_STABLE_VERSION: ${{ github.ref_name }}
|
||||
|
||||
- name: Attach verified version info to tagged release
|
||||
run: >-
|
||||
gh release upload "${GITHUB_REF_NAME}"
|
||||
website/_site/kst4ContestVersionInfo.xml
|
||||
--clobber
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload verified website artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: kst4contest-website-${{ github.ref_name }}
|
||||
path: website/_site/
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -44,3 +44,6 @@ website/_site/
|
||||
|
||||
# Local secrets for act testing
|
||||
.secrets
|
||||
|
||||
# Apple notarization private keys - never commit these
|
||||
*.p8
|
||||
|
||||
@@ -1,42 +1,98 @@
|
||||
# KST4Contest
|
||||
|
||||
KST4Contest (also known as pratiKST) is a Java-based chat client for [ON4KST](http://www.on4kst.com/chat), focused on VHF/UHF/SHF contest operation.
|
||||
KST4Contest is a Java-based client for the [ON4KST chat](https://www.on4kst.org/chat/login.php), developed for coordinated VHF, UHF and microwave contest operation.
|
||||
|
||||
## Website
|
||||
The application is developed by Marc Fröhlich (DO5AMF) and since 2026 Philipp Wagner (DN9APW).
|
||||
|
||||
The offical Website of KST4Contest is now instead of [do5amf.funkerportal.de](https://do5amf.funkerportal.de) the new website [here](https://kst4contest.hamradioonline.de) [https://kst4contest.hamradioonline.de](https://kst4contest.hamradioonline.de)
|
||||
## Start here
|
||||
|
||||
- [Project website](https://kst4contest.hamradioonline.de/)
|
||||
- [Download Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
|
||||
- [Online manual](https://kst4contest.hamradioonline.de/manual/)
|
||||
- [GitHub wiki](https://github.com/praktimarc/kst4contest/wiki)
|
||||
- [Issues and bug reports](https://github.com/praktimarc/kst4contest/issues)
|
||||
- [Development roadmap](https://kst4contest.hamradioonline.de/roadmap/)
|
||||
|
||||
## What KST4Contest does
|
||||
|
||||
KST4Contest combines the ON4KST chat with information that is useful when coordinating contacts during a contest.
|
||||
|
||||
Among other things, it can:
|
||||
|
||||
- display and filter stations from the supported ON4KST chat categories;
|
||||
- derive band and frequency information from chat messages and station names;
|
||||
- maintain Worked and NOT QRV information for the available bands;
|
||||
- calculate station priorities from distance, activity and other available information;
|
||||
- manage internal skeds and received Win-Test skeds;
|
||||
- use AirScout information when evaluating possible aircraft-scatter contacts;
|
||||
- display stations, paths and additional propagation information on maps;
|
||||
- exchange information with supported logging programs, Win-Test, PSTRotator and a local DX Cluster interface;
|
||||
- provide configurable automatic replies for recurring chat requests.
|
||||
|
||||
Calculated scores, aircraft-scatter information and path assessments are operating aids. They depend on the available data and should not be treated as guarantees that a contact is possible.
|
||||
|
||||
## Installation
|
||||
|
||||
Ready-to-use packages are available for Windows, Linux and macOS. These packages include the required Java runtime, so a separate Java installation is normally not necessary.
|
||||
|
||||
Use the central download page to select the appropriate build:
|
||||
|
||||
- **Stable** is intended for normal contest operation.
|
||||
- **Beta** contains changes that are being prepared for a stable release.
|
||||
- **Nightly** contains the latest automated development build and is mainly intended for testing.
|
||||
|
||||
[Open the download page](https://kst4contest.hamradioonline.de/download/)
|
||||
|
||||
## Documentation
|
||||
|
||||
The full user documentation is maintained in the project wiki:
|
||||
The documentation is available in German and English:
|
||||
|
||||
- https://github.com/praktimarc/kst4contest/wiki
|
||||
- [German manual](https://github.com/praktimarc/kst4contest/wiki/de-Home)
|
||||
- [English manual](https://github.com/praktimarc/kst4contest/wiki/en-Home)
|
||||
- [Online manual](https://kst4contest.hamradioonline.de/manual/)
|
||||
|
||||
Direct entry points:
|
||||
The Markdown sources used for the wiki and the generated PDF manuals are stored in [`github_docs`](github_docs/).
|
||||
|
||||
- German start page: https://github.com/praktimarc/kst4contest/wiki/de-Home
|
||||
- English start page: https://github.com/praktimarc/kst4contest/wiki/en-Home
|
||||
Changes to operating behaviour should be documented together with their purpose and limitations. This is especially important for functions whose result depends on external data, heuristics or information derived from chat messages.
|
||||
|
||||
## Build
|
||||
## Building from source
|
||||
|
||||
Compile locally with Maven Wrapper:
|
||||
Building KST4Contest requires JDK 21. The Maven Wrapper included in the repository should be used, so a separate Maven installation is not required.
|
||||
|
||||
Linux and macOS:
|
||||
|
||||
```bash
|
||||
./mvnw clean test
|
||||
./mvnw -B -DskipTests compile
|
||||
```
|
||||
|
||||
## Notes
|
||||
Windows:
|
||||
|
||||
- Source code is under `src/`.
|
||||
- Documentation markdown pages for wiki/PDF are under `github_docs/`.
|
||||
```powershell
|
||||
mvnw.cmd clean test
|
||||
mvnw.cmd -B -DskipTests compile
|
||||
```
|
||||
|
||||
## Status of the latest CI:
|
||||
Wiki Publishing:
|
||||
## Repository structure
|
||||
|
||||
- `src/main/java/` – application source code
|
||||
- `src/test/` – automated tests
|
||||
- `github_docs/` – German and English manual sources
|
||||
- `website/` – project website sources
|
||||
- `packaging/` – platform-specific packaging files
|
||||
|
||||
## CI status
|
||||
|
||||
### Documentation
|
||||
|
||||
[](https://github.com/praktimarc/kst4contest/actions/workflows/github-wiki.yml)
|
||||
|
||||
[](https://github.com/praktimarc/kst4contest/actions/workflows/docs-pdf.yml)
|
||||
|
||||
Builds:
|
||||
### Builds
|
||||
|
||||
[](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml)
|
||||
|
||||
## License
|
||||
|
||||
KST4Contest is distributed under the [GNU General Public License v3.0](LICENSE).
|
||||
@@ -1,10 +1,13 @@
|
||||
# KST4Contest
|
||||
# KST4Contest Manual / Handbuch
|
||||
|
||||
KST4Contest is a desktop client for the [ON4KST Chat](https://www.on4kst.org/chat/login.php), developed for VHF, UHF and SHF contest operation. It combines chat, candidate selection, sked planning, aircraft scatter information and connections to logging and station software.
|
||||
KST4Contest is a desktop client for the [ON4KST Chat](https://www.on4kst.org/chat/login.php), developed for VHF, UHF and SHF contest operation. It combines chat, candidate selection, sked planning, aircraft-scatter information, station mapping and connections to logging and station software.
|
||||
|
||||
KST4Contest ist ein Desktop-Client für den [ON4KST-Chat](https://www.on4kst.org/chat/login.php), der für den Contest-Betrieb auf den VHF-, UHF- und SHF-Bändern entwickelt wurde. Er verbindet Chat, Stationsauswahl, Sked-Planung, Aircraft-Scatter-Daten sowie die Anbindung an Log- und Stationssoftware.
|
||||
KST4Contest ist ein Desktop-Client für den [ON4KST-Chat](https://www.on4kst.org/chat/login.php), der für den Contest-Betrieb auf den VHF-, UHF- und SHF-Bändern entwickelt wurde. Er verbindet Chat, Stationsauswahl, Sked-Planung, Aircraft-Scatter-Daten, Stationskarte sowie die Anbindung an Log- und Stationssoftware.
|
||||
|
||||
Developed by / Entwickelt von **DO5AMF (Marc Fröhlich)**, operator at / Operator bei DM5M.
|
||||
Developed by / Entwickelt von:
|
||||
|
||||
- **DO5AMF (Marc Fröhlich)**, operator at / Operator bei **DM5M**
|
||||
- **DN9APW (Philipp Wagner)**, developer since / Entwickler seit Mai 2025
|
||||
|
||||
---
|
||||
|
||||
@@ -14,12 +17,29 @@ Developed by / Entwickelt von **DO5AMF (Marc Fröhlich)**, operator at / Operato
|
||||
|---|---|
|
||||
| [Deutsches Handbuch](de-Home) | [English manual](en-Home) |
|
||||
|
||||
Both versions follow the same structure and describe the same application state.
|
||||
Beide Sprachfassungen verwenden dieselbe Struktur und beschreiben denselben Programmstand.
|
||||
|
||||
---
|
||||
|
||||
## Start here / Hier beginnen
|
||||
|
||||
- [Download Stable, Beta or Nightly / Stable, Beta oder Nightly herunterladen](https://kst4contest.hamradioonline.de/download/)
|
||||
- [Online manual / Online-Handbuch](https://kst4contest.hamradioonline.de/manual/)
|
||||
- [Installation](de-Installation) / [Installation](en-Installation)
|
||||
- [Configuration](en-Configuration) / [Konfiguration](de-Konfiguration)
|
||||
|
||||
The Stable version is the normal choice for contest operation. Beta and Nightly builds are intended for testing particular changes and may contain functions which have not yet been released as Stable.
|
||||
|
||||
Für den normalen Contestbetrieb ist die Stable-Version vorgesehen. Beta- und Nightly-Builds dienen dem gezielten Test neuer Änderungen und können Funktionen enthalten, die noch nicht als Stable veröffentlicht wurden.
|
||||
|
||||
---
|
||||
|
||||
## Project links / Projektlinks
|
||||
|
||||
- [Project website / Projektwebseite](https://kst4contest.hamradioonline.de/)
|
||||
- [Download the current stable release / Aktuelle stabile Version herunterladen](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Downloads](https://kst4contest.hamradioonline.de/download/)
|
||||
- [Source code / Quellcode](https://github.com/praktimarc/kst4contest)
|
||||
- [GitHub Releases](https://github.com/praktimarc/kst4contest/releases)
|
||||
- [Bug reports and feature requests / Fehler und Funktionswünsche](https://github.com/praktimarc/kst4contest/issues)
|
||||
- [Online manual / Online-Handbuch](https://kst4contest.hamradioonline.de/manual/)
|
||||
- [Development roadmap / Entwicklungsstand](https://kst4contest.hamradioonline.de/roadmap/)
|
||||
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -107,7 +107,7 @@ Die Flugzeugdaten können direkt in Nachrichten eingefügt werden:
|
||||
- `FIRSTAP` → z. B. `a very big AP in 1 min`
|
||||
- `SECONDAP` → z. B. `Next big AP in 9 min`
|
||||
|
||||
Details: [Makros und Variablen](Makros-und-Variablen#variablen)
|
||||
Details: [Makros und Variablen](de-Makros-und-Variablen#variablen)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,11 +4,22 @@
|
||||
|
||||
## Verbinden mit dem Chat
|
||||
|
||||
1. Im Einstellungsfenster eine **Chat-Kategorie** auswählen (z. B. 144 MHz VHF, 432 MHz UHF, …).
|
||||
2. **Connect**-Button klicken.
|
||||
3. Warten bis die Verbindung aufgebaut ist.
|
||||
Vor dem ersten Verbindungsaufbau müssen im Einstellungsfenster mindestens Rufzeichen, Passwort, Locator und primäre Chat-Kategorie konfiguriert werden. Soll zusätzlich eine zweite Kategorie verwendet werden, muss auch deren Login aktiviert und vollständig eingerichtet sein.
|
||||
|
||||
> Trennen und Neu-Verbinden ist nur über das Einstellungsfenster möglich. Es empfiehlt sich daher, das Einstellungsfenster geöffnet zu lassen.
|
||||
Die Verbindung kann auf zwei Wegen aufgebaut werden:
|
||||
|
||||
- Mit **Connect to …** im Einstellungsfenster werden die dort eingetragenen Werte übernommen und die Verbindung gestartet.
|
||||
- **File → Connect to …** verwendet die bereits in KST4Contest übernommenen Einstellungen.
|
||||
|
||||
Geänderte Einstellungen müssen mit **Save Settings** gespeichert werden, wenn sie auch beim nächsten Programmstart verwendet werden sollen.
|
||||
|
||||
Eine bestehende Verbindung kann über **File → Disconnect** oder über **Disconnect** im Einstellungsfenster beendet werden. **Exit + disconnect** beendet zusätzlich das Programm.
|
||||
|
||||
Bei einem unerwarteten Verbindungsverlust versucht KST4Contest nach einer begrenzten Wartezeit, die ON4KST-Verbindung kontrolliert neu aufzubauen. Ein fehlgeschlagener Erstaufbau blockiert die Benutzeroberfläche nicht mehr.
|
||||
|
||||
Ob die Verbindung lediglich als TCP-Verbindung besteht oder bereits vollständig angemeldet und synchronisiert ist, zeigt der [`LINK`-Status](#statusleiste-und-hinweise) im Hauptfenster.
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
@@ -16,10 +27,40 @@
|
||||
|
||||
Das Hauptfenster besteht aus mehreren Bereichen:
|
||||
|
||||
|
||||
### Statusleiste und Hinweise
|
||||
|
||||
Die Statusleiste befindet sich am oberen Rand des Hauptfensters neben dem Menü.
|
||||
|
||||

|
||||
|
||||
Der dauerhaft sichtbare `LINK`-Indikator zeigt den tatsächlichen Zustand der ON4KST-Verbindung:
|
||||
|
||||
| Anzeige | Bedeutung |
|
||||
|---|---|
|
||||
| grünes `LINK` | Login und Synchronisation der konfigurierten Chat-Kategorien sind vollständig abgeschlossen |
|
||||
| gelbes `LINK…` | Verbindung, Anmeldung, Benutzerlistensynchronisation oder kontrolliertes Beenden läuft |
|
||||
| rotes `LINK!` | keine Verbindung oder Wartezeit vor einem automatischen Neuaufbau |
|
||||
|
||||
Der Tooltip enthält den internen Verbindungsstatus und eine genauere Beschreibung des aktuellen Schritts. Der Indikator ist keine Schaltfläche.
|
||||
|
||||
`ONLINE` wird erst gemeldet, nachdem die Anmeldung bestätigt und die Benutzerlisten der konfigurierten Kategorien vollständig empfangen wurden. Während die Verbindung noch aufgebaut oder neu synchronisiert wird, bleiben Sendfeld und **TX** deaktiviert.
|
||||
|
||||
Bei bestimmten Ereignissen erscheinen vorübergehend weitere Hinweise:
|
||||
|
||||
- `SKED` weist auf eine fällige Sked-Erinnerung hin. Der Text enthält das vollständige Zielrufzeichen und die verbleibende Zeit.
|
||||
- `BAND+` erscheint nach einem Logeintrag, wenn für die gearbeitete Station noch mindestens ein gemeinsames, aktiviertes und nicht gearbeitetes Band erkannt wurde.
|
||||
|
||||
Beide Hinweise blinken ungefähr zwölf Sekunden und verschwinden anschließend wieder. Der vollständige Inhalt beziehungsweise die Herleitung steht im jeweiligen Tooltip. Die Anzeigen sind nicht anklickbar.
|
||||
|
||||
|
||||
### PM-Fenster (oben links)
|
||||
|
||||
Zeigt alle empfangenen **Privatnachrichten** sowie abgefangene öffentliche Nachrichten, die das eigene Rufzeichen enthalten. Neue Nachrichten erscheinen in **Rot** und faden alle 30 Sekunden über Gelb bis Weiß ab.
|
||||
Das PM-Fenster zeigt die an die eigenen Chat-Logins gerichteten Privatnachrichten und die zugehörigen ausgehenden Antworten.
|
||||
|
||||
Ist das [QSO-Monitoring](de-Funktionen#qso-monitoring-ab-v131) aktiviert, erscheinen dort zusätzlich die erfassten Nachrichten der überwachten Basisrufzeichen. Diese Einträge erhalten eine `Sniffed:`-Kennzeichnung mit dem vollständigen sichtbaren Absender und Empfänger.
|
||||
|
||||
Neue Nachrichten werden zunächst auffällig dargestellt und wechseln anschließend schrittweise zur normalen Tabellenfarbe. Die farbliche Hervorhebung dient nur als zeitlicher Hinweis; sie verändert weder Inhalt noch Routing der Nachricht.
|
||||
### Benutzerliste (Chat Members)
|
||||
|
||||
Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfiguration):
|
||||
@@ -60,15 +101,47 @@ Jede Statuszelle besitzt einen Tooltip mit der Legende und dem für die betreffe
|
||||
Ein grün und fett dargestelltes Rufzeichen kennzeichnet eine aus einer gerichteten Nachricht hergeleitete Richtungsgelegenheit. Die Markierung bezieht sich auf den Absender der Nachricht und bleibt höchstens fünf Minuten sichtbar. Herleitung und Grenzen: [Richtungsgelegenheiten aus gerichteten Nachrichten](de-Funktionen#richtungsgelegenheiten-aus-gerichteten-nachrichten).
|
||||
### Sendfeld
|
||||
|
||||
Texteingabe für ausgehende Nachrichten. Nach Klick auf ein Rufzeichen in der Benutzerliste erhält das Sendfeld automatisch den Fokus – sofort tippen ohne Doppelklick (ab v1.22).
|
||||
Das Sendfeld enthält den vorbereiteten Text für die nächste ausgehende Nachricht.
|
||||
|
||||
### MYQRG-Feld
|
||||
Wird eine Station bewusst per Maus oder Tastatur in der Benutzerliste ausgewählt, bereitet KST4Contest eine gerichtete Nachricht vor:
|
||||
|
||||
Rechts neben dem Sendbutton. Zeigt die aktuelle eigene QRG an, kann auch manuell eingetragen werden.
|
||||
```text
|
||||
/cq RUFZEICHEN
|
||||
```
|
||||
|
||||
### MYQTF-Feld *(für v1.3)*
|
||||
Dabei werden das vollständige sichtbare Rufzeichen einschließlich eines vorhandenen Suffixes und die Chat-Kategorie der ausgewählten Station beibehalten. Ein Ziel wie `9A0BB-70` wird nicht auf `9A0BB` verkürzt.
|
||||
|
||||
Eingabefeld für die aktuelle Antennenrichtung. Wird für die geplante `MYQTF`-Variable verwendet.
|
||||
Eine Hintergrundaktualisierung, Neusortierung oder Filteränderung darf einen bereits bearbeiteten Nachrichtentext nicht überschreiben. Nur eine tatsächliche Auswahl durch den Operator bereitet den `/cq`-Empfänger erneut vor.
|
||||
|
||||
- **TX** oder `Enter` sendet den vorbereiteten Text.
|
||||
- `Esc` leert das Sendfeld.
|
||||
- Während KST4Contest nicht vollständig mit ON4KST verbunden ist, bleiben Sendfeld und **TX** deaktiviert.
|
||||
|
||||
Shortcuts, Snippets und Variablen sind unter [Makros und Variablen](de-Makros-und-Variablen) beschrieben.
|
||||
|
||||
### MYQRG- und SECONDQRG-Feld
|
||||
|
||||
Die beiden QRG-Felder enthalten die eigenen Frequenzen der primären und sekundären Chat-Kategorie.
|
||||
|
||||
`MYQRG` kann von einer aktivierten TRX-Synchronisation aktualisiert oder – bei deaktivierten automatischen QRG-Quellen – von Hand eingetragen werden. `SECONDQRG` bleibt davon unabhängig und enthält die QRG der zweiten Kategorie.
|
||||
|
||||
Die Auswahl einer Station aus dem zweiten Chat verändert die Bedeutung der beiden Werte nicht: `MYQRG` gehört weiterhin zur primären, `SECONDQRG` zur sekundären Kategorie.
|
||||
|
||||
Weitere Einzelheiten: [TRX-Sync-Einstellungen](de-Konfiguration#trx-sync-einstellungen).
|
||||
|
||||
### MYQTF-Feld
|
||||
|
||||
Das MYQTF-Feld zeigt die aktuelle Antennenrichtung als numerischen Winkel in Grad.
|
||||
|
||||
Ist PSTRotator aktiviert, wird der Wert automatisch übernommen und das Feld ist nicht manuell bearbeitbar. Ohne aktive Rotatorsynchronisation kann die Antennenrichtung direkt eingetragen werden. Die Änderung wird beim Verlassen des Feldes übernommen.
|
||||
|
||||
Der Wert beeinflusst unter anderem:
|
||||
|
||||
- die QTF-Filterung,
|
||||
- die Darstellung des Antennensektors auf der Stationskarte,
|
||||
- die Prioritätsberechnung,
|
||||
- die AP-Timeline und
|
||||
- die Variable `MYQTF`.
|
||||
|
||||
---
|
||||
|
||||
@@ -86,27 +159,77 @@ Damit muss der Divider nicht allein deshalb verschoben werden, um eine einzelne
|
||||
|
||||
---
|
||||
|
||||
## Filter
|
||||
## Filter und Reachability-Steuerung
|
||||
|
||||
Die Filterleiste befindet sich oberhalb der Chatmember-Tabelle. Sie ist in mehrere logisch zusammengehörige Bereiche gegliedert:
|
||||
|
||||
- **Show only QTF** begrenzt die Liste auf eine gewählte Antennenrichtung.
|
||||
- **Show only QRB [km] <=** setzt eine maximale Entfernung.
|
||||
- **Find** sucht nach einem Rufzeichen.
|
||||
- **wkd** blendet Rufzeichen aus, die bereits auf mindestens einem Band gearbeitet wurden.
|
||||
- Die einzelnen Band-Schaltflächen blenden eine Station aus, wenn sie auf dem betreffenden Band bereits gearbeitet oder dort als NOT QRV markiert wurde. Angezeigt werden nur die für die eigene Station aktivierten Bänder.
|
||||
- **Only new grids** zeigt ausschließlich Stationen aus vierstelligen Großfeldern, die auf noch keinem Band gearbeitet wurden.
|
||||
- **Grid color** ist kein Filter. Die Funktion markiert das QRA-Feld bereits gearbeiteter Großfelder, ohne Stationen auszublenden.
|
||||
- **New bands** zeigt Stationen mit mindestens einer erkannten, an der eigenen Station aktivierten und noch nicht gearbeiteten Bandmöglichkeit. NOT-QRV-Markierungen haben Vorrang.
|
||||
- **Reachability**, **Tropo >=0dB** und **AS next 5m** schränken die Liste anhand der gewählten Strecken- beziehungsweise AirScout-Bedingungen ein.
|
||||
|
||||
Die Filterleiste besitzt keine feste Breite. QTF sowie die Worked- und Reachability-Filter nutzen zunächst den gesamten Platz ihrer jeweiligen Zeile. Wird der horizontale Divider nach rechts verschoben und die Chatmember-Ansicht dadurch schmaler, wechseln die Controls erst dann in die nächste Zeile, wenn ihre tatsächlich benötigte Breite nicht mehr zur Verfügung steht.
|
||||
Die Filterleiste befindet sich oberhalb der Chatmember-Tabelle. Filter können miteinander kombiniert werden; eine Station bleibt nur sichtbar, wenn sie alle aktiven Bedingungen erfüllt.
|
||||
|
||||

|
||||
|
||||
Im Klartext: Die Filter bestimmen weiterhin den Inhalt der Tabelle, aber nicht mehr die Mindestbreite der gesamten rechten Programmseite. In der normalen Ansicht bleibt die Leiste kompakt. Erst bei einer tatsächlich schmalen Ansicht benötigt sie mehr Höhe. Der Divider kann anschließend wieder nach links verschoben werden; die Controls ordnen sich unmittelbar neu an.
|
||||
### Stationsfilter
|
||||
|
||||
| Bedienelement | Wirkung |
|
||||
|---|---|
|
||||
| **Show only QTF** | Zeigt nur Stationen innerhalb der gewählten Antennenrichtung und des konfigurierten Öffnungswinkels |
|
||||
| **Show only QRB [km] <=** | Begrenzt die Liste auf die eingetragene maximale Entfernung |
|
||||
| **Find** | Filtert nach einem vollständigen oder teilweisen Rufzeichen |
|
||||
| **wkd** | Blendet Basisrufzeichen aus, die bereits auf mindestens einem unterstützten Band gearbeitet wurden |
|
||||
| einzelne Band-Schaltflächen | Blenden Stationen aus, die auf dem betreffenden Band bereits gearbeitet oder dort als NOT QRV markiert wurden |
|
||||
| **Inactive stations** | Blendet Stationen aus, deren letzte Chataktivität mehr als 20 Minuten zurückliegt |
|
||||
| **Only new grids** | Zeigt nur Stationen aus vierstelligen Großfeldern, die bisher auf keinem Band gearbeitet wurden |
|
||||
| **New bands** | Zeigt Stationen mit mindestens einer erkannten, lokal aktivierten und noch nicht gearbeiteten Bandmöglichkeit |
|
||||
| **Tropo >=0dB** | Zeigt Stationen mit einer berechneten, nicht negativen SSB-Marge |
|
||||
| **AS next 5m** | Zeigt Stationen mit einem aktuellen oder innerhalb der nächsten fünf Minuten erwarteten AirScout-Fenster |
|
||||
|
||||
Bei **New bands** werden aktuelle QRGs, Bandangaben im Namensfeld und aktive Rufzeichenvarianten gemeinsam ausgewertet. Manuelle NOT-QRV-Markierungen haben Vorrang.
|
||||
|
||||
Der Filter **Tropo >=0dB** entfernt nur Stationen, für die eine abgeschlossene Berechnung eine negative Marge ergeben hat. Noch nicht berechnete oder fehlgeschlagene Auswertungen bleiben sichtbar. Andernfalls würde ein fehlender API-Wert wie ein nachgewiesen ungeeigneter Funkweg behandelt.
|
||||
|
||||
### Grid color
|
||||
|
||||
**Grid color** ist kein Filter. Die Funktion verändert ausschließlich die Darstellung des QRA-Feldes und kennzeichnet bereits gearbeitete vierstellige Großfelder.
|
||||
|
||||
Die Station bleibt unabhängig von der Farbmarkierung in der Tabelle sichtbar. **Reset filters** deaktiviert diese Anzeige deshalb nicht.
|
||||
|
||||
### Reachability und Calc selected
|
||||
|
||||
Das Dropdown **Reachability** bestimmt das Band, auf das sich die Tropo-Spalte, der Tropo-Filter und eine ausdrücklich gestartete Streckenberechnung beziehen.
|
||||
|
||||
- **Auto** leitet das Band aus der aktuellen Stations-QRG, Bandangaben im Namensfeld und der unterstützten Chat-Kategorie her.
|
||||
- Ein ausdrücklich gewähltes Band übersteuert diese automatische Auswahl für die Reachability-Auswertung.
|
||||
|
||||
Eine Änderung des Dropdowns startet keine Berechnung für die gesamte Benutzerliste. Das wäre bei einer Online-Höhendatenquelle unnötig langsam und würde externe API-Abfragen vervielfachen.
|
||||
|
||||
**Calc selected** berechnet ausschließlich die aktuell ausgewählte Station auf dem gewählten beziehungsweise automatisch hergeleiteten Band. Das Ergebnis wird anschließend in der Tropo-Spalte und den zugehörigen Ansichten verwendet.
|
||||
|
||||
### Filter zurücksetzen
|
||||
|
||||
**Reset filters** entfernt:
|
||||
|
||||
- den QTF-Filter,
|
||||
- den QRB-Filter,
|
||||
- den Inhalt des Rufzeichen-Suchfeldes,
|
||||
- alle Worked- und Bandfilter,
|
||||
- **Inactive stations**,
|
||||
- **Only new grids**,
|
||||
- **New bands**,
|
||||
- **Tropo >=0dB** und
|
||||
- **AS next 5m**.
|
||||
|
||||
Die internen Filterprädikate werden dabei ausdrücklich geleert. Es genügt nicht, lediglich die sichtbaren Toggle-Buttons zurückzusetzen.
|
||||
|
||||
Nicht verändert werden:
|
||||
|
||||
- **Grid color**, weil es sich um eine Darstellungsoption handelt, und
|
||||
- die Auswahl im **Reachability**-Dropdown, weil sie das Berechnungsband festlegt und nicht unmittelbar die Tabelle filtert.
|
||||
|
||||
### Verhalten bei schmaler Ansicht
|
||||
|
||||
Die Filterleiste besitzt keine feste Breite. QTF-, Worked- und Reachability-Controls nutzen zunächst den verfügbaren Platz ihrer jeweiligen Zeile.
|
||||
|
||||
Wird der mittlere Divider nach rechts verschoben und die Chatmember-Ansicht dadurch schmaler, wechseln Bedienelemente erst dann in die nächste Zeile, wenn ihre tatsächlich benötigte Breite nicht mehr ausreicht. Wird der Bereich wieder breiter, ordnen sie sich unmittelbar neu an.
|
||||
|
||||
Im Klartext: Die Filter bestimmen den Tabelleninhalt, aber nicht mehr die Mindestbreite der gesamten rechten Programmseite.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## Stationsinfo-Panel (Further Info)
|
||||
@@ -132,7 +255,24 @@ Im selben Bereich wird der aktuelle **Priority score** der ausgewählten Station
|
||||
|
||||
Mit **Sked fail** lässt sich ein fehlgeschlagener Versuch markieren. Der Score des normalisierten Basisrufzeichens wird dadurch stark reduziert. **Reset fail** entfernt diese Markierung wieder. Die Markierung gilt für alle aktiven Suffix- und Kategorievarianten der Station und bleibt innerhalb der laufenden Programmsitzung erhalten.
|
||||
|
||||
Darunter können ein Sked und die zugehörigen **Sked-Erinnerungen / Wecker** angelegt werden. Ein nahender Sked erhöht den Priority Score zeitabhängig; unmittelbar vor dem Termin erhält er eine sehr hohe Gewichtung.
|
||||
Darunter befinden sich die Bedienelemente zum Anlegen eines Skeds:
|
||||
|
||||
| Bedienelement | Bedeutung |
|
||||
|---|---|
|
||||
| **Sked in** | Zeit bis zum Sked |
|
||||
| **Band** | vereinbartes Band aus den eigenen aktivierten Bändern |
|
||||
| **Mode** | `SSB` oder `CW` für eine mögliche Win-Test-Übergabe |
|
||||
| **Create sked** | internen Sked anlegen |
|
||||
| **Remind-PM in** | automatische Reminder-PMs aktivieren |
|
||||
| **2+1**, **5+2+1**, **10+5+2+1** | Zeitpunkte der Reminder-PMs vor dem Termin |
|
||||
|
||||

|
||||
|
||||
Das vorgeschlagene Band wird aus aktuellen QRG- und Namensinformationen der Station hergeleitet. Vor dem Anlegen kann es ausdrücklich geändert werden. Die Mode-Auswahl betrifft nur die Übergabe an Win-Test; der interne Sked und die Reminder-PMs funktionieren unabhängig davon.
|
||||
|
||||
**Create sked** legt den Termin immer zuerst in KST4Contest an. Ist der Win-Test-Netzwerk-Listener aktiv, wird anschließend zusätzlich eine Übergabe an Win-Test versucht. Kann keine zum ausgewählten Band passende QRG ermittelt werden oder ist Win-Test nicht erreichbar, bleiben der interne Sked, seine Priorisierung und gegebenenfalls angelegte Reminder erhalten.
|
||||
|
||||
Die vollständige Herleitung und die Grenzen der Funktion sind unter [Skeds und Sked-Erinnerungen](de-Funktionen#skeds-und-sked-erinnerungen) beschrieben.
|
||||
|
||||
---
|
||||
|
||||
@@ -160,24 +300,184 @@ Herleitung und Grenzen: [Prioritätsscore und Prioritätsliste](de-Funktionen#pr
|
||||
|
||||
---
|
||||
|
||||
## Cluster & QSO der anderen
|
||||
## Stationskarte
|
||||
|
||||
Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen – interessant in ruhigeren Phasen.
|
||||
Die Stationskarte kann auf zwei Wegen geöffnet werden:
|
||||
|
||||
- **Windows → Show / hide station map** öffnet oder schließt das Kartenfenster.
|
||||
- **Show on map** im **Further Info**-Bereich öffnet die Karte und fokussiert die ausgewählte Station.
|
||||
|
||||
Die Karte verwendet die Stationen, die nach Anwendung der aktuellen Benutzerlistenfilter noch sichtbar sind. Die Kopfzeile zeigt die Anzahl der dargestellten Stationen und weist mit `filtered view active` auf eine gefilterte Ansicht hin.
|
||||
|
||||

|
||||
|
||||
### Station auswählen
|
||||
|
||||
Ein einzelner Stationsmarker kann direkt angeklickt werden. KST4Contest:
|
||||
|
||||
1. übernimmt den konkreten Chatmember als aktuelle Auswahl,
|
||||
2. scrollt die Benutzerliste zum entsprechenden Eintrag,
|
||||
3. aktualisiert den **Further Info**-Bereich und
|
||||
4. bereitet das vollständige sichtbare Rufzeichen als `/cq`-Empfänger vor.
|
||||
|
||||
Marker, die bei der aktuellen Zoomstufe zu dicht beieinanderliegen, werden als Cluster mit einer Stationsanzahl dargestellt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus.
|
||||
|
||||
Die Kopfzeile ergänzt bei ausgewählter Station:
|
||||
|
||||
- vollständiges Rufzeichen,
|
||||
- Locator,
|
||||
- QRB und QTF,
|
||||
- erkannte aktive Bänder,
|
||||
- eine gegebenenfalls vorhandene `B+`-Bandmöglichkeit und
|
||||
- die zuletzt bekannten QRGs.
|
||||
|
||||
Lange Inhalte werden in der Kopfzeile gekürzt. Der vollständige Text steht im Tooltip.
|
||||
|
||||
### Auswahl mit Reset view löschen
|
||||
|
||||
**Reset view** löscht die Stationsauswahl, ohne die Kartenposition oder den Zoomlevel zu verändern.
|
||||
|
||||
Dabei werden:
|
||||
|
||||
- die ausgewählte Station zurückgesetzt,
|
||||
- die Auswahl in der Benutzerliste aufgehoben,
|
||||
- die Verbindungslinie zur Gegenstation entfernt,
|
||||
- eine noch laufende Auswertung der vorherigen Station verworfen und
|
||||
- der rechte Analysebereich entfernt.
|
||||
|
||||
Die Karte selbst bleibt im zuvor gewählten Ausschnitt. Die Funktion ist deshalb kein geografischer Reset auf den eigenen Standort.
|
||||
|
||||

|
||||
|
||||
Wird anschließend wieder ein einzelner Marker gewählt, erscheinen Stationsauswahl und Analysebereich erneut.
|
||||
|
||||
### DX-Cluster-Spot auslösen
|
||||
|
||||
**Trigger cluster spot** erscheint nur bei ausgewählter Station. Die Schaltfläche sendet einen einzelnen Spot an die mit dem integrierten DX-Cluster-Server verbundenen Logprogramme.
|
||||
|
||||
Vorausgesetzt werden:
|
||||
|
||||
- ein aktivierter lokaler DX-Cluster-Server,
|
||||
- mindestens ein verbundener Cluster-Client und
|
||||
- eine für die ausgewählte Station verwendbare QRG.
|
||||
|
||||
Der Spot wird nicht an einen öffentlichen Internet-Cluster gesendet.
|
||||
|
||||
### Streckenanalyse
|
||||
|
||||
Unterhalb der Karte befindet sich das Höhenprofil. Der rechte Analysebereich zeigt unter anderem:
|
||||
|
||||
- verwendete Datenquelle und Anzahl der Höhenpunkte,
|
||||
- Analysefrequenz,
|
||||
- Erdkrümmungs- beziehungsweise Refraktionsmodell,
|
||||
- Radio- und Geländehorizont,
|
||||
- Fresnel-Freiheit,
|
||||
- erkannte Hindernisse,
|
||||
- Link-Budget,
|
||||
- geschätzten Empfangspegel und
|
||||
- eine zusammenfassende Pfadbewertung.
|
||||
|
||||
Die Auswertung verwendet dasselbe zentral hergeleitete Band wie die Reachability-Funktionen. Ein im **Reachability**-Dropdown ausdrücklich gewähltes Band wird berücksichtigt.
|
||||
|
||||
Die Werte bleiben technische Abschätzungen. Gebäude, Bewuchs, lokale Abschattungen, aktuelle Ausbreitungsbedingungen und nicht bekannte Stationsparameter können das reale Ergebnis deutlich verändern.
|
||||
|
||||
### Streckenanalyse ausblenden
|
||||
|
||||
Mit **Hide path analysis** werden Höhenprofil und rechter Analysebereich gemeinsam ausgeblendet. Der Kartenbereich erhält dadurch mehr Platz.
|
||||
|
||||

|
||||
|
||||
Der Hinweis **Path analysis is hidden** und die Schaltfläche **Show path analysis** bleiben sichtbar. Die Funktion kann daher ohne Umweg wieder eingeschaltet werden.
|
||||
|
||||
Ist beim Wiedereinblenden keine Station ausgewählt, erscheint kein leerer rechter Bereich. Er wird erst wieder aufgebaut, nachdem eine konkrete Station gewählt wurde.
|
||||
|
||||
Die Auswahl wird gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||
|
||||
Der Divider zwischen Karte und Detailbereich lässt sich horizontal verschieben. Bei einem schmalen Detailbereich werden längere Angaben umgebrochen; reicht die verfügbare Höhe nicht aus, erscheint eine vertikale Scrollleiste.
|
||||
|
||||
Ausführliche Herleitung und Grenzen: [Stationskarte und Streckenanalyse](de-Funktionen#stationskarte-und-streckenanalyse-ab-v141).
|
||||
|
||||
---
|
||||
|
||||
## Globale Nachrichtentabs und Monitorfenster
|
||||
|
||||
Der untere Bereich des Hauptfensters enthält drei globale Nachrichtentabs. Ihr Inhalt ist nicht von der aktuell in der Benutzerliste ausgewählten Station abhängig.
|
||||
|
||||
| Tab | Inhalt |
|
||||
|---|---|
|
||||
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
|
||||
| **DXCluster messages** | Über ON4KST empfangene DX-Cluster-Meldungen |
|
||||
| **QSO of the other** | Gerichtete Nachrichten zwischen zwei anderen Stationen |
|
||||
|
||||

|
||||
|
||||
Im Tab **QSO of the other** werden Absender und Empfänger getrennt dargestellt. Die Spalten **Last QRG TX** und **Last QRG RX** enthalten die zuletzt für beide Stationen bekannten Frequenzen. Sie geben nicht zwingend die QRG der angezeigten Unterhaltung wieder.
|
||||
|
||||
**wkd TX?** und **wkd RX?** zeigen den globalen Worked-Status der beiden Basisrufzeichen. Die Angaben sind nicht bandbezogen.
|
||||
|
||||
Der Tab **DXCluster messages** zeigt den meldenden und den gemeldeten Teilnehmer, deren Locator, die QRG, den Meldungstext und den globalen Worked-Status der gemeldeten Station. Welche Felder tatsächlich gefüllt sind, hängt von der vom ON4KST-Server übertragenen Meldung ab.
|
||||
|
||||
Nachrichtentexte bleiben einzeilig. Ist eine Zelle zu schmal, erscheint der vollständige Inhalt als Tooltip. Webadressen im Meldungstext lassen sich anklicken.
|
||||
|
||||
### Separates Monitorfenster
|
||||
|
||||
Zusätzlich öffnet KST4Contest das Fenster **Cluster & QSO of the other**. Es zeigt oben die DX-Cluster-Meldungen und darunter die gerichteten Nachrichten zwischen anderen Stationen.
|
||||
|
||||

|
||||
|
||||
Die Position des vertikalen Dividers sowie die Fenstergröße werden zusammen mit den übrigen UI-Einstellungen gespeichert. Nach einer Änderung **Save Settings** verwenden.
|
||||
|
||||
Das Fenster lässt sich über das Menü aus- und wieder einblenden:
|
||||
|
||||
```text
|
||||
Windows → Hide cluster / stranger QSOs
|
||||
Windows → Show cluster / stranger QSOs
|
||||
```
|
||||
|
||||
Die Tabellen im Hauptfenster und im Monitorfenster greifen auf dieselben Daten zu. Das Ausblenden des Monitorfensters beendet daher weder den Empfang noch die Darstellung in den unteren Tabs.
|
||||
|
||||
Herleitung und Grenzen: [Globale Nachrichtenansichten](de-Funktionen#globale-nachrichtenansichten).
|
||||
|
||||
---
|
||||
|
||||
## Menü
|
||||
|
||||
### Window
|
||||
- **Use Dark Mode** (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.
|
||||
### File
|
||||
|
||||
- **Connect to …** startet die Verbindung mit den bereits übernommenen Einstellungen.
|
||||
- **Disconnect** beendet die aktuelle ON4KST-Verbindung, ohne KST4Contest zu schließen.
|
||||
- **Exit + disconnect** beendet die Verbindung und anschließend das Programm.
|
||||
|
||||
Die Connect- und Disconnect-Einträge werden entsprechend dem aktuellen Verbindungszustand aktiviert oder deaktiviert.
|
||||
|
||||
### Options
|
||||
|
||||
- **Set QRG as name in Chat (main category)** sendet `/SETNAME` mit der aktuellen `MYQRG` an die primäre Chat-Kategorie.
|
||||
- **Show me as away in chat** sendet `/AWAY`.
|
||||
- **Show me as active in chat** sendet `/BACK`.
|
||||
- **Show options** blendet das Einstellungsfenster ein beziehungsweise aus.
|
||||
|
||||
Die serverbezogenen Funktionen sind nur bei vollständig aufgebauter ON4KST-Verbindung verfügbar.
|
||||
|
||||
### Windows
|
||||
|
||||
- **Hide cluster / stranger QSOs** beziehungsweise **Show cluster / stranger QSOs** blendet das zusätzliche Cluster- und QSO-Monitorfenster aus oder wieder ein.
|
||||
- **hide options** beziehungsweise **show options** blendet das Einstellungsfenster aus oder wieder ein.
|
||||
- **Use dark mode design** aktiviert das dunkle Farbschema.
|
||||
- **Use default mode design** aktiviert das normale helle Farbschema.
|
||||
- **Show / hide station map** öffnet beziehungsweise schließt das separate Fenster mit Stationskarte und Streckenanalyse.
|
||||
|
||||
---
|
||||
|
||||
## Fenstergrößen und Divider
|
||||
|
||||
Ab **v1.21** werden beim Klick auf **„Save Settings"** auch Fenstergrößen und Divider-Positionen aller Panels in der Konfigurationsdatei gespeichert und beim nächsten Start wiederhergestellt.
|
||||
Beim Klick auf **Save Settings** speichert KST4Contest die Größen der Programmfenster und die Positionen der relevanten Divider in der Konfigurationsdatei. Diese Werte werden beim nächsten Programmstart wiederverwendet.
|
||||
|
||||
Bei Problemen mit der Darstellung: Konfigurationsdatei löschen → KST4Contest erstellt neue Standardwerte.
|
||||
Das Hauptfenster wird beim Start zusätzlich gegen den sichtbaren Bereich des primären Bildschirms geprüft. Ist die gespeicherte Größe zu groß, verkleinert und verschiebt KST4Contest das Fenster so, dass es wieder erreichbar bleibt. Die genaue Herleitung ist unter [Bildschirmgerechte Größe des Hauptfensters](de-Funktionen#bildschirmgerechte-größe-des-hauptfensters-ab-v141) beschrieben.
|
||||
|
||||
Für die übrigen Programmfenster gilt diese zusätzliche Größenbegrenzung derzeit nicht. Wird beispielsweise das separate Monitorfenster nach einem Wechsel auf einen kleineren Bildschirm zu groß dargestellt, muss seine Größe manuell korrigiert und anschließend erneut mit **Save Settings** gespeichert werden.
|
||||
|
||||
Bei einer ungünstigen Aufteilung sollten zuerst die Divider an eine brauchbare Position verschoben und die Einstellungen erneut gespeichert werden. Das Löschen der Konfigurationsdatei setzt zwar die UI-Werte zurück, entfernt aber auch die übrigen gespeicherten Programmeinstellungen und sollte deshalb nur verwendet werden, wenn sich die Oberfläche auf anderem Weg nicht mehr herstellen lässt.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,25 +4,188 @@
|
||||
|
||||
Versionsverlauf von KST4Contest / PraktiKST.
|
||||
|
||||
Die veröffentlichten Stable-Versionen und ihre Programmpakete stehen unter [GitHub Releases](https://github.com/praktimarc/kst4contest/releases). Zusätzlich enthält diese Seite die Änderungen des aktuellen Entwicklungsstands, soweit sie bereits implementiert und geprüft wurden.
|
||||
|
||||
---
|
||||
|
||||
letzter Changelog bitte aus GitHub entnehmen. Der bisherige Changelog
|
||||
## v1.42 – Nightly / in Entwicklung
|
||||
|
||||
## v1.42 (2026-08)
|
||||
- **QRG-Erkennung präzisiert**: Vollständige und relative Frequenzangaben werden weiterhin erkannt. Nackte dreistellige Zahlen werden nur noch mit erkennbarem Frequenzkontext ausgewertet, damit Signalrapporte, Bandangaben und andere Zahlen keine falsche QRG erzeugen.
|
||||
- **Stationsbezogener Bandkontext**: Bei relativen Frequenzen verwendet KST4Contest zuerst einen höchstens 30 Minuten alten Bandkontext desselben Absenders. Erst danach greift das globale Fallback-Band.
|
||||
- **Fallback-Band als Dropdown**: Das globale Fallback kann nur noch aus den tatsächlich unterstützten Bändern ausgewählt werden und gilt für die gesamte QRG-Erkennung, nicht nur für DX-Cluster-Spots.
|
||||
- **QRG-Anzeige vereinheitlicht**: Frequenzen werden in der Benutzerliste und den Nachrichtentabellen mit mindestens drei Nachkommastellen dargestellt.
|
||||
> Stand dieses Abschnitts: 14. August 2026.
|
||||
> v1.42 ist noch kein veröffentlichtes Stable-Release. Bis zur Freigabe können weitere Änderungen hinzukommen.
|
||||
|
||||
## v1.41
|
||||
**Stationskarte, Performance, Reaktionsfähiges UI**
|
||||
v1.42 führt mehrere bisher getrennte Auswertungen zusammen. Bandinformationen, Worked-Status, NOT-QRV-Markierungen, Rufzeichensuffixe und Frequenzen werden dadurch konsistenter in der Benutzerliste, der Stationskarte, der Prioritätsberechnung und den externen Schnittstellen verwendet.
|
||||
|
||||
**Neu:**
|
||||
- **Stationskarte**: Interaktive OpenStreetMap-Karte zeigt die geografische Position aller aktiven Chatmember. Enthält Stationsmarker, Antennen-Kegel, Verbindungslinie zur ausgewählten Station, Maidenhead-Raster-Overlay und ein Wegprofil-Diagramm mit Geländehöhen-Analyse (Fresnel-Zonen, Horizonterkennung). Geländedaten aus Copernicus GLO-30, Open-Meteo API oder Offline-DEM-Import. Aircraft-Scatter-Weganalyse integriert. Funktioniert in AppImage und Flatpak ohne externe CDN-Verbindung (lokaler Tile-Proxy, eingebettetes Leaflet.js).
|
||||
### Neu
|
||||
|
||||
**Geändert:**
|
||||
- **Nachrichten-Tabellen-Limit auf 30.000 erhöht**: Chat- und Nachrichtentabellen sind auf 30.000 Einträge begrenzt. Ältere Nachrichten werden automatisch verworfen, was die Performance bei mehrtägigem Contest-Betrieb stabil hält.
|
||||
- **Bildschirmgerechte Fenstergröße**: Beim Start wird das Hauptfenster auf den aktuellen Bildschirm angepasst. Wenn KST4Contest zuletzt auf einem größeren Monitor betrieben wurde, wird das Fenster automatisch verkleinert. Das UI-Layout ist kompakter und reaktionsfähiger auf kleineren Bildschirmen.
|
||||
- **Sichtbarer ON4KST-Verbindungsstatus:** Ein kompakter `LINK`-Indicator im Hauptfenster zeigt den tatsächlichen Zustand der ON4KST-Verbindung an. Grün bedeutet vollständig angemeldet und synchronisiert, Gelb kennzeichnet Verbindungsaufbau und Synchronisation, Rot eine unterbrochene Verbindung, einen Fehler oder die Wartezeit vor dem nächsten Verbindungsversuch.
|
||||
|
||||
- **Gemeinsame Herleitung verfügbarer Bänder:** Ein zentraler `BandOpportunityResolver` wertet aktuelle QRGs, Bandangaben im Namensfeld, aktive Rufzeichenvarianten, Worked-Informationen und NOT-QRV-Markierungen gemeinsam aus. Benutzerliste, **New bands**, Band-Upgrade-Hinweis, Priority Score, Stationskarte und automatische Bandauswahl verwenden damit dieselbe Grundlage.
|
||||
|
||||
- **Erweiterte Bandanzeige:** Die Bandspalten unterscheiden jetzt:
|
||||
- `X` für auf diesem Band gearbeitet,
|
||||
- `a` für ein angebotenes, noch nicht gearbeitetes Band einer insgesamt neuen Station,
|
||||
- `B+` für ein angebotenes, noch nicht gearbeitetes Band einer bereits auf einem anderen Band gearbeiteten Station und
|
||||
- `o` für ein auf diesem Band bereits gearbeitetes Locator-Großfeld.
|
||||
|
||||
Die Anzeigen können kombiniert werden, beispielsweise als `ao` oder `B+o`. Die zusätzlichen Kennzeichnungen `a` und `o` lassen sich in den GUI-Einstellungen separat ausblenden.
|
||||
|
||||
- **Unterstützung für 50 und 70 MHz:** Beide Bänder stehen in der Stationskonfiguration, den Worked- und NOT-QRV-Funktionen, der Benutzerliste, den Filtern, der internen Datenbank, der UCXLog-Auswertung und dem Win-Test-Listener zur Verfügung. Bereits gespeicherte Datenbanken werden um die benötigten Spalten ergänzt.
|
||||
|
||||
- **Globale Nachrichtentabs:** Öffentliche Nachrichten, ON4KST-DX-Cluster-Meldungen und gerichtete Nachrichten zwischen anderen Stationen können direkt im Hauptfenster angezeigt werden. Das bisherige separate Monitorfenster bleibt zusätzlich verfügbar und verwendet dieselben Nachrichtenspeicher.
|
||||
|
||||
- **Manuelle QTF-Eingabe:** Die aktuelle Antennenrichtung kann auch ohne PSTRotator direkt in KST4Contest geändert werden.
|
||||
|
||||
- **Filter zurücksetzen:** Ein eigener Reset-Button entfernt die aktiven Filterprädikate der Benutzerliste zuverlässig.
|
||||
|
||||
- **Kartencluster:** Räumlich dicht beieinanderliegende Stationen werden bei niedrigen Zoomstufen zusammengefasst. Die ausgewählte Station und relevante Richtungsgelegenheiten bleiben einzeln sichtbar.
|
||||
|
||||
- **Ausblendbare Streckenanalyse:** Geländeprofil und Analysebereich der Stationskarte können vollständig ausgeblendet werden. Die Auswahl wird gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||
|
||||
### Geändert
|
||||
|
||||
- **Sessionbezogene ON4KST-Verbindungssteuerung:** Socket, Reader, Writer, Messagebus und Warteschlangen gehören jetzt zu einer eindeutig identifizierten Verbindungssession. Veraltete Threads einer abgelösten Verbindung können dadurch keine Daten mehr verarbeiten oder die neue Verbindung schließen. `ONLINE` wird erst nach bestätigtem Login und vollständig empfangenen Benutzerlisten gemeldet. Verbindungsaufbau, Login und Synchronisation besitzen feste Zeitlimits; Heartbeats, ausbleibende Eingangsdaten, EOF sowie Lese- und Schreibfehler werden überwacht und lösen bei Bedarf einen kontrollierten Neuaufbau mit Backoff aus.
|
||||
|
||||
- **ON4KST-Protokollbefehle abgesichert:** Ausgehende Befehle werden zentral aufgebaut und auf gültige Kategorien, Locatoren und unerlaubte Frame-Trennzeichen geprüft. Da ON4KST pro TCP-Session nur einen Locator verwaltet, wird für beide Chat-Kategorien der Hauptlocator verwendet und eine abweichende zweite Konfiguration protokolliert, statt widersprüchliche Befehle an den Server zu senden.
|
||||
|
||||
- **QRG-Erkennung präzisiert:** Vollständige und relative Frequenzangaben werden weiterhin erkannt. Nackte dreistellige Zahlen gelten nur noch bei erkennbarem Frequenzkontext als QRG. Signalrapporte, Bandangaben und andere Zahlen erzeugen dadurch seltener falsche Frequenzen.
|
||||
|
||||
- **Stationsbezogener Frequenzkontext:** Bei relativen QRGs verwendet KST4Contest zuerst einen höchstens 30 Minuten alten Bandkontext derselben Station. Erst wenn dieser fehlt, wird das global konfigurierte Fallback-Band verwendet.
|
||||
|
||||
- **Fallback-Band als Dropdown:** Das globale Fallback kann nur noch aus unterstützten Bandwerten ausgewählt werden. Es betrifft die gesamte QRG-Erkennung und nicht nur DX-Cluster-Spots.
|
||||
|
||||
- **Einheitliche QRG-Darstellung:** Frequenzen werden in Benutzer- und Nachrichtentabellen mit mindestens drei Nachkommastellen dargestellt.
|
||||
|
||||
- **Bandabhängige AirScout- und Streckenberechnung:** KST4Contest leitet für jede Station eine möglichst realistische Frequenz aus der aktuellen QRG und den bekannten Bandinformationen ab. AirScout erhält kanonische Bandwerte. Die frühere Zwischenlösung mit 430 MHz wurde durch 432 MHz ersetzt.
|
||||
|
||||
- **Gemeinsame Frequenzherleitung:** AirScout, **Calc selected** und die Pfadanalyse der Stationskarte verwenden denselben `PropagationFrequencyResolver`. Ein im Reachability-Dropdown ausdrücklich gewähltes Band wird bei manuellen Berechnungen berücksichtigt.
|
||||
|
||||
- **Rufzeichenvarianten getrennt verarbeitet:** Aktive Chatmember werden durch das vollständige Rufzeichen einschließlich Suffix und die Chat-Kategorie unterschieden. `DN9APW`, `DN9APW-2` oder vergleichbare Logins bleiben dadurch getrennte Nachrichtenziele.
|
||||
|
||||
- **Gemeinsame Basisinformationen:** Worked-Flags, NOT-QRV-Informationen und der Priority Score werden weiterhin für Varianten desselben Basisrufzeichens gemeinsam ausgewertet. Getrennte Nachrichtenziele führen damit nicht zu widersprüchlichen Worked-Daten.
|
||||
|
||||
- **Priority Score korrigiert:** Stationen ohne gemeinsames verfügbares Band oder mit übersteuernder NOT-QRV-Markierung werden nicht mehr als Prioritätskandidaten angeboten. Bandgelegenheiten bereits gearbeiteter Stationen können einen eigenen Priority Boost erhalten.
|
||||
|
||||
- **Sked-Erstellung erweitert:** Das Band wird aus den lokal aktivierten Bändern gewählt. Für die Win-Test-Übergabe wird `SSB` oder `CW` ausdrücklich ausgewählt, statt den Mode unzuverlässig aus dem Band abzuleiten.
|
||||
|
||||
- **Win-Test-Sked-Übergabe präzisiert:** Die QRG muss zum gewählten Band passen. Sichtbare KST-Suffixe werden für das Logziel entfernt, portable Bestandteile bleiben erhalten und die Zeitangaben der `ADDSKED`-Pakete werden korrekt erzeugt. Ein Fehler bei der Übergabe entfernt den internen KST4Contest-Sked nicht.
|
||||
|
||||
- **Exakte Sked-Ziele:** Timeline und automatische Erinnerungen verwenden das vollständige sichtbare KST-Rufzeichen. Ein Sked für `DN9APW-2` wird nicht versehentlich an eine andere Variante desselben Basisrufzeichens gesendet.
|
||||
|
||||
- **Beacon und Autoantwort überarbeitet:** Beide Chat-Kategorien verwenden einen gemeinsamen Timer, behalten aber getrennte Aktivierungsschalter und Texte. Das zulässige Mindestintervall beträgt zwei Minuten; Nachrichtentexte sind auf 120 Zeichen begrenzt. Die gespeicherte Beacon-Aktivierung wird beim Start aus der Konfiguration übernommen.
|
||||
|
||||
- **Variablen zentral aufgelöst:** Nachrichtenvariablen für Beacons, Shortcuts, Snippets und andere automatisch erzeugte Texte werden über einen gemeinsamen Resolver verarbeitet.
|
||||
|
||||
- **Nachrichtentabellen verbessert:** Abgeschnittene Nachrichtentexte erhalten einen Tooltip mit dem vollständigen Inhalt. Erkannte Webadressen können im Systembrowser geöffnet werden.
|
||||
|
||||
- **Kompaktere Filterleiste:** Die Filter bleiben bei normaler Breite in einer kompakten Anordnung und werden erst dann umgebrochen, wenn der tatsächlich verfügbare Platz nicht mehr ausreicht. Der mittlere Divider kann dadurch weiter verschoben werden.
|
||||
|
||||
- **DXLog-Gesamtlog übernommen:** Der UCXLog-kompatible UDP-Listener verarbeitet neben `contactinfo` auch `contactreplace`. Dadurch kann ein von DXLog.net als vollständiges Log ausgesendeter Datenbestand eingelesen werden.
|
||||
|
||||
- **Versionserkennung verbessert:** Versionsnummern werden semantisch verglichen, damit beispielsweise Patch-Versionen und Nightly-Stände nicht mehr durch eine einfache Fließkommazahl falsch eingeordnet werden.
|
||||
|
||||
### Behoben
|
||||
|
||||
- **Zuverlässige Benutzerliste beim Login:** Ungültige oder unvollständige `UA0`-Teilnehmerdatensätze werden einzeln verworfen und protokolliert, ohne die Verarbeitung der alphabetisch folgenden Teilnehmer abzubrechen. Die gültigen Einträge werden zunächst pro Kategorie gesammelt und erst mit dem ersten zugehörigen `UE`-Abschlussframe vollständig veröffentlicht.
|
||||
|
||||
- **Benutzerliste verschwindet nach dem Login:** ON4KST kann nach Namens-, Status- oder anderen Live-Änderungen weitere `UE`-Frames für dieselbe Kategorie senden. Wiederholte Abschlussframes werden jetzt erkannt und ignoriert, damit eine bereits gefüllte Benutzerliste nicht durch eine leere Momentaufnahme ersetzt wird.
|
||||
|
||||
- **Fehlgeschlagener Erstaufbau und Verbindungsverlust:** Wenn beim Programmstart keine Verbindung zum Server hergestellt werden kann, läuft KST4Contest nicht mehr in eine Endlos- oder Busy-Wait-Schleife. Die Oberfläche bleibt bedienbar und weitere Versuche erfolgen mit begrenztem Backoff. Auch ein vom Server geschlossener oder über längere Zeit stummer Socket wird zuverlässig erkannt.
|
||||
|
||||
- **Messagebus-Protokollierung:** Bereits korrekt verarbeitete ON4KST-Frames werden nicht mehr zusätzlich als `Critical, detected unhandled Chatmessage` gemeldet. Nur tatsächlich unbekannte Telegramme erreichen noch diesen Logzweig.
|
||||
|
||||
- **Passwort im Fehlerlog:** Das ON4KST-Passwort wird beim Verbindungsaufbau nicht mehr im Klartext in die Konsole oder Logdatei geschrieben.
|
||||
|
||||
- **Langzeitfehler der Stationsauswahl:** Die vom Message-Thread verwalteten Chatmember wurden von der JavaFX-Ansicht entkoppelt. Gleichzeitige Änderungen der Daten und Tabellenansicht führen dadurch nicht mehr nach längerer Laufzeit zu fehlerhaften Auswahlmodellen oder Concurrent-Modification-Problemen.
|
||||
|
||||
- **Keine Phantom-Chatmember durch UM3:** Historische oder zusätzliche Servermeldungen erzeugen keine Benutzerlisteneinträge für Stationen, die nicht tatsächlich im Chat angemeldet sind.
|
||||
|
||||
- **Nachrichten an Rufzeichen mit Suffix:** Mehrere gleichzeitig angemeldete Varianten desselben Basisrufzeichens überschreiben sich nicht mehr gegenseitig. Damit wurde [Issue #73](https://github.com/praktimarc/kst4contest/issues/73) behoben.
|
||||
|
||||
- **DX-Cluster-Locatoren:** Sender und gemeldete Station erhalten nicht mehr versehentlich denselben Locator. Damit wurde [Issue #48](https://github.com/praktimarc/kst4contest/issues/48) behoben.
|
||||
|
||||
- **Worked-Anzeige in „QSO of the other“:** Die Worked-Spalten verwenden wieder die jeweils richtige sendende beziehungsweise empfangende Station.
|
||||
|
||||
- **Fehlende SECONDAP-Daten:** Eine nicht vorhandene zweite Aircraft-Scatter-Gelegenheit führt beim Bearbeiten der Anzeige nicht mehr zu einer ungültigen Textauswahl und JavaFX-Exception.
|
||||
|
||||
- **Historische Rufzeichen:** Das Hervorheben oder Anklicken eines Rufzeichens, das nicht mehr in der aktuellen Benutzerliste vorhanden ist, läuft nicht mehr in eine Exception.
|
||||
|
||||
- **Win-Test-Sked-Zeit und Rufzeichen:** Zeitstempel, Band-QRG-Zuordnung sowie die Behandlung von KST-Suffixen und portablen Rufzeichen wurden korrigiert.
|
||||
|
||||
- **Filter-Reset:** Alle Filterprädikate werden tatsächlich entfernt; der sichtbare Zustand des Buttons entspricht wieder dem wirksamen Filterzustand.
|
||||
|
||||
### Dokumentation und Auslieferung
|
||||
|
||||
- Das deutsche und englische Handbuch wurde systematisch mit dem Quellcode abgeglichen, erweitert und mit aktuellen Screenshots versehen. Die Dokumentation erklärt nicht nur die Bedienelemente, sondern auch Herleitung, Datenquellen und Grenzen der Funktionen.
|
||||
|
||||
- Für Arch Linux stehen `kst4contest-bin`, `kst4contest` und `kst4contest-git` im AUR zur Verfügung.
|
||||
|
||||
- Die Downloadseite unterscheidet Stable, Beta und Nightly und bietet die jeweils tatsächlich vorhandenen Pakete für Windows, Linux und macOS an.
|
||||
|
||||
- Nightly-Pakete werden automatisiert aus dem aktuellen `main`-Branch gebaut. Stable- und Beta-Releases verwenden reproduzierbare Paketnamen für die unterstützten Plattformen.
|
||||
|
||||
- **Signierte und notarisierte macOS-Pakete:** Die DMG-Dateien für Apple Silicon und Intel sind mit einer Apple Developer ID signiert und von Apple notarisiert; das Notarisierungsticket ist in der DMG hinterlegt. Der erste Start funktioniert damit per Doppelklick, ohne den bisherigen Umweg über **Öffnen** im Kontextmenü, und die Prüfung gelingt auch ohne Internetverbindung. Betroffen sind Nightly-, Beta- und Stable-Pakete gleichermaßen. Die Windows-Pakete sind weiterhin nicht signiert.
|
||||
|
||||
- **Korrekte Bundle-Kennung und Version unter macOS:** Die Anwendung meldet sich jetzt als `de.x08.KST4Contest` statt als `kst4contest.view` und trägt die tatsächliche Versionsnummer im Bundle. Bisher wies jedes Release im Finder unter **Informationen** die Version `1.0` aus. Bestehende Einstellungen sind davon nicht betroffen, da KST4Contest seine Daten in `~/.praktiKST/` ablegt und nicht an der Bundle-Kennung festmacht.
|
||||
|
||||
### Bekannte Grenzen
|
||||
|
||||
- Die aktive Geländedatenquelle verwendet Open-Meteo mit Copernicus-GLO-90-Daten und höchstens 100 Höhenpunkten pro Strecke.
|
||||
|
||||
- Der atmosphärische K-Faktor ist derzeit fest auf `4/3` eingestellt.
|
||||
|
||||
- Für die Gegenstation wird eine Antennenhöhe von 10 Metern über Grund angenommen.
|
||||
|
||||
- Aircraft-Scatter-Daten aus AirScout und die Geländeanalyse der Stationskarte sind weiterhin getrennte Bewertungen.
|
||||
|
||||
- Eine genauere Konfiguration von Stationshöhe, Frequenz und K-Faktor wird in [Issue #74](https://github.com/praktimarc/kst4contest/issues/74) weiterverfolgt.
|
||||
|
||||
---
|
||||
|
||||
## v1.41.1 (2026-07-08)
|
||||
|
||||
**Hotfix für Texteingabe und Fokusverhalten**
|
||||
|
||||
### Behoben
|
||||
|
||||
- Das Nachrichteneingabefeld wurde nach einiger Zeit beziehungsweise bei bestimmten UI-Aktualisierungen unerwartet geleert.
|
||||
- Beim Filtern oder Auswählen einer Station wurde der Eingabefokus unbeabsichtigt wieder in das Sendefeld verschoben.
|
||||
|
||||
Die korrigierte Version ist als [Release v1.41.1](https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1) verfügbar.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## v1.41.0 (2026-07-01)
|
||||
|
||||
**Stationskarte, begrenzte Nachrichtenspeicher und bildschirmgerechtes Hauptfenster**
|
||||
|
||||
### Neu
|
||||
|
||||
- **Stationskarte:** Eine interaktive OpenStreetMap-Karte stellt aktive Chatmember mit brauchbarem Locator geografisch dar.
|
||||
|
||||
- **Antennensektor und Verbindungslinie:** Die Karte zeigt den aktuellen eigenen QTF, den konfigurierten Antennen-Öffnungswinkel, das maximale QRB und die Verbindung zur ausgewählten Station.
|
||||
|
||||
- **Maidenhead-Raster:** Ein an die Zoomstufe angepasstes Locator-Raster erleichtert die geografische Einordnung.
|
||||
|
||||
- **Geländeprofil:** Für ausgewählte Stationen kann ein Höhenprofil über die Open-Meteo Elevation API berechnet werden. Die aktive Datenquelle verwendet Copernicus GLO-90 und höchstens 100 gleichmäßig verteilte Abfragepunkte.
|
||||
|
||||
- **Geometrische Streckenanalyse:** Die Auswertung berücksichtigt Sichtlinie, Erdkrümmung mit `k = 4/3`, Radio- und Geländehorizont, erste Fresnel-Zone und eine grobe Hindernisabschätzung.
|
||||
|
||||
- **Lokaler Karten-Proxy:** Leaflet wird mit der Anwendung ausgeliefert. Kartenkacheln werden über einen lokalen Proxy geladen, damit keine externe JavaScript-Bibliothek zur Laufzeit nachgeladen werden muss. Für die OpenStreetMap-Kacheln und die Online-Höhendaten ist weiterhin eine Internetverbindung erforderlich.
|
||||
|
||||
### Geändert
|
||||
|
||||
- **Begrenzte Nachrichtenspeicher:** Die globale Chatnachrichtenliste wird oberhalb von 30.000 Einträgen auf 25.000 verkleinert. Der getrennte DX-Cluster-Speicher wird oberhalb von 10.000 Einträgen auf 8.000 verkleinert.
|
||||
|
||||
- **Bildschirmgerechte Startgröße:** Das Hauptfenster wird beim Start gegen den sichtbaren Bereich des primären Bildschirms geprüft und bei Bedarf verkleinert oder verschoben.
|
||||
|
||||
- **Kompaktere Benutzeroberfläche:** Mehrere Bereiche wurden für kleinere Bildschirme und verstellbare Divider angepasst.
|
||||
|
||||
### Einordnung der Kartenfunktion
|
||||
|
||||
Die Stationskarte und AirScout können dieselbe Gegenstation betreffen, führen aber getrennte Berechnungen durch. Aircraft-Scatter-Flugzeuge werden nicht in das Geländeprofil eingerechnet.
|
||||
|
||||
Im Quellcode vorhandene Klassen für Copernicus GLO-30, Offline-DEM-Import und weitere Terrain-Provider waren in v1.41 nicht Bestandteil der aktiv verwendeten Berechnungskette. Die tatsächlich verwendete Online-Höhenquelle ist Open-Meteo auf Basis von Copernicus GLO-90.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -143,15 +143,17 @@ Mehrere DX-Cluster-Clients können gleichzeitig verbunden werden. Ein erzeugter
|
||||
|
||||
## Verbindung testen
|
||||
|
||||
Die Schaltfläche **Send test spot** erzeugt einen neutralen Testeintrag:
|
||||
Die Schaltfläche **Send test spot** erzeugt folgenden Testeintrag:
|
||||
|
||||
```text
|
||||
Spotted callsign: DO5AMF
|
||||
Comment: KST4CONTEST TEST
|
||||
Comment: Testing DXC-Spot: Congrats, you donated $100!
|
||||
Frequency: .300 des konfigurierten Fallback-Bandes
|
||||
```
|
||||
|
||||
Bei einem Fallback-Band von `144` erscheint der Spot daher auf ungefähr `144.300 MHz`.
|
||||
Bei einem Fallback-Band von `144 MHz` erscheint der Spot daher auf ungefähr `144.300 MHz`.
|
||||
|
||||
Der Kommentar ist ein bewusst beibehaltenes Easteregg. Er dient ausschließlich dazu, den Testspot im Logprogramm eindeutig wiederzuerkennen. Eine tatsächliche Spende oder sonstige externe Aktion wird dadurch selbstverständlich nicht ausgelöst.
|
||||
|
||||
Vor dem Test müssen drei Bedingungen erfüllt sein:
|
||||
|
||||
|
||||
@@ -277,9 +277,13 @@ Nicht jede im ON4KST-Chat eingeloggte Station nimmt am gerade laufenden Contest
|
||||
|
||||
KST4Contest kann darauf mit einem vorher festgelegten Text reagieren. Die eingehende Privatnachricht bleibt dabei sichtbar; sie wird weder blockiert noch verworfen. Davon getrennt lässt sich eine QRG-Antwort aktivieren, die auf typische Fragen wie `qrg?`, `freq?` oder `pse qrg` reagiert.
|
||||
|
||||
Bei zwei gleichzeitig geöffneten Chat-Kategorien bleibt der Zusammenhang erhalten: Die Antwort wird in der Kategorie der eingegangenen Nachricht gesendet. Eine QRG-Anfrage erhält außerdem nur die QRG dieser Kategorie und nicht eine Liste aller konfigurierten Frequenzen.
|
||||
Bei zwei gleichzeitig geöffneten Chat-Kategorien bleibt der Zusammenhang erhalten: Die Antwort wird an das vollständige Absenderrufzeichen und in der Kategorie der eingegangenen Nachricht gesendet. Eine QRG-Anfrage erhält außerdem nur die QRG dieser Kategorie und nicht eine Liste aller konfigurierten Frequenzen.
|
||||
|
||||
Automatische Antworten benötigen Grenzen. KST4Contest versieht sie daher mit `[KST4C Automsg]`, ignoriert entsprechend gekennzeichnete Nachrichten bei der allgemeinen und QRG-bezogenen Antwort und begrenzt weitere Antworten an dieselbe Station in derselben Kategorie auf eine Nachricht innerhalb von zwei Minuten. Der Schutz gilt gemeinsam für beide Antwortarten.
|
||||
Fehlt die QRG der betreffenden Kategorie, sendet KST4Contest keine inhaltslose Auskunft. Auch ein leerer oder protokollwidriger allgemeiner Antworttext wird nicht in die Sendequeue übernommen.
|
||||
|
||||
Automatische Antworten benötigen Grenzen. KST4Contest versieht sie daher mit `[KST4C Automsg]`, ignoriert entsprechend gekennzeichnete Nachrichten und begrenzt weitere Antworten an dasselbe vollständige Rufzeichen in derselben Kategorie auf eine Nachricht innerhalb von zwei Minuten. Der Schutz gilt gemeinsam für die allgemeine und die QRG-bezogene Antwort.
|
||||
|
||||
Die Sperrzeit wird nur nach einer vollständigen und lokal gültigen Antwort gestartet. Eine fehlende QRG oder ein verworfener Antworttext verhindert daher keine spätere gültige Antwort.
|
||||
|
||||
Im Klartext: Die Funktion verhindert keine Massenanfragen. Sie verhindert aber, dass der Empfänger jede davon einzeln mit derselben Absage beantworten muss. Sie soll keine Unterhaltung simulieren und erst recht keine endlose Diskussion mit einem zweiten automatischen Client beginnen.
|
||||
|
||||
@@ -287,6 +291,7 @@ Konfiguration, erkannte QRG-Anfragen und genaue Kategorienzuordnung: [Konfigurat
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Multi-Channel-Login (ab v1.26)
|
||||
|
||||
Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Beide Chats werden parallel überwacht.
|
||||
@@ -313,50 +318,200 @@ Für ausgewählte Stationen in der Benutzerliste gibt es direkte Buttons, um das
|
||||
|
||||
---
|
||||
|
||||
## Sked-Erinnerungen mit ALERT (ab v1.40)
|
||||
## Skeds und Sked-Erinnerungen
|
||||
|
||||
Für jeden Chatmember kann ein Sked-Erinnerungsdienst mit automatischen Nachrichten aktiviert werden. Konfigurierbare Intervallmuster:
|
||||
> Verfügbar ab v1.40; Band-, Rufzeichen- und Win-Test-Behandlung erweitert in Nightly / v1.42.
|
||||
|
||||
- **2+1 Minuten**: Nachrichten bei 2 min und 1 min vor dem Sked.
|
||||
- **5+2+1 Minuten**: Nachrichten bei 5, 2 und 1 min vor dem Sked.
|
||||
- **10+5+2+1 Minuten**: Nachrichten bei 10, 5, 2 und 1 min vor dem Sked.
|
||||
Ein Sked ist mehr als eine Erinnerung an eine Uhrzeit. Er muss während des laufenden Contestbetriebs rechtzeitig sichtbar werden, die vereinbarte Station priorisieren und – sofern gewünscht – die Gegenstation noch einmal an den Termin erinnern.
|
||||
|
||||
Zusätzlich zu den Nachrichten an die Gegenstation gibt es eine **akustische und optische Benachrichtigung** für den eigenen Operator, sodass kein Sked vergessen wird.
|
||||
KST4Contest behandelt deshalb drei voneinander unabhängige Aufgaben:
|
||||
|
||||
Aktivierung: FurtherInfo-Panel der entsprechenden Station.
|
||||
1. Der Sked wird intern gespeichert und in die Prioritätsberechnung einbezogen.
|
||||
2. Der Termin erscheint in der AP- und Sked-Timeline.
|
||||
3. Optional werden vor dem Termin automatische Privatnachrichten gesendet.
|
||||
|
||||
Ist der Win-Test-Netzwerk-Listener aktiviert, versucht KST4Contest zusätzlich, den Sked an Win-Test zu übergeben. Ein Problem bei dieser Übergabe löscht oder verhindert den internen Sked nicht.
|
||||
|
||||
### Sked anlegen
|
||||
|
||||
Zuerst die gewünschte Station in der Benutzerliste auswählen. Die Bedienelemente befinden sich anschließend unten im Bereich **Further Info**.
|
||||
|
||||
| Bedienelement | Funktion |
|
||||
|---|---|
|
||||
| **Sked in** | Legt fest, in wie vielen Minuten der Sked stattfinden soll. Verfügbar sind 2 bis 15 sowie 20 Minuten. |
|
||||
| **Band** | Wählt das Band des Skeds. Angeboten werden die unter **Station → my station uses …** aktivierten eigenen Bänder. |
|
||||
| **Mode** | Legt den an Win-Test zu übertragenden Mode fest. Verfügbar sind `SSB` und `CW`. Die Auswahl hat keinen Einfluss auf den internen Sked oder die Reminder-PMs. |
|
||||
| **Create sked** | Legt den internen Sked an und versucht bei aktiviertem Win-Test-Netzwerk-Listener zusätzlich die Übergabe an Win-Test. |
|
||||
| **Remind-PM in** | Aktiviert die automatischen Privatnachrichten vor dem Termin. |
|
||||
| **2+1**, **5+2+1**, **10+5+2+1** | Legt fest, wie viele Minuten vor dem Sked die Reminder-PMs gesendet werden. |
|
||||
|
||||

|
||||
|
||||
KST4Contest versucht, ein sinnvolles Band vorzuwählen. Dafür werden nacheinander folgende Informationen verwendet:
|
||||
|
||||
1. eine höchstens 30 Minuten alte QRG der ausgewählten Station auf einem eigenen aktivierten Band,
|
||||
2. eine eindeutige Bandangabe im Namensfeld der Station und
|
||||
3. das erste aktivierte eigene Band.
|
||||
|
||||
Aktive Rufzeichenvarianten desselben Basisrufzeichens werden bei der Suche nach einer aktuellen Bandinformation gemeinsam betrachtet. Eine manuelle NOT-QRV-Markierung wird bei der automatischen Vorauswahl berücksichtigt. Das Band kann trotzdem ausdrücklich geändert werden, wenn der Operator bewusst eine andere Vereinbarung getroffen hat.
|
||||
|
||||
### Auswirkung auf den Priority Score
|
||||
|
||||
Ein eingetragener Sked erhöht den Score des normalisierten Basisrufzeichens:
|
||||
|
||||
| Zeitraum | Sked-Anteil am Score |
|
||||
|---|---:|
|
||||
| mehr als 15 Minuten vor dem Termin | `+40` |
|
||||
| 15 bis 3 Minuten vor dem Termin | kontinuierlicher Anstieg von `+300` bis in Richtung `+1200` |
|
||||
| weniger als 3 Minuten vor bis 1 Minute nach dem Termin | `+5000` |
|
||||
| später als 1 Minute nach dem Termin | kein Sked-Boost mehr |
|
||||
|
||||
Die starke Gewichtung unmittelbar vor dem Termin ist beabsichtigt. Ein vereinbarter Sked soll dann nicht durch eine gerade sehr aktive, aber nicht fest eingeplante Station aus der Prioritätsliste verdrängt werden.
|
||||
|
||||
Der Score wird für das Basisrufzeichen berechnet. Ein Sked mit `DN9APW-2` beeinflusst daher auch den gemeinsamen Score weiterer aktiver Varianten von `DN9APW`. Das konkrete Nachrichtenziel bleibt trotzdem `DN9APW-2` in der beim Anlegen ausgewählten Chat-Kategorie.
|
||||
|
||||
Fünf Minuten nach dem Termin wird der Sked aus der internen Liste entfernt.
|
||||
|
||||
### Reminder-PMs
|
||||
|
||||
Reminder-PMs werden nur angelegt, wenn **Remind-PM in** aktiviert ist. Je nach ausgewähltem Muster sendet KST4Contest beispielsweise zwei und eine Minute vor dem Sked folgende Privatnachricht:
|
||||
|
||||
```text
|
||||
[KST4C Autoreminder] sked in 2 min
|
||||
```
|
||||
|
||||
Die Nachricht geht an das vollständige sichtbare KST-Rufzeichen und in die Chat-Kategorie, in der der Sked angelegt wurde. Ein Sked für `DN9APW-2` wird daher nicht versehentlich an `DN9APW`, `DN9APW-70` oder eine gleichnamige Station in einer anderen Kategorie gesendet.
|
||||
|
||||
Beim tatsächlichen Reminder zeigt KST4Contest zusätzlich den optischen **SKED**-Hinweis an. Ist die einfache Soundausgabe aktiviert, wird außerdem ein Hinweiston abgespielt. Das bloße Aktivieren des Reminders löst noch kein Blinken aus.
|
||||
|
||||
Wird für dasselbe vollständige Rufzeichen ein neuer Satz Reminder aktiviert, ersetzt dieser die zuvor geplanten Reminder dieses Rufzeichens.
|
||||
|
||||
### Speicherung und Grenzen
|
||||
|
||||
Skeds und Reminder-Zeitpläne werden nur im Arbeitsspeicher geführt. Nach einem Neustart von KST4Contest müssen noch benötigte Termine erneut angelegt werden.
|
||||
|
||||
Die automatische Bandvorauswahl ist eine Herleitung aus vorhandenen Chatinformationen. Sie beweist nicht, dass die Station noch auf der zuletzt genannten QRG arbeitet. Band, Uhrzeit und Mode sollten deshalb vor **Create sked** kontrolliert werden.
|
||||
|
||||
Bedienung: [Stationsinfo-Panel](de-Benutzeroberflaeche#stationsinfo-panel-further-info)
|
||||
|
||||
Darstellung: [AP- und Sked-Timeline](#ap--und-sked-timeline)
|
||||
|
||||
Win-Test-Übergabe: [Log-Synchronisation – Win-Test](de-Log-Synchronisation#win-test)
|
||||
|
||||
---
|
||||
|
||||
## QSO-Monitoring (ab v1.31)
|
||||
|
||||
Für ausgewählte Rufzeichen kann KST4Contest gerichtete Nachrichten zusätzlich in der PM-Tabelle anzeigen. Dabei werden sowohl Nachrichten berücksichtigt, die das überwachte Rufzeichen sendet, als auch Nachrichten, die an dieses Rufzeichen gerichtet sind.
|
||||
Während eines Contests kann es sinnvoll sein, die Kommunikation einer bestimmten Station zu verfolgen. Das betrifft nicht nur seltene Stationen oder DXpeditionen. Auch die Sked-Absprachen einer anderen Station des eigenen Teams können im öffentlichen Nachrichtenaufkommen schnell übersehen werden.
|
||||
|
||||
Die Nachricht bleibt gleichzeitig in ihrer ursprünglichen Tabelle erhalten und wird im PM-Fenster mit Absender und Empfänger als überwachte Kommunikation gekennzeichnet.
|
||||
KST4Contest kann deshalb alle Nachrichten, die eine überwachte Station sendet oder empfängt, zusätzlich in der PM-Tabelle anzeigen. Die Nachricht bleibt gleichzeitig in ihrer ursprünglichen Tabelle erhalten.
|
||||
|
||||
Die Überwachung arbeitet mit dem normalisierten Basisrufzeichen. Ein Eintrag für `DN9APW` erfasst daher auch Nachrichten von oder an:
|
||||
|
||||
```text
|
||||
DN9APW-2
|
||||
DN9APW-70
|
||||
DN9APW-144
|
||||
DN9APW-432
|
||||
```
|
||||
|
||||
Auch wenn `DN9APW-70` in die Monitoring-Liste eingegeben wird, speichert KST4Contest nur `DN9APW`. Eine Station mit mehreren band- oder arbeitsplatzbezogenen KST-Suffixen muss dadurch nur einmal eingetragen werden.
|
||||
|
||||
Die eigentliche Nachricht behält trotzdem die vollständigen Rufzeichen:
|
||||
|
||||
```text
|
||||
Sniffed: (DN9APW-70 > 9A0BB-23) pse sked 19:30
|
||||
```
|
||||
|
||||
Das ist ein wichtiger Unterschied. Das Monitoring fasst die Varianten zusammen, damit keine Nachricht übersehen wird. Das Nachrichtenrouting fasst sie nicht zusammen, weil andernfalls nicht mehr eindeutig wäre, welcher Login tatsächlich angesprochen wurde.
|
||||
|
||||
Das Basisrufzeichen wird außerdem unabhängig von der Chat-Kategorie ausgewertet. Ist KST4Contest gleichzeitig mit zwei Kategorien verbunden, gilt derselbe Monitoring-Eintrag für beide Nachrichtenströme. Die Kategorie der einzelnen Nachricht bleibt dabei erhalten.
|
||||
|
||||
Erfasst werden:
|
||||
|
||||
- gerichtete Nachrichten, die eine überwachte Station sendet,
|
||||
- gerichtete Nachrichten an eine überwachte Station und
|
||||
- öffentliche Nachrichten, die eine überwachte Station an `ALL` sendet.
|
||||
|
||||
Eine bloße Nennung des Rufzeichens im Nachrichtentext löst das Monitoring nicht aus.
|
||||
|
||||
Nachrichten, die bereits direkt an das eigene Rufzeichen gerichtet sind, erscheinen als normale Privatnachrichten. Sie werden nicht zusätzlich als `Sniffed:` gekennzeichnet. Das QSO-Monitoring erzeugt außerdem keinen eigenen Hinweiston. Die akustische Benachrichtigung für tatsächlich an die eigene Station gerichtete Privatnachrichten bleibt davon unabhängig.
|
||||
|
||||
Die Funktion fordert keine zusätzlichen Nachrichten vom ON4KST-Server an. Sie ordnet lediglich den bereits empfangenen Chatverkehr zusätzlich in die PM-Ansicht ein.
|
||||
|
||||
Im Klartext: Das QSO-Monitoring entscheidet nicht, ob eine Nachricht tatsächlich einen Sked enthält oder für den eigenen Contestbetrieb relevant ist. Es sorgt lediglich dafür, dass die Kommunikation der ausgewählten Station nicht zwischen den übrigen Nachrichten verschwindet.
|
||||
|
||||
Konfiguration: [Konfiguration – Sniffer-Einstellungen](de-Konfiguration#sniffer-einstellungen-ab-v131)
|
||||
|
||||
---
|
||||
|
||||
## Win-Test-Integration (ab v1.31, vollständig ab v1.40)
|
||||
|
||||
KST4Contest unterstützt [Win-Test](https://www.win-test.com/) vollständig als Logprogramm:
|
||||
|
||||
- **Log-Synchronisation**: Gearbeitete Stationen werden automatisch aus Win-Test übernommen und in der Benutzerliste markiert.
|
||||
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz wird aus Win-Test-UDP-Paketen ausgewertet und befüllt die `MYQRG`-Variable.
|
||||
- **Sked-Übergabe (SKED Push via UDP)**: Vereinbarte Skeds aus KST4Contest können direkt an Win-Test übertragen werden, sodass das Rufzeichen der Gegenstation im Win-Test-Sked-Fenster erscheint.
|
||||
|
||||
Details zur Konfiguration: [Konfiguration – Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener)
|
||||
Trennung vollständiger Rufzeichen und Chat-Kategorien: [Multi-Channel-Login und Rufzeichenvarianten](#multi-channel-login-ab-v126)
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator-Interface (ab v1.31, vollständig ab v1.40)
|
||||
## Win-Test-Integration
|
||||
|
||||
KST4Contest kann die Antennenrichtung direkt über **PSTRotator** steuern. Wenn in der Benutzerliste eine Station ausgewählt wird, kann der Rotator automatisch auf den QTF zur ausgewählten Station gedreht werden.
|
||||
KST4Contest verwendet für Win-Test einen eigenen Listener für das native Win-Test-Netzwerkprotokoll. Darüber werden drei voneinander getrennte Funktionen bereitgestellt:
|
||||
|
||||
Konfiguration: [Konfiguration – PSTRotator-Einstellungen](de-Konfiguration#pstrotator-einstellungen-ab-v131)
|
||||
- neue QSOs einschließlich Band- und gegebenenfalls Locatorinformation übernehmen,
|
||||
- die aktuelle QRG aus Win-Test-STATUS-Paketen auswerten und
|
||||
- intern angelegte Skeds als `ADDSKED` an das Win-Test-Netzwerk übergeben.
|
||||
|
||||
Bei der Sked-Übergabe wird die QRG nicht durch eine feste Standardfrequenz ersetzt. KST4Contest sendet nur dann einen Win-Test-Sked, wenn eine zum ausgewählten Band passende QRG ermittelt werden konnte. Der interne Sked, die Timeline und die Reminder-PMs funktionieren unabhängig davon weiter.
|
||||
|
||||
Ein sichtbarer KST-Suffix wie `-2`, `-70` oder `-144` bleibt innerhalb von KST4Contest erhalten, wird für das Win-Test-Logrufzeichen jedoch entfernt. Portable Bestandteile wie `/P`, `/M` oder ein Länderpräfix bleiben bestehen.
|
||||
|
||||
Einrichtung und genaue Datenbehandlung: [Log-Synchronisation – Win-Test](de-Log-Synchronisation#win-test)
|
||||
|
||||
Einstellungen: [Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener-ab-v131)
|
||||
|
||||
|
||||
|
||||
## PSTRotator-Interface (ab v1.31, vollständig konfigurierbar ab v1.40)
|
||||
|
||||
KST4Contest kann die Antenne über die UDP-Schnittstelle von PSTRotator auf die ausgewählte Gegenstation drehen. Der dafür verwendete Azimut wird aus dem eigenen und dem fremden Locator berechnet.
|
||||
|
||||
Nach der Auswahl einer Station steht im **Further Info**-Bereich der Button **Turn ant1 to …** zur Verfügung:
|
||||
|
||||

|
||||
|
||||
Ein Klick führt folgende Schritte aus:
|
||||
|
||||
1. KST4Contest beendet den Tracking-Modus von PSTRotator.
|
||||
2. Der QTF der ausgewählten Station wird als ganzzahliger Azimut übertragen.
|
||||
3. PSTRotator steuert den konfigurierten Rotator.
|
||||
4. Die zurückgemeldete Position wird als aktuelle QTF in KST4Contest übernommen.
|
||||
|
||||
Der Button bleibt auch sichtbar, wenn die PSTRotator-Integration deaktiviert ist. In diesem Fall wird kein Rotatorbefehl gesendet.
|
||||
|
||||
### Rückmeldung und SPID-Kompatibilität
|
||||
|
||||
KST4Contest fragt die aktuelle Azimutposition alle zwei Sekunden ab. Die Rückmeldung aktualisiert das eigene QTF-Feld und damit alle Funktionen, die von der Antennenrichtung abhängen.
|
||||
|
||||
Einige SPID-Konfigurationen übernehmen den ersten Richtungsbefehl gelegentlich nicht. KST4Contest prüft deshalb zwei Sekunden nach dem Befehl, ob PSTRotator eine Bewegung beziehungsweise das Erreichen des Ziels gemeldet hat.
|
||||
|
||||
Ist die Position unverändert und wurde das Ziel nicht erreicht, sendet KST4Contest einmalig eine Kompatibilitätssequenz über `0°` und anschließend erneut den eigentlichen Zielwert.
|
||||
|
||||
Diese Prüfung läuft im Hintergrund. Die Benutzeroberfläche bleibt während der zwei Sekunden bedienbar. Wird zwischenzeitlich ein neuer Drehbefehl ausgelöst, ersetzt er die noch ausstehende Prüfung des vorherigen Befehls.
|
||||
|
||||
### Was bestätigt die Positionsanzeige?
|
||||
|
||||
Die angezeigte QTF ist die von PSTRotator gemeldete Azimutposition. Sie bestätigt, dass KST4Contest eine auswertbare UDP-Rückmeldung erhalten hat.
|
||||
|
||||
Sie beweist nicht in jedem Aufbau, dass die Antenne mechanisch exakt in dieser Richtung steht. Das hängt von PSTRotator, dem angeschlossenen Controller, der Kalibrierung, möglichen Offsets und der tatsächlichen Rückmeldung des Rotors ab.
|
||||
|
||||
UDP selbst bestätigt außerdem keine Paketzustellung. Bleibt die QTF unverändert, sollten daher zuerst folgende Punkte geprüft werden:
|
||||
|
||||
- **UDP Control** ist in PSTRotator aktiviert.
|
||||
- Host und Steuerport stimmen überein.
|
||||
- Der Rückmeldeport `Steuerport + 1` ist frei.
|
||||
- Die Firewall lässt beide UDP-Richtungen zu.
|
||||
- PSTRotator zeigt selbst eine plausible Rotorposition an.
|
||||
|
||||
Im Klartext: KST4Contest liefert die Zielrichtung und verarbeitet die gemeldete Position. Die mechanische Realität bleibt Aufgabe des Rotators – und gelegentlich der Blick aus dem Fenster.
|
||||
|
||||
Konfiguration und Portbelegung: [Konfiguration – PSTRotator-Einstellungen](de-Konfiguration#pstrotator-einstellungen-ab-v131-vollstaendig-konfigurierbar-ab-v140).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Band-Upgrade-Hinweis nach einem Logeintrag
|
||||
|
||||
Meldet UCXLog oder Win-Test einen neuen Logeintrag mit Bandinformation, prüft KST4Contest, ob die gearbeitete Station noch ein weiteres gemeinsames Band anbietet.
|
||||
@@ -482,70 +637,361 @@ Zugehörige Einstellungen:
|
||||
|
||||
---
|
||||
|
||||
## AP-Timeline (ab v1.40)
|
||||
## AP- und Sked-Timeline
|
||||
|
||||
Eine visuelle Zeitleiste zeigt für jeden möglichen AP-Ankunftsminuten-Slot bis zu 4 hochbewertete Stationen, die per Aircraft Scatter erreichbar wären. Priorisierungskriterien:
|
||||
Die Timeline stellt bevorstehende Aircraft-Scatter-Gelegenheiten und eingetragene Skeds für die nächsten 30 Minuten gemeinsam dar. Sie beantwortet damit zwei Fragen auf einen Blick:
|
||||
|
||||
- Bevorzugt werden APs mit dem **höchsten Reflexionspotenzial** (nicht unbedingt die schnellste Ankunft).
|
||||
- Stationen, auf die die eigene Antenne nicht zeigt, werden **transparent** dargestellt.
|
||||
- Wann entsteht voraussichtlich eine interessante AP-Gelegenheit?
|
||||
- Welcher bereits vereinbarte Sked nähert sich unabhängig davon?
|
||||
|
||||
So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und über welche Flugzeuge erreichbar sein werden.
|
||||
Weiter in der Zukunft liegende Ereignisse erscheinen rechts. Mit ablaufender Zeit wandern sie nach links in Richtung des aktuellen Zeitpunkts.
|
||||
|
||||
---
|
||||

|
||||
|
||||
### AP-Kandidaten
|
||||
|
||||
AP-Kandidaten erscheinen in den oberen Spuren. Pro Ankunftsminute können bis zu vier ausgewählte Kandidaten dargestellt werden. Die Auswahl berücksichtigt den Priority Score und das von AirScout gemeldete Reflexionspotenzial.
|
||||
|
||||
Die Farbe des AP-Symbols kennzeichnet das Reflexionspotenzial:
|
||||
|
||||
| Farbe | Reflexionspotenzial |
|
||||
|---|---:|
|
||||
| Magenta | mindestens 95 % |
|
||||
| Rot | mindestens 75 % |
|
||||
| Gelb | mindestens 50 % |
|
||||
| Blau | unter 50 % |
|
||||
|
||||
Die Farbe ist keine QSO-Wahrscheinlichkeit. Sie gibt den von AirScout übernommenen Wert für die berechnete Reflexionsgeometrie wieder.
|
||||
|
||||
Ein Klick auf einen AP-Kandidaten wählt den dazugehörigen aktiven Chatmember einschließlich Rufzeichensuffix und Chat-Kategorie aus. Dadurch kann unmittelbar eine passende Nachricht vorbereitet werden.
|
||||
|
||||
### Skeds
|
||||
|
||||
Skeds erscheinen als Rauten in der unteren Spur. Die Beschriftung verwendet das vollständige KST-Rufzeichen, beispielsweise `SKED: DN9APW-2`. Dadurch bleibt erkennbar, welcher konkrete Login für den Termin ausgewählt wurde.
|
||||
|
||||
Der Tooltip eines Skeds zeigt mindestens:
|
||||
|
||||
- das vollständige KST-Rufzeichen,
|
||||
- das vereinbarte Band und
|
||||
- den QTF zur Gegenstation.
|
||||
|
||||
Sind passende AirScout-Daten vorhanden, werden zusätzlich die aktuelle AP-Erreichbarkeit und die nächste berechnete AP-Gelegenheit angezeigt.
|
||||
|
||||
### Berücksichtigung der Antennenrichtung
|
||||
|
||||
Liegt der QTF eines Ereignisses deutlich außerhalb der aktuellen Antennenrichtung, wird dessen Symbol transparenter dargestellt. Die Beschriftung bleibt lesbar. Liegt das Ziel nahe der Mitte des konfigurierten Antennenbereichs, wird das Symbol zusätzlich hervorgehoben.
|
||||
|
||||
Diese Darstellung verändert weder den Sked noch den Priority Score. Sie ist eine optische Hilfe, um Kandidaten in der aktuellen Antennenrichtung schneller zu erkennen.
|
||||
|
||||
Die Timeline ist eine Vorschau. AirScout-Daten können sich ändern, und ein eingetragener Sked garantiert weder eine freie Frequenz noch eine tatsächlich vorhandene Ausbreitungsverbindung.
|
||||
|
||||
## Intervall-Beacon
|
||||
|
||||
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Globale Variablen wie `MYQRG`, `SECONDQRG` oder `MYLOCATOR` werden unmittelbar vor jeder Aussendung aktualisiert.
|
||||
|
||||
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht: Die eigene QRG wird regelmäßig veröffentlicht, ohne dass derselbe Text von Hand wiederholt werden muss.
|
||||
|
||||
Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht. Beim Absuchen oder häufigen Wechseln der QRG sollte er ausgeschaltet werden, damit keine inzwischen falsche Frequenz verbreitet wird. Details: [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
|
||||
Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Der zweite Beacon wird nur gesendet, wenn auch der zweite Chat aktiviert und verbunden ist.
|
||||
|
||||
Globale Variablen wie `MYQRG`, `SECONDQRG`, `MYLOCATOR` oder `MYQTF` werden unmittelbar vor jeder Aussendung neu aufgelöst. Eine zwischenzeitlich vom Logprogramm aktualisierte QRG kann dadurch bereits im nächsten Beacon erscheinen.
|
||||
|
||||
Vor dem Versand prüft KST4Contest den vollständig aufgelösten Nachrichtentext. Leere Nachrichten, Zeilenumbrüche, das Protokoll-Trennzeichen `|` und Texte mit mehr als 120 Zeichen werden nicht gesendet.
|
||||
|
||||
Beim Absuchen oder häufigen Wechseln der QRG sollte der Beacon ausgeschaltet werden. Eine automatisch verbreitete Frequenz ist nur dann hilfreich, wenn dort tatsächlich jemand hört und ruft.
|
||||
|
||||
Konfiguration, Timer-Verhalten und verfügbare Variablen: [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Simplelogfile
|
||||
|
||||
Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||
Details: [Log-Synchronisation](de-Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||
|
||||
---
|
||||
|
||||
## Cluster & QSO der anderen
|
||||
## Globale Nachrichtenansichten
|
||||
|
||||
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
|
||||
Das Stationsinfo-Panel und die PM-Tabelle beantworten Fragen zu einer bestimmten Station oder zur eigenen Kommunikation. Daneben gibt es Nachrichtenströme, die unabhängig von der aktuell ausgewählten Station betrachtet werden müssen.
|
||||
|
||||
Dieses Fenster kann minimiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
|
||||
KST4Contest fasst diese globalen Informationen in drei Ansichten zusammen:
|
||||
|
||||
| Ansicht | Inhalt |
|
||||
|---|---|
|
||||
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
|
||||
| **DXCluster messages** | Vom ON4KST-Server gelieferte DX-Cluster-Meldungen |
|
||||
| **QSO of the other** | Gerichtete Chatnachrichten zwischen zwei anderen Stationen |
|
||||
|
||||
Die Ansichten befinden sich als Tabs im unteren Bereich des Hauptfensters. **Public messages** ist nach dem Programmstart vorausgewählt.
|
||||
|
||||

|
||||
|
||||
### DXCluster messages
|
||||
|
||||
Der Tab **DXCluster messages** zeigt DX-Cluster-Meldungen, die über die bestehende ON4KST-Verbindung empfangen werden. Je nach Inhalt der Meldung stehen folgende Informationen zur Verfügung:
|
||||
|
||||
- Zeitpunkt,
|
||||
- sendende beziehungsweise meldende Station,
|
||||
- deren Locator,
|
||||
- gemeldete Station,
|
||||
- deren Locator,
|
||||
- QRG,
|
||||
- Meldungstext und
|
||||
- globaler Worked-Status der gemeldeten Station.
|
||||
|
||||
Nicht jede vom Server übertragene Meldung enthält alle Felder. Ein leeres Locator- oder Nachrichtenfeld bedeutet deshalb nicht zwangsläufig einen Verarbeitungsfehler.
|
||||
|
||||
Diese Anzeige darf nicht mit dem [integrierten lokalen DX-Cluster-Server](de-DX-Cluster-Server) verwechselt werden. Der Tab zeigt empfangene ON4KST-Clusterinformationen. Der lokale Server erzeugt dagegen aus einer erkannten Richtungsgelegenheit einen Spot und gibt ihn an ein verbundenes Logprogramm weiter.
|
||||
|
||||
### QSO of the other
|
||||
|
||||
Der Tab **QSO of the other** zeigt gerichtete Chatnachrichten, bei denen weder Absender noch Empfänger die eigene Station sind. Öffentliche Nachrichten an `ALL` werden nicht aufgenommen.
|
||||
|
||||
Die Tabelle enthält:
|
||||
|
||||
| Spalte | Bedeutung |
|
||||
|---|---|
|
||||
| **Time** | Zeitpunkt der Chatnachricht |
|
||||
| **Call TX** | Absender der Nachricht |
|
||||
| **Last QRG TX** | zuletzt für den Absender bekannte QRG |
|
||||
| **wkd TX?** | globaler Worked-Status des Absenders |
|
||||
| **Call RX** | Empfänger der Nachricht |
|
||||
| **Last QRG RX** | zuletzt für den Empfänger bekannte QRG |
|
||||
| **wkd RX?** | globaler Worked-Status des Empfängers |
|
||||
| **Message** | Inhalt der gerichteten Nachricht |
|
||||
| **Category** | Chat-Kategorie der Nachricht |
|
||||
|
||||
Die beiden QRG-Spalten zeigen den zuletzt in KST4Contest bekannten Wert der jeweiligen Station. Das ist nicht zwangsläufig die Frequenz, auf der sich die beiden Stationen gerade verabreden. Die QRG kann aus einer früheren Nachricht stammen und sich inzwischen geändert haben.
|
||||
|
||||
Auch die Worked-Spalten sind bewusst bandunabhängig. Ein `X` bedeutet, dass das betreffende Basisrufzeichen auf mindestens einem Band gearbeitet wurde. Daraus folgt nicht, dass es auf der in der Tabelle sichtbaren oder vermuteten QRG bereits gearbeitet wurde.
|
||||
|
||||
Die Bezeichnung **QSO of the other** ist eine praktische Kurzform. Eine gerichtete Nachricht beweist weder, dass anschließend ein Funkkontakt zustande kam, noch dass beide Stationen tatsächlich auf derselben Frequenz arbeiten. Die Ansicht zeigt beobachtbare Koordination im Chat – nicht das Logbuch der anderen Stationen.
|
||||
|
||||
### Zusätzliches Monitorfenster
|
||||
|
||||
Dieselben DX-Cluster-Meldungen und gerichteten Nachrichten stehen weiterhin im separaten Fenster **Cluster & QSO of the other** zur Verfügung. Dort erscheinen die DX-Cluster-Tabelle oben und die Nachrichten zwischen anderen Stationen darunter.
|
||||
|
||||

|
||||
|
||||
Das separate Fenster und die Tabs verwenden dieselben zugrunde liegenden Listen. Eine Meldung wird dadurch nicht doppelt empfangen oder doppelt gespeichert. Es handelt sich lediglich um zwei Darstellungen derselben Daten.
|
||||
|
||||
Das Monitorfenster kann über **Windows → Hide cluster / stranger QSOs** ausgeblendet und mit **Show cluster / stranger QSOs** wieder eingeblendet werden. Wer den Platz nicht benötigt, kann das Fenster daher schließen oder minimieren, ohne auf die entsprechenden Tabs im Hauptfenster verzichten zu müssen.
|
||||
|
||||
Die vollständigen Texte abgeschnittener Nachrichten erscheinen als Tooltip. Erkannte Webadressen können wie in den übrigen Nachrichtentabellen angeklickt und im Standardbrowser geöffnet werden.
|
||||
|
||||
Die Ansichten helfen dabei, Aktivität und Koordination anderer Stationen zu erkennen. Bei hohem Chat-Aufkommen entsteht daraus allerdings schnell mehr Information als Erkenntnis. Das separate Fenster ist deshalb vor allem dann nützlich, wenn ein bestimmter Kommunikationsfluss gezielt beobachtet werden soll.
|
||||
|
||||
---
|
||||
|
||||
## Stationskarte (ab v1.41)
|
||||
## Stationskarte und Streckenanalyse (ab v1.41)
|
||||
|
||||
Eine interaktive OpenStreetMap-Karte zeigt die geografische Position aller aktiven Chatmember.
|
||||
Eine lange Benutzerliste beantwortet zwei geografische Fragen nur unzureichend: Wo befinden sich die eingeloggten Stationen, und welche davon liegen ungefähr in der aktuellen Antennenrichtung? Die Stationskarte überträgt deshalb die bereits bekannten Locator-, Richtungs-, Band- und Worked-Informationen in eine interaktive Kartenansicht.
|
||||
|
||||
**Funktionen:**
|
||||
Die Karte ist keine zweite, unabhängig verwaltete Stationsliste. Sie verwendet die aktuell durch die Filter der Benutzerliste sichtbaren Chatmember. Wird beispielsweise nach Entfernung, Richtung, Worked-Status oder einem bestimmten Band gefiltert, wirkt sich dies auch auf die dargestellten Stationen aus. In der Kopfzeile der Karte wird angezeigt, wie viele Stationen sichtbar sind und ob eine gefilterte Ansicht aktiv ist.
|
||||
|
||||
- Stationsmarker mit Rufzeichen-Labels, farblich nach Aktivität und Sked-Status
|
||||
- **Antennen-Kegel** für die eigene Station
|
||||
- **Verbindungslinie** zur aktuell ausgewählten Station
|
||||
- **Maidenhead-Raster** (QRA-Locator-Gitter als Overlay)
|
||||
- **Wegprofil-Diagramm**: Geländehöhen-Querschnitt zwischen eigener und ausgewählter Station, inklusive Fresnel-Zonen-Analyse und Horizonterkennung
|
||||
- Mehrere Terrainquellen: **Copernicus GLO-30** (hochauflösendes DEM), **Open-Meteo API**, synthetischer Fallback und **Offline-DEM-Import** für den Betrieb ohne Internetverbindung
|
||||
- Aircraft-Scatter-Weganalyse verknüpft mit den Geländedaten
|
||||

|
||||
|
||||
Die Karte funktioniert in gepackten Umgebungen (AppImage, Flatpak) ohne Zugriff auf externe CDNs: Die Kartenkacheln werden über einen lokalen Tile-Proxy abgerufen, die Leaflet.js-Bibliothek ist in der Anwendung eingebettet.
|
||||
### Welche Stationen werden dargestellt?
|
||||
|
||||
Für einen Kartenmarker benötigt KST4Contest einen brauchbaren sechsstelligen Locator. Chatmember ohne einen solchen Locator können in der Benutzerliste vorhanden sein, erscheinen aber nicht auf der Karte.
|
||||
|
||||
Mehrere aktive Chat-Einträge desselben Basisrufzeichens werden für die Kartenansicht zusammengefasst. Das verhindert, dass beispielsweise getrennte Logins in mehreren Chat-Kategorien mehrere Marker an derselben geografischen Position erzeugen. Als sichtbares Rufzeichen und für die Detailinformationen wird die zuletzt geeignete aktive Variante verwendet.
|
||||
|
||||
Die Beschriftung eines Markers kann zusätzlich enthalten:
|
||||
|
||||
- die für die Station erkannten aktiven Bänder,
|
||||
- `B+`, wenn mindestens ein eigenes aktiviertes und noch nicht gearbeitetes Band angeboten wird.
|
||||
|
||||
Die Bandangaben verwenden dieselbe Herleitung wie die Bandspalten, der Filter **New bands** und der Priority Score. Aktuelle QRG-Erkennungen, Bandangaben im Namensfeld, Worked-Informationen und manuelle NOT-QRV-Markierungen werden daher auch in der Kartenansicht konsistent berücksichtigt.
|
||||
|
||||
### Bedeutung der Markerfarben
|
||||
|
||||
| Darstellung | Bedeutung |
|
||||
|---|---|
|
||||
| Blauer Rand | Station ohne eine der nachfolgenden besonderen Markierungen |
|
||||
| Gelber Rand | Das Basisrufzeichen wurde bereits auf mindestens einem Band gearbeitet |
|
||||
| Grün | Für die Station besteht eine aus gerichteten Chatnachrichten hergeleitete Richtungsgelegenheit |
|
||||
| Orange | Aktuell ausgewählte Station |
|
||||
|
||||
Treffen mehrere Zustände gleichzeitig zu, hat die für den Betrieb wichtigere Markierung Vorrang. Eine ausgewählte Station bleibt deshalb orange; eine Richtungsgelegenheit wird grün dargestellt, auch wenn das Rufzeichen bereits gearbeitet wurde.
|
||||
|
||||
Bei niedrigen Zoomstufen werden räumlich dicht beieinanderliegende Stationen zu einem Cluster zusammengefasst. Die Zahl im Cluster gibt die Anzahl der enthaltenen Stationen an. Ein Klick zoomt weiter hinein, wählt aber noch keine einzelne Station aus. Die aktuell ausgewählte Station und grün markierte Richtungsgelegenheiten bleiben auch bei niedriger Zoomstufe als einzelne Marker sichtbar.
|
||||
|
||||
### Auswahl und geografische Hilfen
|
||||
|
||||
Ein Klick auf einen einzelnen Stationsmarker:
|
||||
|
||||
1. wählt den dazugehörigen aktiven Chatmember aus,
|
||||
2. scrollt die Benutzerliste zu diesem Eintrag,
|
||||
3. aktualisiert den **Further Info**-Bereich und
|
||||
4. bereitet das Rufzeichen wie bei einer Auswahl in der Benutzerliste als Nachrichtenziel vor.
|
||||
|
||||
Für die ausgewählte Station zeichnet KST4Contest eine Verbindungslinie von der eigenen Station zum Ziel. Der eingezeichnete Antennensektor verwendet:
|
||||
|
||||
- den aktuellen eigenen QTF,
|
||||
- den konfigurierten Antennen-Öffnungswinkel und
|
||||
- das konfigurierte Standard-Maximum-QRB.
|
||||
|
||||
Das Maidenhead-Raster passt seine Genauigkeit an die Zoomstufe und den sichtbaren Kartenausschnitt an. Es dient der räumlichen Orientierung; die Position eines Stationsmarkers wird aus dem sechsstelligen Locator abgeleitet und ist deshalb keine exakte GPS-Position.
|
||||
|
||||
### Strecken- und Geländeprofil
|
||||
|
||||
Nach Auswahl einer Station fordert KST4Contest ein Höhenprofil zwischen dem eigenen und dem fremden Locator an. Die aktive Online-Datenquelle ist die Open-Meteo Elevation API mit Geländedaten auf Basis von **Copernicus GLO-90**.
|
||||
|
||||
Die Online-Abfrage ist auf höchstens 100 gleichmäßig über die Strecke verteilte Höhenpunkte begrenzt. Eine 100 Kilometer lange Strecke wird damit grob im Abstand von etwa einem Kilometer abgetastet. Bei kürzeren Strecken wird der Abstand entsprechend kleiner, schmale Hindernisse können trotzdem zwischen zwei Abfragepunkten liegen.
|
||||
|
||||
Aus den Höhenpunkten berechnet KST4Contest unter anderem:
|
||||
|
||||
- das Geländeprofil,
|
||||
- die geometrische Sichtlinie,
|
||||
- die Erdkrümmung mit einem festen effektiven Erdradiusfaktor von `k = 4/3`,
|
||||
- den geometrischen Radiohorizont beider Stationen,
|
||||
- relevante Geländehorizonte,
|
||||
- die erste Fresnel-Zone,
|
||||
- die geringste Fresnel-Freiheit,
|
||||
- den stärksten erkannten Eingriff in die Fresnel-Zone und
|
||||
- eine grobe Einzelhindernis- beziehungsweise Knife-Edge-Abschätzung.
|
||||
|
||||
Die eingestellte eigene Antennenhöhe wird zur lokalen Geländehöhe addiert. Für die Gegenstation wird derzeit eine feste angenommene Antennenhöhe von 10 Metern über Grund verwendet.
|
||||
|
||||
Bewegst du die Maus über das Profil, wird der dazugehörige Abfragepunkt zusätzlich auf der Karte markiert. Dadurch lässt sich ein auffälliger Berg oder Geländeeinschnitt leichter einer geografischen Position zuordnen.
|
||||
|
||||
### Welche Frequenz wird für die Berechnung verwendet?
|
||||
|
||||
Die Frequenz beeinflusst insbesondere die Größe der Fresnel-Zone, die Freiraumdämpfung und das Link-Budget. KST4Contest versucht deshalb, für die ausgewählte Station eine passende Analysefrequenz zu bestimmen.
|
||||
|
||||
Vorrangig wird eine aktuell bekannte QRG auf einem eigenen aktivierten und für die Gegenstation nutzbaren Band verwendet. Fehlt eine geeignete QRG, wird aus den vorhandenen Bandinformationen ein automatisches Analyseband hergeleitet und dessen Standardfrequenz verwendet. Manuelle NOT-QRV-Markierungen werden dabei berücksichtigt.
|
||||
|
||||
Die tatsächlich verwendete Frequenz steht im Feld **Frequency** der Streckenanalyse. Sie sollte kontrolliert werden, wenn die automatische Zuordnung nicht zur vorgesehenen Funkverbindung passt. Eine Berechnung auf 144 MHz ist für eine geplante Verbindung auf 1296 MHz keine gleichwertige Näherung.
|
||||
|
||||
### Link-Budget und Tropo-Spalte
|
||||
|
||||
Zusätzlich zur geometrischen Bewertung erstellt KST4Contest eine vereinfachte Link-Budget-Abschätzung. Verwendet werden:
|
||||
|
||||
- die konfigurierte eigene Sendeleistung,
|
||||
- der eigene Antennengewinn,
|
||||
- die angenommene Sendeleistung der Gegenstation,
|
||||
- der angenommene Antennengewinn der Gegenstation,
|
||||
- eine frequenzabhängig geschätzte Speiseleitungsdämpfung,
|
||||
- die Freiraumdämpfung und
|
||||
- gegebenenfalls eine grobe zusätzliche Hindernisdämpfung.
|
||||
|
||||
Antennengewinne werden in `dBi` eingegeben. Ein in `dBd` bekannter Wert muss deshalb vor der Eingabe um `2,15 dB` erhöht werden.
|
||||
|
||||
Die Berechnung betrachtet beide Übertragungsrichtungen. Der daraus abgeleitete ungünstigere SSB-Wert wird als Tropo-Marge für die Station gespeichert und kann anschließend in der **Tropo**-Spalte, beim Sortieren und durch den Filter **Tropo >=0dB** verwendet werden.
|
||||
|
||||
Karte und Benutzerliste führen keine voneinander unabhängigen Berechnungen durch. Sie verwenden denselben Reachability-Service und denselben Ergebnisspeicher. Eine über die Karte oder mit **Calc selected** angestoßene Berechnung kann deshalb anschließend auch in der Benutzerliste erscheinen.
|
||||
|
||||
Es wird absichtlich keine automatische Online-Geländeabfrage für jeden sichtbaren Chatmember gestartet. Eine Berechnung erfolgt durch eine ausdrückliche Auswahl in der Karte oder über **Calc selected**. Das begrenzt API-Anfragen und verhindert, dass jede Tabellenaktualisierung eine neue Serie von Höhenabfragen auslöst.
|
||||
|
||||
### Pfadanalyse ausblenden
|
||||
|
||||
Das Geländeprofil und die ausführliche Analyse benötigen einen erheblichen Teil der Fensterhöhe. Werden sie gerade nicht gebraucht, können beide Bereiche gemeinsam mit **Hide path analysis** ausgeblendet werden. Die Karte nutzt den frei werdenden Platz unmittelbar.
|
||||
|
||||

|
||||
|
||||
Mit **Show path analysis** werden Profil und Detailwerte wieder eingeblendet. Das zuletzt berechnete Ergebnis bleibt erhalten. Die gewählte Sichtbarkeit wird in den Einstellungen gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||
|
||||
Der rechte Detailbereich ist über einen Divider in der Breite verstellbar. Er kann so weit verkleinert werden, dass mehr Platz für die Karte entsteht, ohne ein Rufzeichen mit üblicher Länge vollständig zu verdecken.
|
||||
|
||||
### Was sagt die Streckenanalyse nicht aus?
|
||||
|
||||
Die Auswertung ist ein geometrisches und rechnerisches Modell. Sie misst weder die tatsächliche Feldstärke noch die momentanen Ausbreitungsbedingungen.
|
||||
|
||||
Insbesondere kennt KST4Contest nicht:
|
||||
|
||||
- die wirkliche Antennenhöhe und den tatsächlichen Antennengewinn der Gegenstation,
|
||||
- deren Sendeleistung und Speiseleitungsverluste,
|
||||
- Gebäude, Bewuchs und andere Hindernisse, die im Höhenmodell nicht enthalten sind,
|
||||
- lokale Störungen und Empfängereigenschaften,
|
||||
- den aktuellen atmosphärischen K-Faktor,
|
||||
- Inversionsschichten oder Ducting und
|
||||
- die tatsächliche Antennenrichtung der Gegenstation.
|
||||
|
||||
Die unter **Mechanisms** genannten Ausbreitungswege sind eine allgemeine Einordnung anhand der Geländegeometrie. Eine Nennung von Aircraft Scatter bedeutet nicht, dass aktuelle Flugzeuge aus AirScout in die Geländeanalyse eingerechnet wurden.
|
||||
|
||||
Im Klartext: Ein freier Weg ist ein nützlicher positiver Hinweis. Ein geometrisch versperrter Weg bedeutet im VHF-, UHF- und Mikrowellenbereich aber nicht automatisch „unmöglich“. Ebenso garantiert ein positives Link-Budget kein QSO.
|
||||
|
||||
Bedienung: [Stationskarte in der Benutzeroberfläche](de-Benutzeroberflaeche#stationskarte)
|
||||
|
||||
Konfiguration: [Streckenanalyse und Link-Budget](de-Konfiguration#streckenanalyse-und-link-budget)
|
||||
|
||||
---
|
||||
|
||||
## Optimierte Nachrichtenverarbeitung / 30.000-Nachrichten-Limit (ab v1.41)
|
||||
## Begrenzte Nachrichtenspeicher (ab v1.41)
|
||||
|
||||
Die internen Chat- und Nachrichtentabellen sind auf **30.000 Einträge** begrenzt. Ältere Nachrichten werden automatisch verworfen, sobald das Limit erreicht wird. Damit bleiben Speicherverbrauch und Darstellungsperformance auch bei mehrtägigen Contest-Betrieb stabil.
|
||||
Während eines längeren Contests können mehrere zehntausend Chat- und DX-Cluster-Meldungen eintreffen. Würden diese Listen während der gesamten Programmlaufzeit unbegrenzt wachsen, stiege nicht nur der Speicherverbrauch. Auch das Filtern, Sortieren und Aktualisieren der darauf aufbauenden Tabellen würde zunehmend aufwendiger.
|
||||
|
||||
KST4Contest verwendet deshalb zwei getrennte, begrenzte Nachrichtenspeicher:
|
||||
|
||||
| Nachrichtenspeicher | Aufräumen ab | Größe nach dem Aufräumen |
|
||||
|---|---:|---:|
|
||||
| Chatnachrichten | mehr als 30.000 Einträge | 25.000 Einträge |
|
||||
| DX-Cluster-Meldungen | mehr als 10.000 Einträge | 8.000 Einträge |
|
||||
|
||||
Neue Nachrichten werden am Anfang der jeweiligen Liste eingefügt. Wird der obere Grenzwert überschritten, entfernt KST4Contest die ältesten Einträge am Ende der Liste, bis die angegebene Zielgröße erreicht ist.
|
||||
|
||||
### Warum gibt es zwei Grenzwerte?
|
||||
|
||||
Der Speicher wird nicht nach jeder einzelnen Nachricht wieder exakt auf seine Maximalgröße verkleinert. Nach dem Aufräumen bleiben bei den Chatnachrichten 5.000 und bei den DX-Cluster-Meldungen 2.000 freie Plätze.
|
||||
|
||||
Dadurch muss KST4Contest nicht für jede anschließend eintreffende Nachricht erneut Listeneinträge entfernen. Das Aufräumen erfolgt blockweise und damit deutlich seltener.
|
||||
|
||||
### Welche Tabellen teilen sich einen Speicher?
|
||||
|
||||
Die folgenden Ansichten sind gefilterte Darstellungen derselben globalen Chatnachrichtenliste:
|
||||
|
||||
- **Public messages**,
|
||||
- die PM-Tabelle,
|
||||
- die Nachrichten im Bereich **Further Info** und
|
||||
- **QSO of the other**.
|
||||
|
||||
Diese Tabellen speichern nicht jeweils zusätzlich bis zu 30.000 Nachrichten. Wird eine alte Chatnachricht aus dem gemeinsamen Speicher entfernt, verschwindet sie gleichzeitig aus allen darauf basierenden Ansichten.
|
||||
|
||||
Ebenso verwenden der Tab **DXCluster messages** und die DX-Cluster-Tabelle im separaten Monitorfenster denselben Cluster-Speicher. Das zusätzliche Fenster erzeugt weder eine zweite Nachrichtenverbindung noch eine Kopie der empfangenen Meldungen.
|
||||
|
||||
Chatnachrichten und DX-Cluster-Meldungen besitzen dagegen voneinander unabhängige Speicher und Grenzwerte. Ein hohes Aufkommen an öffentlichen Chatnachrichten verkleinert deshalb nicht den DX-Cluster-Speicher und umgekehrt.
|
||||
|
||||
### Keine dauerhafte Historie
|
||||
|
||||
Beide Nachrichtenspeicher liegen ausschließlich im Arbeitsspeicher. Sie werden weder in die interne Worked-Datenbank noch in eine andere lokale Nachrichtendatei geschrieben.
|
||||
|
||||
Nach einem Neustart beginnen die Tabellen wieder mit leeren Listen und werden ausschließlich aus den neu empfangenen Meldungen aufgebaut. Die Ansichten sind damit ein Arbeitsmittel für die laufende Sitzung und kein dauerhaftes Chatarchiv.
|
||||
|
||||
---
|
||||
|
||||
## Bildschirmgerechte Fenstergröße (ab v1.41)
|
||||
## Bildschirmgerechte Größe des Hauptfensters (ab v1.41)
|
||||
|
||||
Beim Programmstart berechnet KST4Contest eine bildschirmgerechte Startgröße für das Hauptfenster:
|
||||
KST4Contest speichert die zuletzt verwendete Größe des Hauptfensters. Das ist praktisch, solange das Programm beim nächsten Start auf einem vergleichbaren Bildschirm läuft. Wurde die Anwendung zuvor auf einem größeren Monitor verwendet, kann die gespeicherte Größe auf einem kleineren Bildschirm jedoch außerhalb des sichtbaren Bereichs liegen.
|
||||
|
||||
- Die gespeicherte Fenstergröße aus der letzten Session wird verwendet – aber **niemals größer als der aktuelle Bildschirm**.
|
||||
- Wenn KST4Contest zuletzt auf einem größeren Monitor betrieben wurde, wird das Fenster automatisch auf die aktuelle Anzeige verkleinert.
|
||||
- Das UI-Layout ist **kompakter und reaktionsfähiger auf kleineren Bildschirmen**.
|
||||
KST4Contest prüft die gespeicherte Größe deshalb beim Programmstart gegen den nutzbaren Bereich des primären Bildschirms.
|
||||
|
||||
Damit werden unbrauchbare, abgeschnittene Fenster beim Wechsel zwischen Geräten oder Monitoren verhindert.
|
||||
### Wie wird die Startgröße bestimmt?
|
||||
|
||||
Sofern die gespeicherten Werte gültig sind, verwendet KST4Contest zunächst die zuletzt gespeicherte Höhe und Breite. Fehlen brauchbare Werte, gilt eine Standardgröße von:
|
||||
|
||||
- 1.234 Pixel Breite und
|
||||
- 768 Pixel Höhe.
|
||||
|
||||
Als verfügbare Fläche verwendet KST4Contest nicht die vollständige Bildschirmauflösung, sondern den von JavaFX gemeldeten sichtbaren Bereich des primären Bildschirms. Taskleiste, Dock und vergleichbare Bereiche des Betriebssystems sind darin bereits ausgenommen.
|
||||
|
||||
Von dieser Fläche wird zusätzlich ein Sicherheitsabstand von 40 Pixeln abgezogen. Überschreitet die gespeicherte Breite oder Höhe den verbleibenden Platz, wird nur der betreffende Wert verkleinert.
|
||||
|
||||
Nachdem die Oberfläche mit dieser Scene-Größe aufgebaut wurde, prüft KST4Contest zusätzlich das tatsächliche native Fenster einschließlich seiner vom Betriebssystem erzeugten Rahmen und Titelleiste. Das Fenster wird bei Bedarf noch einmal verkleinert oder in den sichtbaren Bereich verschoben.
|
||||
|
||||
Damit werden zwei unterschiedliche Fälle abgefangen:
|
||||
|
||||
1. Die gespeicherte Inhaltsfläche ist größer als der aktuelle Bildschirm.
|
||||
2. Die Inhaltsfläche passt, das vollständige native Fenster ragt durch Rahmen oder Position trotzdem über den sichtbaren Bereich hinaus.
|
||||
|
||||
### Was passiert mit dem Layout?
|
||||
|
||||
Die Oberfläche wird nicht als Ganzes proportional skaliert. Stattdessen erhält das Hauptfenster weniger Platz, und die dafür vorgesehenen UI-Bereiche reagieren auf die verfügbare Breite.
|
||||
|
||||
Die Filterleiste bleibt bei normaler Fensterbreite kompakt. Erst wenn der tatsächlich benötigte Platz nicht mehr ausreicht, werden Bedienelemente in zusätzliche Zeilen umgebrochen. Divider können weiterhin verwendet werden, um den Platz zwischen den Nachrichten- und Stationsbereichen aufzuteilen.
|
||||
|
||||
### Grenzen der automatischen Korrektur
|
||||
|
||||
Die Prüfung verwendet immer den **primären Bildschirm**. Sie stellt nicht die frühere Position auf einem bestimmten sekundären Monitor wieder her.
|
||||
|
||||
Die automatische Größenbegrenzung gilt derzeit außerdem nur für das Hauptfenster. Das Einstellungsfenster, das separate Cluster- und QSO-Monitorfenster sowie weitere Zusatzfenster verwenden weiterhin ihre jeweils gespeicherten Größen, ohne dieselbe zusätzliche Prüfung gegen den primären Bildschirm.
|
||||
|
||||
Im Klartext: Die Schutzfunktion verhindert vor allem, dass das zentrale Hauptfenster nach einem Wechsel auf einen kleineren Bildschirm unbenutzbar startet. Sie ist keine vollständige Verwaltung aller Fensterpositionen in einem wechselnden Mehrmonitor-Setup.
|
||||
@@ -50,15 +50,15 @@ Download, unterstützte Betriebssysteme und Installationswege sind im Kapitel [I
|
||||
|
||||
## Versionsstand dieses Handbuchs
|
||||
|
||||
Dieses Handbuch beschreibt die stabile Version **v1.41.1**.
|
||||
Dieses Handbuch unterscheidet zwischen der veröffentlichten Stable-Version und dem aktuellen Entwicklungsstand.
|
||||
|
||||
Funktionen oder Änderungen, die nur im aktuellen Entwicklungsstand enthalten sind, werden ausdrücklich als **Nightly / v1.42** gekennzeichnet. Fehlt eine solche Kennzeichnung, bezieht sich die Beschreibung auf die stabile Version.
|
||||
Die derzeit veröffentlichte Stable-Version ist **v1.41.1**. Funktionen oder Änderungen, die erst im Entwicklungsstand für v1.42 enthalten sind, werden ausdrücklich als **Nightly / v1.42** gekennzeichnet. Fehlt eine solche Kennzeichnung, bezieht sich die Beschreibung auf die Stable-Version.
|
||||
|
||||
- [Aktuelle stabile Version herunterladen](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Nightly-Builds und automatisierte Builds](https://github.com/praktimarc/kst4contest/actions)
|
||||
- [Versionsgeschichte](de-Changelog)
|
||||
- [Stable, Beta und Nightly herunterladen](https://kst4contest.hamradioonline.de/download/)
|
||||
- [Veröffentlichte GitHub Releases](https://github.com/praktimarc/kst4contest/releases)
|
||||
- [Versionsgeschichte und aktueller Nightly-Stand](de-Changelog)
|
||||
|
||||
Für einen Contest ist grundsätzlich die stabile Version zu empfehlen. Nightly-Builds enthalten neuere Korrekturen und Funktionen, können sich aber zwischen zwei Builds verändern. Sie sind sinnvoll, wenn eine bestimmte Änderung getestet werden soll – weniger sinnvoll ist der erste Versuch zehn Minuten vor Contestbeginn.
|
||||
Für einen Contest ist grundsätzlich die Stable-Version zu empfehlen. Nightly-Builds enthalten neuere Korrekturen und Funktionen, können sich aber zwischen zwei Builds verändern. Sie sind sinnvoll, wenn eine konkrete Änderung getestet werden soll. Der erste Versuch zehn Minuten vor Contestbeginn ist dagegen meistens eine recht effiziente Methode, gleichzeitig Software und Operator zu testen.
|
||||
|
||||
---
|
||||
|
||||
@@ -80,7 +80,7 @@ Für einen Contest ist grundsätzlich die stabile Version zu empfehlen. Nightly-
|
||||
|
||||
## Kontakt und Support
|
||||
|
||||
- **Download:** [Aktuelle stabile Version](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- **Download:** [Stable, Beta und Nightly](https://kst4contest.hamradioonline.de/download/)
|
||||
- **Quellcode:** [praktimarc/kst4contest](https://github.com/praktimarc/kst4contest)
|
||||
- **Fehler und Funktionswünsche:** [GitHub Issues](https://github.com/praktimarc/kst4contest/issues)
|
||||
- **E-Mail:** praktimarc+kst4contest@gmail.com
|
||||
@@ -112,6 +112,17 @@ In Datei- und Verzeichnisnamen wird teilweise noch der technische Name `praktiKS
|
||||
|
||||
## Danksagungen
|
||||
|
||||
KST4Contest wurde durch Rückmeldungen aus dem praktischen Contest-Betrieb wesentlich verbessert.
|
||||
KST4Contest wurde durch Rückmeldungen aus dem praktischen Contest-Betrieb wesentlich verbessert. Viele Funktionen entstanden nicht aus einer theoretischen Anforderungsliste, sondern aus Situationen, in denen während eines Contests eine Information fehlte, zu spät sichtbar wurde oder schlicht an der falschen Stelle stand.
|
||||
|
||||
Besonderer Dank gilt Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), Viliam Petrik (OM0AAO, Idee zum DX-Cluster), Konrad Neitzel (DC9DJ, Projektstruktur), Andreas (DO5ALF, Webmaster von funkerportal.de), Franz van Velzen (PE0WGA, Tester), DN9APW als neuem Entwickler im Team und Master über die CI/CD pipelines sowie allen weiteren Testern und Ideengebern.
|
||||
Besonderer Dank gilt:
|
||||
|
||||
- Gianluca Costantino (IU3OAR),
|
||||
- Alessandro Murador (IZ3VTH),
|
||||
- Reczetár István (HA1FV),
|
||||
- Viliam Petrik (OM0AAO) für die Idee zum integrierten DX-Cluster,
|
||||
- Konrad Neitzel (DC9DJ) für Unterstützung bei der Projektstruktur,
|
||||
- Andreas (DO5ALF), Webmaster von funkerportal.de,
|
||||
- Franz van Velzen (PE0WGA) für Tests sowie
|
||||
- DN9APW (Philipp Wagner), der seit Mai 2026 an der Entwicklung und den CI/CD-Pipelines mitarbeitet.
|
||||
|
||||
Ebenso wichtig sind die Fehlermeldungen, Tests und konkreten Betriebserfahrungen aller weiteren Nutzer. Eine genaue Beschreibung eines reproduzierbaren Problems hilft dem Projekt meistens mehr als ein allgemeines „läuft gut“ – auch wenn Letzteres natürlich angenehmer zu lesen ist.
|
||||
|
||||
@@ -267,7 +267,19 @@ Für Apple-Silicon-Macs wird das Paket mit `arm64` benötigt. Für Intel-Macs is
|
||||
3. Ziehe `KST4Contest.app` in den Ordner **Programme**.
|
||||
4. Starte KST4Contest aus dem Programme-Ordner oder über das Launchpad.
|
||||
|
||||
Die Anwendung ist derzeit nicht von Apple notarisiert. macOS kann den ersten Start deshalb blockieren.
|
||||
Ab Version 1.42 sind die macOS-Pakete mit einer Apple Developer ID signiert und von Apple notarisiert. Der erste Start funktioniert damit per Doppelklick, ohne Umweg über das Kontextmenü und ohne Sicherheitsabfrage. Das Notarisierungsticket liegt in der DMG-Datei selbst, sodass die Prüfung auch ohne Internetverbindung gelingt.
|
||||
|
||||
Wer nachsehen möchte, ob ein heruntergeladenes Paket wirklich signiert ist, kann das im Terminal prüfen:
|
||||
|
||||
```bash
|
||||
spctl --assess --type open --context context:primary-signature -v KST4Contest-v<Version>-macos-arm64.dmg
|
||||
```
|
||||
|
||||
Erwartet wird `accepted` zusammen mit `source=Notarized Developer ID`.
|
||||
|
||||
### Versionen bis einschließlich 1.41.1
|
||||
|
||||
Ältere Pakete sind nicht notarisiert. macOS blockiert den ersten Start deshalb.
|
||||
|
||||
Falls die Anwendung aus dem offiziellen GitHub Release stammt:
|
||||
|
||||
@@ -408,7 +420,9 @@ Starte die Datei anschließend aus einem Terminal. Fehlermeldungen sind dort mei
|
||||
|
||||
### macOS blockiert die Anwendung
|
||||
|
||||
Verwende die unter [Installation unter macOS](#installation-unter-macos) beschriebene Funktion **Öffnen** im Kontextmenü. Prüfe vorher, ob die DMG-Datei aus dem offiziellen GitHub Release stammt.
|
||||
Ab Version 1.42 sollte das nicht mehr vorkommen, da die Pakete signiert und notarisiert sind. Tritt die Blockade trotzdem auf, ist die DMG-Datei meist unvollständig heruntergeladen oder nachträglich verändert worden. Lade sie in dem Fall erneut aus dem offiziellen GitHub Release.
|
||||
|
||||
Bei älteren Versionen ist die Blockade zu erwarten. Verwende dort die unter [Installation unter macOS](#installation-unter-macos) beschriebene Funktion **Öffnen** im Kontextmenü.
|
||||
|
||||
### Das Problem bleibt bestehen
|
||||
|
||||
|
||||
@@ -62,17 +62,46 @@ Der Wert sollte zum eigenen Stationsaufbau und zum vorgesehenen Contestbetrieb p
|
||||
|
||||
---
|
||||
|
||||
## Server-Einstellungen (ab v1.31)
|
||||
### Streckenanalyse und Link-Budget
|
||||
|
||||
Der Chat-Server-DNS und -Port sind in den Preferences konfigurierbar:
|
||||
Die Stationskarte verwendet mehrere Werte aus dem Reiter **Station**, um das Geländeprofil und das Link-Budget zur ausgewählten Gegenstation zu berechnen. Die Angaben zum eigenen Stationsaufbau sollten deshalb möglichst realistisch sein. Bei der Gegenstation handelt es sich dagegen um globale Annahmen, solange keine genaueren Daten vorliegen.
|
||||
|
||||
- **Server-DNS**: Standard `www.on4kst.org` (ab v1.31 geändert von `www.on4kst.info`).
|
||||
- **Port**: Standardport des ON4KST-Servers.
|
||||
| Einstellung | Verwendung |
|
||||
|---|---|
|
||||
| **Own antenna height AGL** | Höhe der eigenen Antenne über dem lokalen Gelände in Metern |
|
||||
| **Own TX power W** | Eigene Sendeleistung in Watt |
|
||||
| **Own ant. gain dBi** | Gewinn der eigenen Antenne in dBi |
|
||||
| **DX OM TX power W** | Angenommene Sendeleistung der Gegenstation in Watt |
|
||||
| **DX OM ant. gain dBi** | Angenommener Antennengewinn der Gegenstation in dBi |
|
||||
|
||||
Eine Änderung ist nur notwendig, wenn der Server umzieht oder ein alternativer Endpunkt genutzt wird.
|
||||
**AGL** bedeutet *Above Ground Level*. Trage hier nicht die Höhe über dem Meeresspiegel ein. Die Geländehöhe am eigenen Standort stammt bereits aus dem Höhenprofil; KST4Contest addiert die konfigurierte Antennenhöhe zu diesem Wert.
|
||||
|
||||
Für die Gegenstation verwendet KST4Contest derzeit eine feste Antennenhöhe von 10 Metern über dem lokalen Gelände. Sendeleistung und Antennengewinn der Gegenstation stammen aus den beiden **DX OM**-Feldern. Diese Werte sind bewusst nur Annahmen: Der ON4KST-Chat überträgt weder die tatsächliche Antennenhöhe noch die vollständigen Stationsdaten der Gegenstation.
|
||||
|
||||
Antennengewinne müssen in `dBi` eingetragen werden. Liegt ein Wert in `dBd` vor, muss vor der Eingabe `2.15 dB` addiert werden:
|
||||
|
||||
```text
|
||||
dBi = dBd + 2.15 dB
|
||||
```
|
||||
|
||||
Die aktuelle QTF, der konfigurierte Antennen-Öffnungswinkel und das Standard-Maximum-QRB beeinflussen die Darstellung der Stationskarte. Die für die eigene Station aktivierten Bänder und die für die Gegenstation hergeleitete Frequenz wirken sich zusätzlich auf die Auswahl der Analysefrequenz aus.
|
||||
|
||||
Für das Geländeprofil berücksichtigt KST4Contest die Antennenhöhen, das Höhenmodell und die Erdkrümmung mit einem festen effektiven Erdradiusfaktor von `k = 4/3`. Das Link-Budget verwendet außerdem:
|
||||
|
||||
- die Entfernung und die aktuelle Analysefrequenz,
|
||||
- die Sendeleistungen und Antennengewinne beider Stationen,
|
||||
- frequenzabhängig geschätzte Speiseleitungsverluste,
|
||||
- die Freiraumdämpfung und
|
||||
- eine grobe zusätzliche Dämpfung durch das maßgebliche Hindernis im Geländeprofil.
|
||||
|
||||
Die Berechnung erfolgt für beide Übertragungsrichtungen. Für die gemeinsame SSB- beziehungsweise CW-Marge ist die ungünstigere Richtung maßgeblich. Zu optimistische Leistungs- oder Antennenwerte verbessern daher zwar das angezeigte Ergebnis, nicht aber den realen Funkweg.
|
||||
|
||||
Die Werte bleiben technische Abschätzungen. Aktuelle Ausbreitungsbedingungen, lokale Abschattungen, Bewuchs, Gebäude, Störungen und nicht bekannte Stationsparameter können das tatsächliche Ergebnis deutlich verändern. Bedienung, Frequenzauswahl und Grenzen der Berechnung sind unter [Stationskarte und Streckenanalyse](de-Funktionen#stationskarte-und-streckenanalyse-ab-v141) beschrieben.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Log-Sync-Einstellungen
|
||||
|
||||
Drei Methoden stehen zur Verfügung, um gearbeitete Stationen automatisch zu markieren. Details: [Log-Synchronisation](de-Log-Synchronisation).
|
||||
@@ -93,18 +122,197 @@ Dedizierter Netzwerk-Erkenner für Win-Test. KST4Contest empfängt und verarbeit
|
||||
|
||||
## TRX-Sync-Einstellungen
|
||||
|
||||
Empfängt die aktuelle Frequenz des Transceivers vom Logprogramm via UDP. Ermöglicht die automatische Befüllung der Variable `MYQRG`. Nützlich für:
|
||||
Die TRX-Synchronisation übernimmt die aktuelle Frequenz aus dem Logprogramm und stellt sie in KST4Contest als eigene QRG der ersten Chat-Kategorie bereit. QSO- und Frequenzsynchronisation verwenden teilweise denselben UDP-Empfänger, sind funktional aber voneinander getrennt: Ein empfangenes `RadioInfo`-Paket markiert keine Station als gearbeitet, und ein QSO-Paket ändert nicht automatisch die eigene QRG.
|
||||
|
||||
- Schnelles Einfügen der eigenen QRG in Chat-Nachrichten.
|
||||
- Automatische CQ-Baken mit aktueller Frequenz.
|
||||

|
||||
|
||||
> **Hinweis für Multi-Setup**: Wenn zwei Logprogramme an zwei Computern betrieben werden, aber nur eine KST4Contest-Instanz, darf nur ein Logprogramm die Frequenzpakete senden. KST4Contest kann nicht zwischen den Quellen unterscheiden.
|
||||
### Verfügbare QRG-Quellen
|
||||
|
||||
| Quelle | Aktivierung | Verhalten |
|
||||
|---|---|---|
|
||||
| **Allgemeiner RadioInfo-Listener** | `Update MYQRG from RadioInfo messages received on the shared log-sync port` | Verarbeitet kompatible `RadioInfo`-Pakete auf dem gemeinsam mit der QSO-Synchronisation verwendeten UDP-Port. Der Standardport ist `12060`. |
|
||||
| **Win-Test STATUS** | `Win-Test STATUS QRG Sync` | Verarbeitet die Haupt- oder Pass-Frequenz aus nativen Win-Test-`STATUS`-Paketen. Der Win-Test-Listener verwendet seinen separat konfigurierten Port, standardmäßig `9871`. |
|
||||
| **Manuelle Eingabe** | Beide automatischen QRG-Quellen deaktivieren | Die eigene QRG kann im Hauptfenster von Hand eingetragen werden. |
|
||||
|
||||
Der allgemeine Listener ist für Logprogramme vorgesehen, die kompatible `RadioInfo`-Pakete senden. Dazu gehören – abhängig von deren jeweiliger Konfiguration – UCXLog, N1MM+, QARTest und DXLog.net. QSO- und `RadioInfo`-Pakete verwenden denselben unter **Log sync** konfigurierten Port. Dort wird jedoch getrennt festgelegt, ob KST4Contest QSO-Informationen, TRX-Informationen oder beide Paketarten verarbeitet.
|
||||
|
||||
Wird der gemeinsame UDP-Port geändert, muss KST4Contest neu gestartet werden. Eine reine Änderung der Checkboxen wird dagegen sofort berücksichtigt.
|
||||
|
||||
### Welche QRG wird aktualisiert?
|
||||
|
||||
Beide automatischen Quellen aktualisieren ausschließlich `MYQRG`. Das ist die eigene QRG der ersten beziehungsweise primären Chat-Kategorie.
|
||||
|
||||
Bei aktiviertem zweiten Chat bleibt dessen QRG davon unabhängig. Sie wird nicht aus den empfangenen TRX-Paketen abgeleitet und steht als `SECONDQRG` zur Verfügung. Dadurch kann beispielsweise die erste Kategorie automatisch der Frequenz des Logprogramms folgen, während für die zweite Kategorie eine eigene QRG von Hand eingetragen wird.
|
||||
|
||||
Sobald mindestens eine automatische QRG-Quelle aktiviert ist, wird das QRG-Feld der ersten Kategorie im Hauptfenster an den empfangenen Wert gebunden. Eine manuelle Eingabe in dieses Feld ist wieder möglich, wenn sowohl der allgemeine RadioInfo-Listener als auch die Win-Test-STATUS-Synchronisation deaktiviert sind.
|
||||
|
||||
### Haupt- oder Pass-Frequenz aus Win-Test
|
||||
|
||||
Standardmäßig verwendet KST4Contest die Hauptfrequenz des empfangenen Win-Test-`STATUS`-Pakets.
|
||||
|
||||
Die Option `Use pass frequency from Win-Test STATUS` verwendet stattdessen die im Paket enthaltene Pass-Frequenz. Das ist beispielsweise sinnvoll, wenn Win-Test im Split-Betrieb eine abweichende Frequenz führt und genau diese im Chat als Arbeitsfrequenz veröffentlicht werden soll.
|
||||
|
||||
Enthält das Paket keine gültige Pass-Frequenz, fällt KST4Contest automatisch auf die Hauptfrequenz zurück. Eine fehlende Pass-Frequenz löscht daher weder `MYQRG` noch ersetzt sie den Wert durch eine offensichtlich falsche Zahl.
|
||||
|
||||
Die Frequenzen werden einheitlich im KST4Contest-Format dargestellt, beispielsweise:
|
||||
|
||||
```text
|
||||
50.300.00
|
||||
144.300.00
|
||||
1296.100.00
|
||||
10368.100.00
|
||||
```
|
||||
|
||||
Die Werte werden erst beim Auflösen des Nachrichtentextes eingesetzt. Ändert das Logprogramm zwischen zwei Beacon-Läufen die Frequenz, verwendet die nächste Nachricht bereits den aktualisierten Wert.
|
||||
|
||||
Die eigene QRG kann außerdem als Fallback für die Übergabe eines Skeds an Win-Test verwendet werden. Das geschieht nur, wenn die QRG auswertbar ist und zum ausdrücklich ausgewählten Sked-Band gehört. Einzelheiten stehen unter [Log-Synchronisation](de-Log-Synchronisation#skeds-an-win-test-übergeben).
|
||||
|
||||
Weitere Informationen zu den Textvariablen: [Makros und Variablen](de-Makros-und-Variablen#variablen).
|
||||
|
||||
### Mehrere Logger oder Funkgeräte
|
||||
|
||||
Alle aktivierten QRG-Quellen schreiben in denselben Wert `MYQRG`. KST4Contest ordnet eingehende `RadioInfo`- oder `STATUS`-Pakete derzeit weder einem bestimmten Funkgerät noch einer Chat-Kategorie zu.
|
||||
|
||||
Sind der allgemeine RadioInfo-Listener und die Win-Test-Synchronisation gleichzeitig aktiviert, bestimmt deshalb das zuletzt verarbeitete Paket die angezeigte QRG. Dasselbe gilt, wenn mehrere Logger ihre Frequenzpakete an dieselbe KST4Contest-Instanz senden.
|
||||
|
||||
Für ein Setup mit mehreren Funkgeräten gilt daher:
|
||||
|
||||
- QSO-Pakete dürfen von mehreren Loggern empfangen werden.
|
||||
- Frequenzpakete sollten nur von der Quelle gesendet werden, die `MYQRG` tatsächlich steuern soll.
|
||||
- In einem Win-Test-Netzwerk sollte zusätzlich der Stationsfilter verwendet werden.
|
||||
- Werden zwei vollständig unabhängige QRG-Synchronisationen benötigt, sind zwei getrennte KST4Contest-Instanzen die eindeutigere Lösung.
|
||||
|
||||
Anders ausgedrückt: Mehrere Worked-Quellen lassen sich sinnvoll zusammenführen. Mehrere gleichzeitig sendende Frequenzquellen erzeugen dagegen keine zusätzliche Information, sondern lediglich einen Wettbewerb darum, welches Paket zuletzt angekommen ist.
|
||||
|
||||
Nach Abschluss der Konfiguration **Save Settings** verwenden.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## AirScout-Einstellungen
|
||||
|
||||
Konfiguration der Schnittstelle zu AirScout für die Flugzeug-Scatter-Erkennung. Details: [AirScout-Integration](de-AirScout-Integration).
|
||||
Im Reiter **AirScout** wird die UDP-Verbindung zwischen KST4Contest und AirScout eingerichtet. KST4Contest fordert dort keine allgemeinen Flugzeugdaten an, sondern übermittelt die aktuell relevanten Stationspfade. AirScout berechnet die dazu passenden Flugzeuge und sendet das Ergebnis an die anfragende KST4Contest-Instanz zurück.
|
||||
|
||||
Vorausgesetzt wird AirScout `0.9.9.5` oder neuer.
|
||||
|
||||
{ width=85% }
|
||||
|
||||
### Einstellungen der UDP-Verbindung
|
||||
|
||||
| Einstellung | Standardwert | Verwendung |
|
||||
|---|---:|---|
|
||||
| **Enable AirScout UDP integration** | deaktiviert | Aktiviert das Senden von AirScout-Anfragen und die Verarbeitung der Antworten |
|
||||
| **AirScout server identifier** | `AS` | Logischer Name der angesprochenen AirScout-Instanz |
|
||||
| **KST4Contest client identifier** | `KST` | Logischer Name dieser KST4Contest-Instanz |
|
||||
| **AirScout UDP port** | `9872` | Gemeinsamer UDP-Port für Anfragen und Antworten |
|
||||
| **Select AirScout frequency automatically per station** | aktiviert | Ermittelt Band und Frequenz für jede Gegenstation aus dem aktuellen Stationskontext |
|
||||
| **Forced AirScout band value** | `1440000` | Verwendet bei deaktivierter Automatik einen festen AirScout-Bandwert für alle Stationen |
|
||||
|
||||
Ist **Enable AirScout UDP integration** deaktiviert, sendet KST4Contest keine AirScout-Anfragen und verwirft eingehende AirScout-Antworten. Der UDP-Empfänger kann trotzdem gebunden bleiben, damit sich die Funktion während einer laufenden Verbindung wieder einschalten lässt.
|
||||
|
||||
KST4Contest verwendet für ausgehende AirScout-Pakete die Broadcast-Adresse `255.255.255.255`. Eine Ziel-IP wird deshalb nicht separat konfiguriert. AirScout und KST4Contest müssen den verwendeten UDP-Broadcast empfangen können; Router leiten einen solchen Broadcast normalerweise nicht in ein anderes Netz weiter. Bei Problemen sollten daher zuerst der UDP-Port, die lokale Firewall und die Netzzuordnung geprüft werden.
|
||||
|
||||
### Automatische Bandauswahl pro Station
|
||||
|
||||
**Auto per station** ist die empfohlene Einstellung. KST4Contest verwendet dann nicht einen festen Bandwert für alle Gegenstationen, sondern leitet eine geeignete Frequenz aus den verfügbaren Informationen ab.
|
||||
|
||||
Die Quellen werden in folgender Reihenfolge ausgewertet:
|
||||
|
||||
1. die zuletzt erkannte, höchstens 30 Minuten alte QRG der Gegenstation,
|
||||
2. eine eindeutige vollständige QRG im Namensfeld eines aktiven Chat-Eintrags,
|
||||
3. eindeutige Bandangaben im Namensfeld,
|
||||
4. 432 MHz, wenn dieselbe Station gleichzeitig in der VHF/UHF- und Microwave-Kategorie aktiv ist und 432 MHz für die eigene Station aktiviert wurde,
|
||||
5. das niedrigste für die eigene Station aktivierte Band, das zur unterstützten Chat-Kategorie passt.
|
||||
|
||||
Aktive Chat-Varianten desselben Basisrufzeichens werden gemeinsam ausgewertet. Die Einträge `CALLSIGN`, `CALLSIGN-2` und `CALLSIGN-432` können dadurch gemeinsam zur Bandherleitung beitragen, bleiben für die Nachrichtenverarbeitung aber getrennte Chat-Teilnehmer.
|
||||
|
||||
Nur Bänder, die unter **My station uses …** aktiviert wurden, kommen für die automatische Auswahl infrage. Ein manuell gesetztes NOT-QRV-Kennzeichen schließt das betreffende Band aus und hat Vorrang vor automatisch erkannten QRG- oder Namensinformationen.
|
||||
|
||||
Unterstützt werden die Chat-Kategorien für 50/70 MHz, VHF/UHF, Microwave und EME/JT65. Andere ON4KST-Kategorien werden für die AirScout-Bandherleitung ignoriert. Kann keine ausreichend belastbare Frequenz bestimmt werden, sendet KST4Contest für diese Station keine Anfrage. Ein beliebiger Rückfall auf 144 MHz würde zwar ein syntaktisch vollständiges Paket erzeugen, aber nicht zwangsläufig eine sinnvolle Berechnung.
|
||||
|
||||
Die automatische AirScout-Auswahl verwendet dieselbe Herleitung wie die interne Streckenanalyse. Damit bewerten beide Funktionen den Stationspfad auf derselben fachlichen Grundlage.
|
||||
|
||||
### Festes AirScout-Band
|
||||
|
||||
Wird **Auto per station** deaktiviert, verwendet KST4Contest den unter **Forced AirScout band value** eingetragenen Wert für alle Stationen.
|
||||
|
||||
Der Wert wird in der von der AirScout-UDP-Schnittstelle verwendeten Einheit eingetragen:
|
||||
|
||||
| Band | AirScout-Wert |
|
||||
|---|---:|
|
||||
| 50 MHz | `500000` |
|
||||
| 70 MHz | `700000` |
|
||||
| 144 MHz | `1440000` |
|
||||
| 432 MHz | `4320000` |
|
||||
| 1296 MHz | `12960000` |
|
||||
| 2320 MHz | `23200000` |
|
||||
| 3400 MHz | `34000000` |
|
||||
| 5760 MHz | `57600000` |
|
||||
| 10368 MHz | `103680000` |
|
||||
| 24048 MHz | `240480000` |
|
||||
|
||||
Im festen Modus wird weder die zuletzt erkannte QRG noch das im Namen genannte Band der Gegenstation berücksichtigt. Diese Einstellung ist deshalb hauptsächlich für einen eindeutig auf ein Band begrenzten Stationsbetrieb oder zur Fehlersuche sinnvoll.
|
||||
|
||||
### Server- und Client-Identifier
|
||||
|
||||
Die Identifier gehören zum AirScout-Protokoll und sind keine DNS-Namen oder IP-Adressen.
|
||||
|
||||
Ausgehende Anfragen enthalten zunächst den Client- und anschließend den Server-Identifier:
|
||||
|
||||
```text
|
||||
"KST" "AS"
|
||||
```
|
||||
|
||||
AirScout antwortet in umgekehrter Reihenfolge:
|
||||
|
||||
```text
|
||||
"AS" "KST"
|
||||
```
|
||||
|
||||
KST4Contest verarbeitet eine Antwort nur, wenn beide Identifier exakt mit der aktuellen Konfiguration übereinstimmen. Der Vergleich unterscheidet zwischen Groß- und Kleinschreibung.
|
||||
|
||||
Die Identifier dürfen nicht leer sein und keine Anführungszeichen oder Zeilenumbrüche enthalten.
|
||||
|
||||
Werden mehrere KST4Contest-Instanzen im selben Netz betrieben, sollte jede einen eigenen Client-Identifier erhalten, beispielsweise:
|
||||
|
||||
```text
|
||||
KST-144
|
||||
KST-432
|
||||
```
|
||||
|
||||
Bei mehreren AirScout-Instanzen müssen zusätzlich unterschiedliche Server-Identifier verwendet werden. Dadurch wird verhindert, dass die Antwort für einen Arbeitsplatz von einer anderen KST4Contest-Instanz verarbeitet wird.
|
||||
|
||||
### Welche Stationen werden angefragt?
|
||||
|
||||
KST4Contest startet die erste periodische AirScout-Abfrage ungefähr zehn Sekunden nach dem Aufbau der Chat-Verbindung. Weitere Abfragen folgen im Abstand von 60 Sekunden.
|
||||
|
||||
Eine aktive Station wird nur berücksichtigt, wenn:
|
||||
|
||||
- ein verwendbares Rufzeichen vorhanden ist,
|
||||
- ein Locator vorhanden ist,
|
||||
- die Entfernung berechnet werden konnte,
|
||||
- die Entfernung kleiner als das konfigurierte **Maximum-QRB** ist und
|
||||
- ein verwendbares Band bestimmt werden konnte.
|
||||
|
||||
Mehrere aktive Chat-Einträge desselben Basisrufzeichens erzeugen nicht für jeden Suffix eine eigene identische Pfadberechnung. Die zurückgegebenen AirScout-Informationen werden anschließend wieder den passenden aktiven Chat-Varianten zugeordnet.
|
||||
|
||||
Die Auswahl begrenzt nicht nur den Netzwerkverkehr. Sie verhindert außerdem, dass AirScout dauerhaft Pfade berechnet, die außerhalb des für die eigene Station vorgesehenen Arbeitsbereichs liegen.
|
||||
|
||||
### Übernahme geänderter Einstellungen
|
||||
|
||||
Folgende Änderungen werden nach Verlassen des Eingabefeldes beziehungsweise Betätigen der Checkbox sofort für neue Pakete verwendet:
|
||||
|
||||
- Aktivierung oder Deaktivierung der AirScout-Integration,
|
||||
- Server-Identifier,
|
||||
- Client-Identifier,
|
||||
- automatische oder feste Bandauswahl und
|
||||
- fester Bandwert.
|
||||
|
||||
Nach einer Änderung des UDP-Ports muss die Chat-Verbindung getrennt und neu aufgebaut oder KST4Contest neu gestartet werden. Der bereits laufende UDP-Empfänger bleibt sonst weiterhin an den vorherigen Port gebunden.
|
||||
|
||||
Zum dauerhaften Speichern anschließend **Save Settings** verwenden.
|
||||
|
||||
Die Einrichtung der AirScout-Seite, die Anzeige der Flugzeuge und die Bedeutung der AP-Daten sind unter [AirScout-Integration](de-AirScout-Integration) beschrieben.
|
||||
|
||||
---
|
||||
|
||||
@@ -162,11 +370,28 @@ Folgende Einstellungen und Schaltflächen gehören zur lokalen DX-Cluster-Ausgab
|
||||
- **TCP port**: Port, auf dem KST4Contest Verbindungen von DX-Cluster-Clients annimmt. Der Standardwert ist `8000`. Wird der Port während einer laufenden Verbindung geändert, startet KST4Contest den Server auf dem neuen Port neu. Der Logger muss sich anschließend ebenfalls mit dem neuen Port verbinden.
|
||||
- **Fallback band for relative QRG detection**: Das oben beschriebene globale Fallback-Band. Der Testspot verwendet `.300` dieses Bandes. Reale Spots verwenden dagegen die für den jeweiligen Absender erkannte QRG.
|
||||
- **Spotter callsign**: Rufzeichen, das im erzeugten DX-Cluster-Spot als Spotter erscheint. Hier sollte ein anderes Rufzeichen als das im Contest verwendete Stationsrufzeichen eingetragen werden. Einige Logprogramme filtern Spots des eigenen Rufzeichens oder behandeln sie anders als fremde Spots.
|
||||
- **Send test spot**: Sendet einen Testspot für `DL0TEST` auf `.300` des ausgewählten Fallback-Bandes. Der Test funktioniert nur, wenn KST4Contest mit dem Chat verbunden, der lokale DX-Cluster-Server aktiviert und mindestens ein DX-Cluster-Client verbunden ist.
|
||||
- **Send test spot**: Sendet den folgenden Testspot an alle aktuell verbundenen DX-Cluster-Clients:
|
||||
|
||||
KST4Contest erzeugt nicht bei jeder im Chat gefundenen Frequenz automatisch einen Spot. Ein Spot entsteht nur dann, wenn eine gerichtete Nachricht zwischen zwei Stationen auf eine für die eigene Station interessante Antennenrichtung schließen lässt und für den Absender eine nutzbare Frequenz bekannt ist.
|
||||
```text
|
||||
Spotted callsign: DO5AMF
|
||||
Comment: Testing DXC-Spot: Congrats, you donated $100!
|
||||
Frequency: .300 des ausgewählten Fallback-Bandes
|
||||
```
|
||||
|
||||
Bei einem Fallback-Band von `144 MHz` wird daraus beispielsweise eine Frequenz von ungefähr `144.300 MHz`.
|
||||
|
||||
Der Kommentar des Testspots ist ein bewusst beibehaltenes Easteregg. Er hat keine technische Bedeutung und löst – trotz seiner erfreulich konkreten Formulierung – keine Zahlung aus. Entscheidend ist, dass der Spot im verbundenen Logprogramm erscheint.
|
||||
|
||||
Der Test funktioniert nur, wenn
|
||||
|
||||
1. KST4Contest mit dem ON4KST-Chat verbunden ist,
|
||||
2. der lokale DX-Cluster-Server aktiviert ist und
|
||||
3. mindestens ein DX-Cluster-Client mit KST4Contest verbunden ist.
|
||||
|
||||
KST4Contest erzeugt nicht bei jeder im Chat gefundenen Frequenz automatisch einen Spot. Ein realer Spot entsteht nur dann, wenn eine gerichtete Nachricht zwischen zwei Stationen auf eine für die eigene Station interessante Antennenrichtung schließen lässt und für den Absender eine nutzbare Frequenz bekannt ist.
|
||||
|
||||
Die vollständige Herleitung und die Einrichtung des Logprogramms sind im Kapitel [Integrierter DX-Cluster-Server](de-DX-Cluster-Server) beschrieben.
|
||||
|
||||
### Band-Upgrade-Hinweis nach einem Logeintrag
|
||||
|
||||
Nach einem über UCXLog oder Win-Test empfangenen Logeintrag kann KST4Contest prüfen, ob die gerade gearbeitete Station noch ein weiteres gemeinsames, aber bisher nicht gearbeitetes Band anbietet.
|
||||
@@ -200,17 +425,42 @@ Weitere Hintergründe: [Band-Upgrade-Hinweis nach einem Logeintrag](de-Funktione
|
||||
|
||||
### Sniffer-Einstellungen (ab v1.31)
|
||||
|
||||
Das QSO-Monitoring ist für Stationen gedacht, deren Kommunikation man gezielt verfolgen möchte. Das kann beispielsweise eine seltene Station, eine DXpedition oder eine andere Station des eigenen Contest-Teams sein.
|
||||
Das QSO-Monitoring ist für Stationen gedacht, deren Kommunikation man gezielt verfolgen möchte. Das kann beispielsweise eine seltene Station, eine DXpedition oder eine andere Station des eigenen Contest-Teams sein, deren Sked-Absprachen im allgemeinen Chatverkehr nicht untergehen sollen.
|
||||
|
||||
Für jedes eingetragene Rufzeichen zeigt KST4Contest Nachrichten zusätzlich in der PM-Tabelle an, wenn das Rufzeichen entweder Absender oder Empfänger der Nachricht ist. Die ursprüngliche Nachricht wird dabei nicht aus ihrer normalen Tabelle entfernt.
|
||||
Die Rufzeichen werden im Reiter **Notification** unter **QSO monitoring** verwaltet.
|
||||
|
||||
Überwachte Nachrichten werden in der PM-Tabelle eindeutig gekennzeichnet:
|
||||
Für jedes eingetragene Basisrufzeichen zeigt KST4Contest Nachrichten zusätzlich in der PM-Tabelle an, wenn eine Variante dieses Rufzeichens entweder Absender oder Empfänger der Nachricht ist. Berücksichtigt werden beide verbundenen Chat-Kategorien.
|
||||
|
||||
Die Liste arbeitet bewusst mit dem normalisierten Basisrufzeichen. Folgende Eingaben erzeugen deshalb denselben Monitoring-Eintrag:
|
||||
|
||||
```text
|
||||
Sniffed: (SENDER > RECEIVER) Nachrichtentext
|
||||
DN9APW
|
||||
DN9APW-2
|
||||
DN9APW-70
|
||||
DN9APW-144
|
||||
```
|
||||
|
||||
So wird sichtbar, dass die Nachricht nicht an die eigene Station gerichtet war.
|
||||
In allen Fällen speichert und zeigt KST4Contest den Eintrag als:
|
||||
|
||||
```text
|
||||
DN9APW
|
||||
```
|
||||
|
||||
Damit müssen die verschiedenen KST-Suffixe einer Station nicht einzeln eingetragen werden. Wird später eine Nachricht von `DN9APW-2` gesendet oder an `DN9APW-70` adressiert, wird sie durch denselben Eintrag erfasst.
|
||||
|
||||
Diese Zusammenführung gilt ausschließlich für das QSO-Monitoring. Die aktiven ChatMember-Objekte, vollständigen Nachrichtenempfänger und Chat-Kategorien bleiben getrennt. Eine an `DN9APW-70` gerichtete Nachricht wird deshalb nicht an `DN9APW-2` umgeleitet.
|
||||
|
||||
Überwachte Nachrichten werden in der PM-Tabelle mit den vollständigen sichtbaren Rufzeichen von Absender und Empfänger gekennzeichnet:
|
||||
|
||||
```text
|
||||
Sniffed: (DN9APW-2 > DL0ABC-70) Nachrichtentext
|
||||
```
|
||||
|
||||
Die ursprüngliche Nachricht bleibt gleichzeitig in ihrer normalen Tabelle erhalten. Das Monitoring verändert weder den Nachrichteninhalt noch dessen Routing.
|
||||
|
||||
Erfasst werden Nachrichten, bei denen das überwachte Rufzeichen tatsächlich Absender oder Empfänger ist. Eine bloße Erwähnung des Rufzeichens im Nachrichtentext reicht nicht aus. Öffentliche Nachrichten einer überwachten Station werden ebenfalls angezeigt; als Empfänger erscheint dabei `ALL`.
|
||||
|
||||
Ist eine Nachricht bereits direkt an das eigene Rufzeichen gerichtet, erscheint sie als normale Privatnachricht und erhält keine zusätzliche `Sniffed:`-Kennzeichnung.
|
||||
|
||||
Rufzeichen werden folgendermaßen verwaltet:
|
||||
|
||||
@@ -218,27 +468,97 @@ Rufzeichen werden folgendermaßen verwaltet:
|
||||
2. Ein vorhandenes Rufzeichen per Doppelklick bearbeiten und die Änderung mit `Enter` übernehmen.
|
||||
3. Zum Entfernen den Inhalt einer Tabellenzelle löschen und mit `Enter` bestätigen.
|
||||
|
||||
Doppelte oder syntaktisch ungültige Rufzeichen werden nicht übernommen. Die Liste wird mit **Save Settings** in der `preferences.xml` gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||
Die Eingabe darf ein sichtbares KST-Suffix oder portable Bestandteile enthalten. KST4Contest normalisiert sie vor dem Speichern auf das Basisrufzeichen. Mehrere Varianten desselben Basisrufzeichens gelten deshalb als Duplikat.
|
||||
|
||||
Die Änderung der Liste wirkt sofort. Zum dauerhaften Speichern anschließend **Save Settings** verwenden. Die Basisrufzeichen werden in der `preferences.xml` gespeichert und beim nächsten Programmstart wiederhergestellt.
|
||||
|
||||
> Die Zusammenführung der KST-Suffixe über das Basisrufzeichen ist im Nightly beziehungsweise ab v1.42 enthalten.
|
||||
|
||||
Weitere Hintergründe und die Abgrenzung zum Nachrichtenrouting: [QSO-Monitoring](de-Funktionen#qso-monitoring-ab-v131).
|
||||
---
|
||||
|
||||
## Shortcut Settings (Schnellzugriff-Schaltflächen)
|
||||
|
||||
Konfiguration von Schnellzugriff-Schaltflächen, die direkt im Hauptfenster erscheinen. Ein Klick auf eine Schaltfläche fügt den konfigurierten Text in das Sendfeld ein. Alle [Variablen](Makros-und-Variablen#variablen) können verwendet werden.
|
||||

|
||||
|
||||
Jeder Eintrag im oberen Bereich des Reiters **Shortcuts** erzeugt eine Schaltfläche oberhalb des Nachrichteneingabefeldes im Hauptfenster. Ein Klick hängt den konfigurierten Text an den bereits vorhandenen Inhalt des Sendfeldes an.
|
||||
|
||||
Enthält der Shortcut eine [Variable](de-Makros-und-Variablen#variablen), wird sie beim Einfügen durch ihren aktuellen Wert ersetzt. Ein Shortcut wie
|
||||
|
||||
```text
|
||||
pse call me at MYQRGSHORT
|
||||
```
|
||||
|
||||
kann dadurch beispielsweise folgenden Text einfügen:
|
||||
|
||||
```text
|
||||
pse call me at 144.388
|
||||
```
|
||||
|
||||
Die exakten Einträge `MYQRG` und `SECONDQRG` werden zusätzlich als QRG-Schaltflächen hervorgehoben. Sie fügen die aktuelle QRG der ersten beziehungsweise zweiten Chat-Kategorie ein.
|
||||
|
||||
Auch der Shortcut `/SETNAME MYQRG` wird hervorgehoben. Beim Anklicken löst KST4Contest `MYQRG` auf und übernimmt den vollständigen Serverbefehl in das Sendfeld. Der Befehl wird nicht automatisch versendet und kann vor dem Senden mit `Enter` oder **TX** noch geprüft werden.
|
||||
|
||||
|
||||
Die Reihenfolge der Tabelle entspricht der Reihenfolge der Schaltflächen im Hauptfenster. Die Einträge werden folgendermaßen verwaltet:
|
||||
|
||||
1. Mit **Add shortcut** wird am Anfang der Liste ein neuer Eintrag angelegt und sofort zur Bearbeitung geöffnet.
|
||||
2. Ein vorhandener Eintrag kann per Doppelklick bearbeitet werden. `Enter` übernimmt die Änderung.
|
||||
3. Wird der Inhalt vollständig gelöscht und anschließend mit `Enter` bestätigt, entfernt KST4Contest den Eintrag.
|
||||
4. Mit **Move selected up** und **Move selected down** wird der markierte Eintrag innerhalb der Liste verschoben.
|
||||
|
||||
Änderungen werden sofort im Hauptfenster sichtbar. Damit sie auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
|
||||
|
||||
---
|
||||
|
||||
## Snippet Settings (Text-Snippets)
|
||||
|
||||
Text-Snippets sind über folgende Wege abrufbar:
|
||||
Snippets sind längere Textbausteine, die vor allem für Nachrichten an eine ausgewählte Station vorgesehen sind. Sie können über folgende Wege aufgerufen werden:
|
||||
|
||||
- **Rechtsklick** auf ein Rufzeichen in der Benutzerliste
|
||||
- **Rechtsklick** in der CQ-Nachrichtentabelle
|
||||
- **Rechtsklick** in der PM-Nachrichtentabelle
|
||||
- **Tastenkombinationen**: `Ctrl+1` bis `Ctrl+0` für die ersten 10 Snippets
|
||||
- per Rechtsklick auf eine Station in der Benutzerliste,
|
||||
- per Rechtsklick auf eine Nachricht in der öffentlichen Chat-Tabelle,
|
||||
- per Rechtsklick auf eine Nachricht in der PM-Tabelle oder
|
||||
- mit `Ctrl+1` bis `Ctrl+0` für die ersten zehn Einträge der Snippet-Liste.
|
||||
|
||||
Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als Direktnachricht adressiert:
|
||||
`/CQ RUFZEICHEN <Snippet-Text>`
|
||||
Bei den Tastenkombinationen entspricht die Zuordnung der Tabellenreihenfolge:
|
||||
|
||||
| Tastenkombination | Snippet |
|
||||
|---|---:|
|
||||
| `Ctrl+1` | erster Eintrag |
|
||||
| `Ctrl+2` | zweiter Eintrag |
|
||||
| … | … |
|
||||
| `Ctrl+9` | neunter Eintrag |
|
||||
| `Ctrl+0` | zehnter Eintrag |
|
||||
|
||||
Ein über das Kontextmenü ausgewähltes Snippet wird an den bereits vorbereiteten Nachrichtentext angehängt. Die Auswahl einer Station oder Nachricht hat das Sendfeld zuvor normalerweise bereits mit dem passenden `/cq`-Empfänger vorbereitet.
|
||||
|
||||
Eine Tastenkombination verhält sich etwas anders: Sie ersetzt den bisherigen Inhalt des Sendfeldes durch eine vollständig adressierte Privatnachricht:
|
||||
|
||||
```text
|
||||
/cq RUFZEICHEN Snippet-Text
|
||||
```
|
||||
|
||||
Dabei wird das vollständige sichtbare Rufzeichen einschließlich eines vorhandenen Suffixes verwendet. Für `9A0BB-70` entsteht daher beispielsweise:
|
||||
|
||||
```text
|
||||
/cq 9A0BB-70 pse ur qrg?
|
||||
```
|
||||
|
||||
Die Chat-Kategorie der ausgewählten Station bleibt für den späteren Versand erhalten. Ist keine Station ausgewählt oder ist für die gedrückte Tastenkombination kein Snippet vorhanden, wird nichts eingefügt.
|
||||
|
||||
Variablen werden beim Einfügen des Snippets aufgelöst. Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` verwenden die aktuell ausgewählte Station. Der vorbereitete Text wird nicht automatisch gesendet und kann deshalb noch geprüft oder geändert werden. `Enter` oder **TX** sendet die Nachricht; `Esc` leert das Sendfeld.
|
||||
|
||||
Die Snippet-Liste wird genauso bearbeitet wie die Shortcut-Liste:
|
||||
|
||||
1. **Add new snippet** legt am Anfang der Liste einen neuen Eintrag an.
|
||||
2. Ein Doppelklick öffnet einen vorhandenen Eintrag zur Bearbeitung.
|
||||
3. `Enter` übernimmt die Änderung.
|
||||
4. Ein leer bestätigter Eintrag wird entfernt.
|
||||
5. **Move selected up** und **Move selected down** ändern die Reihenfolge und damit auch die Zuordnung zu `Ctrl+1` bis `Ctrl+0`.
|
||||
|
||||
Die Kontextmenüs und Tastenkombinationen werden nach einer Änderung sofort aktualisiert. Für die dauerhafte Speicherung anschließend **Save Settings** verwenden.
|
||||
|
||||
Eine vollständige Übersicht der verfügbaren Platzhalter und ihrer Grenzen steht unter [Makros und Variablen](de-Makros-und-Variablen).
|
||||
|
||||
---
|
||||
|
||||
@@ -246,7 +566,7 @@ Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als D
|
||||
|
||||

|
||||
|
||||
Ein Beacon sendet in regelmäßigen Abständen eine öffentliche CQ-Nachricht. Er ist für Betriebssituationen gedacht, in denen die eigene Station über längere Zeit auf einer festen Frequenz ruft. Andere Stationen erhalten dadurch eine aktuelle QRG-Information, ohne dass der Operator denselben Text wiederholt von Hand in den Chat schreiben muss.
|
||||
Ein Beacon sendet in regelmäßigen Abständen eine öffentliche CQ-Nachricht. Er ist für Betriebssituationen gedacht, in denen die eigene Station über längere Zeit auf einer festen Frequenz ruft. Andere Stationen erhalten dadurch eine aktuelle QRG-Information, ohne dass der Operator denselben Text immer wieder von Hand in den Chat schreiben muss.
|
||||
|
||||
KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivierung und Nachrichtentext werden trotzdem getrennt konfiguriert:
|
||||
|
||||
@@ -260,21 +580,56 @@ Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in
|
||||
|
||||
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert ist eine Minute.
|
||||
|
||||
Nach dem Aufbau der Chat-Verbindung prüft KST4Contest die Beacons erstmals nach ungefähr zehn Sekunden. Anschließend gilt das eingestellte Intervall. Wird der Wert während einer laufenden Verbindung geändert, beginnt der Countdown mit dem neuen Intervall erneut. Die Änderung selbst löst keine sofortige Nachricht aus.
|
||||
Nach dem Aufbau der Chat-Verbindung prüft KST4Contest die Beacons erstmals nach ungefähr zehn Sekunden. Anschließend gilt das eingestellte Intervall.
|
||||
|
||||
Wird das Intervall während einer laufenden Verbindung geändert, beginnt der Countdown mit dem neuen Wert erneut. Die Änderung selbst löst keine sofortige Beacon-Nachricht aus.
|
||||
|
||||
Beide Kategorien verwenden denselben Timer. Unterschiedliche Intervalle für den ersten und zweiten Chat können deshalb nicht eingestellt werden.
|
||||
|
||||
### Nachrichtentext und Variablen
|
||||
|
||||
Ein Beacon darf nach der Variablenauflösung höchstens 120 Zeichen enthalten. KST4Contest prüft deshalb nicht nur das eingetragene Template, sondern den tatsächlich zu sendenden Text.
|
||||
Ein Beacon darf die [globalen Variablen](de-Makros-und-Variablen#variablen-im-beacon) verwenden, die sich ausschließlich auf die eigene Station beziehen:
|
||||
|
||||
Im Beacon können alle [globalen Variablen](de-Makros-und-Variablen#variablen-im-beacon) verwendet werden, beispielsweise:
|
||||
- `MYQRG`
|
||||
- `MYQRGSHORT`
|
||||
- `SECONDQRG`
|
||||
- `MYLOCATOR`
|
||||
- `MYLOCATORSHORT`
|
||||
- `MYCALL`
|
||||
- `MYQTF`
|
||||
|
||||
Eine mögliche Nachricht für die erste Chat-Kategorie ist:
|
||||
|
||||
```text
|
||||
calling cq at MYQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
calling cq at MYQRGSHORT, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Die Variablen werden bei jedem Timer-Lauf neu aufgelöst. Ändert die Logsoftware zwischenzeitlich die in `MYQRG` gespeicherte Frequenz, verwendet bereits der nächste Beacon den neuen Wert.
|
||||
Für den zweiten Chat muss `SECONDQRG` verwendet werden, wenn dessen Frequenz von der ersten Kategorie abweicht:
|
||||
|
||||
Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` benötigen dagegen eine ausgewählte Gegenstation. Da ein öffentlicher Beacon keine Gegenstation adressiert, werden diese Variablen im Beacon nicht aufgelöst.
|
||||
```text
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Die Variablen werden bei jedem Timer-Lauf neu aufgelöst. Ändert die Logsoftware zwischenzeitlich die in `MYQRG` gespeicherte Frequenz, verwendet bereits der nächste Beacon den aktualisierten Wert. Das Template selbst muss dafür nicht geändert werden.
|
||||
|
||||
`MYQRG` und `MYQRGSHORT` beziehen sich immer auf die erste Chat-Kategorie. Die Auswahl oder Aktivierung des zweiten Chats ändert diese Zuordnung nicht.
|
||||
|
||||
Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` benötigen eine ausgewählte Gegenstation. Da ein öffentlicher Beacon keine bestimmte Station adressiert, werden diese Variablen im Beacon nicht aufgelöst.
|
||||
|
||||
### Prüfung des Nachrichtentextes
|
||||
|
||||
KST4Contest prüft sowohl das eingetragene Template als auch den nach der Variablenauflösung tatsächlich zu sendenden Text.
|
||||
|
||||
Für Beacon-Nachrichten gelten folgende Bedingungen:
|
||||
|
||||
- Der endgültige Nachrichtentext darf nicht leer sein.
|
||||
- Er darf höchstens 120 Zeichen enthalten.
|
||||
- Das Protokoll-Trennzeichen `|` ist nicht zulässig.
|
||||
- Zeilenumbrüche sind nicht zulässig.
|
||||
|
||||
Eine ungültige Eingabe wird nicht als neue Beacon-Konfiguration übernommen. Wird ein Template erst durch eine spätere Variablenauflösung ungültig, beispielsweise weil der aufgelöste Text länger als 120 Zeichen ist, wird dieser Beacon-Lauf ausgelassen.
|
||||
|
||||
Ein Template, das ausschließlich aus einer momentan noch leeren globalen Variable besteht, kann gespeichert werden. Das ist beispielsweise beim Start möglich, bevor die erste QRG vom Logprogramm empfangen wurde. Solange die Variable keinen verwendbaren Inhalt liefert, sendet KST4Contest jedoch keine leere Nachricht.
|
||||
|
||||
### Wann sollte der Beacon ausgeschaltet werden?
|
||||
|
||||
@@ -283,6 +638,7 @@ Der Beacon ist nur dann hilfreich, wenn seine QRG-Angabe zum tatsächlichen Betr
|
||||
Im Klartext: Solange auf einer festen QRG CQ gerufen wird, spart der Beacon Arbeit. Beim „Schleichen“ über das Band sollte er ausgeschaltet werden.
|
||||
|
||||
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung, Texte und Intervall auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
|
||||
|
||||
---
|
||||
|
||||
## Messagehandling Settings (ab v1.25)
|
||||
@@ -303,9 +659,19 @@ Eine zweckmäßige Nachricht ist beispielsweise:
|
||||
Sri, I am not taking part in this contest. No skeds.
|
||||
```
|
||||
|
||||
Das Präfix `[KST4C Automsg]` muss nicht in das Eingabefeld geschrieben werden. KST4Contest ergänzt es automatisch.
|
||||
|
||||
Die beim Empfänger sichtbare Nachricht lautet daher beispielsweise:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] Sri, I am not taking part in this contest. No skeds.
|
||||
```
|
||||
|
||||
Die eingegangene Privatnachricht bleibt sichtbar. Die Funktion blockiert oder verwirft keine Anfrage, sondern erspart lediglich die wiederholte manuelle Antwort.
|
||||
|
||||
Die Antwort wird in derselben Chat-Kategorie gesendet, in der die Privatnachricht eingegangen ist. Das ist bei einem parallelen Login in zwei Kategorien entscheidend: Eine Nachricht aus dem Microwave-Chat darf nicht versehentlich im VHF/UHF-Chat beantwortet werden.
|
||||
Die Antwort wird an das vollständige Rufzeichen des Absenders einschließlich eines vorhandenen Suffixes und in derselben Chat-Kategorie gesendet, in der die Privatnachricht eingegangen ist. Das ist bei einem parallelen Login in zwei Kategorien entscheidend: Eine Nachricht aus dem Microwave-Chat darf nicht versehentlich im VHF/UHF-Chat beantwortet werden.
|
||||
|
||||
Ein leerer oder ausschließlich aus Leerzeichen bestehender Antworttext erzeugt keine automatische Nachricht. Enthält der Text ein Protokoll-Trennzeichen wie `|` oder einen Zeilenumbruch, wird die Antwort ebenfalls verworfen.
|
||||
|
||||
### Automatische QRG-Antwort
|
||||
|
||||
@@ -319,16 +685,24 @@ freq?
|
||||
pse qrg
|
||||
```
|
||||
|
||||
Die Antwort enthält nur die QRG der Kategorie, in der die Anfrage eingegangen ist:
|
||||
Die Antwort enthält ausschließlich die QRG der Kategorie, in der die Anfrage eingegangen ist:
|
||||
|
||||
| Eingegangene Privatnachricht | Verwendete QRG |
|
||||
|---|---|
|
||||
| Hauptkategorie | aktuelle QRG der Hauptkategorie |
|
||||
| zweite Chat-Kategorie | aktuelle QRG der zweiten Kategorie |
|
||||
|
||||
Eine mögliche Antwort lautet:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] QRG is: 144.300.00
|
||||
```
|
||||
|
||||
Die Werte stammen aus denselben QRG-Feldern, die auch von `MYQRG` und `SECONDQRG` verwendet werden. Die Haupt-QRG kann manuell eingetragen oder durch die [TRX-Synchronisation](#trx-sync-einstellungen) aktualisiert werden. Für die zweite Kategorie wird der dort konfigurierte beziehungsweise manuell eingetragene Wert verwendet.
|
||||
|
||||
Sind die allgemeine und die QRG-bezogene Antwort gleichzeitig aktiviert, hat die QRG-Antwort Vorrang. Eine erkannte QRG-Anfrage erzeugt daher nicht zusätzlich den allgemeinen Antworttext.
|
||||
Ist für die betreffende Kategorie keine QRG vorhanden, sendet KST4Contest keine unvollständige Antwort. Eine Nachricht wie `QRG is:` ohne Frequenz würde zwar auf die Frage reagieren, dem Anfragenden aber keine Information liefern. Sie wird deshalb bereits vor der Übergabe an die Sendequeue verworfen.
|
||||
|
||||
Sind die allgemeine und die QRG-bezogene Antwort gleichzeitig aktiviert, hat die QRG-Antwort Vorrang. Eine erkannte QRG-Anfrage erzeugt daher nicht zusätzlich den allgemeinen Antworttext. Fehlt die benötigte QRG, fällt KST4Contest auch nicht auf die allgemeine Antwort zurück.
|
||||
|
||||
### Schutz vor wiederholten Antworten
|
||||
|
||||
@@ -340,9 +714,16 @@ Jede automatisch erzeugte Nachricht trägt das feste Präfix:
|
||||
|
||||
Die allgemeine und die QRG-bezogene Antwort reagieren nicht auf Nachrichten, die dieses Präfix bereits enthalten. Dadurch beantworten sich zwei entsprechend arbeitende Clients nicht gegenseitig in einer Schleife.
|
||||
|
||||
Zusätzlich gilt eine gemeinsame Sperrzeit von zwei Minuten für beide Antwortarten. Die Sperre wird getrennt je Rufzeichen und Chat-Kategorie geführt. Hat eine Station gerade in der Hauptkategorie eine automatische Antwort erhalten, kann sie deshalb weiterhin eine Antwort in der zweiten Kategorie erhalten. Weitere Nachrichten derselben Station in derselben Kategorie lösen während der folgenden zwei Minuten dagegen keine neue automatische Antwort aus.
|
||||
Zusätzlich gilt eine gemeinsame Sperrzeit von zwei Minuten für beide Antwortarten. Die Sperre wird getrennt nach vollständigem Rufzeichen und Chat-Kategorie geführt.
|
||||
|
||||
Die Sperrzeit beginnt nur, wenn KST4Contest tatsächlich eine Antwort sendet.
|
||||
Daraus folgt:
|
||||
|
||||
- `CALLSIGN-2` und `CALLSIGN-70` besitzen getrennte Sperrzeiten.
|
||||
- Dasselbe vollständige Rufzeichen kann in einer anderen Chat-Kategorie unabhängig beantwortet werden.
|
||||
- Eine allgemeine Antwort sperrt für zwei Minuten auch eine QRG-Antwort an dasselbe Rufzeichen in derselben Kategorie.
|
||||
- Eine QRG-Antwort sperrt entsprechend auch die allgemeine Antwort.
|
||||
|
||||
Die Sperrzeit beginnt nur, wenn KST4Contest eine vollständige und lokal gültige Antwort in die Sendequeue übernimmt. Eine fehlende QRG, ein leerer allgemeiner Antworttext oder ein wegen ungültiger Zeichen verworfener Text startet keine Sperrzeit. Sobald die fehlende Information korrigiert wurde, kann daher unmittelbar eine gültige Antwort erzeugt werden.
|
||||
|
||||
> **Hinweis**: Der Antworttext sollte den tatsächlichen Status eindeutig benennen. Wer den Contest nur beobachtet und keine Skeds fahren möchte, sollte genau das mitteilen. Eine vage Nachricht erzeugt im Zweifel nur die nächste Rückfrage – und damit exakt die Arbeit, welche die Funktion vermeiden soll.
|
||||
|
||||
@@ -354,31 +735,103 @@ Weitere Hintergründe: [Automatische Antworten auf Privatnachrichten](de-Funktio
|
||||
|
||||
## Win-Test-Netzwerk-Listener (ab v1.31)
|
||||
|
||||
Dedizierter Empfänger für Win-Test-spezifische UDP-Pakete. Ermöglicht:
|
||||
Der Win-Test-Netzwerk-Listener verarbeitet das native Win-Test-UDP-Protokoll. Er ist vom allgemeinen QSO-UDP-Listener auf Port `12060` unabhängig und übernimmt drei Aufgaben:
|
||||
|
||||
- **Log-Synchronisation**: Gearbeitete Stationen werden aus Win-Test übernommen und in der Benutzerliste markiert.
|
||||
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz aus Win-Test befüllt die `MYQRG`-Variable.
|
||||
- **Sked-Übergabe (SKED Push)**: Skeds aus KST4Contest werden via UDP direkt an Win-Test übergeben. Der UDP-Broadcast-Standardport von Win-Test (9871) wird verwendet.
|
||||
- QSOs einschließlich Band- und Locatorinformation auswerten,
|
||||
- STATUS-Pakete für die eigene QRG verarbeiten und
|
||||
- Skeds an das Win-Test-Netzwerk übergeben.
|
||||
|
||||
Einstellungen:
|
||||
- **Aktivieren/Deaktivieren**: Checkbox in den Preferences (ab v1.40).
|
||||
- **Port**: Konfigurierbarer UDP-Port für den Win-Test-Listener.
|
||||
- **Sked-UDP-Adresse und Port**: Zieladresse und Port für die SKED-Übergabe an Win-Test.
|
||||
### Einstellungen unter Log sync
|
||||
|
||||
> **Hinweis**: Der Win-Test-Listener ist ein **zusätzlicher** Listener – der Standard-QSO-UDP-Broadcast-Listener auf Port 12060 bleibt davon unabhängig.
|
||||
| Einstellung | Funktion |
|
||||
|---|---|
|
||||
| **Receive Win-Test network based UDP log messages** | Aktiviert den Win-Test-Netzwerk-Listener. Bei aktiviertem Listener wird nach **Create sked** auch die Sked-Übergabe versucht. |
|
||||
| **UDP-Port for Win-Test listener** | Port des Win-Test-Netzwerks. Standard ist `9871`. Der Port wird auch für die Sked-Übergabe verwendet. |
|
||||
| **KST station name in Win-Test network (src of SKED packets)** | Stationsname, unter dem KST4Contest die Sked-Pakete sendet. In einem Netzwerk mit mehreren Clients sollte ein eindeutiger Name verwendet werden. |
|
||||
| **Win-Test network broadcast address** | Zieladresse für ausgehende Win-Test-Netzwerkpakete. Bei lokalem Netzwerkbetrieb muss hier eine vom Win-Test-Rechner erreichbare Broadcast-Adresse eingetragen sein. |
|
||||
|
||||
---
|
||||
Die Broadcast-Adresse ist konfigurierbar, weil `255.255.255.255` nicht in jedem Stationsnetz und nicht über jede Netzwerkschnittstelle zuverlässig weitergeleitet wird. Bei mehreren Rechnern kann stattdessen die zum Stationsnetz gehörende gerichtete Broadcast-Adresse erforderlich sein.
|
||||
|
||||
## PSTRotator-Einstellungen (ab v1.31)
|
||||
### Einstellungen unter TRX sync
|
||||
|
||||
KST4Contest kann die Antennenrichtung über PSTRotator steuern.
|
||||
| Einstellung | Funktion |
|
||||
|---|---|
|
||||
| **Win-Test STATUS QRG Sync** | Übernimmt die aktuelle Frequenz aus Win-Test-STATUS-Paketen als eigene QRG. |
|
||||
| **Use pass frequency from Win-Test STATUS** | Verwendet die übertragene Pass-Frequenz anstelle der normalen TRX-QRG. |
|
||||
| **Win-Test station name filter** | Verarbeitet nur STATUS-Pakete der angegebenen Win-Test-Station. Ein leeres Feld akzeptiert alle Stationsnamen. |
|
||||
|
||||
Einstellungen:
|
||||
- **Aktivieren/Deaktivieren**: Checkbox in den Preferences (ab v1.40).
|
||||
- **IP-Adresse**: IP-Adresse des PSTRotator-Rechners (Standard: `127.0.0.1` bei Betrieb auf demselben PC).
|
||||
- **Port**: Kommunikationsport von PSTRotator.
|
||||
Der Stationsfilter ist insbesondere bei mehreren Win-Test-Clients sinnvoll. Ohne Filter kann die zuletzt eingegangene STATUS-Meldung eines anderen Arbeitsplatzes die eigene QRG in KST4Contest überschreiben.
|
||||
|
||||
> **Hinweis**: Nach einem Klick auf den Richtungs-Button wartet KST4Contest kurz auf die Rotatorantwort. Bei langsamen Rotoren (z. B. SPID) kann es zu einer kleinen Verzögerung kommen.
|
||||
### Sked-Übergabe
|
||||
|
||||
Für die Sked-Übergabe gibt es keinen davon getrennten internen Sked-Modus. Ist der Listener aktiviert, versucht **Create sked** zusätzlich zur internen Anlage die Übertragung an Win-Test.
|
||||
|
||||
KST4Contest sendet nur dann ein `ADDSKED`-Paket, wenn eine QRG ermittelt wurde, die zum ausdrücklich ausgewählten Band gehört. Kann keine passende QRG gefunden werden, bleibt der interne Sked bestehen und die Win-Test-Übergabe wird ausgelassen.
|
||||
|
||||
Die Auswahl `SSB` oder `CW` erfolgt direkt im Further-Info-Bereich beim Anlegen des Skeds. Eine automatische Mode-Ableitung wird nicht verwendet.
|
||||
|
||||
Nach Änderungen **Save Settings** verwenden, damit Port, Stationsname, Broadcast-Adresse und TRX-Optionen beim nächsten Programmstart wiederhergestellt werden.
|
||||
|
||||
Datenbehandlung und QRG-Auswahl: [Log-Synchronisation – Win-Test](de-Log-Synchronisation#win-test)
|
||||
|
||||
|
||||
## PSTRotator-Einstellungen (ab v1.31, vollständig konfigurierbar ab v1.40)
|
||||
|
||||
KST4Contest kann eine ausgewählte Antennenrichtung über die UDP-Schnittstelle von [PSTRotator](https://www.pstrotator.com/) einstellen und die von PSTRotator gemeldete aktuelle Position als eigene QTF übernehmen.
|
||||
|
||||
Die Einstellungen befinden sich im Reiter **Station**:
|
||||
|
||||
| Einstellung | Standardwert | Verwendung |
|
||||
|---|---:|---|
|
||||
| **Enable PSTRotator** | deaktiviert | Startet die UDP-Kommunikation mit PSTRotator |
|
||||
| **PSTRotator host** | `127.0.0.1` | Hostname oder IP-Adresse des Rechners, auf dem PSTRotator läuft |
|
||||
| **PSTRotator UDP port** | `12000` | UDP-Port, auf dem PSTRotator die Steuerbefehle empfängt |
|
||||
|
||||
Bei Betrieb auf demselben Rechner ist `127.0.0.1` normalerweise die eindeutigste Einstellung. Läuft PSTRotator auf einem anderen Rechner im Stationsnetz, muss dessen erreichbare IP-Adresse oder DNS-Name eingetragen werden.
|
||||
|
||||
Der Port darf zwischen `1` und `65534` liegen. Port `65535` ist nicht möglich, weil PSTRotator seine Positionsmeldungen auf dem jeweils folgenden Port sendet.
|
||||
|
||||
### PSTRotator vorbereiten
|
||||
|
||||
In PSTRotator muss unter **Communication → UDP Control Port** derselbe UDP-Port eingetragen werden wie in KST4Contest. Anschließend muss **UDP Control** in PSTRotator aktiviert werden.
|
||||
|
||||
Bei der Standardeinstellung ergibt sich folgendes Portpaar:
|
||||
|
||||
| Richtung | UDP-Port |
|
||||
|---|---:|
|
||||
| KST4Contest → PSTRotator | `12000` |
|
||||
| PSTRotator → KST4Contest | `12001` |
|
||||
|
||||
KST4Contest bindet den Empfangsport automatisch. Er wird nicht separat konfiguriert.
|
||||
|
||||
Bei Betrieb auf zwei Rechnern müssen die lokale Firewall und das Stationsnetz UDP-Pakete in beiden Richtungen zulassen. Ist der Empfangsport bereits durch ein anderes Programm belegt, kann KST4Contest die Positionsmeldungen nicht empfangen.
|
||||
|
||||
Das vollständige UDP-Protokoll ist im [PSTRotatorAz User Manual](https://www.qsl.net/yo3dmu/ANT/PstRotatorAz%20User%20Manual.pdf) beschrieben.
|
||||
|
||||
### Übernahme der aktuellen QTF
|
||||
|
||||
KST4Contest fragt PSTRotator alle zwei Sekunden nach der aktuellen Azimutposition und dem Betriebsmodus. Die zurückgemeldete Azimutposition wird als `actualQTF` übernommen.
|
||||
|
||||
Bei aktivierter PSTRotator-Integration ist das QTF-Feld im Hauptfenster deshalb nicht manuell editierbar. Es zeigt die zuletzt von PSTRotator gemeldete Position.
|
||||
|
||||
Diese QTF wird unter anderem verwendet für:
|
||||
|
||||
- den Richtungsfilter,
|
||||
- die Bewertung von Richtungsgelegenheiten,
|
||||
- den Priority Score,
|
||||
- die Darstellung des Antennensektors auf der Stationskarte,
|
||||
- die AP- und Sked-Timeline und
|
||||
- die Variable `MYQTF`.
|
||||
|
||||
Eine empfangene Rotatorposition ist damit nicht nur eine Anzeige. Sie verändert mehrere Funktionen, die auf der aktuellen Antennenrichtung beruhen.
|
||||
|
||||
KST4Contest verwendet derzeit nur den Azimut. Eine Elevationssteuerung oder eine vollständige Azimut-/Elevationsnachführung ist nicht Bestandteil dieser Integration.
|
||||
|
||||
### Änderungen übernehmen
|
||||
|
||||
Aktivierung, Host und Port werden beim Start der Rotatorverbindung ausgewertet. Nach einer Änderung sollte die ON4KST-Verbindung getrennt und erneut aufgebaut oder KST4Contest neu gestartet werden.
|
||||
|
||||
Anschließend **Save Settings** verwenden, damit die Werte auch beim nächsten Programmstart wiederhergestellt werden.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -86,35 +86,76 @@ Beim Broadcast des vollständigen Logbuchs verwendet DXLog.net `contactreplace`
|
||||
|
||||
### Win-Test
|
||||
|
||||
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
|
||||
Win-Test wird über einen eigenen UDP-Listener für das native Win-Test-Netzwerkprotokoll angebunden. Dieser Listener ist vom allgemeinen QSO-UDP-Listener auf Port `12060` unabhängig.
|
||||
|
||||
Bei einem neuen QSO übernimmt KST4Contest das Rufzeichen und löst die native Win-Test-Band-ID auf. Dabei werden auch 50 und 70 MHz verarbeitet. Ist im Paket ein gültiger Locator enthalten, wird zusätzlich das gearbeitete Großfeld für das erkannte Band gespeichert.
|
||||
#### QSO- und Worked-Synchronisation
|
||||
|
||||
**Vorteile der Win-Test Integration:**
|
||||
- **Bandbezogene Worked-Daten:** Neue QSOs setzen die Worked-Markierung des von Win-Test gemeldeten Bandes und aktualisieren – sofern vorhanden – den Großfeldstatus.
|
||||
- Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen.
|
||||
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieser auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet* – automatisch, sobald der Listener aktiv ist.
|
||||
- Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden.
|
||||
- **Automatische QRG-Auflösung für SKEDs:** KST4Contest wählt die Sked-Frequenz intelligent:
|
||||
1. Hat die Gegenstation in einer Chat-Nachricht ihre QRG genannt, wird diese verwendet.
|
||||
2. Sonst wird die eigene aktuelle QRG verwendet (aus Win-Test STATUS oder manueller Eingabe).
|
||||
Bei einem neuen QSO übernimmt KST4Contest:
|
||||
|
||||
**Einstellungen im Reiter „Log-Synchronisation":**
|
||||
- `Receive Win-Test network based UDP log messages` aktivieren.
|
||||
- `UDP-Port for Win-Test listener` (Standard: 9871).
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST").
|
||||
- `Win-Test network broadcast address`: Wird i.d.R. automatisch erkannt; erforderlich für das Senden von Sked-Paketen.
|
||||
- das geloggte Rufzeichen,
|
||||
- die native Win-Test-Band-ID und
|
||||
- einen gültigen Locator, sofern er im Paket enthalten ist.
|
||||
|
||||
**Einstellungen im Reiter „TRX-Synchronisation":**
|
||||
- `Win-Test STATUS QRG Sync`: Wenn aktiviert, übernimmt KST4Contest die aktuelle Transceiverfrequenz aus dem Win-Test STATUS-Paket als eigene QRG (MYQRG).
|
||||
- `Use pass frequency from Win-Test STATUS`: Statt der eigenen TRX-QRG wird die im STATUS-Paket enthaltene Pass-Frequenz als MYQRG verwendet (für Multi-Op-Setups, bei denen mit einer Pass-QRG gearbeitet wird).
|
||||
- `Win-Test station name filter`: Wird hier ein Name eingetragen (z.B. "STN1"), verarbeitet KST4Contest nur Pakete dieser Win-Test-Instanz. Leer lassen, um alle zu akzeptieren.
|
||||
Die Band-IDs für 50 und 70 MHz werden ebenso verarbeitet wie die VHF-, UHF- und SHF-Bänder. Das Rufzeichen wird global und auf dem erkannten Band als gearbeitet markiert. Liegt zusätzlich ein Locator vor, wird dessen vierstelliges Großfeld für dieses Band gespeichert.
|
||||
|
||||
**Einstellungen in Win-Test:**
|
||||
- Das Netzwerk in Win-Test muss aktiv sein.
|
||||
- Win-Test muss so konfiguriert sein, dass es seine Broadcasts an den entsprechenden Port (Standard 9871) sendet bzw. empfängt.
|
||||
Die Daten werden in derselben internen Datenbank abgelegt wie Worked-Informationen aus den übrigen QSO-UDP-Schnittstellen und nach einem Neustart wiederhergestellt.
|
||||
|
||||
#### Skeds an Win-Test übergeben
|
||||
|
||||
Mit **Create sked** wird zunächst ein interner KST4Contest-Sked angelegt. Ist der Win-Test-Netzwerk-Listener aktiviert, versucht KST4Contest anschließend automatisch, den Sked als `ADDSKED` an das Win-Test-Netzwerk zu übertragen.
|
||||
|
||||
Die QRG wird in folgender Reihenfolge bestimmt:
|
||||
|
||||
1. KST4Contest sucht die neueste, höchstens 30 Minuten alte QRG der Gegenstation auf dem ausdrücklich ausgewählten Band. Dabei werden aktive Varianten desselben Basisrufzeichens gemeinsam ausgewertet.
|
||||
2. Fehlt eine solche QRG, wird die eigene QRG der Chat-Kategorie geprüft, in der der Sked angelegt wurde. Sie wird nur verwendet, wenn sie sich auswerten lässt und tatsächlich zum ausgewählten Band gehört.
|
||||
3. Kann auf keinem dieser Wege eine passende QRG ermittelt werden, wird kein `ADDSKED` gesendet.
|
||||
|
||||
Eine feste Ersatzfrequenz wie `144.300` wird bewusst nicht verwendet. Eine technisch erfolgreiche Übergabe mit falschem Band oder falscher QRG wäre im Contestbetrieb schlechter als eine sichtbar ausgelassene Übergabe.
|
||||
|
||||
Der interne Sked bleibt in jedem Fall erhalten. Das gilt auch bei einer ungültigen Broadcast-Adresse, einem Netzwerkfehler oder einem nicht erreichbaren Win-Test-Client.
|
||||
|
||||
#### Behandlung von KST-Rufzeichensuffixen
|
||||
|
||||
KST-Suffixe kennzeichnen häufig den verwendeten Chat-Login oder ein Band. Sie gehören nicht in jedem Fall zum Logrufzeichen. Für Win-Test entfernt KST4Contest deshalb einen mit `-` abgetrennten KST-Suffix, erhält aber portable und internationale Rufzeichenbestandteile:
|
||||
|
||||
| Rufzeichen im KST-Chat | Übergabe an Win-Test |
|
||||
|---|---|
|
||||
| `DN9APW-2` | `DN9APW` |
|
||||
| `9A0BB-70` | `9A0BB` |
|
||||
| `EA5/G8MBI/P-70` | `EA5/G8MBI/P` |
|
||||
| `DN9APW-2/P` | `DN9APW/P` |
|
||||
|
||||
Innerhalb von KST4Contest bleibt das vollständige Rufzeichen erhalten. Timeline, Reminder-PMs und Chat-Kategorie beziehen sich weiterhin auf den konkret ausgewählten Login.
|
||||
|
||||
#### Mode, Zeitpunkt und Notizen
|
||||
|
||||
Der Mode wird beim Anlegen des Skeds ausdrücklich als `SSB` oder `CW` gewählt. Eine automatische Ableitung aus der QRG findet nicht statt, weil eine begrenzte Liste angenommener Bandsegmente nicht alle unterstützten VHF-, UHF- und SHF-Bänder zuverlässig abbilden kann.
|
||||
|
||||
KST4Contest überträgt den tatsächlichen Sked-Zeitpunkt ohne einen zusätzlichen Minutenversatz. Die Notizen enthalten – soweit bekannt – Locator und QTF sowie den Hinweis, dass der Sked über KST4Contest angelegt wurde.
|
||||
|
||||
Für die Übergabe sendet KST4Contest die Win-Test-Pakete `LOCKSKED`, `ADDSKED` und `UNLOCKSKED`.
|
||||
|
||||

|
||||
|
||||
#### Einstellungen
|
||||
|
||||
Im Reiter **Log sync**:
|
||||
|
||||
- `Receive Win-Test network based UDP log messages`
|
||||
- `UDP-Port for Win-Test listener`, standardmäßig `9871`
|
||||
- `KST station name in Win-Test network (src of SKED packets)`
|
||||
- `Win-Test network broadcast address`
|
||||
|
||||
Im Reiter **TRX sync**:
|
||||
|
||||
- `Win-Test STATUS QRG Sync`
|
||||
- `Use pass frequency from Win-Test STATUS`
|
||||
- `Win-Test station name filter`
|
||||
|
||||
Das Win-Test-Netzwerk muss in Win-Test aktiviert sein. Bei mehreren Computern muss die Broadcast-Adresse das betreffende lokale Netzwerk erreichen. Der Stationsname sollte die sendende KST4Contest-Instanz innerhalb des Win-Test-Netzwerks eindeutig erkennen lassen.
|
||||
|
||||
Ausführliche Beschreibung der Einstellungen: [Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener-ab-v131)
|
||||
|
||||
---
|
||||
|
||||
## TRX-Frequenz-Synchronisation
|
||||
|
||||
|
||||
@@ -2,177 +2,363 @@
|
||||
|
||||
> 🇬🇧 [English version](en-Macros-and-Variables) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
KST4Contest bietet ein flexibles System aus Text-Snippets, Shortcuts und eingebauten Variablen, die den Chat-Workflow im Contest erheblich beschleunigen.
|
||||
KST4Contest unterscheidet zwischen Shortcut-Schaltflächen, Text-Snippets und Variablen. Shortcuts und Snippets enthalten vorbereitete Texte. Variablen ergänzen Informationen, die sich während des Betriebs ändern können.
|
||||
|
||||
Der eingefügte Text bleibt im Sendfeld sichtbar und kann vor dem Senden geprüft oder geändert werden.
|
||||
|
||||
---
|
||||
|
||||
## Überblick
|
||||
|
||||
| Typ | Aufruf | Zweck |
|
||||
| Mechanismus | Aufruf | Verwendung |
|
||||
|---|---|---|
|
||||
| **Shortcuts** | Button in der Toolbar | Schneller Text-Insert ins Sendfeld |
|
||||
| **Snippets** | Rechtsklick / Ctrl+1..0 | Text-Bausteine, optionaler PM-Versand |
|
||||
| **Variablen** | In allen Text-Feldern verwendbar | Dynamische Werte (QRG, Locator, AP-Daten) |
|
||||
| **Shortcut** | Schaltfläche oberhalb des Sendfeldes | Fügt einen konfigurierten Text in das Sendfeld ein |
|
||||
| **Snippet** | Kontextmenü oder `Ctrl+1` bis `Ctrl+0` | Bereitet einen Text für die ausgewählte Station vor |
|
||||
| **Variable** | Platzhalter innerhalb eines Nachrichtentextes | Fügt aktuelle QRG-, Locator-, Richtungs-, Stations- oder AirScout-Informationen ein |
|
||||
|
||||
Shortcuts und Snippets speichern Texte. Variablen liefern die dazugehörigen aktuellen Werte.
|
||||
|
||||
Ein Shortcut wie
|
||||
|
||||
```text
|
||||
pse sked?
|
||||
```
|
||||
|
||||
fügt immer denselben Text ein. Ein Shortcut mit
|
||||
|
||||
```text
|
||||
pse call me at MYQRGSHORT
|
||||
```
|
||||
|
||||
verwendet dagegen die QRG, die beim Anklicken des Shortcuts aktuell in KST4Contest hinterlegt ist.
|
||||
|
||||
---
|
||||
|
||||
## Shortcuts (Schnellzugriff-Schaltflächen)
|
||||
## Shortcut-Schaltflächen
|
||||
|
||||
Konfigurierbar in den Preferences → **Shortcut Settings**.
|
||||
Shortcuts werden unter **Preferences → Shortcut Settings** konfiguriert.
|
||||
|
||||
- Jeder konfigurierte Text erzeugt **einen Button** in der Benutzeroberfläche.
|
||||
- Ein Klick fügt den Text in das **Sendfeld** ein.
|
||||
- **Alle Variablen** können in Shortcuts verwendet werden und werden beim Einfügen sofort aufgelöst.
|
||||
- Auch längere Texte möglich.
|
||||

|
||||
|
||||
**Tipp**: Häufig verwendete Abkürzungen wie „pse", „rrr", „tnx", „73" als Shortcuts anlegen.
|
||||
Jeder Eintrag erzeugt eine Schaltfläche im Hauptfenster. Ein Klick hängt den konfigurierten Text an den vorhandenen Inhalt des Sendfeldes an. Ein bereits vorbereiteter Nachrichtentext wird dabei nicht gelöscht.
|
||||
|
||||
Enthält der Shortcut eine Variable, wird sie beim Einfügen aufgelöst. Aus
|
||||
|
||||
```text
|
||||
pse call me at MYQRGSHORT
|
||||
```
|
||||
|
||||
kann beispielsweise werden:
|
||||
|
||||
```text
|
||||
pse call me at 144.388
|
||||
```
|
||||
|
||||
Die exakten Einträge `MYQRG` und `SECONDQRG` werden als QRG-Schaltflächen hervorgehoben. Sie fügen die aktuelle QRG der ersten beziehungsweise zweiten Chat-Kategorie ein.
|
||||
|
||||
Auch der Shortcut
|
||||
|
||||
```text
|
||||
/SETNAME MYQRG
|
||||
```
|
||||
|
||||
wird hervorgehoben. Beim Anklicken wird `MYQRG` aufgelöst und der daraus entstehende Serverbefehl in das Sendfeld übernommen. Der Befehl wird nicht automatisch gesendet.
|
||||
|
||||
Die Reihenfolge der Einträge in den Einstellungen bestimmt die Reihenfolge der Schaltflächen im Hauptfenster. Bearbeitung, Sortierung und Speicherung sind unter [Konfiguration – Shortcut Settings](de-Konfiguration#shortcut-settings-schnellzugriff-schaltflächen) beschrieben.
|
||||
|
||||
---
|
||||
|
||||
## Snippets (Text-Bausteine)
|
||||
## Text-Snippets
|
||||
|
||||
Konfigurierbar in den Preferences → **Snippet Settings**.
|
||||
Snippets werden unter **Preferences → Snippet Settings** konfiguriert. Sie sind vor allem für wiederkehrende Nachrichten an eine bestimmte Station vorgesehen.
|
||||
|
||||
### Aufruf
|
||||
Snippets können aufgerufen werden:
|
||||
|
||||
- **Rechtsklick** auf ein Rufzeichen in der Benutzerliste
|
||||
- **Rechtsklick** in der CQ-Nachrichtentabelle
|
||||
- **Rechtsklick** in der PM-Nachrichtentabelle
|
||||
- **Tastaturkürzel**: `Ctrl+1` bis `Ctrl+0` für die ersten 10 Snippets
|
||||
- per Rechtsklick auf eine Station in der Benutzerliste,
|
||||
- per Rechtsklick auf eine öffentliche Nachricht,
|
||||
- per Rechtsklick auf eine Privatnachricht oder
|
||||
- mit `Ctrl+1` bis `Ctrl+0` für die ersten zehn Einträge der Snippet-Liste.
|
||||
|
||||
### Verhalten mit ausgewähltem Rufzeichen
|
||||
### Verwendung über das Kontextmenü
|
||||
|
||||
Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als **Privatnachricht** adressiert:
|
||||
Die Auswahl einer Station oder Nachricht bereitet normalerweise bereits den passenden `/cq`-Empfänger im Sendfeld vor. Das anschließend im Kontextmenü ausgewählte Snippet wird an diesen Text angehängt.
|
||||
|
||||
```
|
||||
/CQ RUFZEICHEN <Snippet-Text>
|
||||
Ein bereits vorhandener Nachrichtentext kann dadurch gezielt erweitert werden.
|
||||
|
||||
### Verwendung über die Tastatur
|
||||
|
||||
Ein mit `Ctrl+1` bis `Ctrl+0` aufgerufenes Snippet ersetzt den bisherigen Inhalt des Sendfeldes durch eine vollständig adressierte Nachricht:
|
||||
|
||||
```text
|
||||
/cq RUFZEICHEN Snippet-Text
|
||||
```
|
||||
|
||||
Anschließend kann mit **Enter** direkt gesendet werden – auch wenn das Sendfeld nicht den Fokus hat.
|
||||
Das vollständige sichtbare Rufzeichen einschließlich eines vorhandenen Suffixes bleibt erhalten. Für die ausgewählte Station `9A0BB-70` kann beispielsweise entstehen:
|
||||
|
||||
### Hardware-Makro-Tastatur
|
||||
```text
|
||||
/cq 9A0BB-70 pse ur qrg?
|
||||
```
|
||||
|
||||
*(Idee von IU3OAR, Gianluca Costantino)*
|
||||
KST4Contest behält intern auch die Chat-Kategorie der ausgewählten Station bei. Ein Snippet für `9A0BB-70` wird deshalb nicht versehentlich über die andere aktive Chat-Kategorie gesendet.
|
||||
|
||||
Die Tastenkombinationen `Ctrl+1` bis `Ctrl+0` können auf einer programmierbaren Makro-Tastatur belegt werden. Ein weiterer Tastendruck (auf eine „Enter"-Taste) sendet den Text sofort. Im Contest-Betrieb spart das erheblich Zeit.
|
||||
Ist keine Station ausgewählt oder existiert für die gedrückte Tastenkombination kein Snippet, wird nichts eingefügt.
|
||||
|
||||
### Vordefinierte Standard-Snippets
|
||||
Der vorbereitete Text wird nicht automatisch versendet:
|
||||
|
||||
Beim ersten Start werden einige Snippets vorbelegt, z. B.:
|
||||
- `Enter` oder **TX** sendet die Nachricht.
|
||||
- `Esc` leert das Sendfeld.
|
||||
|
||||
- `Hi OM, try sked?`
|
||||
- `I am calling cq ur dir, pse lsn to me at MYQRG`
|
||||
- `pse ur qrg?`
|
||||
- `rrr, I move to your qrg nw, pse ant dir me`
|
||||
### Zuordnung der Tastenkombinationen
|
||||
|
||||
Diese können in den Preferences angepasst oder gelöscht werden.
|
||||
Die Zuordnung folgt der Reihenfolge in der Snippet-Liste:
|
||||
|
||||
| Tastenkombination | Verwendeter Eintrag |
|
||||
|---|---:|
|
||||
| `Ctrl+1` | erster Eintrag |
|
||||
| `Ctrl+2` | zweiter Eintrag |
|
||||
| … | … |
|
||||
| `Ctrl+9` | neunter Eintrag |
|
||||
| `Ctrl+0` | zehnter Eintrag |
|
||||
|
||||
Die Tastenkombinationen können auch einer programmierbaren Makro-Tastatur zugewiesen werden. Die Idee zu dieser Bedienung stammt von IU3OAR, Gianluca Costantino.
|
||||
|
||||
KST4Contest legt keine verbindliche Liste von Standard-Snippets fest. Welche Texte sinnvoll sind, hängt vom eigenen Contestbetrieb und der verwendeten Betriebsart ab.
|
||||
|
||||
Bearbeitung, Sortierung und Speicherung sind unter [Konfiguration – Snippet Settings](de-Konfiguration#snippet-settings-text-snippets) beschrieben.
|
||||
|
||||
---
|
||||
|
||||
## Variablen
|
||||
|
||||
Variablen werden in geschriebenen Texten (Snippets, Shortcuts, Beacon, Sendfeld) durch ihre aktuellen Werte ersetzt. Einfach den Variablennamen **großgeschrieben** in den Text einfügen.
|
||||
Variablen sind reservierte Platzhalter innerhalb eines Nachrichtentextes. Sie müssen in Großbuchstaben geschrieben werden und unterscheiden zwischen Groß- und Kleinschreibung.
|
||||
|
||||
### MYQRG
|
||||
Variablen können verwendet werden in:
|
||||
|
||||
Wird durch die aktuelle Transceiverfrequenz ersetzt.
|
||||
- Shortcuts,
|
||||
- Snippets,
|
||||
- Beacon-Texten und
|
||||
- direkt eingegebenen oder eingefügten Nachrichtentexten.
|
||||
|
||||
- Quelle: TRX-Sync via UDP vom Logprogramm (wenn aktiviert)
|
||||
- Fallback: Manuell eingetragener Wert im MYQRG-Textfeld rechts neben dem Sendbutton
|
||||
- Format: `144.388.03`
|
||||
Bei einem Shortcut oder Snippet werden die Variablen bereits beim Einfügen in das Sendfeld aufgelöst. Direkt in das Sendfeld geschriebene oder eingefügte Variablen werden unmittelbar vor der Übernahme in die Sendewarteschlange aufgelöst.
|
||||
|
||||
**Beispiel**: `calling cq at MYQRG` → `calling cq at 144.388.03`
|
||||
Stationsbezogene Variablen verwenden immer die aktuell ausgewählte Station. KST4Contest leitet diese Station nicht aus einem von Hand in den Nachrichtentext geschriebenen `/cq`-Empfänger ab.
|
||||
|
||||
### MYQRGSHORT
|
||||
---
|
||||
|
||||
Wie MYQRG, aber nur die ersten 7 Zeichen.
|
||||
## Globale Variablen
|
||||
|
||||
- Format: `144.388`
|
||||
Globale Variablen benötigen keine ausgewählte Gegenstation.
|
||||
|
||||
**Beispiel**: `qrg: MYQRGSHORT` → `qrg: 144.388`
|
||||
| Variable | Ersetzter Wert |
|
||||
|---|---|
|
||||
| `MYQRG` | aktuelle QRG der ersten beziehungsweise primären Chat-Kategorie |
|
||||
| `MYQRGSHORT` | erste sieben Zeichen von `MYQRG` |
|
||||
| `SECONDQRG` | aktuelle QRG der zweiten Chat-Kategorie |
|
||||
| `MYLOCATOR` | vollständiger Locator der eigenen Station |
|
||||
| `MYLOCATORSHORT` | erste vier Zeichen des eigenen Locators |
|
||||
| `MYCALL` | konfiguriertes eigenes Rufzeichen |
|
||||
| `MYQTF` | aktuelle Antennenrichtung als numerischer Wert in Grad |
|
||||
|
||||
### MYLOCATOR
|
||||
Beispiel:
|
||||
|
||||
Wird durch den eigenen Maidenhead-Locator (6-stellig) ersetzt.
|
||||
```text
|
||||
cq at MYQRGSHORT, qtf MYQTF, loc MYLOCATOR
|
||||
```
|
||||
|
||||
- Format: `JO51IJ`
|
||||
kann aufgelöst werden zu:
|
||||
|
||||
**Beispiel**: `my loc: MYLOCATOR` → `my loc: JO51IJ`
|
||||
```text
|
||||
cq at 144.388, qtf 135, loc JO51IJ
|
||||
```
|
||||
|
||||
### MYLOCATORSHORT
|
||||
### QRG-Variablen
|
||||
|
||||
Wie MYLOCATOR, aber nur die ersten 4 Zeichen.
|
||||
`MYQRG` enthält die QRG der ersten Chat-Kategorie. Der Wert kann aus der TRX-Synchronisation des Logprogramms oder aus dem manuell bearbeiteten QRG-Feld stammen.
|
||||
|
||||
- Format: `JO51`
|
||||
`MYQRGSHORT` verwendet denselben Wert, beschränkt ihn aber auf die ersten sieben Zeichen:
|
||||
|
||||
**Beispiel**: `loc: MYLOCATORSHORT` → `loc: JO51`
|
||||
```text
|
||||
144.388.03 → 144.388
|
||||
```
|
||||
|
||||
`SECONDQRG` enthält die QRG der zweiten Chat-Kategorie. Die Auswahl einer Station aus dem zweiten Chat verändert die Bedeutung von `MYQRG` nicht. Soll ausdrücklich die QRG der zweiten Kategorie eingesetzt werden, muss `SECONDQRG` verwendet werden.
|
||||
|
||||
### Locator-Variablen
|
||||
|
||||
`MYLOCATOR` übernimmt den vollständigen konfigurierten Locator der eigenen Station:
|
||||
|
||||
```text
|
||||
JO51IJ
|
||||
```
|
||||
|
||||
`MYLOCATORSHORT` verwendet nur die ersten vier Zeichen:
|
||||
|
||||
```text
|
||||
JO51
|
||||
```
|
||||
|
||||
### MYQTF
|
||||
|
||||
`MYQTF` übernimmt die aktuelle, in KST4Contest hinterlegte Antennenrichtung als numerischen Winkel in Grad.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
ant MYQTF deg
|
||||
```
|
||||
|
||||
kann werden zu:
|
||||
|
||||
```text
|
||||
ant 135 deg
|
||||
```
|
||||
|
||||
Die Richtung wird nicht in Himmelsrichtungen wie `north`, `north-east` oder `south-west` umgewandelt.
|
||||
|
||||
---
|
||||
|
||||
## Variablen für die ausgewählte Station
|
||||
|
||||
Diese Variablen benötigen eine ausgewählte Gegenstation:
|
||||
|
||||
| Variable | Ersetzter Wert |
|
||||
|---|---|
|
||||
| `QRZNAME` | Name der ausgewählten Station oder deren vollständiges Rufzeichen, wenn kein Name verfügbar ist |
|
||||
| `FIRSTAP` | Beschreibung und Ankunftszeit des ersten von AirScout gemeldeten Flugzeugs |
|
||||
| `SECONDAP` | Beschreibung und Ankunftszeit des zweiten von AirScout gemeldeten Flugzeugs |
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Hi QRZNAME, FIRSTAP, pse lsn at MYQRGSHORT
|
||||
```
|
||||
|
||||
kann werden zu:
|
||||
|
||||
```text
|
||||
Hi David, a very big AP in 2 min, pse lsn at 144.388
|
||||
```
|
||||
|
||||
### QRZNAME
|
||||
|
||||
Wird durch den **Namen** der aktuell ausgewählten Station aus dem Chat-Namenfeld ersetzt.
|
||||
|
||||
**Beispiel**: `Hi QRZNAME, sked?` → `Hi Gianluca, sked?`
|
||||
KST4Contest verwendet den Namen aus dem Namensfeld der ausgewählten Station. Ist dort kein verwendbarer Name vorhanden, wird stattdessen das vollständige sichtbare Rufzeichen eingesetzt.
|
||||
|
||||
### FIRSTAP
|
||||
|
||||
Wird durch Daten des ersten reflektierbaren Flugzeugs zur ausgewählten Station ersetzt (sofern vorhanden).
|
||||
Ist ein AirScout-Kandidat verfügbar, enthält `FIRSTAP` dessen Beschreibung und die voraussichtliche Zeit bis zum Reflexionsfenster.
|
||||
|
||||
- Bedingung: AirScout ist aktiv und ein Flugzeug ist verfügbar.
|
||||
- Format-Beispiel: `a very big AP in 1 min`
|
||||
Beispiel:
|
||||
|
||||
**Beispiel**: `AP info: FIRSTAP` → `AP info: a very big AP in 1 min`
|
||||
```text
|
||||
a very big AP in 2 min
|
||||
```
|
||||
|
||||
Ist für die ausgewählte Station kein Flugzeug verfügbar, wird eingesetzt:
|
||||
|
||||
```text
|
||||
no ap available
|
||||
```
|
||||
|
||||
### SECONDAP
|
||||
|
||||
Wie FIRSTAP, aber für das zweite verfügbare Flugzeug.
|
||||
`SECONDAP` verwendet den zweiten verfügbaren AirScout-Kandidaten.
|
||||
|
||||
- Format-Beispiel: `Next big AP in 9 min`
|
||||
Beispiel:
|
||||
|
||||
**Beispiel**: `also: SECONDAP` → `also: Next big AP in 9 min`
|
||||
```text
|
||||
Next big AP in 9 min
|
||||
```
|
||||
|
||||
### MYQTF *(geplant für v1.3)*
|
||||
Ist kein zweiter Kandidat vorhanden, wird `SECONDAP` durch einen leeren Text ersetzt.
|
||||
|
||||
Wird durch die aktuelle Antennenrichtung in Worten ersetzt (z. B. `north`, `north east`, `east`, …).
|
||||
### Verhalten ohne ausgewählte Station
|
||||
|
||||
- Quelle: Winkelwert im MYQTF-Eingabefeld (rechts neben dem MYQRG-Feld)
|
||||
Ist keine Station ausgewählt, bleiben `QRZNAME`, `FIRSTAP` und `SECONDAP` im Text sichtbar. KST4Contest entfernt diese Platzhalter nicht automatisch.
|
||||
|
||||
Ein sichtbarer, nicht aufgelöster Platzhalter ist eindeutiger als eine formal vollständige Nachricht, in der unbemerkt eine wichtige Information fehlt. Vor dem Senden sollte deshalb geprüft werden, ob die richtige Station ausgewählt ist und alle benötigten Variablen aufgelöst wurden.
|
||||
|
||||
---
|
||||
|
||||
## Variablen im Beacon
|
||||
## Variablen im Beacon
|
||||
|
||||
Ein öffentlicher Beacon besitzt keine ausgewählte Gegenstation. Deshalb können hier ausschließlich Variablen verwendet werden, die nur von der eigenen Station und ihrer aktuellen Konfiguration abhängen:
|
||||
Ein öffentlicher Beacon besitzt keine ausgewählte Gegenstation. Deshalb können dort ausschließlich globale Variablen verwendet werden:
|
||||
|
||||
| Variable | Wert im Beacon |
|
||||
|---|---|
|
||||
| `MYQRG` | aktuelle QRG der ersten Chat-Kategorie |
|
||||
| `MYQRGSHORT` | auf sieben Zeichen gekürzte QRG der ersten Kategorie |
|
||||
| `SECONDQRG` | aktuelle QRG der zweiten Chat-Kategorie |
|
||||
| `MYLOCATOR` | eigener vollständiger Locator |
|
||||
| `MYLOCATORSHORT` | eigener vierstelliger Locator |
|
||||
| `MYCALL` | eigenes Rufzeichen |
|
||||
| `MYQTF` | aktuelle Antennenrichtung |
|
||||
- `MYQRG`
|
||||
- `MYQRGSHORT`
|
||||
- `SECONDQRG`
|
||||
- `MYLOCATOR`
|
||||
- `MYLOCATORSHORT`
|
||||
- `MYCALL`
|
||||
- `MYQTF`
|
||||
|
||||
`QRZNAME`, `FIRSTAP` und `SECONDAP` benötigen eine ausgewählte Station. In einem öffentlichen Beacon werden sie daher nicht aufgelöst.
|
||||
`QRZNAME`, `FIRSTAP` und `SECONDAP` dürfen in einem Beacon nicht verwendet werden.
|
||||
|
||||
Eine zweckmäßige Konfiguration ist beispielsweise:
|
||||
Eine mögliche Vorlage für die erste Chat-Kategorie ist:
|
||||
|
||||
```
|
||||
calling cq at MYQRG, loc MYLOCATOR, GL all!
|
||||
```text
|
||||
calling cq at MYQRGSHORT, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Die globalen Variablen werden bei jedem Timer-Lauf neu ausgewertet. Dadurch kann eine vom Logprogramm aktualisierte QRG bereits in der nächsten Beacon-Nachricht erscheinen.
|
||||
Verwendet die zweite Chat-Kategorie eine andere QRG, muss deren Vorlage `SECONDQRG` enthalten:
|
||||
|
||||
Der vollständig aufgelöste Nachrichtentext darf höchstens 120 Zeichen enthalten. Weitere Angaben zum gemeinsamen Intervall und zum Verhalten beider Chat-Kategorien stehen unter [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
|
||||
```text
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Die globalen Variablen werden bei jedem Timer-Lauf erneut ausgewertet. Eine inzwischen vom Logprogramm aktualisierte QRG kann dadurch bereits in der nächsten Beacon-Nachricht erscheinen.
|
||||
|
||||
Der vollständig aufgelöste Beacon-Text:
|
||||
|
||||
- muss mindestens ein gültiges Zeichen enthalten,
|
||||
- darf höchstens 120 Zeichen lang sein,
|
||||
- darf das Protokoll-Trennzeichen `|` nicht enthalten und
|
||||
- darf keine Zeilenumbrüche enthalten.
|
||||
|
||||
Ist der Text beim vorgesehenen Versand leer oder ungültig, wird der betreffende Beacon-Lauf ausgelassen.
|
||||
|
||||
Intervall, Aktivierung und Verhalten beider Kategorien sind unter [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon) beschrieben.
|
||||
|
||||
---
|
||||
## Beispiel-Workflow mit Makros im Contest
|
||||
|
||||
1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt.
|
||||
2. `Ctrl+1` drücken → Snippet „Hi OM, try sked?" wird als PM adressiert.
|
||||
3. Enter drücken → Nachricht wird gesendet.
|
||||
4. Station antwortet mit Frequenz → QRG-Spalte wird automatisch befüllt.
|
||||
5. `Ctrl+2` → Snippet „I am calling cq ur dir, pse lsn to me at 144.388" (MYQRG aufgelöst).
|
||||
6. Enter → Gesendet.
|
||||
## Beispiel für einen Snippet-Workflow
|
||||
|
||||
Ohne manuelle Tipparbeit, ohne Fehler, ohne Unterbrechung des CQ-Rufens.
|
||||
Als erstes Snippet ist beispielsweise konfiguriert:
|
||||
|
||||
```text
|
||||
Hi QRZNAME, pse sked? I call at MYQRGSHORT
|
||||
```
|
||||
|
||||
Der Ablauf kann dann folgendermaßen aussehen:
|
||||
|
||||
1. In der Benutzerliste wird `DL1ABC-432` ausgewählt.
|
||||
2. `Ctrl+1` wird gedrückt.
|
||||
3. KST4Contest bereitet die adressierte Nachricht vor und löst die Variablen auf.
|
||||
4. Der vollständige Text wird im Sendfeld geprüft.
|
||||
5. Falls die Gegenstation eine andere QRG vorgeschlagen hat, wird der Text entsprechend angepasst.
|
||||
6. `Enter` oder **TX** sendet die Nachricht.
|
||||
|
||||
Das Ergebnis kann beispielsweise lauten:
|
||||
|
||||
```text
|
||||
/cq DL1ABC-432 Hi Peter, pse sked? I call at 432.088
|
||||
```
|
||||
|
||||
Das vollständige Rufzeichen bestimmt den Empfänger. Die ausgewählte Chat-Kategorie bestimmt den Versandweg. Die Variablen verringern die wiederholte Texteingabe, entscheiden aber nicht, ob die eingesetzten Informationen noch zur aktuellen Betriebssituation passen.
|
||||
|
||||
---
|
||||
|
||||
## Grenzen der Variablenauflösung
|
||||
|
||||
Variablen geben den Informationsstand wieder, den KST4Contest im Moment der Auflösung besitzt.
|
||||
|
||||
Dabei ist insbesondere zu beachten:
|
||||
|
||||
- Eine vom Logprogramm gelieferte QRG kann sich inzwischen geändert haben.
|
||||
- Eine manuell eingetragene QRG bleibt aktiv, bis sie erneut geändert wird.
|
||||
- `MYQRG` bleibt die QRG der primären Kategorie, auch wenn eine Station aus der zweiten Kategorie ausgewählt wurde.
|
||||
- Die ausgewählte Station kann von einem manuell eingegebenen `/cq`-Empfänger abweichen.
|
||||
- AirScout kann für die betreffende Strecke keine aktuellen Flugzeugdaten liefern.
|
||||
- Stationsbezogene Variablen bleiben sichtbar, wenn keine Station ausgewählt ist.
|
||||
- Der eingefügte Text wird nicht automatisch auf seine betriebliche Richtigkeit geprüft.
|
||||
|
||||
Das Sendfeld bleibt deshalb nach dem Einfügen eines Shortcuts oder Snippets bearbeitbar. Die Variablen vermeiden wiederholte Eingaben; die abschließende Prüfung bleibt beim Operator.
|
||||
@@ -107,7 +107,7 @@ Aircraft data can be inserted directly into messages:
|
||||
- `FIRSTAP` → e.g. `a very big AP in 1 min`
|
||||
- `SECONDAP` → e.g. `Next big AP in 9 min`
|
||||
|
||||
Details: [Macros and Variables](Macros-and-Variables#variables)
|
||||
Details: [Macros and Variables](en-Macros-and-Variables#variables)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,22 +4,188 @@
|
||||
|
||||
Version history of KST4Contest / PraktiKST.
|
||||
|
||||
---
|
||||
|
||||
For the latest changelog, please refer to GitHub. The previous changelog is below.
|
||||
|
||||
## v1.41
|
||||
**Station Map, Performance, Responsive UI**
|
||||
|
||||
**New:**
|
||||
- **Station Map**: Interactive OpenStreetMap-based map showing the geographic position of all active chat members. Includes station markers, antenna beam cone, connection line to the selected station, Maidenhead grid overlay, and a path profile chart with terrain elevation analysis (Fresnel zones, horizon detection). Terrain data from Copernicus GLO-30, Open-Meteo API, or offline DEM import. Aircraft scatter path analysis integrated. Works in AppImage and Flatpak without external CDN access (local tile proxy, bundled Leaflet.js).
|
||||
|
||||
**Changed:**
|
||||
- **Message table limit raised to 30,000**: Chat and message tables are capped at 30,000 entries. Older messages are automatically discarded, keeping performance stable during multi-day contest operations.
|
||||
- **Screen-aware window sizing**: On startup, the main window is sized to fit the current screen. If KST4Contest was last used on a larger monitor, the window is automatically scaled down. The UI layout is more compact and responsive on smaller screens.
|
||||
Published Stable versions and their application packages are available under [GitHub Releases](https://github.com/praktimarc/kst4contest/releases). This page also lists changes from the current development version where they have already been implemented and tested.
|
||||
|
||||
---
|
||||
|
||||
## v1.42 – Nightly / in development
|
||||
|
||||
> Status of this section: 14 August 2026.
|
||||
> v1.42 is not a published Stable release yet. Further changes may be added before release.
|
||||
|
||||
v1.42 brings several previously separate calculations together. Band information, Worked status, NOT-QRV marks, callsign suffixes and frequencies are now used more consistently by the user list, station map, priority calculation and external interfaces.
|
||||
|
||||
### Added
|
||||
|
||||
- **Visible ON4KST connection state:** A compact `LINK` indicator in the main window displays the actual state of the ON4KST connection. Green means fully authenticated and synchronised, yellow indicates connection setup or synchronisation, and red indicates a lost connection, an error or the delay before the next connection attempt.
|
||||
|
||||
- **Shared band-opportunity calculation:** A central `BandOpportunityResolver` evaluates recent QRGs, band designators in station names, active callsign variants, Worked information and NOT-QRV marks. The user list, **New bands**, band-upgrade hint, Priority Score, station map and automatic band selection now use the same basis.
|
||||
|
||||
- **Extended band status display:** The band columns now distinguish:
|
||||
- `X` for worked on this band,
|
||||
- `a` for an offered and unworked band of a completely new callsign,
|
||||
- `B+` for an offered and unworked band of a callsign already worked elsewhere, and
|
||||
- `o` for a locator square already worked on this band.
|
||||
|
||||
The codes can be combined, for example as `ao` or `B+o`. The additional `a` and `o` indicators can be hidden separately in the GUI settings.
|
||||
|
||||
- **50 and 70 MHz support:** Both bands are available in station settings, Worked and NOT-QRV handling, the user list, filters, the internal database, UCXLog processing and the Win-Test listener. Existing databases are extended with the required columns.
|
||||
|
||||
- **Global message tabs:** Public messages, ON4KST DX cluster messages and directed messages between other stations can be displayed directly in the main window. The existing separate monitor window remains available and uses the same underlying message stores.
|
||||
|
||||
- **Manual QTF input:** The current antenna direction can be changed directly in KST4Contest when PSTRotator is not being used.
|
||||
|
||||
- **Filter reset:** A dedicated reset button reliably removes the active user-list filter predicates.
|
||||
|
||||
- **Map clustering:** Stations close to each other are grouped at lower zoom levels. The selected station and relevant direction opportunities remain individually visible.
|
||||
|
||||
- **Hideable path analysis:** The terrain profile and analysis section of the station map can be hidden completely. The selected state is stored and restored at the next application start.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Session-based ON4KST connection lifecycle:** Each socket, reader, writer, message bus and queue now belongs to an explicitly identified connection session. Delayed threads from an obsolete connection can therefore no longer process data or close its replacement. `ONLINE` is reported only after the login has been accepted and all requested user lists have been received. Connection setup, login and synchronisation use bounded timeouts, while heartbeats, missing inbound traffic, EOF and read or write failures trigger controlled reconnect attempts with backoff where appropriate.
|
||||
|
||||
- **Validated ON4KST protocol commands:** Outgoing frames are built centrally and checked for valid categories, locators and prohibited frame delimiters. Because ON4KST maintains one locator per TCP session, the main locator is used for both chat categories and a conflicting secondary configuration is logged instead of sending contradictory commands to the server.
|
||||
|
||||
- **More precise QRG recognition:** Complete and relative frequency references continue to be recognised. Bare three-digit numbers are treated as QRGs only when a frequency context is available. Signal reports, band designators and unrelated numbers therefore produce fewer false frequencies.
|
||||
|
||||
- **Station-specific frequency context:** For relative QRGs, KST4Contest first uses a band context for the same station which is no more than 30 minutes old. The globally configured fallback band is used only when this context is unavailable.
|
||||
|
||||
- **Fallback band dropdown:** The global fallback can only be selected from supported band values. It applies to the complete QRG parser, not merely to DX cluster spots.
|
||||
|
||||
- **Consistent QRG formatting:** Frequencies are displayed with at least three decimal places in the user list and message tables.
|
||||
|
||||
- **Band-aware AirScout and path calculations:** KST4Contest derives a realistic frequency for each station from its current QRG and known band information. AirScout receives canonical band values. The temporary 430 MHz fallback has been replaced with 432 MHz.
|
||||
|
||||
- **Shared propagation-frequency selection:** AirScout, **Calc selected** and the station-map path analysis use the same `PropagationFrequencyResolver`. A band selected explicitly in the Reachability dropdown is respected for manual calculations.
|
||||
|
||||
- **Separate callsign variants:** Active chat members are distinguished by their complete callsign, including suffix, and by chat category. `DN9APW`, `DN9APW-2` and similar logins therefore remain separate message targets.
|
||||
|
||||
- **Shared base-callsign information:** Worked flags, NOT-QRV information and Priority Scores continue to be evaluated across variants of the same base callsign. Separate message targets no longer result in contradictory Worked information.
|
||||
|
||||
- **Corrected Priority Score eligibility:** Stations without a common available band, or with an overriding NOT-QRV mark, are no longer offered as priority candidates. An open band at a station already worked elsewhere can receive a separate Priority Boost.
|
||||
|
||||
- **Extended sked creation:** The band is selected from those enabled locally. `SSB` or `CW` is selected explicitly for the Win-Test handover instead of attempting to infer the mode unreliably from the band.
|
||||
|
||||
- **More precise Win-Test sked handover:** The QRG must belong to the selected band. Visible KST suffixes are removed for the logging target while portable callsign components are preserved. `ADDSKED` timestamps are generated correctly. A failed handover does not remove the internal KST4Contest sked.
|
||||
|
||||
- **Exact sked targets:** The timeline and automatic reminders use the complete visible KST callsign. A sked for `DN9APW-2` is not accidentally sent to another variant of the same base callsign.
|
||||
|
||||
- **Reworked beacon and automatic replies:** Both chat categories use one shared timer while retaining separate enable switches and message texts. The minimum permitted interval is two minutes and message texts are limited to 120 characters. The stored beacon state is restored from the configuration at startup.
|
||||
|
||||
- **Central variable resolution:** Message variables used by beacons, shortcuts, snippets and other generated text are processed by one shared resolver.
|
||||
|
||||
- **Improved message tables:** Truncated messages show their complete text in a tooltip. Recognised web addresses can be opened in the system browser.
|
||||
|
||||
- **More compact filter bar:** Filters retain their compact arrangement at normal widths and wrap only when the available space is genuinely insufficient. This allows the centre divider to be moved further to the right.
|
||||
|
||||
- **DXLog full-log import:** In addition to `contactinfo`, the UCXLog-compatible UDP listener processes `contactreplace`. This allows a complete log broadcast by DXLog.net to be imported.
|
||||
|
||||
- **Improved version comparison:** Versions are compared semantically so that patch releases and Nightly versions are not misclassified by conversion to a floating-point number.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Reliable initial user list:** Invalid or incomplete `UA0` member records are rejected and logged individually without preventing alphabetically following members from being processed. Valid entries are staged per category and published as one complete snapshot when the first corresponding `UE` end marker is received.
|
||||
|
||||
- **User list disappearing after login:** ON4KST may send additional `UE` frames for the same category after name, state or other live updates. Repeated end markers are now detected and ignored so that an already populated user list cannot be replaced by an empty snapshot.
|
||||
|
||||
- **Failed initial connection and lost sockets:** An unavailable server during startup no longer sends KST4Contest into an endless or busy-wait loop. The user interface remains responsive and further attempts use bounded reconnect backoff. Sockets closed by the server, or connections without inbound traffic for an excessive period, are also detected reliably.
|
||||
|
||||
- **Message-bus diagnostics:** Correctly processed ON4KST frames are no longer reported additionally as `Critical, detected unhandled Chatmessage`. Only genuinely unknown frames reach the fallback diagnostic branch.
|
||||
|
||||
- **Long-running station-selection failure:** Chat members managed by the message thread have been decoupled from the JavaFX view. Simultaneous data and table updates therefore no longer cause broken selection models or concurrent-modification problems after longer runtimes.
|
||||
|
||||
- **No phantom chat members from UM3:** Historical or additional server messages no longer create user-list entries for stations which are not actually logged into the chat.
|
||||
|
||||
- **Messages to callsigns with suffixes:** Several active variants of the same base callsign no longer overwrite each other. This resolves [Issue #73](https://github.com/praktimarc/kst4contest/issues/73).
|
||||
|
||||
- **DX cluster locators:** The reporting and reported station no longer receive the same locator accidentally. This resolves [Issue #48](https://github.com/praktimarc/kst4contest/issues/48).
|
||||
|
||||
- **Worked state in “QSO of the other”:** The Worked columns again use the correct transmitting and receiving station.
|
||||
|
||||
- **Missing SECONDAP data:** A missing second aircraft-scatter opportunity no longer produces an invalid text selection and JavaFX exception when the field is edited.
|
||||
|
||||
- **Historical callsigns:** Highlighting or selecting a callsign which is no longer present in the active user list no longer causes an exception.
|
||||
|
||||
- **Win-Test sked time and callsign handling:** Timestamps, band-to-QRG assignment, KST suffixes and portable callsigns are handled correctly.
|
||||
|
||||
- **Filter reset:** All filter predicates are actually removed, and the visible button state again matches the effective filter state.
|
||||
|
||||
### Documentation and packaging
|
||||
|
||||
- The German and English manuals have been systematically checked against the source code, extended and supplied with current screenshots. The documentation now describes not only the controls, but also how information is derived and where the limits of a result lie.
|
||||
|
||||
- The AUR provides `kst4contest-bin`, `kst4contest` and `kst4contest-git` for Arch Linux.
|
||||
|
||||
- The download page distinguishes between Stable, Beta and Nightly and offers only packages which actually belong to the corresponding channel.
|
||||
|
||||
- Nightly packages are built automatically from the current `main` branch. Stable and Beta releases use predictable asset names for the supported platforms.
|
||||
|
||||
- **Signed and notarized macOS packages:** The DMG files for Apple Silicon and Intel are signed with an Apple Developer ID and notarized by Apple, with the notarization ticket stapled into the DMG. The first launch now works by double-clicking, without the previous detour through **Open** in the context menu, and the check also succeeds without an internet connection. This applies to Nightly, Beta and Stable packages alike. The Windows packages remain unsigned.
|
||||
|
||||
- **Correct bundle identifier and version on macOS:** The application now identifies itself as `de.x08.KST4Contest` rather than `kst4contest.view`, and carries its actual version number in the bundle. Previously every release reported version `1.0` in Finder's **Get Info** panel. Existing settings are unaffected, because KST4Contest stores its data in `~/.praktiKST/` rather than keying it to the bundle identifier.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- The active terrain provider uses Open-Meteo with Copernicus GLO-90 data and no more than 100 elevation samples per path.
|
||||
|
||||
- The atmospheric K factor is currently fixed at `4/3`.
|
||||
|
||||
- An antenna height of 10 metres above ground is assumed for the remote station.
|
||||
|
||||
- Aircraft-scatter information from AirScout and the terrain analysis in the station map remain separate assessments.
|
||||
|
||||
- More detailed configuration of station height, frequency and K factor is being tracked in [Issue #74](https://github.com/praktimarc/kst4contest/issues/74).
|
||||
|
||||
---
|
||||
|
||||
## v1.41.1 (2026-07-08)
|
||||
|
||||
**Hotfix for text input and focus handling**
|
||||
|
||||
### Fixed
|
||||
|
||||
- The message input field was unexpectedly cleared after some time or during particular UI updates.
|
||||
- Filtering or selecting a station could move the input focus back to the send field unintentionally.
|
||||
|
||||
The corrected version is available as [Release v1.41.1](https://github.com/praktimarc/kst4contest/releases/tag/v1.41.1).
|
||||
|
||||
---
|
||||
## v1.41.0 (2026-07-01)
|
||||
|
||||
**Station map, bounded message stores and screen-aware main window**
|
||||
|
||||
### Added
|
||||
|
||||
- **Station map:** An interactive OpenStreetMap-based map displays active chat members for which a usable locator is available.
|
||||
|
||||
- **Antenna sector and connection line:** The map shows the current local QTF, configured antenna beamwidth, maximum QRB and the path to the selected station.
|
||||
|
||||
- **Maidenhead grid:** A locator grid adapted to the current zoom level provides geographical orientation.
|
||||
|
||||
- **Terrain profile:** An elevation profile for a selected station can be requested from the Open-Meteo Elevation API. The active source uses Copernicus GLO-90 data and no more than 100 evenly distributed samples.
|
||||
|
||||
- **Geometrical path analysis:** The calculation includes line of sight, Earth curvature using `k = 4/3`, radio and terrain horizons, the first Fresnel zone and a rough obstruction estimate.
|
||||
|
||||
- **Local map proxy:** Leaflet is bundled with the application. Map tiles are loaded through a local proxy so that no external JavaScript library is required at runtime. An Internet connection is still required for OpenStreetMap tiles and online elevation data.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Bounded message stores:** The global chat-message list is reduced from more than 30,000 entries to 25,000. The separate DX cluster store is reduced from more than 10,000 entries to 8,000.
|
||||
|
||||
- **Screen-aware startup size:** The main window is checked against the visible area of the primary screen at startup and reduced or repositioned when necessary.
|
||||
|
||||
- **More compact user interface:** Several UI sections were adapted for smaller displays and movable dividers.
|
||||
|
||||
### Scope of the map function
|
||||
|
||||
The station map and AirScout may refer to the same remote station, but their calculations remain separate. Aircraft used by AirScout are not included in the terrain profile.
|
||||
|
||||
Classes for Copernicus GLO-30, offline DEM imports and additional terrain providers existed in the source tree but were not part of the active calculation chain in v1.41. The elevation source actually used was Open-Meteo based on Copernicus GLO-90.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## v1.40 (2026-02-16)
|
||||
**Major Feature Release: Score System, AP Timeline, Win-Test, PSTRotator**
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ After a change, click **Save Settings** and restart KST4Contest. Band columns an
|
||||
|
||||
### Antenna Beamwidth
|
||||
|
||||
Enter a realistic value for your antenna's beamwidth (in degrees). This value is used for the [Sked Direction Highlighting](Features#sked-direction-highlighting). A test value of 50° has proven effective; DM5M uses quads with 69°.
|
||||
Enter a realistic value for your antenna's beamwidth (in degrees). This value is used for the [Sked Direction Highlighting](en-Features#sked-direction-highlighting). A test value of 50° has proven effective; DM5M uses quads with 69°.
|
||||
|
||||
> **Do not** enter fantasy values – the direction calculations will become useless.
|
||||
|
||||
@@ -48,51 +48,314 @@ Enter a realistic value for your antenna's beamwidth (in degrees). This value is
|
||||
|
||||
Maximum distance (in km) for which direction warnings should be triggered. A realistic value for DM5M is 900 km. Stations farther away are ignored for highlighting purposes.
|
||||
|
||||
### Path Analysis and Link Budget
|
||||
|
||||
The station map uses several values from the **Station** tab to calculate the terrain profile and link budget for the selected remote station. Enter the local station data as realistically as possible. The remote-station values remain global assumptions unless more accurate information is available.
|
||||
|
||||
| Setting | Use |
|
||||
|---|---|
|
||||
| **Own antenna height AGL** | Height of the local antenna above the surrounding terrain in metres |
|
||||
| **Own TX power W** | Local transmit power in watts |
|
||||
| **Own ant. gain dBi** | Gain of the local antenna in dBi |
|
||||
| **DX OM TX power W** | Assumed transmit power of the remote station in watts |
|
||||
| **DX OM ant. gain dBi** | Assumed antenna gain of the remote station in dBi |
|
||||
|
||||
**AGL** means *Above Ground Level*. Do not enter the height above sea level. The terrain elevation at the local station already comes from the elevation profile; KST4Contest adds the configured antenna height to this value.
|
||||
|
||||
For the remote station, KST4Contest currently uses a fixed antenna height of 10 metres above the local terrain. Its transmit power and antenna gain come from the two **DX OM** fields. These values are deliberately assumptions: ON4KST provides neither the actual antenna height nor the complete station data of the remote operator.
|
||||
|
||||
Antenna gains must be entered in `dBi`. Add `2.15 dB` before entering a value specified in `dBd`:
|
||||
|
||||
```text
|
||||
dBi = dBd + 2.15 dB
|
||||
```
|
||||
|
||||
The current QTF, configured antenna beamwidth and default maximum QRB affect the map display. The bands enabled for the local station and the frequency derived for the remote station also affect the selection of the analysis frequency.
|
||||
|
||||
For the terrain profile, KST4Contest uses the antenna heights, elevation model and Earth curvature with a fixed effective Earth-radius factor of `k = 4/3`. The link budget additionally includes:
|
||||
|
||||
- distance and the current analysis frequency,
|
||||
- transmit powers and antenna gains in both directions,
|
||||
- frequency-dependent estimated feeder losses,
|
||||
- free-space path loss, and
|
||||
- a rough additional loss caused by the relevant obstruction in the terrain profile.
|
||||
|
||||
The calculation is performed in both directions. The weaker direction determines the common SSB or CW margin. Optimistic power or antenna values therefore improve the displayed result, but they do not improve the real radio path.
|
||||
|
||||
The results remain engineering estimates. Current propagation conditions, local obstructions, vegetation, buildings, interference and unknown station parameters can change the actual result considerably. Operation, frequency selection and the limits of the calculation are described under [Station Map and Path Analysis](en-Features#station-map-and-path-analysis-from-v141).
|
||||
|
||||
---
|
||||
|
||||
## Server Settings (from v1.31)
|
||||
|
||||
The chat server DNS and port are configurable in the Preferences:
|
||||
The connection details for the ON4KST chat server are shown at the top of the **Station** tab. They normally do not need to be changed.
|
||||
|
||||
- **Server DNS**: Default `www.on4kst.org` (changed from `www.on4kst.info` in v1.31 hotfix).
|
||||
- **Port**: Default port of the ON4KST server.
|
||||
| Setting | Meaning |
|
||||
|---|---|
|
||||
| **ON4KST server** | DNS name or IP address of the chat server; the software default is `www.on4kst.org` |
|
||||
| **Port** | TCP port of the chat server; the default is `23001` |
|
||||
|
||||
A change is only needed if the server moves or an alternative endpoint is used.
|
||||
`www.on4kst.info` may also remain in an existing, working configuration. There is no reason to change it merely because the displayed name differs from the software default, provided that the chat connection remains reliable.
|
||||
|
||||
These values apply only to the outgoing ON4KST chat connection. They do not change the local DX cluster server or any connection to AirScout, PSTRotator or logging software.
|
||||
|
||||
The port must be between `1` and `65535`. An invalid entry is rejected and replaced with the last valid value. An incorrect server name or port, on the other hand, prevents KST4Contest from connecting to the chat.
|
||||
|
||||
Changing either value does not alter an existing TCP connection. Disconnect and reconnect the chat, or restart KST4Contest. Then use **Save Settings** so that the values are retained for the next program start.
|
||||
|
||||
---
|
||||
|
||||
## Log Sync Settings
|
||||
|
||||
Three methods are available for automatically marking worked stations. Details: [Log Synchronisation](en-Log-Sync).
|
||||
The **Log sync** tab selects the sources from which KST4Contest imports worked stations. The three input paths provide different levels of detail:
|
||||
|
||||
### Universal File Based Callsign Interpreter (Simplelogfile)
|
||||

|
||||
|
||||
Interprets any log file using regex for callsign patterns. No band information is available. Suitable as a fallback or for log programs that are not directly supported.
|
||||
| Input path | Data used | Result in KST4Contest |
|
||||
|---|---|---|
|
||||
| **Simplelogfile** | Callsigns read from a selected file | global Worked status, but no band or locator information |
|
||||
| **General QSO UDP listener** | QSO packets from UCXLog, QARTest, N1MM+ and DXLog.net | global and per-band Worked status plus worked grid square where both band and locator are transmitted |
|
||||
| **Win-Test network listener** | native Win-Test network packets | global and per-band Worked status, locator information and, depending on the settings, QRG synchronisation and sked handover |
|
||||
|
||||
### Network Listener for Logger's QSO UDP Broadcast
|
||||
The file-based interpreter is mainly useful when the logging application provides no supported network interface. A callsign match alone, however, contains neither a reliable band nor a locator. Use one of the network listeners wherever possible if per-band information is required.
|
||||
|
||||
**Recommended method.** KST4Contest listens for UDP packets sent by the logging software to the broadcast address when a QSO is saved. Stations are marked with band information. UDP port: default **12060**. (Used by UCXLog, N1MM+, QARTest, DXLog.net, etc.).
|
||||
The general QSO UDP listener is the recommended interface for UCXLog, QARTest, N1MM+ and DXLog.net. QSO and `RadioInfo` packets use the same configurable UDP port; the default is `12060`. Separate options in **Log sync** and **TRX sync** determine whether the received QSO and frequency information is processed.
|
||||
|
||||
### Win-Test Network Listener (Additional UDP Listener)
|
||||
Win-Test uses its own network protocol and therefore has a separate listener. Its default port is `9871`. If this port is changed while the listener is enabled, KST4Contest restarts the Win-Test listener on the new port. After changing the shared UDP port `12060`, KST4Contest must instead be restarted completely.
|
||||
|
||||
A dedicated network listener for Win-Test. KST4Contest receives and processes Win-Test-specific UDP packets (including sked handovers) on the configured port.
|
||||
All enabled input paths may be used in parallel. Their Worked information is merged into the same internal database; identical reports do not create separate Worked states. KST4Contest must be running when a QSO is saved unless the logging application can resend the existing log.
|
||||
|
||||
Configuration of the individual logging applications, band and locator handling, and the Win-Test sked handover are described under [Log Synchronisation](en-Log-Sync).
|
||||
|
||||
---
|
||||
|
||||
## TRX Sync Settings
|
||||
|
||||
Receives the current transceiver frequency from the logging software via UDP. This enables the automatic population of the `MYQRG` variable. Useful for:
|
||||
TRX synchronisation imports the current frequency from the logging application and makes it available in KST4Contest as the local QRG of the first chat category. QSO and frequency synchronisation may use the same UDP receiver, but they remain separate functions: receiving a `RadioInfo` packet does not mark a station as worked, and receiving a QSO packet does not automatically change the local QRG.
|
||||
|
||||
- Quickly inserting your own QRG into chat messages.
|
||||
- Automatic CQ beacon with current frequency.
|
||||

|
||||
|
||||
> **Note for multi-setup**: When running two logging programs on two computers but only one KST4Contest instance, only one logging program should send frequency packets. KST4Contest cannot distinguish between sources.
|
||||
### Available QRG Sources
|
||||
|
||||
| Source | Setting | Behaviour |
|
||||
|---|---|---|
|
||||
| **General RadioInfo listener** | `Update MYQRG from RadioInfo messages received on the shared log-sync port` | Processes compatible `RadioInfo` packets on the UDP port shared with QSO synchronisation. The default port is `12060`. |
|
||||
| **Win-Test STATUS** | `Win-Test STATUS QRG Sync` | Processes the main or pass frequency from native Win-Test `STATUS` packets. The Win-Test listener uses its separately configured port, which defaults to `9871`. |
|
||||
| **Manual entry** | Disable both automatic QRG sources | The local QRG can be entered manually in the main window. |
|
||||
|
||||
The general listener is intended for logging applications which transmit compatible `RadioInfo` packets. Depending on their individual configuration, this includes UCXLog, N1MM+, QARTest and DXLog.net. QSO and `RadioInfo` packets use the same port configured under **Log sync**, but separate options determine whether KST4Contest processes QSO information, TRX information or both packet types.
|
||||
|
||||
Restart KST4Contest after changing the shared UDP port. Changes to the two QRG-sync checkboxes take effect immediately.
|
||||
|
||||
### Which QRG Is Updated?
|
||||
|
||||
Both automatic sources update `MYQRG` only. This is the local QRG of the first or primary chat category.
|
||||
|
||||
If a second chat is enabled, its QRG remains independent. It is not derived from incoming TRX packets and is available through `SECONDQRG`. The first category can therefore follow the logging application's frequency automatically while a separate QRG is entered manually for the second category.
|
||||
|
||||
As soon as at least one automatic QRG source is enabled, the first category's QRG field in the main window is bound to the received value. Manual entry becomes available again when both the general RadioInfo listener and Win-Test STATUS synchronisation are disabled.
|
||||
|
||||
### Main or Pass Frequency from Win-Test
|
||||
|
||||
By default, KST4Contest uses the main frequency contained in the Win-Test `STATUS` packet.
|
||||
|
||||
Enable `Use pass frequency from Win-Test STATUS` to use the packet's pass frequency instead. This is useful, for example, when Win-Test maintains a different frequency during split operation and that is the frequency which should be announced in the chat.
|
||||
|
||||
If the packet does not contain a valid pass frequency, KST4Contest automatically falls back to the main frequency. A missing pass frequency therefore neither clears `MYQRG` nor replaces it with an obviously incorrect number.
|
||||
|
||||
Frequencies use a consistent KST4Contest display format, for example:
|
||||
|
||||
```text
|
||||
50.300.00
|
||||
144.300.00
|
||||
1296.100.00
|
||||
10368.100.00
|
||||
```
|
||||
|
||||
The number of digits before the first dot is derived from the frequency. Microwave frequencies with four or five MHz digits are therefore formatted correctly as well.
|
||||
|
||||
### Selecting the Win-Test Station
|
||||
|
||||
Several stations in a Win-Test network may transmit `STATUS` packets at the same time. `Win-Test station name filter` selects the station which is allowed to update the local QRG in KST4Contest.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
STN1
|
||||
```
|
||||
|
||||
The comparison is case-insensitive. If the field is empty, `STATUS` packets from every Win-Test station are accepted.
|
||||
|
||||
In a multi-operator setup, set the filter to the station name of the operating position which actually belongs to this KST4Contest instance. Otherwise, a packet from another position may replace the QRG currently being displayed.
|
||||
|
||||
### Using MYQRG and SECONDQRG
|
||||
|
||||
The synchronised QRG can be used in every text processed by the common KST4Contest variable resolver. This includes:
|
||||
|
||||
- the send field,
|
||||
- shortcuts,
|
||||
- snippets, and
|
||||
- automatic beacons.
|
||||
|
||||
`MYQRG` contains the complete QRG of the first chat category. `MYQRGSHORT` uses only its first seven characters. `SECONDQRG` contains the separately entered QRG of the second chat category.
|
||||
|
||||
Examples:
|
||||
|
||||
```text
|
||||
I am calling cq at MYQRG
|
||||
cq on MYQRGSHORT
|
||||
second chat qrg SECONDQRG
|
||||
```
|
||||
|
||||
The values are inserted when the message text is resolved. If the logging application changes frequency between two beacon runs, the next message already uses the updated value.
|
||||
|
||||
The local QRG may also be used as a fallback when handing a sked over to Win-Test. This only happens if the QRG can be parsed and belongs to the band explicitly selected for the sked. See [Log Synchronisation](en-Log-Sync#handing-skeds-over-to-win-test) for details.
|
||||
|
||||
Further information about text variables: [Macros and Variables](en-Macros-and-Variables#variables).
|
||||
|
||||
### Multiple Loggers or Radios
|
||||
|
||||
Every enabled QRG source writes to the same `MYQRG` value. KST4Contest does not currently assign incoming `RadioInfo` or `STATUS` packets to a particular radio or chat category.
|
||||
|
||||
If the general RadioInfo listener and Win-Test synchronisation are enabled at the same time, the most recently processed packet therefore determines the displayed QRG. The same applies when several logging applications send frequency packets to one KST4Contest instance.
|
||||
|
||||
For a setup containing several radios:
|
||||
|
||||
- QSO packets may be received from several logging applications.
|
||||
- Frequency packets should only be transmitted by the source which is intended to control `MYQRG`.
|
||||
- A Win-Test network should additionally use the station-name filter.
|
||||
- If two completely independent QRG synchronisations are required, two separate KST4Contest instances provide the clearer arrangement.
|
||||
|
||||
In other words, combining several Worked sources is useful. Combining several simultaneously transmitting frequency sources merely creates a contest over which packet arrived last.
|
||||
|
||||
Click **Save Settings** after completing the configuration.
|
||||
|
||||
---
|
||||
|
||||
## AirScout Settings
|
||||
|
||||
Configuration of the interface to AirScout for aircraft scatter detection. Details: [AirScout Integration](en-AirScout-Integration).
|
||||
The **AirScout** tab configures the UDP connection between KST4Contest and AirScout. KST4Contest does not request a general aircraft feed. It submits the station paths which are currently relevant, while AirScout calculates the matching aircraft and returns the result to the requesting KST4Contest instance.
|
||||
|
||||
AirScout `0.9.9.5` or newer is required.
|
||||
|
||||
{ width=85% }
|
||||
|
||||
### UDP Connection Settings
|
||||
|
||||
| Setting | Default | Use |
|
||||
|---|---:|---|
|
||||
| **Enable AirScout UDP integration** | disabled | Enables AirScout requests and the processing of returned information |
|
||||
| **AirScout server identifier** | `AS` | Logical name of the AirScout instance being addressed |
|
||||
| **KST4Contest client identifier** | `KST` | Logical name of this KST4Contest instance |
|
||||
| **AirScout UDP port** | `9872` | Shared UDP port for requests and responses |
|
||||
| **Select AirScout frequency automatically per station** | enabled | Derives a suitable band and frequency from the current context of each remote station |
|
||||
| **Forced AirScout band value** | `1440000` | Uses one fixed AirScout band value for every station when automatic selection is disabled |
|
||||
|
||||
When **Enable AirScout UDP integration** is disabled, KST4Contest neither sends AirScout requests nor processes incoming AirScout responses. The UDP receiver may remain bound so that the integration can be enabled again during the current connection.
|
||||
|
||||
KST4Contest sends AirScout packets to the broadcast address `255.255.255.255`. No separate destination IP address is therefore configured. AirScout and KST4Contest must be able to receive the same UDP broadcast; routers do not normally forward this type of broadcast into another network. If communication fails, check the UDP port, local firewall and network assignment first.
|
||||
|
||||
### Automatic Band Selection per Station
|
||||
|
||||
**Auto per station** is the recommended setting. Instead of using one fixed band value for every remote station, KST4Contest derives a suitable frequency from the available operating context.
|
||||
|
||||
The sources are evaluated in this order:
|
||||
|
||||
1. the most recently detected QRG of the remote station, provided that it is no more than 30 minutes old,
|
||||
2. one unambiguous complete QRG in the name field of an active chat entry,
|
||||
3. unambiguous band designators in the name field,
|
||||
4. 432 MHz if the same station is active in both the VHF/UHF and Microwave categories and 432 MHz is enabled for the local station,
|
||||
5. the lowest locally enabled band belonging to the supported chat category.
|
||||
|
||||
Active chat variants of the same base callsign are evaluated together. Entries such as `CALLSIGN`, `CALLSIGN-2` and `CALLSIGN-432` can therefore contribute to the same band decision while remaining separate chat members for message processing.
|
||||
|
||||
Only bands enabled under **My station uses …** are eligible for automatic selection. A manually assigned NOT-QRV mark excludes the corresponding band and takes precedence over automatically detected QRG or name information.
|
||||
|
||||
The 50/70 MHz, VHF/UHF, Microwave and EME/JT65 chat categories are supported. Other ON4KST categories do not participate in AirScout band resolution. If no sufficiently reliable frequency can be determined, KST4Contest omits the request for that station. An arbitrary fallback to 144 MHz would produce a syntactically complete packet, but not necessarily a useful calculation.
|
||||
|
||||
Automatic AirScout selection uses the same propagation-frequency resolver as the internal path analysis. Both functions therefore evaluate the station path from the same technical basis.
|
||||
|
||||
### Fixed AirScout Band
|
||||
|
||||
When **Auto per station** is disabled, KST4Contest uses the value entered under **Forced AirScout band value** for every station.
|
||||
|
||||
Enter the value in the unit used by the AirScout UDP interface:
|
||||
|
||||
| Band | AirScout value |
|
||||
|---|---:|
|
||||
| 50 MHz | `500000` |
|
||||
| 70 MHz | `700000` |
|
||||
| 144 MHz | `1440000` |
|
||||
| 432 MHz | `4320000` |
|
||||
| 1296 MHz | `12960000` |
|
||||
| 2320 MHz | `23200000` |
|
||||
| 3400 MHz | `34000000` |
|
||||
| 5760 MHz | `57600000` |
|
||||
| 10368 MHz | `103680000` |
|
||||
| 24048 MHz | `240480000` |
|
||||
|
||||
In fixed mode, neither the remote station's recently detected QRG nor a band stated in its name is considered. This option is therefore mainly useful for a station setup which is clearly limited to one band, or for troubleshooting.
|
||||
|
||||
### Server and Client Identifiers
|
||||
|
||||
The identifiers belong to the AirScout protocol. They are not DNS names or IP addresses.
|
||||
|
||||
Outgoing requests contain the client identifier followed by the server identifier:
|
||||
|
||||
```text
|
||||
"KST" "AS"
|
||||
```
|
||||
|
||||
AirScout returns them in the opposite order:
|
||||
|
||||
```text
|
||||
"AS" "KST"
|
||||
```
|
||||
|
||||
KST4Contest processes a response only if both identifiers exactly match the current configuration. The comparison is case-sensitive.
|
||||
|
||||
Identifiers must not be empty and must not contain quotation marks or line breaks.
|
||||
|
||||
If several KST4Contest instances operate in the same network, assign a distinct client identifier to each one, for example:
|
||||
|
||||
```text
|
||||
KST-144
|
||||
KST-432
|
||||
```
|
||||
|
||||
If several AirScout instances are present, use distinct server identifiers as well. This prevents a response intended for one operating position from being processed by another KST4Contest instance.
|
||||
|
||||
### Which Stations Are Requested?
|
||||
|
||||
KST4Contest starts the first periodical AirScout request approximately ten seconds after the chat connection has been established. Further requests follow every 60 seconds.
|
||||
|
||||
An active station is included only if:
|
||||
|
||||
- a usable callsign is available,
|
||||
- a locator is available,
|
||||
- its distance has been calculated,
|
||||
- its distance is below the configured **Maximum QRB**, and
|
||||
- a usable band can be determined.
|
||||
|
||||
Several active chat entries of the same base callsign do not create a separate identical path calculation for every suffix. The returned AirScout information is subsequently assigned to the corresponding active chat variants.
|
||||
|
||||
This selection does more than reduce network traffic. It also prevents AirScout from continuously calculating paths outside the intended working range of the local station.
|
||||
|
||||
### Applying Changed Settings
|
||||
|
||||
The following changes are used for new packets immediately after leaving the input field or changing the checkbox:
|
||||
|
||||
- enabling or disabling the AirScout integration,
|
||||
- server identifier,
|
||||
- client identifier,
|
||||
- automatic or fixed band selection, and
|
||||
- the forced band value.
|
||||
|
||||
After changing the UDP port, disconnect and reconnect the chat or restart KST4Contest. The existing UDP receiver otherwise remains bound to the previous port.
|
||||
|
||||
Click **Save Settings** afterwards to retain the configuration.
|
||||
|
||||
AirScout setup, aircraft display and the meaning of the returned AP information are described under [AirScout Integration](en-AirScout-Integration).
|
||||
|
||||
---
|
||||
|
||||
@@ -129,7 +392,44 @@ The dropdown is neither a filter nor an override for complete frequencies. `432.
|
||||
|
||||
Before using the fallback, KST4Contest checks the sender's recent band context. If a complete frequency has been detected for the same station during the previous 30 minutes, that band takes precedence. A fallback setting of `144 MHz` therefore still turns `.100` into `432.100 MHz` if the station mentioned `432.088` shortly before.
|
||||
|
||||
Although the setting is located in the Notification tab, it affects the general QRG parser. It therefore influences the QRG column, detected active bands, priority calculations, band-upgrade hints and other functions which use a known station frequency – not only DX cluster spots.
|
||||
Although this setting is located in the Notification tab, it affects the general QRG parser. It therefore influences the QRG column, detected active bands, priority calculations, band-upgrade hints and other functions which use a known station frequency – not only DX cluster spots.
|
||||
|
||||
Further details, including numbers which are deliberately ignored, are described under [QRG Detection](en-Features#qrg-detection).
|
||||
|
||||
### Local DX Cluster Output
|
||||
|
||||
KST4Contest can forward detected directional opportunities to logging software as DX cluster spots. A frequency recognised in the chat can therefore appear directly in the logger's band map without being entered manually.
|
||||
|
||||
The **Enable the local DX Cluster server …** checkbox starts or stops the local TCP server. When KST4Contest is connected to the chat, the change takes effect immediately.
|
||||
|
||||
The following settings and controls belong to the local DX cluster output:
|
||||
|
||||
- **TCP port**: Port on which KST4Contest accepts connections from DX cluster clients. The default is `8000`. Changing the port while the server is running restarts it on the new port. The logger must then reconnect to that port.
|
||||
- **Fallback band for relative QRG detection**: The global fallback band described above. The test spot uses `.300` on this band. Actual spots use the QRG detected for the respective sender.
|
||||
- **Spotter callsign**: Callsign shown as the spotter in generated DX cluster entries. A callsign different from the contest callsign should be used. Some logging programs filter spots apparently sent by the local station or treat them differently from external spots.
|
||||
- **Send test spot**: Sends the following entry to every currently connected DX cluster client:
|
||||
|
||||
```text
|
||||
Spotted callsign: DO5AMF
|
||||
Comment: Testing DXC-Spot: Congrats, you donated $100!
|
||||
Frequency: .300 on the selected fallback band
|
||||
```
|
||||
|
||||
With `144 MHz` selected as the fallback band, the resulting frequency is approximately `144.300 MHz`.
|
||||
|
||||
The comment is a deliberately retained Easter egg. It has no technical meaning and, despite being remarkably specific, does not initiate a payment. Its practical purpose is to make the test spot easy to identify in the logging software.
|
||||
|
||||
The test works only if
|
||||
|
||||
1. KST4Contest is connected to the ON4KST chat,
|
||||
2. the local DX cluster server is enabled, and
|
||||
3. at least one DX cluster client is connected to KST4Contest.
|
||||
|
||||
KST4Contest does not generate a spot for every frequency found in the chat. An actual spot is created only when a directed message between two stations indicates a relevant antenna direction for the local station and a usable frequency is known for the sender.
|
||||
|
||||
The complete derivation and logger setup are described under [Built-in DX Cluster Server](en-DX-Cluster-Server).
|
||||
|
||||
---
|
||||
|
||||
### Band Upgrade Hint after a Log Entry
|
||||
|
||||
@@ -154,7 +454,7 @@ The two options serve different purposes:
|
||||
|
||||
The Priority Boost is only one factor in the complete calculation. Distance, antenna direction, recent activity, AirScout data, skeds and negative hints may still change the final order. Enabling the option therefore guarantees neither a particular score nor a particular position in the priority list.
|
||||
|
||||
The other score weights currently have no separate user-interface controls. Several existing settings nevertheless provide input data for the calculation, particularly the [enabled bands](#enabled-bands), [antenna beamwidth](#antenna-beamwidth), [default maximum QRB](#default-maximum-qrb) and [AirScout settings](#airscout-settings).
|
||||
The other score weights currently have no separate user-interface controls. Several existing settings nevertheless provide input data for the calculation, particularly the [active bands](#active-bands), [antenna beamwidth](#antenna-beamwidth), [default maximum QRB](#default-maximum-qrb) and [AirScout settings](#airscout-settings).
|
||||
|
||||
The complete calculation is described under [Priority Score and Priority List](en-Features#priority-score-and-priority-list-from-v140).
|
||||
|
||||
@@ -167,80 +467,416 @@ Further explanation: [Band Upgrade Hint after a Log Entry](en-Features#band-upgr
|
||||
|
||||
## Shortcut Settings
|
||||
|
||||
Configuration of quick-access buttons that appear directly in the main window. Clicking a button inserts the configured text into the send field. All [variables](Macros-and-Variables#variables) can be used.
|
||||

|
||||
|
||||
Each entry in the upper part of the **Shortcuts** tab creates one button above the message field in the main window. Pressing the button appends its configured text to the current contents of the send field.
|
||||
|
||||
If the shortcut contains a [variable](en-Macros-and-Variables#variables), it is replaced with its current value when the text is inserted. A shortcut such as
|
||||
|
||||
```text
|
||||
pse call me at MYQRGSHORT
|
||||
```
|
||||
|
||||
may therefore insert:
|
||||
|
||||
```text
|
||||
pse call me at 144.388
|
||||
```
|
||||
|
||||
The exact entries `MYQRG` and `SECONDQRG` are additionally highlighted as QRG buttons. They insert the current frequency of the primary or secondary chat category respectively.
|
||||
|
||||
The shortcut `/SETNAME MYQRG` is highlighted as well. When it is pressed, KST4Contest resolves `MYQRG` and inserts the complete server command into the send field. The command is not transmitted automatically and can still be checked before it is sent with `Enter` or **TX**.
|
||||
|
||||
|
||||
The order of the table determines the order of the buttons in the main window. Manage the entries as follows:
|
||||
|
||||
1. **Add shortcut** creates a new entry at the beginning of the list and immediately opens it for editing.
|
||||
2. Double-click an existing entry to edit it. Press `Enter` to accept the change.
|
||||
3. To remove an entry, delete its complete contents and confirm with `Enter`.
|
||||
4. Use **Move selected up** and **Move selected down** to change the position of the selected entry.
|
||||
|
||||
Changes appear in the main window immediately. Use **Save Settings** afterwards if they should remain available after the next program start.
|
||||
|
||||
---
|
||||
|
||||
## Snippet Settings
|
||||
|
||||
Text snippets are accessible via:
|
||||
Snippets are longer text blocks intended primarily for messages to a selected station. They can be opened through:
|
||||
|
||||
- **Right-click** on a callsign in the user list
|
||||
- **Right-click** in the CQ message table
|
||||
- **Right-click** in the PM message table
|
||||
- **Keyboard shortcuts**: `Ctrl+1` to `Ctrl+0` for the first 10 snippets
|
||||
- a right-click on a station in the user list;
|
||||
- a right-click on a message in the public chat table;
|
||||
- a right-click on a message in the PM table; or
|
||||
- `Ctrl+1` through `Ctrl+0` for the first ten entries in the snippet list.
|
||||
|
||||
If a callsign is selected in the user list, the snippet is addressed as a direct message:
|
||||
`/CQ CALLSIGN <snippet text>`
|
||||
The keyboard mapping follows the order of the table:
|
||||
|
||||
| Key combination | Snippet |
|
||||
|---|---:|
|
||||
| `Ctrl+1` | first entry |
|
||||
| `Ctrl+2` | second entry |
|
||||
| … | … |
|
||||
| `Ctrl+9` | ninth entry |
|
||||
| `Ctrl+0` | tenth entry |
|
||||
|
||||
A snippet selected from a context menu is appended to the message already prepared in the send field. Selecting a station or message will normally have inserted the appropriate `/cq` destination first.
|
||||
|
||||
A keyboard shortcut behaves differently: it replaces the current contents of the send field with a complete directed message:
|
||||
|
||||
```text
|
||||
/cq CALLSIGN snippet text
|
||||
```
|
||||
|
||||
The complete visible callsign, including any suffix, is retained. Selecting `9A0BB-70` may therefore produce:
|
||||
|
||||
```text
|
||||
/cq 9A0BB-70 pse ur qrg?
|
||||
```
|
||||
|
||||
The selected station's chat category is retained for transmission. If no station is selected, or no snippet is assigned to the chosen key combination, nothing is inserted.
|
||||
|
||||
Variables are resolved when the snippet is inserted. Station-specific variables such as `QRZNAME`, `FIRSTAP` and `SECONDAP` refer to the currently selected station. The prepared message is not sent automatically and can still be checked or edited. Press `Enter` or **TX** to send it; `Esc` clears the send field.
|
||||
|
||||
The snippet list is edited in the same way as the shortcut list:
|
||||
|
||||
1. **Add new snippet** creates a new entry at the beginning of the list.
|
||||
2. Double-click an existing entry to edit it.
|
||||
3. Press `Enter` to accept the change.
|
||||
4. Confirming an empty entry removes it.
|
||||
5. **Move selected up** and **Move selected down** change both the displayed order and the assignment to `Ctrl+1` through `Ctrl+0`.
|
||||
|
||||
The context menus and keyboard mappings are updated immediately. Use **Save Settings** afterwards to store the modified list permanently.
|
||||
|
||||
The complete list of available placeholders and their limitations is described under [Macros and Variables](en-Macros-and-Variables).
|
||||
|
||||
---
|
||||
|
||||
## Beacon Settings
|
||||
|
||||
Configuration of an automatic interval beacon in the public chat channel. Recommended: use the `MYQRG` variable in the text so the current frequency is always up to date. Interval and text are freely configurable.
|
||||

|
||||
|
||||
> **Tip**: Enable the beacon when calling CQ and quickly disable it in the settings window when not calling.
|
||||
A beacon sends a public CQ message at regular intervals. It is intended for operating situations in which the local station calls CQ on a fixed frequency for an extended period. Other stations receive current QRG information without requiring the operator to enter the same message repeatedly.
|
||||
|
||||
KST4Contest uses one shared timer for both chat categories. Each category nevertheless has its own enable setting and message template:
|
||||
|
||||
- **Enable CQ beacon** enables the beacon for the respective category.
|
||||
- **Beacon message** contains the public message for that category.
|
||||
- **Shared beacon interval** sets the common interval used by both categories.
|
||||
|
||||
When both beacons are enabled, they are sent one after the other in their respective categories during the same timer run. The second beacon is only considered while the second chat is enabled and connected.
|
||||
|
||||
### Interval and timer behaviour
|
||||
|
||||
The interval is entered in whole minutes. The minimum permitted value is one minute.
|
||||
|
||||
After the chat connection has been established, KST4Contest performs the first beacon check after approximately ten seconds. The configured interval applies after that initial check.
|
||||
|
||||
Changing the interval while connected restarts the countdown with the new value. The change itself does not cause an immediate beacon message.
|
||||
|
||||
Both categories use the same timer. Separate intervals for the primary and secondary chat cannot be configured.
|
||||
|
||||
### Message text and variables
|
||||
|
||||
A beacon may use the [global variables](en-Macros-and-Variables#variables-in-the-beacon) which depend only on the local station:
|
||||
|
||||
- `MYQRG`
|
||||
- `MYQRGSHORT`
|
||||
- `SECONDQRG`
|
||||
- `MYLOCATOR`
|
||||
- `MYLOCATORSHORT`
|
||||
- `MYCALL`
|
||||
- `MYQTF`
|
||||
|
||||
A suitable message for the primary chat category is:
|
||||
|
||||
```text
|
||||
calling cq at MYQRGSHORT, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Use `SECONDQRG` for the second chat if it operates on a different frequency:
|
||||
|
||||
```text
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Variables are resolved again on every timer run. If the logging software changes the QRG stored in `MYQRG`, the next beacon can already contain the updated value. The message template does not have to be edited.
|
||||
|
||||
`MYQRG` and `MYQRGSHORT` always refer to the primary chat category. Enabling or selecting the second chat does not change this assignment.
|
||||
|
||||
Station-specific variables such as `QRZNAME`, `FIRSTAP` and `SECONDAP` require a selected remote station. A public beacon has no such destination, so these variables are not resolved in beacon messages.
|
||||
|
||||
### Message validation
|
||||
|
||||
KST4Contest validates both the configured template and the message which remains after all variables have been resolved.
|
||||
|
||||
The following restrictions apply:
|
||||
|
||||
- The final message must not be empty.
|
||||
- It must not exceed 120 characters.
|
||||
- The protocol separator `|` is not permitted.
|
||||
- Line breaks are not permitted.
|
||||
|
||||
An invalid entry is not accepted as the new beacon configuration. If a template becomes invalid only after resolving its variables, for example because the resulting text exceeds 120 characters, that beacon run is skipped.
|
||||
|
||||
A template which consists only of a temporarily empty global variable may still be stored. This can happen during startup before the first QRG has been received from the logger. KST4Contest does not send an empty message while the variable has no usable value.
|
||||
|
||||
### When should the beacon be disabled?
|
||||
|
||||
The beacon is useful only while its QRG matches the actual operation. Leaving it enabled while searching the band or changing frequencies frequently may cause other stations to look for the local station on an obsolete frequency.
|
||||
|
||||
In plain terms: the beacon saves work while calling CQ on a fixed QRG. It should be disabled while moving around the band.
|
||||
|
||||
Changes take effect during the current connection. Use **Save Settings** afterwards to retain the enable settings, message templates and interval for the next program start.
|
||||
|
||||
---
|
||||
|
||||
## Messagehandling Settings (from v1.25)
|
||||
|
||||
New settings section with the following options:
|
||||

|
||||
|
||||
- **Auto-reply to all incoming messages**: Configurable automatic reply to private messages.
|
||||
- **Auto-reply with own CQ QRG**: When someone asks for your QRG, KST4Contest automatically replies with the content of the `MYQRG` variable.
|
||||
- **Default filter for the userinfo window**: Pre-configured message filter for the station info window *(for Gianluca :-) )*.
|
||||
The most important use of the general automatic reply concerns stations which are logged into the ON4KST chat but are not taking part in the current contest. During larger contests, sked requests are sometimes sent to many logged-in stations without first checking whether they are participating. Without an automatic reply, the recipients would have to enter the same refusal repeatedly.
|
||||
|
||||
KST4Contest can answer these requests with a predefined message. A separate function provides the local QRG when a private message contains a recognised frequency request. Both functions can be enabled independently.
|
||||
|
||||
### General automatic reply
|
||||
|
||||
**Enable automatic reply to all private messages** answers incoming private messages with the text entered in the adjacent field. One common text is used for both chat categories. The configured capitalisation is preserved.
|
||||
|
||||
A suitable message is:
|
||||
|
||||
```text
|
||||
Sri, I am not taking part in this contest. No skeds.
|
||||
```
|
||||
|
||||
Do not add the `[KST4C Automsg]` prefix to the configured text. KST4Contest inserts it automatically.
|
||||
|
||||
The message received by the remote station may therefore be:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] Sri, I am not taking part in this contest. No skeds.
|
||||
```
|
||||
|
||||
The incoming private message remains visible. The function neither blocks nor discards the request; it merely avoids entering the same answer repeatedly.
|
||||
|
||||
The reply is addressed to the sender's complete callsign, including any visible suffix, and sent through the chat category in which the private message was received. This distinction matters when two categories are connected at the same time: a request received through the microwave chat must not be answered accidentally through the VHF/UHF chat.
|
||||
|
||||
An empty or whitespace-only answer does not produce an automatic message. A configured text containing the protocol separator `|` or a line break is rejected as well.
|
||||
|
||||
### Automatic QRG reply
|
||||
|
||||
**Enable automatic QRG replies** reacts to common QRG requests. Matching is case-insensitive and looks for the following text fragments:
|
||||
|
||||
```text
|
||||
ur qrg?
|
||||
your qrg?
|
||||
qrg?
|
||||
freq?
|
||||
pse qrg
|
||||
```
|
||||
|
||||
The answer contains only the QRG belonging to the category in which the request was received:
|
||||
|
||||
| Incoming private message | QRG used for the reply |
|
||||
|---|---|
|
||||
| Primary category | current QRG of the primary category |
|
||||
| Second chat category | current QRG of the second category |
|
||||
|
||||
A possible reply is:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] QRG is: 144.300.00
|
||||
```
|
||||
|
||||
The values come from the same QRG fields used by `MYQRG` and `SECONDQRG`. The primary QRG may be entered manually or updated through [TRX synchronisation](#trx-sync-settings). The second category uses the value configured or entered for that category.
|
||||
|
||||
If no QRG is available for the incoming category, KST4Contest does not send an incomplete reply. A message containing `QRG is:` without a frequency would technically answer the request while providing no useful information. It is therefore rejected before reaching the transmit queue.
|
||||
|
||||
When both automatic-reply functions are enabled, the QRG reply takes precedence. A recognised QRG request does not additionally produce the general reply. If the required QRG is missing, KST4Contest does not fall back to the general answer.
|
||||
|
||||
### Protection against repeated replies
|
||||
|
||||
Every automatically generated reply contains the fixed prefix:
|
||||
|
||||
```text
|
||||
[KST4C Automsg]
|
||||
```
|
||||
|
||||
The general and QRG-specific functions ignore messages which already contain this prefix. This prevents two clients with automatic replies enabled from answering each other indefinitely.
|
||||
|
||||
A common two-minute cooldown additionally applies to both reply types. The cooldown is tracked separately for each complete callsign and chat category.
|
||||
|
||||
This means:
|
||||
|
||||
- `CALLSIGN-2` and `CALLSIGN-70` have separate cooldowns.
|
||||
- The same complete callsign can still receive an independent reply in another chat category.
|
||||
- A general reply also suppresses a QRG reply to the same callsign in the same category for two minutes.
|
||||
- A QRG reply likewise suppresses the general reply.
|
||||
|
||||
The cooldown starts only after KST4Contest has produced a complete, locally valid message and placed it in the transmit queue. A missing QRG, an empty general reply or a message rejected because of invalid characters does not start the cooldown. Once the missing information has been corrected, a valid reply can therefore be generated immediately.
|
||||
|
||||
> **Note**: The configured text should describe the actual operating status clearly. If the station is only observing the contest and does not accept skeds, say exactly that. A vague automatic message is likely to produce another question – which is precisely the work this function is intended to avoid.
|
||||
|
||||
Changes take effect during the current connection. Use **Save Settings** afterwards to retain the enable settings and general reply text for the next program start.
|
||||
|
||||
Further background: [Automatic Replies to Private Messages](en-Features#automatic-replies-to-private-messages-from-v125).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Win-Test Network Listener (from v1.31)
|
||||
|
||||
A dedicated listener for Win-Test-specific UDP packets. Enables:
|
||||
The Win-Test network listener processes the native Win-Test UDP protocol. It is independent of the general QSO UDP listener on port `12060` and has three separate tasks:
|
||||
|
||||
- **Log synchronisation**: Worked stations are retrieved from Win-Test and marked in the user list.
|
||||
- **Frequency parsing**: The current TRX frequency from Win-Test populates the `MYQRG` variable.
|
||||
- **Sked handover (SKED push)**: Skeds from KST4Contest are passed directly to Win-Test via UDP. Win-Test's default UDP broadcast port (9871) is used.
|
||||
- processing QSOs including band and locator information,
|
||||
- processing STATUS packets for the local QRG, and
|
||||
- handing skeds over to the Win-Test network.
|
||||
|
||||
Settings:
|
||||
- **Enable/Disable**: Checkbox in Preferences (from v1.40).
|
||||
- **Port**: Configurable UDP port for the Win-Test listener.
|
||||
- **Sked UDP address and port**: Target address and port for SKED handover to Win-Test.
|
||||
### Log sync settings
|
||||
|
||||
> **Note**: The Win-Test listener is an **additional** listener – the standard QSO UDP broadcast listener on port 12060 remains independent.
|
||||
| Setting | Function |
|
||||
|---|---|
|
||||
| **Receive Win-Test network based UDP log messages** | Enables the Win-Test network listener. When the listener is enabled, pressing **Create sked** also attempts the Win-Test handover. |
|
||||
| **UDP-Port for Win-Test listener** | Port used by the Win-Test network. The default is `9871`. The same port is used for the sked handover. |
|
||||
| **KST station name in Win-Test network (src of SKED packets)** | Station name used by KST4Contest when sending sked packets. A unique name should be used in a network containing several clients. |
|
||||
| **Win-Test network broadcast address** | Destination address for outgoing Win-Test network packets. In a local network, the address must be reachable by the Win-Test computer. |
|
||||
|
||||
The broadcast address is configurable because `255.255.255.255` is not forwarded reliably through every station network or network interface. In a multi-computer setup, the directed broadcast address belonging to the station network may be required instead.
|
||||
|
||||
### TRX sync settings
|
||||
|
||||
| Setting | Function |
|
||||
|---|---|
|
||||
| **Win-Test STATUS QRG Sync** | Takes the current frequency from Win-Test STATUS packets and uses it as the local QRG. |
|
||||
| **Use pass frequency from Win-Test STATUS** | Uses the transmitted pass frequency instead of the normal TRX QRG. |
|
||||
| **Win-Test station name filter** | Only processes STATUS packets from the specified Win-Test station. An empty field accepts every station name. |
|
||||
|
||||
The station filter is particularly useful when several Win-Test clients are active. Without a filter, the most recently received STATUS packet from another operating position can overwrite the local QRG in KST4Contest.
|
||||
|
||||
### Sked handover
|
||||
|
||||
There is no separate internal sked mode for the Win-Test handover. When the listener is enabled, **Create sked** attempts the Win-Test transfer in addition to creating the internal sked.
|
||||
|
||||
KST4Contest only sends an `ADDSKED` packet when it can determine a QRG which belongs to the explicitly selected band. If no matching QRG is available, the internal sked remains intact and the Win-Test handover is omitted.
|
||||
|
||||
`SSB` or `CW` is selected directly in the Further Info section when the sked is created. No automatic mode inference is used.
|
||||
|
||||
Click **Save Settings** after making changes so that the port, station name, broadcast address and TRX options are restored at the next start.
|
||||
|
||||
Data handling and QRG selection: [Log Synchronisation – Win-Test](en-Log-Sync#win-test)
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator Settings (from v1.31)
|
||||
## PSTRotator Settings (from v1.31)## PSTRotator Settings (from v1.31, fully configurable from v1.40)
|
||||
|
||||
KST4Contest can control antenna direction via PSTRotator.
|
||||
KST4Contest can set an antenna direction through the PSTRotator UDP interface and use the current position reported by PSTRotator as the local QTF.
|
||||
|
||||
Settings:
|
||||
- **Enable/Disable**: Checkbox in Preferences (from v1.40).
|
||||
- **IP address**: IP address of the PSTRotator computer (default: `127.0.0.1` when running on the same PC).
|
||||
- **Port**: Communication port of PSTRotator.
|
||||
The settings are located in the **Station** tab:
|
||||
|
||||
> **Note**: After clicking a direction button, KST4Contest waits briefly for the rotator response. With slow rotors (e.g. SPID) there may be a small delay.
|
||||
| Setting | Default | Purpose |
|
||||
|---|---:|---|
|
||||
| **Enable PSTRotator** | disabled | Starts UDP communication with PSTRotator |
|
||||
| **PSTRotator host** | `127.0.0.1` | Hostname or IP address of the computer running PSTRotator |
|
||||
| **PSTRotator UDP port** | `12000` | UDP port on which PSTRotator receives control commands |
|
||||
|
||||
When both applications run on the same computer, `127.0.0.1` is normally the clearest setting. If PSTRotator runs on another computer in the station network, enter its reachable IP address or DNS name.
|
||||
|
||||
The port must be between `1` and `65534`. Port `65535` cannot be used because PSTRotator reports its position on the following port.
|
||||
|
||||
### Preparing PSTRotator
|
||||
|
||||
Configure the same UDP port under **Communication → UDP Control Port** in PSTRotator and enable **UDP Control**.
|
||||
|
||||
The default configuration uses the following pair:
|
||||
|
||||
| Direction | UDP port |
|
||||
|---|---:|
|
||||
| KST4Contest → PSTRotator | `12000` |
|
||||
| PSTRotator → KST4Contest | `12001` |
|
||||
|
||||
KST4Contest binds the return port automatically. It is not configured separately.
|
||||
|
||||
When the programs run on different computers, the local firewalls and station network must permit UDP traffic in both directions. KST4Contest cannot receive position reports if another program already occupies the return port.
|
||||
|
||||
The complete UDP protocol is documented in the [PSTRotatorAz User Manual](https://www.qsl.net/yo3dmu/ANT/PstRotatorAz%20User%20Manual.pdf).
|
||||
|
||||
### Updating the local QTF
|
||||
|
||||
KST4Contest asks PSTRotator for the current azimuth and operating mode every two seconds. The reported azimuth becomes `actualQTF`.
|
||||
|
||||
While PSTRotator integration is enabled, the QTF field in the main window is therefore read-only. It displays the most recent position reported by PSTRotator.
|
||||
|
||||
This QTF is used by:
|
||||
|
||||
- the direction filter;
|
||||
- the derivation of direction opportunities;
|
||||
- the Priority Score;
|
||||
- the antenna sector on the station map;
|
||||
- the AP and sked timeline; and
|
||||
- the `MYQTF` variable.
|
||||
|
||||
A received rotator position is therefore more than a displayed value. It changes several functions which depend on the current antenna direction.
|
||||
|
||||
The current integration uses azimuth only. Elevation control and complete azimuth/elevation tracking are outside its present scope.
|
||||
|
||||
### Applying changed settings
|
||||
|
||||
The enable setting, host and port are evaluated when the rotator connection is started. After changing them, disconnect and reconnect the ON4KST session or restart KST4Contest.
|
||||
|
||||
Use **Save Settings** afterwards so that the values are restored at the next program start.
|
||||
|
||||
---
|
||||
|
||||
## Sniffer Settings (from v1.31)
|
||||
|
||||
The QSO sniffer filters chat messages from configurable callsigns and forwards them to the PM window.
|
||||
QSO monitoring is intended for stations whose communication should remain visible during busy chat activity. This may be a rare station, a DXpedition or another station in the same contest team whose sked arrangements should not disappear in the general message traffic.
|
||||
|
||||
Settings:
|
||||
- **Callsign list**: Comma-separated list of callsigns whose messages are always forwarded to the PM window.
|
||||
The callsign list is maintained under **QSO monitoring** in the **Notification** tab.
|
||||
|
||||
Use case: Keep track of important stations (e.g. DX expeditions or trusted contest allies) without constantly monitoring the main chat.
|
||||
For every monitored base callsign, KST4Contest additionally shows messages in the PM table when a variant of that callsign is either the sender or the receiver. Both connected chat categories are included.
|
||||
|
||||
The list intentionally uses the normalised base callsign. The following entries therefore produce the same monitoring entry:
|
||||
|
||||
```text
|
||||
DN9APW
|
||||
DN9APW-2
|
||||
DN9APW-70
|
||||
DN9APW-144
|
||||
```
|
||||
|
||||
In every case, KST4Contest stores and displays:
|
||||
|
||||
```text
|
||||
DN9APW
|
||||
```
|
||||
|
||||
The different KST suffixes of one station do not have to be entered separately. A later message sent by `DN9APW-2` or addressed to `DN9APW-70` is covered by the same entry.
|
||||
|
||||
This aggregation applies to QSO monitoring only. Active ChatMember objects, complete message destinations and chat categories remain separate. A message addressed to `DN9APW-70` is therefore not redirected to `DN9APW-2`.
|
||||
|
||||
A monitored message is marked in the PM table using the complete visible callsigns of its sender and receiver:
|
||||
|
||||
```text
|
||||
Sniffed: (DN9APW-2 > DL0ABC-70) Message text
|
||||
```
|
||||
|
||||
The original message remains in its normal table. Monitoring changes neither its contents nor its routing.
|
||||
|
||||
A message is included only when the monitored station is actually its sender or receiver. Merely mentioning the callsign in the message text is not sufficient. Public messages sent by a monitored station are included as well; their receiver is displayed as `ALL`.
|
||||
|
||||
A message which is already addressed directly to the local callsign remains a normal private message and does not receive an additional `Sniffed:` marker.
|
||||
|
||||
Manage the list as follows:
|
||||
|
||||
1. Press **Add monitored callsign** to add an entry.
|
||||
2. Double-click an existing entry to edit it and press `Enter` to apply the change.
|
||||
3. To remove an entry, delete the complete cell contents and press `Enter`.
|
||||
|
||||
The entered value may contain a visible KST suffix or portable components. KST4Contest normalises it to the base callsign before storing it. Different variants of the same base callsign are therefore treated as duplicates.
|
||||
|
||||
Changes to the list take effect immediately. Press **Save Settings** afterwards to retain them. The base callsigns are stored in `preferences.xml` and restored at the next program start.
|
||||
|
||||
> Base-callsign monitoring across KST suffixes is included in Nightly / v1.42.
|
||||
|
||||
Further background and the distinction from message routing: [QSO Sniffer](en-Features#qso-sniffer-from-v131).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -43,6 +43,30 @@ Similar settings:
|
||||
|
||||
---
|
||||
|
||||
## Testing the Connection
|
||||
|
||||
After the logger's DX cluster client has connected, use **Send test spot** to generate the following entry:
|
||||
|
||||
```text
|
||||
Spotted callsign: DO5AMF
|
||||
Comment: Testing DXC-Spot: Congrats, you donated $100!
|
||||
Frequency: .300 on the configured fallback band
|
||||
```
|
||||
|
||||
With `144 MHz` selected as the fallback band, the spot appears at approximately `144.300 MHz`.
|
||||
|
||||
The comment is a deliberately retained Easter egg. It makes the test entry easy to identify but has no other function. In particular, no donation or other external action is triggered.
|
||||
|
||||
Three conditions must be met before running the test:
|
||||
|
||||
1. KST4Contest is connected to the ON4KST chat.
|
||||
2. The local DX cluster server is enabled.
|
||||
3. The logging software's DX cluster client is connected to KST4Contest.
|
||||
|
||||
If no client is connected, KST4Contest displays a corresponding message. A successful test therefore confirms that at least one connected client received the generated spot.
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
A spot is generated when **both** conditions are met:
|
||||
|
||||
@@ -28,7 +28,7 @@ The calculation is based on the following logic:
|
||||
|
||||
The calculation does not include topographic path calculations – this is a deliberate simplification. It may be added in a future version.
|
||||
|
||||
> Configuration: [Configuration – Antenna Beamwidth](Configuration#antenna-beamwidth)
|
||||
> Configuration: [Configuration – Antenna Beamwidth](en-Configuration#antenna-beamwidth)
|
||||
|
||||
---
|
||||
|
||||
@@ -185,6 +185,27 @@ KST4Contest detects such messages that contain your own callsign and automatical
|
||||
|
||||
---
|
||||
|
||||
## Automatic Replies to Private Messages (from v1.25)
|
||||
|
||||
Not every station logged into the ON4KST chat is taking part in the current contest. Sked requests may nevertheless be sent to many visible callsigns without first checking whether they are participating. Without an automatic reply, those stations would have to enter the same refusal repeatedly.
|
||||
|
||||
KST4Contest can answer such private messages with a predefined text. The incoming message remains visible; it is neither blocked nor discarded. A separate QRG reply recognises common requests such as `qrg?`, `freq?` and `pse qrg`.
|
||||
|
||||
When two chat categories are connected, the original context is retained. The reply is addressed to the complete sender callsign and sent through the category in which the request arrived. A QRG request receives only the QRG belonging to that category.
|
||||
|
||||
If the required QRG is missing, KST4Contest sends no incomplete answer. Empty or locally invalid general reply texts are rejected as well.
|
||||
|
||||
Automatic replies need limits. KST4Contest adds the fixed prefix `[KST4C Automsg]`, ignores incoming messages which already contain that prefix and permits only one automatic reply to the same complete callsign in the same category within two minutes. The cooldown is shared by the general and QRG-specific reply functions.
|
||||
|
||||
A rejected reply does not start the cooldown. After entering the missing QRG or correcting the configured text, KST4Contest can therefore answer the next request immediately.
|
||||
|
||||
In plain terms: the function cannot prevent indiscriminate sked requests. It prevents the recipient from having to answer every one of them with the same refusal. It is not intended to simulate a conversation, let alone start an endless discussion with another automatic client.
|
||||
|
||||
Configuration, recognised QRG requests and category handling: [Configuration – Messagehandling Settings](en-Configuration#messagehandling-settings-from-v125).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Multi-Channel Login (from v1.26)
|
||||
|
||||
Simultaneous login to **two chat categories** (e.g. 144 MHz and 432 MHz). Both chats are monitored in parallel.
|
||||
@@ -211,45 +232,195 @@ For selected stations in the user list, there are direct buttons to open the **Q
|
||||
|
||||
---
|
||||
|
||||
## Sked Reminders with ALERT (from v1.40)
|
||||
## Skeds and Sked Reminders
|
||||
|
||||
A sked reminder service with automatic messages can be activated for each chat member. Configurable interval patterns:
|
||||
> Available from v1.40; band, callsign and Win-Test handling extended in Nightly / v1.42.
|
||||
|
||||
- **2+1 minutes**: Messages at 2 min and 1 min before the sked.
|
||||
- **5+2+1 minutes**: Messages at 5, 2 and 1 min before the sked.
|
||||
- **10+5+2+1 minutes**: Messages at 10, 5, 2 and 1 min before the sked.
|
||||
A sked is more than a reminder tied to a particular time. During a contest, it must become visible early enough, move the agreed station up the priority list and – if required – remind the remote station as well.
|
||||
|
||||
In addition to the automated messages to the remote station, there is an **acoustic and visual notification** for your own operator so no sked is ever missed.
|
||||
KST4Contest therefore treats three tasks separately:
|
||||
|
||||
Activate from the FurtherInfo panel of the corresponding station.
|
||||
1. The sked is stored internally and included in the priority calculation.
|
||||
2. The scheduled contact appears in the AP and sked timeline.
|
||||
3. Automatic private reminder messages can optionally be sent before the agreed time.
|
||||
|
||||
When the Win-Test network listener is enabled, KST4Contest also attempts to hand the sked over to Win-Test. A failed handover neither removes nor prevents the internal sked.
|
||||
|
||||
### Creating a sked
|
||||
|
||||
First select the required station in the user list. The sked controls then appear at the bottom of the **Further Info** section.
|
||||
|
||||
| Control | Function |
|
||||
|---|---|
|
||||
| **Sked in** | Sets the number of minutes until the sked. Available values are 2 through 15 and 20 minutes. |
|
||||
| **Band** | Selects the sked band. The dropdown contains the local bands enabled under **Station → my station uses …**. |
|
||||
| **Mode** | Sets the mode passed to Win-Test. Available values are `SSB` and `CW`. This selection does not affect the internal sked or reminder PMs. |
|
||||
| **Create sked** | Creates the internal sked and, if the Win-Test network listener is enabled, also attempts the Win-Test handover. |
|
||||
| **Remind-PM in** | Enables automatic private reminder messages before the sked. |
|
||||
| **2+1**, **5+2+1**, **10+5+2+1** | Selects how many minutes before the sked the reminder PMs are sent. |
|
||||
|
||||

|
||||
|
||||
KST4Contest attempts to preselect a useful band. It checks the following information in this order:
|
||||
|
||||
1. a QRG of the selected station which is no more than 30 minutes old and belongs to a locally enabled band,
|
||||
2. an unambiguous band designator in the station's name field, and
|
||||
3. the first locally enabled band.
|
||||
|
||||
Active callsign variants belonging to the same base callsign are evaluated together when looking for recent band information. A manual NOT-QRV mark is taken into account by the automatic selection. The operator can still select another band explicitly when a different arrangement has been made.
|
||||
|
||||
### Effect on the Priority Score
|
||||
|
||||
A stored sked raises the score of the normalised base callsign:
|
||||
|
||||
| Time relative to the sked | Contribution to the score |
|
||||
|---|---:|
|
||||
| more than 15 minutes before the sked | `+40` |
|
||||
| 15 to 3 minutes before the sked | continuous increase from `+300` towards `+1200` |
|
||||
| less than 3 minutes before until 1 minute after the sked | `+5000` |
|
||||
| more than 1 minute after the sked | no remaining sked boost |
|
||||
|
||||
The strong weighting immediately around the scheduled time is intentional. An agreed sked should not disappear from the priority list merely because another station is currently very active but has no fixed appointment.
|
||||
|
||||
The score is calculated for the base callsign. A sked created for `DN9APW-2` therefore also affects the shared score of other active `DN9APW` variants. The actual message target nevertheless remains `DN9APW-2` in the chat category selected when the sked was created.
|
||||
|
||||
The sked is removed from the internal list five minutes after its scheduled time.
|
||||
|
||||
### Reminder PMs
|
||||
|
||||
Reminder PMs are only scheduled when **Remind-PM in** is enabled. Depending on the selected pattern, KST4Contest sends a private message such as the following two and one minute before the sked:
|
||||
|
||||
```text
|
||||
[KST4C Autoreminder] sked in 2 min
|
||||
```
|
||||
|
||||
The message is sent to the complete visible KST callsign in the chat category in which the sked was created. A sked for `DN9APW-2` is therefore not accidentally sent to `DN9APW`, `DN9APW-70` or a similarly named station in another category.
|
||||
|
||||
When a reminder is actually triggered, KST4Contest also displays the visual **SKED** indication. If simple notification sounds are enabled, a short sound is played as well. Merely arming a reminder does not start the blinking indication.
|
||||
|
||||
Creating a new set of reminders for the same complete callsign replaces the previously scheduled reminders for that callsign.
|
||||
|
||||
### Storage and limitations
|
||||
|
||||
Skeds and reminder schedules are kept in memory only. Any skeds which are still required must be recreated after restarting KST4Contest.
|
||||
|
||||
The automatic band selection is derived from available chat information. It cannot prove that the station is still operating on the most recently mentioned QRG. Check the band, time and mode before pressing **Create sked**.
|
||||
|
||||
Operation: [Station Info Panel](en-User-Interface#station-info-panel-further-info)
|
||||
|
||||
Display: [AP and Sked Timeline](#ap-and-sked-timeline)
|
||||
|
||||
Win-Test handover: [Log Synchronisation – Win-Test](en-Log-Sync#win-test)
|
||||
|
||||
---
|
||||
|
||||
## QSO Sniffer (from v1.31)
|
||||
|
||||
The QSO sniffer monitors the chat for messages from a configurable callsign list and automatically forwards them to the **PM window**. This prevents relevant messages from being lost in the general chat traffic.
|
||||
During a contest, it may be useful to follow the communication of one particular station. This is not limited to rare stations or DXpeditions. Sked arrangements made by another station in the same team can disappear just as easily in a busy public chat.
|
||||
|
||||
KST4Contest can therefore show every message sent or received by a monitored station additionally in the PM table. The message remains in its original table at the same time.
|
||||
|
||||
Monitoring uses the normalised base callsign. An entry for `DN9APW` consequently covers messages sent by or addressed to:
|
||||
|
||||
```text
|
||||
DN9APW-2
|
||||
DN9APW-70
|
||||
DN9APW-144
|
||||
DN9APW-432
|
||||
```
|
||||
|
||||
Entering `DN9APW-70` in the monitoring list still stores `DN9APW`. A station using several band- or operating-position-specific KST suffixes therefore needs only one entry.
|
||||
|
||||
The actual message retains the complete callsigns:
|
||||
|
||||
```text
|
||||
Sniffed: (DN9APW-70 > 9A0BB-23) pse sked 19:30
|
||||
```
|
||||
|
||||
This distinction is intentional. Monitoring aggregates the variants so that their communication remains visible. Message routing does not aggregate them because the intended login would otherwise become ambiguous.
|
||||
|
||||
The base callsign is evaluated across both connected chat categories. The category of each individual message remains unchanged.
|
||||
|
||||
The monitoring view includes:
|
||||
|
||||
- directed messages sent by a monitored station;
|
||||
- directed messages addressed to a monitored station; and
|
||||
- public messages sent by a monitored station to `ALL`.
|
||||
|
||||
A callsign which merely occurs in the message text does not trigger monitoring.
|
||||
|
||||
Messages already addressed directly to the local callsign remain normal private messages and are not additionally marked as `Sniffed:`. QSO monitoring also does not generate a separate notification sound. Sound notifications for private messages actually addressed to the local station remain independent.
|
||||
|
||||
The function does not request additional messages from the ON4KST server. It only provides another view of chat traffic which KST4Contest has already received.
|
||||
|
||||
In plain terms: QSO monitoring does not decide whether a message really contains a sked or matters to the local contest operation. It makes the communication of the selected station easier to find. The operator still decides what to do with it.
|
||||
|
||||
Configuration: [Configuration – Sniffer Settings](en-Configuration#sniffer-settings-from-v131)
|
||||
|
||||
---
|
||||
|
||||
## Win-Test Integration (from v1.31, fully configurable from v1.40)
|
||||
|
||||
KST4Contest fully supports [Win-Test](https://www.win-test.com/) as a logging programme:
|
||||
|
||||
- **Log synchronisation**: Worked stations are automatically retrieved from Win-Test and marked in the user list.
|
||||
- **Frequency parsing**: The current TRX frequency is read from Win-Test UDP packets and populates the `MYQRG` variable.
|
||||
- **Sked handover (SKED push via UDP)**: Agreed skeds from KST4Contest can be pushed directly to Win-Test, so the remote callsign appears in Win-Test's sked window.
|
||||
|
||||
Details: [Configuration – Win-Test Network Listener](en-Configuration#win-test-network-listener)
|
||||
Complete callsigns and chat-category separation: [Multi-Channel Login](#multi-channel-login-from-v126)
|
||||
|
||||
---
|
||||
|
||||
## Win-Test Integration
|
||||
|
||||
KST4Contest uses a dedicated listener for the native Win-Test network protocol. It provides three separate functions:
|
||||
|
||||
- importing new QSOs including band and, where available, locator information,
|
||||
- reading the current QRG from Win-Test STATUS packets, and
|
||||
- handing internally created skeds over to the Win-Test network as `ADDSKED` packets.
|
||||
|
||||
The sked handover does not replace a missing QRG with a fixed default frequency. KST4Contest only sends a Win-Test sked when it can determine a QRG which belongs to the selected band. The internal sked, timeline and reminder PMs continue to work independently.
|
||||
|
||||
A visible KST suffix such as `-2`, `-70` or `-144` is retained inside KST4Contest but removed from the callsign passed to the Win-Test log. Portable components such as `/P`, `/M` and country prefixes are preserved.
|
||||
|
||||
Setup and data handling: [Log Synchronisation – Win-Test](en-Log-Sync#win-test)
|
||||
|
||||
Settings: [Win-Test Network Listener](en-Configuration#win-test-network-listener-from-v131)
|
||||
|
||||
|
||||
## PSTRotator Interface (from v1.31, fully configurable from v1.40)
|
||||
|
||||
KST4Contest can control antenna direction directly via **PSTRotator**. When a station is selected in the user list, the rotator can automatically be turned to the QTF of the selected station.
|
||||
KST4Contest can point the antenna at the selected remote station through the PSTRotator UDP interface. The required azimuth is calculated from the local and remote locators.
|
||||
|
||||
Configuration: [Configuration – PSTRotator Settings](en-Configuration#pstrotator-settings-from-v131)
|
||||
After selecting a station, the **Further Info** section provides the **Turn ant1 to …** button:
|
||||
|
||||

|
||||
|
||||
Pressing the button performs the following steps:
|
||||
|
||||
1. KST4Contest disables PSTRotator tracking mode.
|
||||
2. The QTF of the selected station is transmitted as an integer azimuth.
|
||||
3. PSTRotator controls the configured rotor.
|
||||
4. The reported position becomes the current QTF in KST4Contest.
|
||||
|
||||
The button remains visible while PSTRotator integration is disabled. In that case, no rotator command is sent.
|
||||
|
||||
### Position feedback and SPID compatibility
|
||||
|
||||
KST4Contest requests the current azimuth every two seconds. Position reports update the local QTF field and every function which depends on the antenna direction.
|
||||
|
||||
Some SPID configurations occasionally ignore the first direction command. KST4Contest therefore checks two seconds later whether PSTRotator reported movement or reached the requested target.
|
||||
|
||||
If the position remained unchanged and the target was not reached, KST4Contest sends one compatibility sequence through `0°` followed by the actual target value.
|
||||
|
||||
This check runs in the background. The user interface remains responsive during the two-second interval. A new direction command replaces the pending check belonging to the previous command.
|
||||
|
||||
### What does the reported position confirm?
|
||||
|
||||
The displayed QTF is the azimuth reported by PSTRotator. It confirms that KST4Contest received a usable UDP position message.
|
||||
|
||||
Depending on the station setup, it does not necessarily prove that the antenna is mechanically aligned to exactly that value. This still depends on the controller, calibration, configured offsets and the feedback available to PSTRotator.
|
||||
|
||||
UDP itself provides no delivery acknowledgement. If the displayed QTF does not change, check:
|
||||
|
||||
- that **UDP Control** is enabled in PSTRotator;
|
||||
- that the host and control port match;
|
||||
- that `control port + 1` is available for the position report;
|
||||
- that the firewall permits both UDP directions; and
|
||||
- that PSTRotator itself displays a plausible rotor position.
|
||||
|
||||
In plain terms: KST4Contest provides the target and processes the reported position. The mechanical reality remains the responsibility of the rotor – and occasionally a glance outside.
|
||||
|
||||
Configuration and port assignment: [Configuration – PSTRotator Settings](en-Configuration#pstrotator-settings-from-v131-fully-configurable-from-v140).
|
||||
|
||||
---
|
||||
|
||||
@@ -371,7 +542,7 @@ In practical terms, the score does not replace the operator’s decision. It pre
|
||||
|
||||
Related settings:
|
||||
|
||||
- [Enabled Bands](en-Configuration#enabled-bands)
|
||||
- [Active Bands](en-Configuration#active-bands)
|
||||
- [Antenna Beamwidth](en-Configuration#antenna-beamwidth)
|
||||
- [Default Maximum QRB](en-Configuration#default-maximum-qrb)
|
||||
- [AirScout Settings](en-Configuration#airscout-settings)
|
||||
@@ -379,65 +550,344 @@ Related settings:
|
||||
|
||||
---
|
||||
|
||||
## AP Timeline (from v1.40)
|
||||
## AP and Sked Timeline
|
||||
|
||||
A visual timeline shows up to 4 highly-scored stations per minute slot that should be workable via aircraft scatter. Prioritisation criteria:
|
||||
The timeline combines upcoming aircraft-scatter opportunities and stored skeds for the next 30 minutes. It therefore answers two different questions in the same place:
|
||||
|
||||
- **Highest reflection potential** is preferred (not necessarily the fastest arrival).
|
||||
- Stations towards which your antenna is not pointing are shown **transparently**.
|
||||
- When is an interesting AP opportunity expected?
|
||||
- Which previously agreed sked is approaching independently of that opportunity?
|
||||
|
||||
Events further in the future appear on the right. As time passes, they move left towards the current time.
|
||||
|
||||

|
||||
|
||||
### AP candidates
|
||||
|
||||
AP candidates appear in the upper lanes. Up to four selected candidates can be displayed for each aircraft arrival minute. The selection takes the Priority Score and the reflection potential reported by AirScout into account.
|
||||
|
||||
The colour of an AP marker represents the reflection potential:
|
||||
|
||||
| Colour | Reflection potential |
|
||||
|---|---:|
|
||||
| Magenta | at least 95% |
|
||||
| Red | at least 75% |
|
||||
| Yellow | at least 50% |
|
||||
| Blue | below 50% |
|
||||
|
||||
The colour is not a QSO probability. It represents the AirScout value for the calculated reflection geometry.
|
||||
|
||||
Clicking an AP candidate selects the corresponding active chat member, including its callsign suffix and chat category. A suitable message can then be prepared immediately.
|
||||
|
||||
### Skeds
|
||||
|
||||
Skeds appear as diamonds in the lower lane. Their labels use the complete KST callsign, for example `SKED: DN9APW-2`. This makes it clear which particular login was selected for the scheduled contact.
|
||||
|
||||
A sked tooltip shows at least:
|
||||
|
||||
- the complete KST callsign,
|
||||
- the agreed band, and
|
||||
- the QTF towards the remote station.
|
||||
|
||||
Where suitable AirScout data is available, the tooltip also includes current AP reachability and the next calculated AP opportunity.
|
||||
|
||||
### Antenna direction
|
||||
|
||||
When the QTF of an event is clearly outside the current antenna direction, its marker becomes more transparent. The label remains readable. A target close to the centre of the configured antenna beam is highlighted.
|
||||
|
||||
This visual effect changes neither the sked nor the Priority Score. It is simply a quick way of identifying candidates which fit the current antenna direction.
|
||||
|
||||
The timeline is a preview. AirScout data can change, and a stored sked guarantees neither a clear frequency nor an actual propagation path.
|
||||
|
||||
This gives the contest operator a quick overview of which stations will be reachable via which aircraft and at what time.
|
||||
|
||||
---
|
||||
|
||||
## Interval Beacon
|
||||
|
||||
Automatic CQ messages in the public channel at a configurable interval. Recommended: use the `MYQRG` variable so the current frequency is always accurate. Details: [Configuration – Beacon Settings](Configuration#beacon-settings).
|
||||
KST4Contest can send recurring CQ messages to the public chat. The beacon is intended for longer periods of calling CQ on a fixed frequency: it publishes the local QRG regularly without requiring the operator to enter the same text again.
|
||||
|
||||
Both chat categories use one shared interval, but each category has its own enable setting and message template. The second beacon is only sent while the second chat is enabled and connected.
|
||||
|
||||
Global variables such as `MYQRG`, `SECONDQRG`, `MYLOCATOR` and `MYQTF` are resolved immediately before every transmission. A QRG updated by the logging software can therefore appear in the next beacon.
|
||||
|
||||
Before transmission, KST4Contest validates the completely resolved message. Empty messages, line breaks, the protocol separator `|` and messages exceeding 120 characters are not sent.
|
||||
|
||||
Disable the beacon while searching the band or changing QRG frequently. An automatically published frequency is useful only while somebody is actually listening and calling there.
|
||||
|
||||
Configuration, timer behaviour and available variables: [Configuration – Beacon Settings](en-Configuration#beacon-settings).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Simplelogfile
|
||||
|
||||
File-based log evaluation using regex. Details: [Log Synchronisation](Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||
File-based log evaluation using regex. Details: [Log Synchronisation](en-Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||
---
|
||||
|
||||
## Global Message Views
|
||||
|
||||
Most message tables in KST4Contest are deliberately tied either to the local station or to the station currently selected in the user list. Some message streams must, however, remain visible independently of that selection.
|
||||
|
||||
KST4Contest therefore provides three global message tabs below the main user list:
|
||||
|
||||
| Tab | Content |
|
||||
|---|---|
|
||||
| **Public messages** | Public chat messages, including CQ calls and beacon messages |
|
||||
| **DXCluster messages** | DX cluster messages delivered by the ON4KST server |
|
||||
| **QSO of the other** | Directed chat messages between chat logins other than the local station |
|
||||
|
||||
**Public messages** is selected by default. Changing the selected station does not affect any of these three views.
|
||||
|
||||

|
||||
|
||||
### DXCluster messages
|
||||
|
||||
The DX cluster table shows cluster messages received through the ON4KST connection. Depending on the information contained in the source message, the table displays:
|
||||
|
||||
- the time,
|
||||
- the reporting station and its locator,
|
||||
- the reported station and its locator,
|
||||
- the QRG,
|
||||
- the message text, and
|
||||
- the global Worked state of the reported station.
|
||||
|
||||
An empty locator or another empty field does not necessarily indicate a processing error. The corresponding information may simply be absent from the source message.
|
||||
|
||||
This view must not be confused with the [built-in DX Cluster server](en-DX-Cluster-Server). The built-in server sends derived direction spots to connected logging software. The **DXCluster messages** tab displays cluster traffic received from ON4KST.
|
||||
|
||||
### QSO of the other
|
||||
|
||||
The **QSO of the other** table displays directed chat messages for which neither the sender nor the receiver is the local station. Messages addressed to `ALL` are not included.
|
||||
|
||||
The table contains the following columns:
|
||||
|
||||
| Column | Meaning |
|
||||
|---|---|
|
||||
| **Time** | Time of the chat message |
|
||||
| **Call TX** | Complete callsign of the sender |
|
||||
| **Last QRG TX** | Most recently detected QRG assigned to the sender |
|
||||
| **wkd TX?** | Global Worked state of the sender |
|
||||
| **Call RX** | Complete callsign of the receiver |
|
||||
| **Last QRG RX** | Most recently detected QRG assigned to the receiver |
|
||||
| **wkd RX?** | Global Worked state of the receiver |
|
||||
| **Message** | Message text |
|
||||
| **Category** | Chat category in which the message was received |
|
||||
|
||||
The QRG columns are not a historical record of the frequency used for the displayed message. They show the latest QRG currently known for the respective chat member. The value may originate from another message and may change when a newer QRG is detected.
|
||||
|
||||
The two Worked columns show the global callsign state. They do not indicate whether the station has already been worked on the QRG or band shown next to it.
|
||||
|
||||
The expression “QSO of the other” is used as a compact user-interface label. A directed chat message does not prove that an actual radio QSO has taken place. It may equally be a sked request, a frequency exchange or another private message between two chat logins.
|
||||
|
||||
### Separate monitor window
|
||||
|
||||
The DX cluster and QSO-of-the-other tables are additionally available in a separate monitor window. It places the DX cluster table above the directed messages between other stations.
|
||||
|
||||

|
||||
|
||||
The tabs and the monitor window use the same underlying message stores. Opening the separate window does not create another connection, receive the messages a second time or maintain an independent history.
|
||||
|
||||
The window can be hidden or restored through:
|
||||
|
||||
**Windows → Hide cluster / stranger QSOs**
|
||||
|
||||
or:
|
||||
|
||||
**Windows → Show cluster / stranger QSOs**
|
||||
|
||||
The additional window is useful when these message streams should remain visible on a second monitor or while another part of the main window is being used. During periods with heavy chat traffic, the global tabs are usually more compact.
|
||||
|
||||
When a table cell cannot display its complete message, moving the mouse over the cell shows the full text in a tooltip. Web links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
|
||||
|
||||
---
|
||||
|
||||
## Cluster & QSO of Others
|
||||
## Station Map and Path Analysis (from v1.41)
|
||||
|
||||
A separate window showing the QSO flow between other stations. Particularly interesting during quieter night-time hours of a contest. This window can be minimised when not needed. Future plan: filtering to stations in your selected QTF.
|
||||
The station map shows the geographical relationship between the local station and the chat members which are currently relevant in the main window. It is not a second, independent user list: filters applied to the chat-member table also determine which stations are passed to the map.
|
||||
|
||||

|
||||
|
||||
### Stations and markers
|
||||
|
||||
A station can be displayed only if a usable six-character Maidenhead locator is available. Chat entries without a sufficiently precise locator remain in the user list but cannot be positioned reliably on the map.
|
||||
|
||||
Active chat variants belonging to the same normalised base callsign are combined into one map marker. This avoids several markers being placed at exactly the same position when, for example, a station is logged in with separate suffixes for different bands. The marker information includes the currently derived bands and, where applicable, open `B+` opportunities.
|
||||
|
||||
Marker colours provide a compact status indication:
|
||||
|
||||
| Colour | Meaning |
|
||||
|---|---|
|
||||
| Blue | Normal station marker |
|
||||
| Yellow | The callsign has already been worked on at least one band |
|
||||
| Green | The station is inside the current antenna sector and is relevant as a directional candidate |
|
||||
| Orange | Currently selected station |
|
||||
|
||||
The selected state has the highest display priority, followed by the directional warning and Worked state. A selected station therefore remains orange even if it also meets one of the other conditions.
|
||||
|
||||
At lower zoom levels, nearby markers are combined into screen-based clusters. This is a display function and does not merge the underlying chat members. Selected stations and important directional candidates remain individually visible where possible.
|
||||
|
||||
Clicking a station marker selects the corresponding active chat member in the main window. KST4Contest scrolls to the entry in the user list, updates the **Further Info** panel and prepares the complete visible callsign as the message target. The chat suffix and category therefore remain relevant even though several variants may share one map marker.
|
||||
|
||||
### Antenna sector, connection line and locator grid
|
||||
|
||||
The map displays the local station together with the currently configured antenna direction, beamwidth and maximum QRB. These values form the visible antenna sector.
|
||||
|
||||
Selecting a remote station adds a connection line between both locations. The Maidenhead overlay provides a geographical reference without requiring the operator to translate every locator mentally.
|
||||
|
||||
The map does not know the actual radiation pattern, side lobes or elevation angle of the antenna. The displayed sector is therefore a geometrical representation of the configured horizontal beamwidth, not a complete antenna model.
|
||||
|
||||
### Terrain profile
|
||||
|
||||
For the selected path, KST4Contest requests terrain elevations from the Open-Meteo elevation service. The active provider uses Copernicus GLO-90 data and requests no more than 100 evenly distributed elevation coordinates for one path.
|
||||
|
||||
The terrain resolution and the sampling distance are not the same thing. On a long path, the distance between two requested points can be considerably larger than the nominal resolution of the elevation model. Small terrain features may therefore remain undetected.
|
||||
|
||||
The profile combines:
|
||||
|
||||
- terrain elevation,
|
||||
- the geometrical line between both antennas,
|
||||
- Earth-curvature correction using an effective Earth-radius factor of `k = 4/3`,
|
||||
- the radio and terrain horizons,
|
||||
- the first Fresnel zone,
|
||||
- minimum Fresnel clearance,
|
||||
- detected Fresnel-zone intrusion, and
|
||||
- a rough knife-edge diffraction estimate for relevant obstructions.
|
||||
|
||||
The configured **Own antenna height AGL** is added to the terrain elevation at the local station. For the remote station, KST4Contest currently assumes an antenna height of 10 metres above the local terrain.
|
||||
|
||||
Moving the mouse over the path profile marks the corresponding position on the map. This makes it easier to identify which hill or terrain section causes a reported obstruction.
|
||||
|
||||
### Frequency selection
|
||||
|
||||
Fresnel clearance and link-budget results depend on frequency. KST4Contest therefore attempts to derive a usable analysis frequency from recent QRG or band information associated with the selected station.
|
||||
|
||||
The value displayed as **Frequency** in the analysis panel is the frequency actually used for the calculation. Check it before interpreting the result. A frequency which merely belongs to a possible band is still only an approximation if the station is expected to operate elsewhere.
|
||||
|
||||
This matters particularly on the microwave bands. The Fresnel zone becomes smaller as frequency increases, while free-space path loss and feeder loss increase. A calculation performed for the wrong band may therefore look plausible while describing a different radio path.
|
||||
|
||||
### Link budget and propagation assessment
|
||||
|
||||
The link-budget estimate uses:
|
||||
|
||||
- the configured local and remote transmit powers,
|
||||
- the configured antenna gains,
|
||||
- estimated feeder losses,
|
||||
- free-space path loss, and
|
||||
- a rough additional loss derived from the terrain obstruction.
|
||||
|
||||
Antenna gains must be entered in dBi. Values specified in dBd must first be converted.
|
||||
|
||||
The calculation produces an estimated received power and a bidirectional SSB margin. The result is also made available to the Reachability calculation used by the **Tropo** column and the corresponding filter in the main window.
|
||||
|
||||
The map and the table use the same `ReachabilityService` and calculation cache. A result calculated for the map can therefore also become available to the user list without repeating the complete request.
|
||||
|
||||
KST4Contest deliberately does not request an online terrain profile for every visible chat member whenever the list changes. That would create unnecessary API traffic and make normal chat processing dependent on a large number of external requests. Select the required station on the map or use **Calc selected** when a current calculation is needed.
|
||||
|
||||
### Compact view
|
||||
|
||||
The lower analysis panel can be hidden with **Hide path analysis** and restored with **Show path analysis**. Its visibility is stored in the preferences and restored at the next start.
|
||||
|
||||
The divider between the map and the analysis panel can be moved to allocate more space to either section. Hiding the analysis panel does not discard the selected station or close the map.
|
||||
|
||||
Operation of the map window is described under [Station Map](en-User-Interface#station-map).
|
||||
|
||||
Configuration of antenna height, power and gain is described under [Path Analysis and Link Budget](en-Configuration#path-analysis-and-link-budget).
|
||||
|
||||
### Limits of the result
|
||||
|
||||
The path analysis is an engineering estimate. Among other things, it does not know:
|
||||
|
||||
- the actual antenna height and station setup of the remote operator,
|
||||
- vegetation, buildings and other clutter which is not represented in the elevation data,
|
||||
- the current refractivity profile of the atmosphere,
|
||||
- ducting, scattering or reflection conditions,
|
||||
- local interference or receiver performance, or
|
||||
- whether a detected QRG is still in use.
|
||||
|
||||
The **Mechanisms** indication lists propagation mechanisms which may be consistent with the calculated geometry. It does not prove that one of them is currently available.
|
||||
|
||||
Aircraft Scatter information is not currently coupled to the terrain-profile calculation. AirScout data and the path analysis may both describe the same remote station, but they remain separate assessments.
|
||||
|
||||
OpenStreetMap tiles and the active elevation provider require an Internet connection. Leaflet and the map application itself are bundled locally, and tile requests pass through a local proxy, but this proxy is not a permanent offline map store.
|
||||
|
||||
In plain terms: the analysis helps to identify plausible paths, obvious obstructions and incorrect assumptions. It does not replace propagation experience or a real signal.
|
||||
|
||||
---
|
||||
|
||||
## Bounded Message Stores (from v1.41)
|
||||
|
||||
During a long contest, KST4Contest may receive tens of thousands of chat and DX cluster messages. If these lists were allowed to grow without limit for the complete runtime, memory consumption would not be the only problem. Filtering, sorting and updating the tables built on top of them would also become increasingly expensive.
|
||||
|
||||
KST4Contest therefore uses two separate bounded message stores:
|
||||
|
||||
| Message store | Clean-up starts above | Size after clean-up |
|
||||
|---|---:|---:|
|
||||
| Chat messages | 30,000 entries | 25,000 entries |
|
||||
| DX cluster messages | 10,000 entries | 8,000 entries |
|
||||
|
||||
New messages are inserted at the beginning of the respective list. When the upper limit is exceeded, KST4Contest removes the oldest entries from the end until the specified target size is reached.
|
||||
|
||||
### Why are there two thresholds?
|
||||
|
||||
The store is not reduced to its maximum size again after every single incoming message. After a clean-up, the chat-message store has room for another 5,000 entries and the DX cluster store for another 2,000.
|
||||
|
||||
KST4Contest therefore removes old entries in batches instead of modifying the end of the list again for every subsequent message. The clean-up runs much less frequently as a result.
|
||||
|
||||
### Which tables share a store?
|
||||
|
||||
The following views are filtered representations of the same global chat-message list:
|
||||
|
||||
- **Public messages**,
|
||||
- the private-message table,
|
||||
- the messages in the **Further Info** panel, and
|
||||
- **QSO of the other**.
|
||||
|
||||
These tables do not each retain another 30,000 messages. When an old chat message is removed from the shared store, it disappears from all views based on that store at the same time.
|
||||
|
||||
The **DXCluster messages** tab and the DX cluster table in the separate monitor window likewise use the same cluster-message store. Opening the additional window neither creates a second message connection nor duplicates the received messages.
|
||||
|
||||
The chat and DX cluster stores are independent of each other. Heavy public-chat traffic therefore does not reduce the capacity available for DX cluster messages, and vice versa.
|
||||
|
||||
### No permanent history
|
||||
|
||||
Both message stores exist in memory only. They are written neither to the internal Worked database nor to another local message file.
|
||||
|
||||
After restarting KST4Contest, the tables begin with empty lists and are rebuilt exclusively from newly received messages. These views are working tools for the current session, not a permanent chat archive.
|
||||
|
||||
---
|
||||
|
||||
## Station Map (from v1.41)
|
||||
## Screen-Aware Main Window Sizing (from v1.41)
|
||||
|
||||
An interactive OpenStreetMap-based map showing the geographic position of all active chat members.
|
||||
KST4Contest stores the most recently used size of the main window. This is useful as long as the application is started on a comparable display the next time. If it was previously used on a larger monitor, however, the stored size may extend beyond the visible area of a smaller screen.
|
||||
|
||||
**Features:**
|
||||
KST4Contest therefore checks the stored size against the usable area of the primary screen during startup.
|
||||
|
||||
- Station markers with callsign labels, coloured by activity and sked state
|
||||
- Antenna **beam cone** visualisation for the own station
|
||||
- **Connection line** to the currently selected station
|
||||
- **Maidenhead grid** overlay (QRA locator grid)
|
||||
- **Path profile chart**: Terrain elevation cross-section between own station and the selected station, including Fresnel zone analysis and obstruction/horizon detection
|
||||
- Multiple terrain data sources: **Copernicus GLO-30** (high-resolution DEM), **Open-Meteo API**, synthetic fallback, and **offline DEM import** for air-gapped use
|
||||
- Aircraft scatter path analysis integrated with the terrain data
|
||||
### How is the startup size determined?
|
||||
|
||||
The map works in packaged environments (AppImage, Flatpak) without internet access to external CDNs: map tiles are fetched via a local tile proxy, and the Leaflet.js library is bundled inside the application.
|
||||
If the stored values are valid, KST4Contest initially uses the last saved height and width. If no usable values are available, the following default size is used:
|
||||
|
||||
---
|
||||
- 1,234 pixels wide and
|
||||
- 768 pixels high.
|
||||
|
||||
## Optimised Message Handling / 30,000 Message Limit (from v1.41)
|
||||
KST4Contest does not use the complete screen resolution as the available area. It uses the visual bounds reported by JavaFX for the primary screen. Taskbars, docks and similar operating-system areas are already excluded from these bounds.
|
||||
|
||||
The internal chat and message tables are capped at **30,000 entries**. Older messages are automatically discarded when the limit is reached. This keeps memory usage and rendering performance stable during multi-day contest operations.
|
||||
An additional safety margin of 40 pixels is subtracted. If the stored width or height exceeds the remaining space, only the affected value is reduced.
|
||||
|
||||
---
|
||||
After the user interface has been built with this content size, KST4Contest checks the complete native operating-system window, including its title bar and borders. The window is reduced or moved into the visible area again if necessary.
|
||||
|
||||
## Screen-Aware Window Sizing (from v1.41)
|
||||
This catches two different cases:
|
||||
|
||||
On startup, KST4Contest calculates a screen-aware size for the main window:
|
||||
1. The stored content area is larger than the current screen.
|
||||
2. The content area fits, but the complete native window still extends beyond the visible area because of its borders or position.
|
||||
|
||||
- The stored window size from the previous session is used – but **never larger than the current screen**.
|
||||
- If KST4Contest was last used on a larger monitor, the window is automatically scaled down to fit the current display without clipping.
|
||||
- The UI layout is more **compact and responsive on smaller screens**, showing the same information in less space.
|
||||
### What happens to the layout?
|
||||
|
||||
This prevents unusable oversized windows when switching between machines or monitors.
|
||||
The complete interface is not scaled proportionally. Instead, the main window receives less space and the UI areas designed for this situation react to the available width.
|
||||
|
||||
The filter bar remains compact at normal window sizes. Its controls wrap into additional rows only when their actual required width no longer fits. The dividers can still be used to distribute the available space between the message and station areas.
|
||||
|
||||
### Limits of the automatic correction
|
||||
|
||||
The check always uses the **primary screen**. It does not restore the previous position on a particular secondary monitor.
|
||||
|
||||
The automatic size restriction currently applies to the main window only. The settings window, the separate cluster and QSO monitor window and other auxiliary windows continue to use their stored sizes without the same additional check against the primary screen.
|
||||
|
||||
In plain terms: the protection mainly prevents the central main window from becoming unusable after moving to a smaller display. It is not a complete window-position manager for a changing multi-monitor setup.
|
||||
@@ -50,15 +50,15 @@ Downloads, supported operating systems and installation methods are described in
|
||||
|
||||
## Manual version
|
||||
|
||||
This manual describes the stable release **v1.41.1**.
|
||||
This manual describes the current stable release of KST4Contest.
|
||||
|
||||
Features or changes that are only available in the current development version are explicitly marked as **Nightly / v1.42**. If no such label is present, the description refers to the stable release.
|
||||
Functions that are only available in a Beta or Nightly build are marked accordingly. If no such note is present, the description applies to the stable release.
|
||||
|
||||
- [Download the current stable release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Nightly and automated builds](https://github.com/praktimarc/kst4contest/actions)
|
||||
- [Download Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
|
||||
- [GitHub releases](https://github.com/praktimarc/kst4contest/releases)
|
||||
- [Version history](en-Changelog)
|
||||
|
||||
For contest operation, the stable release is generally the appropriate choice. Nightly builds contain newer fixes and features, but may change between builds. They are useful when a particular change needs testing. Trying one for the first time ten minutes before a contest is less useful.
|
||||
The stable release is normally the appropriate choice for contest operation. Beta and Nightly builds contain newer fixes and functions, but may still change between builds. They are intended for testing specific changes. Ten minutes before a contest is usually not the ideal time for a first test.
|
||||
|
||||
---
|
||||
|
||||
@@ -80,7 +80,7 @@ For contest operation, the stable release is generally the appropriate choice. N
|
||||
|
||||
## Contact and support
|
||||
|
||||
- **Download:** [Current stable release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- **Download:** [Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
|
||||
- **Source code:** [praktimarc/kst4contest](https://github.com/praktimarc/kst4contest)
|
||||
- **Bug reports and feature requests:** [GitHub Issues](https://github.com/praktimarc/kst4contest/issues)
|
||||
- **Email:** praktimarc+kst4contest@gmail.com
|
||||
@@ -112,6 +112,18 @@ Some file and directory names still use the technical name `praktiKST`. They ref
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
KST4Contest has benefited considerably from feedback gathered during actual contest operation.
|
||||
Many functions and corrections in KST4Contest originate from observations made during actual contest operation. Reports that describe not only what happened, but also under which conditions it happened, are particularly useful.
|
||||
|
||||
Special thanks go to Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), Viliam Petrik (OM0AAO, DX Cluster idea), Konrad Neitzel (DC9DJ, project structure), Andreas (DO5ALF, webmaster of funkerportal.de), Franz van Velzen (PE0WGA, testing), and all other testers and contributors.
|
||||
Special thanks go to:
|
||||
|
||||
- Gianluca Costantino (IU3OAR)
|
||||
- Alessandro Murador (IZ3VTH)
|
||||
- Reczetár István (HA1FV)
|
||||
- Viliam Petrik (OM0AAO) for the DX Cluster idea
|
||||
- Konrad Neitzel (DC9DJ) for his work on the project structure
|
||||
- Andreas (DO5ALF), webmaster of funkerportal.de
|
||||
- Franz van Velzen (PE0WGA) for testing
|
||||
- Philipp (DN9APW) for further development of KST4Contest and the CI/CD infrastructure
|
||||
- all other testers and contributors who supplied reproducible reports, ideas and corrections
|
||||
|
||||
Not every suggestion can be implemented unchanged. Nevertheless, reports from real operation remain an important basis for deciding which problems should be solved first.
|
||||
@@ -267,7 +267,19 @@ Apple Silicon Macs require the package marked `arm64`. Intel Macs require the pa
|
||||
3. Drag `KST4Contest.app` into the **Applications** folder.
|
||||
4. Start KST4Contest from the Applications folder or Launchpad.
|
||||
|
||||
The application is not currently notarized by Apple. macOS may therefore block the first launch.
|
||||
From version 1.42 onwards the macOS packages are signed with an Apple Developer ID and notarized by Apple. The first launch therefore works by double-clicking, without going through the context menu and without a security prompt. The notarization ticket is stapled into the DMG file itself, so the check also succeeds without an internet connection.
|
||||
|
||||
To confirm that a downloaded package really is signed, check it in a terminal:
|
||||
|
||||
```bash
|
||||
spctl --assess --type open --context context:primary-signature -v KST4Contest-v<version>-macos-arm64.dmg
|
||||
```
|
||||
|
||||
The expected result is `accepted` together with `source=Notarized Developer ID`.
|
||||
|
||||
### Versions up to and including 1.41.1
|
||||
|
||||
Older packages are not notarized, so macOS blocks the first launch.
|
||||
|
||||
If the application came from the official GitHub Release:
|
||||
|
||||
@@ -408,7 +420,9 @@ Then start the file from a terminal. Error messages shown there are usually more
|
||||
|
||||
### macOS blocks the application
|
||||
|
||||
Use the **Open** function described under [Installing on macOS](#installing-on-macos). Before doing so, verify that the DMG file came from the official GitHub Release.
|
||||
From version 1.42 onwards this should no longer happen, because the packages are signed and notarized. If it does occur, the DMG file was most likely downloaded incompletely or modified afterwards; download it again from the official GitHub Release.
|
||||
|
||||
On older versions the block is expected. Use the **Open** function described under [Installing on macOS](#installing-on-macos).
|
||||
|
||||
### The problem remains
|
||||
|
||||
|
||||
@@ -86,35 +86,77 @@ When broadcasting the complete logbook, DXLog.net uses `contactreplace` instead
|
||||
|
||||
### Win-Test
|
||||
|
||||
Win-Test is supported with a dedicated UDP network listener that understands the native Win-Test network protocol.
|
||||
Win-Test is connected through a dedicated UDP listener for the native Win-Test network protocol. This listener is independent of the general QSO UDP listener on port `12060`.
|
||||
|
||||
For a new QSO, KST4Contest imports the callsign and resolves the native Win-Test band ID. This includes 50 and 70 MHz. If the packet contains a valid locator, the worked grid square is also stored for the detected band.
|
||||
#### QSO and Worked synchronisation
|
||||
|
||||
**Advantages of Win-Test Integration:**
|
||||
- **Per-band Worked data:** New QSOs set the Worked mark for the band reported by Win-Test and update the grid-square status where a locator is available.
|
||||
- Automatic QSO synchronization to mark worked stations.
|
||||
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet* – automatically, as soon as the listener is active. No separate toggle is needed.
|
||||
- You can choose between "AUTO", "SSB", or "CW" sked modes.
|
||||
- **Automatic QRG resolution for SKEDs:** KST4Contest selects the sked frequency intelligently:
|
||||
1. If the other station mentioned their QRG in a recent chat message, that frequency is used.
|
||||
2. Otherwise, your own current QRG is used (from Win-Test STATUS or manual entry).
|
||||
For a new QSO, KST4Contest imports:
|
||||
|
||||
**Settings in the "Log Synchronisation" tab:**
|
||||
- Enable `Receive Win-Test network based UDP log messages`.
|
||||
- `UDP-Port for Win-Test listener` (default: 9871).
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Defines the station name KST4Contest uses in the WT network (e.g. "KST").
|
||||
- `Win-Test network broadcast address`: Usually detected automatically; required to send sked packets to the network.
|
||||
- the logged callsign,
|
||||
- the native Win-Test band ID, and
|
||||
- a valid locator where one is included in the packet.
|
||||
|
||||
**Settings in the "TRX Synchronisation" tab:**
|
||||
- `Win-Test STATUS QRG Sync`: When enabled, KST4Contest takes the current transceiver frequency from the Win-Test STATUS packet and uses it as your own QRG (MYQRG).
|
||||
- `Use pass frequency from Win-Test STATUS`: Instead of the main TRX frequency, the pass frequency contained in the STATUS packet is used as MYQRG (useful for multi-op setups that operate with a dedicated pass QRG).
|
||||
- `Win-Test station name filter`: If a name is entered here (e.g. "STN1"), KST4Contest only processes packets from that specific Win-Test instance. Leave empty to accept all.
|
||||
Band IDs for 50 and 70 MHz are processed in the same way as the VHF, UHF and SHF bands. The callsign is marked as worked globally and on the detected band. If a locator is also available, its four-character grid square is stored for that band.
|
||||
|
||||
The information is written to the same internal database as Worked data received through the other QSO UDP interfaces and is restored after a restart.
|
||||
|
||||
#### Handing skeds over to Win-Test
|
||||
|
||||
Pressing **Create sked** first creates an internal KST4Contest sked. If the Win-Test network listener is enabled, KST4Contest then automatically attempts to send the sked to the Win-Test network as an `ADDSKED` packet.
|
||||
|
||||
The QRG is selected in the following order:
|
||||
|
||||
1. KST4Contest looks for the most recent QRG of the remote station on the explicitly selected band. The QRG must be no more than 30 minutes old. Active variants of the same base callsign are evaluated together.
|
||||
2. If no such QRG is available, KST4Contest checks the local QRG of the chat category in which the sked was created. It is only used if it can be parsed and actually belongs to the selected band.
|
||||
3. If neither source provides a matching QRG, no `ADDSKED` packet is sent.
|
||||
|
||||
A fixed replacement frequency such as `144.300` is deliberately not used. During a contest, a technically successful handover containing the wrong band or QRG is worse than a visibly omitted handover.
|
||||
|
||||
The internal sked remains intact in every case. This also applies when the broadcast address is invalid, the network fails or no Win-Test client can be reached.
|
||||
|
||||
#### Handling KST callsign suffixes
|
||||
|
||||
KST suffixes often identify a particular chat login or band. They are not necessarily part of the log callsign. KST4Contest therefore removes a suffix separated by `-` before handing the callsign over to Win-Test, while preserving portable and international callsign components:
|
||||
|
||||
| Callsign in the KST chat | Callsign passed to Win-Test |
|
||||
|---|---|
|
||||
| `DN9APW-2` | `DN9APW` |
|
||||
| `9A0BB-70` | `9A0BB` |
|
||||
| `EA5/G8MBI/P-70` | `EA5/G8MBI/P` |
|
||||
| `DN9APW-2/P` | `DN9APW/P` |
|
||||
|
||||
The complete callsign remains available inside KST4Contest. The timeline, reminder PMs and chat category continue to refer to the login which was actually selected.
|
||||
|
||||
#### Mode, time and notes
|
||||
|
||||
The mode is selected explicitly as `SSB` or `CW` when the sked is created. It is not inferred automatically from the QRG because a limited list of assumed band segments cannot represent every supported VHF, UHF and SHF band reliably.
|
||||
|
||||
KST4Contest sends the actual scheduled time without adding an extra minute. Where available, the notes include the locator and QTF together with an indication that the sked was created through KST4Contest.
|
||||
|
||||
The handover consists of the Win-Test packets `LOCKSKED`, `ADDSKED` and `UNLOCKSKED`.
|
||||
|
||||

|
||||
|
||||
#### Settings
|
||||
|
||||
In the **Log sync** tab:
|
||||
|
||||
- `Receive Win-Test network based UDP log messages`
|
||||
- `UDP-Port for Win-Test listener`, default `9871`
|
||||
- `KST station name in Win-Test network (src of SKED packets)`
|
||||
- `Win-Test network broadcast address`
|
||||
|
||||
In the **TRX sync** tab:
|
||||
|
||||
- `Win-Test STATUS QRG Sync`
|
||||
- `Use pass frequency from Win-Test STATUS`
|
||||
- `Win-Test station name filter`
|
||||
|
||||
The Win-Test network must be enabled in Win-Test. When several computers are used, the broadcast address must reach the relevant local network. The station name should identify the sending KST4Contest instance unambiguously within the Win-Test network.
|
||||
|
||||
Detailed settings: [Win-Test Network Listener](en-Configuration#win-test-network-listener-from-v131)
|
||||
|
||||
**Settings in Win-Test:**
|
||||
- The network in Win-Test must be active.
|
||||
- Win-Test must be configured to send/receive its broadcasts on the corresponding port (default 9871).
|
||||
|
||||
---
|
||||
|
||||
## TRX Frequency Synchronisation
|
||||
|
||||
|
||||
@@ -140,16 +140,41 @@ Replaced by the current antenna direction in words (e.g. `north`, `north east`,
|
||||
|
||||
## Variables in the Beacon
|
||||
|
||||
All variables can also be used in the **automatic beacon** (interval messages). Recommended beacon configuration:
|
||||
A public beacon has no selected remote station. It can therefore make meaningful use only of variables which depend on the local station and its current configuration:
|
||||
|
||||
```
|
||||
calling cq at MYQRG, loc MYLOCATOR, GL all!
|
||||
| Variable | Value used in the beacon |
|
||||
|---|---|
|
||||
| `MYQRG` | current QRG of the primary chat category |
|
||||
| `MYQRGSHORT` | first seven characters of the primary QRG |
|
||||
| `SECONDQRG` | current QRG of the second chat category |
|
||||
| `MYLOCATOR` | complete configured locator of the local station |
|
||||
| `MYLOCATORSHORT` | four-character locator of the local station |
|
||||
| `MYCALL` | configured local callsign |
|
||||
| `MYQTF` | current antenna heading |
|
||||
|
||||
`QRZNAME`, `FIRSTAP` and `SECONDAP` require a selected remote station. They are therefore not resolved in a public beacon.
|
||||
|
||||
A suitable configuration for the primary category is:
|
||||
|
||||
```text
|
||||
calling cq at MYQRGSHORT, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Since KST4Contest automatically reads QRG data from chat messages: if other stations also use KST4Contest, they will immediately see your QRG in the QRG column of their user list.
|
||||
For the second category, use `SECONDQRG` if that category should publish a different frequency:
|
||||
|
||||
```text
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Global variables are evaluated again on every timer run. A QRG updated by the logging software can therefore appear in the next beacon message.
|
||||
|
||||
The completely resolved text must contain at least one valid character and must not exceed 120 characters. The protocol separator `|` and line breaks are not permitted. If the text is still empty or invalid when transmission is due, that beacon run is skipped.
|
||||
|
||||
The common interval and the behaviour of both chat categories are described under [Configuration – Beacon Settings](en-Configuration#beacon-settings).
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Example Contest Workflow with Macros
|
||||
|
||||
1. Select a station in the user list → callsign is now pre-selected.
|
||||
|
||||
@@ -111,7 +111,24 @@ The current **Priority score** of the selected station is displayed in the same
|
||||
|
||||
**Sked fail** marks an unsuccessful attempt and strongly reduces the score of the normalised base callsign. **Reset fail** removes the mark. It applies to all active suffix and category variants of the station and remains active for the current program session.
|
||||
|
||||
A sked and the corresponding **sked reminders** can be created underneath these controls. An approaching sked raises the Priority Score over time and receives very high priority immediately before the scheduled contact.
|
||||
The controls underneath are used to create a sked:
|
||||
|
||||
| Control | Meaning |
|
||||
|---|---|
|
||||
| **Sked in** | Time remaining until the sked |
|
||||
| **Band** | Agreed band selected from the locally enabled bands |
|
||||
| **Mode** | `SSB` or `CW` for a possible Win-Test handover |
|
||||
| **Create sked** | Create the internal sked |
|
||||
| **Remind-PM in** | Enable automatic reminder PMs |
|
||||
| **2+1**, **5+2+1**, **10+5+2+1** | Times at which reminder PMs are sent before the sked |
|
||||
|
||||

|
||||
|
||||
The proposed band is derived from recent QRG and name information for the station. It can be changed explicitly before creating the sked. The mode selection only affects the Win-Test handover; the internal sked and reminder PMs work independently.
|
||||
|
||||
**Create sked** always creates the appointment inside KST4Contest first. If the Win-Test network listener is active, KST4Contest then attempts an additional handover to Win-Test. If no QRG matching the selected band can be found or Win-Test cannot be reached, the internal sked, its priority contribution and any scheduled reminders remain intact.
|
||||
|
||||
The complete derivation and limitations are described under [Skeds and Sked Reminders](en-Features#skeds-and-sked-reminders).
|
||||
|
||||
---
|
||||
|
||||
@@ -139,24 +156,115 @@ Calculation and limitations: [Priority Score and Priority List](en-Features#prio
|
||||
|
||||
---
|
||||
|
||||
## Cluster & QSO of Others
|
||||
## Station Map
|
||||
|
||||
Separate window (can be minimised). Shows the communication flow between other stations – interesting during quieter contest periods.
|
||||
The station map is opened or closed through:
|
||||
|
||||
**Windows → Show / hide station map**
|
||||
|
||||
The window uses the chat members currently visible in the filtered user list. Changing the QRB, QTF, Worked, band or Reachability filters can therefore also change the stations shown on the map.
|
||||
|
||||
A station can additionally be opened directly from the **Further Info** panel using **Show on map**. This selects the station on the map and requests the associated path analysis.
|
||||
|
||||
Stations with the same normalised base callsign and position are combined into one marker. At lower zoom levels, nearby markers may additionally be displayed as clusters. These are display groups only; the individual chat logins remain separate message targets inside KST4Contest.
|
||||
|
||||
Clicking a station marker:
|
||||
|
||||
1. selects the corresponding chat member,
|
||||
2. scrolls the main user list to that entry,
|
||||
3. updates the **Further Info** panel, and
|
||||
4. prepares the complete visible callsign as the message target.
|
||||
|
||||
The map details for the selected station include its locator, QRB, QTF, detected bands and available band opportunities. **Trigger cluster spot** sends a spot through the built-in local DX Cluster server so that connected logging software can receive the selected station and QRG.
|
||||
|
||||
The path-analysis section shows the terrain profile and the calculated route between both stations. Depending on the available data, it includes:
|
||||
|
||||
- the analysis frequency,
|
||||
- line-of-sight and horizon information,
|
||||
- Fresnel-zone clearance,
|
||||
- detected obstructions,
|
||||
- an estimated link budget,
|
||||
- received power and SSB margin, and
|
||||
- a short assessment of the path.
|
||||
|
||||
Moving the mouse over the terrain profile highlights the corresponding geographical position on the map.
|
||||
|
||||
The analysis can be hidden using **Hide path analysis** when more space is required for the map. The compact state displays **Path analysis is hidden.** together with the **Show path analysis** button.
|
||||
|
||||

|
||||
|
||||
The selected station and map contents remain available while the analysis panel is hidden. The setting is stored and restored at the next start.
|
||||
|
||||
Calculation method and limitations: [Station Map and Path Analysis](en-Features#station-map-and-path-analysis-from-v141).
|
||||
|
||||
---
|
||||
|
||||
## Global Message Tabs and Monitor Window
|
||||
|
||||
Three global message tabs are located below the main user list. Unlike the **Further Info** panel, their contents do not depend on the station currently selected.
|
||||
|
||||
| Tab | Displayed messages |
|
||||
|---|---|
|
||||
| **Public messages** | All public chat messages, including CQ calls and beacons |
|
||||
| **DXCluster messages** | DX cluster messages received from the ON4KST server |
|
||||
| **QSO of the other** | Directed messages between chat logins other than the local station |
|
||||
|
||||
The **Public messages** tab is selected by default.
|
||||
|
||||

|
||||
|
||||
The **DXCluster messages** table contains the time, reporting and reported stations, locators, QRG, message text and global Worked state where these values are available in the received message.
|
||||
|
||||
The **QSO of the other** table contains:
|
||||
|
||||
- the complete sender and receiver callsigns,
|
||||
- the latest QRG currently known for each station,
|
||||
- the global Worked state of each station,
|
||||
- the message text, and
|
||||
- the chat category.
|
||||
|
||||
The displayed QRG is not necessarily the frequency on which the stations intend to make a contact. It is the latest QRG currently associated with the respective chat member. The Worked state is global and not specific to the displayed QRG or band.
|
||||
|
||||
A directed chat message in this table does not prove that a radio QSO has taken place. The table also contains sked requests, frequency exchanges and other directed messages between third-party chat logins.
|
||||
|
||||
### Separate monitor window
|
||||
|
||||
The DX cluster and QSO-of-the-other tables can also be displayed together in a separate window.
|
||||
|
||||

|
||||
|
||||
The separate window and the tabs use the same underlying messages. Hiding the window does not stop message processing or remove messages from the tabs.
|
||||
|
||||
Use **Windows → Hide cluster / stranger QSOs** to hide the window and **Windows → Show cluster / stranger QSOs** to restore it.
|
||||
|
||||
If a message is too long for its table cell, moving the mouse over the cell displays the complete text in a tooltip. Links beginning with `http://`, `https://` or `www.` can be opened in the system browser.
|
||||
|
||||
---
|
||||
|
||||
## Menu
|
||||
|
||||
### Window
|
||||
- **Use Dark Mode** (from v1.26): Toggle dark colour scheme on/off.
|
||||
### Windows
|
||||
|
||||
- **Hide cluster / stranger QSOs** hides the separate monitor window for DX cluster messages and directed messages between other stations.
|
||||
- **Show cluster / stranger QSOs** restores the monitor window.
|
||||
- **hide options** hides the settings window.
|
||||
- **show options** restores the settings window.
|
||||
- **Use dark mode design** activates the dark colour scheme.
|
||||
- **Use default mode design** restores the default colour scheme.
|
||||
- **Show / hide station map** opens or closes the separate station-map and path-analysis window.
|
||||
|
||||
---
|
||||
|
||||
## Window Sizes and Dividers
|
||||
|
||||
From **v1.21**, clicking **"Save Settings"** also saves window sizes and divider positions of all panels in the configuration file, which are restored on the next start.
|
||||
When **Save Settings** is clicked, KST4Contest stores the programme-window sizes and the positions of the relevant dividers in the configuration file. These values are reused at the next start.
|
||||
|
||||
The main window is additionally checked against the visible area of the primary screen during startup. If the stored size is too large, KST4Contest reduces and moves the window so that it remains accessible. The complete process is described under [Screen-Aware Main Window Sizing](en-Features#screen-aware-main-window-sizing-from-v141).
|
||||
|
||||
The other programme windows do not currently use this additional size restriction. If, for example, the separate monitor window appears too large after moving to a smaller screen, its size must be corrected manually and stored again using **Save Settings**.
|
||||
|
||||
If the layout has become inconvenient, first move the dividers back to usable positions and save the settings again. Deleting the configuration file also resets the UI values, but it removes the other stored programme settings as well. It should therefore be used only when the interface cannot be restored in another way.
|
||||
|
||||
If you encounter display problems: delete the configuration file → KST4Contest creates new default values.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 233 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 4.1 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Derives the jpackage --add-modules list from module-info.java so that the
|
||||
* packaging scripts never drift from the module descriptor again.
|
||||
*
|
||||
* Run as a single file source program, which behaves identically on the Linux,
|
||||
* macOS and Windows runners:
|
||||
*
|
||||
* java packaging/AddModules.java print the module list
|
||||
* java packaging/AddModules.java --verify-pom fail if pom.xml drifted
|
||||
*
|
||||
* Only platform modules are emitted. Third party requires such as jlayer are
|
||||
* skipped because they are supplied as ordinary jars on the class path, and
|
||||
* automatic modules cannot be linked into a runtime image at all. Test only
|
||||
* requires such as org.junit.jupiter.api are skipped for the same reason.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class AddModules {
|
||||
|
||||
private static final Path DESCRIPTOR =
|
||||
Path.of("src", "main", "java", "module-info.java");
|
||||
|
||||
private static final Path POM = Path.of("pom.xml");
|
||||
|
||||
/** Matches "requires [transitive] [static] some.module;" in any order. */
|
||||
private static final Pattern REQUIRES = Pattern.compile(
|
||||
"requires\\s+((?:transitive\\s+|static\\s+)*)([A-Za-z0-9_.]+)\\s*;");
|
||||
|
||||
private static final Pattern ADD_MODULE =
|
||||
Pattern.compile("<addmodule>\\s*([A-Za-z0-9_.]+)\\s*</addmodule>");
|
||||
|
||||
private static final Pattern BLOCK_COMMENT =
|
||||
Pattern.compile("/\\*.*?\\*/", Pattern.DOTALL);
|
||||
|
||||
private static final Pattern LINE_COMMENT = Pattern.compile("//[^\\n]*");
|
||||
|
||||
private AddModules() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
boolean verifyPom = args.length > 0 && "--verify-pom".equals(args[0]);
|
||||
|
||||
Set<String> required = platformModules(read(DESCRIPTOR));
|
||||
if (required.isEmpty()) {
|
||||
fail("No platform modules found in " + DESCRIPTOR);
|
||||
}
|
||||
|
||||
if (!verifyPom) {
|
||||
System.out.println(String.join(",", required));
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> declared = new TreeSet<>();
|
||||
Matcher matcher = ADD_MODULE.matcher(read(POM));
|
||||
while (matcher.find()) {
|
||||
declared.add(matcher.group(1));
|
||||
}
|
||||
|
||||
if (declared.equals(required)) {
|
||||
System.out.println("pom.xml <addmodules> matches module-info.java ("
|
||||
+ required.size() + " modules)");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> missing = new TreeSet<>(required);
|
||||
missing.removeAll(declared);
|
||||
Set<String> extra = new TreeSet<>(declared);
|
||||
extra.removeAll(required);
|
||||
|
||||
System.err.println("pom.xml <addmodules> drifted from module-info.java.");
|
||||
if (!missing.isEmpty()) {
|
||||
System.err.println(" missing in pom.xml: " + String.join(", ", missing));
|
||||
}
|
||||
if (!extra.isEmpty()) {
|
||||
System.err.println(" not required by module-info.java: "
|
||||
+ String.join(", ", extra));
|
||||
}
|
||||
System.err.println(" expected: " + String.join(",", required));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
/** Returns the platform modules required by the given descriptor, sorted. */
|
||||
static Set<String> platformModules(String source) {
|
||||
String stripped = LINE_COMMENT.matcher(
|
||||
BLOCK_COMMENT.matcher(source).replaceAll(" ")).replaceAll(" ");
|
||||
|
||||
Set<String> modules = new TreeSet<>();
|
||||
Matcher matcher = REQUIRES.matcher(stripped);
|
||||
while (matcher.find()) {
|
||||
// "requires static" is a compile time only dependency and must not
|
||||
// be linked into the shipped runtime image.
|
||||
if (matcher.group(1).contains("static")) {
|
||||
continue;
|
||||
}
|
||||
String module = matcher.group(2);
|
||||
if (isPlatformModule(module)) {
|
||||
modules.add(module);
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
private static boolean isPlatformModule(String module) {
|
||||
return module.startsWith("java.")
|
||||
|| module.startsWith("jdk.")
|
||||
|| module.startsWith("javafx.");
|
||||
}
|
||||
|
||||
private static String read(Path path) throws IOException {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
fail("Not found: " + path.toAbsolutePath()
|
||||
+ " (run this from the repository root)");
|
||||
}
|
||||
return Files.readString(path);
|
||||
}
|
||||
|
||||
private static void fail(String message) {
|
||||
System.err.println(message);
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
@@ -33,14 +33,16 @@ build() {
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pkgbase = kst4contest
|
||||
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation
|
||||
pkgver = 1.41.1
|
||||
pkgrel = 1
|
||||
pkgrel = 2
|
||||
url = https://github.com/praktimarc/kst4contest
|
||||
arch = x86_64
|
||||
license = GPL-3.0-only
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
|
||||
pkgname=kst4contest
|
||||
pkgver=1.41.1
|
||||
pkgrel=1
|
||||
pkgrel=2
|
||||
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
@@ -26,14 +26,30 @@ build() {
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
mkdir -p dist
|
||||
# This PKGBUILD builds from a released source tarball, which may predate
|
||||
# packaging/AddModules.java. Older tarballs carry the same list in pom.xml,
|
||||
# which the build keeps in sync with module-info.java from v1.42.0 onwards.
|
||||
if [ -f packaging/AddModules.java ]; then
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
else
|
||||
ADD_MODULES="$(sed -n 's:.*<addmodule>\(.*\)</addmodule>.*:\1:p' pom.xml | paste -sd,)"
|
||||
fi
|
||||
# Same story for the packaging icon: without --icon jpackage silently ships
|
||||
# its own Duke placeholder, but tarballs older than v1.42.0 have no icon to
|
||||
# point at, so only pass the flag when the file is actually there.
|
||||
ICON_ARGS=()
|
||||
if [ -f packaging/icons/kst4contest.png ]; then
|
||||
ICON_ARGS=(--icon packaging/icons/kst4contest.png)
|
||||
fi
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
"${ICON_ARGS[@]}" \
|
||||
--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,java.net.http,jdk.crypto.ec \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180" width="180" height="180"><rect x="0" y="0" width="180" height="180" rx="40" fill="#5fd63a"></rect><g transform="translate(40,40) scale(1)">
|
||||
<path d="M20 34 h60 a12 12 0 0 1 12 12 v20 a12 12 0 0 1 -12 12 h-30 l-16 12 v-12 h-14 a12 12 0 0 1 -12 -12 v-20 a12 12 0 0 1 12 -12 z" fill="#0a110a"></path>
|
||||
<text x="50" y="65" text-anchor="middle" font-family="'Space Grotesk','Helvetica Neue',Arial,sans-serif" font-weight="700" font-size="24" letter-spacing="0.5" fill="#5fd63a">KST</text>
|
||||
<line x1="50" y1="34" x2="50" y2="17" stroke="#0a110a" stroke-width="6" stroke-linecap="round"></line>
|
||||
<circle cx="50" cy="13" r="4.5" fill="#0a110a"></circle>
|
||||
<path d="M58 6 a9 9 0 0 1 0 14" fill="none" stroke="#0a110a" stroke-width="4" stroke-linecap="round"></path>
|
||||
<path d="M64 1 a15 15 0 0 1 0 24" fill="none" stroke="#0a110a" stroke-width="4" stroke-linecap="round"></path>
|
||||
<path d="M42 6 a9 9 0 0 0 0 14" fill="none" stroke="#0a110a" stroke-width="4" stroke-linecap="round"></path>
|
||||
<path d="M36 1 a15 15 0 0 0 0 24" fill="none" stroke="#0a110a" stroke-width="4" stroke-linecap="round"></path></g></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Local signed (and optionally notarized) macOS build.
|
||||
#
|
||||
# jpackage cannot sign the app itself: it ad-hoc signs the embedded runtime and
|
||||
# then re-runs codesign on the same files without --force, which codesign
|
||||
# rejects with "is already signed". So this builds an unsigned app-image, signs
|
||||
# it from the inside out ourselves, and only then wraps it into a DMG.
|
||||
#
|
||||
# Required:
|
||||
# SIGNING_IDENTITY The name part of the Developer ID Application certificate,
|
||||
# without the "Developer ID Application: " prefix. Example:
|
||||
# SIGNING_IDENTITY="Philipp Wagner (ABCDE12345)"
|
||||
# List available ones with:
|
||||
# security find-identity -v -p codesigning
|
||||
#
|
||||
# Optional:
|
||||
# Notarization, either as three separate values (what CI uses)...
|
||||
# NOTARY_KEY Path to the App Store Connect .p8 private key
|
||||
# NOTARY_KEY_ID The key's ID, also part of the .p8 filename
|
||||
# NOTARY_ISSUER The issuer UUID, shown above the key list in the portal
|
||||
# ...or as a keychain profile previously created with
|
||||
# NOTARY_PROFILE xcrun notarytool store-credentials <name>
|
||||
#
|
||||
# With neither, the build is signed but not notarized -- enough to test
|
||||
# locally, not enough to distribute.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/../.."
|
||||
REPO_ROOT="$PWD"
|
||||
|
||||
BUNDLE_ID="de.x08.KST4Contest"
|
||||
ENTITLEMENTS="packaging/macos/kst4contest.entitlements"
|
||||
|
||||
if [ -z "${SIGNING_IDENTITY:-}" ]; then
|
||||
echo "SIGNING_IDENTITY is not set. Available signing identities:" >&2
|
||||
security find-identity -v -p codesigning >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
FULL_IDENTITY="Developer ID Application: $SIGNING_IDENTITY"
|
||||
|
||||
# notarytool takes either an API key triple or a stored keychain profile. The
|
||||
# triple needs no keychain at all, which is why CI uses it.
|
||||
NOTARY_ARGS=()
|
||||
if [ -n "${NOTARY_KEY:-}" ] && [ -n "${NOTARY_KEY_ID:-}" ] && [ -n "${NOTARY_ISSUER:-}" ]; then
|
||||
NOTARY_ARGS=(--key "$NOTARY_KEY" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER")
|
||||
elif [ -n "${NOTARY_PROFILE:-}" ]; then
|
||||
NOTARY_ARGS=(--keychain-profile "$NOTARY_PROFILE")
|
||||
fi
|
||||
|
||||
echo "==> Building JAR and collecting runtime dependencies"
|
||||
chmod +x mvnw
|
||||
./mvnw -B -DskipTests package \
|
||||
dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
JAR="$(ls -t target/praktiKST-*.jar | head -n 1)"
|
||||
cp "$JAR" target/dist-libs/app.jar
|
||||
|
||||
# jpackage only accepts a numeric major[.minor[.patch]] as the macOS bundle
|
||||
# version, so a Maven qualifier like "-nightly" has to be trimmed off.
|
||||
POM_VERSION="${JAR##*/praktiKST-}"
|
||||
POM_VERSION="${POM_VERSION%.jar}"
|
||||
APP_VERSION="$(printf '%s' "$POM_VERSION" | sed -e 's/[^0-9.].*$//' -e 's/\.*$//')"
|
||||
[ -n "$APP_VERSION" ] || { echo "Could not derive app version from $JAR" >&2; exit 1; }
|
||||
echo "==> Version: $POM_VERSION -> bundle version $APP_VERSION"
|
||||
|
||||
echo "==> Step 1/4: jpackage app-image (unsigned)"
|
||||
rm -rf dist
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
|
||||
MACOSX_DEPLOYMENT_TARGET="13.0" jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--app-version "$APP_VERSION" \
|
||||
--icon packaging/icons/kst4contest.icns \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--mac-package-identifier "$BUNDLE_ID" \
|
||||
--mac-package-name KST4Contest \
|
||||
--dest dist/appimage
|
||||
|
||||
APP="dist/appimage/KST4Contest.app"
|
||||
[ -d "$APP" ] || { echo "jpackage produced no app image" >&2; exit 1; }
|
||||
|
||||
echo "==> Step 2/4: signing bundle contents (this takes a few minutes)"
|
||||
|
||||
# Apple's notary service unpacks JARs and checks the native libraries inside
|
||||
# them. sqlite-jdbc ships libsqlitejdbc.dylib for both architectures that way,
|
||||
# and an unsigned binary in there fails the whole submission. So sign those
|
||||
# first: the app bundle's seal covers Contents/app, and rewriting a JAR
|
||||
# afterwards would invalidate it.
|
||||
echo " scanning jars for native libraries"
|
||||
find "$APP/Contents/app" -name '*.jar' -type f | while read -r JARPATH; do
|
||||
# Only unpack jars that can plausibly hold a native library. Note the
|
||||
# plain grep: "grep -q" exits at the first match, which hands unzip a
|
||||
# SIGPIPE, and under "set -o pipefail" that failure becomes the pipeline's
|
||||
# status -- inverting this very test.
|
||||
if ! unzip -l "$JARPATH" | grep -E '\.(dylib|jnilib|so)$' >/dev/null; then
|
||||
continue
|
||||
fi
|
||||
|
||||
JARABS="$(cd "$(dirname "$JARPATH")" && pwd)/$(basename "$JARPATH")"
|
||||
JARTMP="$(mktemp -d)"
|
||||
unzip -q "$JARABS" -d "$JARTMP"
|
||||
|
||||
NATIVES="$(mktemp)"
|
||||
( cd "$JARTMP" && find . -type f \( -name '*.dylib' -o -name '*.jnilib' -o -name '*.so' \) \
|
||||
| while read -r n; do
|
||||
if [ "$(file --mime-type -b "$n")" = "application/x-mach-binary" ]; then
|
||||
printf '%s\n' "${n#./}"
|
||||
fi
|
||||
done ) > "$NATIVES"
|
||||
|
||||
if [ -s "$NATIVES" ]; then
|
||||
echo " $(basename "$JARPATH"): $(wc -l < "$NATIVES" | tr -d ' ') native lib(s)"
|
||||
( cd "$JARTMP" && xargs -I {} codesign --force --timestamp --options runtime \
|
||||
--sign "$FULL_IDENTITY" {} < "$NATIVES" )
|
||||
# Update in place rather than repacking, so the rest of the jar --
|
||||
# manifest, module descriptor, entry order -- stays byte for byte.
|
||||
( cd "$JARTMP" && xargs jar --update --file "$JARABS" < "$NATIVES" )
|
||||
fi
|
||||
|
||||
rm -rf "$JARTMP" "$NATIVES"
|
||||
done
|
||||
|
||||
# Every Mach-O file has to carry its own signature before the enclosing bundle
|
||||
# can be sealed, so collect them first. jpackage leaves them ad-hoc signed,
|
||||
# hence --force on every call.
|
||||
# file(1) pads its output into columns when given several arguments at once,
|
||||
# so ask it one file at a time with -b and get an unambiguous answer.
|
||||
MACHO_LIST="$(mktemp)"
|
||||
find "$APP" -type f -print0 | while IFS= read -r -d '' f; do
|
||||
case "$(file --mime-type -b "$f")" in
|
||||
application/x-mach-binary) printf '%s\n' "$f" ;;
|
||||
esac
|
||||
done > "$MACHO_LIST"
|
||||
|
||||
COUNT="$(wc -l < "$MACHO_LIST" | tr -d ' ')"
|
||||
echo " $COUNT Mach-O files to sign"
|
||||
|
||||
# Serially, deliberately. Running codesign concurrently over several files of
|
||||
# the same bundle fails intermittently -- a CI run died with "replacing existing
|
||||
# signature" immediately followed by "No such file or directory" for that same
|
||||
# path, while the identical script passed locally. Each call contacts Apple's
|
||||
# timestamp server, so this costs about a minute for a runtime this size.
|
||||
xargs -I {} codesign --force --timestamp --options runtime \
|
||||
--sign "$FULL_IDENTITY" {} < "$MACHO_LIST"
|
||||
rm -f "$MACHO_LIST"
|
||||
|
||||
# The embedded JDK is a bundle in its own right and must be sealed before the
|
||||
# app that contains it.
|
||||
echo " sealing embedded runtime"
|
||||
codesign --force --timestamp --options runtime \
|
||||
--sign "$FULL_IDENTITY" "$APP/Contents/runtime"
|
||||
|
||||
# Entitlements go on the outermost bundle: the hardened runtime derives the
|
||||
# process's entitlements from the main executable's signature.
|
||||
echo " sealing app bundle"
|
||||
codesign --force --timestamp --options runtime \
|
||||
--entitlements "$ENTITLEMENTS" \
|
||||
--sign "$FULL_IDENTITY" "$APP"
|
||||
|
||||
# Apple rejects the whole submission over a single unsigned native library, and
|
||||
# a round trip to the notary service costs minutes. Check its two criteria --
|
||||
# a Developer ID authority and a secure timestamp -- locally first.
|
||||
echo " preflight: verifying native libraries inside jars"
|
||||
PREFLIGHT_ERRORS="$(mktemp)"
|
||||
find "$APP/Contents/app" -name '*.jar' -type f | while read -r JARPATH; do
|
||||
if ! unzip -l "$JARPATH" | grep -E '\.(dylib|jnilib|so)$' >/dev/null; then
|
||||
continue
|
||||
fi
|
||||
CHECKTMP="$(mktemp -d)"
|
||||
unzip -q "$JARPATH" -d "$CHECKTMP"
|
||||
find "$CHECKTMP" -type f \( -name '*.dylib' -o -name '*.jnilib' -o -name '*.so' \) \
|
||||
| while read -r NATIVE; do
|
||||
[ "$(file --mime-type -b "$NATIVE")" = "application/x-mach-binary" ] || continue
|
||||
INFO="$(codesign -dv --verbose=2 "$NATIVE" 2>&1 || true)"
|
||||
LABEL="$(basename "$JARPATH")/${NATIVE#"$CHECKTMP"/}"
|
||||
printf '%s' "$INFO" | grep -q "Authority=Developer ID Application" \
|
||||
|| echo "$LABEL: not signed with a Developer ID certificate" >> "$PREFLIGHT_ERRORS"
|
||||
printf '%s' "$INFO" | grep -q "Timestamp=" \
|
||||
|| echo "$LABEL: signature has no secure timestamp" >> "$PREFLIGHT_ERRORS"
|
||||
done
|
||||
rm -rf "$CHECKTMP"
|
||||
done
|
||||
if [ -s "$PREFLIGHT_ERRORS" ]; then
|
||||
echo "ERROR: these would fail notarization:" >&2
|
||||
sed 's/^/ /' "$PREFLIGHT_ERRORS" >&2
|
||||
rm -f "$PREFLIGHT_ERRORS"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$PREFLIGHT_ERRORS"
|
||||
echo " preflight ok"
|
||||
|
||||
echo "==> Step 3/4: building the dmg"
|
||||
|
||||
# Not with jpackage: "jpackage --type dmg --app-image" re-signs the app it is
|
||||
# handed, replacing our Developer ID signature with an ad-hoc one and dropping
|
||||
# the hardened runtime flag. hdiutil copies the bundle verbatim instead.
|
||||
DMG="dist/KST4Contest-${APP_VERSION}.dmg"
|
||||
STAGE="$(mktemp -d)"
|
||||
# ditto rather than cp -R: it preserves the extended attributes the code
|
||||
# signature depends on.
|
||||
ditto "$APP" "$STAGE/KST4Contest.app"
|
||||
ln -s /Applications "$STAGE/Applications"
|
||||
|
||||
hdiutil create -volname "KST4Contest" -srcfolder "$STAGE" \
|
||||
-ov -format UDZO -quiet "$DMG"
|
||||
rm -rf "$STAGE"
|
||||
[ -f "$DMG" ] || { echo "hdiutil produced no DMG" >&2; exit 1; }
|
||||
|
||||
# Signing the DMG itself is not what Gatekeeper judges -- that is the .app
|
||||
# inside -- but Apple expects the container to be signed too.
|
||||
codesign --force --timestamp --sign "$FULL_IDENTITY" "$DMG"
|
||||
echo "==> Built $DMG"
|
||||
|
||||
echo "==> Step 4/4: verification"
|
||||
|
||||
if [ ${#NOTARY_ARGS[@]} -gt 0 ]; then
|
||||
echo " submitting for notarization (waits for Apple's verdict)"
|
||||
# Without a timeout a stalled submission would hang a CI job forever.
|
||||
xcrun notarytool submit "$DMG" "${NOTARY_ARGS[@]}" --wait --timeout 30m
|
||||
echo " stapling ticket"
|
||||
xcrun stapler staple "$DMG"
|
||||
else
|
||||
echo " no notarization credentials set, skipping notarization"
|
||||
fi
|
||||
|
||||
# Everything below inspects the app as it actually ships, mounted from the DMG,
|
||||
# rather than the staging copy on disk.
|
||||
MOUNT_POINT="$(mktemp -d)"
|
||||
hdiutil attach "$DMG" -nobrowse -quiet -mountpoint "$MOUNT_POINT"
|
||||
trap 'hdiutil detach "$MOUNT_POINT" -quiet 2>/dev/null || hdiutil detach "$MOUNT_POINT" -force -quiet 2>/dev/null || true' EXIT
|
||||
SHIPPED_APP="$MOUNT_POINT/KST4Contest.app"
|
||||
|
||||
echo "--- codesign --verify on the app inside the DMG ---"
|
||||
codesign --verify --deep --strict --verbose=2 "$SHIPPED_APP"
|
||||
|
||||
echo "--- app identity ---"
|
||||
codesign -dv --verbose=2 "$SHIPPED_APP" 2>&1 | grep -iE "identifier|authority|teamidentifier|flags"
|
||||
|
||||
# An ad-hoc signature here means something along the way re-signed the bundle.
|
||||
if codesign -dv "$SHIPPED_APP" 2>&1 | grep -q "adhoc"; then
|
||||
echo "ERROR: the app inside the DMG is ad-hoc signed, not Developer ID signed" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "--- entitlements as signed ---"
|
||||
codesign -d --entitlements - --xml "$SHIPPED_APP" 2>/dev/null | plutil -convert xml1 -o - - | grep -E "key|true|false"
|
||||
|
||||
echo "--- dmg identity ---"
|
||||
codesign -dv --verbose=2 "$DMG" 2>&1 | grep -iE "authority|teamidentifier" | head -2
|
||||
|
||||
echo "--- spctl assessment ---"
|
||||
# Without notarization this reports "rejected"; that is expected.
|
||||
spctl --assess --type execute --verbose=4 "$SHIPPED_APP" || true
|
||||
|
||||
if [ ${#NOTARY_ARGS[@]} -gt 0 ]; then
|
||||
echo "--- stapler validate ---"
|
||||
xcrun stapler validate "$DMG"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "Done: $REPO_ROOT/$DMG"
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Import the Developer ID certificate into a throwaway keychain on a CI runner.
|
||||
#
|
||||
# A runner cannot answer the keychain's authorization dialog, so the login
|
||||
# keychain is unusable there. This creates a dedicated keychain instead, whose
|
||||
# password is generated here and needed nowhere else -- it is discarded with the
|
||||
# keychain at the end of the job.
|
||||
#
|
||||
# Reads from the environment:
|
||||
# MACOS_CERT_P12 base64 of the exported .p12
|
||||
# MACOS_CERT_PASSWORD the password that .p12 was exported with
|
||||
#
|
||||
# Exports to $GITHUB_ENV:
|
||||
# SIGNING_IDENTITY for packaging/macos/build-signed-dmg.sh
|
||||
# SIGNING_KEYCHAIN so the cleanup step knows what to delete
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
: "${MACOS_CERT_P12:?MACOS_CERT_P12 is not set}"
|
||||
: "${MACOS_CERT_PASSWORD:?MACOS_CERT_PASSWORD is not set}"
|
||||
: "${RUNNER_TEMP:?RUNNER_TEMP is not set}"
|
||||
: "${GITHUB_ENV:?GITHUB_ENV is not set}"
|
||||
|
||||
KEYCHAIN="$RUNNER_TEMP/kst4contest-signing.keychain-db"
|
||||
KEYCHAIN_PASSWORD="$(uuidgen)"
|
||||
CERT="$RUNNER_TEMP/cert.p12"
|
||||
|
||||
printf '%s' "$MACOS_CERT_P12" | base64 --decode > "$CERT"
|
||||
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
||||
# Keychains re-lock after five minutes by default, which would strand a build
|
||||
# halfway through signing.
|
||||
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
|
||||
|
||||
security import "$CERT" -k "$KEYCHAIN" -P "$MACOS_CERT_PASSWORD" \
|
||||
-T /usr/bin/codesign -T /usr/bin/security
|
||||
rm -f "$CERT"
|
||||
|
||||
# Lets codesign reach the private key without the UI prompt a runner has no way
|
||||
# of answering.
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: \
|
||||
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null
|
||||
|
||||
# codesign searches the keychain list, so the new keychain has to be on it --
|
||||
# added to whatever the runner already had, not in place of it.
|
||||
EXISTING_KEYCHAINS="$(security list-keychains -d user | sed -e 's/^[[:space:]]*"//' -e 's/"$//')"
|
||||
# shellcheck disable=SC2086
|
||||
security list-keychains -d user -s "$KEYCHAIN" $EXISTING_KEYCHAINS
|
||||
|
||||
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" \
|
||||
| sed -n 's/.*"Developer ID Application: \(.*\)".*/\1/p' | head -n 1)"
|
||||
|
||||
if [ -z "$IDENTITY" ]; then
|
||||
echo "No 'Developer ID Application' identity found in the imported certificate." >&2
|
||||
echo "What the keychain does contain:" >&2
|
||||
security find-identity -v -p codesigning "$KEYCHAIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Imported identity: Developer ID Application: $IDENTITY"
|
||||
echo "SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV"
|
||||
echo "SIGNING_KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV"
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- The JVM compiles bytecode to machine code at runtime and executes it
|
||||
from memory it allocated itself. Under the hardened runtime all three
|
||||
of these are required or the app is killed on launch. -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<!-- jpackage bundles JavaFX native libraries that are signed with our own
|
||||
identity rather than Apple's, and the JVM dlopen()s them at runtime. -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- KST4Contest talks to the ON4KST chat servers. -->
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -56,6 +56,7 @@
|
||||
<javafx.maven.plugin>0.0.8</javafx.maven.plugin>
|
||||
<spotbugs.maven.plugin>4.9.8.2</spotbugs.maven.plugin>
|
||||
<spotbugs.version>4.9.8</spotbugs.version>
|
||||
<exec.maven.plugin>3.1.0</exec.maven.plugin>
|
||||
|
||||
<!-- other properties -->
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -208,6 +209,36 @@
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<!--
|
||||
Fails the build whenever the jpackage module list below drifts
|
||||
from the requires clauses in src/main/java/module-info.java.
|
||||
This is bound to validate rather than to a workflow trigger so
|
||||
it also fires on direct pushes to main, on local builds and in
|
||||
the AUR PKGBUILDs, which never run the pull request check.
|
||||
-->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>${exec.maven.plugin}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>verify-packaging-module-list</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>${java.home}/bin/java</executable>
|
||||
<workingDirectory>${project.basedir}</workingDirectory>
|
||||
<arguments>
|
||||
<argument>packaging/AddModules.java</argument>
|
||||
<argument>--verify-pom</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
@@ -436,15 +467,26 @@
|
||||
</dependencyset>
|
||||
</dependencysets>
|
||||
</modulepath>
|
||||
<!--
|
||||
Keep in sync with the requires clauses in
|
||||
src/main/java/module-info.java. The packaging
|
||||
scripts derive this list automatically via
|
||||
packaging/AddModules.java, and the PR check
|
||||
runs that helper in its pom verification mode
|
||||
so this block cannot drift unnoticed.
|
||||
-->
|
||||
<addmodules>
|
||||
<addmodule>javafx.controls</addmodule>
|
||||
<addmodule>javafx.graphics</addmodule>
|
||||
<addmodule>javafx.fxml</addmodule>
|
||||
<addmodule>javafx.web</addmodule>
|
||||
<addmodule>javafx.media</addmodule>
|
||||
<addmodule>java.sql</addmodule>
|
||||
<addmodule>java.desktop</addmodule>
|
||||
<addmodule>java.net.http</addmodule>
|
||||
<addmodule>java.sql</addmodule>
|
||||
<addmodule>javafx.controls</addmodule>
|
||||
<addmodule>javafx.fxml</addmodule>
|
||||
<addmodule>javafx.media</addmodule>
|
||||
<addmodule>javafx.web</addmodule>
|
||||
<addmodule>jdk.crypto.ec</addmodule>
|
||||
<addmodule>jdk.jsobject</addmodule>
|
||||
<addmodule>jdk.net</addmodule>
|
||||
<addmodule>jdk.xml.dom</addmodule>
|
||||
</addmodules>
|
||||
<mainclass>${main.class}</mainclass>
|
||||
<input>${project.build.directory}/modules</input>
|
||||
|
||||
@@ -5,13 +5,19 @@ import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.TimerTask;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
|
||||
/**
|
||||
* Sends periodical path requests and an AirScout watchlist for the currently
|
||||
* active ON4KST stations.
|
||||
@@ -24,8 +30,17 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
|
||||
private static final String BROADCAST_ADDRESS = "255.255.255.255";
|
||||
|
||||
|
||||
|
||||
private final ChatController client;
|
||||
|
||||
/*
|
||||
* ASWATCHLIST is sent as one common list. Remember one syntactically valid
|
||||
* AirScout band value so an empty list can still be sent on a later cycle
|
||||
* to remove stations which are no longer active.
|
||||
*/
|
||||
private String lastWatchListBandValue;
|
||||
|
||||
public AirScoutPeriodicalAPReflectionInquirerTask(
|
||||
ChatController client
|
||||
) {
|
||||
@@ -50,9 +65,6 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
client.getChatPreferences().getAirScout_asClientNameString();
|
||||
String serverIdentifier =
|
||||
client.getChatPreferences().getAirScout_asServerNameString();
|
||||
String bandValue =
|
||||
client.getChatPreferences().getAirScout_asBandString();
|
||||
|
||||
String ownCallSign = normalizeOwnCallSign(
|
||||
client.getChatPreferences().getStn_loginCallSign()
|
||||
);
|
||||
@@ -79,14 +91,10 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
+ "\" \"" + serverIdentifier + "\" ";
|
||||
|
||||
String ownStation = ownCallSign + "," + ownLocator;
|
||||
StringBuilder watchListMessage = new StringBuilder(
|
||||
watchListPrefix
|
||||
+ bandValue
|
||||
+ ","
|
||||
+ ownStation
|
||||
);
|
||||
|
||||
List<ChatMember> activeMembers = client.snapshotChatMembers();
|
||||
List<String> watchListTargets = new ArrayList<>();
|
||||
Set<String> processedCallsigns = new LinkedHashSet<>();
|
||||
String watchListBandValue = null;
|
||||
int port = client.getChatPreferences()
|
||||
.getAirScout_asCommunicationPort();
|
||||
|
||||
@@ -102,11 +110,38 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (member.getQrb()
|
||||
if (member.getQrb() == null
|
||||
|| member.getQrb()
|
||||
>= client.getChatPreferences().getStn_maxQRBDefault()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String callsignKey = member.getCallSignRaw();
|
||||
if (callsignKey == null || callsignKey.isBlank()) {
|
||||
callsignKey = member.getCallSign();
|
||||
}
|
||||
if (callsignKey == null
|
||||
|| !processedCallsigns.add(
|
||||
callsignKey.trim().toUpperCase(Locale.ROOT)
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* The resolver may deliberately return an exact QRG. AirScout must
|
||||
* only see a canonical protocol band value such as 4320000.
|
||||
*/
|
||||
String bandValue = canonicalizeAirScoutBandValue(
|
||||
client.resolveAirScoutBandValue(member)
|
||||
);
|
||||
if (bandValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (watchListBandValue == null) {
|
||||
watchListBandValue = bandValue;
|
||||
}
|
||||
|
||||
String targetStation =
|
||||
member.getCallSign() + "," + member.getQra();
|
||||
|
||||
@@ -126,19 +161,47 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
pathQuery
|
||||
);
|
||||
|
||||
watchListMessage
|
||||
.append(",")
|
||||
.append(targetStation);
|
||||
watchListTargets.add(targetStation);
|
||||
}
|
||||
|
||||
watchListMessage.append(" ");
|
||||
/*
|
||||
* AirScout keeps one watchlist per client/server pair. Do not send
|
||||
* separate lists for the individual station bands because a later
|
||||
* list would replace stations from an earlier one.
|
||||
*
|
||||
* If there are no targets in this cycle, reuse the last valid band
|
||||
* token and send an empty list so AirScout can clear stale entries.
|
||||
*/
|
||||
if (watchListBandValue == null) {
|
||||
watchListBandValue = lastWatchListBandValue;
|
||||
}
|
||||
|
||||
if (watchListBandValue != null) {
|
||||
StringBuilder watchListMessage = new StringBuilder(
|
||||
watchListPrefix
|
||||
+ watchListBandValue
|
||||
+ ","
|
||||
+ ownStation
|
||||
);
|
||||
|
||||
for (String targetStation : watchListTargets) {
|
||||
watchListMessage
|
||||
.append(",")
|
||||
.append(targetStation);
|
||||
}
|
||||
|
||||
watchListMessage.append(" ");
|
||||
|
||||
sendPacket(
|
||||
socket,
|
||||
broadcastAddress,
|
||||
port,
|
||||
watchListMessage.toString()
|
||||
);
|
||||
|
||||
lastWatchListBandValue = watchListBandValue;
|
||||
}
|
||||
|
||||
sendPacket(
|
||||
socket,
|
||||
broadcastAddress,
|
||||
port,
|
||||
watchListMessage.toString()
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
@@ -148,6 +211,60 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Converts a frequency-like value returned by the station resolver into the
|
||||
* canonical band token expected by the AirScout UDP protocol.
|
||||
*
|
||||
* <p>The internal resolver may keep an exact working frequency for path
|
||||
* analysis. This method removes that precision only at the AirScout protocol
|
||||
* boundary. For example, {@code 4321740} is sent to AirScout as
|
||||
* {@code 4320000}.</p>
|
||||
*
|
||||
* @param resolvedValue frequency-like AirScout value produced by the resolver
|
||||
* @return canonical AirScout band value, or {@code null} if unsupported
|
||||
*/
|
||||
private String canonicalizeAirScoutBandValue(String resolvedValue) {
|
||||
if (resolvedValue == null || resolvedValue.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalizedValue = resolvedValue.trim();
|
||||
|
||||
if ("off".equalsIgnoreCase(normalizedValue)
|
||||
|| "auto".equalsIgnoreCase(normalizedValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final long numericValue;
|
||||
try {
|
||||
numericValue = Long.parseLong(normalizedValue);
|
||||
} catch (NumberFormatException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Unsupported AirScout band value: " + resolvedValue,
|
||||
exception
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
double frequencyMHz = numericValue / 10_000.0;
|
||||
Band band = Band.fromFrequency(frequencyMHz);
|
||||
|
||||
if (band == null) {
|
||||
LOGGER.warning(
|
||||
"AirScout query skipped because frequency "
|
||||
+ frequencyMHz
|
||||
+ " MHz does not belong to a supported band."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return band.getPrefix() + "0000";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the ON4KST login suffix because AirScout expects the actual
|
||||
* station callsign, for example 9A1W instead of 9A1W-2.
|
||||
|
||||
@@ -2,6 +2,7 @@ package kst4contest.controller;
|
||||
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMessage;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
@@ -12,6 +13,10 @@ import kst4contest.model.ThreadStateMessage;
|
||||
* interval. Their enable flags and message templates remain independent. Every
|
||||
* run reads the current preferences, resolves global message variables and
|
||||
* sends only the categories which are currently enabled.</p>
|
||||
*
|
||||
* <p>Beacon messages use the regular outbound chat-message pipeline. They are
|
||||
* not assembled as raw ON4KST frames, because that would bypass the common
|
||||
* category, delimiter and message-text validation.</p>
|
||||
*/
|
||||
public class BeaconTask extends TimerTask {
|
||||
|
||||
@@ -39,35 +44,40 @@ public class BeaconTask extends TimerTask {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
reportStatus(THREAD_NICKNAME, true, "initialized", false);
|
||||
|
||||
MessageVariableResolver variableResolver =
|
||||
new MessageVariableResolver(chatController.getChatPreferences());
|
||||
|
||||
sendMainCategoryBeacon(variableResolver);
|
||||
sendSecondCategoryBeacon(variableResolver);
|
||||
sendMainCategoryBeacon();
|
||||
sendSecondCategoryBeacon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the main-category beacon if it is currently enabled.
|
||||
*/
|
||||
private void sendMainCategoryBeacon(MessageVariableResolver variableResolver) {
|
||||
if (!chatController.getChatPreferences().isBcn_beaconsEnabledMainCat()) {
|
||||
reportStatus(THREAD_NICKNAME + " 1", false, "off", false);
|
||||
private void sendMainCategoryBeacon() {
|
||||
if (!chatController.getChatPreferences()
|
||||
.isBcn_beaconsEnabledMainCat()) {
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 1",
|
||||
false,
|
||||
"off",
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextMainCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategoryMain()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
.getLoginChatCategoryMain(),
|
||||
chatController.getChatPreferences()
|
||||
.getBcn_beaconTextMainCat(),
|
||||
"main category"
|
||||
);
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 1", false, "invalid text", true);
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 1",
|
||||
false,
|
||||
"invalid text",
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -76,37 +86,50 @@ public class BeaconTask extends TimerTask {
|
||||
+ " [BeaconTask, Info]: Sending main-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
reportStatus(THREAD_NICKNAME + " 1", true, "on", false);
|
||||
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 1",
|
||||
true,
|
||||
"on",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the second-category beacon if the second login and its beacon are
|
||||
* currently enabled.
|
||||
*/
|
||||
private void sendSecondCategoryBeacon(
|
||||
MessageVariableResolver variableResolver
|
||||
) {
|
||||
if (!chatController.getChatPreferences().isLoginToSecondChatEnabled()
|
||||
private void sendSecondCategoryBeacon() {
|
||||
if (!chatController.getChatPreferences()
|
||||
.isLoginToSecondChatEnabled()
|
||||
|| !chatController.getChatPreferences()
|
||||
.isBcn_beaconsEnabledSecondCat()) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "off", false);
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 2",
|
||||
false,
|
||||
"off",
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextSecondCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategorySecond()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
.getLoginChatCategorySecond(),
|
||||
chatController.getChatPreferences()
|
||||
.getBcn_beaconTextSecondCat(),
|
||||
"second category"
|
||||
);
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "invalid text", true);
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 2",
|
||||
false,
|
||||
"invalid text",
|
||||
true
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,47 +138,65 @@ public class BeaconTask extends TimerTask {
|
||||
+ " [BeaconTask, Info]: Sending second-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
reportStatus(THREAD_NICKNAME + " 2", true, "on", false);
|
||||
|
||||
reportStatus(
|
||||
THREAD_NICKNAME + " 2",
|
||||
true,
|
||||
"on",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the server-directed message after validating the resolved payload.
|
||||
* Resolves and validates one beacon before placing it in the regular outbound
|
||||
* message queue.
|
||||
*
|
||||
* <p>The resolved text is checked rather than only the configured template
|
||||
* because inserted values can increase the final message length.</p>
|
||||
* <p>The returned message contains only the public-chat payload and its chat
|
||||
* category. {@link WriteThread} creates the final ON4KST frame through
|
||||
* {@link On4KstProtocol#chatMessage(int, String)}. This prevents a configurable
|
||||
* beacon text from bypassing the common protocol validation.</p>
|
||||
*
|
||||
* @param categoryNumber ON4KST category number
|
||||
* @param resolvedText fully resolved beacon payload
|
||||
* @param category target ON4KST chat category
|
||||
* @param configuredText configured beacon template
|
||||
* @param categoryDescription text used in diagnostic output
|
||||
* @return prepared message, or {@code null} if the payload is invalid
|
||||
* @return prepared message, or {@code null} if the category or text is invalid
|
||||
*/
|
||||
private ChatMessage buildBeaconMessage(
|
||||
int categoryNumber,
|
||||
String resolvedText,
|
||||
ChatCategory category,
|
||||
String configuredText,
|
||||
String categoryDescription
|
||||
) {
|
||||
if (resolvedText == null
|
||||
|| resolvedText.length() > ChatController.MAX_BEACON_TEXT_LENGTH) {
|
||||
int actualLength = resolvedText == null ? 0 : resolvedText.length();
|
||||
try {
|
||||
if (category == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"No chat category is configured."
|
||||
);
|
||||
}
|
||||
|
||||
On4KstProtocol.category(category.getCategoryNumber());
|
||||
|
||||
String resolvedText =
|
||||
chatController.resolveAndValidateBeaconText(
|
||||
configuredText
|
||||
);
|
||||
|
||||
ChatMessage beaconMessage = new ChatMessage();
|
||||
beaconMessage.setMessageText(resolvedText);
|
||||
beaconMessage.setChatCategory(category);
|
||||
beaconMessage.setMessageDirectedToServer(false);
|
||||
|
||||
return beaconMessage;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
System.out.println(
|
||||
"[BeaconTask, Warning]: Beacon for "
|
||||
+ categoryDescription
|
||||
+ " was not sent because the resolved text contains "
|
||||
+ actualLength
|
||||
+ " characters; maximum is "
|
||||
+ ChatController.MAX_BEACON_TEXT_LENGTH
|
||||
+ "."
|
||||
+ " was not queued: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
ChatMessage beaconMessage = new ChatMessage();
|
||||
beaconMessage.setMessageText(
|
||||
"MSG|" + categoryNumber + "|0|" + resolvedText + "|0|"
|
||||
);
|
||||
beaconMessage.setMessageDirectedToServer(true);
|
||||
return beaconMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,6 +214,9 @@ public class BeaconTask extends TimerTask {
|
||||
information,
|
||||
criticalState
|
||||
);
|
||||
callbackToController.onThreadStatus(THREAD_NICKNAME, stateMessage);
|
||||
callbackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
stateMessage
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ import kst4contest.ApplicationConstants;
|
||||
import kst4contest.locatorUtils.DirectionUtils;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.model.*;
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.function.LongPredicate;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -36,6 +39,11 @@ public class MessageBusManagementThread extends Thread {
|
||||
private PrintWriter writer;
|
||||
// private Socket socket;
|
||||
private ChatController client;
|
||||
|
||||
private final long connectionSessionId;
|
||||
private final LinkedBlockingQueue<ChatMessage> receiveQueue;
|
||||
private final LongPredicate connectionSessionIsActive;
|
||||
|
||||
// private File fileLogRAW;
|
||||
// private TimerTask userActualizationTask; // Is used as a temporary userout-print
|
||||
// private TimerTask userActualizationTask; //kst4contest.test 4 23001
|
||||
@@ -58,7 +66,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
* would be converted into plausible but incorrect frequencies.
|
||||
*/
|
||||
private static final Pattern SMART_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![\\d])(\\d{3,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
"(?<![\\d])(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
+ "|(?<![\\d])([.,]\\d{3}(?:[.,]\\d{1,3})?)(?![\\d])"
|
||||
+ "|(?<=\\s|^)(\\d{3})(?=\\s|$)"
|
||||
);
|
||||
@@ -117,9 +125,22 @@ public class MessageBusManagementThread extends Thread {
|
||||
}
|
||||
|
||||
public MessageBusManagementThread(ChatController client, ThreadStatusCallback callBack) {
|
||||
this(client, callBack, 0L, client.getMessageRXBus(), ignored -> true);
|
||||
}
|
||||
|
||||
public MessageBusManagementThread(
|
||||
ChatController client,
|
||||
ThreadStatusCallback callBack,
|
||||
long connectionSessionId,
|
||||
LinkedBlockingQueue<ChatMessage> receiveQueue,
|
||||
LongPredicate connectionSessionIsActive
|
||||
) {
|
||||
|
||||
this.callBackToController = callBack;
|
||||
this.client = client;
|
||||
this.connectionSessionId = connectionSessionId;
|
||||
this.receiveQueue = receiveQueue;
|
||||
this.connectionSessionIsActive = connectionSessionIsActive;
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
@@ -319,16 +340,28 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
try {
|
||||
String reconstructed =
|
||||
candidateBand.getPrefix() + "." + foundRaw;
|
||||
double candidateFrequency = Double.parseDouble(
|
||||
normalizeFrequencyString(reconstructed)
|
||||
);
|
||||
candidateBand.getPrefix()
|
||||
+ "."
|
||||
+ foundRaw;
|
||||
|
||||
if (candidateBand.isPlausible(candidateFrequency)
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
FrequencyTextParser.parseExplicitFrequency(
|
||||
reconstructed
|
||||
);
|
||||
|
||||
if (detectedFrequency != null
|
||||
&& detectedFrequency.getBand() == candidateBand
|
||||
&& info.timestampEpoch > bestTimestamp) {
|
||||
finalDetectedFrequency = candidateFrequency;
|
||||
finalDetectedBand = candidateBand;
|
||||
bestTimestamp = info.timestampEpoch;
|
||||
|
||||
finalDetectedFrequency =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
finalDetectedBand =
|
||||
candidateBand;
|
||||
|
||||
bestTimestamp =
|
||||
info.timestampEpoch;
|
||||
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Try the next known band.
|
||||
@@ -357,27 +390,40 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
try {
|
||||
String reconstructed =
|
||||
fallbackBand.getPrefix() + "." + foundRaw;
|
||||
double candidateFrequency = Double.parseDouble(
|
||||
normalizeFrequencyString(reconstructed)
|
||||
);
|
||||
fallbackBand.getPrefix()
|
||||
+ "."
|
||||
+ foundRaw;
|
||||
|
||||
if (fallbackBand.isPlausible(candidateFrequency)) {
|
||||
finalDetectedFrequency = candidateFrequency;
|
||||
finalDetectedBand = fallbackBand;
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
FrequencyTextParser.parseExplicitFrequency(
|
||||
reconstructed
|
||||
);
|
||||
|
||||
if (detectedFrequency != null
|
||||
&& detectedFrequency.getBand() == fallbackBand) {
|
||||
|
||||
finalDetectedFrequency =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
finalDetectedBand =
|
||||
fallbackBand;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// The matched value cannot be converted into a frequency.
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
finalDetectedFrequency = Double.parseDouble(
|
||||
normalizeFrequencyString(foundRaw)
|
||||
);
|
||||
finalDetectedBand = Band.fromFrequency(finalDetectedFrequency);
|
||||
} catch (NumberFormatException ignored) {
|
||||
// Continue with the next possible match in the message.
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
FrequencyTextParser.parseExplicitFrequency(
|
||||
foundRaw
|
||||
);
|
||||
|
||||
if (detectedFrequency != null) {
|
||||
finalDetectedFrequency =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
finalDetectedBand =
|
||||
detectedFrequency.getBand();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,22 +508,22 @@ public class MessageBusManagementThread extends Thread {
|
||||
* Example: "144.210.10" -> "144.21010"
|
||||
* Example: "144.210" -> "144.210"
|
||||
*/
|
||||
private String normalizeFrequencyString(String rawInput) {
|
||||
// Input is already guaranteed to have only dots as separators (commas replaced earlier)
|
||||
|
||||
int firstDotIndex = rawInput.indexOf(".");
|
||||
|
||||
if (firstDotIndex != -1) {
|
||||
// Check if there are more dots after the first one
|
||||
String decimalPart = rawInput.substring(firstDotIndex + 1);
|
||||
if (decimalPart.contains(".")) {
|
||||
// Remove all subsequent dots to make it a valid double
|
||||
decimalPart = decimalPart.replace(".", "");
|
||||
return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
|
||||
}
|
||||
}
|
||||
return rawInput;
|
||||
}
|
||||
// private String normalizeFrequencyString(String rawInput) {
|
||||
// // Input is already guaranteed to have only dots as separators (commas replaced earlier)
|
||||
//
|
||||
// int firstDotIndex = rawInput.indexOf(".");
|
||||
//
|
||||
// if (firstDotIndex != -1) {
|
||||
// // Check if there are more dots after the first one
|
||||
// String decimalPart = rawInput.substring(firstDotIndex + 1);
|
||||
// if (decimalPart.contains(".")) {
|
||||
// // Remove all subsequent dots to make it a valid double
|
||||
// decimalPart = decimalPart.replace(".", "");
|
||||
// return rawInput.substring(0, firstDotIndex) + "." + decimalPart;
|
||||
// }
|
||||
// }
|
||||
// return rawInput;
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
@@ -496,7 +542,8 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
messageToProcess.setMessageText(reduce);
|
||||
|
||||
if (messageToProcess.getMessageText().isEmpty()) {
|
||||
if (messageToProcess.getMessageText() == null
|
||||
|| messageToProcess.getMessageText().isEmpty()) {
|
||||
// System.out.println("[MSGBUSMGTT:] ###################### no processable data");
|
||||
} else {
|
||||
|
||||
@@ -728,6 +775,50 @@ public class MessageBusManagementThread extends Thread {
|
||||
return fallbackReceiver;
|
||||
}
|
||||
|
||||
private boolean validateInboundUserFrame(String[] fields) {
|
||||
if (fields == null || fields.length < 6) {
|
||||
logRejectedInboundUserFrame(
|
||||
"Ignoring truncated ON4KST user frame", fields);
|
||||
return false;
|
||||
}
|
||||
if (fields[2] == null || fields[2].isBlank()) {
|
||||
logRejectedInboundUserFrame(
|
||||
"Ignoring ON4KST user frame with empty callsign", fields);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
On4KstProtocol.category(Integer.parseInt(fields[1]));
|
||||
On4KstProtocol.locator(fields[4]);
|
||||
Integer.parseInt(fields[5]);
|
||||
return true;
|
||||
} catch (IllegalArgumentException invalidUser) {
|
||||
logRejectedInboundUserFrame(
|
||||
"Ignoring malformed ON4KST user '" + fields[2]
|
||||
+ "': " + invalidUser.getMessage(), fields);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a rejected user frame without writing the complete raw frame or user
|
||||
* name to the diagnostic log.
|
||||
*
|
||||
* <p>The opcode, category and callsign are sufficient to identify the offending
|
||||
* list position. Omitting the remaining fields avoids unnecessary disclosure of
|
||||
* free-form profile text.</p>
|
||||
*/
|
||||
private void logRejectedInboundUserFrame(String reason, String[] fields) {
|
||||
String opcode = fields != null && fields.length > 0 ? fields[0] : "UNKNOWN";
|
||||
String category = fields != null && fields.length > 1 ? fields[1] : "UNKNOWN";
|
||||
String callsign = fields != null && fields.length > 2 ? fields[2] : "UNKNOWN";
|
||||
client.onOn4KstConnectionWarning(
|
||||
reason + "; opcode=" + opcode
|
||||
+ ", category=" + category
|
||||
+ ", callsign=" + callsign
|
||||
+ ", fieldCount=" + (fields == null ? 0 : fields.length));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes received messages via port 23001 (improved telnet Interface)
|
||||
*
|
||||
@@ -782,23 +873,33 @@ public class MessageBusManagementThread extends Thread {
|
||||
* here we have a helper list for identifying questions for my qrg which can be autoanswered later
|
||||
*/
|
||||
|
||||
if (messageToProcess.getMessageText().isEmpty()) {
|
||||
// System.out.println("[MSGBUSMGTT:] no processable data");
|
||||
|
||||
if (messageToProcess.getMessageText() == null
|
||||
|| messageToProcess.getMessageText().isEmpty()) {
|
||||
// No processable data.
|
||||
} else {
|
||||
|
||||
if (messageToProcess.getMessageText().contains(SRVR_LOGSTAT)) {
|
||||
String logstatMessage[];
|
||||
logstatMessage = messageToProcess.getMessageText().split("\\|");
|
||||
if (logstatMessage[1].contains(SRVR_LOGINOK)) {
|
||||
this.client.setConnectedAndLoggedIn(true);
|
||||
} else {
|
||||
this.client.setConnectedAndNOTLoggedIn(true);
|
||||
this.client.setConnectedAndLoggedIn(false);
|
||||
}
|
||||
if (messageToProcess.getMessageText().startsWith(SRVR_LOGSTAT + "|")) {
|
||||
String[] logstatMessage =
|
||||
messageToProcess.getMessageText().split("\\|", -1);
|
||||
this.client.onOn4KstLogstat(
|
||||
connectionSessionId,
|
||||
logstatMessage);
|
||||
}
|
||||
|
||||
String splittedMessageLine[] = messageToProcess.getMessageText().split("\\|");
|
||||
String[] splittedMessageLine =
|
||||
messageToProcess.getMessageText().split("\\|");
|
||||
|
||||
String opcode = splittedMessageLine.length == 0
|
||||
? ""
|
||||
: splittedMessageLine[0];
|
||||
|
||||
if ((INITIALUSERLISTENTRY.equals(opcode)
|
||||
|| USERENTEREDCHAT.equals(opcode)
|
||||
|| USERENTEREDCHAT2.equals(opcode))
|
||||
&& !validateInboundUserFrame(splittedMessageLine)) {
|
||||
return;
|
||||
}
|
||||
// String splittedMessageLine[] = messageToProcess.getMessageText().split("\\|");
|
||||
|
||||
/**
|
||||
* Initializes the Userlist if entry fits UA0
|
||||
@@ -806,7 +907,7 @@ public class MessageBusManagementThread extends Thread {
|
||||
*
|
||||
*
|
||||
*/
|
||||
if (splittedMessageLine[0].contains(INITIALUSERLISTENTRY)) {
|
||||
if (splittedMessageLine[0].equals(INITIALUSERLISTENTRY)) {
|
||||
// System.out.println("MSGBUS: User detected");
|
||||
|
||||
ChatMember newMember = new ChatMember();
|
||||
@@ -827,7 +928,9 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
|
||||
if (!client.getChatPreferences().getStn_loginCallSign().equals(newMember.getCallSign())) {
|
||||
this.client.addOrUpdateActiveChatMember(newMember); // the own call will not be in the list
|
||||
this.client.stageInitialOn4KstChatMember(
|
||||
connectionSessionId,
|
||||
newMember);
|
||||
// this.client.getReachabilityService().ensureAutoTropoMarginCalculated(newMember);
|
||||
// Reachability is calculated on demand only: map click, selected station, or manual request.
|
||||
}
|
||||
@@ -851,7 +954,8 @@ public class MessageBusManagementThread extends Thread {
|
||||
* UA2|2|W5ADD|Parker|EM40WL|2|
|
||||
*
|
||||
*/
|
||||
if (splittedMessageLine[0].contains(USERENTEREDCHAT) || splittedMessageLine[0].contains(USERENTEREDCHAT2)) {
|
||||
if (splittedMessageLine[0].equals(USERENTEREDCHAT)
|
||||
|| splittedMessageLine[0].equals(USERENTEREDCHAT2)) {
|
||||
// System.out.println("MSGBUS: User detected");
|
||||
|
||||
|
||||
@@ -1063,16 +1167,22 @@ public class MessageBusManagementThread extends Thread {
|
||||
// }
|
||||
// }
|
||||
|
||||
// ==== Unified auto-answer (generic + QRG) with ping-pong guard and per-remote cooldown ====
|
||||
// ==== Unified auto-answer (generic + QRG) with ping-pong guard
|
||||
// and per-remote cooldown ====
|
||||
final String incomingText = newMessageArrived.getMessageText();
|
||||
final String incomingLower = (incomingText == null) ? "" : incomingText.toLowerCase(Locale.ROOT);
|
||||
final String incomingLower =
|
||||
(incomingText == null)
|
||||
? ""
|
||||
: incomingText.toLowerCase(Locale.ROOT);
|
||||
|
||||
// Never answer another automatically generated message.
|
||||
// Never answer another automatically generated message.
|
||||
if (!isAutoMessage(newMessageArrived)) {
|
||||
|
||||
boolean qrgRequested = false;
|
||||
|
||||
if (this.client.getChatPreferences().isMessageHandling_autoAnswerToQRGRequestEnabled()) {
|
||||
if (this.client.getChatPreferences()
|
||||
.isMessageHandling_autoAnswerToQRGRequestEnabled()) {
|
||||
|
||||
for (String lookForQRGString : qrgQuestionTexts) {
|
||||
if (incomingLower.contains(lookForQRGString)) {
|
||||
qrgRequested = true;
|
||||
@@ -1081,36 +1191,47 @@ public class MessageBusManagementThread extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
boolean genericEnabled = this.client.getChatPreferences().isMsgHandling_autoAnswerEnabled();
|
||||
boolean genericEnabled =
|
||||
this.client.getChatPreferences()
|
||||
.isMsgHandling_autoAnswerEnabled();
|
||||
|
||||
// A QRG reply takes precedence over the generic reply.
|
||||
String payload = null;
|
||||
String automaticAnswerText = buildAutoAnswerMessageText(
|
||||
newMessageArrived,
|
||||
qrgRequested,
|
||||
genericEnabled
|
||||
);
|
||||
|
||||
if (qrgRequested) {
|
||||
payload = "QRG is: " + getAutoAnswerQrgForCategory(newMessageArrived.getChatCategory());
|
||||
} else if (genericEnabled) {
|
||||
|
||||
payload = this.client.getChatPreferences().getMessageHandling_autoAnswerTextMainCat();
|
||||
}
|
||||
|
||||
// Apply the cooldown only when this client is about to send a reply.
|
||||
if (payload != null && isAutoAnswerAllowedNow(newMessageArrived)) {
|
||||
/*
|
||||
* Invalid or incomplete replies are rejected before the cooldown
|
||||
* is checked or updated. A missing QRG must therefore not suppress
|
||||
* a later valid reply.
|
||||
*/
|
||||
if (automaticAnswerText != null
|
||||
&& isAutoAnswerAllowedNow(newMessageArrived)) {
|
||||
|
||||
ChatMessage automaticAnswer = new ChatMessage();
|
||||
ChatMember itsMe = new ChatMember();
|
||||
itsMe.setCallSign(this.client.getChatPreferences().getStn_loginCallSign());
|
||||
|
||||
itsMe.setCallSign(
|
||||
this.client.getChatPreferences()
|
||||
.getStn_loginCallSign()
|
||||
);
|
||||
|
||||
automaticAnswer.setSender(itsMe);
|
||||
automaticAnswer.setReceiver(newMessageArrived.getSender());
|
||||
automaticAnswer.setChatCategory(newMessageArrived.getChatCategory());
|
||||
|
||||
// The fixed prefix prevents automatic clients from answering each other.
|
||||
automaticAnswer.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
|
||||
+ " " + AUTOANSWER_PREFIX + " " + payload);
|
||||
automaticAnswer.setReceiver(
|
||||
newMessageArrived.getSender()
|
||||
);
|
||||
automaticAnswer.setChatCategory(
|
||||
newMessageArrived.getChatCategory()
|
||||
);
|
||||
automaticAnswer.setMessageText(automaticAnswerText);
|
||||
|
||||
this.client.getMessageTXBus().add(automaticAnswer);
|
||||
|
||||
// Record only locally generated replies, not the later server echo.
|
||||
/*
|
||||
* Record the cooldown only after a complete and locally
|
||||
* validated reply has been placed in the transmit queue.
|
||||
*/
|
||||
markLocalAutoAnswerSent(newMessageArrived);
|
||||
}
|
||||
}
|
||||
@@ -1167,27 +1288,31 @@ public class MessageBusManagementThread extends Thread {
|
||||
|
||||
if (client.getChatPreferences().isNotify_dxClusterServerEnabled()) {
|
||||
try {
|
||||
if (newMessageArrived.getSender().getFrequency() != null) {
|
||||
//TODO: testing for next version 3.33: additional information will be displayed in cluster if there is such information
|
||||
ChatMember sender = newMessageArrived.getSender();
|
||||
|
||||
String detectedFrequency = sender.getFrequency() == null
|
||||
? null
|
||||
: sender.getFrequency().getValue();
|
||||
|
||||
if (detectedFrequency != null && !detectedFrequency.isBlank()) {
|
||||
/*
|
||||
* The DX Cluster spot must not depend on AirScout data. A known
|
||||
* frequency and a valid directional opportunity are sufficient.
|
||||
* Available AP information is added only as an optional comment.
|
||||
*/
|
||||
ChatMember onlyForSpottingObject = new ChatMember();
|
||||
onlyForSpottingObject.setCallSign(newMessageArrived.getSender().getCallSign());
|
||||
onlyForSpottingObject.setFrequency(newMessageArrived.getSender().getFrequency());
|
||||
onlyForSpottingObject.setCallSign(sender.getCallSign());
|
||||
onlyForSpottingObject.setFrequency(sender.getFrequency());
|
||||
onlyForSpottingObject.setQra(
|
||||
buildDxClusterSpotComment(sender)
|
||||
);
|
||||
|
||||
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 0) {
|
||||
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + " , AP: " +
|
||||
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getArrivingDurationMinutes() + "min, " +
|
||||
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(0).getPotential() + "%");
|
||||
this.client
|
||||
.getDxClusterServer()
|
||||
.broadcastSingleDXClusterEntryToLoggers(
|
||||
onlyForSpottingObject
|
||||
);
|
||||
|
||||
if (newMessageArrived.getSender().getAirPlaneReflectInfo().getAirPlanesReachableCntr() > 1) {
|
||||
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra() + "; " +
|
||||
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getArrivingDurationMinutes() + "min, " +
|
||||
newMessageArrived.getSender().getAirPlaneReflectInfo().getRisingAirplanes().get(1).getPotential() + "%");
|
||||
}
|
||||
} else {
|
||||
onlyForSpottingObject.setQra(newMessageArrived.getSender().getQra());
|
||||
}
|
||||
|
||||
this.client.getDxClusterServer().broadcastSingleDXClusterEntryToLoggers(onlyForSpottingObject);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
System.out.println("[MSGBUSMGT, ERROR:] DXCluster messageserver error while processing spot for 0: " + newMessageArrived.getSender().getCallSign() + " // " + exception.getMessage());
|
||||
@@ -1373,9 +1498,12 @@ public class MessageBusManagementThread extends Thread {
|
||||
newDXCListSender3.setQra(splittedMessageLine[5]);
|
||||
|
||||
ChatMember newDXCListReceiver3 = new ChatMember();
|
||||
// newDXCListReceiver3.setFrequency(splittedMessageLine[4]);
|
||||
newDXCListReceiver3.setCallSign(splittedMessageLine[4]);
|
||||
newDXCListReceiver3.setQra(splittedMessageLine[5]);
|
||||
/*
|
||||
* MA format:
|
||||
* MA|0|epoch|sender|receiver|sender locator|receiver locator|
|
||||
*/
|
||||
newDXCListReceiver3.setQra(splittedMessageLine[6]);
|
||||
|
||||
dxcMsg3.setSender(newDXCListSender3);
|
||||
dxcMsg3.setReceiver(newDXCListReceiver3);
|
||||
@@ -1607,18 +1735,29 @@ public class MessageBusManagementThread extends Thread {
|
||||
/**
|
||||
* Userinfo-update: UE|2|22562|
|
||||
*/
|
||||
if (splittedMessageLine[0].contains(SRVR_USERLISTEND)) {
|
||||
if (SRVR_USERLISTEND.equals(opcode)) {
|
||||
if (splittedMessageLine.length < 2) {
|
||||
System.out.println(
|
||||
"[MSGBUSMGT, Warning:] Ignoring malformed UE frame: "
|
||||
+ messageToProcess.getMessageText());
|
||||
return;
|
||||
}
|
||||
|
||||
// No worthy information, count of users
|
||||
} else
|
||||
this.client.onOn4KstInitialUserListCompleted(
|
||||
connectionSessionId,
|
||||
util_getChatCategoryByCategoryNrString(
|
||||
splittedMessageLine[1]));
|
||||
|
||||
if (splittedMessageLine[0].contains(SRVR_DXCEND)) {
|
||||
} else if (SRVR_DXCEND.equals(opcode)) {
|
||||
|
||||
// No worthy information, count of users
|
||||
} else
|
||||
// DF marks the end of the initial DX-cluster data.
|
||||
// The frame contains no data that needs to be published.
|
||||
|
||||
} else if (SRVR_COMMUNICATIONK.equals(opcode)) {
|
||||
|
||||
// CK is a regular server delimiter/acknowledgement.
|
||||
// It is intentionally accepted without further processing.
|
||||
|
||||
if (splittedMessageLine[0].contains(SRVR_COMMUNICATIONK)) {
|
||||
// No worthy information, end of srvrmsgs
|
||||
} else
|
||||
|
||||
//-> LOGSTAT|114|Wrong password!|
|
||||
@@ -1691,6 +1830,70 @@ public class MessageBusManagementThread extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds the comment transmitted with a local DX Cluster spot.
|
||||
*
|
||||
* <p>The station locator is always retained. AirScout information is optional:
|
||||
* a missing response, an empty aircraft list or an incomplete aircraft entry
|
||||
* must never prevent the spot itself from being sent.</p>
|
||||
*
|
||||
* @param sender station for which the DX Cluster spot is generated
|
||||
* @return locator with up to two optional AP entries
|
||||
*/
|
||||
private String buildDxClusterSpotComment(ChatMember sender) {
|
||||
if (sender == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String locator = sender.getQra() == null
|
||||
? ""
|
||||
: sender.getQra().trim();
|
||||
|
||||
AirPlaneReflectionInfo reflectionInfo =
|
||||
sender.getAirPlaneReflectInfo();
|
||||
|
||||
if (reflectionInfo == null
|
||||
|| reflectionInfo.getRisingAirplanes() == null
|
||||
|| reflectionInfo.getRisingAirplanes().isEmpty()) {
|
||||
return locator;
|
||||
}
|
||||
|
||||
ArrayList<String> aircraftComments = new ArrayList<>();
|
||||
|
||||
int aircraftCount = Math.min(
|
||||
2,
|
||||
reflectionInfo.getRisingAirplanes().size()
|
||||
);
|
||||
|
||||
for (int index = 0; index < aircraftCount; index++) {
|
||||
AirPlane aircraft =
|
||||
reflectionInfo.getRisingAirplanes().get(index);
|
||||
|
||||
if (aircraft == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
aircraftComments.add(
|
||||
aircraft.getArrivingDurationMinutes()
|
||||
+ "min, "
|
||||
+ aircraft.getPotential()
|
||||
+ "%"
|
||||
);
|
||||
}
|
||||
|
||||
if (aircraftComments.isEmpty()) {
|
||||
return locator;
|
||||
}
|
||||
|
||||
String apComment =
|
||||
"AP: " + String.join("; ", aircraftComments);
|
||||
|
||||
return locator.isEmpty()
|
||||
? apComment
|
||||
: locator + " , " + apComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method gets a String with a messagecategory-number and returns out of which of the existing categories
|
||||
* (chat channels) this message/user had written from
|
||||
@@ -1720,6 +1923,101 @@ public class MessageBusManagementThread extends Thread {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds and validates one automatic private reply.
|
||||
*
|
||||
* <p>A QRG request is answered only when the QRG belonging to the
|
||||
* incoming chat category is available. The generic answer is used
|
||||
* only for other private messages and only when it contains actual
|
||||
* text.</p>
|
||||
*
|
||||
* <p>The complete message is validated before it enters the transmit
|
||||
* queue. Invalid configuration values must neither produce an empty
|
||||
* automatic reply nor start the cooldown.</p>
|
||||
*
|
||||
* @param incoming incoming private message
|
||||
* @param qrgRequested whether the message contains a recognised QRG request
|
||||
* @param genericEnabled whether the general automatic reply is enabled
|
||||
* @return validated message text or {@code null} when no reply may be sent
|
||||
*/
|
||||
private String buildAutoAnswerMessageText(
|
||||
ChatMessage incoming,
|
||||
boolean qrgRequested,
|
||||
boolean genericEnabled
|
||||
) {
|
||||
if (incoming == null
|
||||
|| incoming.getSender() == null
|
||||
|| incoming.getSender().getCallSign() == null
|
||||
|| incoming.getSender().getCallSign().isBlank()) {
|
||||
|
||||
System.err.println(
|
||||
"KST4Contest auto-answer skipped: "
|
||||
+ "incoming message has no valid sender callsign."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
String payload;
|
||||
|
||||
if (qrgRequested) {
|
||||
String qrg = getAutoAnswerQrgForCategory(
|
||||
incoming.getChatCategory()
|
||||
);
|
||||
|
||||
if (qrg == null || qrg.isBlank()) {
|
||||
System.err.println(
|
||||
"KST4Contest QRG auto-answer skipped for "
|
||||
+ incoming.getSender().getCallSign()
|
||||
+ ": no QRG is available for chat category "
|
||||
+ autoAnswerCooldownKey(incoming)
|
||||
+ "."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
payload = "QRG is: " + qrg.trim();
|
||||
|
||||
} else if (genericEnabled) {
|
||||
payload = this.client.getChatPreferences()
|
||||
.getMessageHandling_autoAnswerTextMainCat();
|
||||
|
||||
if (payload == null || payload.isBlank()) {
|
||||
System.err.println(
|
||||
"KST4Contest generic auto-answer skipped for "
|
||||
+ incoming.getSender().getCallSign()
|
||||
+ ": the configured answer text is empty."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
payload = payload.trim();
|
||||
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
String messageText =
|
||||
"/CQ "
|
||||
+ incoming.getSender().getCallSign().trim()
|
||||
+ " "
|
||||
+ AUTOANSWER_PREFIX
|
||||
+ " "
|
||||
+ payload;
|
||||
|
||||
try {
|
||||
return On4KstProtocol.messageText(messageText);
|
||||
|
||||
} catch (IllegalArgumentException invalidMessage) {
|
||||
System.err.println(
|
||||
"KST4Contest auto-answer skipped for "
|
||||
+ incoming.getSender().getCallSign()
|
||||
+ ": "
|
||||
+ invalidMessage.getMessage()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a message carries the fixed marker used for automatic replies.
|
||||
*/
|
||||
@@ -1834,13 +2132,17 @@ public class MessageBusManagementThread extends Thread {
|
||||
while (true) {
|
||||
|
||||
try {
|
||||
messageTextRaw = client.getMessageRXBus().take();
|
||||
messageTextRaw = receiveQueue.take();
|
||||
|
||||
if (messageTextRaw.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL) && messageTextRaw.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
client.getMessageRXBus().clear();
|
||||
if (ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(messageTextRaw.getMessageText())
|
||||
&& ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(messageTextRaw.getMessageSenderName())) {
|
||||
receiveQueue.clear();
|
||||
break;
|
||||
}
|
||||
else {
|
||||
if (!connectionSessionIsActive.test(connectionSessionId)) {
|
||||
break;
|
||||
}
|
||||
messageLine = messageTextRaw.getMessageText();
|
||||
|
||||
/***********************************************
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import jdk.net.ExtendedSocketOptions;
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatMessage;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
/**
|
||||
* Owns the complete lifecycle of the single ON4KST TCP session.
|
||||
*
|
||||
* <p>Every reader, writer, queue and parser belongs to an immutable session id.
|
||||
* A delayed failure from an old socket can therefore never close or consume data
|
||||
* from its replacement.</p>
|
||||
*/
|
||||
final class On4KstConnectionManager {
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(On4KstConnectionManager.class.getName());
|
||||
private static final DateTimeFormatter LIVE_MESSAGE_TIMESTAMP =
|
||||
DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
|
||||
static final int CONNECT_TIMEOUT_MILLIS = 10_000; //TCP-Connect-Timeout
|
||||
static final long LOGIN_FALLBACK_MILLIS = 2_000L; //Login-Fallback
|
||||
static final long HANDSHAKE_TIMEOUT_MILLIS = 45_000L; //Handshake-Timeout
|
||||
static final long APPLICATION_HEARTBEAT_AFTER_MILLIS = 90_000L; //Application-Heartbeat
|
||||
static final long INBOUND_STALE_AFTER_MILLIS = 210_000L; //Stale-Timeout - time without rxed data
|
||||
static final List<Long> RECONNECT_DELAYS_MILLIS =
|
||||
List.of(2_000L, 5_000L, 10_000L, 20_000L, 30_000L); //Reconnect-Backoff if no connection possible
|
||||
|
||||
private final ChatController controller;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final AtomicLong generation = new AtomicLong();
|
||||
private final AtomicLong lastReceivedMessageTimestamp = new AtomicLong();
|
||||
|
||||
private volatile Session activeSession;
|
||||
private volatile On4KstConnectionState state =
|
||||
On4KstConnectionState.DISCONNECTED;
|
||||
private volatile boolean stopRequested = true;
|
||||
private int reconnectAttempt;
|
||||
|
||||
On4KstConnectionManager(ChatController controller) {
|
||||
this.controller = controller;
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
|
||||
Thread thread = new Thread(runnable, "On4KstConnectionSupervisor");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
this.scheduler.scheduleAtFixedRate(
|
||||
this::monitorActiveSession, 5L, 5L, TimeUnit.SECONDS);
|
||||
LOGGER.fine("ON4KST connection supervisor initialized");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last lifecycle state published by the connection supervisor.
|
||||
*
|
||||
* @return current immutable connection-state value
|
||||
*/
|
||||
On4KstConnectionState getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a callback still belongs to the currently installed session.
|
||||
*
|
||||
* <p>Every reconnect receives a new id. Late EOF, write or parser callbacks from
|
||||
* an obsolete socket therefore become harmless instead of closing the replacement
|
||||
* connection.</p>
|
||||
*
|
||||
* @param sessionId id captured by the calling worker
|
||||
* @return {@code true} only for the current, open and non-stopped session
|
||||
*/
|
||||
boolean isActiveSession(long sessionId) {
|
||||
Session session = activeSession;
|
||||
return session != null
|
||||
&& session.id == sessionId
|
||||
&& !session.closed
|
||||
&& !stopRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a non-blocking connection attempt.
|
||||
*
|
||||
* <p>Configuration is validated before a socket is opened. A duplicate Connect
|
||||
* action is ignored while another attempt or session is active. Connection work
|
||||
* runs on the supervisor executor, so an unreachable server cannot block the
|
||||
* JavaFX application thread.</p>
|
||||
*/
|
||||
void start() {
|
||||
long token;
|
||||
synchronized (this) {
|
||||
if (!stopRequested && state.isConnectionAttemptActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
validateConfiguration();
|
||||
} catch (IllegalArgumentException invalidConfiguration) {
|
||||
stopRequested = true;
|
||||
transition(On4KstConnectionState.DISCONNECTED,
|
||||
"Invalid ON4KST configuration: "
|
||||
+ invalidConfiguration.getMessage(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
stopRequested = false;
|
||||
reconnectAttempt = 0;
|
||||
token = generation.incrementAndGet();
|
||||
transition(On4KstConnectionState.CONNECTING,
|
||||
"Opening ON4KST connection", false);
|
||||
}
|
||||
|
||||
scheduler.execute(() -> openConnection(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current session and invalidates every scheduled callback or reconnect
|
||||
* belonging to it.
|
||||
*/
|
||||
void stopByUser() {
|
||||
Session oldSession;
|
||||
synchronized (this) {
|
||||
stopRequested = true;
|
||||
generation.incrementAndGet();
|
||||
transition(On4KstConnectionState.STOPPING,
|
||||
"Disconnecting from ON4KST", false);
|
||||
oldSession = activeSession;
|
||||
activeSession = null;
|
||||
}
|
||||
|
||||
closeSession(oldSession);
|
||||
controller.onOn4KstConnectionLost();
|
||||
transition(On4KstConnectionState.DISCONNECTED,
|
||||
"Disconnected by user", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Records one received protocol line as proof of application-level liveness.
|
||||
*
|
||||
* <p>TCP's {@code isConnected()} only states that a connection once succeeded.
|
||||
* It does not prove that the peer is still reachable. Updating the inbound
|
||||
* timestamp here gives the monitor a meaningful end-to-end signal.</p>
|
||||
*
|
||||
* @param sessionId immutable source-session id
|
||||
* @param line complete protocol line received from ON4KST
|
||||
*/
|
||||
void onInboundActivity(long sessionId, String line) {
|
||||
Session session = activeSession;
|
||||
if (session == null || session.id != sessionId || session.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
session.lastInboundMillis.set(now);
|
||||
session.lastProgressMillis.set(now);
|
||||
|
||||
String opcode = opcode(line);
|
||||
if ("CK".equals(opcode)) {
|
||||
sendHeartbeat(session);
|
||||
}
|
||||
|
||||
if (!session.loginSent
|
||||
&& line != null
|
||||
&& line.toLowerCase(Locale.ROOT).contains("login")) {
|
||||
scheduler.execute(() -> sendLogin(sessionId));
|
||||
}
|
||||
|
||||
if ("CH".equals(opcode) || "CR".equals(opcode)) {
|
||||
recordHistoryTimestamp(line);
|
||||
}
|
||||
}
|
||||
|
||||
void onLogstat(long sessionId, String[] fields) {
|
||||
String[] copy = fields == null ? new String[0] : fields.clone();
|
||||
scheduler.execute(() -> handleLogstat(sessionId, copy));
|
||||
}
|
||||
|
||||
void stageInitialChatMember(long sessionId, ChatMember member) {
|
||||
Session session = activeSession;
|
||||
if (session == null || session.id != sessionId || member == null
|
||||
|| member.getChatCategory() == null || member.getCallSign() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int category = member.getChatCategory().getCategoryNumber();
|
||||
session.initialMembers
|
||||
.computeIfAbsent(category, ignored -> new ConcurrentHashMap<>())
|
||||
.put(member.getCallSign().trim().toUpperCase(Locale.ROOT), member);
|
||||
session.lastProgressMillis.set(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
void onInitialUserListCompleted(long sessionId, ChatCategory category) {
|
||||
if (category == null) {
|
||||
return;
|
||||
}
|
||||
scheduler.execute(() -> completeInitialUserList(
|
||||
sessionId, category.getCategoryNumber()));
|
||||
}
|
||||
|
||||
private void openConnection(long token) {
|
||||
if (!mayOpen(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.log(Level.INFO,
|
||||
"Opening ON4KST TCP session {0}", token);
|
||||
Socket socket = new Socket();
|
||||
try {
|
||||
ChatPreferences preferences = controller.getChatPreferences();
|
||||
socket.connect(new InetSocketAddress(
|
||||
preferences.getStn_on4kstServersDns(),
|
||||
preferences.getStn_on4kstServersPort()),
|
||||
CONNECT_TIMEOUT_MILLIS);
|
||||
configureSocket(socket);
|
||||
LOGGER.log(Level.INFO,
|
||||
"ON4KST TCP session {0} connected to {1}",
|
||||
new Object[] {token, socket.getRemoteSocketAddress()});
|
||||
|
||||
LinkedBlockingQueue<ChatMessage> receiveQueue =
|
||||
new LinkedBlockingQueue<>();
|
||||
LinkedBlockingQueue<ChatMessage> transmitQueue =
|
||||
new LinkedBlockingQueue<>();
|
||||
Session session = new Session(token, socket, receiveQueue, transmitQueue);
|
||||
|
||||
ReadThread readThread = new ReadThread(
|
||||
token, socket, receiveQueue, this::isActiveSession,
|
||||
line -> onInboundActivity(token, line),
|
||||
failure -> onConnectionFailure(token, failure));
|
||||
WriteThread writeThread = new WriteThread(
|
||||
token, socket, transmitQueue,
|
||||
controller.getChatPreferences().getLoginChatCategoryMain()
|
||||
.getCategoryNumber(),
|
||||
this::isActiveSession,
|
||||
failure -> onConnectionFailure(token, failure),
|
||||
controller::onOn4KstOutboundFrameRejected);
|
||||
MessageBusManagementThread messageProcessor =
|
||||
new MessageBusManagementThread(
|
||||
controller, controller, token, receiveQueue,
|
||||
this::isActiveSession);
|
||||
|
||||
session.readThread = readThread;
|
||||
session.writeThread = writeThread;
|
||||
session.messageProcessor = messageProcessor;
|
||||
|
||||
synchronized (this) {
|
||||
if (!mayOpen(token)) {
|
||||
closeSession(session);
|
||||
return;
|
||||
}
|
||||
activeSession = session;
|
||||
controller.installOn4KstSession(
|
||||
token, socket, receiveQueue, transmitQueue,
|
||||
readThread, writeThread, messageProcessor);
|
||||
transition(On4KstConnectionState.WAITING_FOR_LOGIN_PROMPT,
|
||||
"TCP connected; waiting for ON4KST login prompt", false);
|
||||
}
|
||||
|
||||
messageProcessor.start();
|
||||
writeThread.start();
|
||||
readThread.start();
|
||||
scheduler.schedule(
|
||||
() -> sendLogin(token), LOGIN_FALLBACK_MILLIS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
} catch (Throwable exception) {
|
||||
// Errors must be caught as well: an Error escaping here would be
|
||||
// swallowed by the scheduler and leave the state machine stuck in
|
||||
// CONNECTING without any reconnect attempt or user visible failure.
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignored) {
|
||||
// The original connection exception is more useful.
|
||||
}
|
||||
scheduler.execute(() -> handleOpenFailure(token, exception));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean mayOpen(long token) {
|
||||
return !stopRequested && generation.get() == token;
|
||||
}
|
||||
|
||||
private void sendLogin(long sessionId) {
|
||||
Session session = activeSession;
|
||||
if (session == null || session.id != sessionId || session.loginSent
|
||||
|| session.closed || stopRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
ChatPreferences preferences = controller.getChatPreferences();
|
||||
int mainCategory = preferences.getLoginChatCategoryMain()
|
||||
.getCategoryNumber();
|
||||
long historyFrom = Math.max(
|
||||
0L, lastReceivedMessageTimestamp.get() - 1L);
|
||||
String login = On4KstProtocol.login(
|
||||
preferences.getStn_loginCallSign(),
|
||||
preferences.getStn_loginPassword(),
|
||||
mainCategory,
|
||||
"KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION,
|
||||
historyFrom);
|
||||
|
||||
session.loginSent = true;
|
||||
session.lastProgressMillis.set(System.currentTimeMillis());
|
||||
LOGGER.log(Level.INFO,
|
||||
"Sending ON4KST login for session {0}, main category {1}",
|
||||
new Object[] {sessionId, mainCategory});
|
||||
transition(On4KstConnectionState.AUTHENTICATING,
|
||||
"ON4KST login sent", false);
|
||||
sendControl(session, login);
|
||||
} catch (IllegalArgumentException invalidConfiguration) {
|
||||
failPermanently(session,
|
||||
"Invalid ON4KST login configuration: "
|
||||
+ invalidConfiguration.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLogstat(long sessionId, String[] fields) {
|
||||
Session session = activeSession;
|
||||
if (session == null || session.id != sessionId || session.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
String code = fields.length > 1 ? fields[1] : "";
|
||||
if (!"100".equals(code)) {
|
||||
String serverText = fields.length > 2 ? fields[2] : "Login rejected";
|
||||
failPermanently(session,
|
||||
"ON4KST login rejected (" + code + "): " + serverText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.authenticated) {
|
||||
return;
|
||||
}
|
||||
session.authenticated = true;
|
||||
session.lastProgressMillis.set(System.currentTimeMillis());
|
||||
LOGGER.log(Level.INFO,
|
||||
"ON4KST login accepted for session {0}", sessionId);
|
||||
|
||||
int mainCategory = controller.getChatPreferences()
|
||||
.getLoginChatCategoryMain().getCategoryNumber();
|
||||
transition(On4KstConnectionState.SYNCING_MAIN_CHAT,
|
||||
"Login accepted; loading main chat", false);
|
||||
sendControl(session, On4KstProtocol.settingsDone(mainCategory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the initial user snapshot for one chat category exactly once.
|
||||
*
|
||||
* <p>ON4KST can send further {@code UE} frames after live user updates or
|
||||
* after commands such as {@code SETNAME} and {@code BACK}. Those frames do
|
||||
* not announce a new, empty snapshot. Treating them as another initial-list
|
||||
* completion would remove the already published members because the staging
|
||||
* map was consumed by the first {@code UE} frame.</p>
|
||||
*
|
||||
* <p>The completed-category set is updated before the staging map is removed.
|
||||
* This makes the operation idempotent even if completion callbacks should
|
||||
* later be invoked from more than one thread. A genuinely empty initial list
|
||||
* remains valid: the first {@code UE} for a category is always processed,
|
||||
* even when no preceding valid {@code UA0} frame was staged.</p>
|
||||
*
|
||||
* @param sessionId immutable id of the socket session that received the frame
|
||||
* @param categoryNumber numeric ON4KST category terminated by {@code UE}
|
||||
*/
|
||||
private void completeInitialUserList(long sessionId, int categoryNumber) {
|
||||
Session session = activeSession;
|
||||
if (session == null || session.id != sessionId || session.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!session.completedInitialUserLists.add(categoryNumber)) {
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST session {0}: ignoring duplicate user-list end "
|
||||
+ "marker for category {1}; the initial snapshot "
|
||||
+ "has already been published",
|
||||
new Object[] {sessionId, categoryNumber});
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, ChatMember> staged =
|
||||
session.initialMembers.remove(categoryNumber);
|
||||
Collection<ChatMember> completeMembers = staged == null
|
||||
? List.of()
|
||||
: new ArrayList<>(staged.values());
|
||||
|
||||
LOGGER.log(Level.INFO,
|
||||
"ON4KST session {0}: complete user list for category {1} "
|
||||
+ "contains {2} valid users",
|
||||
new Object[] {
|
||||
sessionId,
|
||||
categoryNumber,
|
||||
completeMembers.size()
|
||||
});
|
||||
|
||||
controller.replaceActiveChatMembersForCategory(
|
||||
sessionId,
|
||||
new ChatCategory(categoryNumber),
|
||||
completeMembers);
|
||||
|
||||
ChatPreferences preferences = controller.getChatPreferences();
|
||||
int mainCategory =
|
||||
preferences.getLoginChatCategoryMain().getCategoryNumber();
|
||||
|
||||
if (categoryNumber == mainCategory && !session.mainListComplete) {
|
||||
session.mainListComplete = true;
|
||||
configureMainChat(session);
|
||||
|
||||
if (hasDistinctSecondChat(preferences)) {
|
||||
int secondCategory =
|
||||
preferences.getLoginChatCategorySecond()
|
||||
.getCategoryNumber();
|
||||
|
||||
transition(
|
||||
On4KstConnectionState.SYNCING_SECOND_CHAT,
|
||||
"Main chat ready; loading second chat",
|
||||
false);
|
||||
|
||||
sendControl(
|
||||
session,
|
||||
On4KstProtocol.addChat(
|
||||
secondCategory,
|
||||
Math.max(
|
||||
0L,
|
||||
lastReceivedMessageTimestamp.get() - 1L)));
|
||||
} else {
|
||||
markOnline(session);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasDistinctSecondChat(preferences)
|
||||
&& categoryNumber
|
||||
== preferences.getLoginChatCategorySecond()
|
||||
.getCategoryNumber()
|
||||
&& !session.secondListComplete) {
|
||||
session.secondListComplete = true;
|
||||
configureSecondChat(session);
|
||||
markOnline(session);
|
||||
}
|
||||
}
|
||||
|
||||
private void configureMainChat(Session session) {
|
||||
ChatPreferences preferences = controller.getChatPreferences();
|
||||
int category = preferences.getLoginChatCategoryMain().getCategoryNumber();
|
||||
sendControl(session, On4KstProtocol.setLocator(
|
||||
category, preferences.getStn_loginLocatorMainCat()));
|
||||
if (preferences.getStn_loginNameMainCat() != null
|
||||
&& !preferences.getStn_loginNameMainCat().isBlank()) {
|
||||
sendControl(session, On4KstProtocol.setName(
|
||||
category, preferences.getStn_loginNameMainCat()));
|
||||
}
|
||||
sendControl(session, On4KstProtocol.back(category));
|
||||
|
||||
String secondLocator = preferences.getStn_loginLocatorSecondCat();
|
||||
String mainLocator = preferences.getStn_loginLocatorMainCat();
|
||||
if (preferences.isLoginToSecondChatEnabled()
|
||||
&& secondLocator != null && !secondLocator.isBlank()
|
||||
&& !secondLocator.equalsIgnoreCase(mainLocator)) {
|
||||
controller.onOn4KstConnectionWarning(
|
||||
"ON4KST uses one locator per TCP session. The second-chat locator '"
|
||||
+ secondLocator + "' is ignored; using '" + mainLocator + "'.");
|
||||
}
|
||||
}
|
||||
|
||||
private void configureSecondChat(Session session) {
|
||||
ChatPreferences preferences = controller.getChatPreferences();
|
||||
int category = preferences.getLoginChatCategorySecond().getCategoryNumber();
|
||||
if (preferences.getStn_loginNameSecondCat() != null
|
||||
&& !preferences.getStn_loginNameSecondCat().isBlank()) {
|
||||
sendControl(session, On4KstProtocol.setName(
|
||||
category, preferences.getStn_loginNameSecondCat()));
|
||||
}
|
||||
sendControl(session, On4KstProtocol.back(category));
|
||||
}
|
||||
|
||||
private void markOnline(Session session) {
|
||||
if (!isActiveSession(session.id)) {
|
||||
return;
|
||||
}
|
||||
reconnectAttempt = 0;
|
||||
session.online = true;
|
||||
session.lastProgressMillis.set(System.currentTimeMillis());
|
||||
LOGGER.log(Level.INFO,
|
||||
"ON4KST session {0} is authenticated and synchronized",
|
||||
session.id);
|
||||
transition(On4KstConnectionState.ONLINE,
|
||||
"ON4KST session is authenticated and synchronized", false);
|
||||
controller.onOn4KstConnectionOnline();
|
||||
}
|
||||
|
||||
private void sendControl(Session session, String frame) {
|
||||
if (session == null || !isActiveSession(session.id)) {
|
||||
return;
|
||||
}
|
||||
ChatMessage message = new ChatMessage();
|
||||
message.setMessageDirectedToServer(true);
|
||||
message.setMessageText(frame);
|
||||
session.transmitQueue.offer(message);
|
||||
}
|
||||
|
||||
private void sendHeartbeat(Session session) {
|
||||
if (session == null || !isActiveSession(session.id)) {
|
||||
return;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
session.lastHeartbeatMillis.set(now);
|
||||
LOGGER.log(Level.FINE,
|
||||
"Sending application heartbeat for ON4KST session {0}",
|
||||
session.id);
|
||||
ChatMessage heartbeat = new ChatMessage();
|
||||
heartbeat.setMessageDirectedToServer(true);
|
||||
heartbeat.setMessageText("");
|
||||
session.transmitQueue.offer(heartbeat);
|
||||
}
|
||||
|
||||
private void onConnectionFailure(long sessionId, Throwable failure) {
|
||||
scheduler.execute(() -> failSession(sessionId, failure));
|
||||
}
|
||||
|
||||
private void failSession(long sessionId, Throwable failure) {
|
||||
Session failedSession;
|
||||
synchronized (this) {
|
||||
failedSession = activeSession;
|
||||
if (failedSession == null || failedSession.id != sessionId
|
||||
|| failedSession.closed) {
|
||||
return;
|
||||
}
|
||||
activeSession = null;
|
||||
failedSession.closed = true;
|
||||
}
|
||||
|
||||
closeSession(failedSession);
|
||||
controller.onOn4KstConnectionLost();
|
||||
if (stopRequested) {
|
||||
transition(On4KstConnectionState.DISCONNECTED,
|
||||
"ON4KST connection stopped", false);
|
||||
return;
|
||||
}
|
||||
|
||||
scheduleReconnect(failure);
|
||||
}
|
||||
|
||||
private void handleOpenFailure(long token, Throwable failure) {
|
||||
if (!mayOpen(token)) {
|
||||
return;
|
||||
}
|
||||
controller.onOn4KstConnectionLost();
|
||||
scheduleReconnect(failure);
|
||||
}
|
||||
|
||||
private void scheduleReconnect(Throwable failure) {
|
||||
if (stopRequested) {
|
||||
transition(On4KstConnectionState.DISCONNECTED,
|
||||
"ON4KST connection stopped", false);
|
||||
return;
|
||||
}
|
||||
|
||||
String reason = describeFailure(failure);
|
||||
LOGGER.log(Level.WARNING,
|
||||
"ON4KST connection lost; automatic reconnect scheduled", failure);
|
||||
long delay = RECONNECT_DELAYS_MILLIS.get(Math.min(
|
||||
reconnectAttempt, RECONNECT_DELAYS_MILLIS.size() - 1));
|
||||
reconnectAttempt++;
|
||||
transition(On4KstConnectionState.RECONNECT_WAIT,
|
||||
"Connection lost (" + reason + "); reconnecting in "
|
||||
+ Duration.ofMillis(delay).toSeconds() + " s", true);
|
||||
|
||||
long nextToken = generation.incrementAndGet();
|
||||
scheduler.schedule(() -> {
|
||||
if (!mayOpen(nextToken)) {
|
||||
return;
|
||||
}
|
||||
transition(On4KstConnectionState.CONNECTING,
|
||||
"Reconnecting to ON4KST", false);
|
||||
openConnection(nextToken);
|
||||
}, delay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void failPermanently(Session session, String reason) {
|
||||
if (session == null || !isActiveSession(session.id)) {
|
||||
return;
|
||||
}
|
||||
stopRequested = true;
|
||||
generation.incrementAndGet();
|
||||
activeSession = null;
|
||||
session.closed = true;
|
||||
closeSession(session);
|
||||
controller.onOn4KstConnectionLost();
|
||||
LOGGER.log(Level.WARNING, reason);
|
||||
transition(On4KstConnectionState.DISCONNECTED, reason, true);
|
||||
}
|
||||
|
||||
private void monitorActiveSession() {
|
||||
try {
|
||||
Session session = activeSession;
|
||||
if (session == null || session.closed || stopRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.socket.isClosed()) {
|
||||
failSession(session.id,
|
||||
new SocketException("Socket is closed"));
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
if (!session.online
|
||||
&& now - session.lastProgressMillis.get()
|
||||
> HANDSHAKE_TIMEOUT_MILLIS) {
|
||||
failSession(session.id,
|
||||
new SocketException("ON4KST handshake timed out"));
|
||||
return;
|
||||
}
|
||||
|
||||
long inboundIdle = now - session.lastInboundMillis.get();
|
||||
if (inboundIdle > INBOUND_STALE_AFTER_MILLIS) {
|
||||
failSession(session.id,
|
||||
new SocketException("No ON4KST data received for "
|
||||
+ inboundIdle / 1_000L + " seconds"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (inboundIdle > APPLICATION_HEARTBEAT_AFTER_MILLIS
|
||||
&& session.lastHeartbeatMillis.get()
|
||||
< session.lastInboundMillis.get()) {
|
||||
sendHeartbeat(session);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
LOGGER.log(Level.WARNING,
|
||||
"ON4KST connection monitor failed", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateConfiguration() {
|
||||
ChatPreferences preferences = controller.getChatPreferences();
|
||||
On4KstProtocol.login(
|
||||
preferences.getStn_loginCallSign(),
|
||||
preferences.getStn_loginPassword(),
|
||||
preferences.getLoginChatCategoryMain().getCategoryNumber(),
|
||||
"KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION,
|
||||
0L);
|
||||
On4KstProtocol.locator(preferences.getStn_loginLocatorMainCat());
|
||||
if (preferences.getStn_loginNameMainCat() != null
|
||||
&& !preferences.getStn_loginNameMainCat().isBlank()) {
|
||||
On4KstProtocol.field(
|
||||
preferences.getStn_loginNameMainCat(), "main chat name");
|
||||
}
|
||||
|
||||
if (preferences.isLoginToSecondChatEnabled()) {
|
||||
if (preferences.getLoginChatCategorySecond() == null) {
|
||||
throw new IllegalArgumentException("Second chat has no category");
|
||||
}
|
||||
On4KstProtocol.category(
|
||||
preferences.getLoginChatCategorySecond().getCategoryNumber());
|
||||
if (preferences.getLoginChatCategorySecond().getCategoryNumber()
|
||||
== preferences.getLoginChatCategoryMain().getCategoryNumber()) {
|
||||
controller.onOn4KstConnectionWarning(
|
||||
"Second ON4KST chat equals the main chat and will not be added twice.");
|
||||
}
|
||||
if (preferences.getStn_loginNameSecondCat() != null
|
||||
&& !preferences.getStn_loginNameSecondCat().isBlank()) {
|
||||
On4KstProtocol.field(
|
||||
preferences.getStn_loginNameSecondCat(), "second chat name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasDistinctSecondChat(ChatPreferences preferences) {
|
||||
return preferences.isLoginToSecondChatEnabled()
|
||||
&& preferences.getLoginChatCategorySecond() != null
|
||||
&& preferences.getLoginChatCategorySecond().getCategoryNumber()
|
||||
!= preferences.getLoginChatCategoryMain().getCategoryNumber();
|
||||
}
|
||||
|
||||
private void configureSocket(Socket socket) throws IOException {
|
||||
socket.setTcpNoDelay(true);
|
||||
socket.setKeepAlive(true);
|
||||
|
||||
try {
|
||||
socket.setOption(ExtendedSocketOptions.TCP_KEEPIDLE, 45);
|
||||
socket.setOption(ExtendedSocketOptions.TCP_KEEPINTERVAL, 15);
|
||||
socket.setOption(ExtendedSocketOptions.TCP_KEEPCOUNT, 3);
|
||||
} catch (UnsupportedOperationException | IOException | LinkageError exception) {
|
||||
// LinkageError covers runtime images built without the jdk.net module;
|
||||
// the connection stays usable, only kernel side keepalive is missing.
|
||||
LOGGER.log(Level.INFO,
|
||||
"Platform does not support configurable TCP keepalive; "
|
||||
+ "application heartbeat remains active", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeSession(Session session) {
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
LOGGER.log(Level.FINE,
|
||||
"Closing ON4KST session {0}", session.id);
|
||||
session.closed = true;
|
||||
if (session.readThread != null) {
|
||||
session.readThread.interrupt();
|
||||
}
|
||||
if (session.writeThread != null) {
|
||||
session.writeThread.interrupt();
|
||||
}
|
||||
if (session.messageProcessor != null) {
|
||||
session.messageProcessor.interrupt();
|
||||
}
|
||||
try {
|
||||
session.socket.close();
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(Level.FINE, "Error closing obsolete ON4KST socket", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void transition(
|
||||
On4KstConnectionState newState,
|
||||
String detail,
|
||||
boolean critical
|
||||
) {
|
||||
On4KstConnectionState previousState = state;
|
||||
state = newState;
|
||||
Level level = critical
|
||||
|| newState == On4KstConnectionState.DISCONNECTED
|
||||
|| newState == On4KstConnectionState.RECONNECT_WAIT
|
||||
? Level.WARNING : Level.INFO;
|
||||
LOGGER.log(level,
|
||||
"ON4KST state {0} -> {1}; detail: {2}",
|
||||
new Object[] {previousState, newState, detail});
|
||||
controller.updateOn4KstConnectionState(newState, detail, critical);
|
||||
}
|
||||
|
||||
private void recordHistoryTimestamp(String line) {
|
||||
long timestamp = parseMessageTimestamp(line);
|
||||
if (timestamp > 0L) {
|
||||
lastReceivedMessageTimestamp.accumulateAndGet(timestamp, Math::max);
|
||||
}
|
||||
}
|
||||
|
||||
static long parseMessageTimestamp(String line) {
|
||||
String[] fields = line == null ? new String[0] : line.split("\\|", -1);
|
||||
if (fields.length < 3) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
try {
|
||||
long numeric = Long.parseLong(fields[2]);
|
||||
long now = System.currentTimeMillis() / 1_000L;
|
||||
if (numeric > 0L && numeric <= now + 86_400L) {
|
||||
return numeric;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
try {
|
||||
return LocalDateTime.parse(fields[2], LIVE_MESSAGE_TIMESTAMP)
|
||||
.toEpochSecond(ZoneOffset.UTC);
|
||||
} catch (DateTimeParseException ignored) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
private String opcode(String line) {
|
||||
if (line == null) {
|
||||
return "";
|
||||
}
|
||||
int separator = line.indexOf('|');
|
||||
return (separator < 0 ? line : line.substring(0, separator))
|
||||
.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String describeFailure(Throwable failure) {
|
||||
if (failure == null) {
|
||||
return "unknown error";
|
||||
}
|
||||
String message = failure.getMessage();
|
||||
return message == null || message.isBlank()
|
||||
? failure.getClass().getSimpleName() : message;
|
||||
}
|
||||
|
||||
private static final class Session {
|
||||
private final Set<Integer> completedInitialUserLists =
|
||||
ConcurrentHashMap.newKeySet();
|
||||
|
||||
private final long id;
|
||||
private final Socket socket;
|
||||
private final LinkedBlockingQueue<ChatMessage> receiveQueue;
|
||||
private final LinkedBlockingQueue<ChatMessage> transmitQueue;
|
||||
private final long connectedMillis = System.currentTimeMillis();
|
||||
private final AtomicLong lastInboundMillis =
|
||||
new AtomicLong(connectedMillis);
|
||||
private final AtomicLong lastProgressMillis =
|
||||
new AtomicLong(connectedMillis);
|
||||
private final AtomicLong lastHeartbeatMillis = new AtomicLong();
|
||||
private final Map<Integer, Map<String, ChatMember>> initialMembers =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private volatile ReadThread readThread;
|
||||
private volatile WriteThread writeThread;
|
||||
private volatile MessageBusManagementThread messageProcessor;
|
||||
private volatile boolean loginSent;
|
||||
private volatile boolean authenticated;
|
||||
private volatile boolean mainListComplete;
|
||||
private volatile boolean secondListComplete;
|
||||
private volatile boolean online;
|
||||
private volatile boolean closed;
|
||||
|
||||
private Session(
|
||||
long id,
|
||||
Socket socket,
|
||||
LinkedBlockingQueue<ChatMessage> receiveQueue,
|
||||
LinkedBlockingQueue<ChatMessage> transmitQueue
|
||||
) {
|
||||
this.id = id;
|
||||
this.socket = socket;
|
||||
this.receiveQueue = receiveQueue;
|
||||
this.transmitQueue = transmitQueue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
/**
|
||||
* Observable lifecycle of the ON4KST TCP session.
|
||||
*
|
||||
* <p>A connected TCP socket is deliberately not synonymous with an authenticated
|
||||
* chat session. The intermediate states make that distinction visible to the UI
|
||||
* and prevent application messages from being sent in the wrong protocol context.</p>
|
||||
*/
|
||||
public enum On4KstConnectionState {
|
||||
DISCONNECTED,
|
||||
CONNECTING,
|
||||
WAITING_FOR_LOGIN_PROMPT,
|
||||
AUTHENTICATING,
|
||||
SYNCING_MAIN_CHAT,
|
||||
SYNCING_SECOND_CHAT,
|
||||
ONLINE,
|
||||
RECONNECT_WAIT,
|
||||
STOPPING;
|
||||
|
||||
/**
|
||||
* Returns whether the complete application-level ON4KST handshake has finished.
|
||||
*
|
||||
* @return {@code true} only after authentication and all requested user lists
|
||||
* have been synchronized
|
||||
*/
|
||||
public boolean isOnline() {
|
||||
return this == ONLINE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a connection attempt or usable session is currently owned by
|
||||
* the connection manager.
|
||||
*
|
||||
* <p>This is intentionally broader than {@link #isOnline()}. The UI uses it to
|
||||
* prevent a second Connect action while authentication, synchronization or a
|
||||
* scheduled reconnect is already in progress.</p>
|
||||
*
|
||||
* @return {@code true} while connecting, synchronizing, online or waiting for
|
||||
* an automatic reconnect
|
||||
*/
|
||||
public boolean isConnectionAttemptActive() {
|
||||
return switch (this) {
|
||||
case CONNECTING, WAITING_FOR_LOGIN_PROMPT, AUTHENTICATING,
|
||||
SYNCING_MAIN_CHAT, SYNCING_SECOND_CHAT, ONLINE,
|
||||
RECONNECT_WAIT -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Builds ON4KST port-23001 frames and rejects values that could break framing or
|
||||
* put the server into an invalid chat context.
|
||||
*
|
||||
* <p>All outbound protocol construction is concentrated here. User-controlled
|
||||
* values may therefore never introduce a field separator or a second line, and
|
||||
* category and locator validation happens before the frame reaches the socket.</p>
|
||||
*/
|
||||
final class On4KstProtocol {
|
||||
private static final Pattern LOCATOR_6 =
|
||||
Pattern.compile("^[A-Ra-r]{2}[0-9]{2}[A-Xa-x]{2}$");
|
||||
|
||||
private On4KstProtocol() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the initial authenticated login frame.
|
||||
*
|
||||
* @param callsign login callsign
|
||||
* @param password ON4KST password; never logged by this class
|
||||
* @param category primary chat category
|
||||
* @param clientName client identification sent to the server
|
||||
* @param lastMessageTimestamp earliest history timestamp to request
|
||||
* @return validated frame without CR/LF terminator
|
||||
*/
|
||||
static String login(
|
||||
String callsign,
|
||||
String password,
|
||||
int category,
|
||||
String clientName,
|
||||
long lastMessageTimestamp
|
||||
) {
|
||||
return "LOGINC|" + field(callsign, "callsign")
|
||||
+ "|" + password(password)
|
||||
+ "|" + category(category)
|
||||
+ "|" + field(clientName, "client name")
|
||||
+ "|25|0|1|" + Math.max(0L, lastMessageTimestamp) + "|0|";
|
||||
}
|
||||
|
||||
/** Builds the settings-complete frame for the supplied chat category. */
|
||||
static String settingsDone(int category) {
|
||||
return "SDONE|" + category(category) + "|";
|
||||
}
|
||||
|
||||
/** Builds the frame used to add a distinct second chat to the same session. */
|
||||
static String addChat(int category, long lastMessageTimestamp) {
|
||||
return "ACHAT|" + category(category)
|
||||
+ "|25|10|2|" + Math.max(0L, lastMessageTimestamp)
|
||||
+ "|0|";
|
||||
}
|
||||
|
||||
/** Builds a category-qualified locator command after validating Maidenhead syntax. */
|
||||
static String setLocator(int category, String locator) {
|
||||
return command(category, "/SETLOC " + locator(locator));
|
||||
}
|
||||
|
||||
/** Builds a category-qualified chat-name command. */
|
||||
static String setName(int category, String name) {
|
||||
return command(category, "/SETNAME " + field(name, "chat name"));
|
||||
}
|
||||
|
||||
/** Builds the command that changes the operator state back to available. */
|
||||
static String back(int category) {
|
||||
return command(category, "/BACK");
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps one validated slash command in an ON4KST message frame.
|
||||
*
|
||||
* @return frame without CR/LF terminator
|
||||
*/
|
||||
static String command(int category, String command) {
|
||||
return "MSG|" + category(category) + "|0|"
|
||||
+ messageText(command) + "|0|";
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps one operator chat message in a category-qualified ON4KST frame.
|
||||
*
|
||||
* @return frame without CR/LF terminator
|
||||
*/
|
||||
static String chatMessage(int category, String text) {
|
||||
return "MSG|" + category(category) + "|0|"
|
||||
+ messageText(text) + "|0|";
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes trailing line terminators from a legacy raw frame while rejecting an
|
||||
* embedded line break that could inject a second server command.
|
||||
*
|
||||
* @param frame legacy raw frame, possibly with trailing CR/LF
|
||||
* @return exactly one normalized protocol line
|
||||
* @throws IllegalArgumentException if the value is {@code null} or contains an
|
||||
* embedded line break
|
||||
*/
|
||||
static String normalizeRawFrame(String frame) {
|
||||
if (frame == null) {
|
||||
throw new IllegalArgumentException("ON4KST frame must not be null");
|
||||
}
|
||||
|
||||
int end = frame.length();
|
||||
while (end > 0) {
|
||||
char last = frame.charAt(end - 1);
|
||||
if (last != '\r' && last != '\n') {
|
||||
break;
|
||||
}
|
||||
end--;
|
||||
}
|
||||
|
||||
String normalized = frame.substring(0, end);
|
||||
if (normalized.indexOf('\r') >= 0 || normalized.indexOf('\n') >= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"ON4KST frame contains an embedded line break");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and normalizes a six-character Maidenhead locator.
|
||||
*
|
||||
* @return upper-case locator
|
||||
*/
|
||||
static String locator(String locator) {
|
||||
String normalized = field(locator, "locator").toUpperCase(Locale.ROOT);
|
||||
if (!LOCATOR_6.matcher(normalized).matches()) {
|
||||
throw new IllegalArgumentException(
|
||||
"Locator must be a six-character Maidenhead locator: " + normalized);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** Rejects message text containing an ON4KST field or line delimiter. */
|
||||
static String messageText(String text) {
|
||||
String value = field(text, "message text");
|
||||
if (value.indexOf('|') >= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Message text contains the ON4KST field separator '|'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates one required, non-password protocol field.
|
||||
*
|
||||
* @param value field value
|
||||
* @param label diagnostic label used in validation errors
|
||||
* @return trimmed value
|
||||
*/
|
||||
static String field(String value, String label) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(label + " must not be empty");
|
||||
}
|
||||
if (value.indexOf('|') >= 0
|
||||
|| value.indexOf('\r') >= 0
|
||||
|| value.indexOf('\n') >= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
label + " contains an ON4KST frame delimiter");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String password(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
throw new IllegalArgumentException("password must not be empty");
|
||||
}
|
||||
if (value.indexOf('|') >= 0
|
||||
|| value.indexOf('\r') >= 0
|
||||
|| value.indexOf('\n') >= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"password contains an ON4KST frame delimiter");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the category range supported by ON4KST.
|
||||
*
|
||||
* @return the unchanged category for convenient inline use
|
||||
*/
|
||||
static int category(int category) {
|
||||
if (category < 1 || category > 12) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported ON4KST chat category: " + category);
|
||||
}
|
||||
return category;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
|
||||
class On4KstProtocolTest {
|
||||
@Test
|
||||
void buildsLoginWithReplayOverlap() {
|
||||
assertEquals(
|
||||
"LOGINC|DL1ABC|secret|2|KST4Contest v1.2.3|25|0|1|12344|0|",
|
||||
On4KstProtocol.login(
|
||||
"DL1ABC", "secret", 2,
|
||||
"KST4Contest v1.2.3", 12_344L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildsContextSafeSecondChatFrames() {
|
||||
assertEquals("SDONE|2|", On4KstProtocol.settingsDone(2));
|
||||
assertEquals("ACHAT|3|25|10|2|100|0|",
|
||||
On4KstProtocol.addChat(3, 100L));
|
||||
assertEquals("MSG|2|0|/SETLOC JO31AA|0|",
|
||||
On4KstProtocol.setLocator(2, "jo31aa"));
|
||||
assertEquals("MSG|3|0|/SETNAME 10G 10368.200|0|",
|
||||
On4KstProtocol.setName(3, "10G 10368.200"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stripsOnlyTrailingLineEndings() {
|
||||
assertEquals("CK|", On4KstProtocol.normalizeRawFrame("CK|\r\n"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> On4KstProtocol.normalizeRawFrame("CK|\rBROKEN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsValuesThatCouldCreateASecondProtocolFrame() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> On4KstProtocol.chatMessage(2, "hello|0|"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> On4KstProtocol.chatMessage(2, "hello\r\nQUIT|"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> On4KstProtocol.login(
|
||||
"DL1ABC", "bad|password", 2, "client", 0L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsInvalidLocatorAndCategoryBeforeTheyReachTheServer() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> On4KstProtocol.setLocator(2, "JO31"));
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> On4KstProtocol.settingsDone(99));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsBothHistoryAndLiveMessageTimestampsForReconnect() {
|
||||
assertEquals(1_186_819_108L,
|
||||
On4KstConnectionManager.parseMessageTimestamp(
|
||||
"CR|2|1186819108|EA6VQ|Gabriel|0|msg|0|"));
|
||||
assertEquals(
|
||||
LocalDateTime.of(2026, 8, 13, 12, 34, 56)
|
||||
.toEpochSecond(ZoneOffset.UTC),
|
||||
On4KstConnectionManager.parseMessageTimestamp(
|
||||
"CH|2|20260813123456|DL1ABC|Op|0|msg|0|"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesBeaconVariablesBeforeApplyingProtocolValidation() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
preferences.setMYQRGFirstCat("144.300");
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setChatPreferences(preferences);
|
||||
|
||||
controller.validateBeaconTemplate(
|
||||
"calling cq at MYQRG"
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"calling cq at 144.300",
|
||||
controller.resolveAndValidateBeaconText(
|
||||
"calling cq at MYQRG"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsTemporarilyUnresolvedVariableOnlyBeaconTemplate() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
preferences.setMYQRGFirstCat("");
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setChatPreferences(preferences);
|
||||
|
||||
assertDoesNotThrow(
|
||||
() -> controller.validateBeaconTemplate("MYQRG")
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.resolveAndValidateBeaconText("MYQRG")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsEmptyOverlongAndProtocolBreakingBeaconText() {
|
||||
ChatPreferences preferences = new ChatPreferences();
|
||||
|
||||
ChatController controller = new ChatController();
|
||||
controller.setChatPreferences(preferences);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate(" ")
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate(
|
||||
"cq at 144.300|0|QUIT"
|
||||
)
|
||||
);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate(
|
||||
"x".repeat(
|
||||
ChatController.MAX_BEACON_TEXT_LENGTH
|
||||
+ 1
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
class On4KstSocketThreadTest {
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void eofIsReportedImmediately() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
CompletableFuture<Void> serverDone = CompletableFuture.runAsync(() -> {
|
||||
try (Socket accepted = server.accept();
|
||||
OutputStreamWriter out = new OutputStreamWriter(
|
||||
accepted.getOutputStream(), StandardCharsets.UTF_8)) {
|
||||
out.write("CK|\r\n");
|
||||
out.flush();
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
});
|
||||
|
||||
try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {
|
||||
LinkedBlockingQueue<ChatMessage> queue = new LinkedBlockingQueue<>();
|
||||
AtomicBoolean active = new AtomicBoolean(true);
|
||||
CompletableFuture<Throwable> failure = new CompletableFuture<>();
|
||||
ReadThread reader = new ReadThread(
|
||||
7L, client, queue, ignored -> active.get(), ignored -> { },
|
||||
failure::complete);
|
||||
reader.start();
|
||||
|
||||
assertEquals("CK|", queue.poll(2, TimeUnit.SECONDS).getMessageText());
|
||||
failure.get(2, TimeUnit.SECONDS);
|
||||
active.set(false);
|
||||
reader.join(Duration.ofSeconds(2).toMillis());
|
||||
}
|
||||
serverDone.get(2, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Timeout(5)
|
||||
void writerUsesOneExactCrLfPerFrameIncludingHeartbeat() throws Exception {
|
||||
try (ServerSocket server = new ServerSocket(0)) {
|
||||
CompletableFuture<String> firstLine = new CompletableFuture<>();
|
||||
CompletableFuture<String> secondLine = new CompletableFuture<>();
|
||||
CompletableFuture<Void> serverDone = CompletableFuture.runAsync(() -> {
|
||||
try (Socket accepted = server.accept();
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(
|
||||
accepted.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
firstLine.complete(in.readLine());
|
||||
secondLine.complete(in.readLine());
|
||||
} catch (Exception exception) {
|
||||
throw new RuntimeException(exception);
|
||||
}
|
||||
});
|
||||
|
||||
try (Socket client = new Socket("127.0.0.1", server.getLocalPort())) {
|
||||
LinkedBlockingQueue<ChatMessage> queue = new LinkedBlockingQueue<>();
|
||||
AtomicBoolean active = new AtomicBoolean(true);
|
||||
WriteThread writer = new WriteThread(
|
||||
11L, client, queue, 2, ignored -> active.get(),
|
||||
ignored -> { }, ignored -> { });
|
||||
writer.start();
|
||||
|
||||
queue.add(serverFrame(""));
|
||||
queue.add(serverFrame("SDONE|2|\r"));
|
||||
assertEquals("", firstLine.get(2, TimeUnit.SECONDS));
|
||||
assertEquals("SDONE|2|", secondLine.get(2, TimeUnit.SECONDS));
|
||||
|
||||
active.set(false);
|
||||
writer.interrupt();
|
||||
writer.join(Duration.ofSeconds(2).toMillis());
|
||||
}
|
||||
serverDone.get(2, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private ChatMessage serverFrame(String text) {
|
||||
ChatMessage message = new ChatMessage();
|
||||
message.setMessageDirectedToServer(true);
|
||||
message.setMessageText(text);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package kst4contest.controller;
|
||||
import kst4contest.logic.BandOpportunityResolver;
|
||||
import kst4contest.view.map.MapCallsignRawSnapshot;
|
||||
import kst4contest.logic.PropagationFrequencyResolver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -12,9 +13,7 @@ import java.util.function.Consumer;
|
||||
import javafx.application.Platform;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.view.map.GeometryOnlyPathAnalysisService;
|
||||
|
||||
import kst4contest.view.map.OpenMeteoTerrainProfileProvider;
|
||||
@@ -22,7 +21,6 @@ import kst4contest.view.map.PathAnalysisRequest;
|
||||
import kst4contest.view.map.PathAnalysisResult;
|
||||
import kst4contest.view.map.PathAnalysisService;
|
||||
import kst4contest.view.map.PathGeometryUtils;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
@@ -139,9 +137,11 @@ public final class ReachabilityService {
|
||||
* @param member best matching ChatMember, may be null when only a map snapshot exists
|
||||
* @param selectedSnapshot selected map snapshot
|
||||
* @param fxCallback callback executed on the JavaFX thread
|
||||
* @param requestedBandOverride operator-selected band, or null for automatic resolution
|
||||
*/
|
||||
public void requestPathAnalysisForMap(ChatMember member,
|
||||
MapCallsignRawSnapshot selectedSnapshot,
|
||||
Band requestedBandOverride,
|
||||
Consumer<PathAnalysisResult> fxCallback) {
|
||||
|
||||
String ownLocator6 = normalizeLocator6(chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
@@ -165,22 +165,23 @@ public final class ReachabilityService {
|
||||
return;
|
||||
}
|
||||
|
||||
double analysisFrequencyMHz = PathGeometryUtils.resolveAnalysisFrequencyMHz(
|
||||
selectedSnapshot.lastKnownFrequenciesByBand()
|
||||
);
|
||||
Band analysisBand;
|
||||
double analysisFrequencyMHz;
|
||||
|
||||
Band analysisBand = Band.fromFrequency(analysisFrequencyMHz);
|
||||
if (analysisBand != null && !isUsableAutomaticBand(member, analysisBand)) {
|
||||
analysisFrequencyMHz = Double.NaN;
|
||||
analysisBand = null;
|
||||
}
|
||||
if (requestedBandOverride != null) {
|
||||
/*
|
||||
* An explicit operator selection has priority over automatic propagation
|
||||
* resolution. Exact recent QRG information on that band is still used
|
||||
* when available; otherwise the band's default analysis frequency is used.
|
||||
*/
|
||||
analysisBand = requestedBandOverride;
|
||||
analysisFrequencyMHz =
|
||||
resolveAnalysisFrequencyForBand(member, analysisBand);
|
||||
} else {
|
||||
PropagationFrequencyResolver.Resolution frequencyResolution =
|
||||
resolveAutomaticPropagationFrequency(member);
|
||||
|
||||
if (!Double.isFinite(analysisFrequencyMHz)
|
||||
|| analysisFrequencyMHz <= 0.0
|
||||
|| analysisBand == null) {
|
||||
|
||||
Band fallbackBand = resolveAutoBand(member);
|
||||
if (fallbackBand == null) {
|
||||
if (frequencyResolution == null) {
|
||||
dispatchFxCallback(
|
||||
fxCallback,
|
||||
PathAnalysisResult.waitingForUsableBand(
|
||||
@@ -192,8 +193,9 @@ public final class ReachabilityService {
|
||||
return;
|
||||
}
|
||||
|
||||
analysisBand = fallbackBand;
|
||||
analysisFrequencyMHz = resolveAnalysisFrequencyForBand(member, fallbackBand);
|
||||
analysisBand = frequencyResolution.getBand();
|
||||
analysisFrequencyMHz =
|
||||
frequencyResolution.getAnalysisFrequencyMHz();
|
||||
}
|
||||
|
||||
PathAnalysisRequest request = buildRequest(
|
||||
@@ -236,72 +238,32 @@ public final class ReachabilityService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the auto reachability band.
|
||||
*
|
||||
* <ol>
|
||||
* <li>Use the lowest band detected in this session.</li>
|
||||
* <li>If no session band exists and the station is in the microwave category, use 1296 MHz.</li>
|
||||
* <li>Otherwise use 144 MHz.</li>
|
||||
* </ol>
|
||||
* Resolves the auto reachability band through the shared propagation
|
||||
* frequency selection used by AirScout and path analysis.
|
||||
*
|
||||
* @param member member to inspect
|
||||
* @return resolved band
|
||||
*/
|
||||
public Band resolveAutoBand(ChatMember member) {
|
||||
EnumSet<Band> enabledBands = getEnabledStationBands();
|
||||
if (enabledBands.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolveAutomaticPropagationFrequency(member);
|
||||
return resolution == null ? null : resolution.getBand();
|
||||
}
|
||||
|
||||
List<ChatMember> variants = resolveCallsignVariants(member);
|
||||
BandOpportunityResolver.Resolution resolution =
|
||||
BandOpportunityResolver.resolve(variants, System.currentTimeMillis());
|
||||
|
||||
EnumSet<Band> availableOfferedBands = resolution.getAvailableBands();
|
||||
availableOfferedBands.retainAll(enabledBands);
|
||||
|
||||
if (!availableOfferedBands.isEmpty()) {
|
||||
return availableOfferedBands.stream()
|
||||
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
// Known evidence exists, but every matching band is disabled or NOT QRV.
|
||||
if (resolution.hasBandEvidence()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
EnumSet<Band> fallbackBands = EnumSet.copyOf(enabledBands);
|
||||
fallbackBands.removeAll(resolution.getNotQrvBands());
|
||||
if (fallbackBands.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (member != null
|
||||
&& member.getChatCategory() != null
|
||||
&& member.getChatCategory().getCategoryNumber() == ChatCategory.MICROWAVE
|
||||
&& fallbackBands.contains(Band.B_1296)) {
|
||||
return Band.B_1296;
|
||||
}
|
||||
|
||||
if (member != null
|
||||
&& member.getChatCategory() != null
|
||||
&& member.getChatCategory().getCategoryNumber() == ChatCategory.FIFTYSEVENTYMHz) {
|
||||
if (fallbackBands.contains(Band.B_50)) {
|
||||
return Band.B_50;
|
||||
}
|
||||
if (fallbackBands.contains(Band.B_70)) {
|
||||
return Band.B_70;
|
||||
}
|
||||
}
|
||||
|
||||
if (fallbackBands.contains(Band.B_144)) {
|
||||
return Band.B_144;
|
||||
}
|
||||
|
||||
return fallbackBands.stream()
|
||||
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
|
||||
.orElse(null);
|
||||
/**
|
||||
* Resolves one automatic band and exact analysis frequency for a station.
|
||||
*
|
||||
* @param member any active category variant of the target station
|
||||
* @return shared propagation resolution, or {@code null} for unsupported data
|
||||
*/
|
||||
public PropagationFrequencyResolver.Resolution resolveAutomaticPropagationFrequency(
|
||||
ChatMember member
|
||||
) {
|
||||
return PropagationFrequencyResolver.resolve(
|
||||
resolveCallsignVariants(member),
|
||||
getEnabledStationBands(),
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,27 +291,7 @@ public final class ReachabilityService {
|
||||
return variants.isEmpty() ? List.of(member) : variants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that an automatically selected map/snapshot frequency belongs to a
|
||||
* locally enabled band that is still available after NOT-QRV resolution.
|
||||
* Manual UI band overrides are handled separately and are not changed here.
|
||||
*/
|
||||
private boolean isUsableAutomaticBand(ChatMember member, Band band) {
|
||||
if (band == null || !getEnabledStationBands().contains(band)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (member == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
BandOpportunityResolver.Resolution resolution = BandOpportunityResolver.resolve(
|
||||
resolveCallsignVariants(member),
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
return resolution.getAvailableBands().contains(band);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the background executor.
|
||||
@@ -541,28 +483,7 @@ public final class ReachabilityService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the analysis frequency from a map snapshot first, because the map
|
||||
* aggregates all visible ChatMember variants and often knows the best current
|
||||
* frequency per band.
|
||||
*
|
||||
* @param member fallback member
|
||||
* @param selectedSnapshot selected map snapshot
|
||||
* @return analysis frequency in MHz
|
||||
*/
|
||||
private double resolveAnalysisFrequencyForSnapshot(ChatMember member, MapCallsignRawSnapshot selectedSnapshot) {
|
||||
if (selectedSnapshot != null) {
|
||||
double snapshotFrequencyMHz =
|
||||
PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
|
||||
|
||||
if (Double.isFinite(snapshotFrequencyMHz) && snapshotFrequencyMHz > 0.0) {
|
||||
return snapshotFrequencyMHz;
|
||||
}
|
||||
}
|
||||
|
||||
Band fallbackBand = member == null ? Band.B_144 : resolveAutoBand(member);
|
||||
return resolveAnalysisFrequencyForBand(member, fallbackBand);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the analysis frequency for one member/band pair.
|
||||
@@ -575,15 +496,28 @@ public final class ReachabilityService {
|
||||
* </ol>
|
||||
*/
|
||||
private double resolveAnalysisFrequencyForBand(ChatMember member, Band band) {
|
||||
if (member != null && member.getKnownActiveBands() != null) {
|
||||
ChatMember.ActiveFrequencyInfo activeFrequencyInfo = member.getKnownActiveBands().get(band);
|
||||
if (band == null) {
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
ChatMember.ActiveFrequencyInfo latestFrequencyInfo = null;
|
||||
for (ChatMember variant : resolveCallsignVariants(member)) {
|
||||
ChatMember.ActiveFrequencyInfo activeFrequencyInfo =
|
||||
variant.getKnownActiveBands().get(band);
|
||||
|
||||
if (activeFrequencyInfo != null
|
||||
&& Double.isFinite(activeFrequencyInfo.frequency)
|
||||
&& activeFrequencyInfo.frequency > 0.0) {
|
||||
return activeFrequencyInfo.frequency;
|
||||
&& activeFrequencyInfo.frequency > 0.0
|
||||
&& (latestFrequencyInfo == null
|
||||
|| activeFrequencyInfo.timestampEpoch > latestFrequencyInfo.timestampEpoch)) {
|
||||
latestFrequencyInfo = activeFrequencyInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (latestFrequencyInfo != null) {
|
||||
return latestFrequencyInfo.frequency;
|
||||
}
|
||||
|
||||
if (member != null && member.getFrequency() != null && member.getFrequency().getValue() != null) {
|
||||
double parsedFrequencyMHz = PathGeometryUtils.tryParseFrequencyMHz(member.getFrequency().getValue());
|
||||
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
|
||||
|
||||
@@ -1,119 +1,130 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.LongPredicate;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
|
||||
/**
|
||||
* This thread is responsible for reading telnet servers input at port 23001 and printing it
|
||||
* to the console.
|
||||
* It runs in an infinite loop until the client disconnects from the server.
|
||||
* Reads exactly one immutable ON4KST connection session.
|
||||
*
|
||||
* @author www.codejava.net
|
||||
* <p>EOF is a connection-loss event, not an empty chat message. Every line is
|
||||
* associated with the session id captured by this reader, so a delayed exception
|
||||
* from an obsolete socket cannot affect a newer reconnect.</p>
|
||||
*/
|
||||
public class ReadThread extends Thread {
|
||||
private static final Logger LOGGER = Logger.getLogger(ReadThread.class.getName());
|
||||
private BufferedReader reader;
|
||||
private Socket socket;
|
||||
private ChatController client;
|
||||
public boolean accidentalDisconnected;
|
||||
|
||||
|
||||
|
||||
public boolean isAccidentalDisconnected() {
|
||||
return accidentalDisconnected;
|
||||
}
|
||||
|
||||
public void setAccidentalDisconnected(boolean accidentalDisconnected) {
|
||||
this.accidentalDisconnected = accidentalDisconnected;
|
||||
}
|
||||
private final long sessionId;
|
||||
private final Socket socket;
|
||||
private final LinkedBlockingQueue<ChatMessage> receiveQueue;
|
||||
private final LongPredicate sessionIsActive;
|
||||
private final Consumer<String> inboundActivity;
|
||||
private final Consumer<Throwable> connectionFailure;
|
||||
private final BufferedReader reader;
|
||||
|
||||
// private boolean readingFinished = true; //kst4contest.test 4 23001
|
||||
private boolean readingFinished = true;
|
||||
|
||||
InputStream input;
|
||||
|
||||
public ReadThread(Socket socket, ChatController client) {
|
||||
/**
|
||||
* Compatibility constructor for the pre-session controller path.
|
||||
*
|
||||
* @deprecated new connections should be created by
|
||||
* {@link On4KstConnectionManager}
|
||||
*/
|
||||
@Deprecated
|
||||
public ReadThread(Socket socket, ChatController client) throws IOException {
|
||||
this(0L, socket, client.getMessageRXBus(), ignored -> true,
|
||||
ignored -> { }, ignored -> { });
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the reader for one connection generation.
|
||||
*
|
||||
* @param sessionId immutable id of the owning socket session
|
||||
* @param socket connected ON4KST socket
|
||||
* @param receiveQueue private receive queue belonging to this session
|
||||
* @param sessionIsActive guard against callbacks from an obsolete session
|
||||
* @param inboundActivity callback used for liveness and protocol progress
|
||||
* @param connectionFailure callback for EOF, I/O and unexpected runtime errors
|
||||
* @throws IOException if the socket input stream cannot be opened
|
||||
*/
|
||||
public ReadThread(
|
||||
long sessionId,
|
||||
Socket socket,
|
||||
LinkedBlockingQueue<ChatMessage> receiveQueue,
|
||||
LongPredicate sessionIsActive,
|
||||
Consumer<String> inboundActivity,
|
||||
Consumer<Throwable> connectionFailure
|
||||
) throws IOException {
|
||||
this.sessionId = sessionId;
|
||||
this.socket = socket;
|
||||
this.client = client;
|
||||
|
||||
try {
|
||||
input = socket.getInputStream();
|
||||
reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
} catch (IOException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Error getting input stream", ex);
|
||||
}
|
||||
this.receiveQueue = receiveQueue;
|
||||
this.sessionIsActive = sessionIsActive;
|
||||
this.inboundActivity = inboundActivity;
|
||||
this.connectionFailure = connectionFailure;
|
||||
this.reader = new BufferedReader(new InputStreamReader(
|
||||
socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("ReadFromTelnetThread");
|
||||
|
||||
ChatMessage message; //bugfix leak, moved out of while
|
||||
while (true) {
|
||||
|
||||
// System.out.println("rdth");
|
||||
|
||||
try {
|
||||
|
||||
String response = reader.readLine();
|
||||
message = new ChatMessage();
|
||||
message.setMessageText(response);
|
||||
|
||||
// message.setDirectedToServer(false);
|
||||
// message.setDirectedToServer(false);
|
||||
// message.setDirectedToServer(false);
|
||||
|
||||
|
||||
if (response != null) {
|
||||
client.getMessageRXBus().put(message);
|
||||
// System.out.println("[RT]: read message and added it to msgrxqueue --- " + response + " ---");
|
||||
} else {
|
||||
System.out.println("[RT]: read message responsed a nullstring, do nothing, buffersize = " + socket.getReceiveBufferSize() + ", reader ready? "
|
||||
+ reader.ready());
|
||||
// reader = new BufferedReader(new InputStreamReader(input));
|
||||
// response = reader.readLine();
|
||||
this.client.getSocket().close();
|
||||
this.interrupt();
|
||||
Thread.currentThread().setName("ReadFromOn4Kst-" + sessionId);
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST reader started for session {0}", sessionId);
|
||||
|
||||
try {
|
||||
while (!isInterrupted() && sessionIsActive.test(sessionId)) {
|
||||
String response = reader.readLine();
|
||||
if (response == null) {
|
||||
throw new EOFException("ON4KST closed the TCP connection");
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception sexc) {
|
||||
LOGGER.log(Level.SEVERE, "[ReadThread] Socket closed unexpectedly", sexc);
|
||||
try {
|
||||
this.client.getSocket().close();
|
||||
this.interrupt();
|
||||
break;
|
||||
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "[ReadThread] Error closing socket", e);
|
||||
}
|
||||
inboundActivity.accept(response);
|
||||
if (!sessionIsActive.test(sessionId)) {
|
||||
break;
|
||||
}
|
||||
|
||||
ChatMessage message = new ChatMessage();
|
||||
message.setMessageText(response);
|
||||
receiveQueue.put(message);
|
||||
}
|
||||
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (IOException exception) {
|
||||
if (sessionIsActive.test(sessionId)) {
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST read failed for session " + sessionId,
|
||||
exception);
|
||||
connectionFailure.accept(exception);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
if (sessionIsActive.test(sessionId)) {
|
||||
LOGGER.log(Level.SEVERE, "Unexpected ON4KST reader failure", exception);
|
||||
connectionFailure.accept(exception);
|
||||
}
|
||||
} finally {
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST reader stopped for session {0}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Interrupts the read loop and closes the session socket.
|
||||
*
|
||||
* @return always {@code true} after a successful close
|
||||
* @throws IOException if closing the reader or socket fails
|
||||
*/
|
||||
public boolean terminateConnection() throws IOException {
|
||||
this.reader.close();
|
||||
this.input.close();
|
||||
this.socket.close();
|
||||
|
||||
return true;
|
||||
interrupt();
|
||||
reader.close();
|
||||
socket.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isReadingFinished() {
|
||||
return readingFinished;
|
||||
}
|
||||
|
||||
public void setReadingFinished(boolean readingReady) {
|
||||
this.readingFinished = readingReady;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -165,18 +164,79 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Win-Test STATUS packets and update own QRG from WT station.
|
||||
* Formats a frequency from a Win-Test STATUS packet for use as MYQRG.
|
||||
*
|
||||
* Parsing model (tokenized with quotes preserved):
|
||||
* parts[0] = "STATUS"
|
||||
* parts[1] = station name (example: "STN1")
|
||||
* parts[5] = val2 (used to derive mode: 1 => SSB, else CW)
|
||||
* parts[7] = frequency in 0.1 kHz units (example: 1443210 => 144321.0)
|
||||
* <p>Win-Test transmits the frequency in units of 0.1 kHz. KST4Contest
|
||||
* displays frequencies as {@code MHz.kHz.10Hz}, for example
|
||||
* {@code 144.300.00} or {@code 10368.100.00}. The MHz part may contain
|
||||
* between one and five digits. Deriving its length from the complete value
|
||||
* avoids separate and incomplete handling for individual bands.</p>
|
||||
*
|
||||
* @param frequencyIn100Hz frequency received from Win-Test in units of
|
||||
* 0.1 kHz, equivalent to 100 Hz
|
||||
* @return frequency formatted for MYQRG
|
||||
* @throws IllegalArgumentException if the supplied frequency is not positive
|
||||
* or too small to be formatted
|
||||
* @throws ArithmeticException if the supplied value exceeds the supported
|
||||
* numeric range
|
||||
*/
|
||||
private String helper_formatWinTestFrequency(long frequencyIn100Hz) {
|
||||
if (frequencyIn100Hz <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Win-Test frequency must be greater than zero"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Multiplication by ten creates a digit sequence whose final five
|
||||
* digits represent kHz and 10-Hz groups:
|
||||
*
|
||||
* 1443210 -> 14432100 -> 144.321.00
|
||||
* 103681000 -> 1036810000 -> 10368.100.00
|
||||
*/
|
||||
long frequencyIn10Hz = Math.multiplyExact(
|
||||
frequencyIn100Hz,
|
||||
10L
|
||||
);
|
||||
|
||||
String frequencyDigits = Long.toString(frequencyIn10Hz);
|
||||
|
||||
if (frequencyDigits.length() < 6) {
|
||||
throw new IllegalArgumentException(
|
||||
"Win-Test frequency is too small: " + frequencyIn100Hz
|
||||
);
|
||||
}
|
||||
|
||||
int mhzEndIndex = frequencyDigits.length() - 5;
|
||||
int khzEndIndex = frequencyDigits.length() - 2;
|
||||
|
||||
return frequencyDigits.substring(0, mhzEndIndex)
|
||||
+ "."
|
||||
+ frequencyDigits.substring(mhzEndIndex, khzEndIndex)
|
||||
+ "."
|
||||
+ frequencyDigits.substring(khzEndIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a Win-Test STATUS packet and optionally updates MYQRG.
|
||||
*
|
||||
* <p>The packet is tokenised while preserving quoted station names.
|
||||
* The configured station-name filter is applied before any frequency is
|
||||
* processed. An empty filter accepts STATUS packets from every Win-Test
|
||||
* station.</p>
|
||||
*
|
||||
* <p>The main frequency is read from token 7. If pass-frequency use is
|
||||
* enabled and token 11 contains a valid frequency, the pass frequency is
|
||||
* used instead. A missing or invalid pass frequency deliberately falls
|
||||
* back to the main frequency.</p>
|
||||
*
|
||||
* @param msg complete Win-Test STATUS packet
|
||||
*/
|
||||
private void parseStatus(String msg) {
|
||||
try {
|
||||
ArrayList<String> parts = new ArrayList<>();
|
||||
Matcher matcher = STATUS_TOKEN_PATTERN.matcher(msg);
|
||||
|
||||
while (matcher.find()) {
|
||||
if (matcher.group(1) != null) {
|
||||
parts.add(matcher.group(1));
|
||||
@@ -186,86 +246,105 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
}
|
||||
|
||||
if (parts.size() < 8) {
|
||||
System.out.println("[WinTest] STATUS too short: " + msg);
|
||||
System.out.println(
|
||||
"[WinTest] STATUS too short: " + msg
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
String stn = parts.get(1);
|
||||
String stationFilter = client.getChatPreferences().getLogsynch_wintestNetworkStationNameOfWintestClient1();
|
||||
if (stationFilter != null && !stationFilter.isBlank() && !stn.equalsIgnoreCase(stationFilter)) {
|
||||
String stationName = parts.get(1);
|
||||
String stationFilter = client
|
||||
.getChatPreferences()
|
||||
.getLogsynch_wintestNetworkStationNameOfWintestClient1();
|
||||
|
||||
if (stationFilter != null
|
||||
&& !stationFilter.isBlank()
|
||||
&& !stationName.equalsIgnoreCase(stationFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String val2 = parts.get(5);
|
||||
String freqRaw = parts.get(7);
|
||||
double freqFloat = Integer.parseInt(freqRaw) / 10.0;
|
||||
String modeValue = parts.get(5);
|
||||
long mainFrequencyRaw = Long.parseLong(parts.get(7));
|
||||
double mainFrequencyKHz = mainFrequencyRaw / 10.0;
|
||||
|
||||
String mode;
|
||||
if ("1".equals(val2)) {
|
||||
mode = freqFloat > 10000.0 ? "usb" : "lsb";
|
||||
|
||||
if ("1".equals(modeValue)) {
|
||||
mode = mainFrequencyKHz > 10000.0 ? "usb" : "lsb";
|
||||
} else {
|
||||
mode = "cw";
|
||||
}
|
||||
|
||||
// Format as MMM.KKK.HH display format (e.g. 144.300.00) consistent with UCX thread
|
||||
// freqFloat is in kHz (e.g. 144300.0), convert to Hz-string for formatting
|
||||
long freqHzTimes100 = Math.round(freqFloat * 100.0); // e.g. 14430000
|
||||
String hzStr = String.valueOf(freqHzTimes100);
|
||||
String formattedQRG;
|
||||
if (hzStr.length() == 8) {
|
||||
// 144MHz range: 14430000 -> 144.300.00
|
||||
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 3), hzStr.substring(3, 6), hzStr.substring(6, 8));
|
||||
} else if (hzStr.length() == 9) {
|
||||
// 1296MHz range: 129600000 -> 1296.000.00
|
||||
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 4), hzStr.substring(4, 7), hzStr.substring(7, 9));
|
||||
} else if (hzStr.length() == 7) {
|
||||
// 70MHz range: 7010000 -> 70.100.00
|
||||
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 2), hzStr.substring(2, 5), hzStr.substring(5, 7));
|
||||
} else if (hzStr.length() == 6) {
|
||||
// 50MHz range: 5030000 but 6 digits: 503000 -> 5.030.00
|
||||
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 1), hzStr.substring(1, 4), hzStr.substring(4, 6));
|
||||
} else {
|
||||
formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback
|
||||
}
|
||||
// Parse pass frequency from parts[11] if available (WT STATUS format)
|
||||
String formattedPassQRG = null;
|
||||
String formattedMainQrg =
|
||||
helper_formatWinTestFrequency(mainFrequencyRaw);
|
||||
|
||||
/*
|
||||
* Token 11 may contain the pass frequency, depending on the
|
||||
* Win-Test STATUS packet. Small numeric flag values must not be
|
||||
* interpreted as frequencies.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
long passFrequencyRaw =
|
||||
Long.parseLong(parts.get(11));
|
||||
double passFrequencyKHz =
|
||||
passFrequencyRaw / 10.0;
|
||||
|
||||
if (passFrequencyKHz > 100.0) {
|
||||
formattedPassQrg =
|
||||
helper_formatWinTestFrequency(
|
||||
passFrequencyRaw
|
||||
);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// parts[11] not a valid frequency, leave formattedPassQRG as null
|
||||
} catch (IllegalArgumentException
|
||||
| ArithmeticException ignored) {
|
||||
/*
|
||||
* Token 11 does not contain a usable frequency.
|
||||
* The main frequency remains the safe fallback.
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
if (this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) {
|
||||
final String qrgToSet = (this.client.getChatPreferences().isLogsynch_wintestUsePassQrg() && formattedPassQRG != null)
|
||||
? formattedPassQRG
|
||||
: formattedQRG;
|
||||
// JavaFX StringProperty must be updated on the FX Application Thread
|
||||
Platform.runLater(() -> this.client.getChatPreferences().getMYQRGFirstCat().set(qrgToSet));
|
||||
boolean usePassQrg = client
|
||||
.getChatPreferences()
|
||||
.isLogsynch_wintestUsePassQrg();
|
||||
|
||||
final String qrgToSet =
|
||||
usePassQrg && formattedPassQrg != null
|
||||
? formattedPassQrg
|
||||
: formattedMainQrg;
|
||||
|
||||
if (client
|
||||
.getChatPreferences()
|
||||
.isLogsynch_wintestQrgSyncEnabled()) {
|
||||
Platform.runLater(
|
||||
() -> client
|
||||
.getChatPreferences()
|
||||
.getMYQRGFirstCat()
|
||||
.set(qrgToSet)
|
||||
);
|
||||
}
|
||||
|
||||
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG
|
||||
+ (formattedPassQRG != null ? ", passQrg=" + formattedPassQRG : "")
|
||||
+ ", syncActive=" + this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled());
|
||||
} catch (Exception e) {
|
||||
System.out.println("[WinTest] STATUS parsing error: " + e.getMessage());
|
||||
System.out.println(
|
||||
"[WinTest STATUS] stn=" + stationName
|
||||
+ ", mode=" + mode
|
||||
+ ", qrg=" + formattedMainQrg
|
||||
+ (formattedPassQrg != null
|
||||
? ", passQrg=" + formattedPassQrg
|
||||
: "")
|
||||
+ ", selectedQrg=" + qrgToSet
|
||||
+ ", syncActive="
|
||||
+ client
|
||||
.getChatPreferences()
|
||||
.isLogsynch_wintestQrgSyncEnabled()
|
||||
);
|
||||
} catch (Exception exception) {
|
||||
System.out.println(
|
||||
"[WinTest] STATUS parsing error: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,4 +17,20 @@ public interface StatusUpdateListener {
|
||||
void onUserListUpdated(String reason);
|
||||
// new: userlist-update
|
||||
|
||||
}
|
||||
/**
|
||||
* Called whenever the authoritative ON4KST session changes lifecycle state.
|
||||
*
|
||||
* <p>The callback may originate from a background connection supervisor. A UI
|
||||
* implementation must marshal control changes onto its application thread.</p>
|
||||
*
|
||||
* @param state new connection, authentication or synchronization state
|
||||
* @param detail human-readable progress or failure reason
|
||||
*/
|
||||
default void onConnectionStateChanged(
|
||||
On4KstConnectionState state,
|
||||
String detail
|
||||
) {
|
||||
// Optional for non-UI listeners.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -101,102 +101,143 @@ public class Utils4KST {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a chatmembers frequency-string for cluster usage<br/>
|
||||
* <b>returns a frequency String in KHz like = "144300" or "144300.0" to match DXC protocol needs</b>
|
||||
* Converts a frequency detected in a chat message into the kHz representation
|
||||
* used by the DX Cluster protocol.
|
||||
*
|
||||
* @param optionalPrefix: if there is a value like ".300", it have to be decided, wich ".300": 144.300, 432.300, 1296.300 .... prefix means for example "144."
|
||||
* <p>Complete frequencies with two to five MHz digits are supported, for
|
||||
* example 50.200, 144.205, 1296.338, 10368.100 and 24048.100. An optional
|
||||
* second fractional group is retained as sub-kHz precision, for example
|
||||
* 144.205.2 becomes 144205.2 kHz.</p>
|
||||
*
|
||||
* <p>Relative values such as .205 or 205 use the configured fallback-band
|
||||
* prefix. The method only performs this fallback conversion after the general
|
||||
* message parser has already accepted the value as a frequency.</p>
|
||||
*
|
||||
* @param qrgString frequency as detected by the chat parser
|
||||
* @param optionalPrefix fallback MHz prefix for relative frequencies
|
||||
* @return frequency in kHz for a DX Cluster spot, or an empty string if the
|
||||
* value cannot be converted safely
|
||||
*/
|
||||
public static String normalizeFrequencyString(String qrgString, SimpleStringProperty optionalPrefix) {
|
||||
|
||||
// final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
|
||||
|
||||
try {
|
||||
qrgString = qrgString.replace(" ","");
|
||||
} catch (Exception e) {
|
||||
System.out.println("UTILS: QRG NULL, nothing to convert");
|
||||
// e.printStackTrace();
|
||||
public static String normalizeFrequencyString(
|
||||
String qrgString,
|
||||
SimpleStringProperty optionalPrefix
|
||||
) {
|
||||
if (qrgString == null || qrgString.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/*
|
||||
* A comma is accepted as a decimal separator. Spaces are removed so older
|
||||
* stored formats such as "432 088" remain usable.
|
||||
*/
|
||||
String normalizedValue = qrgString
|
||||
.trim()
|
||||
.replace(" ", "")
|
||||
.replace(',', '.');
|
||||
|
||||
final String PTRN_QRG_CAT2_wholeQRGMHz4Digits = "(([0-9]{4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)"; //1296.300.3 etc
|
||||
final String PTRN_QRG_CAT2_wholeQRGMHz3Digits = "(([0-9]{3}[\\.|,| ]?[0-9]{3})([\\.][\\d]{1,2})?)"; //144.300.3 etc
|
||||
final String PTRN_QRG_CAT2_QRGwithoutPrefix = "((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; //144.300.3 etc
|
||||
/*
|
||||
* Complete frequency with an explicit separator:
|
||||
*
|
||||
* 50.200
|
||||
* 144.205
|
||||
* 1296.338
|
||||
* 10368.100
|
||||
* 144.205.2
|
||||
*/
|
||||
Matcher completeFrequencyMatcher = Pattern.compile(
|
||||
"^(\\d{2,5})\\.(\\d{1,3})(?:\\.(\\d{1,2}))?$"
|
||||
).matcher(normalizedValue);
|
||||
|
||||
String stringAggregation = "";
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz4Digits)) {//case 1296.200 or 1296.200.2 etc.
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".","");
|
||||
stringAggregation = stringAggregation.replace(",","");
|
||||
stringAggregation = stringAggregation.replace(" ", "");
|
||||
|
||||
if (stringAggregation.length() == 8) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew + ".0";
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 9) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
}
|
||||
|
||||
} else
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz3Digits)) { //case 144.300 or 144.300.2
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".","");
|
||||
stringAggregation = stringAggregation.replace(",","");
|
||||
stringAggregation = stringAggregation.replace(" ", "");
|
||||
|
||||
if (stringAggregation.length() == 6) {
|
||||
stringAggregation = stringAggregation + ".0";
|
||||
return stringAggregation;
|
||||
}
|
||||
if (stringAggregation.length() == 7) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew + ".0";
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 8) {
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
if (testPattern(qrgString, PTRN_QRG_CAT2_QRGwithoutPrefix)) { //case ".050 or .300 or something like that"
|
||||
stringAggregation = qrgString;
|
||||
|
||||
stringAggregation = stringAggregation.replace(".", "");
|
||||
stringAggregation = stringAggregation.replace(",", "");
|
||||
stringAggregation = stringAggregation.replace(" ", "");
|
||||
|
||||
if (stringAggregation.length() == 3) { // like 050 or 300
|
||||
String stringAggregationNew = optionalPrefix.getValue() + stringAggregation;
|
||||
stringAggregation = stringAggregationNew + ".0";
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 4) { //like 050.2 --> 0502
|
||||
|
||||
stringAggregation = optionalPrefix.getValue() + stringAggregation;
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 1) + "." + stringAggregation.substring(stringAggregation.length() - 1, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
|
||||
} else if (stringAggregation.length() == 5) { //like 050.20 --> 05020
|
||||
|
||||
stringAggregation = optionalPrefix.getValue() + stringAggregation;
|
||||
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 2) + "." + stringAggregation.substring(stringAggregation.length() - 2, stringAggregation.length());
|
||||
stringAggregation = stringAggregationNew;
|
||||
return stringAggregation;
|
||||
}
|
||||
if (completeFrequencyMatcher.matches()) {
|
||||
return formatDxClusterFrequency(
|
||||
completeFrequencyMatcher.group(1),
|
||||
completeFrequencyMatcher.group(2),
|
||||
completeFrequencyMatcher.group(3)
|
||||
);
|
||||
}
|
||||
|
||||
return stringAggregation; //if nothing else helps
|
||||
/*
|
||||
* Compact complete frequency retained for compatibility:
|
||||
*
|
||||
* 144205
|
||||
* 1296338
|
||||
* 10368100
|
||||
* 432088.2
|
||||
*/
|
||||
Matcher compactFrequencyMatcher = Pattern.compile(
|
||||
"^(\\d{2,5})(\\d{3})(?:\\.(\\d{1,2}))?$"
|
||||
).matcher(normalizedValue);
|
||||
|
||||
if (compactFrequencyMatcher.matches()) {
|
||||
return formatDxClusterFrequency(
|
||||
compactFrequencyMatcher.group(1),
|
||||
compactFrequencyMatcher.group(2),
|
||||
compactFrequencyMatcher.group(3)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Relative frequency. Values above 499 are deliberately rejected, matching
|
||||
* the previous behaviour and preventing a report such as 599 from becoming
|
||||
* a plausible-looking cluster frequency.
|
||||
*/
|
||||
Matcher relativeFrequencyMatcher = Pattern.compile(
|
||||
"^\\.?([0-4]\\d{2})(?:\\.(\\d{1,2}))?$"
|
||||
).matcher(normalizedValue);
|
||||
|
||||
if (!relativeFrequencyMatcher.matches()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String fallbackPrefix = optionalPrefix == null
|
||||
? null
|
||||
: optionalPrefix.getValue();
|
||||
|
||||
if (fallbackPrefix == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
fallbackPrefix = fallbackPrefix.trim();
|
||||
|
||||
if (!fallbackPrefix.matches("\\d{2,5}")) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return formatDxClusterFrequency(
|
||||
fallbackPrefix,
|
||||
relativeFrequencyMatcher.group(1),
|
||||
relativeFrequencyMatcher.group(2)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an MHz part, a fractional MHz part and optional sub-kHz digits as a
|
||||
* DX Cluster frequency in kHz.
|
||||
*
|
||||
* <p>The fractional MHz part is padded on the right because 144.2 means
|
||||
* 144.200 MHz, while 144.21 means 144.210 MHz.</p>
|
||||
*
|
||||
* @param mhzPart complete MHz part
|
||||
* @param fractionalMhzPart one to three digits following the first separator
|
||||
* @param subKhzPart optional digits following a second separator
|
||||
* @return DX Cluster frequency in kHz
|
||||
*/
|
||||
private static String formatDxClusterFrequency(
|
||||
String mhzPart,
|
||||
String fractionalMhzPart,
|
||||
String subKhzPart
|
||||
) {
|
||||
String paddedFraction =
|
||||
(fractionalMhzPart + "000").substring(0, 3);
|
||||
|
||||
String subKhz = subKhzPart == null
|
||||
? "0"
|
||||
: subKhzPart;
|
||||
|
||||
return mhzPart
|
||||
+ paddedFraction
|
||||
+ "."
|
||||
+ subKhz;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,26 +39,63 @@ public class WinTestSkedSender {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
|
||||
* sequence via UDP broadcast.
|
||||
* Pushes a ContestSked into Win-Test by sending the
|
||||
* LOCKSKED / ADDSKED / UNLOCKSKED sequence.
|
||||
*
|
||||
* @param sked the sked to push
|
||||
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0)
|
||||
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST")
|
||||
* @param sked sked to push
|
||||
* @param targetCallsign callsign prepared for Win-Test
|
||||
* @param frequencyKHz operating frequency in kHz
|
||||
* @param notes optional notes
|
||||
* @param mode Win-Test mode ID: 0 for CW, 1 for SSB
|
||||
*/
|
||||
public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) {
|
||||
public void pushSkedToWinTest(ContestSked sked,
|
||||
String targetCallsign,
|
||||
double frequencyKHz,
|
||||
String notes,
|
||||
int mode) {
|
||||
|
||||
try {
|
||||
sendLockSked();
|
||||
sendAddSked(sked, frequencyKHz, notes, modeOverride);
|
||||
|
||||
sendAddSked(
|
||||
sked,
|
||||
targetCallsign,
|
||||
frequencyKHz,
|
||||
notes,
|
||||
mode
|
||||
);
|
||||
|
||||
sendUnlockSked();
|
||||
|
||||
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false);
|
||||
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign()
|
||||
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand());
|
||||
} catch (Exception e) {
|
||||
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
|
||||
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
reportStatus(
|
||||
"Sked pushed to WT: " + targetCallsign,
|
||||
false
|
||||
);
|
||||
|
||||
System.out.println(
|
||||
"[WinTestSkedSender] Sked pushed: "
|
||||
+ targetCallsign
|
||||
+ " at "
|
||||
+ frequencyKHz
|
||||
+ " kHz, band="
|
||||
+ sked.getBand()
|
||||
+ ", mode="
|
||||
+ mode
|
||||
);
|
||||
|
||||
} catch (Exception exception) {
|
||||
reportStatus(
|
||||
"ERROR pushing sked: "
|
||||
+ exception.getMessage(),
|
||||
true
|
||||
);
|
||||
|
||||
System.out.println(
|
||||
"[WinTestSkedSender] Error pushing sked: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
|
||||
exception.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,46 +123,53 @@ public class WinTestSkedSender {
|
||||
|
||||
/**
|
||||
* Sends an ADDSKED message with the sked details.
|
||||
* <p>
|
||||
* Win-Test ADDSKED data format (from wtKST):
|
||||
* <pre>
|
||||
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
|
||||
* </pre>
|
||||
* <p>
|
||||
* Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch).
|
||||
* The C# code adds 60 seconds to compensate.
|
||||
*
|
||||
* <p>The wtKST implementation subtracts a reference time of
|
||||
* 1970-01-01 00:01:00 UTC and subsequently adds 60 seconds. Both
|
||||
* operations cancel each other out. The transmitted value is therefore
|
||||
* an ordinary Unix timestamp and must not receive another offset here.</p>
|
||||
*/
|
||||
private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception {
|
||||
// Win-Test timestamp: epoch seconds with 60s offset
|
||||
long epochSeconds = sked.getSkedTimeEpoch() / 1000;
|
||||
long wtTimestamp = epochSeconds + 60;
|
||||
private void sendAddSked(ContestSked sked,
|
||||
String targetCallsign,
|
||||
double frequencyKHz,
|
||||
String notes,
|
||||
int mode) throws Exception {
|
||||
|
||||
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
|
||||
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
|
||||
long wtTimestamp =
|
||||
sked.getSkedTimeEpoch() / 1000L;
|
||||
|
||||
// Win-Test band ID
|
||||
int bandId = toWinTestBandId(sked.getBand());
|
||||
// Frequency in 0.1 kHz units.
|
||||
long frequencyTenthKHz =
|
||||
Math.round(frequencyKHz * 10.0);
|
||||
|
||||
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
|
||||
int mode;
|
||||
if (modeOverride >= 0) {
|
||||
mode = modeOverride;
|
||||
} else {
|
||||
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
|
||||
}
|
||||
int bandId =
|
||||
toWinTestBandId(sked.getBand());
|
||||
|
||||
String data = wtTimestamp
|
||||
+ " " + freqTenthKHz
|
||||
+ " " + bandId
|
||||
+ " " + mode
|
||||
+ " \"" + sked.getTargetCallsign() + "\""
|
||||
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||
/*
|
||||
* Accept only the mode IDs supported by this UI.
|
||||
* Any unexpected value falls back to SSB.
|
||||
*/
|
||||
int winTestMode =
|
||||
mode == 0
|
||||
? 0
|
||||
: 1;
|
||||
|
||||
WinTestMessage msg = new WinTestMessage(
|
||||
String data =
|
||||
wtTimestamp
|
||||
+ " " + frequencyTenthKHz
|
||||
+ " " + bandId
|
||||
+ " " + winTestMode
|
||||
+ " \"" + targetCallsign + "\""
|
||||
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||
|
||||
WinTestMessage message = new WinTestMessage(
|
||||
WinTestMessage.MessageType.ADDSKED,
|
||||
stationName, "",
|
||||
data);
|
||||
sendUdp(msg);
|
||||
stationName,
|
||||
"",
|
||||
data
|
||||
);
|
||||
|
||||
sendUdp(message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,18 +212,6 @@ public class WinTestSkedSender {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Very simple SSB segment heuristic.
|
||||
* A more complete implementation would check actual mode from Win-Test STATUS.
|
||||
*/
|
||||
private boolean isInSsbSegment(double frequencyKHz) {
|
||||
// SSB segments (kHz ranges)
|
||||
if (frequencyKHz >= 144300 && frequencyKHz <= 144399) return true; // 2m SSB
|
||||
if (frequencyKHz >= 432200 && frequencyKHz <= 432399) return true; // 70cm SSB
|
||||
if (frequencyKHz >= 1296200 && frequencyKHz <= 1296399) return true; // 23cm SSB
|
||||
return false;
|
||||
}
|
||||
|
||||
private void reportStatus(String text, boolean isError) {
|
||||
if (callback != null) {
|
||||
callback.onThreadStatus(THREAD_NICKNAME,
|
||||
|
||||
@@ -1,289 +1,173 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.Charset;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.LongPredicate;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
/**
|
||||
* This thread is responsible for sending content to the chat. As we only use
|
||||
* the tx function, there is no content in run() method
|
||||
*
|
||||
* Serializes and writes exactly one immutable ON4KST connection session.
|
||||
*
|
||||
* <p>The writer owns one private queue and appends exactly one CR/LF terminator
|
||||
* per frame. All category selection and delimiter validation happens before bytes
|
||||
* reach the socket.</p>
|
||||
*/
|
||||
public class WriteThread extends Thread {
|
||||
private PrintWriter writer;
|
||||
private Socket socket;
|
||||
private ChatController client;
|
||||
private OutputStream output;
|
||||
|
||||
private ChatMessage messageToBeSend;
|
||||
|
||||
public WriteThread(Socket socket, ChatController client) throws InterruptedException {
|
||||
this.socket = socket;
|
||||
this.client = client;
|
||||
|
||||
try {
|
||||
output = socket.getOutputStream();
|
||||
|
||||
writer = new PrintWriter(output, true, StandardCharsets.UTF_8);
|
||||
|
||||
} catch (IOException ex) {
|
||||
System.out.println("Error getting output stream: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(WriteThread.class.getName());
|
||||
private final long sessionId;
|
||||
private final Socket socket;
|
||||
private final LinkedBlockingQueue<ChatMessage> transmitQueue;
|
||||
private final LongPredicate sessionIsActive;
|
||||
private final Consumer<Throwable> connectionFailure;
|
||||
private final Consumer<String> rejectedFrame;
|
||||
private final BufferedWriter writer;
|
||||
private final int defaultCategory;
|
||||
|
||||
/**
|
||||
* This method is used to send a message to the server, raw formatted. E.g. for
|
||||
* the keepalive message. This method sends only in the main message-Category. To send it in a category
|
||||
* "defined by Chatmessage", use txByRxmsgCatOrigin(Chatmessage "toBeSend")
|
||||
*
|
||||
* @param messageToServer
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
public void tx(ChatMessage messageToServer) throws InterruptedException {
|
||||
|
||||
// writer.println(messageToServer.getMessage()); //kst4contest.test 4 23001
|
||||
// writer.flush(); //kst4contest.test 4 23001
|
||||
System.out.println(messageToServer.getMessageText() + "< sended to the writer");
|
||||
writer.println(messageToServer.getMessageText());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method is used to send a message directly to a receiver in a special chatcategory. The receivers category
|
||||
* will be read out of the Chatmessage.getChatCategory method. <b> The message text will be modified to fit kst
|
||||
* messageformat</b>
|
||||
* Compatibility constructor for the pre-session controller path.
|
||||
*
|
||||
* @param messageToServer
|
||||
* @throws InterruptedException
|
||||
* @deprecated new connections should be created by
|
||||
* {@link On4KstConnectionManager}
|
||||
*/
|
||||
public void txByRxmsgCatOrigin(ChatMessage messageToServer) throws InterruptedException {
|
||||
|
||||
// writer.println(messageToServer.getMessage()); //kst4contest.test 4 23001
|
||||
// writer.flush(); //kst4contest.test 4 23001
|
||||
|
||||
String originalMessageText = messageToServer.getMessageText() + "";
|
||||
|
||||
String newMessageText = "";
|
||||
|
||||
newMessageText = ("MSG|" + messageToServer.getChatCategory().getCategoryNumber()
|
||||
+ "|0|" + originalMessageText + "|0|"); //original before 1.26
|
||||
|
||||
|
||||
System.out.println(newMessageText + "< sended to the writer (DIRECTED REPLY)");
|
||||
writer.println(newMessageText);
|
||||
|
||||
@Deprecated
|
||||
public WriteThread(Socket socket, ChatController client) throws IOException {
|
||||
this(0L, socket, client.getMessageTXBus(),
|
||||
client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber(),
|
||||
ignored -> true,
|
||||
ignored -> { }, System.out::println);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method gets a textmessage to the chat and adds some characters to hit
|
||||
* the neccessarry format to send a message in the on4kst chat either to another
|
||||
* station or to the public.
|
||||
*
|
||||
* @param messageToServer
|
||||
* @throws InterruptedException
|
||||
* Creates the writer for one connection generation.
|
||||
*
|
||||
* @param sessionId immutable id of the owning socket session
|
||||
* @param socket connected ON4KST socket
|
||||
* @param transmitQueue private transmit queue belonging to this session
|
||||
* @param defaultCategory fallback category for unqualified chat messages
|
||||
* @param sessionIsActive guard against writes from an obsolete session
|
||||
* @param connectionFailure callback for socket and unexpected runtime errors
|
||||
* @param rejectedFrame callback for locally rejected protocol content
|
||||
* @throws IOException if the socket output stream cannot be opened
|
||||
*/
|
||||
public void txKSTFormatted(ChatMessage messageToServer) throws InterruptedException {
|
||||
public WriteThread(
|
||||
long sessionId,
|
||||
Socket socket,
|
||||
LinkedBlockingQueue<ChatMessage> transmitQueue,
|
||||
int defaultCategory,
|
||||
LongPredicate sessionIsActive,
|
||||
Consumer<Throwable> connectionFailure,
|
||||
Consumer<String> rejectedFrame
|
||||
) throws IOException {
|
||||
this.sessionId = sessionId;
|
||||
this.socket = socket;
|
||||
this.transmitQueue = transmitQueue;
|
||||
this.defaultCategory = defaultCategory;
|
||||
this.sessionIsActive = sessionIsActive;
|
||||
this.connectionFailure = connectionFailure;
|
||||
this.rejectedFrame = rejectedFrame;
|
||||
this.writer = new BufferedWriter(new OutputStreamWriter(
|
||||
socket.getOutputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
// writer.println(messageToServer.getMessageText());
|
||||
messageToBeSend = messageToServer;
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("WriteToOn4Kst-" + sessionId);
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST writer started for session {0}", sessionId);
|
||||
|
||||
try {
|
||||
|
||||
messageToBeSend = client.getMessageTXBus().take();
|
||||
// this.client.getmesetChatsetServerready(true);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
String messageLine = messageToBeSend.getMessageText();
|
||||
|
||||
if (messageToBeSend.isMessageDirectedToServer()) {
|
||||
/**
|
||||
* We have to check if we only commands the server (keepalive) or want do talk
|
||||
* to the community
|
||||
*/
|
||||
|
||||
try {
|
||||
tx(messageToBeSend);
|
||||
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
ChatMessage ownMSG = new ChatMessage();
|
||||
|
||||
// ownMSG.setMessageText(
|
||||
// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
|
||||
|
||||
ownMSG.setMessageText("MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber()
|
||||
+ "|0|" + messageLine + "|0|"); //original before 1.26
|
||||
|
||||
try {
|
||||
tx(ownMSG);
|
||||
System.out.println("BUS: tx: " + ownMSG.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (messageToBeSend.equals("/QUIT")) {
|
||||
try {
|
||||
this.client.getReadThread().terminateConnection();
|
||||
this.client.getReadThread().interrupt();
|
||||
this.client.getWriteThread().terminateConnection();
|
||||
this.client.getWriteThread().interrupt();
|
||||
this.interrupt();
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean terminateConnection() throws IOException {
|
||||
|
||||
this.output.close();
|
||||
this.socket.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Thread.currentThread().setName("WriteToTelnetThread");
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
messageToBeSend = client.getMessageTXBus().take();
|
||||
|
||||
if (messageToBeSend.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)
|
||||
&& messageToBeSend.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
client.getMessageRXBus().clear();
|
||||
this.interrupt();
|
||||
while (!isInterrupted() && sessionIsActive.test(sessionId)) {
|
||||
ChatMessage message = transmitQueue.take();
|
||||
if (isPoisonPill(message)) {
|
||||
break;
|
||||
}
|
||||
if (!sessionIsActive.test(sessionId)) {
|
||||
break;
|
||||
} else {
|
||||
String messageLine = messageToBeSend.getMessageText();
|
||||
|
||||
if (messageToBeSend.isMessageDirectedToServer()) {
|
||||
/**
|
||||
* We have to check if we only commands the server (keepalive) or want do talk
|
||||
* to the community
|
||||
*/
|
||||
|
||||
try {
|
||||
tx(messageToBeSend);
|
||||
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
} else { //message is not directed to the server, it´s directed to all or to a station
|
||||
|
||||
if (messageToBeSend.getChatCategory() == this.client.getChatCategoryMain() || messageToBeSend.getChatCategory() == this.client.getChatCategorySecondChat()) {
|
||||
|
||||
txByRxmsgCatOrigin(messageToBeSend);
|
||||
|
||||
} else { //default bhv if destination cat is not detectable
|
||||
|
||||
|
||||
ChatMessage ownMSG = new ChatMessage();
|
||||
|
||||
ownMSG.setMessageText(
|
||||
"MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|"
|
||||
+ messageLine + "|0|");
|
||||
|
||||
try {
|
||||
tx(ownMSG);
|
||||
System.out.println("WT: tx (raw): " + ownMSG.getMessageText());
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("WritheTh: got message out of the queue: " + messageToBeSend.getMessageText());
|
||||
|
||||
// this.client.getmesetChatsetServerready(true);
|
||||
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
client.getMessageTXBus().clear();
|
||||
}
|
||||
|
||||
// String messageLine = messageTextRaw.getMessageText();
|
||||
//
|
||||
// if (messageTextRaw.isMessageDirectedToServer()) {
|
||||
// /**
|
||||
// * We have to check if we only commands the server (keepalive) or want do talk
|
||||
// * to the community
|
||||
// */
|
||||
//
|
||||
// try {
|
||||
// tx(messageTextRaw);
|
||||
// System.out.println("BUS: tx: " + messageTextRaw.getMessageText());
|
||||
//
|
||||
// } catch (InterruptedException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
//
|
||||
// } else {
|
||||
//
|
||||
// ChatMessage ownMSG = new ChatMessage();
|
||||
//
|
||||
//// ownMSG.setMessageText(
|
||||
//// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
|
||||
//
|
||||
// ownMSG.setMessageText(
|
||||
// "MSG|" + this.client.getChatPreferences().getLoginChatCategory().getCategoryNumber() + "|0|"
|
||||
// + messageLine + "|0|");
|
||||
//
|
||||
// try {
|
||||
// tx(ownMSG);
|
||||
// System.out.println("BUS: tx: " + ownMSG.getMessageText());
|
||||
//
|
||||
// } catch (InterruptedException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
try {
|
||||
writeFrame(formatFrame(message));
|
||||
} catch (IllegalArgumentException invalidFrame) {
|
||||
LOGGER.log(Level.FINE,
|
||||
"Rejected outbound ON4KST frame in session {0}: {1}",
|
||||
new Object[] {sessionId, invalidFrame.getMessage()});
|
||||
rejectedFrame.accept(invalidFrame.getMessage());
|
||||
}
|
||||
}
|
||||
// if (messageTextRaw.equals("/QUIT")) {
|
||||
// try {
|
||||
// this.client.getReadThread().terminateConnection();
|
||||
// this.client.getReadThread().interrupt();
|
||||
// this.client.getWriteThread().terminateConnection();
|
||||
// this.client.getWriteThread().interrupt();
|
||||
// this.interrupt();
|
||||
//
|
||||
// } catch (IOException e) {
|
||||
// // TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
// }
|
||||
// }
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (IOException exception) {
|
||||
if (sessionIsActive.test(sessionId)) {
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST write failed for session " + sessionId,
|
||||
exception);
|
||||
connectionFailure.accept(exception);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
if (sessionIsActive.test(sessionId)) {
|
||||
LOGGER.log(Level.SEVERE,
|
||||
"Unexpected ON4KST writer failure for session "
|
||||
+ sessionId,
|
||||
exception);
|
||||
connectionFailure.accept(exception);
|
||||
}
|
||||
} finally {
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST writer stopped for session {0}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// while (true) {
|
||||
//
|
||||
// }
|
||||
private String formatFrame(ChatMessage message) {
|
||||
if (message == null) {
|
||||
throw new IllegalArgumentException("Cannot send an empty ON4KST message");
|
||||
}
|
||||
|
||||
if (message.isMessageDirectedToServer()) {
|
||||
return On4KstProtocol.normalizeRawFrame(message.getMessageText());
|
||||
}
|
||||
|
||||
ChatCategory category = message.getChatCategory();
|
||||
return On4KstProtocol.chatMessage(
|
||||
category == null ? defaultCategory : category.getCategoryNumber(),
|
||||
message.getMessageText());
|
||||
}
|
||||
|
||||
private void writeFrame(String frame) throws IOException {
|
||||
writer.write(frame);
|
||||
writer.write("\r\n");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
private boolean isPoisonPill(ChatMessage message) {
|
||||
return message != null
|
||||
&& ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(
|
||||
message.getMessageText())
|
||||
&& ApplicationConstants.DISCONNECT_RDR_POISONPILL.equals(
|
||||
message.getMessageSenderName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Interrupts the write loop and closes the session socket.
|
||||
*
|
||||
* @return always {@code true} after a successful close
|
||||
* @throws IOException if closing the writer or socket fails
|
||||
*/
|
||||
public boolean terminateConnection() throws IOException {
|
||||
interrupt();
|
||||
writer.close();
|
||||
socket.close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,13 @@ import java.util.TimerTask;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
/**
|
||||
* Enqueues the empty application-level reply expected by ON4KST keepalive
|
||||
* handling.
|
||||
*
|
||||
* <p>The writer owns line termination and appends exactly one CR/LF sequence.
|
||||
* Supplying an empty payload here avoids the historic double-terminator frame.</p>
|
||||
*/
|
||||
public class keepAliveMessageSenderTask extends TimerTask {
|
||||
|
||||
private ChatController client;
|
||||
@@ -16,13 +23,14 @@ public class keepAliveMessageSenderTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
|
||||
Thread.currentThread().setName("KeepAliveMessageSenderTask");
|
||||
|
||||
|
||||
// System.out.println("[keepalive: ] Thread runned now");
|
||||
|
||||
|
||||
ChatMessage keepAliveMSG = new ChatMessage();
|
||||
keepAliveMSG.setMessageText("\r");
|
||||
// WriteThread appends exactly one CRLF. An empty frame is the protocol reply.
|
||||
keepAliveMSG.setMessageText("");
|
||||
keepAliveMSG.setMessageDirectedToServer(true);
|
||||
|
||||
// System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " [keepaliveTask]: Sending keepalive: "
|
||||
@@ -33,4 +41,4 @@ public class keepAliveMessageSenderTask extends TimerTask {
|
||||
this.client.getMessageTXBus().add(keepAliveMSG);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -113,12 +113,32 @@ public final class BandOpportunityResolver {
|
||||
return detectedBands;
|
||||
}
|
||||
|
||||
/*
|
||||
* Explicit band descriptions:
|
||||
* 2m, 70cm, 23cm, 432, 1296, ...
|
||||
*/
|
||||
for (Map.Entry<Band, Pattern> entry : STATION_NAME_BAND_PATTERNS.entrySet()) {
|
||||
if (entry.getValue().matcher(stationName).find()) {
|
||||
detectedBands.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Explicit full QRGs in name field:
|
||||
* 432.357 -> 432 MHz
|
||||
* 1296.210 -> 1296 MHz
|
||||
* etc.
|
||||
*/
|
||||
for (FrequencyTextParser.DetectedFrequency detectedFrequency
|
||||
: FrequencyTextParser.findExplicitFrequencies(
|
||||
stationName
|
||||
)) {
|
||||
|
||||
detectedBands.add(
|
||||
detectedFrequency.getBand()
|
||||
);
|
||||
}
|
||||
|
||||
return detectedBands;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package kst4contest.logic;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Common parser for explicit amateur-radio frequencies embedded in text.
|
||||
*
|
||||
* <p>This parser deliberately handles only complete frequencies such as
|
||||
* 144.300, 432.357 or 10368.100. Relative forms such as ".210" or ambiguous
|
||||
* bare values such as "210" require additional message context and remain the
|
||||
* responsibility of the chat-message parser.</p>
|
||||
*/
|
||||
public final class FrequencyTextParser {
|
||||
|
||||
/*
|
||||
* Examples:
|
||||
* 50.150
|
||||
* 144.300
|
||||
* 432,357
|
||||
* 10368.100
|
||||
* 144.300.03
|
||||
*
|
||||
* At least two digits are required before the decimal separator. This
|
||||
* intentionally prevents "1.2" from being interpreted as a frequency.
|
||||
*/
|
||||
private static final Pattern EXPLICIT_FREQUENCY_PATTERN = Pattern.compile(
|
||||
"(?<![A-Z0-9])"
|
||||
+ "(\\d{2,5}[.,]\\d{1,3}(?:[.,]\\d{1,3})?)"
|
||||
+ "(?!\\d)",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
|
||||
private FrequencyTextParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all distinct explicit frequencies that fall into one of the bands
|
||||
* supported by {@link Band}.
|
||||
*/
|
||||
public static List<DetectedFrequency> findExplicitFrequencies(String text) {
|
||||
if (text == null || text.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
Matcher matcher = EXPLICIT_FREQUENCY_PATTERN.matcher(text);
|
||||
|
||||
Map<String, DetectedFrequency> uniqueFrequencies =
|
||||
new LinkedHashMap<>();
|
||||
|
||||
while (matcher.find()) {
|
||||
DetectedFrequency detected =
|
||||
parseExplicitFrequency(matcher.group(1));
|
||||
|
||||
if (detected == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String uniqueKey =
|
||||
detected.getBand().name()
|
||||
+ "|"
|
||||
+ Double.toString(
|
||||
detected.getFrequencyMHz()
|
||||
);
|
||||
|
||||
uniqueFrequencies.putIfAbsent(
|
||||
uniqueKey,
|
||||
detected
|
||||
);
|
||||
}
|
||||
|
||||
return List.copyOf(
|
||||
new ArrayList<>(uniqueFrequencies.values())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one complete frequency.
|
||||
*
|
||||
* @return detected band/frequency or {@code null} when the value is invalid
|
||||
* or outside the supported amateur bands
|
||||
*/
|
||||
public static DetectedFrequency parseExplicitFrequency(
|
||||
String rawFrequency
|
||||
) {
|
||||
if (rawFrequency == null || rawFrequency.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized =
|
||||
normalizeFrequencyString(
|
||||
rawFrequency
|
||||
.trim()
|
||||
.replace(',', '.')
|
||||
);
|
||||
|
||||
try {
|
||||
double frequencyMHz =
|
||||
Double.parseDouble(normalized);
|
||||
|
||||
if (!Double.isFinite(frequencyMHz)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Band band =
|
||||
Band.fromFrequency(frequencyMHz);
|
||||
|
||||
if (band == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DetectedFrequency(
|
||||
band,
|
||||
frequencyMHz,
|
||||
rawFrequency.trim()
|
||||
);
|
||||
|
||||
} catch (NumberFormatException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes KST-style frequency strings with an optional second decimal
|
||||
* separator.
|
||||
*
|
||||
* Examples:
|
||||
* 144.300.03 -> 144.30003
|
||||
* 144.300 -> 144.300
|
||||
*/
|
||||
private static String normalizeFrequencyString(
|
||||
String rawInput
|
||||
) {
|
||||
int firstDotIndex = rawInput.indexOf('.');
|
||||
|
||||
if (firstDotIndex < 0) {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
String decimalPart =
|
||||
rawInput.substring(firstDotIndex + 1);
|
||||
|
||||
if (!decimalPart.contains(".")) {
|
||||
return rawInput;
|
||||
}
|
||||
|
||||
decimalPart =
|
||||
decimalPart.replace(".", "");
|
||||
|
||||
return rawInput.substring(0, firstDotIndex + 1)
|
||||
+ decimalPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable result of one explicit-frequency detection.
|
||||
*/
|
||||
public static final class DetectedFrequency {
|
||||
|
||||
private final Band band;
|
||||
private final double frequencyMHz;
|
||||
private final String sourceText;
|
||||
|
||||
private DetectedFrequency(
|
||||
Band band,
|
||||
double frequencyMHz,
|
||||
String sourceText
|
||||
) {
|
||||
this.band = band;
|
||||
this.frequencyMHz = frequencyMHz;
|
||||
this.sourceText = sourceText;
|
||||
}
|
||||
|
||||
public Band getBand() {
|
||||
return band;
|
||||
}
|
||||
|
||||
public double getFrequencyMHz() {
|
||||
return frequencyMHz;
|
||||
}
|
||||
|
||||
public String getSourceText() {
|
||||
return sourceText;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,12 +83,13 @@ public class PriorityCalculator {
|
||||
// --------------------------------------------------------------------
|
||||
double score = 100.0;
|
||||
|
||||
// if (!member.isWorked()) {
|
||||
// score += 200.0;
|
||||
// }
|
||||
|
||||
//"worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
|
||||
// EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
|
||||
/*
|
||||
* A manual Sked-fail mark represents an explicit operator decision. The
|
||||
* penalty is therefore applied after all positive contributions, including
|
||||
* an imminent sked. Applying it earlier would allow the later sked boost to
|
||||
* almost completely cancel the failure mark.
|
||||
*/
|
||||
boolean manualSkedFailed = false;
|
||||
|
||||
if (workedBandsForScoring.isEmpty()) {
|
||||
score += 200.0; // never worked on any supported band -> higher priority
|
||||
@@ -190,9 +191,11 @@ public class PriorityCalculator {
|
||||
}
|
||||
|
||||
// Manual sked fail (path likely bad) => strong, permanent penalty until reset
|
||||
if (mx.manualSkedFailed) {
|
||||
score *= 0.15;
|
||||
}
|
||||
/*
|
||||
* Remember the explicit failure state here. The actual penalty is applied
|
||||
* after the sked contribution has been calculated.
|
||||
*/
|
||||
manualSkedFailed = mx.manualSkedFailed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +229,25 @@ public class PriorityCalculator {
|
||||
// --------------------------------------------------------------------
|
||||
// 8) Legacy penalty: failed attempts in ChatMember
|
||||
// --------------------------------------------------------------------
|
||||
// --------------------------------------------------------------------
|
||||
// 8) Legacy penalty: failed attempts in ChatMember
|
||||
// --------------------------------------------------------------------
|
||||
if (member.getFailedQSOAttempts() > 0) {
|
||||
score = score / (member.getFailedQSOAttempts() + 1);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// 9) Explicit operator override: manual Sked fail
|
||||
// --------------------------------------------------------------------
|
||||
if (manualSkedFailed) {
|
||||
/*
|
||||
* Apply the penalty to the complete result. This includes an imminent
|
||||
* sked and prevents its strong time-dependent boost from cancelling the
|
||||
* operator's explicit failure assessment.
|
||||
*/
|
||||
score *= 0.15;
|
||||
}
|
||||
|
||||
return Math.max(0.0, score);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
package kst4contest.logic;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Selects one realistic propagation frequency for a station.
|
||||
*
|
||||
* <p>The same resolution is used by AirScout and by the internal path analysis.
|
||||
* Only the chat categories supported by these features participate. This keeps
|
||||
* unrelated KST categories from silently falling back to 144 MHz.</p>
|
||||
*/
|
||||
public final class PropagationFrequencyResolver {
|
||||
|
||||
// private static final double DUAL_VUHF_MICROWAVE_FALLBACK_MHZ = 430.0;
|
||||
|
||||
private PropagationFrequencyResolver() {
|
||||
}
|
||||
|
||||
/** Explains why a frequency was selected. */
|
||||
public enum Source {
|
||||
CURRENT_QRG,
|
||||
STATION_NAME_QRG,
|
||||
STATION_NAME,
|
||||
DUAL_CATEGORY_FALLBACK,
|
||||
CHAT_CATEGORY
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the frequency from all active category variants of one base
|
||||
* callsign.
|
||||
*
|
||||
* <ol>
|
||||
* <li>Most recently detected QRG</li>
|
||||
* <li>Lowest band explicitly named by the station</li>
|
||||
* <li>432 MHz if the station is present in VUHF and Microwave</li>
|
||||
* <li>Lowest usable fallback band of the supported chat category</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Locally disabled and manually excluded bands are never selected.</p>
|
||||
*
|
||||
* @param variants active category variants of one callsign
|
||||
* @param enabledBands bands enabled for the local station
|
||||
* @param nowEpochMs current time used for the QRG age check
|
||||
* @return one resolution, or {@code null} if no safe choice exists
|
||||
*/
|
||||
public static Resolution resolve(Collection<ChatMember> variants,
|
||||
EnumSet<Band> enabledBands,
|
||||
long nowEpochMs) {
|
||||
|
||||
if (variants == null || variants.isEmpty()
|
||||
|| enabledBands == null || enabledBands.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ChatMember> supportedVariants = variants.stream()
|
||||
.filter(PropagationFrequencyResolver::isSupportedVariant)
|
||||
.toList();
|
||||
|
||||
if (supportedVariants.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BandOpportunityResolver.Resolution opportunityResolution =
|
||||
BandOpportunityResolver.resolve(supportedVariants, nowEpochMs);
|
||||
|
||||
EnumSet<Band> usableBands = EnumSet.copyOf(enabledBands);
|
||||
usableBands.removeAll(opportunityResolution.getNotQrvBands());
|
||||
|
||||
if (usableBands.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
FrequencyCandidate latestQrg = findLatestQrg(
|
||||
supportedVariants,
|
||||
usableBands,
|
||||
nowEpochMs
|
||||
);
|
||||
|
||||
if (latestQrg != null) {
|
||||
return new Resolution(
|
||||
latestQrg.band,
|
||||
latestQrg.frequencyMHz,
|
||||
Source.CURRENT_QRG
|
||||
);
|
||||
}
|
||||
|
||||
FrequencyCandidate stationNameQrg =
|
||||
findUniqueStationNameQrg(
|
||||
supportedVariants,
|
||||
usableBands
|
||||
);
|
||||
|
||||
if (stationNameQrg != null) {
|
||||
return new Resolution(
|
||||
stationNameQrg.band,
|
||||
stationNameQrg.frequencyMHz,
|
||||
Source.STATION_NAME_QRG
|
||||
);
|
||||
}
|
||||
|
||||
EnumSet<Band> nameBands = EnumSet.noneOf(Band.class);
|
||||
for (ChatMember variant : supportedVariants) {
|
||||
nameBands.addAll(
|
||||
BandOpportunityResolver.detectBandsFromStationName(variant.getName())
|
||||
);
|
||||
}
|
||||
nameBands.retainAll(usableBands);
|
||||
|
||||
Band nameBand = lowestBand(nameBands);
|
||||
if (nameBand != null) {
|
||||
return new Resolution(
|
||||
nameBand,
|
||||
nameBand.getDefaultAnalysisFrequencyMHz(),
|
||||
Source.STATION_NAME
|
||||
);
|
||||
}
|
||||
|
||||
EnumSet<SupportedCategory> categories = collectSupportedCategories(supportedVariants);
|
||||
|
||||
if (categories.contains(SupportedCategory.VUHF)
|
||||
&& categories.contains(SupportedCategory.MICROWAVE)
|
||||
&& usableBands.contains(Band.B_432)) {
|
||||
return new Resolution(
|
||||
Band.B_432,
|
||||
Band.B_432.getDefaultAnalysisFrequencyMHz(),
|
||||
Source.DUAL_CATEGORY_FALLBACK
|
||||
);
|
||||
}
|
||||
|
||||
EnumSet<Band> categoryBands = EnumSet.noneOf(Band.class);
|
||||
for (SupportedCategory category : categories) {
|
||||
categoryBands.addAll(category.fallbackBands);
|
||||
}
|
||||
categoryBands.retainAll(usableBands);
|
||||
|
||||
Band categoryBand = lowestBand(categoryBands);
|
||||
if (categoryBand == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Resolution(
|
||||
categoryBand,
|
||||
categoryBand.getDefaultAnalysisFrequencyMHz(),
|
||||
Source.CHAT_CATEGORY
|
||||
);
|
||||
}
|
||||
|
||||
private static FrequencyCandidate findLatestQrg(List<ChatMember> variants,
|
||||
EnumSet<Band> usableBands,
|
||||
long nowEpochMs) {
|
||||
FrequencyCandidate latest = null;
|
||||
|
||||
for (ChatMember variant : variants) {
|
||||
for (var entry : variant.getKnownActiveBands().entrySet()) {
|
||||
Band band = entry.getKey();
|
||||
ChatMember.ActiveFrequencyInfo info = entry.getValue();
|
||||
|
||||
if (band == null || info == null || !usableBands.contains(band)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long ageMs = nowEpochMs - info.timestampEpoch;
|
||||
if (ageMs < 0L
|
||||
|| ageMs > BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS
|
||||
|| !Double.isFinite(info.frequency)
|
||||
|| !band.isPlausible(info.frequency)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (latest == null || info.timestampEpoch > latest.timestampEpochMs) {
|
||||
latest = new FrequencyCandidate(
|
||||
band,
|
||||
info.frequency,
|
||||
info.timestampEpoch
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return latest;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves one exact station-name QRG only when the evidence is unambiguous.
|
||||
*
|
||||
* <p>The same QRG repeated in several category variants counts only once.
|
||||
* If different explicit QRGs are advertised, no run frequency is guessed and
|
||||
* the caller continues with normal band-name/category resolution.</p>
|
||||
*/
|
||||
private static FrequencyCandidate findUniqueStationNameQrg(
|
||||
List<ChatMember> variants,
|
||||
EnumSet<Band> usableBands
|
||||
) {
|
||||
java.util.LinkedHashMap<String, FrequencyCandidate> uniqueCandidates =
|
||||
new java.util.LinkedHashMap<>();
|
||||
|
||||
for (ChatMember variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (FrequencyTextParser.DetectedFrequency detectedFrequency
|
||||
: FrequencyTextParser.findExplicitFrequencies(
|
||||
variant.getName()
|
||||
)) {
|
||||
|
||||
Band band =
|
||||
detectedFrequency.getBand();
|
||||
|
||||
if (!usableBands.contains(band)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double frequencyMHz =
|
||||
detectedFrequency.getFrequencyMHz();
|
||||
|
||||
String key =
|
||||
band.name()
|
||||
+ "|"
|
||||
+ Double.toString(frequencyMHz);
|
||||
|
||||
uniqueCandidates.putIfAbsent(
|
||||
key,
|
||||
new FrequencyCandidate(
|
||||
band,
|
||||
frequencyMHz,
|
||||
Long.MIN_VALUE
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* More than one different exact QRG means that we cannot safely
|
||||
* identify one run frequency.
|
||||
*/
|
||||
if (uniqueCandidates.size() > 1) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueCandidates.size() == 1
|
||||
? uniqueCandidates.values()
|
||||
.iterator()
|
||||
.next()
|
||||
: null;
|
||||
}
|
||||
|
||||
|
||||
private static boolean isSupportedVariant(ChatMember member) {
|
||||
if (member == null || member.getChatCategory() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int categoryNumber = member.getChatCategory().getCategoryNumber();
|
||||
return categoryNumber == ChatCategory.FIFTYSEVENTYMHz
|
||||
|| categoryNumber == ChatCategory.VUHF
|
||||
|| categoryNumber == ChatCategory.MICROWAVE
|
||||
|| categoryNumber == ChatCategory.EMEJT65;
|
||||
}
|
||||
|
||||
private static EnumSet<SupportedCategory> collectSupportedCategories(
|
||||
List<ChatMember> variants
|
||||
) {
|
||||
EnumSet<SupportedCategory> categories = EnumSet.noneOf(SupportedCategory.class);
|
||||
|
||||
for (ChatMember variant : variants) {
|
||||
int categoryNumber = variant.getChatCategory().getCategoryNumber();
|
||||
|
||||
if (categoryNumber == ChatCategory.FIFTYSEVENTYMHz) {
|
||||
categories.add(SupportedCategory.FIFTY_SEVENTY);
|
||||
} else if (categoryNumber == ChatCategory.VUHF) {
|
||||
categories.add(SupportedCategory.VUHF);
|
||||
} else if (categoryNumber == ChatCategory.MICROWAVE) {
|
||||
categories.add(SupportedCategory.MICROWAVE);
|
||||
} else if (categoryNumber == ChatCategory.EMEJT65) {
|
||||
categories.add(SupportedCategory.EME);
|
||||
}
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static Band lowestBand(Collection<Band> bands) {
|
||||
if (bands == null || bands.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bands.stream()
|
||||
.min(Comparator.comparingDouble(Band::getDefaultAnalysisFrequencyMHz))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private enum SupportedCategory {
|
||||
FIFTY_SEVENTY(EnumSet.of(Band.B_50, Band.B_70)),
|
||||
VUHF(EnumSet.of(Band.B_144, Band.B_432)),
|
||||
MICROWAVE(EnumSet.of(
|
||||
Band.B_1296,
|
||||
Band.B_2320,
|
||||
Band.B_3400,
|
||||
Band.B_5760,
|
||||
Band.B_10G,
|
||||
Band.B_24G
|
||||
)),
|
||||
EME(EnumSet.of(
|
||||
Band.B_144,
|
||||
Band.B_432,
|
||||
Band.B_1296,
|
||||
Band.B_2320,
|
||||
Band.B_3400,
|
||||
Band.B_5760,
|
||||
Band.B_10G,
|
||||
Band.B_24G
|
||||
));
|
||||
|
||||
private final EnumSet<Band> fallbackBands;
|
||||
|
||||
SupportedCategory(EnumSet<Band> fallbackBands) {
|
||||
this.fallbackBands = fallbackBands;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FrequencyCandidate {
|
||||
|
||||
private final Band band;
|
||||
private final double frequencyMHz;
|
||||
private final long timestampEpochMs;
|
||||
|
||||
private FrequencyCandidate(Band band,
|
||||
double frequencyMHz,
|
||||
long timestampEpochMs) {
|
||||
this.band = band;
|
||||
this.frequencyMHz = frequencyMHz;
|
||||
this.timestampEpochMs = timestampEpochMs;
|
||||
}
|
||||
}
|
||||
|
||||
/** Immutable selected band/frequency pair. */
|
||||
public static final class Resolution {
|
||||
|
||||
private final Band band;
|
||||
private final double analysisFrequencyMHz;
|
||||
private final Source source;
|
||||
|
||||
private Resolution(Band band,
|
||||
double analysisFrequencyMHz,
|
||||
Source source) {
|
||||
this.band = band;
|
||||
this.analysisFrequencyMHz = analysisFrequencyMHz;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public Band getBand() {
|
||||
return band;
|
||||
}
|
||||
|
||||
public double getAnalysisFrequencyMHz() {
|
||||
return analysisFrequencyMHz;
|
||||
}
|
||||
|
||||
public Source getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts MHz to AirScout's 100-Hz protocol unit.
|
||||
*
|
||||
* @return integer protocol value, for example 1442100 for 144.210 MHz
|
||||
*/
|
||||
public String getAirScoutBandValue() {
|
||||
return Long.toString(Math.round(analysisFrequencyMHz * 10_000.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -518,6 +518,7 @@ public class ChatMember {
|
||||
this.frequency = frequency;
|
||||
}
|
||||
|
||||
|
||||
public long getActivityTimeLastInEpoch() {
|
||||
return activityTimeLastInEpoch;
|
||||
}
|
||||
@@ -595,6 +596,40 @@ public class ChatMember {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes the compatibility frequency only when no frequency is known yet.
|
||||
*
|
||||
* <p>This is used for an explicit QRG contained in the ON4KST station name.
|
||||
* A subsequently detected chat QRG may overwrite this initial value, but a
|
||||
* station-name QRG never replaces an already known frequency.</p>
|
||||
*
|
||||
* @param frequencyMhz explicit station-name frequency
|
||||
* @return true when the frequency was initialized
|
||||
*/
|
||||
public boolean initializeFrequencyIfEmpty(double frequencyMhz) {
|
||||
if (!Double.isFinite(frequencyMhz)
|
||||
|| frequencyMhz <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (frequency == null) {
|
||||
frequency = new SimpleStringProperty();
|
||||
}
|
||||
|
||||
String currentFrequency = frequency.get();
|
||||
|
||||
if (currentFrequency != null
|
||||
&& !currentFrequency.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frequency.set(
|
||||
Double.toString(frequencyMhz)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets all worked information of this object to false. Scope: GUI, Reset Button
|
||||
* for worked info, called by appcontroller
|
||||
|
||||
@@ -224,7 +224,7 @@ public class ChatPreferences {
|
||||
boolean logsynch_wintestNetworkListenerEnabled = true; // default true = bisheriges Verhalten
|
||||
String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test
|
||||
boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP
|
||||
String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO
|
||||
String logsynch_wintestSkedMode = "SSB"; // Supported values: SSB or CW
|
||||
boolean logsynch_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet
|
||||
boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet
|
||||
|
||||
@@ -245,6 +245,7 @@ public class ChatPreferences {
|
||||
boolean AirScout_asUDPListenerEnabled = true;
|
||||
String AirScout_asServerNameString = "AS";
|
||||
String AirScout_asClientNameString = "KST";
|
||||
boolean AirScout_autoBandSelectionEnabled = true;
|
||||
String AirScout_asBandString = "1440000";
|
||||
int AirScout_asCommunicationPort = 9872;
|
||||
|
||||
@@ -819,7 +820,11 @@ public class ChatPreferences {
|
||||
}
|
||||
|
||||
public void setStn_pstRotatorPort(int stn_pstRotatorPort) {
|
||||
if (stn_pstRotatorPort < 1 || stn_pstRotatorPort > 65535) {
|
||||
/*
|
||||
* PSTRotator sends position reports to the configured UDP port + 1.
|
||||
* Port 65535 would therefore require the invalid local port 65536.
|
||||
*/
|
||||
if (stn_pstRotatorPort < 1 || stn_pstRotatorPort > 65534) {
|
||||
this.stn_pstRotatorPort = 12000;
|
||||
} else {
|
||||
this.stn_pstRotatorPort = stn_pstRotatorPort;
|
||||
@@ -1004,6 +1009,16 @@ public class ChatPreferences {
|
||||
return AirScout_asBandString;
|
||||
}
|
||||
|
||||
public boolean isAirScout_autoBandSelectionEnabled() {
|
||||
return AirScout_autoBandSelectionEnabled;
|
||||
}
|
||||
|
||||
public void setAirScout_autoBandSelectionEnabled(
|
||||
boolean airScoutAutoBandSelectionEnabled
|
||||
) {
|
||||
AirScout_autoBandSelectionEnabled = airScoutAutoBandSelectionEnabled;
|
||||
}
|
||||
|
||||
public void setAirScout_asBandString(String airScout_asBandString) {
|
||||
if (airScout_asBandString == null) {
|
||||
AirScout_asBandString = "1440000";
|
||||
@@ -1695,6 +1710,13 @@ public class ChatPreferences {
|
||||
asQry_airScoutUDPPort.setTextContent(this.getAirScout_asCommunicationPort()+"");
|
||||
AirScoutQuerier.appendChild(asQry_airScoutUDPPort);
|
||||
|
||||
Element asQry_airScoutAutoBandSelectionEnabled =
|
||||
doc.createElement("asQry_airScoutAutoBandSelectionEnabled");
|
||||
asQry_airScoutAutoBandSelectionEnabled.setTextContent(
|
||||
Boolean.toString(this.isAirScout_autoBandSelectionEnabled())
|
||||
);
|
||||
AirScoutQuerier.appendChild(asQry_airScoutAutoBandSelectionEnabled);
|
||||
|
||||
Element asQry_airScoutBandValue = doc.createElement("asQry_airScoutBandValue");
|
||||
asQry_airScoutBandValue.setTextContent(this.getAirScout_asBandString());
|
||||
AirScoutQuerier.appendChild(asQry_airScoutBandValue);
|
||||
@@ -2508,6 +2530,14 @@ public class ChatPreferences {
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_autoBandSelectionEnabled(
|
||||
getBoolean(
|
||||
airScoutEl,
|
||||
AirScout_autoBandSelectionEnabled,
|
||||
"asQry_airScoutAutoBandSelectionEnabled"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asBandString(
|
||||
getText(
|
||||
airScoutEl,
|
||||
@@ -2525,6 +2555,8 @@ public class ChatPreferences {
|
||||
+ AirScout_asClientNameString
|
||||
+ ", port="
|
||||
+ AirScout_asCommunicationPort
|
||||
+ ", automatic band selection="
|
||||
+ AirScout_autoBandSelectionEnabled
|
||||
+ ", band="
|
||||
+ AirScout_asBandString
|
||||
);
|
||||
@@ -2599,10 +2631,15 @@ public class ChatPreferences {
|
||||
|
||||
if (callSign != null
|
||||
&& !callSign.isBlank()) {
|
||||
/*
|
||||
* Older preference files may still contain complete KST callsigns
|
||||
* such as DN9APW-2. Monitoring is stored by base callsign so that
|
||||
* every suffix of the same station is covered by one entry.
|
||||
*/
|
||||
String normalizedCallSign =
|
||||
callSign
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
ChatMember.normalizeCallSignToBaseCallSign(
|
||||
callSign
|
||||
);
|
||||
|
||||
if (!lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.contains(normalizedCallSign)) {
|
||||
@@ -2628,12 +2665,28 @@ public class ChatPreferences {
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
if (child.getNodeType() == Node.ELEMENT_NODE) {
|
||||
String word = child.getTextContent();
|
||||
if (word != null) {
|
||||
word = word.trim();
|
||||
}
|
||||
if (word != null && !word.isEmpty()) {
|
||||
lstNotify_QSOSniffer_sniffedWordsList.add(word);
|
||||
String callSign = child.getTextContent();
|
||||
|
||||
if (callSign != null
|
||||
&& !callSign.isBlank()) {
|
||||
|
||||
/*
|
||||
* Older preference files may contain complete KST callsigns
|
||||
* such as DN9APW-2. QSO monitoring is stored by base callsign,
|
||||
* so one entry covers every suffix of the same station.
|
||||
*/
|
||||
String normalizedCallSign =
|
||||
ChatMember.normalizeCallSignToBaseCallSign(
|
||||
callSign
|
||||
);
|
||||
|
||||
if (normalizedCallSign != null
|
||||
&& !normalizedCallSign.isBlank()
|
||||
&& !lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.contains(normalizedCallSign)) {
|
||||
lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.add(normalizedCallSign);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,25 +3,58 @@ package kst4contest.model;
|
||||
/**
|
||||
* Represents a scheduled event or an AirScout opportunity in the future.
|
||||
* Used for the Timeline View and Priority Calculation.
|
||||
*
|
||||
* <p>The base callsign remains the grouping key for scoring and worked-state
|
||||
* handling. The exact KST login and its chat category are stored separately
|
||||
* because reminders and external logger handover refer to the selected
|
||||
* ChatMember entity.</p>
|
||||
*/
|
||||
public class ContestSked {
|
||||
|
||||
private String targetCallsign;
|
||||
private double targetAzimuth; // Required for Antenna-Visuals
|
||||
private long skedTimeEpoch; // The peak time (e.g., AP)
|
||||
private String targetChatCallsign;
|
||||
private ChatCategory targetChatCategory;
|
||||
private double targetAzimuth;
|
||||
private long skedTimeEpoch;
|
||||
private Band band;
|
||||
|
||||
// Opportunity potential (0..100). -1 means "unknown".
|
||||
int opportunityPotentialPercent = -1;
|
||||
|
||||
// Status flags to prevent spamming alarms
|
||||
// Status flags to prevent spamming alarms.
|
||||
private boolean warning3MinSent = false;
|
||||
private boolean warningNowSent = false;
|
||||
|
||||
public ContestSked(String call, double azimuth, long time, Band b) {
|
||||
this.targetCallsign = call;
|
||||
/**
|
||||
* Backward-compatible constructor.
|
||||
*/
|
||||
public ContestSked(String call, double azimuth, long time, Band band) {
|
||||
this(call, call, null, azimuth, time, band);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a sked for one exact KST login.
|
||||
*
|
||||
* @param callRaw base callsign used for scoring and worked states
|
||||
* @param chatCallsign exact KST login, including an optional dash suffix
|
||||
* @param chatCategory category in which the selected login is active
|
||||
* @param azimuth target azimuth
|
||||
* @param time sked time in epoch milliseconds
|
||||
* @param band selected amateur-radio band
|
||||
*/
|
||||
public ContestSked(String callRaw,
|
||||
String chatCallsign,
|
||||
ChatCategory chatCategory,
|
||||
double azimuth,
|
||||
long time,
|
||||
Band band) {
|
||||
|
||||
this.targetCallsign = callRaw;
|
||||
this.targetChatCallsign = chatCallsign;
|
||||
this.targetChatCategory = chatCategory;
|
||||
this.targetAzimuth = azimuth;
|
||||
this.skedTimeEpoch = time;
|
||||
this.band = b;
|
||||
this.band = band;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,15 +65,54 @@ public class ContestSked {
|
||||
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
|
||||
}
|
||||
|
||||
// Getters and Setters...
|
||||
public String getTargetCallsign() { return targetCallsign; }
|
||||
public double getTargetAzimuth() { return targetAzimuth; }
|
||||
public long getSkedTimeEpoch() { return skedTimeEpoch; }
|
||||
public Band getBand() { return band; }
|
||||
public boolean isWarning3MinSent() { return warning3MinSent; }
|
||||
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
|
||||
public boolean isWarningNowSent() { return warningNowSent; }
|
||||
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
|
||||
/**
|
||||
* Returns the base callsign used for scoring and worked-state grouping.
|
||||
*/
|
||||
public String getTargetCallsign() {
|
||||
return targetCallsign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exact KST login selected when the sked was created.
|
||||
*/
|
||||
public String getTargetChatCallsign() {
|
||||
if (targetChatCallsign == null || targetChatCallsign.isBlank()) {
|
||||
return targetCallsign;
|
||||
}
|
||||
return targetChatCallsign;
|
||||
}
|
||||
|
||||
public ChatCategory getTargetChatCategory() {
|
||||
return targetChatCategory;
|
||||
}
|
||||
|
||||
public double getTargetAzimuth() {
|
||||
return targetAzimuth;
|
||||
}
|
||||
|
||||
public long getSkedTimeEpoch() {
|
||||
return skedTimeEpoch;
|
||||
}
|
||||
|
||||
public Band getBand() {
|
||||
return band;
|
||||
}
|
||||
|
||||
public boolean isWarning3MinSent() {
|
||||
return warning3MinSent;
|
||||
}
|
||||
|
||||
public void setWarning3MinSent(boolean warning3MinSent) {
|
||||
this.warning3MinSent = warning3MinSent;
|
||||
}
|
||||
|
||||
public boolean isWarningNowSent() {
|
||||
return warningNowSent;
|
||||
}
|
||||
|
||||
public void setWarningNowSent(boolean warningNowSent) {
|
||||
this.warningNowSent = warningNowSent;
|
||||
}
|
||||
|
||||
public int getOpportunityPotentialPercent() {
|
||||
return opportunityPotentialPercent;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
import kst4contest.model.Band;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FrequencyTextParserTest {
|
||||
|
||||
@Test
|
||||
void detectsExplicitStationNameFrequencies() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"Phil 432.357"
|
||||
);
|
||||
|
||||
assertEquals(1, detected.size());
|
||||
assertEquals(
|
||||
Band.B_432,
|
||||
detected.get(0).getBand()
|
||||
);
|
||||
assertEquals(
|
||||
432.357,
|
||||
detected.get(0).getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsCommaAndMicrowaveFrequencies() {
|
||||
List<FrequencyTextParser.DetectedFrequency> detected =
|
||||
FrequencyTextParser.findExplicitFrequencies(
|
||||
"QRV 1296,210 / 10368.100"
|
||||
);
|
||||
|
||||
assertEquals(2, detected.size());
|
||||
|
||||
assertEquals(
|
||||
Band.B_1296,
|
||||
detected.get(0).getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_10G,
|
||||
detected.get(1).getBand()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detectsFiftyAndSeventyMhzFrequencies() {
|
||||
assertEquals(
|
||||
Band.B_50,
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies("50.150")
|
||||
.get(0)
|
||||
.getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_70,
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies("70.200")
|
||||
.get(0)
|
||||
.getBand()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ignoresRelativeAndAmbiguousValues() {
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"Mike .180"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"Mike 180"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"David 1.2"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"RST 599"
|
||||
)
|
||||
.isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void normalizesSubKhzNotation() {
|
||||
FrequencyTextParser.DetectedFrequency detected =
|
||||
FrequencyTextParser
|
||||
.findExplicitFrequencies(
|
||||
"144.300.03"
|
||||
)
|
||||
.get(0);
|
||||
|
||||
assertEquals(Band.B_144, detected.getBand());
|
||||
assertEquals(
|
||||
144.30003,
|
||||
detected.getFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import kst4contest.controller.Utils4KST;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class Utils4KSTFrequencyTest {
|
||||
|
||||
@Test
|
||||
void convertsCompleteFrequenciesAcrossAllSupportedPrefixLengths() {
|
||||
assertEquals("50200.0", convert("50.200", "144"));
|
||||
assertEquals("70250.0", convert("70,250", "144"));
|
||||
assertEquals("144205.0", convert("144.205", "432"));
|
||||
assertEquals("432088.0", convert("432.088", "144"));
|
||||
assertEquals("1296338.0", convert("1296.338", "144"));
|
||||
assertEquals("10368100.0", convert("10368.100", "144"));
|
||||
assertEquals("24048100.0", convert("24048.100", "144"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void retainsSubKhzPrecision() {
|
||||
assertEquals("144205.2", convert("144.205.2", "432"));
|
||||
assertEquals("1296338.25", convert("1296.338.25", "144"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsCompactLegacyFormats() {
|
||||
assertEquals("432088.0", convert("432088", "144"));
|
||||
assertEquals("10368100.0", convert("10368100", "144"));
|
||||
assertEquals("432088.2", convert("432088.2", "144"));
|
||||
assertEquals("432088.0", convert("432 088", "144"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void appliesFallbackOnlyToRelativeFrequencies() {
|
||||
assertEquals("432205.0", convert(".205", "432"));
|
||||
assertEquals("144205.0", convert("205", "144"));
|
||||
assertEquals("10368300.0", convert(".300", "10368"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrUnsupportedValues() {
|
||||
assertEquals("", convert(null, "144"));
|
||||
assertEquals("", convert("", "144"));
|
||||
assertEquals("", convert("not-a-frequency", "144"));
|
||||
assertEquals("", convert("599", "144"));
|
||||
assertEquals("", convert(".205", null));
|
||||
assertEquals("", convert(".205", "invalid"));
|
||||
}
|
||||
|
||||
private String convert(String frequency, String fallbackPrefix) {
|
||||
SimpleStringProperty fallback = fallbackPrefix == null
|
||||
? null
|
||||
: new SimpleStringProperty(fallbackPrefix);
|
||||
|
||||
return Utils4KST.normalizeFrequencyString(
|
||||
frequency,
|
||||
fallback
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,66 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import kst4contest.controller.ChatController;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import java.net.URL;
|
||||
|
||||
public class GuiUtils {
|
||||
|
||||
private static final String APPLICATION_ICON_RESOURCE = "/icons/kst4contest.png";
|
||||
|
||||
private static Image applicationIcon;
|
||||
|
||||
/**
|
||||
* Applies the common KST4Contest application icon to a JavaFX stage.
|
||||
*
|
||||
* <p>The icon is loaded only once and reused for all application windows.
|
||||
* A missing icon resource must never prevent a window from opening.</p>
|
||||
*
|
||||
* @param stage stage that should receive the application icon
|
||||
*/
|
||||
public static void applyApplicationIcon(Stage stage) {
|
||||
|
||||
if (stage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Image icon = getApplicationIcon();
|
||||
|
||||
if (icon != null && !stage.getIcons().contains(icon)) {
|
||||
stage.getIcons().add(icon);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and caches the common KST4Contest application icon.
|
||||
*
|
||||
* @return application icon or null if the resource is unavailable
|
||||
*/
|
||||
private static Image getApplicationIcon() {
|
||||
|
||||
if (applicationIcon != null) {
|
||||
return applicationIcon;
|
||||
}
|
||||
|
||||
URL iconUrl = GuiUtils.class.getResource(APPLICATION_ICON_RESOURCE);
|
||||
|
||||
if (iconUrl == null) {
|
||||
System.err.println(
|
||||
"Application icon resource not found: "
|
||||
+ APPLICATION_ICON_RESOURCE
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
applicationIcon = new Image(iconUrl.toExternalForm());
|
||||
return applicationIcon;
|
||||
}
|
||||
|
||||
private static final String PTRN_CALLSIGNSYNTAX = "^(?:[A-Z]{1,2}[0-9]|[0-9][A-Z])[0-9A-Z]{1,3}$";
|
||||
/**
|
||||
* Checks wheter the input value of the String is numeric or not, true if yes
|
||||
|
||||
@@ -208,7 +208,12 @@ public class TimelineView extends Pane {
|
||||
|
||||
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent()));
|
||||
|
||||
String baseToolTipFallBack = sked.getTargetCallsign() + " (" + sked.getBand() + ")\nAz: " + sked.getTargetAzimuth();
|
||||
String baseToolTipFallBack =
|
||||
sked.getTargetChatCallsign()
|
||||
+ " ("
|
||||
+ sked.getBand()
|
||||
+ ")\nAz: "
|
||||
+ sked.getTargetAzimuth();
|
||||
|
||||
if (skedTooltipExtraTextProvider != null) {
|
||||
String extra = skedTooltipExtraTextProvider.apply(sked);
|
||||
@@ -220,7 +225,9 @@ public class TimelineView extends Pane {
|
||||
Tooltip t = new Tooltip(baseToolTipFallBack);
|
||||
Tooltip.install(diamond, t);
|
||||
|
||||
Label lbl = new Label("SKED: " + sked.getTargetCallsign());
|
||||
Label lbl = new Label(
|
||||
"SKED: " + sked.getTargetChatCallsign()
|
||||
);
|
||||
// lbl.setFont(new Font(9));
|
||||
// lbl.setTextFill(Color.WHITE);
|
||||
lbl.setLayoutY(14);
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import kst4contest.logic.FrequencyTextParser;
|
||||
|
||||
/**
|
||||
* Builds immutable map snapshots from the currently visible chat members.
|
||||
@@ -204,6 +205,92 @@ public final class MapCallsignRawSnapshotBuilder {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* A QRG explicitly contained in the current station name does not expire.
|
||||
*
|
||||
* Recent dynamic QRG evidence has priority. Therefore station-name QRGs fill
|
||||
* only bands for which no recent dynamic frequency is available.
|
||||
*
|
||||
* Different explicit QRGs for the same band are considered ambiguous and are
|
||||
* not reduced to one arbitrary run frequency.
|
||||
*/
|
||||
Map<Band, Map<String, FrequencyTextParser.DetectedFrequency>>
|
||||
stationNameFrequenciesByBand =
|
||||
new EnumMap<>(Band.class);
|
||||
|
||||
for (ChatMember variant : variants) {
|
||||
if (variant == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (FrequencyTextParser.DetectedFrequency detectedFrequency
|
||||
: FrequencyTextParser.findExplicitFrequencies(
|
||||
variant.getName()
|
||||
)) {
|
||||
|
||||
Band band =
|
||||
detectedFrequency.getBand();
|
||||
|
||||
if (availableBands == null
|
||||
|| !availableBands.contains(band)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
stationNameFrequenciesByBand
|
||||
.computeIfAbsent(
|
||||
band,
|
||||
ignored -> new LinkedHashMap<>()
|
||||
)
|
||||
.putIfAbsent(
|
||||
Double.toString(
|
||||
detectedFrequency.getFrequencyMHz()
|
||||
),
|
||||
detectedFrequency
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<
|
||||
Band,
|
||||
Map<String, FrequencyTextParser.DetectedFrequency>>
|
||||
entry : stationNameFrequenciesByBand.entrySet()) {
|
||||
|
||||
Band band = entry.getKey();
|
||||
|
||||
/*
|
||||
* A recent QRG detected from chat always wins.
|
||||
*/
|
||||
if (latestByBand.containsKey(band)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* Do not guess when several different QRGs were published for the
|
||||
* same band.
|
||||
*/
|
||||
if (entry.getValue().size() != 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FrequencyTextParser.DetectedFrequency detectedFrequency =
|
||||
entry.getValue()
|
||||
.values()
|
||||
.iterator()
|
||||
.next();
|
||||
|
||||
latestByBand.put(
|
||||
band,
|
||||
new FrequencyCandidate(
|
||||
band,
|
||||
formatFrequency(
|
||||
detectedFrequency.getFrequencyMHz()
|
||||
),
|
||||
Long.MIN_VALUE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
LinkedHashMap<String, String> ordered = new LinkedHashMap<>();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package kst4contest.view.map;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -496,37 +495,7 @@ public final class PathGeometryUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves one usable analysis frequency from aggregated marker frequency data.
|
||||
*
|
||||
* <p>Strategy:
|
||||
* <ol>
|
||||
* <li>Prefer 144 MHz data if available</li>
|
||||
* <li>Otherwise use the first parsable known station frequency</li>
|
||||
* <li>Otherwise use the central default frequency</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param frequenciesByBand known frequencies grouped by band
|
||||
* @return resolved analysis frequency in MHz
|
||||
*/
|
||||
public static double resolveAnalysisFrequencyMHz(Map<String, String> frequenciesByBand) {
|
||||
if (frequenciesByBand != null && !frequenciesByBand.isEmpty()) {
|
||||
String band144Text = frequenciesByBand.get("144");
|
||||
double parsed144 = tryParseFrequencyMHz(band144Text);
|
||||
if (Double.isFinite(parsed144) && parsed144 > 0.0) {
|
||||
return parsed144;
|
||||
}
|
||||
|
||||
for (String value : frequenciesByBand.values()) {
|
||||
double parsedFrequencyMHz = tryParseFrequencyMHz(value);
|
||||
if (Double.isFinite(parsedFrequencyMHz) && parsedFrequencyMHz > 0.0) {
|
||||
return parsedFrequencyMHz;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_ANALYSIS_FREQUENCY_MHZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Small immutable geographic point used by great-circle interpolation.
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.util.Objects;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import java.util.function.Predicate;
|
||||
@@ -40,6 +41,7 @@ public final class StationMapBridge {
|
||||
private final TableView<ChatMember> chatMemberTable;
|
||||
private final StationMapView stationMapView;
|
||||
private final Consumer<ChatMember> focusChatMemberConsumer;
|
||||
private final Supplier<Band> reachabilityBandOverrideSupplier;
|
||||
|
||||
|
||||
|
||||
@@ -55,22 +57,30 @@ public final class StationMapBridge {
|
||||
public StationMapBridge(ChatController chatController,
|
||||
TableView<ChatMember> chatMemberTable,
|
||||
StationMapView stationMapView,
|
||||
Consumer<ChatMember> focusChatMemberConsumer) {
|
||||
Consumer<ChatMember> focusChatMemberConsumer,
|
||||
Supplier<Band> reachabilityBandOverrideSupplier) {
|
||||
|
||||
this.chatController = Objects.requireNonNull(chatController, "chatController");
|
||||
this.chatMemberTable = Objects.requireNonNull(chatMemberTable, "chatMemberTable");
|
||||
this.stationMapView = Objects.requireNonNull(stationMapView, "stationMapView");
|
||||
this.focusChatMemberConsumer = Objects.requireNonNull(focusChatMemberConsumer, "focusChatMemberConsumer");
|
||||
this.focusChatMemberConsumer = Objects.requireNonNull(
|
||||
focusChatMemberConsumer,
|
||||
"focusChatMemberConsumer"
|
||||
);
|
||||
this.reachabilityBandOverrideSupplier = Objects.requireNonNull(
|
||||
reachabilityBandOverrideSupplier,
|
||||
"reachabilityBandOverrideSupplier"
|
||||
);
|
||||
|
||||
this.refreshCoalescer.setOnFinished(event -> refreshNow());
|
||||
|
||||
|
||||
}
|
||||
|
||||
public void install() {
|
||||
stationMapView.setOnCallsignRawSelected(this::handleMapCallsignSelection);
|
||||
stationMapView.setOnTriggerClusterSpot(this::handleExplicitClusterSpot);
|
||||
|
||||
stationMapView.setOnResetView(this::handleMapReset);
|
||||
|
||||
chatController.getLst_chatMemberSortedFilteredList().addListener(
|
||||
(ListChangeListener<ChatMember>) change -> scheduleRefresh()
|
||||
);
|
||||
@@ -90,6 +100,35 @@ public final class StationMapBridge {
|
||||
requestImmediateRefresh();
|
||||
}
|
||||
|
||||
private void handleMapReset() {
|
||||
Runnable resetAction = () -> {
|
||||
/*
|
||||
* Ignore an analysis result that may still arrive for the
|
||||
* previously selected station.
|
||||
*/
|
||||
pathAnalysisGeneration.incrementAndGet();
|
||||
lastPathAnalysisRequestSignature = "";
|
||||
|
||||
/*
|
||||
* Clear the application's central station selection.
|
||||
*/
|
||||
chatController.getScoreService().setSelectedChatMember(null);
|
||||
|
||||
/*
|
||||
* Keep the main station table synchronized with the central selection.
|
||||
*/
|
||||
chatMemberTable.getSelectionModel().clearSelection();
|
||||
|
||||
requestImmediateRefresh();
|
||||
};
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
resetAction.run();
|
||||
} else {
|
||||
Platform.runLater(resetAction);
|
||||
}
|
||||
}
|
||||
|
||||
public void showWindow() {
|
||||
stationMapView.showWindow();
|
||||
requestImmediateRefresh();
|
||||
@@ -115,6 +154,18 @@ public final class StationMapBridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the currently selected map path to be requested again.
|
||||
*
|
||||
* <p>This is used by the explicit "Calc selected" action after the operator
|
||||
* changed the reachability band. Merely changing the ComboBox still does not
|
||||
* trigger terrain analysis.</p>
|
||||
*/
|
||||
public void requestSelectedPathAnalysisRefresh() {
|
||||
lastPathAnalysisRequestSignature = "";
|
||||
requestImmediateRefresh();
|
||||
}
|
||||
|
||||
public void focusSelectedCallsign() {
|
||||
showWindow();
|
||||
|
||||
@@ -222,9 +273,12 @@ public final class StationMapBridge {
|
||||
|
||||
ChatMember selectedMember = resolveBestChatMember(targetCallsignRaw);
|
||||
|
||||
Band requestedBandOverride = reachabilityBandOverrideSupplier.get();
|
||||
|
||||
chatController.getReachabilityService().requestPathAnalysisForMap(
|
||||
selectedMember,
|
||||
selectedSnapshot,
|
||||
requestedBandOverride,
|
||||
result -> {
|
||||
if (generation != pathAnalysisGeneration.get()) {
|
||||
return;
|
||||
@@ -232,6 +286,7 @@ public final class StationMapBridge {
|
||||
stationMapView.setPathAnalysisResult(result);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,6 +314,13 @@ public final class StationMapBridge {
|
||||
chatMemberTable.scrollTo(resolved);
|
||||
|
||||
focusChatMemberConsumer.accept(resolved);
|
||||
|
||||
/*
|
||||
* A map click is an explicit operator action. Clear the signature so a
|
||||
* newly selected reachability band is honored even when the same station
|
||||
* is clicked again.
|
||||
*/
|
||||
lastPathAnalysisRequestSignature = "";
|
||||
requestImmediateRefresh();
|
||||
});
|
||||
}
|
||||
@@ -315,10 +377,16 @@ public final class StationMapBridge {
|
||||
|
||||
private double resolveAnalysisFrequencyMHz(MapCallsignRawSnapshot selectedSnapshot) {
|
||||
if (selectedSnapshot == null) {
|
||||
return PathGeometryUtils.DEFAULT_ANALYSIS_FREQUENCY_MHZ;
|
||||
return Double.NaN;
|
||||
}
|
||||
|
||||
return PathGeometryUtils.resolveAnalysisFrequencyMHz(selectedSnapshot.lastKnownFrequenciesByBand());
|
||||
ChatMember selectedMember = resolveBestChatMember(selectedSnapshot.callSignRaw());
|
||||
var resolution = chatController.getReachabilityService()
|
||||
.resolveAutomaticPropagationFrequency(selectedMember);
|
||||
|
||||
return resolution == null
|
||||
? Double.NaN
|
||||
: resolution.getAnalysisFrequencyMHz();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import javafx.scene.layout.VBox;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import kst4contest.view.GuiUtils;
|
||||
import netscape.javascript.JSObject;
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
@@ -86,14 +87,15 @@ public final class StationMapView {
|
||||
private final Button pathAnalysisVisibilityButton = new Button();
|
||||
private final Tooltip pathAnalysisVisibilityTooltip = new Tooltip();
|
||||
|
||||
private final Button resetViewButton = new Button("Reset view");
|
||||
private final Tooltip statusTooltip = new Tooltip();
|
||||
|
||||
private Runnable onResetView;
|
||||
|
||||
private double lastDetailDividerPosition = 0.65;
|
||||
|
||||
|
||||
|
||||
private final Label detailCallsignValue = new Label("-");
|
||||
private final Label detailLocatorValue = new Label("-");
|
||||
private final Label detailQrbValue = new Label("-");
|
||||
private final Label detailQtfValue = new Label("-");
|
||||
private final Label detailBandsValue = new Label("-");
|
||||
private final TextArea detailFrequenciesArea = new TextArea();
|
||||
private final Label detailAirplanesValue = new Label("-");
|
||||
private final Button triggerClusterSpotButton = new Button("Trigger cluster spot");
|
||||
|
||||
private final Label detailPathFromLocatorValue = new Label("-");
|
||||
@@ -179,6 +181,8 @@ public final class StationMapView {
|
||||
|
||||
public StationMapView(ChatPreferences chatPreferences) {
|
||||
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
|
||||
GuiUtils.applyApplicationIcon(stage);
|
||||
|
||||
try {
|
||||
tileProxyServer = new TileProxyServer();
|
||||
} catch (IOException e) {
|
||||
@@ -207,6 +211,11 @@ public final class StationMapView {
|
||||
this.onTriggerClusterSpot = onTriggerClusterSpot;
|
||||
}
|
||||
|
||||
public void setOnResetView(Runnable onResetView) {
|
||||
this.onResetView = onResetView;
|
||||
}
|
||||
|
||||
|
||||
public void showWindow() {
|
||||
|
||||
applyThemeFromPreferences();
|
||||
@@ -270,6 +279,15 @@ public final class StationMapView {
|
||||
updateStatusLabel();
|
||||
updateDetailPanel(selectedSnapshot);
|
||||
|
||||
/*
|
||||
* The detail pane is useful only while path analysis is enabled and a
|
||||
* concrete station is selected. Reset view therefore removes the pane
|
||||
* instead of leaving an empty analysis area beside the map.
|
||||
*/
|
||||
updateDetailPanePresence(
|
||||
profileSection.isVisible() && selectedSnapshot != null
|
||||
);
|
||||
|
||||
if (mapReady) {
|
||||
renderAll();
|
||||
}
|
||||
@@ -279,10 +297,6 @@ public final class StationMapView {
|
||||
|
||||
stage.setTitle("Station Map");
|
||||
|
||||
detailFrequenciesArea.setEditable(false);
|
||||
detailFrequenciesArea.setWrapText(true);
|
||||
detailFrequenciesArea.setPrefRowCount(4);
|
||||
|
||||
detailPathEndpointsValue.setWrapText(true);
|
||||
detailPathEndpointsValue.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
@@ -296,12 +310,23 @@ public final class StationMapView {
|
||||
detailPathMechanismsValue.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
triggerClusterSpotButton.setDisable(true);
|
||||
triggerClusterSpotButton.setVisible(false);
|
||||
triggerClusterSpotButton.setManaged(false);
|
||||
triggerClusterSpotButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
triggerClusterSpotButton.setOnAction(event -> {
|
||||
if (detailCallsignRaw != null && onTriggerClusterSpot != null) {
|
||||
onTriggerClusterSpot.accept(detailCallsignRaw);
|
||||
}
|
||||
});
|
||||
|
||||
resetViewButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
resetViewButton.setOnAction(event -> {
|
||||
if (onResetView != null) {
|
||||
onResetView.run();
|
||||
}
|
||||
});
|
||||
|
||||
pathAnalysisVisibilityButton.setMinWidth(Region.USE_PREF_SIZE);
|
||||
pathAnalysisVisibilityButton.setTooltip(pathAnalysisVisibilityTooltip);
|
||||
pathAnalysisVisibilityButton.setOnAction(event ->
|
||||
@@ -374,7 +399,7 @@ public final class StationMapView {
|
||||
);
|
||||
|
||||
pathAnalysisSection = createPathAnalysisSection();
|
||||
detailPane = new VBox(10, createSelectedStationSection(), pathAnalysisSection);
|
||||
detailPane = new VBox(10, pathAnalysisSection);
|
||||
|
||||
detailPane.setPadding(new Insets(10));
|
||||
|
||||
@@ -486,20 +511,25 @@ public final class StationMapView {
|
||||
* must always have an obvious way to restore a previously hidden analysis.
|
||||
*/
|
||||
private HBox createMapHeader() {
|
||||
// The status may be shortened before the show/hide control is ever clipped.
|
||||
statusLabel.setMinWidth(0);
|
||||
statusLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
statusLabel.setTextOverrun(OverrunStyle.ELLIPSIS);
|
||||
statusLabel.setTooltip(statusTooltip);
|
||||
|
||||
HBox header = new HBox(
|
||||
10,
|
||||
statusLabel,
|
||||
triggerClusterSpotButton,
|
||||
resetViewButton,
|
||||
pathAnalysisHiddenHintLabel,
|
||||
pathAnalysisVisibilityButton
|
||||
);
|
||||
|
||||
header.setAlignment(Pos.CENTER_LEFT);
|
||||
header.setPadding(new Insets(8));
|
||||
|
||||
HBox.setHgrow(statusLabel, Priority.ALWAYS);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
@@ -521,6 +551,15 @@ public final class StationMapView {
|
||||
pathAnalysisSection.setVisible(visible);
|
||||
pathAnalysisSection.setManaged(visible);
|
||||
|
||||
/*
|
||||
* Showing path analysis must not create an empty detail pane when no
|
||||
* station is selected. The pane is restored automatically with the next
|
||||
* valid station selection.
|
||||
*/
|
||||
updateDetailPanePresence(
|
||||
visible && lastSelectedSnapshot != null
|
||||
);
|
||||
|
||||
pathAnalysisHiddenHintLabel.setVisible(!visible);
|
||||
pathAnalysisHiddenHintLabel.setManaged(!visible);
|
||||
|
||||
@@ -564,45 +603,6 @@ public final class StationMapView {
|
||||
: MINIMUM_HEIGHT_WITHOUT_PATH_ANALYSIS;
|
||||
}
|
||||
|
||||
private VBox createSelectedStationSection() {
|
||||
GridPane detailGrid = new GridPane();
|
||||
detailGrid.setHgap(8);
|
||||
detailGrid.setVgap(6);
|
||||
|
||||
configureCompactGrid(detailGrid);
|
||||
|
||||
int row = 0;
|
||||
|
||||
detailGrid.add(new Label("Station:"), 0, row);
|
||||
detailGrid.add(detailCallsignValue, 1, row++);
|
||||
|
||||
detailGrid.add(new Label("Locator:"), 0, row);
|
||||
detailGrid.add(detailLocatorValue, 1, row++);
|
||||
|
||||
detailGrid.add(new Label("Path:"), 0, row);
|
||||
Label compactPathValue = new Label();
|
||||
compactPathValue.textProperty().bind(
|
||||
detailQrbValue.textProperty()
|
||||
.concat(" / ")
|
||||
.concat(detailQtfValue.textProperty())
|
||||
);
|
||||
detailGrid.add(compactPathValue, 1, row++);
|
||||
|
||||
detailGrid.add(new Label("Bands:"), 0, row);
|
||||
detailGrid.add(detailBandsValue, 1, row++);
|
||||
|
||||
// Frequencies are useful, but they consume vertical space. Keep them compact.
|
||||
detailFrequenciesArea.setPrefRowCount(2);
|
||||
detailGrid.add(new Label("QRG:"), 0, row);
|
||||
detailGrid.add(detailFrequenciesArea, 1, row++);
|
||||
|
||||
return new VBox(8,
|
||||
new Label("Selected station"),
|
||||
new Separator(Orientation.HORIZONTAL),
|
||||
detailGrid,
|
||||
triggerClusterSpotButton
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ensures that the labels remains visible
|
||||
@@ -947,58 +947,107 @@ public final class StationMapView {
|
||||
|
||||
private void updateStatusLabel() {
|
||||
StringBuilder text = new StringBuilder();
|
||||
text.append("Showing ").append(lastSnapshots.size()).append(" visible stations");
|
||||
|
||||
text.append("Showing ")
|
||||
.append(lastSnapshots.size())
|
||||
.append(" visible stations");
|
||||
|
||||
if (filteredViewActive) {
|
||||
text.append(" | filtered view active");
|
||||
}
|
||||
|
||||
statusLabel.setText(text.toString());
|
||||
MapCallsignRawSnapshot selectedSnapshot = lastSelectedSnapshot;
|
||||
|
||||
if (selectedSnapshot != null) {
|
||||
text.append(" | Selected: ")
|
||||
.append(selectedSnapshot.displayCallSign());
|
||||
|
||||
if (!selectedSnapshot.locator6().isBlank()) {
|
||||
text.append(" | ")
|
||||
.append(selectedSnapshot.locator6());
|
||||
}
|
||||
|
||||
text.append(" | ")
|
||||
.append(String.format(
|
||||
Locale.US,
|
||||
"%.0f km / %.0f°",
|
||||
selectedSnapshot.qrbKm(),
|
||||
selectedSnapshot.qtfDeg()
|
||||
));
|
||||
|
||||
String bandText = selectedSnapshot.bandSummary().isBlank()
|
||||
? "-"
|
||||
: selectedSnapshot.bandSummary();
|
||||
|
||||
if (selectedSnapshot.offersSelectedBand()) {
|
||||
bandText += " B+";
|
||||
}
|
||||
|
||||
text.append(" | Bands: ")
|
||||
.append(bandText);
|
||||
|
||||
String frequencies = selectedSnapshot.detailFrequencyText();
|
||||
|
||||
if (frequencies != null && !frequencies.isBlank()) {
|
||||
frequencies = frequencies
|
||||
.replace('\n', ' ')
|
||||
.replace('\r', ' ')
|
||||
.replaceAll("\\s+", " ")
|
||||
.trim();
|
||||
|
||||
text.append(" | QRG: ")
|
||||
.append(frequencies);
|
||||
}
|
||||
}
|
||||
|
||||
String statusText = text.toString();
|
||||
|
||||
statusLabel.setText(statusText);
|
||||
statusTooltip.setText(statusText);
|
||||
}
|
||||
|
||||
private void updateDetailPanel(MapCallsignRawSnapshot selectedSnapshot) {
|
||||
if (selectedSnapshot == null) {
|
||||
clearSelectedStationPanel();
|
||||
detailCallsignRaw = null;
|
||||
|
||||
triggerClusterSpotButton.setDisable(true);
|
||||
triggerClusterSpotButton.setVisible(false);
|
||||
triggerClusterSpotButton.setManaged(false);
|
||||
|
||||
clearPathAnalysisPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
updateSelectedStationPanel(selectedSnapshot);
|
||||
|
||||
if (selectedSnapshot == null) {
|
||||
updatePathAnalysisPanel(PathAnalysisResult.waitingForSelection(homeLocator6));
|
||||
} else {
|
||||
updatePathAnalysisPanel(lastPathAnalysisResult);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void clearSelectedStationPanel() {
|
||||
detailCallsignRaw = null;
|
||||
detailCallsignValue.setText("-");
|
||||
detailLocatorValue.setText("-");
|
||||
detailQrbValue.setText("-");
|
||||
detailQtfValue.setText("-");
|
||||
detailBandsValue.setText("-");
|
||||
detailFrequenciesArea.setText("-");
|
||||
detailAirplanesValue.setText("-");
|
||||
triggerClusterSpotButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void updateSelectedStationPanel(MapCallsignRawSnapshot selectedSnapshot) {
|
||||
detailCallsignRaw = selectedSnapshot.callSignRaw();
|
||||
detailCallsignValue.setText(selectedSnapshot.displayCallSign());
|
||||
detailLocatorValue.setText(selectedSnapshot.locator6());
|
||||
detailQrbValue.setText(String.format(Locale.US, "%.0f km", selectedSnapshot.qrbKm()));
|
||||
detailQtfValue.setText(String.format(Locale.US, "%.0f°", selectedSnapshot.qtfDeg()));
|
||||
String bandText = selectedSnapshot.bandSummary().isBlank() ? "-" : selectedSnapshot.bandSummary();
|
||||
if (selectedSnapshot.offersSelectedBand()) {
|
||||
bandText += " B+";
|
||||
}
|
||||
detailBandsValue.setText(bandText);
|
||||
detailFrequenciesArea.setText(selectedSnapshot.detailFrequencyText());
|
||||
detailAirplanesValue.setText(String.valueOf(selectedSnapshot.reachableAirplanes()));
|
||||
|
||||
triggerClusterSpotButton.setDisable(false);
|
||||
triggerClusterSpotButton.setVisible(true);
|
||||
triggerClusterSpotButton.setManaged(true);
|
||||
|
||||
updatePathAnalysisPanel(lastPathAnalysisResult);
|
||||
}
|
||||
|
||||
private void updateDetailPanePresence(boolean visible) {
|
||||
if (mainSplitPane == null || detailScrollPane == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
if (!mainSplitPane.getItems().contains(detailScrollPane)) {
|
||||
mainSplitPane.getItems().add(detailScrollPane);
|
||||
|
||||
Platform.runLater(() ->
|
||||
mainSplitPane.setDividerPositions(lastDetailDividerPosition)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!mainSplitPane.getDividers().isEmpty()) {
|
||||
lastDetailDividerPosition =
|
||||
mainSplitPane.getDividers().get(0).getPosition();
|
||||
}
|
||||
|
||||
mainSplitPane.getItems().remove(detailScrollPane);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearPathAnalysisPanel() {
|
||||
@@ -1328,13 +1377,13 @@ public final class StationMapView {
|
||||
mainSplitPane.setStyle("-fx-background-color: #2b3035;");
|
||||
detailPane.setStyle("-fx-background-color: #31373c; -fx-border-color: #4c565c; -fx-border-width: 0 0 0 1;");
|
||||
statusLabel.setStyle("-fx-background-color: #373e43; -fx-text-fill: lightgray; -fx-padding: 8 10 8 10; -fx-background-radius: 4;");
|
||||
detailFrequenciesArea.setStyle("-fx-control-inner-background: #444b50; -fx-text-fill: lightgray;");
|
||||
// detailFrequenciesArea.setStyle("-fx-control-inner-background: #444b50; -fx-text-fill: lightgray;");
|
||||
} else {
|
||||
rootPane.setStyle("-fx-background-color: #f2f2f2;");
|
||||
mainSplitPane.setStyle("-fx-background-color: #f2f2f2;");
|
||||
detailPane.setStyle("-fx-background-color: #f7f7f7; -fx-border-color: #d0d0d0; -fx-border-width: 0 0 0 1;");
|
||||
statusLabel.setStyle("-fx-background-color: #f7f7f7; -fx-text-fill: #333333; -fx-padding: 8 10 8 10; -fx-background-radius: 4;");
|
||||
detailFrequenciesArea.setStyle("");
|
||||
// detailFrequenciesArea.setStyle("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ module praktiKST {
|
||||
requires java.net.http;
|
||||
requires java.desktop;
|
||||
requires jdk.crypto.ec;
|
||||
requires jdk.net;
|
||||
requires org.junit.jupiter.api;
|
||||
requires org.mockito;
|
||||
exports kst4contest.controller.interfaces;
|
||||
|
||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
@@ -14,6 +14,39 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class BandOpportunityResolverTest {
|
||||
|
||||
@Test
|
||||
void explicitStationNameFrequencyProvidesBandEvidence() {
|
||||
ChatMember station = new ChatMember();
|
||||
station.setName("Phil 432.357");
|
||||
|
||||
BandOpportunityResolver.Resolution resolution =
|
||||
BandOpportunityResolver.resolve(
|
||||
List.of(station),
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
EnumSet.of(Band.B_432),
|
||||
resolution.getOfferedBands()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void relativeStationNameFrequencyDoesNotProvideBandEvidence() {
|
||||
ChatMember station = new ChatMember();
|
||||
station.setName("Mike .180");
|
||||
|
||||
BandOpportunityResolver.Resolution resolution =
|
||||
BandOpportunityResolver.resolve(
|
||||
List.of(station),
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
assertTrue(
|
||||
resolution.getOfferedBands().isEmpty()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesCommonShorthandBandsFromStationName() {
|
||||
EnumSet<Band> bands = BandOpportunityResolver.detectBandsFromStationName(
|
||||
|
||||
@@ -15,6 +15,70 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MapCallsignRawSnapshotBuilderTest {
|
||||
|
||||
@Test
|
||||
void explicitStationNameQrgIsShownInMapSnapshot() {
|
||||
ChatMember station =
|
||||
buildStation(
|
||||
"G0JSB",
|
||||
"Phil 432.357",
|
||||
"IO91AA",
|
||||
1_000L
|
||||
);
|
||||
|
||||
MapCallsignRawSnapshot snapshot =
|
||||
new MapCallsignRawSnapshotBuilder()
|
||||
.buildSnapshots(
|
||||
List.of(station),
|
||||
null,
|
||||
EnumSet.of(Band.B_432)
|
||||
)
|
||||
.get(0);
|
||||
|
||||
assertEquals(
|
||||
"432",
|
||||
snapshot.bandSummary()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"432.357",
|
||||
snapshot
|
||||
.lastKnownFrequenciesByBand()
|
||||
.get("432")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recentDynamicQrgWinsOverStationNameQrgInMap() {
|
||||
ChatMember station =
|
||||
buildStation(
|
||||
"G0JSB",
|
||||
"Phil 432.357",
|
||||
"IO91AA",
|
||||
1_000L
|
||||
);
|
||||
|
||||
station.addKnownFrequency(
|
||||
Band.B_432,
|
||||
432.335
|
||||
);
|
||||
|
||||
MapCallsignRawSnapshot snapshot =
|
||||
new MapCallsignRawSnapshotBuilder()
|
||||
.buildSnapshots(
|
||||
List.of(station),
|
||||
null,
|
||||
EnumSet.of(Band.B_432)
|
||||
)
|
||||
.get(0);
|
||||
|
||||
assertEquals(
|
||||
"432.335",
|
||||
snapshot
|
||||
.lastKnownFrequenciesByBand()
|
||||
.get("432")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void marksSnapshotWhenNameAdvertisesSelectedBand() {
|
||||
ChatMember station = buildStation("DL1ABC", "QRV 2-70-23", "JN58TD", 1_000L);
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
package kst4contest.test;
|
||||
|
||||
import kst4contest.logic.PropagationFrequencyResolver;
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
class PropagationFrequencyResolverTest {
|
||||
|
||||
private static final long NOW = 10_000_000L;
|
||||
|
||||
|
||||
@Test
|
||||
void exactStationNameQrgWinsOverBandAndCategoryFallback() {
|
||||
ChatMember station =
|
||||
station(
|
||||
ChatCategory.VUHF,
|
||||
"Phil 432.357"
|
||||
);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(
|
||||
Band.B_144,
|
||||
Band.B_432
|
||||
)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
Band.B_432,
|
||||
resolution.getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
432.357,
|
||||
resolution.getAnalysisFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
PropagationFrequencyResolver.Source.STATION_NAME_QRG,
|
||||
resolution.getSource()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void recentChatQrgWinsOverExactStationNameQrg() {
|
||||
ChatMember station =
|
||||
station(
|
||||
ChatCategory.VUHF,
|
||||
"Phil 432.357"
|
||||
);
|
||||
|
||||
addCurrentQrg(
|
||||
station,
|
||||
Band.B_432,
|
||||
432.335,
|
||||
NOW - 1_000L
|
||||
);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(Band.B_432)
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
432.335,
|
||||
resolution.getAnalysisFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
PropagationFrequencyResolver.Source.CURRENT_QRG,
|
||||
resolution.getSource()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleStationNameQrgsAreNotTreatedAsOneRunFrequency() {
|
||||
ChatMember station =
|
||||
station(
|
||||
ChatCategory.EMEJT65,
|
||||
"QRV 432.357 1296.210"
|
||||
);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(
|
||||
Band.B_432,
|
||||
Band.B_1296
|
||||
)
|
||||
);
|
||||
|
||||
/*
|
||||
* Both bands are known, but no exact run QRG is guessed.
|
||||
* Existing station-name band selection therefore chooses the
|
||||
* lowest usable advertised band.
|
||||
*/
|
||||
assertEquals(
|
||||
Band.B_432,
|
||||
resolution.getBand()
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
432.0,
|
||||
resolution.getAnalysisFrequencyMHz(),
|
||||
0.000_001
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
PropagationFrequencyResolver.Source.STATION_NAME,
|
||||
resolution.getSource()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void currentQrgWinsOverNameAndCategoryFallback() {
|
||||
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
|
||||
addCurrentQrg(station, Band.B_2320, 2320.175, NOW - 1_000L);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(Band.B_1296, Band.B_2320, Band.B_10G)
|
||||
);
|
||||
|
||||
assertEquals(Band.B_2320, resolution.getBand());
|
||||
assertEquals(2320.175, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||
assertEquals(PropagationFrequencyResolver.Source.CURRENT_QRG, resolution.getSource());
|
||||
assertEquals("23201750", resolution.getAirScoutBandValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void mostRecentlyDetectedQrgWinsAcrossCategoryVariants() {
|
||||
ChatMember vhf = station(ChatCategory.VUHF, "");
|
||||
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
|
||||
addCurrentQrg(vhf, Band.B_144, 144.210, NOW - 10_000L);
|
||||
addCurrentQrg(microwave, Band.B_1296, 1296.210, NOW - 1_000L);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||
List.of(vhf, microwave),
|
||||
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
|
||||
);
|
||||
|
||||
assertEquals(Band.B_1296, resolution.getBand());
|
||||
assertEquals(1296.210, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void microwaveFallsBackToLowestEnabledMicrowaveBand() {
|
||||
ChatMember station = station(ChatCategory.MICROWAVE, "");
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(Band.B_2320, Band.B_3400)
|
||||
);
|
||||
|
||||
assertEquals(Band.B_2320, resolution.getBand());
|
||||
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||
assertEquals(PropagationFrequencyResolver.Source.CHAT_CATEGORY, resolution.getSource());
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportedCategoriesUseTheirAgreedLowestFallbackBand() {
|
||||
assertEquals(
|
||||
Band.B_50,
|
||||
resolve(
|
||||
List.of(station(ChatCategory.FIFTYSEVENTYMHz, "")),
|
||||
EnumSet.of(Band.B_50, Band.B_70)
|
||||
).getBand()
|
||||
);
|
||||
assertEquals(
|
||||
Band.B_144,
|
||||
resolve(
|
||||
List.of(station(ChatCategory.VUHF, "")),
|
||||
EnumSet.of(Band.B_144, Band.B_432)
|
||||
).getBand()
|
||||
);
|
||||
assertEquals(
|
||||
Band.B_1296,
|
||||
resolve(
|
||||
List.of(station(ChatCategory.MICROWAVE, "")),
|
||||
EnumSet.of(Band.B_1296, Band.B_2320)
|
||||
).getBand()
|
||||
);
|
||||
assertEquals(
|
||||
Band.B_144,
|
||||
resolve(
|
||||
List.of(station(ChatCategory.EMEJT65, "")),
|
||||
EnumSet.of(Band.B_144, Band.B_1296)
|
||||
).getBand()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void vhfAndMicrowaveUse432MhzFallback() {
|
||||
ChatMember vhf = station(ChatCategory.VUHF, "");
|
||||
ChatMember microwave = station(ChatCategory.MICROWAVE, "");
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||
List.of(vhf, microwave),
|
||||
EnumSet.of(Band.B_144, Band.B_432, Band.B_1296)
|
||||
);
|
||||
|
||||
assertEquals(Band.B_432, resolution.getBand());
|
||||
assertEquals(432.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||
assertEquals(
|
||||
PropagationFrequencyResolver.Source.DUAL_CATEGORY_FALLBACK,
|
||||
resolution.getSource()
|
||||
);
|
||||
assertEquals("4320000", resolution.getAirScoutBandValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsupportedChatCategoriesDoNotFallBackTo144Mhz() {
|
||||
for (int categoryNumber = ChatCategory.LOWBAND;
|
||||
categoryNumber <= ChatCategory.TENMeter;
|
||||
categoryNumber++) {
|
||||
ChatMember unsupported = station(categoryNumber, "QRV 2m");
|
||||
addCurrentQrg(unsupported, Band.B_144, 144.300, NOW - 1_000L);
|
||||
|
||||
assertNull(
|
||||
resolve(
|
||||
List.of(unsupported),
|
||||
EnumSet.of(Band.B_144, Band.B_432)
|
||||
),
|
||||
"Category " + categoryNumber + " must be ignored"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualNotQrvExclusionForcesNextUsableMicrowaveBand() {
|
||||
ChatMember station = station(ChatCategory.MICROWAVE, "");
|
||||
station.setQrv1240(false);
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(Band.B_1296, Band.B_2320)
|
||||
);
|
||||
|
||||
assertEquals(Band.B_2320, resolution.getBand());
|
||||
assertEquals(2320.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void stationNameBandHintWinsOverCategoryFallback() {
|
||||
ChatMember station = station(ChatCategory.MICROWAVE, "QRV 3cm");
|
||||
|
||||
PropagationFrequencyResolver.Resolution resolution = resolve(
|
||||
List.of(station),
|
||||
EnumSet.of(Band.B_1296, Band.B_10G)
|
||||
);
|
||||
|
||||
assertEquals(Band.B_10G, resolution.getBand());
|
||||
assertEquals(10368.0, resolution.getAnalysisFrequencyMHz(), 0.000_001);
|
||||
assertEquals(PropagationFrequencyResolver.Source.STATION_NAME, resolution.getSource());
|
||||
}
|
||||
|
||||
private PropagationFrequencyResolver.Resolution resolve(
|
||||
List<ChatMember> variants,
|
||||
EnumSet<Band> enabledBands
|
||||
) {
|
||||
return PropagationFrequencyResolver.resolve(variants, enabledBands, NOW);
|
||||
}
|
||||
|
||||
private ChatMember station(int categoryNumber, String name) {
|
||||
ChatMember station = new ChatMember();
|
||||
station.setCallSign("DL1ABC");
|
||||
station.setChatCategory(new ChatCategory(categoryNumber));
|
||||
station.setName(name);
|
||||
return station;
|
||||
}
|
||||
|
||||
private void addCurrentQrg(ChatMember station,
|
||||
Band band,
|
||||
double frequencyMHz,
|
||||
long timestampEpochMs) {
|
||||
station.addKnownFrequency(band, frequencyMHz);
|
||||
station.getKnownActiveBands().get(band).timestampEpoch = timestampEpochMs;
|
||||
}
|
||||
}
|
||||
@@ -2382,4 +2382,12 @@ OV3T;Thomas;JO46CM;StringProperty [value: null]; wkd true; wkd144 true; wkd432fa
|
||||
OZ6TY;Henning;JO55XE;StringProperty [value: 144.196]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
DF7KF;Dithmar;JO30FK;StringProperty [value: null]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
DH1NFJ;Jochen;JO50QL;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 3: Microwave
|
||||
PA2RU;René;JO32LT;StringProperty [value: null]; wkd true; wkd144 false; wkd432false; wkd1240true; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
PA2RU;René;JO32LT;StringProperty [value: null]; wkd true; wkd144 false; wkd432false; wkd1240true; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 144.135]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
SM7EYW;Torleif 432,205;JO65NK;StringProperty [value: 144.205]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
DJ8MS;Tor_70cm;JO54UC;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
DK0MM;Jens/Alex;JN49IU;StringProperty [value: 432.305]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 144.135]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
SM6VTZ;Chris .135;JO58UJ;StringProperty [value: 432.135]; wkd true; wkd144 true; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
LA0BY;Stefan @ hilltop;JO59IX;StringProperty [value: 144.062]; wkd true; wkd144 true; wkd432false; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; 2: 144/432 MHz
|
||||
LA0BY;null;JO49ML;StringProperty [value: null]; wkd true; wkd144 false; wkd432true; wkd1240false; wkd2300false; wkd3400false; wkd5600false; wkd10Gfalse ; null
|
||||
@@ -71,6 +71,14 @@ function getManualPageOrder(lang, slug) {
|
||||
return index >= 0 ? index : 999;
|
||||
}
|
||||
|
||||
function githubCompatibleSlug(value) {
|
||||
return (value || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s_-]/gu, "")
|
||||
.replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function rewriteManualLinks(content, lang) {
|
||||
return (content || "")
|
||||
|
||||
@@ -157,6 +165,7 @@ module.exports = function (eleventyConfig) {
|
||||
linkify: true,
|
||||
typographer: true
|
||||
}).use(markdownItAnchor, {
|
||||
slugify: githubCompatibleSlug,
|
||||
permalink: markdownItAnchor.permalink.headerLink()
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "eleventy --serve",
|
||||
"build": "eleventy"
|
||||
"build": "eleventy && npm run validate:version-info",
|
||||
"validate:version-info": "node scripts/validate-version-info.js",
|
||||
"test": "node --test test/*.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^3.0.0"
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const DEFAULT_FILE = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"_site",
|
||||
"kst4ContestVersionInfo.xml"
|
||||
);
|
||||
|
||||
function fail(message) {
|
||||
throw new Error(`[versionInfo validation] ${message}`);
|
||||
}
|
||||
|
||||
function normaliseVersion(value) {
|
||||
return String(value || "")
|
||||
.trim()
|
||||
.replace(/^v/, "")
|
||||
.split("-")[0]
|
||||
.split("+")[0];
|
||||
}
|
||||
|
||||
function extractTag(xml, tagName) {
|
||||
const match = xml.match(
|
||||
new RegExp(`<${tagName}>([\\s\\S]*?)<\\/${tagName}>`)
|
||||
);
|
||||
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
function assertWellFormedStructure(xml) {
|
||||
if (/&(?!amp;|lt;|gt;|quot;|apos;|#\d+;|#x[0-9a-f]+;)/i.test(xml)) {
|
||||
fail("the XML contains an unescaped ampersand");
|
||||
}
|
||||
|
||||
const withoutCommentsAndDeclaration = xml
|
||||
.replace(/<!--[\s\S]*?-->/g, "")
|
||||
.replace(/<\?[\s\S]*?\?>/g, "");
|
||||
|
||||
const tagPattern =
|
||||
/<\/?([A-Za-z_][\w:.-]*)(?:\s[^<>]*?)?\s*\/?>/g;
|
||||
|
||||
const stack = [];
|
||||
let match;
|
||||
|
||||
while (
|
||||
(match = tagPattern.exec(withoutCommentsAndDeclaration)) !== null
|
||||
) {
|
||||
const fullTag = match[0];
|
||||
const tagName = match[1];
|
||||
|
||||
if (fullTag.startsWith("</")) {
|
||||
const openTag = stack.pop();
|
||||
|
||||
if (openTag !== tagName) {
|
||||
fail(
|
||||
`closing tag </${tagName}> does not match `
|
||||
+ `<${openTag || "none"}>`
|
||||
);
|
||||
}
|
||||
} else if (!fullTag.endsWith("/>")) {
|
||||
stack.push(tagName);
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length > 0) {
|
||||
fail(`unclosed tag <${stack[stack.length - 1]}>`);
|
||||
}
|
||||
|
||||
const remainingMarkup =
|
||||
withoutCommentsAndDeclaration.replace(tagPattern, "");
|
||||
|
||||
// A plain ">" is legal character data and occurs in historical
|
||||
// changelog notation such as "->". A remaining "<" cannot be legal
|
||||
// after all valid tags have been removed.
|
||||
if (/</.test(remainingMarkup)) {
|
||||
fail("the XML contains malformed markup");
|
||||
}
|
||||
}
|
||||
|
||||
function assertContainsTag(block, tagName) {
|
||||
const pattern =
|
||||
new RegExp(`<${tagName}>[\\s\\S]*?<\\/${tagName}>`);
|
||||
|
||||
if (!pattern.test(block)) {
|
||||
fail(`required element <${tagName}> is missing`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateVersionInfo(xml, expectedStableVersion = "") {
|
||||
if (!xml || Buffer.byteLength(xml, "utf8") < 500) {
|
||||
fail("the generated file is empty or implausibly small");
|
||||
}
|
||||
|
||||
assertWellFormedStructure(xml);
|
||||
|
||||
const completeDocumentPattern =
|
||||
/^<\?xml[^>]*>\s*<praktiKST>[\s\S]*<\/praktiKST>\s*$/;
|
||||
|
||||
if (!completeDocumentPattern.test(xml)) {
|
||||
fail(
|
||||
"the document does not contain one complete "
|
||||
+ "<praktiKST> root element"
|
||||
);
|
||||
}
|
||||
|
||||
const latestVersionBlock = extractTag(xml, "latestVersion");
|
||||
|
||||
if (latestVersionBlock === null) {
|
||||
fail("<latestVersion> is missing");
|
||||
}
|
||||
|
||||
for (const tagName of [
|
||||
"versionNumber",
|
||||
"semanticVersion",
|
||||
"adminMessage",
|
||||
"majorChanges",
|
||||
"latestVersionPathOnWebserver"
|
||||
]) {
|
||||
assertContainsTag(latestVersionBlock, tagName);
|
||||
}
|
||||
|
||||
const legacyVersion =
|
||||
extractTag(latestVersionBlock, "versionNumber");
|
||||
|
||||
const semanticVersion =
|
||||
extractTag(latestVersionBlock, "semanticVersion");
|
||||
|
||||
const releaseUrl =
|
||||
extractTag(
|
||||
latestVersionBlock,
|
||||
"latestVersionPathOnWebserver"
|
||||
);
|
||||
|
||||
if (
|
||||
!legacyVersion
|
||||
|| !/^\d+(?:\.\d+)?$/.test(legacyVersion)
|
||||
) {
|
||||
fail(
|
||||
"<versionNumber> is not a valid legacy numeric version"
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!semanticVersion
|
||||
|| !/^\d+\.\d+(?:\.\d+)?$/.test(semanticVersion)
|
||||
) {
|
||||
fail("<semanticVersion> is not a valid Stable version");
|
||||
}
|
||||
|
||||
if (
|
||||
!releaseUrl
|
||||
|| !releaseUrl.startsWith(
|
||||
"https://github.com/praktimarc/"
|
||||
+ "kst4contest/releases/tag/"
|
||||
)
|
||||
) {
|
||||
fail(
|
||||
"<latestVersionPathOnWebserver> is not "
|
||||
+ "a KST4Contest release URL"
|
||||
);
|
||||
}
|
||||
|
||||
const changeLogs = [
|
||||
...xml.matchAll(
|
||||
/<changeLog>([\s\S]*?)<\/changeLog>/g
|
||||
)
|
||||
].map((match) => match[1]);
|
||||
|
||||
if (changeLogs.length === 0) {
|
||||
fail(
|
||||
"the document does not contain any "
|
||||
+ "<changeLog> entries"
|
||||
);
|
||||
}
|
||||
|
||||
for (const entry of changeLogs) {
|
||||
for (const tagName of [
|
||||
"changedVersionNumber",
|
||||
"date",
|
||||
"description",
|
||||
"added",
|
||||
"changed",
|
||||
"fixed",
|
||||
"removed"
|
||||
]) {
|
||||
assertContainsTag(entry, tagName);
|
||||
}
|
||||
}
|
||||
|
||||
const expected = normaliseVersion(expectedStableVersion);
|
||||
|
||||
if (expected) {
|
||||
if (semanticVersion !== expected) {
|
||||
fail(
|
||||
`latest Stable version ${semanticVersion} `
|
||||
+ `does not match expected release ${expected}`
|
||||
);
|
||||
}
|
||||
|
||||
const releaseEntryExists = changeLogs.some(
|
||||
(entry) =>
|
||||
extractTag(
|
||||
entry,
|
||||
"changedVersionNumber"
|
||||
) === expected
|
||||
);
|
||||
|
||||
if (!releaseEntryExists) {
|
||||
fail(
|
||||
"the changelog does not contain the expected "
|
||||
+ `Stable release ${expected}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
semanticVersion,
|
||||
changeLogEntries: changeLogs.length
|
||||
};
|
||||
}
|
||||
|
||||
function parseArguments(argv) {
|
||||
const result = {
|
||||
file: DEFAULT_FILE,
|
||||
expectedStableVersion:
|
||||
process.env.EXPECTED_STABLE_VERSION || ""
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index++) {
|
||||
if (
|
||||
argv[index] === "--file"
|
||||
&& argv[index + 1]
|
||||
) {
|
||||
result.file = path.resolve(argv[++index]);
|
||||
} else if (
|
||||
argv[index] === "--expected-stable"
|
||||
&& argv[index + 1]
|
||||
) {
|
||||
result.expectedStableVersion = argv[++index];
|
||||
} else {
|
||||
fail(
|
||||
`unknown or incomplete argument: ${argv[index]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
try {
|
||||
const options =
|
||||
parseArguments(process.argv.slice(2));
|
||||
|
||||
const xml =
|
||||
fs.readFileSync(options.file, "utf8");
|
||||
|
||||
const result =
|
||||
validateVersionInfo(
|
||||
xml,
|
||||
options.expectedStableVersion
|
||||
);
|
||||
|
||||
console.log(
|
||||
`[versionInfo validation] OK: `
|
||||
+ `Stable ${result.semanticVersion}, `
|
||||
+ `${result.changeLogEntries} changelog entries, `
|
||||
+ options.file
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normaliseVersion,
|
||||
validateVersionInfo
|
||||
};
|
||||