Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a96d9ef1c3
|
@@ -1,60 +0,0 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Report a problem with KST4Contest / Ein Problem mit KST4Contest melden
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
---
|
||||
|
||||
**DE:** Bitte fülle alle Felder so vollständig wie möglich aus. Das hilft, den Fehler schneller zu finden.
|
||||
**EN:** Please fill in all fields as completely as possible. This helps to find the bug faster.
|
||||
|
||||
## Description / Beschreibung
|
||||
|
||||
<!-- EN: A clear and concise description of the bug. -->
|
||||
<!-- DE: Eine klare und präzise Beschreibung des Problems. -->
|
||||
|
||||
## Steps to reproduce / Schritte zum Reproduzieren
|
||||
|
||||
<!-- EN: Step-by-step instructions to reproduce the bug. -->
|
||||
<!-- DE: Schritt-für-Schritt-Anleitung, um den Fehler zu reproduzieren. -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Expected behaviour / Erwartetes Verhalten
|
||||
|
||||
<!-- EN: What did you expect to happen? / DE: Was hätte passieren sollen? -->
|
||||
|
||||
## Actual behaviour / Tatsächliches Verhalten
|
||||
|
||||
<!-- EN: What actually happened? / DE: Was ist stattdessen passiert? -->
|
||||
|
||||
## Log file content / Inhalt der Logdatei
|
||||
|
||||
**EN:** Please paste the content of the error log file here. It is written automatically and contains error messages only — no personal data.
|
||||
**DE:** Bitte füge hier den Inhalt der Fehler-Logdatei ein. Sie wird automatisch geschrieben und enthält nur Fehlermeldungen — keine persönlichen Daten.
|
||||
|
||||
| OS | Path / Pfad |
|
||||
|----|-------------|
|
||||
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
|
||||
| Windows | `C:\Users\<YourName>\.praktiKST\kst4contest-errors.log` |
|
||||
|
||||
```
|
||||
Paste log content here / Loginhalt hier einfügen
|
||||
```
|
||||
|
||||
## Version
|
||||
|
||||
**KST4Contest version / Version** (e.g. 1.41.0):
|
||||
|
||||
**Java version / Java-Version** (`java -version`, e.g. 17.0.9):
|
||||
|
||||
**Operating system / Betriebssystem:** <!-- Linux / Windows / macOS -->
|
||||
|
||||
**Logging software / Logprogramm** (if applicable / falls relevant, e.g. UCXLog, N1MM+, WinTest):
|
||||
|
||||
## Checklist / Checkliste
|
||||
|
||||
- [ ] I have attached the log file / Ich habe die Logdatei angehängt
|
||||
- [ ] I have checked that this issue has not been reported before / Ich habe geprüft, dass dieses Problem noch nicht gemeldet wurde
|
||||
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -1,33 +0,0 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest a new feature or improvement / Neue Funktion oder Verbesserung vorschlagen
|
||||
title: "[FEATURE] "
|
||||
labels: enhancement
|
||||
---
|
||||
|
||||
**DE:** Bitte beschreibe deine Idee so genau wie möglich.
|
||||
**EN:** Please describe your idea as precisely as possible.
|
||||
|
||||
## Summary / Zusammenfassung
|
||||
|
||||
<!-- EN: A short summary of the feature you'd like. -->
|
||||
<!-- DE: Eine kurze Zusammenfassung der gewünschten Funktion. -->
|
||||
|
||||
## Motivation / Begründung
|
||||
|
||||
<!-- EN: Why would this feature be useful? What problem does it solve? -->
|
||||
<!-- DE: Warum wäre diese Funktion nützlich? Welches Problem löst sie? -->
|
||||
|
||||
## Detailed description / Detaillierte Beschreibung
|
||||
|
||||
<!-- EN: Describe the feature in detail. How should it work? -->
|
||||
<!-- DE: Beschreibe die Funktion im Detail. Wie soll sie funktionieren? -->
|
||||
|
||||
## Alternatives considered / Geprüfte Alternativen
|
||||
|
||||
<!-- EN: Have you considered any alternative solutions or workarounds? -->
|
||||
<!-- DE: Hast du alternative Lösungen oder Workarounds in Betracht gezogen? -->
|
||||
|
||||
## Checklist / Checkliste
|
||||
|
||||
- [ ] I have checked that this feature has not been requested before / Ich habe geprüft, dass diese Funktion noch nicht angefragt wurde
|
||||
@@ -1,155 +0,0 @@
|
||||
name: AUR Nightly (kst4contest-git)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Nightly Runtime Artifacts"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
AUR_SSH_DIR: /tmp/aur-ssh
|
||||
|
||||
jobs:
|
||||
update-aur-git:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
container:
|
||||
image: archlinux:latest
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pacman -Sy --noconfirm git openssh base-devel
|
||||
useradd -m builder
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Mark workspace as safe Git directory
|
||||
run: |
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
|
||||
- name: Compute pkgver
|
||||
id: ver
|
||||
run: |
|
||||
BASE=$(grep -m1 '<version>' pom.xml \
|
||||
| sed 's/.*<version>\(.*\)<\/version>.*/\1/' \
|
||||
| sed 's/[-.]nightly//')
|
||||
|
||||
GIT_PKGVER="${BASE}.r$(git rev-list --count HEAD).g$(git rev-parse --short HEAD)"
|
||||
|
||||
echo "Computed pkgver: ${GIT_PKGVER}"
|
||||
echo "pkgver=${GIT_PKGVER}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update PKGBUILD
|
||||
env:
|
||||
PKGVER: ${{ steps.ver.outputs.pkgver }}
|
||||
run: |
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" \
|
||||
packaging/aur/kst4contest-git/PKGBUILD
|
||||
|
||||
- name: Generate .SRCINFO
|
||||
run: |
|
||||
cp -r packaging/aur/kst4contest-git /tmp/kst4contest-git
|
||||
chown -R builder:builder /tmp/kst4contest-git
|
||||
|
||||
su builder -c \
|
||||
"cd /tmp/kst4contest-git && makepkg --printsrcinfo > .SRCINFO"
|
||||
|
||||
cp /tmp/kst4contest-git/.SRCINFO \
|
||||
packaging/aur/kst4contest-git/.SRCINFO
|
||||
|
||||
echo "Generated .SRCINFO:"
|
||||
cat packaging/aur/kst4contest-git/.SRCINFO
|
||||
|
||||
- name: Set up and verify AUR SSH
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p "${AUR_SSH_DIR}"
|
||||
|
||||
printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" \
|
||||
> "${AUR_SSH_DIR}/aur_ed25519"
|
||||
|
||||
sed -i 's/\r$//' "${AUR_SSH_DIR}/aur_ed25519"
|
||||
chmod 600 "${AUR_SSH_DIR}/aur_ed25519"
|
||||
|
||||
ssh-keygen -y \
|
||||
-f "${AUR_SSH_DIR}/aur_ed25519" \
|
||||
> /dev/null
|
||||
|
||||
ssh-keyscan \
|
||||
-T 10 \
|
||||
-t ed25519 \
|
||||
aur.archlinux.org \
|
||||
> "${AUR_SSH_DIR}/known_hosts"
|
||||
|
||||
if [ ! -s "${AUR_SSH_DIR}/known_hosts" ]; then
|
||||
echo "::error::No SSH host key was received from aur.archlinux.org."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Received AUR host-key fingerprint:"
|
||||
ssh-keygen -lf "${AUR_SSH_DIR}/known_hosts"
|
||||
|
||||
if ! ssh-keygen -lf "${AUR_SSH_DIR}/known_hosts" \
|
||||
| grep -Fq "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"; then
|
||||
echo "::error::The AUR SSH host-key fingerprint does not match the official fingerprint."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' \
|
||||
"Host aur.archlinux.org" \
|
||||
" HostName aur.archlinux.org" \
|
||||
" User aur" \
|
||||
" IdentityFile ${AUR_SSH_DIR}/aur_ed25519" \
|
||||
" IdentitiesOnly yes" \
|
||||
" StrictHostKeyChecking yes" \
|
||||
" UserKnownHostsFile ${AUR_SSH_DIR}/known_hosts" \
|
||||
> "${AUR_SSH_DIR}/config"
|
||||
|
||||
chmod 600 "${AUR_SSH_DIR}/config"
|
||||
chmod 600 "${AUR_SSH_DIR}/known_hosts"
|
||||
|
||||
- name: Push package metadata to AUR
|
||||
env:
|
||||
GIT_SSH_COMMAND: ssh -F /tmp/aur-ssh/config
|
||||
run: |
|
||||
git config --global user.email "philipp@wagnersnetz.de"
|
||||
git config --global user.name "Philipp Wagner"
|
||||
|
||||
AUR_DIR="/tmp/aur/kst4contest-git"
|
||||
|
||||
mkdir -p "$(dirname "${AUR_DIR}")"
|
||||
|
||||
git -c init.defaultBranch=master clone \
|
||||
"ssh://aur@aur.archlinux.org/kst4contest-git.git" \
|
||||
"${AUR_DIR}"
|
||||
|
||||
cp packaging/aur/kst4contest-git/PKGBUILD \
|
||||
"${AUR_DIR}/PKGBUILD"
|
||||
|
||||
cp packaging/aur/kst4contest-git/.SRCINFO \
|
||||
"${AUR_DIR}/.SRCINFO"
|
||||
|
||||
git -C "${AUR_DIR}" add PKGBUILD .SRCINFO
|
||||
|
||||
if git -C "${AUR_DIR}" diff --cached --quiet; then
|
||||
echo "kst4contest-git: package metadata is already current."
|
||||
else
|
||||
git -C "${AUR_DIR}" commit \
|
||||
-m "Update pkgver to ${{ steps.ver.outputs.pkgver }}"
|
||||
|
||||
git -C "${AUR_DIR}" push origin HEAD:master
|
||||
|
||||
echo "kst4contest-git: successfully pushed to AUR."
|
||||
fi
|
||||
@@ -1,177 +0,0 @@
|
||||
name: Publish AUR Packages
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tagged Release Build"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Release tag (e.g. v1.41.1) — defaults to latest stable release"
|
||||
required: false
|
||||
default: ""
|
||||
dry_run:
|
||||
description: "Dry run — skip AUR push and repo commit (for testing)"
|
||||
type: choice
|
||||
options: ["false", "true"]
|
||||
default: "false"
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
publish-aur:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: archlinux:latest
|
||||
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pacman -Sy --noconfirm git openssh curl base-devel nodejs
|
||||
useradd -m builder
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Resolve release version
|
||||
id: ver
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
if [[ -n "${INPUT_VERSION}" ]]; then
|
||||
TAG="${INPUT_VERSION}"
|
||||
else
|
||||
TAG=$(curl -sf \
|
||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/releases/latest" \
|
||||
| grep '"tag_name"' | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/')
|
||||
fi
|
||||
PKGVER="${TAG#v}"
|
||||
echo "tag=${TAG} → pkgver=${PKGVER}"
|
||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "pkgver=${PKGVER}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Compute SHA256 checksums
|
||||
id: sha
|
||||
run: |
|
||||
TAG="${{ steps.ver.outputs.tag }}"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
echo "Hashing pre-built Arch package..."
|
||||
SHA_BIN=$(curl -sfL \
|
||||
"https://github.com/${REPO}/releases/download/${TAG}/KST4Contest-${TAG}-archlinux-x86_64.pkg.tar.zst" \
|
||||
| sha256sum | awk '{print $1}')
|
||||
echo "bin: ${SHA_BIN}"
|
||||
|
||||
echo "Hashing source tarball..."
|
||||
SHA_SRC=$(curl -sfL \
|
||||
"https://github.com/${REPO}/archive/refs/tags/${TAG}.tar.gz" \
|
||||
| sha256sum | awk '{print $1}')
|
||||
echo "src: ${SHA_SRC}"
|
||||
|
||||
echo "bin=${SHA_BIN}" >> "$GITHUB_OUTPUT"
|
||||
echo "src=${SHA_SRC}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Update PKGBUILDs
|
||||
env:
|
||||
PKGVER: ${{ steps.ver.outputs.pkgver }}
|
||||
SHA_BIN: ${{ steps.sha.outputs.bin }}
|
||||
SHA_SRC: ${{ steps.sha.outputs.src }}
|
||||
run: |
|
||||
# kst4contest-bin
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" packaging/aur/kst4contest-bin/PKGBUILD
|
||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA_BIN}')/" packaging/aur/kst4contest-bin/PKGBUILD
|
||||
|
||||
# kst4contest (source build)
|
||||
sed -i "s/^pkgver=.*/pkgver=${PKGVER}/" packaging/aur/kst4contest/PKGBUILD
|
||||
sed -i "s/^sha256sums=.*/sha256sums=('${SHA_SRC}')/" 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//')
|
||||
GIT_PKGVER="${BASE_VER}.r$(git rev-list --count HEAD).g$(git rev-parse --short HEAD)"
|
||||
sed -i "s/^pkgver=.*/pkgver=${GIT_PKGVER}/" packaging/aur/kst4contest-git/PKGBUILD
|
||||
|
||||
echo "=== Updated PKGBUILD versions ==="
|
||||
grep -H '^pkgver=\|^sha256sums=' packaging/aur/*/PKGBUILD
|
||||
|
||||
- name: Generate .SRCINFO files
|
||||
run: |
|
||||
for pkg in kst4contest-bin kst4contest kst4contest-git; do
|
||||
cp -r "packaging/aur/${pkg}" "/tmp/${pkg}"
|
||||
chown -R builder:builder "/tmp/${pkg}"
|
||||
su builder -c "cd /tmp/${pkg} && makepkg --printsrcinfo > .SRCINFO"
|
||||
cp "/tmp/${pkg}/.SRCINFO" "packaging/aur/${pkg}/.SRCINFO"
|
||||
echo "=== ${pkg}/.SRCINFO ==="
|
||||
cat "packaging/aur/${pkg}/.SRCINFO"
|
||||
done
|
||||
|
||||
- name: Commit updated PKGBUILDs to repo
|
||||
if: inputs.dry_run != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git"
|
||||
git add packaging/aur/
|
||||
git diff --cached --quiet && echo "No PKGBUILD changes to commit." && exit 0
|
||||
git commit -m "chore: update AUR packages to ${{ steps.ver.outputs.tag }} [skip ci]"
|
||||
git push
|
||||
|
||||
- name: Set up AUR SSH
|
||||
if: inputs.dry_run != 'true'
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
printf '%s\n' "${AUR_SSH_PRIVATE_KEY}" > ~/.ssh/aur_ed25519
|
||||
chmod 600 ~/.ssh/aur_ed25519
|
||||
ssh-keyscan -t ed25519 aur.archlinux.org >> ~/.ssh/known_hosts
|
||||
cat >> ~/.ssh/config << 'EOF'
|
||||
Host aur.archlinux.org
|
||||
IdentityFile ~/.ssh/aur_ed25519
|
||||
User aur
|
||||
EOF
|
||||
|
||||
- name: Push to AUR
|
||||
if: inputs.dry_run != 'true'
|
||||
env:
|
||||
TAG: ${{ steps.ver.outputs.tag }}
|
||||
run: |
|
||||
git config --global user.email "philipp@wagnersnetz.de"
|
||||
git config --global user.name "Philipp Wagner"
|
||||
|
||||
push_to_aur() {
|
||||
local pkg="$1"
|
||||
local msg="$2"
|
||||
local aur_dir="/tmp/aur/${pkg}"
|
||||
|
||||
git clone "ssh://aur@aur.archlinux.org/${pkg}.git" "${aur_dir}" 2>/dev/null || {
|
||||
mkdir -p "${aur_dir}"
|
||||
git -C "${aur_dir}" init
|
||||
git -C "${aur_dir}" remote add origin "ssh://aur@aur.archlinux.org/${pkg}.git"
|
||||
}
|
||||
|
||||
cp "packaging/aur/${pkg}/PKGBUILD" "${aur_dir}/"
|
||||
cp "packaging/aur/${pkg}/.SRCINFO" "${aur_dir}/"
|
||||
git -C "${aur_dir}" add PKGBUILD .SRCINFO
|
||||
git -C "${aur_dir}" diff --cached --quiet \
|
||||
&& echo "${pkg}: no changes, skipping push" && return 0
|
||||
git -C "${aur_dir}" commit -m "${msg}"
|
||||
git -C "${aur_dir}" push origin HEAD:master
|
||||
echo "${pkg}: pushed to AUR"
|
||||
}
|
||||
|
||||
push_to_aur kst4contest-bin "Update to ${TAG}"
|
||||
push_to_aur kst4contest "Update to ${TAG}"
|
||||
push_to_aur kst4contest-git \
|
||||
"Update pkgver to $(grep '^pkgver=' packaging/aur/kst4contest-git/PKGBUILD | cut -d= -f2)"
|
||||
@@ -4,22 +4,13 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "src/**"
|
||||
- "packaging/icons/**"
|
||||
- "pom.xml"
|
||||
- "mvnw"
|
||||
- "mvnw.cmd"
|
||||
- ".github/workflows/nightly-artifacts.yml"
|
||||
schedule:
|
||||
- cron: "20 2 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-windows-zip:
|
||||
name: Build Windows ZIP
|
||||
@@ -39,11 +30,11 @@ jobs:
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "SHORT_SHA=$shortSha"
|
||||
Add-Content -Path $env:GITHUB_ENV -Value "ASSET_BASENAME=praktiKST-$version-$shortSha"
|
||||
|
||||
- name: Set up Java 21
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
java-version: "17"
|
||||
|
||||
- name: Install WiX Toolset
|
||||
shell: pwsh
|
||||
@@ -63,16 +54,7 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
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 javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec `
|
||||
--dest dist
|
||||
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
|
||||
|
||||
- name: Create Windows ZIP
|
||||
shell: pwsh
|
||||
@@ -101,13 +83,15 @@ jobs:
|
||||
run: |
|
||||
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 21
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -123,12 +107,11 @@ jobs:
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
- name: Create AppDir metadata
|
||||
@@ -170,433 +153,6 @@ jobs:
|
||||
path: dist/KST4Contest-*-linux-x86_64.AppImage
|
||||
retention-days: 14
|
||||
|
||||
build-linux-deb:
|
||||
name: Build Debian package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Resolve nightly version info
|
||||
run: |
|
||||
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends fakeroot
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build Debian package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type deb \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--linux-package-deps "libgstreamer1.0-0,libgstreamer-plugins-base1.0-0,gstreamer1.0-plugins-good" \
|
||||
--dest dist
|
||||
DEB="$(ls dist/*.deb | head -n 1)"
|
||||
if [ -z "$DEB" ]; then
|
||||
echo "No DEB produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$DEB" "dist/${ASSET_BASENAME}-debian-amd64.deb"
|
||||
|
||||
- name: Upload Debian artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-debian
|
||||
path: dist/KST4Contest-*-debian-amd64.deb
|
||||
retention-days: 14
|
||||
|
||||
build-linux-rpm:
|
||||
name: Build Fedora package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Resolve nightly version info
|
||||
run: |
|
||||
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends rpm
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build Fedora package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type rpm \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--linux-package-deps "gstreamer1,gstreamer1-plugins-base,gstreamer1-plugins-good" \
|
||||
--dest dist
|
||||
RPM="$(ls dist/*.rpm | head -n 1)"
|
||||
if [ -z "$RPM" ]; then
|
||||
echo "No RPM produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$RPM" "dist/${ASSET_BASENAME}-fedora-x86_64.rpm"
|
||||
|
||||
- name: Upload Fedora artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-fedora
|
||||
path: dist/KST4Contest-*-fedora-x86_64.rpm
|
||||
retention-days: 14
|
||||
|
||||
build-linux-arch:
|
||||
name: Build Arch Linux package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Resolve nightly version info
|
||||
run: |
|
||||
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends zstd
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--dest dist
|
||||
|
||||
- name: Build Arch Linux package artifact
|
||||
run: |
|
||||
ARCH=$(uname -m)
|
||||
PKGVER=$(printf '%s' "${VERSION}-${SHORT_SHA}" | sed 's/[^[:alnum:].+_]/_/g')
|
||||
PKGROOT="target/archpkg"
|
||||
rm -rf "$PKGROOT"
|
||||
mkdir -p "$PKGROOT/usr/lib/KST4Contest" "$PKGROOT/usr/bin"
|
||||
cp -a dist/KST4Contest/. "$PKGROOT/usr/lib/KST4Contest/"
|
||||
cat > "$PKGROOT/usr/bin/KST4Contest" << 'EOF'
|
||||
#!/bin/sh
|
||||
exec /usr/lib/KST4Contest/bin/KST4Contest "$@"
|
||||
EOF
|
||||
chmod 755 "$PKGROOT/usr/bin/KST4Contest"
|
||||
mkdir -p "$PKGROOT/usr/share/applications" "$PKGROOT/usr/share/icons/hicolor/256x256/apps"
|
||||
cat > "$PKGROOT/usr/share/applications/KST4Contest.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=KST4Contest
|
||||
Comment=ON4KST Chat Client for VHF/UHF contest operation
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
if [ -f "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" ]; then
|
||||
cp "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" "$PKGROOT/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
INSTALLED_SIZE=$(du -sb "$PKGROOT" | cut -f1)
|
||||
BUILDDATE=$(date +%s)
|
||||
WORKFLOW_SHA256=$(sha256sum "$GITHUB_WORKSPACE/.github/workflows/nightly-artifacts.yml" | awk '{print $1}')
|
||||
{
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "xdata = pkgtype=pkg"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgdesc = KST4Contest amateur radio contest logger"
|
||||
echo "url = https://github.com/${{ github.repository }}"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "size = ${INSTALLED_SIZE}"
|
||||
echo "arch = ${ARCH}"
|
||||
echo "license = custom"
|
||||
echo "depend = java-runtime"
|
||||
echo "depend = gst-plugins-base"
|
||||
echo "depend = gst-plugins-good"
|
||||
} > "$PKGROOT/.PKGINFO"
|
||||
{
|
||||
echo "format = 2"
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgarch = ${ARCH}"
|
||||
echo "pkgbuild_sha256sum = ${WORKFLOW_SHA256}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "builddir = /build"
|
||||
echo "startdir = /build"
|
||||
echo "buildtool = makepkg"
|
||||
echo "buildtoolver = 7.0.0-1-x86_64"
|
||||
echo "buildenv = !distcc !color !ccache check !sign"
|
||||
echo "options = !strip docs libtool staticlibs emptydirs zipman purge !debug !lto"
|
||||
} > "$PKGROOT/.BUILDINFO"
|
||||
tar --zstd --transform 's|^\./||' -cf "dist/${ASSET_BASENAME}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
|
||||
|
||||
- name: Upload Arch Linux artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-arch
|
||||
path: dist/KST4Contest-*-archlinux-*.pkg.tar.zst
|
||||
retention-days: 14
|
||||
|
||||
build-flatpak:
|
||||
name: Build Flatpak
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Resolve nightly version info
|
||||
run: |
|
||||
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Install Flatpak tooling
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils
|
||||
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak --user install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
mkdir -p target/flatpak-src
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--dest target/flatpak-src
|
||||
|
||||
- name: Create Flatpak manifest
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cat > target/de.x08.KST4Contest.yml << 'EOF'
|
||||
app-id: de.x08.KST4Contest
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: "24.08"
|
||||
sdk: org.freedesktop.Sdk
|
||||
command: KST4Contest
|
||||
finish-args:
|
||||
- --socket=wayland
|
||||
- --socket=x11
|
||||
- --socket=pulseaudio
|
||||
- --share=network
|
||||
- --share=ipc
|
||||
- --device=dri
|
||||
- --filesystem=~/.praktiKST
|
||||
- --env=ALSA_CONFIG_PATH=/app/share/alsa/asound.conf
|
||||
modules:
|
||||
- name: kst4contest
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- install -d /app/lib/KST4Contest /app/bin /app/share/applications /app/share/alsa
|
||||
- printf '@include /usr/share/alsa/alsa.conf\npcm.!default { type pulse }\nctl.!default { type pulse }\n' > /app/share/alsa/asound.conf
|
||||
- cp -a . /app/lib/KST4Contest/
|
||||
- printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest
|
||||
- chmod 755 /app/bin/KST4Contest
|
||||
- echo '[Desktop Entry]' > /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Type=Application' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Name=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Comment=ON4KST Chat Client for VHF/UHF contest operation' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Exec=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Icon=de.x08.KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- printf 'Categories=Network;HamRadio;\n' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Terminal=false' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- test -f /app/lib/KST4Contest/lib/KST4Contest.png && install -Dm644 /app/lib/KST4Contest/lib/KST4Contest.png /app/share/icons/hicolor/256x256/apps/de.x08.KST4Contest.png || true
|
||||
sources:
|
||||
- type: dir
|
||||
path: flatpak-src/KST4Contest
|
||||
EOF
|
||||
|
||||
- name: Import Flatpak signing key
|
||||
run: |
|
||||
echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import
|
||||
FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')
|
||||
echo "FLATPAK_GPG_KEY_ID=$FLATPAK_GPG_KEY_ID" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build Flatpak repo (nightly)
|
||||
run: |
|
||||
flatpak-builder --force-clean target/flatpak-build target/de.x08.KST4Contest.yml
|
||||
flatpak build-export --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo target/flatpak-build nightly
|
||||
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo
|
||||
|
||||
- name: Create flatpakref (nightly)
|
||||
run: |
|
||||
REPO_NAME="${GITHUB_REPOSITORY#*/}"
|
||||
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
|
||||
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
|
||||
cat > "dist/de.x08.KST4Contest.nightly.flatpakref" << EOF
|
||||
[Flatpak Ref]
|
||||
Name=de.x08.KST4Contest
|
||||
Branch=nightly
|
||||
Title=KST4Contest (Nightly) – ON4KST Chat Client
|
||||
Url=${PAGES_URL}
|
||||
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
|
||||
GPGKey=${GPG_KEY_B64}
|
||||
IsRuntime=false
|
||||
EOF
|
||||
|
||||
- name: Upload flatpakref
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: flatpakref
|
||||
path: dist/de.x08.KST4Contest.nightly.flatpakref
|
||||
|
||||
- name: Upload Flatpak OSTree repo
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: flatpak-ostree-repo
|
||||
path: target/flatpak-repo/
|
||||
|
||||
publish-flatpak-repo:
|
||||
name: Publish Flatpak OSTree Repo (nightly)
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-flatpak
|
||||
|
||||
steps:
|
||||
- name: Install Flatpak tooling
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends flatpak
|
||||
|
||||
- name: Import Flatpak signing key
|
||||
run: |
|
||||
echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import
|
||||
echo "FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download OSTree repo artifact
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: flatpak-ostree-repo
|
||||
path: flatpak-ostree-repo/
|
||||
|
||||
- name: Checkout existing flatpak-repo branch
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: flatpak-repo
|
||||
path: existing-flatpak-repo
|
||||
|
||||
- name: Merge nightly build into flatpak-repo
|
||||
run: |
|
||||
cd existing-flatpak-repo
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
# Copy the new OSTree build into the repo
|
||||
rsync -a ../flatpak-ostree-repo/ ./
|
||||
# Regenerate summary with all branches
|
||||
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" .
|
||||
# Generate .flatpakrepo with embedded GPG key so users can do remote-add without errors
|
||||
REPO_NAME="${GITHUB_REPOSITORY#*/}"
|
||||
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
|
||||
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
|
||||
cat > kst4contest.flatpakrepo << EOF
|
||||
[Flatpak Repo]
|
||||
Title=KST4Contest
|
||||
Url=${PAGES_URL}
|
||||
Homepage=https://github.com/${GITHUB_REPOSITORY}
|
||||
Comment=KST4Contest – ON4KST Chat Client for VHF/UHF contests
|
||||
GPGKey=${GPG_KEY_B64}
|
||||
EOF
|
||||
# Stage all changes (new/updated refs, summary, objects in OSTree)
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "Nightly flatpak: $(echo ${{ github.sha }} | cut -c1-7)"
|
||||
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:flatpak-repo
|
||||
fi
|
||||
|
||||
build-macos-dmg:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -618,11 +174,11 @@ jobs:
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
echo "ARCH=$ARCH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 21
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -638,12 +194,11 @@ jobs:
|
||||
jpackage \
|
||||
--type dmg \
|
||||
--name KST4Contest \
|
||||
--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 javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
env:
|
||||
@@ -662,4 +217,4 @@ jobs:
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.os }}
|
||||
path: dist/KST4Contest-*-macos-*.dmg
|
||||
retention-days: 14
|
||||
retention-days: 14
|
||||
|
||||
@@ -11,18 +11,18 @@ env:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
name: Compile (Java 21)
|
||||
name: Compile (Java 17)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
@@ -7,10 +7,7 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
issues: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
@@ -24,11 +21,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
java-version: "17"
|
||||
|
||||
- name: Install WiX Toolset
|
||||
shell: pwsh
|
||||
@@ -48,16 +45,7 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
New-Item -ItemType Directory -Force -Path dist | Out-Null
|
||||
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 javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec `
|
||||
--dest dist
|
||||
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
|
||||
|
||||
- name: Create Windows ZIP
|
||||
shell: pwsh
|
||||
@@ -81,11 +69,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -101,12 +89,11 @@ jobs:
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
- name: Create AppDir metadata
|
||||
@@ -147,354 +134,6 @@ jobs:
|
||||
name: linux-appimage
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
|
||||
build-linux-deb:
|
||||
name: Build Debian package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends fakeroot
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build Debian package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type deb \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--linux-package-deps "libgstreamer1.0-0,libgstreamer-plugins-base1.0-0,gstreamer1.0-plugins-good" \
|
||||
--dest dist
|
||||
DEB="$(ls dist/*.deb | head -n 1)"
|
||||
if [ -z "$DEB" ]; then
|
||||
echo "No DEB produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$DEB" "dist/KST4Contest-${{ github.ref_name }}-debian-amd64.deb"
|
||||
|
||||
- name: Upload Debian artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-debian
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-debian-amd64.deb
|
||||
|
||||
build-linux-rpm:
|
||||
name: Build Fedora package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends rpm
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build Fedora package
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type rpm \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--linux-package-deps "gstreamer1,gstreamer1-plugins-base,gstreamer1-plugins-good" \
|
||||
--dest dist
|
||||
RPM="$(ls dist/*.rpm | head -n 1)"
|
||||
if [ -z "$RPM" ]; then
|
||||
echo "No RPM produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$RPM" "dist/KST4Contest-${{ github.ref_name }}-fedora-x86_64.rpm"
|
||||
|
||||
- name: Upload Fedora artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-fedora
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-fedora-x86_64.rpm
|
||||
|
||||
build-linux-arch:
|
||||
name: Build Arch Linux package
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends zstd
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--dest dist
|
||||
|
||||
- name: Build Arch Linux package artifact
|
||||
run: |
|
||||
ARCH=$(uname -m)
|
||||
PKGVER=$(printf '%s' "${{ github.ref_name }}" | sed 's/[^[:alnum:].+_]/_/g')
|
||||
PKGROOT="target/archpkg"
|
||||
rm -rf "$PKGROOT"
|
||||
mkdir -p "$PKGROOT/usr/lib/KST4Contest" "$PKGROOT/usr/bin"
|
||||
cp -a dist/KST4Contest/. "$PKGROOT/usr/lib/KST4Contest/"
|
||||
cat > "$PKGROOT/usr/bin/KST4Contest" << 'EOF'
|
||||
#!/bin/sh
|
||||
exec /usr/lib/KST4Contest/bin/KST4Contest "$@"
|
||||
EOF
|
||||
chmod 755 "$PKGROOT/usr/bin/KST4Contest"
|
||||
mkdir -p "$PKGROOT/usr/share/applications" "$PKGROOT/usr/share/icons/hicolor/256x256/apps"
|
||||
cat > "$PKGROOT/usr/share/applications/KST4Contest.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=KST4Contest
|
||||
Comment=ON4KST Chat Client for VHF/UHF contest operation
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
if [ -f "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" ]; then
|
||||
cp "$PKGROOT/usr/lib/KST4Contest/lib/KST4Contest.png" "$PKGROOT/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
INSTALLED_SIZE=$(du -sb "$PKGROOT" | cut -f1)
|
||||
BUILDDATE=$(date +%s)
|
||||
WORKFLOW_SHA256=$(sha256sum "$GITHUB_WORKSPACE/.github/workflows/tagged-release.yml" | awk '{print $1}')
|
||||
{
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "xdata = pkgtype=pkg"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgdesc = KST4Contest amateur radio contest logger"
|
||||
echo "url = https://github.com/${{ github.repository }}"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "size = ${INSTALLED_SIZE}"
|
||||
echo "arch = ${ARCH}"
|
||||
echo "license = custom"
|
||||
echo "depend = java-runtime"
|
||||
echo "depend = gst-plugins-base"
|
||||
echo "depend = gst-plugins-good"
|
||||
} > "$PKGROOT/.PKGINFO"
|
||||
{
|
||||
echo "format = 2"
|
||||
echo "pkgname = kst4contest"
|
||||
echo "pkgbase = kst4contest"
|
||||
echo "pkgver = ${PKGVER}-1"
|
||||
echo "pkgarch = ${ARCH}"
|
||||
echo "pkgbuild_sha256sum = ${WORKFLOW_SHA256}"
|
||||
echo "packager = GitHub Actions"
|
||||
echo "builddate = ${BUILDDATE}"
|
||||
echo "builddir = /build"
|
||||
echo "startdir = /build"
|
||||
echo "buildtool = makepkg"
|
||||
echo "buildtoolver = 7.0.0-1-x86_64"
|
||||
echo "buildenv = !distcc !color !ccache check !sign"
|
||||
echo "options = !strip docs libtool staticlibs emptydirs zipman purge !debug !lto"
|
||||
} > "$PKGROOT/.BUILDINFO"
|
||||
tar --zstd --transform 's|^\./||' -cf "dist/KST4Contest-${{ github.ref_name }}-archlinux-${ARCH}.pkg.tar.zst" -C "$PKGROOT" .
|
||||
|
||||
- name: Upload Arch Linux artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-arch
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-archlinux-*.pkg.tar.zst
|
||||
|
||||
build-flatpak:
|
||||
name: Build Flatpak
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Install Flatpak tooling
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends flatpak flatpak-builder elfutils
|
||||
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak --user install -y flathub org.freedesktop.Platform//24.08 org.freedesktop.Sdk//24.08
|
||||
|
||||
- name: Build app-image with jpackage
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
mkdir -p target/flatpak-src
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon src/main/resources/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 \
|
||||
--dest target/flatpak-src
|
||||
|
||||
- name: Create Flatpak manifest
|
||||
run: |
|
||||
mkdir -p dist
|
||||
cat > target/de.x08.KST4Contest.yml << 'EOF'
|
||||
app-id: de.x08.KST4Contest
|
||||
runtime: org.freedesktop.Platform
|
||||
runtime-version: "24.08"
|
||||
sdk: org.freedesktop.Sdk
|
||||
command: KST4Contest
|
||||
finish-args:
|
||||
- --socket=wayland
|
||||
- --socket=x11
|
||||
- --socket=pulseaudio
|
||||
- --share=network
|
||||
- --share=ipc
|
||||
- --device=dri
|
||||
- --filesystem=~/.praktiKST
|
||||
- --env=ALSA_CONFIG_PATH=/app/share/alsa/asound.conf
|
||||
modules:
|
||||
- name: kst4contest
|
||||
buildsystem: simple
|
||||
build-commands:
|
||||
- install -d /app/lib/KST4Contest /app/bin /app/share/applications /app/share/alsa
|
||||
- printf '@include /usr/share/alsa/alsa.conf\npcm.!default { type pulse }\nctl.!default { type pulse }\n' > /app/share/alsa/asound.conf
|
||||
- cp -a . /app/lib/KST4Contest/
|
||||
- printf '#!/bin/sh\nexec /app/lib/KST4Contest/bin/KST4Contest "$@"\n' > /app/bin/KST4Contest
|
||||
- chmod 755 /app/bin/KST4Contest
|
||||
- echo '[Desktop Entry]' > /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Type=Application' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Name=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Comment=ON4KST Chat Client for VHF/UHF contest operation' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Exec=KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Icon=de.x08.KST4Contest' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- printf 'Categories=Network;HamRadio;\n' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- echo 'Terminal=false' >> /app/share/applications/de.x08.KST4Contest.desktop
|
||||
- test -f /app/lib/KST4Contest/lib/KST4Contest.png && install -Dm644 /app/lib/KST4Contest/lib/KST4Contest.png /app/share/icons/hicolor/256x256/apps/de.x08.KST4Contest.png || true
|
||||
sources:
|
||||
- type: dir
|
||||
path: flatpak-src/KST4Contest
|
||||
EOF
|
||||
|
||||
- name: Import Flatpak signing key
|
||||
run: |
|
||||
echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import
|
||||
FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')
|
||||
echo "FLATPAK_GPG_KEY_ID=$FLATPAK_GPG_KEY_ID" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Determine OSTree branch and flatpakref filename
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == beta-* ]]; then
|
||||
echo "OSTREE_BRANCH=beta" >> "$GITHUB_ENV"
|
||||
echo "FLATPAKREF_NAME=de.x08.KST4Contest.beta.flatpakref" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "OSTREE_BRANCH=stable" >> "$GITHUB_ENV"
|
||||
echo "FLATPAKREF_NAME=de.x08.KST4Contest.flatpakref" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
- name: Build Flatpak repo
|
||||
run: |
|
||||
flatpak-builder --force-clean target/flatpak-build target/de.x08.KST4Contest.yml
|
||||
flatpak build-export --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo target/flatpak-build "$OSTREE_BRANCH"
|
||||
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" target/flatpak-repo
|
||||
|
||||
- name: Create flatpakref
|
||||
run: |
|
||||
REPO_NAME="${GITHUB_REPOSITORY#*/}"
|
||||
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
|
||||
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
|
||||
cat > "dist/${FLATPAKREF_NAME}" << EOF
|
||||
[Flatpak Ref]
|
||||
Name=de.x08.KST4Contest
|
||||
Branch=${OSTREE_BRANCH}
|
||||
Title=KST4Contest (${{ startsWith(github.ref_name, 'beta-') && 'Beta' || 'Release' }}) – ON4KST Chat Client
|
||||
Url=${PAGES_URL}
|
||||
RuntimeRepo=https://flathub.org/repo/flathub.flatpakrepo
|
||||
GPGKey=${GPG_KEY_B64}
|
||||
IsRuntime=false
|
||||
EOF
|
||||
|
||||
- name: Upload flatpakref
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: flatpakref
|
||||
path: dist/*.flatpakref
|
||||
|
||||
- name: Upload Flatpak OSTree repo
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: flatpak-ostree-repo
|
||||
path: target/flatpak-repo/
|
||||
|
||||
build-macos-dmg:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -506,11 +145,11 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 21
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
@@ -526,14 +165,13 @@ jobs:
|
||||
jpackage \
|
||||
--type dmg \
|
||||
--name KST4Contest \
|
||||
--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 javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
|
||||
@@ -629,80 +267,16 @@ jobs:
|
||||
name: docs-pdf
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-manual-*.pdf
|
||||
|
||||
publish-flatpak-repo:
|
||||
name: Publish Flatpak OSTree Repo (${{ github.ref_name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-flatpak
|
||||
|
||||
steps:
|
||||
- name: Install Flatpak tooling
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends flatpak
|
||||
|
||||
- name: Import Flatpak signing key
|
||||
run: |
|
||||
echo "${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}" | gpg --batch --import
|
||||
echo "FLATPAK_GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: '/^fpr/{print $10; exit}')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Download OSTree repo artifact
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: flatpak-ostree-repo
|
||||
path: flatpak-ostree-repo/
|
||||
|
||||
- name: Checkout existing flatpak-repo branch
|
||||
uses: actions/checkout@v4.1.7
|
||||
with:
|
||||
ref: flatpak-repo
|
||||
path: existing-flatpak-repo
|
||||
|
||||
- name: Merge build into flatpak-repo
|
||||
run: |
|
||||
cd existing-flatpak-repo
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
rsync -a ../flatpak-ostree-repo/ ./
|
||||
# Regenerate summary with all branches (not just the one just built)
|
||||
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" .
|
||||
# Generate .flatpakrepo with embedded GPG key so users can do remote-add without errors
|
||||
REPO_NAME="${GITHUB_REPOSITORY#*/}"
|
||||
PAGES_URL="https://${GITHUB_REPOSITORY_OWNER}.github.io/${REPO_NAME}/"
|
||||
GPG_KEY_B64=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 -w 0)
|
||||
cat > kst4contest.flatpakrepo << EOF
|
||||
[Flatpak Repo]
|
||||
Title=KST4Contest
|
||||
Url=${PAGES_URL}
|
||||
Homepage=https://github.com/${GITHUB_REPOSITORY}
|
||||
Comment=KST4Contest – ON4KST Chat Client for VHF/UHF contests
|
||||
GPGKey=${GPG_KEY_B64}
|
||||
EOF
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
else
|
||||
git commit -m "Flatpak repo: ${{ github.ref_name }}"
|
||||
git push https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git HEAD:flatpak-repo
|
||||
fi
|
||||
|
||||
release-tag:
|
||||
name: Publish Tagged Release
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-windows-zip
|
||||
- build-linux-appimage
|
||||
- build-linux-deb
|
||||
- build-linux-rpm
|
||||
- build-linux-arch
|
||||
- build-macos-dmg
|
||||
- build-flatpak
|
||||
- build-docs-pdf
|
||||
- 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:
|
||||
@@ -722,30 +296,6 @@ jobs:
|
||||
merge-multiple: true
|
||||
path: release-assets/macos
|
||||
|
||||
- name: Download Debian artifact
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: linux-debian
|
||||
path: release-assets/debian
|
||||
|
||||
- name: Download Fedora artifact
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: linux-fedora
|
||||
path: release-assets/fedora
|
||||
|
||||
- name: Download Arch Linux artifact
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: linux-arch
|
||||
path: release-assets/archlinux
|
||||
|
||||
- name: Download flatpakref
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: flatpakref
|
||||
path: release-assets/flatpakref
|
||||
|
||||
- name: Download PDF manuals
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
@@ -766,52 +316,6 @@ jobs:
|
||||
artifacts: >-
|
||||
release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,
|
||||
release-assets/linux/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage,
|
||||
release-assets/debian/KST4Contest-${{ github.ref_name }}-debian-amd64.deb,
|
||||
release-assets/fedora/KST4Contest-${{ github.ref_name }}-fedora-x86_64.rpm,
|
||||
release-assets/archlinux/KST4Contest-${{ github.ref_name }}-archlinux-*.pkg.tar.zst,
|
||||
release-assets/flatpakref/*.flatpakref,
|
||||
release-assets/macos/KST4Contest-${{ github.ref_name }}-macos-*.dmg,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-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
|
||||
@@ -34,13 +34,4 @@ build/
|
||||
dist/
|
||||
|
||||
#zip files for local backups
|
||||
*.zip
|
||||
|
||||
# node Modules in website
|
||||
website/node_modules/
|
||||
|
||||
# built website output (rebuilt on the server)
|
||||
website/_site/
|
||||
|
||||
# Local secrets for act testing
|
||||
.secrets
|
||||
*.zip
|
||||
@@ -1,98 +1,38 @@
|
||||
# KST4Contest
|
||||
|
||||
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.
|
||||
|
||||
The application is developed by Marc Fröhlich (DO5AMF) and since 2026 Philipp Wagner (DN9APW).
|
||||
|
||||
## 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/)
|
||||
KST4Contest (also known as pratiKST) is a Java-based chat client for ON4KST, focused on VHF/UHF/SHF contest operation.
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is available in German and English:
|
||||
The full user documentation is maintained in the project 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/)
|
||||
- https://github.com/praktimarc/kst4contest/wiki
|
||||
|
||||
The Markdown sources used for the wiki and the generated PDF manuals are stored in [`github_docs`](github_docs/).
|
||||
Direct entry points:
|
||||
|
||||
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.
|
||||
- German start page: https://github.com/praktimarc/kst4contest/wiki/de-Home
|
||||
- English start page: https://github.com/praktimarc/kst4contest/wiki/en-Home
|
||||
|
||||
## Building from source
|
||||
## Build
|
||||
|
||||
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:
|
||||
Compile locally with Maven Wrapper:
|
||||
|
||||
```bash
|
||||
./mvnw clean test
|
||||
./mvnw -B -DskipTests compile
|
||||
```
|
||||
|
||||
Windows:
|
||||
## Notes
|
||||
|
||||
```powershell
|
||||
mvnw.cmd clean test
|
||||
mvnw.cmd -B -DskipTests compile
|
||||
```
|
||||
- Source code is under `src/`.
|
||||
- Documentation markdown pages for wiki/PDF are under `github_docs/`.
|
||||
|
||||
## 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
|
||||
## Status of the latest CI:
|
||||
Wiki Publishing:
|
||||
|
||||
[](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).
|
||||
[](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml)
|
||||
@@ -0,0 +1,2 @@
|
||||
dr2x
|
||||
oe3cin
|
||||
@@ -1,45 +1,53 @@
|
||||
# KST4Contest Manual / Handbuch
|
||||
# KST4Contest – Wiki
|
||||
|
||||
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** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), speziell entwickelt für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern.
|
||||
|
||||
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**
|
||||
- **DN9APW (Philipp Wagner)**, developer since / Entwickler seit Mai 2025
|
||||
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
|
||||
|
||||
---
|
||||
|
||||
## Language / Sprache
|
||||
## 🌐 Sprache / Language
|
||||
|
||||
| Deutsch | English |
|
||||
| 🇩🇪 Deutsch | 🇬🇧 English |
|
||||
|---|---|
|
||||
| [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.
|
||||
| [Startseite (Deutsch)](de-Home) | [Home (English)](en-Home) |
|
||||
|
||||
---
|
||||
|
||||
## Start here / Hier beginnen
|
||||
## 🇩🇪 Inhalt (Deutsch)
|
||||
|
||||
- [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.
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
|
||||
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
|
||||
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
|
||||
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
|
||||
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
|
||||
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
|
||||
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
|
||||
| [Changelog](de-Changelog) | Versionsgeschichte |
|
||||
|
||||
---
|
||||
|
||||
## Project links / Projektlinks
|
||||
## 🇬🇧 Contents (English)
|
||||
|
||||
- [Project website / Projektwebseite](https://kst4contest.hamradioonline.de/)
|
||||
- [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)
|
||||
- [Development roadmap / Entwicklungsstand](https://kst4contest.hamradioonline.de/roadmap/)
|
||||
| Page | Contents |
|
||||
|---|---|
|
||||
| [Installation](en-Installation) | Download, Java requirements, updates |
|
||||
| [Configuration](en-Configuration) | All settings in detail |
|
||||
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
|
||||
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
|
||||
| [Features](en-Features) | All features at a glance |
|
||||
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
|
||||
| [User Interface](en-User-Interface) | UI explained and how to operate it |
|
||||
| [Changelog](en-Changelog) | Version history |
|
||||
|
||||
---
|
||||
|
||||
## Schnellinfo / Quick Info
|
||||
|
||||
- **Download**: https://github.com/praktimarc/kst4contest/releases
|
||||
- **GitHub**: https://github.com/praktimarc/kst4contest
|
||||
- **Kontakt / Contact**: praktimarc+kst4contest@gmail.com
|
||||
|
||||
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 86 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](de-Makros-und-Variablen#variablen)
|
||||
Details: [Makros und Variablen](Makros-und-Variablen#variablen)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -26,38 +26,17 @@ Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfigu
|
||||
|
||||
| Spalte | Inhalt |
|
||||
|---|---|
|
||||
| Callsign | Rufzeichen der Station |
|
||||
| Name | Name beziehungsweise Zusatzinformationen aus dem Chat-Namensfeld |
|
||||
| QRA | Maidenhead-Locator |
|
||||
| Call | Rufzeichen der Station |
|
||||
| Name | Name aus dem Chat-Namenfeld |
|
||||
| Loc | Maidenhead-Locator |
|
||||
| QRB | Entfernung in km |
|
||||
| QTF | Richtung in Grad |
|
||||
| QRG | Zuletzt aus einer Chat-Nachricht erkannte Frequenz |
|
||||
| Tropo | Ergebnis der bandbezogenen Tropo- beziehungsweise Streckenbewertung |
|
||||
| Score | Aktueller, numerisch sortierbarer Prioritätswert des normalisierten Basisrufzeichens |
|
||||
| Act | Minuten seit der letzten Aktivität |
|
||||
| AP | AirScout-Flugzeugdaten, sofern aktiviert |
|
||||
| worked | Bandbezogener Worked-, Bandmöglichkeits- und Großfeldstatus sowie `wkdany` |
|
||||
| NOT QRV @ | Bänder, auf denen die Station manuell als nicht QRV markiert wurde |
|
||||
| Category | Chat-Kategorie des Eintrags |
|
||||
|
||||
Die QRG-Spalte zeigt die zuletzt für eine Station erkannte Frequenz. Fehlende Nullen werden für die Anzeige ergänzt, sodass beispielsweise `144.21` als `144.210` erscheint. Erkennt KST4Contest nacheinander Frequenzen auf mehreren Bändern, zeigt die Spalte den letzten Treffer; die internen Bandinformationen können trotzdem mehrere aktuelle Bänder der Station enthalten.
|
||||
|
||||
Relative Angaben werden zunächst mit einem höchstens 30 Minuten alten Bandkontext desselben Absenders kombiniert. Nur wenn dieser fehlt, verwendet KST4Contest das globale Fallback-Band. Erkennungsregeln, Beispiele und Grenzen: [QRG-Erkennung](de-Funktionen#qrg-erkennung).
|
||||
|
||||
### Worked-, Band- und Großfeldstatus
|
||||
|
||||
Die Unterspalten unter **worked** sind kompakt, weil bei mehreren aktivierten Bändern kaum Platz für ausgeschriebene Zustände bleibt. `X` kennzeichnet ein auf diesem Band gearbeitetes Rufzeichen. `a` und `B+` weisen auf ein angebotenes, noch nicht gearbeitetes Band hin. Ein angehängtes `o` bedeutet, dass das vierstellige Großfeld auf diesem Band bereits gearbeitet wurde.
|
||||
|
||||
Die Unterspalte **wkdany** ist bandunabhängig: `x` steht für ein bereits gearbeitetes Rufzeichen, `o` für ein auf irgendeinem Band gearbeitetes Großfeld und `xo` für beides.
|
||||
|
||||
Jede Statuszelle besitzt einen Tooltip mit der Legende und dem für die betreffende Station ermittelten Zustand. Die vollständige Herleitung einschließlich NOT-QRV-Vorrang: [Gearbeitete Rufzeichen, neue Bänder und neue Großfelder](de-Funktionen#gearbeitete-rufzeichen-neue-bänder-und-neue-großfelder).
|
||||
|
||||

|
||||
| QRG | Automatisch erkannte Frequenz |
|
||||
| AP | AirScout-Flugzeugdaten (wenn aktiv) |
|
||||
| Band-Farben | Worked/NOT-QRV-Status pro Band |
|
||||
|
||||
**Sortierung**: Klick auf Spaltenköpfe. QRB-Sortierung arbeitet numerisch (ab v1.22 korrigiert).
|
||||
|
||||
|
||||
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).
|
||||
@@ -72,223 +51,49 @@ Eingabefeld für die aktuelle Antennenrichtung. Wird für die geplante `MYQTF`-V
|
||||
|
||||
---
|
||||
|
||||
## Nachrichtentabellen
|
||||
|
||||
KST4Contest zeigt Nachrichtentexte bewusst einzeilig an. So bleiben auch bei hohem Chat-Aufkommen viele Einträge gleichzeitig sichtbar. Der Nachteil liegt auf der Hand: Bei einer schmalen **Message**-Spalte passt nicht jede Nachricht vollständig in die Zeile.
|
||||
|
||||
Ist ein Nachrichtentext breiter als die sichtbare Zelle, zeigt KST4Contest den vollständigen Inhalt als Tooltip an. Dazu die Maus kurz über die betreffende **Message**-Zelle halten. Passt der Text vollständig in die Spalte, wird kein zusätzlicher Volltext-Tooltip eingeblendet.
|
||||
|
||||
Webadressen mit `http://`, `https://` oder dem Präfix `www.` werden innerhalb des Nachrichtentextes als Links dargestellt. Ein Klick öffnet die Adresse im Standardbrowser des Betriebssystems. Andere Protokolle werden nicht als Link behandelt.
|
||||
|
||||

|
||||
|
||||
Damit muss der Divider nicht allein deshalb verschoben werden, um eine einzelne längere Nachricht zu lesen. Für einen dauerhaft breiteren Nachrichtenbereich kann er selbstverständlich weiterhin angepasst werden.
|
||||
|
||||
---
|
||||
|
||||
## Filter
|
||||
|
||||
Die Filterleiste befindet sich oberhalb der Chatmember-Tabelle. Sie ist in mehrere logisch zusammengehörige Bereiche gegliedert:
|
||||
Die Filter-Leiste (ab v1.21 als Flowpane für kleine Bildschirme):
|
||||
|
||||
- **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.
|
||||
- **Show only QTF**: Richtungsfilter aktivieren (Buttons N/NE/E/… oder Grad-Eingabe)
|
||||
- **Show only QRB [km] <=**: Entfernungsfilter aktivieren (Toggle-Button)
|
||||
- **Hide Worked [Band]**: Gearbeitete Stationen pro Band ausblenden (je ein Toggle pro Band)
|
||||
- **Hide NOT-QRV [Band]**: NOT-QRV-markierte Stationen pro Band ausblenden
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
---
|
||||
|
||||
## Stationsinfo-Panel (Further Info)
|
||||
|
||||
Rechts unten werden die Nachrichten der ausgewählten Station zusammengeführt. Dazu gehören öffentliche Nachrichten, Privatnachrichten an die eigene Station und – soweit im Chat sichtbar – Privatnachrichten an andere Stationen.
|
||||
Rechts unten: Zeigt alle Nachrichten einer ausgewählten Station (CQ-Nachrichten und PMs in einem Panel). Ein Nachrichtenfilter lässt sich über den Standard-Filter in den Preferences vorbelegen.
|
||||
|
||||
Der im Panel gewählte Filter bestimmt, welche dieser Nachrichten angezeigt werden. Unter **Settings → GUI** lässt sich festlegen, welcher Filter beim Öffnen einer Stationsinformation vorausgewählt ist:
|
||||
|
||||
- alle Nachrichten,
|
||||
- Privatnachrichten an die eigene Station,
|
||||
- Privatnachrichten an andere Stationen oder
|
||||
- öffentliche Nachrichten.
|
||||
|
||||
Die Einstellung verändert nur die Darstellung im Stationsinfo-Panel. Nachrichten werden dadurch weder verworfen noch aus den übrigen Nachrichtentabellen entfernt. Der Filter kann im Panel jederzeit für die aktuell betrachtete Station gewechselt werden.
|
||||
|
||||
Im unteren Bereich können für die ausgewählte Station bandbezogene **Not QRV**-Markierungen gesetzt werden. Sichtbar sind die Bänder, die in den Stationseinstellungen für die eigene Station aktiviert wurden. **tag not qrv all** setzt beziehungsweise entfernt die Markierung für alle unterstützten Bänder gemeinsam, einschließlich momentan nicht eingeblendeter Bänder.
|
||||
|
||||
Die Änderung wirkt sofort auf die Spalte **NOT QRV @**, die Bandmöglichkeiten und die zugehörigen Filter. Sie wird in der internen Datenbank gespeichert und nach einem Neustart wiederhergestellt.
|
||||
|
||||

|
||||
|
||||
Im selben Bereich wird der aktuelle **Priority score** der ausgewählten Station angezeigt.
|
||||
|
||||
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 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.
|
||||
Hier können auch **Sked-Erinnerungen** aktiviert werden.
|
||||
|
||||
---
|
||||
|
||||
## Prioritätsliste
|
||||
|
||||
Die kompakte Prioritätsleiste befindet sich rechts zwischen Benutzerliste und Further-Info-Bereich. Sie zeigt die beiden derzeit höchstbewerteten Kandidaten unmittelbar im Hauptfenster:
|
||||
|
||||
```text
|
||||
Priority: 1 RUFZEICHEN SCORE 2 RUFZEICHEN SCORE more
|
||||
```
|
||||
|
||||
Ein Klick auf einen der beiden Kandidaten wählt den dazugehörigen aktiven Chatmember aus. Dabei werden das vollständige Rufzeichen einschließlich Suffix und die zugehörige Chat-Kategorie verwendet.
|
||||
|
||||
Die Schaltfläche **more** öffnet ein separates Fenster mit bis zu 15 Kandidaten. Die Liste ist nach absteigendem Score sortiert. Ein Doppelklick wählt den betreffenden Kandidaten aus und schließt das Fenster.
|
||||
|
||||

|
||||
|
||||
Stationen mit einem Score von `0` werden nicht in die Prioritätsliste aufgenommen. In der Benutzerliste bleiben sie sichtbar, sodass der Ausschluss nachvollzogen und beispielsweise durch eine geänderte NOT-QRV-Markierung korrigiert werden kann.
|
||||
|
||||
Der Score wird für das normalisierte Basisrufzeichen berechnet. Mehrere aktive Varianten wie `9A0BB-2` und `9A0BB-70` können daher in der Benutzerliste denselben Wert anzeigen. Die Chatmember bleiben trotzdem getrennte Nachrichtenziele.
|
||||
|
||||
Neue Nachrichten, AirScout-Daten, Skeds und Statusänderungen lösen eine Neuberechnung aus. Zusätzlich erfolgt eine regelmäßige Aktualisierung im Hintergrund. Eine kurzzeitig noch nicht angepasste Reihenfolge ist deshalb kein Fehler.
|
||||
|
||||
Herleitung und Grenzen: [Prioritätsscore und Prioritätsliste](de-Funktionen#prioritätsscore-und-prioritätsliste-ab-v140).
|
||||
Zeigt die vom Score-Service berechneten Top-Kandidaten. Aktualisiert sich automatisch im Hintergrund basierend auf Richtung, Entfernung und AP-Verfügbarkeit.
|
||||
|
||||
---
|
||||
|
||||
## Stationskarte
|
||||
## Cluster & QSO der anderen
|
||||
|
||||
Die Stationskarte kann auf zwei Wegen geöffnet werden:
|
||||
|
||||
- **Windows → Show / hide station map** öffnet oder schließt das Kartenfenster.
|
||||
- **Show on map** im **Further Info**-Bereich öffnet die Karte und zentriert sie auf die ausgewählte Station.
|
||||
|
||||
Die Karte zeigt die Stationen, die auch nach Anwendung der aktuellen Benutzerlistenfilter noch sichtbar sind. Ein Hinweis in der Kopfzeile zeigt an, wenn eine gefilterte Ansicht aktiv ist.
|
||||
|
||||

|
||||
|
||||
Ein einzelner Stationsmarker kann direkt angeklickt werden. KST4Contest übernimmt die Station daraufhin als aktuelle Auswahl, scrollt die Benutzerliste zum passenden Chatmember und aktualisiert den **Further Info**-Bereich.
|
||||
|
||||
Marker, die bei der aktuellen Zoomstufe zu dicht beieinanderliegen, werden als Cluster mit einer Stationsanzahl angezeigt. Ein Klick auf einen Cluster vergrößert den betreffenden Kartenausschnitt. Erst ein anschließend sichtbarer einzelner Marker wählt eine konkrete Station aus.
|
||||
|
||||
Für die ausgewählte Station erscheinen rechts unter **Selected station**:
|
||||
|
||||
- Rufzeichen,
|
||||
- Locator,
|
||||
- QRB und QTF,
|
||||
- erkannte aktive Bänder,
|
||||
- gegebenenfalls `B+` für eine offene Bandmöglichkeit und
|
||||
- die zuletzt bekannten QRGs.
|
||||
|
||||
**Trigger cluster spot** sendet für die ausgewählte Station einen einzelnen Spot an die mit dem integrierten DX-Cluster-Server verbundenen Logprogramme. Die Schaltfläche setzt deshalb einen aktivierten Cluster-Server und mindestens einen verbundenen Client voraus.
|
||||
|
||||
Unterhalb der Karte befindet sich das Höhenprofil. Rechts werden die dazugehörigen Detailwerte angezeigt, unter anderem:
|
||||
|
||||
- verwendete Datenquelle und Anzahl der Höhenpunkte,
|
||||
- Analysefrequenz,
|
||||
- Erdkrümmungs- beziehungsweise Refraktionsmodell,
|
||||
- Radio- und Geländehorizont,
|
||||
- Fresnel-Freiheit,
|
||||
- erkannte Hindernisse,
|
||||
- Link-Budget,
|
||||
- geschätzter Empfangspegel und
|
||||
- eine zusammenfassende Pfadbewertung.
|
||||
|
||||
Mit **Hide path analysis** werden das Profil unterhalb der Karte und die ausführlichen Analysewerte rechts gemeinsam ausgeblendet. Der Kartenbereich erhält dadurch mehr Platz.
|
||||
|
||||

|
||||
|
||||
Der Hinweis **Path analysis is hidden** bleibt zusammen mit **Show path analysis** sichtbar. Die Funktion kann daher ohne Umweg wieder eingeschaltet werden. Der Zustand wird gespeichert.
|
||||
|
||||
Der Divider zwischen Karte und Detailbereich lässt sich horizontal verschieben. Bei schmalem Detailbereich werden längere Angaben umgebrochen; falls die Höhe nicht ausreicht, erscheint dort eine vertikale Scrollleiste.
|
||||
|
||||
Ausführliche Herleitung und Grenzen: [Stationskarte und Streckenanalyse](de-Funktionen#stationskarte-und-streckenanalyse-ab-v141)
|
||||
|
||||
---
|
||||
|
||||
## Globale Nachrichtentabs und Monitorfenster
|
||||
|
||||
Der untere Bereich des Hauptfensters enthält drei globale Nachrichtentabs. Ihr Inhalt ist nicht von der aktuell in der Benutzerliste ausgewählten Station abhängig.
|
||||
|
||||
| Tab | Inhalt |
|
||||
|---|---|
|
||||
| **Public messages** | Öffentliche Chatnachrichten, CQ-Rufe und Beacons |
|
||||
| **DXCluster messages** | Über ON4KST empfangene DX-Cluster-Meldungen |
|
||||
| **QSO of the other** | Gerichtete Nachrichten zwischen zwei anderen Stationen |
|
||||
|
||||

|
||||
|
||||
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).
|
||||
Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen – interessant in ruhigeren Phasen.
|
||||
|
||||
---
|
||||
|
||||
## Menü
|
||||
|
||||
### 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.
|
||||
### Window
|
||||
- **Use Dark Mode** (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.
|
||||
|
||||
---
|
||||
|
||||
## Fenstergrößen und Divider
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
Bei Problemen mit der Darstellung: Konfigurationsdatei löschen → KST4Contest erstellt neue Standardwerte.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,170 +4,9 @@
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## v1.42 – Nightly / in Entwicklung
|
||||
|
||||
> Stand dieses Abschnitts: 10. August 2026.
|
||||
> v1.42 ist noch kein veröffentlichtes Stable-Release. Bis zur Freigabe können weitere Änderungen hinzukommen.
|
||||
|
||||
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
|
||||
|
||||
- **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
|
||||
|
||||
- **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
|
||||
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
letzter Changelog bitte aus GitHub entnehmen. Der bisherige Changelog
|
||||
|
||||
## v1.40 (2026-02-16)
|
||||
**Großes Feature-Release: Score-System, AP-Timeline, Win-Test, PSTRotator**
|
||||
|
||||
@@ -2,224 +2,75 @@
|
||||
|
||||
> 🇬🇧 [English version](en-DX-Cluster-Server) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
Seit Version 1.23 enthält KST4Contest einen lokalen DX-Cluster-Server. Er übergibt erkannte Richtungsgelegenheiten und die dazugehörige Frequenz an den DX-Cluster-Client eines Logprogramms.
|
||||
Ab **Version 1.23** enthält KST4Contest einen integrierten DX-Cluster-Server. Dieser sendet Spots direkt an das Logprogramm, wenn eine Richtungs-Warnung ausgelöst wird.
|
||||
|
||||
Die Idee dazu stammt von OM0AAO, Viliam Petrik. Vielen Dank!
|
||||
*(Idee von OM0AAO, Viliam Petrik – danke!)*
|
||||
|
||||
---
|
||||
|
||||
## Warum überhaupt ein eigener DX-Cluster-Server?
|
||||
## Wozu dient der integrierte DX-Cluster-Server?
|
||||
|
||||
Eine interessante Frequenz im Chat zu erkennen ist nur der erste Schritt. Im Contest muss diese Information dort ankommen, wo sie unmittelbar verwendet werden kann: im Logprogramm und dessen Bandmap.
|
||||
Wenn KST4Contest erkennt, dass eine Station aus der eigenen Richtung ein Sked anfragt und gleichzeitig eine QRG bekannt ist, wird **automatisch ein DX-Cluster-Spot generiert** und an den Cluster-Client des Logprogramms gesendet.
|
||||
|
||||
KST4Contest verbindet deshalb zwei bereits vorhandene Informationen:
|
||||
|
||||
1. Aus einer gerichteten Chat-Nachricht lässt sich abschätzen, in welche Richtung die sendende Station ihre Antenne wahrscheinlich ausgerichtet hat.
|
||||
2. Aus derselben oder einer vorherigen Nachricht kann eine Frequenz der Station bekannt sein.
|
||||
|
||||
Treffen beide Informationen zusammen, erzeugt KST4Contest einen lokalen DX-Cluster-Spot. Der Logger kann diesen Spot in seiner Bandmap anzeigen und – abhängig von seiner eigenen Konfiguration – den Transceiver nach einem Klick auf die entsprechende Frequenz einstellen.
|
||||
|
||||
Im Klartext: Die Information muss nicht erst im Chat gefunden, gelesen, gemerkt und anschließend erneut in den Logger eingetragen werden. Genau diese kleinen Unterbrechungen kosten im Contest überraschend viel Aufmerksamkeit.
|
||||
Das Logprogramm zeigt den Spot in der Bandkarte an. Ein Klick auf den Spot stellt Frequenz und Mode des Transceivers direkt ein – ohne manuelles Eintippen.
|
||||
|
||||
---
|
||||
|
||||
## Wie wird eine Richtungsgelegenheit hergeleitet?
|
||||
## Einrichtung
|
||||
|
||||
Angenommen, Station A schreibt eine gerichtete Nachricht an Station B. KST4Contest verwendet die Richtung von A zu B als Näherung für die aktuelle Antennenrichtung von Station A. Anschließend wird geprüft, ob die eigene Station aus Sicht von A innerhalb des angenommenen Antennenkorridors liegt.
|
||||
### In KST4Contest
|
||||
|
||||
Dafür werden zwei Richtungen verglichen:
|
||||
In den Preferences → **DX-Cluster-Server-Einstellungen**:
|
||||
|
||||
- die Richtung von Station A zu Station B,
|
||||
- die Richtung von Station A zur eigenen Station.
|
||||
1. **Port** des internen Servers eintragen (z. B. 7300 oder 8000 – muss mit dem Logprogramm übereinstimmen).
|
||||
2. **Spotter-Rufzeichen** eintragen – **unbedingt ein anderes Rufzeichen als das Contest-Rufzeichen verwenden!**
|
||||
- Grund: Logprogramme filtern Spots, die vom eigenen Rufzeichen stammen, als „gearbeitet" heraus. Wenn der Spotter dasselbe Rufzeichen hat, werden die Spots nicht angezeigt.
|
||||
3. **Angenommene MHz** eintragen: Bei Frequenzangaben wie „.205" im Chat muss KST4Contest entscheiden, ob 144.205, 432.205 oder 1296.205 gemeint ist. Bei Einband-Contests einfach die entsprechende Bandmitte eintragen. Vollständige Frequenzangaben wie „144.205" oder „1296.338" im Chat werden immer korrekt erkannt.
|
||||
|
||||
Der in den Station Settings konfigurierte Antennen-Öffnungswinkel ist der vollständige Winkel. Für die Prüfung wird jeweils die Hälfte links und rechts der Richtung A → B angesetzt. Bei `70°` sind das somit `±35°`.
|
||||
### In UCXLog
|
||||
|
||||
Da ON4KST keine Antennendaten der fremden Station überträgt, verwendet KST4Contest den Öffnungswinkel der eigenen Antenne zugleich als Näherung für Station A. Das ist keine Messung der tatsächlichen Antennenrichtung, sondern eine bewusst einfache geometrische Annahme.
|
||||
- Verbindung zu einem DX-Cluster-Server konfigurieren:
|
||||
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
|
||||
- Port: Wie in KST4Contest konfiguriert
|
||||
- Passwort: kann leer bleiben
|
||||
- Über die Schaltfläche **„Send a test message to your log"** kann die Verbindung getestet werden.
|
||||
|
||||
Ein DX-Cluster-Spot wird nur erzeugt, wenn alle folgenden Bedingungen erfüllt sind:
|
||||
### In N1MM+
|
||||
|
||||
1. Eine gerichtete Nachricht wurde zwischen zwei anderen Stationen erkannt.
|
||||
2. Für Absender und Empfänger sind gültige Locator bekannt.
|
||||
3. Der Absender liegt innerhalb des konfigurierten maximalen QRB zur eigenen Station.
|
||||
4. Die eigene Station liegt aus Sicht des Absenders innerhalb des angenommenen Antennenkorridors.
|
||||
5. Für den Absender ist eine verwertbare Frequenz bekannt oder in der aktuellen Nachricht erkannt worden.
|
||||
6. Der lokale DX-Cluster-Server ist aktiviert.
|
||||
|
||||
Treffen die Bedingungen zu, wird der Spot unmittelbar beim Verarbeiten der Nachricht erzeugt. Die parallel angezeigte grüne Richtungsmarkierung bleibt dagegen fünf Minuten sichtbar und kann durch spätere Nachrichten verlängert oder vorzeitig entfernt werden.
|
||||
|
||||
Das Verfahren berücksichtigt weder Gelände noch aktuelle Ausbreitungsbedingungen und beweist keine tatsächliche Antennenstellung. Es erkennt eine plausible Gelegenheit. Die ausführliche Herleitung und ein Zahlenbeispiel stehen unter [Richtungsgelegenheiten aus gerichteten Nachrichten](de-Funktionen#richtungsgelegenheiten-aus-gerichteten-nachrichten).
|
||||
Ähnliche Einstellungen:
|
||||
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
|
||||
- Port: Wie in KST4Contest konfiguriert
|
||||
|
||||
---
|
||||
|
||||
## Welche Frequenz wird verwendet?
|
||||
## Funktionsweise
|
||||
|
||||
Ein DX-Cluster-Spot benötigt eine eindeutige Frequenz. KST4Contest verwendet dafür dieselbe QRG-Erkennung wie die Benutzerliste und die übrigen bandbezogenen Funktionen.
|
||||
Ein Spot wird generiert, wenn **beide** Bedingungen erfüllt sind:
|
||||
|
||||
Vollständige Frequenzen bestimmen ihr Band direkt:
|
||||
1. Eine **Richtungs-Warnung** wurde ausgelöst (Station macht ein Sked in die eigene Richtung).
|
||||
2. **QRG der Station ist bekannt** (aus dem Chat ausgelesen oder manuell eingetragen).
|
||||
|
||||
```text
|
||||
144.205
|
||||
432,088
|
||||
1296.338
|
||||
10368.100
|
||||
```
|
||||
Der generierte Spot enthält:
|
||||
- Rufzeichen der Station
|
||||
- Frequenz
|
||||
- Spotterzeit
|
||||
|
||||
Relative Angaben enthalten dagegen nur den Frequenzanteil innerhalb eines Bandes:
|
||||
|
||||
```text
|
||||
.205
|
||||
,205
|
||||
qrg 205
|
||||
freq is 205
|
||||
on 205
|
||||
205 MHz
|
||||
```
|
||||
|
||||
Eine nackte dreistellige Zahl wie `205` wird ohne Frequenzkontext nicht ausgewertet. Dasselbe gilt für `599`, `144` oder Formulierungen wie `worked 210 stations`. So verhindert KST4Contest, dass Signalrapporte, Bandnamen oder Zählwerte als scheinbar plausible QRG gespeichert und später an den Logger weitergegeben werden.
|
||||
|
||||
Bei einer relativen QRG wird das Band in dieser Reihenfolge bestimmt:
|
||||
|
||||
1. KST4Contest prüft, ob für denselben Absender innerhalb der letzten 30 Minuten bereits ein passender Bandkontext bekannt wurde.
|
||||
2. Sind mehrere aktuelle Bänder bekannt, wird der zuletzt aktualisierte plausible Kontext verwendet.
|
||||
3. Erst wenn kein geeigneter Stationskontext vorhanden ist, verwendet KST4Contest das unter **Fallback band for relative QRG detection** ausgewählte Band.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```text
|
||||
Globales Fallback: 144 MHz
|
||||
Letzte vollständige QRG der Station: 432.088 MHz
|
||||
Neue Chat-Angabe derselben Station: .100
|
||||
Erkannte QRG: 432.100 MHz
|
||||
DX-Cluster-Frequenz: 432100.0 kHz
|
||||
```
|
||||
|
||||
Ohne den aktuellen 432-MHz-Kontext würde dieselbe Angabe mit dem globalen Fallback zu `144.100 MHz` ergänzt.
|
||||
|
||||
Die QRG-Erkennung läuft vor der Richtungs- und Spotprüfung. Nennt eine Station ihre Frequenz erstmals in der gerichteten Nachricht, die zugleich eine Richtungsgelegenheit auslöst, kann bereits der daraus erzeugte Spot diese Frequenz enthalten. War zuvor eine andere QRG der Station bekannt, wird sie durch die neu erkannte Angabe aktualisiert.
|
||||
|
||||
Das Fallback-Band ist eine globale Einstellung der QRG-Erkennung. Seine Wirkung ist nicht auf den DX-Cluster beschränkt. Konfiguration, unterstützte Bänder und weitere Folgen sind unter [Fallback-Band für relative QRG-Erkennung](de-Konfiguration#fallback-band-für-relative-qrg-erkennung) beschrieben.
|
||||
Das Logprogramm kann den Spot dann in der Bandkarte anzeigen und den TRX per Mausklick auf die Frequenz abstimmen.
|
||||
|
||||
---
|
||||
|
||||
## Einrichtung in KST4Contest
|
||||
## Multi-Computer-Setup
|
||||
|
||||
Öffne den Reiter **Notification** in den Preferences.
|
||||
Wenn KST4Contest auf einem separaten Computer läuft (nicht auf dem Log-Computer):
|
||||
|
||||

|
||||
|
||||
Konfiguriere anschließend:
|
||||
|
||||
1. **Enable the local DX Cluster server …** aktivieren.
|
||||
2. Einen freien **TCP port** eintragen. Standard ist `8000`.
|
||||
3. Unter **Fallback band for relative QRG detection** das passende Band auswählen.
|
||||
4. Ein **Spotter callsign** festlegen.
|
||||
|
||||
Für das Spotter-Rufzeichen sollte nach Möglichkeit ein anderes Rufzeichen als das Contest-Rufzeichen verwendet werden. Einige Logger filtern Spots, die scheinbar von der eigenen Station stammen. Das Ergebnis wäre technisch korrekt erzeugt, aber in der Bandmap trotzdem unsichtbar – eine besonders unproduktive Art von Erfolg.
|
||||
|
||||
Änderungen am Aktivierungsstatus und am TCP-Port werden während einer laufenden Chat-Verbindung sofort angewendet. Bei einem Portwechsel werden vorhandene DX-Cluster-Verbindungen getrennt und müssen vom Logger neu aufgebaut werden.
|
||||
|
||||
Die Einstellungen werden erst mit **Save Settings** dauerhaft in der `preferences.xml` gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## Einrichtung im Logprogramm
|
||||
|
||||
Das Logprogramm wird als DX-Cluster-Client mit KST4Contest verbunden.
|
||||
|
||||
| Einstellung | KST4Contest und Logger auf demselben Computer | Logger auf einem anderen Computer |
|
||||
|---|---|---|
|
||||
| Host | `127.0.0.1` | IP-Adresse des KST4Contest-Computers |
|
||||
| Port | In KST4Contest konfigurierter TCP-Port | In KST4Contest konfigurierter TCP-Port |
|
||||
| Login | Beliebiges Rufzeichen, falls der Logger eines verlangt | Beliebiges Rufzeichen, falls der Logger eines verlangt |
|
||||
| Passwort | Nicht erforderlich | Nicht erforderlich |
|
||||
|
||||
KST4Contest wertet den vom Logger gesendeten Login nicht zur Authentifizierung aus. Die Verbindung ist für ein lokales oder vertrauenswürdiges Stationsnetz vorgesehen.
|
||||
|
||||
Wenn der Logger auf einem anderen Computer läuft, muss dessen Verbindung durch die lokale Firewall des KST4Contest-Computers zugelassen werden. Der Port sollte nicht ohne weiteren Schutz aus dem Internet erreichbar sein.
|
||||
|
||||
Mehrere DX-Cluster-Clients können gleichzeitig verbunden werden. Ein erzeugter Spot wird an alle aktuell verbundenen Clients gesendet.
|
||||
|
||||
---
|
||||
|
||||
## Verbindung testen
|
||||
|
||||
Die Schaltfläche **Send test spot** erzeugt einen neutralen Testeintrag:
|
||||
|
||||
```text
|
||||
Spotted callsign: DO5AMF
|
||||
Comment: KST4CONTEST TEST
|
||||
Frequency: .300 des konfigurierten Fallback-Bandes
|
||||
```
|
||||
|
||||
Bei einem Fallback-Band von `144` erscheint der Spot daher auf ungefähr `144.300 MHz`.
|
||||
|
||||
Vor dem Test müssen drei Bedingungen erfüllt sein:
|
||||
|
||||
1. KST4Contest ist mit dem ON4KST-Chat verbunden.
|
||||
2. Der lokale DX-Cluster-Server ist aktiviert.
|
||||
3. Der DX-Cluster-Client des Logprogramms ist mit KST4Contest verbunden.
|
||||
|
||||
Fehlt die Client-Verbindung, zeigt KST4Contest eine entsprechende Meldung an. Ein erfolgreich ausgeführter Test bedeutet damit tatsächlich, dass mindestens ein Client den Spot erhalten hat.
|
||||
|
||||
---
|
||||
|
||||
## Inhalt eines erzeugten Spots
|
||||
|
||||
Ein Spot enthält:
|
||||
|
||||
- das konfigurierte Spotter-Rufzeichen,
|
||||
- die normalisierte Frequenz,
|
||||
- das Rufzeichen der erkannten Station,
|
||||
- den Locator,
|
||||
- Flugzeug-Scatter-Informationen, falls vorhanden,
|
||||
- die aktuelle UTC-Zeit.
|
||||
|
||||
Wenn für die Station aktuelle Aircraft-Scatter-Informationen vorliegen, kann KST4Contest diese als zusätzliche AP-Information in den Kommentar des Spots aufnehmen.
|
||||
|
||||
---
|
||||
|
||||
## Wenn kein Spot erscheint
|
||||
|
||||
### Der Testspot kommt nicht im Logger an
|
||||
|
||||
Prüfe:
|
||||
|
||||
- Ist KST4Contest mit dem Chat verbunden?
|
||||
- Ist der lokale DX-Cluster-Server aktiviert?
|
||||
- Verwendet der Logger denselben TCP-Port?
|
||||
- Verwendet der Logger bei lokalem Betrieb `127.0.0.1`?
|
||||
- Blockiert eine Firewall die Verbindung?
|
||||
- Ist im Logger das DX-Cluster-Fenster beziehungsweise die Bandmap aktiviert?
|
||||
|
||||
### Testspot funktioniert, aber reale Spots fehlen
|
||||
|
||||
Dann funktioniert die Verbindung grundsätzlich. Für die betreffende Chat-Situation war wahrscheinlich mindestens eine fachliche Bedingung nicht erfüllt:
|
||||
|
||||
- kein gerichteter Nachrichtenaustausch,
|
||||
- fehlender Locator,
|
||||
- Station außerhalb des maximalen QRB,
|
||||
- Richtung außerhalb des konfigurierten Öffnungswinkels,
|
||||
- keine erkannte Frequenz.
|
||||
|
||||
KST4Contest sendet absichtlich nicht jede gefundene Frequenz an den Logger. Andernfalls würde aus einer Arbeitserleichterung sehr schnell eine lokale Spot-Schleuder.
|
||||
|
||||
### Der Spot erscheint auf dem falschen Band
|
||||
|
||||
Prüfe zuerst, welche Frequenzen für die betreffende Station innerhalb der letzten 30 Minuten erkannt wurden. Bei einer relativen Angabe hat dieser Stationskontext Vorrang vor dem globalen Fallback.
|
||||
|
||||
Ist kein aktueller Stationskontext vorhanden, prüfe die Auswahl unter **Fallback band for relative QRG detection**. Das Fallback wird nur benötigt, wenn sich das Band weder aus einer vollständigen Frequenz noch aus dem aktuellen Kontext des Absenders ergibt.
|
||||
|
||||
### Der Spot wird vom Logger ausgeblendet
|
||||
|
||||
Verwende ein Spotter-Rufzeichen, das nicht mit dem eigenen Contest-Rufzeichen identisch ist. Abhängig vom Logger können eigene Spots gefiltert oder besonders behandelt werden.
|
||||
- Host im Logprogramm: IP des KST4Contest-Computers (nicht `127.0.0.1`)
|
||||
- Entspricht der Konfiguration der QSO-UDP-Broadcast-Pakete (siehe [Log-Synchronisation](de-Log-Synchronisation))
|
||||
|
||||
---
|
||||
|
||||
## Getestete Logprogramme
|
||||
|
||||
Die Schnittstelle wurde mit folgenden Logprogrammen verwendet:
|
||||
- **UCXLog** ✓
|
||||
- **N1MM+** ✓
|
||||
|
||||
- UCXLog
|
||||
- N1MM+
|
||||
|
||||
Weitere Logger können funktionieren, wenn sie eine normale TCP-Verbindung zu einem DX-Cluster-Server unterstützen.
|
||||
Weitere Testergebnisse sind willkommen – bitte per E-Mail an DO5AMF melden.
|
||||
|
||||
@@ -2,209 +2,57 @@
|
||||
|
||||
> 🇬🇧 [English version](en-Features) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
Dieses Kapitel beschreibt die wichtigsten Funktionen von KST4Contest, ihre Herleitung und die Grenzen der daraus gewonnenen Informationen.
|
||||
Übersicht aller Hauptfunktionen von KST4Contest.
|
||||
|
||||
---
|
||||
## Richtungsgelegenheiten aus gerichteten Nachrichten
|
||||
|
||||
Im ON4KST-Chat ist sichtbar, welche Station eine Nachricht an welche andere Station richtet. Eine tatsächliche Antennenrichtung wird dabei nicht übertragen. Für den Contestbetrieb lässt sich aus einer solchen Nachricht trotzdem eine brauchbare Annahme ableiten: Wer einen Sked anfragt, beantwortet oder vorbereitet, richtet seine Antenne normalerweise zumindest ungefähr auf die angesprochene Station.
|
||||
## Sked-Richtungs-Hervorhebung
|
||||
|
||||
KST4Contest wertet deshalb gerichtete Nachrichten zwischen zwei anderen Stationen aus. Die Nachricht muss nicht ausdrücklich als Sked gekennzeichnet sein. Entscheidend sind der Absender, der Empfänger und deren Locator.
|
||||
Eine der Kernfunktionen: Wenn eine Station ein Sked in die **eigene Richtung** sendet, wird sie in der Benutzerliste **grün und fett** hervorgehoben.
|
||||
|
||||
### Wie wird die Richtung hergeleitet?
|
||||
### Wie funktioniert das?
|
||||
|
||||
Angenommen, Station A schreibt eine gerichtete Nachricht an Station B:
|
||||
Die Berechnung basiert auf folgender Logik:
|
||||
|
||||
1. KST4Contest berechnet die Richtung von Station A zu Station B.
|
||||
2. Diese Richtung wird als wahrscheinliche Antennenrichtung von Station A verwendet.
|
||||
3. Anschließend wird die Richtung von Station A zur eigenen Station berechnet.
|
||||
4. Die Winkeldifferenz wird mit der Hälfte des konfigurierten Antennen-Öffnungswinkels verglichen.
|
||||
5. Zusätzlich muss Station A innerhalb des konfigurierten maximalen QRB liegen.
|
||||
- Wenn Station A eine Sked-Anfrage an Station B sendet, wird angenommen, dass A ihre Antenne auf B ausrichtet.
|
||||
- Wenn die daraus resultierende Richtung von A zur eigenen Station innerhalb des halben Öffnungswinkels der eigenen Antenne liegt, wird A hervorgehoben.
|
||||
|
||||
Ein eingetragener Öffnungswinkel von `70°` ergibt damit einen angenommenen Korridor von jeweils `35°` links und rechts der Richtung von Station A zu Station B.
|
||||
**Beispiel** (Öffnungswinkel 69°, Halbwinkel 34,5°):
|
||||
|
||||
| Beispiel | Ergebnis |
|
||||
| Situation | Ergebnis für DO5AMF in JN49 |
|
||||
|---|---|
|
||||
| Richtung A → B: `120°`, Richtung A → eigene Station: `145°` | Winkeldifferenz `25°`: Richtungsgelegenheit erkannt |
|
||||
| Richtung A → B: `120°`, Richtung A → eigene Station: `165°` | Winkeldifferenz `45°`: außerhalb des angenommenen Korridors |
|
||||
| Locator von A oder B fehlt | Keine Richtungsberechnung möglich |
|
||||
| A liegt außerhalb des maximalen QRB | Keine Richtungsgelegenheit |
|
||||
| Sked von F5FEN → DM5M | ✅ Hervorhebung (F5FEN zeigt Richtung DM5M, das liegt nahe JN49) |
|
||||
| Sked von DM5M → F5FEN | ✅ Hervorhebung (DM5M antwortet in Richtung F5FEN) |
|
||||
| F1DBN ist unbeteiligt | ❌ Keine Hervorhebung |
|
||||
| DO5AMF/P (anderer Standort) | ❌ Keine Hervorhebung für Sked-Antwort |
|
||||
|
||||
### Was wird in der Benutzerliste angezeigt?
|
||||
Die Berechnung berücksichtigt keine topografischen Wegberechnungen – das ist eine bewusste Vereinfachung. Möglicherweise wird das in einer späteren Version ergänzt.
|
||||
|
||||
Wird eine Richtungsgelegenheit erkannt, erscheint das Rufzeichen des Absenders in der Benutzerliste grün und fett. Im Evening-Modus wird dafür ein helleres Grün verwendet. Der Empfänger der Nachricht wird nicht allein deshalb markiert; eine Antwort in Gegenrichtung wird als eigene Nachricht und damit als neuer Fall berechnet.
|
||||
|
||||

|
||||
|
||||
Im Bild sendete DF0GEB eine gerichtete Nachricht an DN9APW und bekam eine Antwort. KST4Contest erkannte die Richtungsgelegenheit und markierte DN9APW in der Benutzerliste.
|
||||
Zur Verdeutlichung ist die MAP eingeblendet. Ich stehe als Empfänger zwischen beiden Stationen und bekomme deswegen die Warnung.
|
||||
|
||||
Die Markierung bleibt fünf Minuten ab der letzten passenden Nachricht sichtbar. Eine weitere passende Nachricht derselben Station beginnt diesen Zeitraum erneut. Sendet die Station vorher eine gerichtete Nachricht, deren Richtung die Bedingungen nicht erfüllt, wird die Markierung unmittelbar entfernt.
|
||||
|
||||
Ist die einfache Soundausgabe aktiviert, gibt KST4Contest beim erstmaligen Erkennen der Richtungsgelegenheit zusätzlich einen kurzen Hinweis aus. Solange die Station bereits markiert ist, wird derselbe Hinweis nicht mit jeder weiteren passenden Nachricht wiederholt.
|
||||
|
||||
### Was bedeutet die Markierung – und was nicht?
|
||||
|
||||
Die Berechnung ist eine geometrische Herleitung. Sie beweist nicht, dass Station A ihre Antenne tatsächlich auf Station B ausgerichtet hat. Ebenso wenig berücksichtigt sie Gelände, aktuelle Ausbreitungsbedingungen, die reale Antennencharakteristik der fremden Station oder deren Rotatorposition.
|
||||
|
||||
ON4KST liefert keinen individuellen Öffnungswinkel für die fremde Station. KST4Contest verwendet deshalb den für die eigene Antenne konfigurierten Wert auch als Näherung für Station A. Ein zu großer Wert erzeugt entsprechend mehr mögliche Richtungsgelegenheiten, ein zu kleiner Wert kann brauchbare Situationen übersehen.
|
||||
|
||||
Im Klartext: Die grüne Markierung ist ein begründeter Hinweis auf eine mögliche Gelegenheit. Sie ist weder eine Ausbreitungsvorhersage noch eine Garantie für ein QSO.
|
||||
|
||||
Konfiguration:
|
||||
|
||||
- [Antennen-Öffnungswinkel](de-Konfiguration#antennen-öffnungswinkel-antenna-beamwidth)
|
||||
- [Standard-Maximum-QRB](de-Konfiguration#standard-maximum-qrb)
|
||||
> Konfiguration: [Konfiguration – Antennen-Öffnungswinkel](Konfiguration#antennen-öffnungswinkel-antenna-beamwidth)
|
||||
|
||||
---
|
||||
|
||||
## Weitergabe als DX-Cluster-Spot
|
||||
## Sked-Richtungs-Spots (Integrierter DX-Cluster)
|
||||
|
||||
Seit Version 1.23 kann KST4Contest eine erkannte Richtungsgelegenheit an den DX-Cluster-Client eines Logprogramms weitergeben. Dafür muss der lokale DX-Cluster-Server aktiviert und für den Absender eine verwertbare Frequenz bekannt sein.
|
||||
|
||||
Die Frequenz kann bereits aus einer früheren Nachricht stammen oder erstmals in der aktuell auslösenden Nachricht stehen. In beiden Fällen steht sie der Spot-Prüfung zur Verfügung. KST4Contest überträgt damit nicht jede im Chat gefundene QRG, sondern nur Frequenzen, die mit einer geometrisch passenden gerichteten Nachricht zusammenfallen.
|
||||
|
||||
Die Fünf-Minuten-Markierung und der DX-Cluster-Spot beruhen auf derselben Richtungsberechnung, haben aber einen unterschiedlichen Lebenszyklus: Die Markierung bleibt vorübergehend in der Benutzerliste sichtbar. Der Spot wird unmittelbar beim Verarbeiten der passenden Nachricht erzeugt.
|
||||
|
||||
Einrichtung, Frequenzbehandlung und Grenzen: [Integrierter DX-Cluster-Server](de-DX-Cluster-Server).
|
||||
Ab **v1.23**: Richtungs-Warnungen werden als DX-Cluster-Spots an das Logprogramm weitergeleitet, wenn eine QRG bekannt ist. Details: [DX-Cluster-Server](de-DX-Cluster-Server).
|
||||
|
||||
---
|
||||
|
||||
## QRG-Erkennung
|
||||
## QRG-Erkennung (QRG Reading)
|
||||
|
||||
Im ON4KST-Chat werden Frequenzen selten einheitlich geschrieben. Eine Station nennt beispielsweise zuerst `432.088`, später nur noch `.100` und in einer weiteren Nachricht `qrg 120`. Für einen Menschen ist der Zusammenhang meistens klar. Ein Programm muss dagegen unterscheiden, ob `120` eine Frequenz, eine Zeitangabe, eine Entfernung oder etwas völlig anderes bedeutet.
|
||||
KST4Contest verarbeitet jede Chat-Nachricht und extrahiert automatisch **Frequenzangaben**. Diese werden in der Benutzerliste in der **QRG-Spalte** angezeigt.
|
||||
|
||||
KST4Contest wertet deshalb den Text jeder öffentlichen und gerichteten Chat-Nachricht aus. Eine erkannte QRG wird dem Absender zugeordnet und in der **QRG-Spalte** der Benutzerliste angezeigt. Die Spalte enthält die zuletzt erkannte Frequenz und stellt mindestens drei Nachkommastellen dar. Ein intern als `144.21` gespeicherter Wert erscheint damit als `144.210`.
|
||||
Erkannte Formate: `144.205`, `432.088`, `.205` (mit konfigurierter Bandannahme), etc.
|
||||
|
||||
### Welche Angaben werden erkannt?
|
||||
|
||||
| Schreibweise | Beispiel | Verarbeitung |
|
||||
|---|---|---|
|
||||
| Vollständige Frequenz | `144.210`, `432,088`, `10368.100` | Das Band ergibt sich direkt aus der Frequenz. |
|
||||
| Relative Frequenz mit Punkt oder Komma | `.210`, `,088` | Das Band wird aus dem Stationskontext oder dem konfigurierten Fallback ergänzt. |
|
||||
| Dreistellige Frequenz mit Textkontext | `qrg 210`, `freq is 210`, `on 210`, `210 MHz` | Die Zahl wird als relative Frequenz behandelt. |
|
||||
| Dreistellige Zahl ohne Frequenzkontext | `210`, `599`, `144` | Die Zahl wird absichtlich nicht als QRG übernommen. |
|
||||
|
||||
Die letzte Einschränkung verhindert plausible, aber falsche Ergebnisse. Mit einem Fallback von `144 MHz` ließe sich ein Signalrapport `599` technisch problemlos zu `144.599 MHz` zusammensetzen. Das Ergebnis wäre formal gültig und fachlich trotzdem Unsinn.
|
||||
|
||||
### Wie wird das Band einer relativen QRG bestimmt?
|
||||
|
||||
KST4Contest verwendet folgende Reihenfolge:
|
||||
|
||||
1. Wurde für denselben Absender innerhalb der letzten 30 Minuten bereits eine passende vollständige Frequenz erkannt, verwendet KST4Contest deren Band.
|
||||
2. Sind mehrere aktuelle Bänder bekannt, wird der zuletzt aktualisierte plausible Bandkontext verwendet.
|
||||
3. Fehlt ein geeigneter Stationskontext, verwendet KST4Contest das unter **Fallback band for relative QRG detection** ausgewählte Band.
|
||||
|
||||
Beispiel: Das globale Fallback steht auf `144 MHz`. Eine Station nennt zunächst `432.088` und schreibt wenige Minuten später `.100`. KST4Contest ergänzt nicht das globale Fallback, sondern den aktuelleren Stationskontext. Das Ergebnis ist `432.100 MHz`. Schreibt eine andere Station ohne vorherige Bandinformation `.100`, wird daraus `144.100 MHz`.
|
||||
|
||||
Das Fallback-Band ist damit tatsächlich nur der letzte Ausweg. Es wird aus den von KST4Contest unterstützten Bandwerten ausgewählt und wirkt auf die gesamte QRG-Erkennung – nicht nur auf den integrierten DX-Cluster.
|
||||
|
||||
### Wofür wird die erkannte QRG verwendet?
|
||||
|
||||
Die zuletzt erkannte Frequenz erscheint in der Benutzerliste. Der zugehörige Bandkontext kann außerdem in weitere Funktionen einfließen, beispielsweise in:
|
||||
|
||||
- die Erkennung aktiver Bänder einer Station,
|
||||
- den Chatmember-Score und die Prioritätslisten,
|
||||
- Band-Upgrade-Hinweise nach einem Logeintrag,
|
||||
- die Frequenzwahl bei Skeds,
|
||||
- einen DX-Cluster-Spot aus einer erkannten Richtungsgelegenheit.
|
||||
|
||||
Steht die QRG erstmals in der Nachricht, die zugleich eine Richtungsgelegenheit auslöst, wird sie vor der Richtungs- und Spotprüfung verarbeitet. Der daraus erzeugte Spot kann deshalb bereits die Frequenz dieser Nachricht verwenden.
|
||||
|
||||
Die Erkennung bleibt eine Textauswertung. KST4Contest kann nicht beweisen, dass die Station noch auf der genannten Frequenz arbeitet oder ob sich eine mehrdeutige Angabe auf einen anderen Zusammenhang bezieht. Genau deshalb werden nackte dreistellige Zahlen ohne Frequenzkontext nicht mehr übernommen.
|
||||
|
||||
Konfiguration und unterstützte Fallback-Bänder: [Fallback-Band für relative QRG-Erkennung](de-Konfiguration#fallback-band-für-relative-qrg-erkennung).
|
||||
|
||||
Verwendung in der Bandmap eines Logprogramms: [Integrierter DX-Cluster-Server](de-DX-Cluster-Server).
|
||||
**Nutzen**: Ohne nachzufragen kann man direkt auf die QRG einer Station schauen und entscheiden, ob eine Verbindung möglich ist.
|
||||
|
||||
---
|
||||
## Gearbeitete Rufzeichen, neue Bänder und neue Großfelder
|
||||
|
||||
KST4Contest unterscheidet drei Informationen, die im Contest ähnlich aussehen können, aber unterschiedliche Fragen beantworten:
|
||||
## Worked-Markierung
|
||||
|
||||
1. Wurde dieses Rufzeichen bereits gearbeitet?
|
||||
2. Wurde dieses Rufzeichen auf einem bestimmten Band gearbeitet?
|
||||
3. Wurde das vierstellige Maidenhead-Großfeld bereits gearbeitet – möglicherweise mit einer anderen Station?
|
||||
|
||||
Diese Trennung ist notwendig. Ein bereits gearbeitetes Rufzeichen kann auf einem anderen Band weiterhin interessant sein. Umgekehrt kann eine noch nicht gearbeitete Station in einem Großfeld liegen, das bereits im Log steht.
|
||||
|
||||
### Worked-Informationen aus dem Log
|
||||
|
||||
Die [Log-Synchronisation](de-Log-Synchronisation) übernimmt neue QSOs aus dem Logprogramm. Welche Informationen dabei zur Verfügung stehen, hängt von der verwendeten Schnittstelle ab:
|
||||
|
||||
- Der dateibasierte Simplelogfile-Interpreter erkennt nur das Rufzeichen. Er kann deshalb lediglich den globalen Worked-Status setzen.
|
||||
- Die QSO-UDP-Schnittstellen und der Win-Test-Netzwerk-Listener können zusätzlich das Band übernehmen.
|
||||
- Enthält das Logpaket einen gültigen Locator, speichert KST4Contest außerdem das gearbeitete vierstellige Großfeld für dieses Band.
|
||||
|
||||
Fehlt eine Information im Logpaket, wird sie nicht geraten. Ein QSO ohne Locator erzeugt deshalb keinen Großfeld-Eintrag; ein Simplelogfile-Treffer erzeugt keine bandbezogene Worked-Markierung.
|
||||
|
||||
### Bedeutung der Bandspalten
|
||||
|
||||
Unter der gemeinsamen Spalte **worked** erscheinen nur die Bänder, die unter **Station → my station uses …** aktiviert wurden. Die Zellen verwenden bewusst kurze Kennzeichen:
|
||||
|
||||
| Anzeige | Bedeutung |
|
||||
|---|---|
|
||||
| `X` | Das Rufzeichen wurde auf diesem Band gearbeitet. |
|
||||
| `a` | Die Station bietet dieses Band an, das Band ist noch nicht gearbeitet und das Rufzeichen wurde bisher auf keinem Band gearbeitet. |
|
||||
| `B+` | Die Station bietet dieses Band an und das Band ist noch nicht gearbeitet. Das Rufzeichen wurde bereits auf einem anderen Band gearbeitet. Ist die getrennte `a`-Anzeige deaktiviert, wird auch ein vollständig neues Rufzeichen als `B+` dargestellt. |
|
||||
| `o` | Das vierstellige Großfeld der Station wurde auf diesem Band bereits gearbeitet – unabhängig vom Rufzeichen. |
|
||||
| leer | Für dieses Band liegt keine passende Information vor. Das ist nicht gleichbedeutend mit „nicht QRV“. |
|
||||
|
||||
Das `o` ist eine unabhängige Zusatzinformation und kann deshalb mit den anderen Kennzeichen kombiniert werden. Möglich sind beispielsweise `Xo`, `ao` oder `B+o`. Ein einzelnes `o` bedeutet: Das Großfeld wurde auf diesem Band bereits gearbeitet, für das angezeigte Rufzeichen liegt aber weder eine Worked-Markierung noch eine aktuelle Bandmöglichkeit vor.
|
||||
|
||||

|
||||
|
||||
### Wie entsteht eine Bandmöglichkeit?
|
||||
|
||||
KST4Contest zeigt `a` oder `B+` nur an, wenn sich eine noch offene gemeinsame Bandmöglichkeit herleiten lässt. Dafür werden folgende Informationen zusammengeführt:
|
||||
|
||||
1. die in den Stationseinstellungen aktivierten eigenen Bänder,
|
||||
2. höchstens 30 Minuten alte QRG-Erkennungen der Gegenstation,
|
||||
3. eindeutige Bandangaben im Namensfeld der Gegenstation,
|
||||
4. die pro Band gespeicherten Worked-Markierungen und
|
||||
5. manuell gesetzte NOT-QRV-Markierungen.
|
||||
|
||||
Aktive Chat-Einträge mit demselben normalisierten Rufzeichen werden gemeinsam ausgewertet. Das ist insbesondere bei mehreren Chat-Kategorien oder unterschiedlichen sichtbaren Rufzeichenvarianten wichtig. Eine eindeutige Bandangabe im Namensfeld bleibt dabei so lange nutzbar, wie der betreffende Chat-Eintrag aktiv ist; eine aus einer Nachricht erkannte QRG läuft nach 30 Minuten aus.
|
||||
|
||||
Anschließend werden nur die Bänder berücksichtigt, die an der eigenen Station aktiviert, für die Gegenstation bekannt und noch nicht gearbeitet sind. Ein manuelles NOT-QRV-Tag übersteuert die automatisch erkannten Hinweise. Die Chat-Kategorie allein reicht dagegen nicht als Nachweis, dass eine einzelne Station auf einem bestimmten Band QRV ist.
|
||||
|
||||
Die globale Worked-Markierung entscheidet nicht darüber, ob eine Bandmöglichkeit besteht. Sie unterscheidet in der Darstellung lediglich zwischen `a` und `B+`. Die eigentliche Bandprüfung arbeitet mit den bandbezogenen Worked-Informationen.
|
||||
|
||||
### Bedeutung von `wkdany`
|
||||
|
||||
Die Unterspalte **wkdany** fasst den globalen Rufzeichen- und Großfeldstatus zusammen:
|
||||
|
||||
| Anzeige | Bedeutung |
|
||||
|---|---|
|
||||
| leer | Weder das Rufzeichen noch das vierstellige Großfeld wurden gearbeitet. |
|
||||
| `x` | Das Rufzeichen wurde auf mindestens einem Band gearbeitet. |
|
||||
| `o` | Das vierstellige Großfeld wurde auf mindestens einem Band gearbeitet. |
|
||||
| `xo` | Rufzeichen und Großfeld wurden bereits gearbeitet. |
|
||||
|
||||
`wkdany` ist eine bandunabhängige Übersicht. Das kleine `x` darf daher nicht mit dem großen `X` in einer Bandspalte verwechselt werden. Der globale Status wird für die Anzeige und den globalen **wkd**-Filter verwendet, nicht als Ersatz für bandbezogene Worked-Informationen.
|
||||
|
||||
### NOT-QRV-Markierungen
|
||||
|
||||
Teilt eine Station mit, dass sie auf einem bestimmten Band nicht QRV ist, kann dies im **Further Info**-Bereich der ausgewählten Station markiert werden:
|
||||
|
||||
1. Station in der Benutzerliste auswählen.
|
||||
2. Im Bereich **Not QRV** das betreffende Band aktivieren.
|
||||
3. **tag not qrv all** nur verwenden, wenn die Station auf keinem der unterstützten Bänder angefragt werden soll.
|
||||
|
||||
Angezeigt werden die einzelnen NOT-QRV-Schalter der Bänder, die für die eigene Station aktiviert sind. **tag not qrv all** setzt dagegen alle unterstützten Bänder, auch wenn einzelne davon momentan nicht in der Benutzeroberfläche eingeblendet sind. Die Markierung wird bandbezogen unter dem normalisierten Rufzeichen gespeichert und auf dessen aktive Chat-Varianten übertragen.
|
||||
|
||||

|
||||
|
||||
NOT-QRV ist eine manuelle Korrektur und hat deshalb Vorrang vor automatisch erkannten QRGs und Bandangaben im Namensfeld. Das betreffende Band wird nicht mehr als `a` oder `B+` angeboten, vom **New bands**-Filter nicht als Gelegenheit gewertet und von den zugehörigen Bandfiltern ausgeblendet.
|
||||
|
||||
Im Klartext: Ein erkannter Hinweis bedeutet „wahrscheinlich auf diesem Band aktiv“. Ein manuelles NOT-QRV-Tag bedeutet „für unsere weitere Auswahl nicht auf diesem Band anfragen“. Diese Entscheidung soll nicht durch die nächste erkannte Zahl wieder aufgehoben werden.
|
||||
|
||||
### Speicherung und Lebensdauer
|
||||
|
||||
Worked-, NOT-QRV- und Großfeldinformationen werden in der internen SQLite-Datenbank gespeichert und beim nächsten Start wieder geladen. Die Einträge laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist deshalb normalerweise nicht erforderlich.
|
||||
|
||||
Ein manueller Reset unter **Workedstn database** entfernt sämtliche Worked-Markierungen, NOT-QRV-Tags und gespeicherten Worked-Großfelder. Die bekannten Rufzeichenzeilen bleiben dabei in der Datenbank erhalten. Einzelheiten: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
|
||||
Gearbeitete Stationen werden in der Benutzerliste visuell markiert – pro Band. Grundlage ist die [Log-Synchronisation](de-Log-Synchronisation) via UDP oder Simplelogfile.
|
||||
|
||||
Vor jedem Contest die Datenbank zurücksetzen: [Konfiguration – Worked Station Database Settings](Konfiguration#worked-station-database-settings).
|
||||
|
||||
---
|
||||
|
||||
@@ -235,19 +83,9 @@ Stationen jenseits einer maximalen Entfernung ausblenden. Schaltfläche **„Sho
|
||||
|
||||
---
|
||||
|
||||
## Filter für Worked-Status, neue Bänder und neue Großfelder
|
||||
## Worked- und NOT-QRV-Filter
|
||||
|
||||
Die Filter oberhalb der Benutzerliste greifen auf dieselben Informationen zurück wie die Worked-Spalten:
|
||||
|
||||
- **wkd** blendet Rufzeichen aus, die auf mindestens einem Band gearbeitet wurden.
|
||||
- Die einzelnen Band-Schaltflächen blenden Stationen aus, wenn das Rufzeichen auf diesem Band bereits gearbeitet oder für dieses Band manuell als NOT QRV markiert wurde.
|
||||
- **New bands** zeigt nur Stationen, für die mindestens ein eigenes aktiviertes, noch nicht gearbeitetes Band bekannt ist. Berücksichtigt werden aktuelle QRG-Erkennungen und Bandangaben im Namensfeld; NOT-QRV hat Vorrang.
|
||||
- **Only new grids** zeigt nur Stationen, deren vierstelliges Großfeld auf noch keinem Band gearbeitet wurde. Stationen ohne auswertbaren Locator erfüllen den Filter nicht.
|
||||
- **Grid color** verändert die Liste nicht. Ist die Funktion aktiv, wird das QRA-Feld eines bereits gearbeiteten Großfelds dezent dunkler dargestellt. Neue Großfelder behalten die normale Tabellenfarbe.
|
||||
|
||||
Mehrere aktivierte Filter werden gemeinsam angewendet. Eine Station bleibt nur sichtbar, wenn sie alle gewählten Bedingungen erfüllt. Die Filter reagieren unmittelbar auf neue Logeinträge und geänderte NOT-QRV-Markierungen.
|
||||
|
||||
Bedienung und Aufbau der Filterleiste: [Benutzeroberfläche – Filter](de-Benutzeroberflaeche#filter).
|
||||
Toggle-Buttons (einer pro Band) zum Ausblenden bereits gearbeiteter Stationen und/oder NOT-QRV-markierter Stationen. Der Filter wirkt **sofort** ohne manuelles Neu-Aktivieren (ab v1.22 live).
|
||||
|
||||
---
|
||||
|
||||
@@ -271,22 +109,6 @@ KST4Contest erkennt solche Nachrichten, die das eigene Rufzeichen enthalten, und
|
||||
|
||||
---
|
||||
|
||||
## Automatische Antworten auf Privatnachrichten (ab v1.25)
|
||||
|
||||
Nicht jede im ON4KST-Chat eingeloggte Station nimmt am gerade laufenden Contest teil. Trotzdem werden Sked-Anfragen während größerer Contests teilweise unkoordiniert und in großer Zahl an erreichbare Rufzeichen verteilt. Ohne automatische Antwort müssten diese Stationen immer wieder von Hand erklären, dass sie nicht mitfunken oder keine Skeds fahren.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Konfiguration, erkannte QRG-Anfragen und genaue Kategorienzuordnung: [Konfiguration – Messagehandling Settings](de-Konfiguration#messagehandling-settings-ab-v125).
|
||||
|
||||
---
|
||||
|
||||
## 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,114 +135,39 @@ Für ausgewählte Stationen in der Benutzerliste gibt es direkte Buttons, um das
|
||||
|
||||
---
|
||||
|
||||
## Skeds und Sked-Erinnerungen
|
||||
## Sked-Erinnerungen mit ALERT (ab v1.40)
|
||||
|
||||
> Verfügbar ab v1.40; Band-, Rufzeichen- und Win-Test-Behandlung erweitert in Nightly / v1.42.
|
||||
Für jeden Chatmember kann ein Sked-Erinnerungsdienst mit automatischen Nachrichten aktiviert werden. Konfigurierbare Intervallmuster:
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
KST4Contest behandelt deshalb drei voneinander unabhängige Aufgaben:
|
||||
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.
|
||||
|
||||
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.
|
||||
Aktivierung: FurtherInfo-Panel der entsprechenden Station.
|
||||
|
||||
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
|
||||
## QSO-Sniffer (ab v1.31)
|
||||
|
||||
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.
|
||||
|
||||
Die Nachricht bleibt gleichzeitig in ihrer ursprünglichen Tabelle erhalten und wird im PM-Fenster mit Absender und Empfänger als überwachte Kommunikation gekennzeichnet.
|
||||
Der QSO-Sniffer überwacht den Chat auf Nachrichten von einer konfigurierbaren Rufzeichen-Liste und leitet diese automatisch in das **PM-Fenster** weiter. So gehen keine relevanten Nachrichten im allgemeinen Chat-Rauschen unter.
|
||||
|
||||
Konfiguration: [Konfiguration – Sniffer-Einstellungen](de-Konfiguration#sniffer-einstellungen-ab-v131)
|
||||
|
||||
---
|
||||
|
||||
## Win-Test-Integration
|
||||
## Win-Test-Integration (ab v1.31, vollständig ab v1.40)
|
||||
|
||||
KST4Contest verwendet für Win-Test einen eigenen Listener für das native Win-Test-Netzwerkprotokoll. Darüber werden drei voneinander getrennte Funktionen bereitgestellt:
|
||||
KST4Contest unterstützt [Win-Test](https://www.win-test.com/) vollständig als Logprogramm:
|
||||
|
||||
- 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)
|
||||
- **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)
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator-Interface (ab v1.31, vollständig ab v1.40)
|
||||
|
||||
@@ -430,478 +177,61 @@ Konfiguration: [Konfiguration – PSTRotator-Einstellungen](de-Konfiguration#pst
|
||||
|
||||
---
|
||||
|
||||
## Band-Upgrade-Hinweis nach einem Logeintrag
|
||||
## Band-Alert bei neuen QSOs (ab v1.40)
|
||||
|
||||
Meldet UCXLog oder Win-Test einen neuen Logeintrag mit Bandinformation, prüft KST4Contest, ob die gearbeitete Station noch ein weiteres gemeinsames Band anbietet.
|
||||
|
||||
Die Herleitung verwendet dieselben Regeln wie `a`, `B+` und der Filter **New bands**: aktivierte eigene Bänder, aktuelle QRG-Erkennungen, Bandangaben im Namensfeld, bandbezogene Worked-Markierungen und NOT-QRV-Tags. Ausgewertet werden die aktiven Chat-Varianten desselben normalisierten Rufzeichens.
|
||||
|
||||
Bleibt mindestens ein gemeinsames, noch nicht gearbeitetes Band übrig, erscheint für ungefähr zwölf Sekunden ein blinkender Hinweis mit Rufzeichen und den betreffenden Bändern, beispielsweise `BAND+ DL0ABC 432, 1296`. Der Tooltip zeigt zusätzlich die bei der Entscheidung berücksichtigten aktivierten, gearbeiteten und als NOT QRV markierten Bänder. Ist die allgemeine Soundausgabe eingeschaltet, wird außerdem ein kurzer Hinweiston abgespielt.
|
||||
|
||||
Der einfache Simplelogfile-Interpreter kann diesen Hinweis nicht zuverlässig auslösen, weil er keine Bandinformation für das gerade geloggte QSO liefert.
|
||||
|
||||
Konfiguration: [Band-Upgrade-Hinweis nach einem Logeintrag](de-Konfiguration#band-upgrade-hinweis-nach-einem-logeintrag).
|
||||
|
||||
Worked-, NOT-QRV- und Großfelddaten laufen nach drei Tagen automatisch ab. Einzelheiten und manueller Reset: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
|
||||
|
||||
---
|
||||
## Prioritätsscore und Prioritätsliste (ab v1.40)
|
||||
|
||||
### Warum wird überhaupt ein Score benötigt?
|
||||
|
||||
Eine klassische Chat-Benutzerliste zeigt zunächst nur, welche Stationen gerade eingeloggt sind. Im Contestbetrieb reicht diese Information nicht aus. Der Operator muss zusätzlich abschätzen, welche Station noch nicht gearbeitet wurde, auf welchem Band ein QSO möglich sein könnte, wohin die Antenne zeigt, ob ein passendes Flugzeug verfügbar ist und ob ein vereinbarter Sked unmittelbar bevorsteht.
|
||||
|
||||
Bei einer kurzen Liste lässt sich das noch im Kopf erledigen. Mit zunehmender Contestdauer, mehreren Bändern und zwei gleichzeitig verwendeten Chat-Kategorien wird daraus jedoch eine ständig wiederholte Entscheidung.
|
||||
|
||||
KST4Contest führt die bereits vorhandenen Informationen deshalb in einem Prioritätsscore zusammen. Der Score beantwortet nicht die Frage, ob ein QSO sicher möglich ist. Er hilft bei der praktisch wichtigeren Frage:
|
||||
|
||||
> Welche der aktuell sichtbaren Stationen sollte ich mir als Nächstes ansehen?
|
||||
|
||||
### Wann wird eine Station ausgeschlossen?
|
||||
|
||||
Vor der eigentlichen Gewichtung prüft KST4Contest, ob überhaupt eine bekannte Bandmöglichkeit besteht. Dafür werden alle aktiven Chat-Einträge desselben normalisierten Basisrufzeichens gemeinsam ausgewertet.
|
||||
|
||||
Berücksichtigt werden:
|
||||
|
||||
1. die in den Stationseinstellungen aktivierten eigenen Bänder,
|
||||
2. höchstens 30 Minuten alte QRG-Erkennungen der Gegenstation,
|
||||
3. eindeutige Bandangaben im Namensfeld ihrer aktiven Chat-Einträge,
|
||||
4. die pro Band gespeicherten Worked-Markierungen und
|
||||
5. manuell gesetzte NOT-QRV-Tags.
|
||||
|
||||
NOT QRV hat dabei Vorrang vor automatisch erkannten Frequenzen oder Bandangaben.
|
||||
|
||||
Sind Bänder der Gegenstation bekannt, aber keines davon ist lokal aktiviert und noch verfügbar, erhält die Station einen Score von `0`. Dasselbe gilt, wenn alle gemeinsam möglichen Bänder bereits gearbeitet wurden.
|
||||
|
||||
Fehlen dagegen sämtliche Bandinformationen, wird die Station nicht allein deshalb ausgeschlossen. Eine unbekannte Bandmöglichkeit ist nicht dasselbe wie eine nachweislich unmögliche Bandmöglichkeit. Erst wenn alle eigenen aktivierten Bänder für die Station manuell als NOT QRV markiert wurden, ist auch in diesem Fall keine aktuelle Contestmöglichkeit mehr vorhanden.
|
||||
|
||||
Stationen mit einem Score von `0` bleiben in der Benutzerliste sichtbar, erscheinen aber nicht in der Prioritätsliste.
|
||||
|
||||
### Welche Informationen erhöhen oder verringern den Score?
|
||||
|
||||
Der Score entsteht aus mehreren voneinander unabhängigen Hinweisen. Ein einzelnes Kriterium entscheidet daher normalerweise nicht über den endgültigen Listenplatz.
|
||||
|
||||
| Faktor | Wirkung auf die Priorisierung |
|
||||
|---|---|
|
||||
| Worked-Status | Ein noch auf keinem unterstützten Band gearbeitetes Rufzeichen erhält eine höhere Ausgangspriorität. Bereits gearbeitete Stationen werden niedriger bewertet, bleiben bei offenen Bandmöglichkeiten aber Kandidaten. |
|
||||
| Verfügbare Bänder | Mehrere gemeinsam nutzbare und noch nicht gearbeitete Bänder erhöhen die Priorität. Optional kann für Band-Upgrades ein zusätzlicher Boost aktiviert werden. |
|
||||
| Entfernung | Entfernungen unter 200 km werden niedriger gewichtet. Der Bereich zwischen 200 km und dem konfigurierten maximalen QRB wird bevorzugt. Stationen jenseits des maximalen QRB werden deutlich herabgestuft. Fehlt der QRB, entfällt dieser Faktor. |
|
||||
| Antennenrichtung | Liegt der QTF zur Gegenstation innerhalb der Hälfte des konfigurierten Antennen-Öffnungswinkels um den aktuellen eigenen QTF, steigt der Score. Je näher die Richtungen zusammenliegen, desto stärker wirkt der Hinweis. |
|
||||
| AirScout | Mindestens ein aktuell erreichbares Flugzeug erhöht den Score. Eine erwartete AP-Gelegenheit in null, einer oder zwei Minuten wird zusätzlich zeitlich gewichtet. |
|
||||
| Aktuelle Chat-Aktivität | Eine Nachricht innerhalb der letzten Minute wirkt stärker als eine Nachricht innerhalb der letzten drei Minuten. Mehrere eingehende Zeilen innerhalb des Aktivitätsfensters erhöhen den Score zusätzlich. |
|
||||
| Positive Signale | Erkannte Angaben wie `QRV`, `READY`, `RGR`, `OK`, `TNX` oder vergleichbare konfigurierte Textmuster werden für einige Minuten als positiver Hinweis berücksichtigt. |
|
||||
| Antwortverhalten | Reagiert eine Station nach einer eigenen `/cq`-Nachricht schnell mit einer weiteren sichtbaren Chat-Zeile, wirkt sich die gemittelte Reaktionszeit positiv aus. Bleibt eine solche Zeile aus, entsteht nach dem konfigurierten Timeout eine negative Bewertung. |
|
||||
| Skeds | Ein eingetragener Sked erhöht die Priorität zunächst leicht. Innerhalb der letzten 15 Minuten vor dem Termin steigt der Einfluss kontinuierlich an. Zwischen drei Minuten vor und einer Minute nach dem Termin erhält der Sked eine sehr hohe Priorität. |
|
||||
| Fehlgeschlagener Versuch | **Sked fail** reduziert den Score der Station stark, bis die Markierung mit **Reset fail** zurückgesetzt oder KST4Contest neu gestartet wird. |
|
||||
|
||||
Das standardmäßige Aktivitätsfenster für die Anzahl eingehender Nachrichten beträgt 180 Sekunden. Eine aktuelle Nachricht innerhalb der letzten 60 Sekunden wird noch einmal gesondert bewertet. Der standardmäßige No-Reply-Timeout beträgt 13 Minuten.
|
||||
|
||||
Beim Antwortverhalten kann KST4Contest nicht sicher feststellen, ob eine nachfolgende öffentliche oder private Nachricht tatsächlich die Antwort auf die eigene Anfrage war. Jede anschließend empfangene Zeile derselben Station beendet deshalb den laufenden Antwortzeitversuch. Der Wert ist eine praktische Näherung und keine statistisch belastbare Antwortquote.
|
||||
|
||||
### Was bedeutet ein eingetragener Sked?
|
||||
|
||||
Ein Sked ist eine zeitlich vereinbarte Arbeitsaufgabe. Deshalb übersteuert ein unmittelbar bevorstehender Sked die meisten normalen Aktivitäts- und Entfernungshinweise. Ohne diese Gewichtung könnte eine gerade sehr aktive Station einen vereinbarten Termin aus der Prioritätsliste verdrängen.
|
||||
|
||||
Der starke Sked-Boost ist absichtlich auf den Zeitraum von drei Minuten vor bis eine Minute nach dem eingetragenen Termin begrenzt. Ein weiter in der Zukunft liegender Sked bleibt sichtbar, soll den laufenden Betrieb aber noch nicht dominieren.
|
||||
|
||||
Die Bewertung wird für das normalisierte Basisrufzeichen vorgenommen. Ein Sked für eine aktive Variante wie `9A0BB-23` beeinflusst daher den gemeinsamen Score der zu `9A0BB` gehörenden Chat-Einträge.
|
||||
|
||||
### Wie werden mehrere SSIDs und Chat-Kategorien behandelt?
|
||||
|
||||
Aktive Rufzeichen wie `9A0BB-2`, `9A0BB-70`, `9A0BB-23` und `9A0BB-13` bleiben getrennte Chatmember. Dadurch können Nachrichten weiterhin an das vollständige Rufzeichen und die richtige Chat-Kategorie adressiert werden.
|
||||
|
||||
Worked-, Band-, NOT-QRV- und Score-Informationen beziehen sich dagegen auf das gemeinsame Basisrufzeichen `9A0BB`. Der Score wird deshalb einmal berechnet und auf alle aktiven Varianten übertragen. Die Benutzerliste kann mehrere getrennte Zeilen mit demselben Score enthalten; in der Prioritätsliste erscheint das Basisrufzeichen nur einmal.
|
||||
|
||||
Als konkretes Nachrichtenziel verwendet KST4Contest den zuletzt passenden aktiven Login in der zuletzt verwendeten Chat-Kategorie. Beim Anklicken eines Kandidaten wird anschließend das vollständige Rufzeichen einschließlich Suffix und Kategorie ausgewählt.
|
||||
|
||||
### Aktualisierung und Anzeige
|
||||
|
||||
Änderungen durch neue Nachrichten, AirScout-Daten, Skeds, Worked-Informationen oder manuelle NOT-QRV- und Sked-fail-Markierungen fordern unmittelbar eine Neuberechnung an. Zusätzlich wird der Score regelmäßig im Hintergrund aktualisiert, weil Aktivitäts-, AP- und Sked-Informationen auch ohne neues Ereignis altern.
|
||||
|
||||
Eine kurze Verzögerung von einigen Sekunden zwischen einem Ereignis und der sichtbaren neuen Reihenfolge ist daher normal.
|
||||
|
||||
Die Benutzeroberfläche zeigt den Score an drei Stellen:
|
||||
|
||||
- als numerisch sortierbare Spalte **Score** in der Benutzerliste,
|
||||
- für die ausgewählte Station im Bereich **Further Info** und
|
||||
- als kompakte Liste der beiden derzeit höchstbewerteten Kandidaten mit einem zusätzlichen Fenster für bis zu 15 Kandidaten.
|
||||
|
||||
Bedienung: [Prioritätsliste in der Benutzeroberfläche](de-Benutzeroberflaeche#prioritätsliste).
|
||||
|
||||
### Was sagt der Score nicht aus?
|
||||
|
||||
Der Zahlenwert ist weder eine Erfolgswahrscheinlichkeit noch eine Signalprognose. Ein doppelt so hoher Score bedeutet nicht, dass ein QSO doppelt so wahrscheinlich ist.
|
||||
|
||||
Die Berechnung kennt unter anderem nicht:
|
||||
|
||||
- die tatsächliche Antennenrichtung der Gegenstation,
|
||||
- deren momentane Betriebssituation,
|
||||
- lokale Störungen,
|
||||
- kurzfristige Ausbreitungsänderungen,
|
||||
- Geländeabschattungen außerhalb der jeweils angebundenen Funktionen oder
|
||||
- die Frage, ob eine im Chat aktive Station tatsächlich gerade am Funkgerät sitzt.
|
||||
|
||||
Auch bekannte Eingabedaten können veraltet oder missverständlich sein. Eine erkannte Frequenz belegt beispielsweise nur, dass diese QRG kürzlich im Zusammenhang mit der Station aufgetreten ist.
|
||||
|
||||
Im Klartext: Der Score ersetzt nicht die Entscheidung des Operators. Er sorgt dafür, dass die dafür bereits vorhandenen Informationen nicht bei jedem Kandidaten erneut im Kopf zusammengesucht werden müssen.
|
||||
|
||||
Zugehörige Einstellungen:
|
||||
|
||||
- [Aktivierte Bänder](de-Konfiguration#aktivierte-bänder)
|
||||
- [Antennen-Öffnungswinkel](de-Konfiguration#antennen-öffnungswinkel-antenna-beamwidth)
|
||||
- [Standard-Maximum-QRB](de-Konfiguration#standard-maximum-qrb)
|
||||
- [AirScout-Einstellungen](de-Konfiguration#airscout-einstellungen)
|
||||
- [Band-Upgrade-Hinweis und Priority Boost](de-Konfiguration#band-upgrade-hinweis-nach-einem-logeintrag)
|
||||
Wenn eine Station geloggt wird, prüft KST4Contest automatisch, ob diese Station im Chat weitere aktive Bänder angezeigt hat, auf denen man selbst ebenfalls QRV ist. Falls ja, erscheint ein **Hinweis-Alert**, damit keine Multi-Band-Möglichkeit übersehen wird.
|
||||
|
||||
---
|
||||
|
||||
## AP- und Sked-Timeline
|
||||
## Worked-Tag-Lebensdauer (ab v1.40)
|
||||
|
||||
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:
|
||||
Gearbeitete Stationen werden nach **3 Tagen** automatisch aus der Datenbank entfernt. Ein manuelles Zurücksetzen der Worked-Datenbank vor jedem Contest ist damit nicht mehr zwingend notwendig – die Datenbank hält sich selbst aktuell.
|
||||
|
||||
- Wann entsteht voraussichtlich eine interessante AP-Gelegenheit?
|
||||
- Welcher bereits vereinbarte Sked nähert sich unabhängig davon?
|
||||
---
|
||||
|
||||
Weiter in der Zukunft liegende Ereignisse erscheinen rechts. Mit ablaufender Zeit wandern sie nach links in Richtung des aktuellen Zeitpunkts.
|
||||
## Chatmember Score-System / Prioritätsliste (ab v1.40)
|
||||
|
||||

|
||||
KST4Contest berechnet automatisch eine **Prioritätsbewertung** für jeden aktiven Chatmember. Der Score setzt sich zusammen aus:
|
||||
|
||||
### AP-Kandidaten
|
||||
- Antennenrichtung der Gegenstation (zeigt sie auf mich?)
|
||||
- QRB (Entfernung)
|
||||
- Aktivitätszeit und Nachrichtenanzahl
|
||||
- Aktive Bänder und Frequenzen
|
||||
- AP-Verfügbarkeit (AirScout)
|
||||
- Sked-Richtung
|
||||
- Sked-Erfolgsrate und Skedfail-Markierungen
|
||||
|
||||
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 Top-Kandidaten werden in einer eigenen Prioritätsliste hervorgehoben und helfen, im Contest-Stress die wichtigsten Stationen nicht zu übersehen.
|
||||
|
||||
Die Farbe des AP-Symbols kennzeichnet das Reflexionspotenzial:
|
||||
Stationen, bei denen ein Sked gescheitert ist, können über den **Skedfail-Button** im FurtherInfo-Panel markiert werden – das senkt ihren Score vorübergehend.
|
||||
|
||||
| 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.
|
||||
## AP-Timeline (ab v1.40)
|
||||
|
||||
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.
|
||||
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:
|
||||
|
||||
### Skeds
|
||||
- Bevorzugt werden APs mit dem **höchsten Reflexionspotenzial** (nicht unbedingt die schnellste Ankunft).
|
||||
- Stationen, auf die die eigene Antenne nicht zeigt, werden **transparent** dargestellt.
|
||||
|
||||
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.
|
||||
So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und über welche Flugzeuge erreichbar sein werden.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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).
|
||||
Automatische CQ-Meldungen im öffentlichen Kanal in konfigurierbarem Intervall. Empfohlene Verwendung mit der Variable `MYQRG` für aktuelle Frequenzangabe. Details: [Konfiguration – Beacon Settings](Konfiguration#beacon-settings-automatischer-beacon).
|
||||
|
||||
---
|
||||
|
||||
## Simplelogfile
|
||||
|
||||
Details: [Log-Synchronisation](de-Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||
Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||
|
||||
---
|
||||
|
||||
## Globale Nachrichtenansichten
|
||||
## Cluster & QSO der anderen
|
||||
|
||||
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.
|
||||
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
|
||||
|
||||
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 und Streckenanalyse (ab v1.41)
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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)
|
||||
|
||||
---
|
||||
|
||||
## Begrenzte Nachrichtenspeicher (ab v1.41)
|
||||
|
||||
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 Größe des Hauptfensters (ab v1.41)
|
||||
|
||||
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.
|
||||
|
||||
KST4Contest prüft die gespeicherte Größe deshalb beim Programmstart gegen den nutzbaren Bereich des primären Bildschirms.
|
||||
|
||||
### 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.
|
||||
Dieses Fenster kann miniaturisiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.
|
||||
|
||||
@@ -1,64 +1,10 @@
|
||||
# KST4Contest – Handbuch
|
||||
# KST4Contest – Wiki
|
||||
|
||||
> [English version](en-Home) | Du liest gerade die deutsche Version
|
||||
> 🇬🇧 [English version](en-Home) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
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 und die Anbindung an weitere Programme in einer gemeinsamen Arbeitsoberfläche.
|
||||
**KST4Contest** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), der speziell für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern (144 MHz und aufwärts) entwickelt wurde.
|
||||
|
||||
Entwickelt wird KST4Contest von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M und (seit Mai 2025) **DN9APW (Philipp Wagner)**. Der Quellcode ist öffentlich auf [GitHub](https://github.com/praktimarc/kst4contest) verfügbar.
|
||||
|
||||
---
|
||||
|
||||
## Wozu braucht man einen eigenen ON4KST-Client?
|
||||
|
||||
Der ON4KST-Chat liefert während eines Contests eine große Menge an Informationen: aktive Stationen, Locator, Frequenzen, Sked-Anfragen und Hinweise auf aktuelle Aktivität. Das eigentliche Problem besteht nicht darin, diese Daten zu sehen. Man muss daraus rechtzeitig die nächste sinnvolle Verbindung ableiten.
|
||||
|
||||
KST4Contest wertet die verfügbaren Informationen aus, ordnet sie und stellt sie in einem contestgerechten Arbeitsablauf dar. Dabei werden unter anderem Antennenrichtung, Entfernung, bekannte Bänder und Frequenzen, bereits gearbeitete Stationen, Chat-Aktivität und Aircraft-Scatter-Zeiten berücksichtigt.
|
||||
|
||||
Das Programm entscheidet jedoch nicht, welches QSO tatsächlich möglich ist. Der Prioritätsscore, die AP-Timeline und die verschiedenen Hervorhebungen sind Entscheidungshilfen. Die endgültige Beurteilung bleibt beim Operator – schon deshalb, weil auch ein recht überzeugender Computerbildschirm noch keine Funkverbindung herstellt.
|
||||
|
||||
---
|
||||
|
||||
## Was KST4Contest im Contest unterstützt
|
||||
|
||||
- **Chat beobachten und einordnen:** KST4Contest kann zwei ON4KST-Chat-Kategorien gleichzeitig darstellen. Nachrichten, Frequenzangaben und bekannte Bandaktivitäten werden den jeweiligen Stationen zugeordnet.
|
||||
|
||||
- **Relevante Stationen herausfiltern:** Richtungs-, Entfernungs-, Worked- und NOT-QRV-Filter reduzieren die Benutzerliste auf die Stationen, die für den aktuellen Betriebszustand tatsächlich interessant sind.
|
||||
|
||||
- **Kandidaten priorisieren:** Das Score-System bewertet aktive Chatmember anhand mehrerer bekannter Kriterien. Die derzeit relevantesten Kandidaten erscheinen zusätzlich in einer eigenen Prioritätsliste.
|
||||
|
||||
- **Skeds vorbereiten und im Blick behalten:** Sked-Erinnerungen, automatische Vorwarnungen und die AP-Timeline helfen dabei, vereinbarte Verbindungen nicht zwischen Chat, Log und laufendem CQ-Betrieb zu verlieren.
|
||||
|
||||
- **Aircraft Scatter einbeziehen:** Über die AirScout-Schnittstelle werden geeignete Flugzeuge und erwartete Reflexionszeiten in die Stationsbewertung und Zeitplanung übernommen.
|
||||
|
||||
- **Log und Funkstation anbinden:** KST4Contest synchronisiert gearbeitete Stationen und Frequenzinformationen mit unterstützten Logprogrammen. Zusätzlich stehen Schnittstellen zu Win-Test, PSTRotator und ein integrierter DX-Cluster-Server zur Verfügung.
|
||||
|
||||
- **Stationen und Funkwege darstellen:** Die Stationskarte zeigt aktive Chatmember, Locator-Felder, Antennenrichtungen und den Weg zur ausgewählten Station. Für ausgewählte Verbindungen kann außerdem ein Geländeprofil berechnet werden.
|
||||
|
||||
Die Funktionen greifen ineinander. Eine erkannte Frequenz kann beispielsweise einem aktiven Band zugeordnet werden, der Worked-Status stammt aus dem Log, Aircraft-Scatter-Daten ergänzen die zeitliche Bewertung und der daraus berechnete Score beeinflusst die Prioritätsliste. Fehlende oder veraltete Eingangsdaten können deshalb auch das Ergebnis beeinflussen.
|
||||
|
||||
---
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Für die Anmeldung ist ein registrierter ON4KST-Account erforderlich. Registrierung und Login sind über die [offizielle ON4KST-Anmeldeseite](https://www.on4kst.org/chat/login.php) erreichbar.
|
||||
|
||||
Die offizielle Sprache im ON4KST-Chat ist Englisch. Das gilt auch für Nachrichten an Stationen aus dem eigenen Land. Übliche Amateurfunk-Abkürzungen wie `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` oder `73` sind dabei normal und meistens erheblich schneller als ausformulierte Prosa.
|
||||
|
||||
Download, unterstützte Betriebssysteme und Installationswege sind im Kapitel [Installation](de-Installation) beschrieben.
|
||||
|
||||
---
|
||||
|
||||
## Versionsstand dieses Handbuchs
|
||||
|
||||
Dieses Handbuch unterscheidet zwischen der veröffentlichten Stable-Version und dem aktuellen Entwicklungsstand.
|
||||
|
||||
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.
|
||||
|
||||
- [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 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.
|
||||
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
|
||||
|
||||
---
|
||||
|
||||
@@ -66,63 +12,40 @@ Für einen Contest ist grundsätzlich die Stable-Version zu empfehlen. Nightly-B
|
||||
|
||||
| Seite | Inhalt |
|
||||
|---|---|
|
||||
| [Installation](de-Installation) | ON4KST-Account, Download, Installation und Updates |
|
||||
| [Konfiguration](de-Konfiguration) | Login, Station, Bänder, Benutzeroberfläche und externe Schnittstellen |
|
||||
| [Log-Synchronisation](de-Log-Synchronisation) | Simplelogfile, UCXLog, N1MM+, QARTest, DXLog.net und Win-Test |
|
||||
| [AirScout-Integration](de-AirScout-Integration) | Verbindung zu AirScout und Auswertung von Aircraft-Scatter-Zeiten |
|
||||
| [DX-Cluster-Server](de-DX-Cluster-Server) | Übergabe erkannter Möglichkeiten an das Logprogramm |
|
||||
| [Funktionen](de-Funktionen) | Arbeitsweise, Herleitung und Grenzen der einzelnen Funktionen |
|
||||
| [Makros und Variablen](de-Makros-und-Variablen) | Wiederverwendbare Texte, Shortcuts und automatisch ersetzte Werte |
|
||||
| [Benutzeroberfläche](de-Benutzeroberflaeche) | Aufbau der Oberfläche und Bedienung im Contest |
|
||||
| [Changelog](de-Changelog) | Releases, Nightly-Änderungen und behobene Fehler |
|
||||
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
|
||||
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
|
||||
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
|
||||
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
|
||||
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
|
||||
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
|
||||
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
|
||||
| [Changelog](de-Changelog) | Versionsgeschichte |
|
||||
|
||||
---
|
||||
|
||||
## Kontakt und Support
|
||||
## Was ist KST4Contest?
|
||||
|
||||
- **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
|
||||
Bitte nur für Themen verwenden, die KST4Contest betreffen.
|
||||
Der ON4KST-Chat ist der De-facto-Standard für Skeds auf den 144-MHz-und-höher-Bändern. KST4Contest erweitert die Chat-Nutzung um contest-spezifische Funktionen:
|
||||
|
||||
### Einen Fehler melden
|
||||
- **Worked-Markierung**: Bereits gearbeitete Stationen werden farblich markiert, direkt aus dem Logprogramm via UDP synchronisiert.
|
||||
- **Sked-Richtungs-Erkennung**: Wenn eine Station eine andere aus deiner Richtung anruft, wird sie grün und fett hervorgehoben.
|
||||
- **QRG-Erkennung**: KST4Contest liest Frequenzen automatisch aus dem Chat-Verkehr und zeigt sie in der Benutzerliste an.
|
||||
- **AirScout-Interface**: Anzeige reflektierbarer Flugzeuge direkt in der Benutzerliste.
|
||||
- **Integrierter DX-Cluster-Server**: Spots werden direkt an das Logprogramm gesendet.
|
||||
- **Dark Mode** (ab v1.26): Schont die Augen in der Nacht.
|
||||
- **Multi-Channel-Login** (ab v1.26): Gleichzeitig in zwei Chat-Kategorien einloggen.
|
||||
|
||||
Ein Fehler lässt sich wesentlich schneller nachvollziehen, wenn die Meldung mindestens folgende Angaben enthält:
|
||||
---
|
||||
|
||||
1. verwendete KST4Contest-Version,
|
||||
2. Betriebssystem und Installationsart,
|
||||
3. genaue Schritte bis zum Fehler,
|
||||
4. erwartetes und tatsächlich beobachtetes Verhalten,
|
||||
5. gegebenenfalls einen Screenshot,
|
||||
6. die Fehler-Logdatei.
|
||||
## Kontakt & Support
|
||||
|
||||
Die Fehler-Logdatei wird hier gespeichert:
|
||||
|
||||
| Betriebssystem | Pfad |
|
||||
|---|---|
|
||||
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
|
||||
| Windows | `C:\Users\<Benutzername>\.praktiKST\kst4contest-errors.log` |
|
||||
|
||||
Vor dem Hochladen sollte die Datei kurz geprüft werden. Ein Fehlerprotokoll enthält überwiegend technische Informationen, kann abhängig vom Fehler aber beispielsweise lokale Dateipfade oder weitere Kontextdaten enthalten.
|
||||
|
||||
In Datei- und Verzeichnisnamen wird teilweise noch der technische Name `praktiKST` verwendet. Gemeint ist dasselbe Programm.
|
||||
- **E-Mail**: praktimarc+kst4contest@gmail.com *(nur für kst4contest-Themen)*
|
||||
- **GitHub**: https://github.com/praktimarc/kst4contest
|
||||
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest
|
||||
|
||||
---
|
||||
|
||||
## Danksagungen
|
||||
|
||||
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) 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 2025 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.
|
||||
Besonderer Dank gilt: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX-Cluster-Idee), DC9DJ (Konrad Neitzel, Projektstruktur), DO5ALF (Andreas, Webmaster funkerportal.de), PE0WGA (Franz van Velzen, Tester) sowie allen weiteren Testern und Ideengebern.
|
||||
|
||||
@@ -2,422 +2,140 @@
|
||||
|
||||
> 🇬🇧 [English version](en-Installation) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
KST4Contest wird für Windows, Linux und macOS als fertiges Programmpaket bereitgestellt. Für die offiziellen Release-Pakete muss Java nicht separat installiert werden: Die benötigte Java-Laufzeitumgebung ist bereits enthalten.
|
||||
|
||||
Benötigt werden damit im Wesentlichen ein unterstütztes Betriebssystem, eine Internetverbindung und ein ON4KST-Account. Das klingt zunächst überschaubar – und ist es normalerweise auch.
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Es wird eine Mindestauflösung von 1200px mal 720px empfohlen
|
||||
|
||||
### ON4KST-Account
|
||||
|
||||
KST4Contest ist ein eigenständiger Client für den ON4KST-Chat, ersetzt aber nicht den dazugehörigen Benutzeraccount.
|
||||
Um den Chat zu nutzen, ist ein registrierter Account beim ON4KST-Chat-Dienst erforderlich:
|
||||
|
||||
Falls noch kein Account vorhanden ist, kann er über die offizielle ON4KST-Seite angelegt werden:
|
||||
- Registrierung unter: http://www.on4kst.info/chat/register.php
|
||||
|
||||
- [ON4KST-Anmeldung und Registrierung](https://www.on4kst.org/chat/login.php)
|
||||
### Verhaltensregeln im Chat
|
||||
|
||||
ON4KST gibt Englisch als gemeinsame Sprache im Chat vor. Das gilt auch dann, wenn beide beteiligten Stationen dieselbe Muttersprache sprechen. Im Contestbetrieb werden häufig die üblichen Amateurfunk-Abkürzungen wie `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` oder `73` verwendet.
|
||||
Die offizielle Sprache im ON4KST-Chat ist **Englisch**. Auch bei Kommunikation mit Stationen aus dem eigenen Land bitte Englisch verwenden. Übliche HAM-Abkürzungen (agn, dir, pse, rrr, tnx, 73 …) sind gang und gäbe.
|
||||
|
||||
Die eigentliche Bedienung des Chats und das Senden persönlicher Nachrichten werden im Kapitel zur Benutzeroberfläche beschrieben. Für die Installation ist zunächst nur wichtig, dass der Account funktioniert.
|
||||
### Persönliche Nachrichten
|
||||
|
||||
### Bildschirmgröße
|
||||
Um eine Privatnachricht an eine andere Station zu senden, immer folgendes Format verwenden:
|
||||
|
||||
Eine nutzbare Bildschirmfläche von ungefähr **1200 × 720 Pixeln** oder mehr wird empfohlen.
|
||||
```
|
||||
/CQ RUFZEICHEN Nachrichtentext
|
||||
```
|
||||
|
||||
KST4Contest passt das Hauptfenster automatisch an die verfügbare Fläche des primären Bildschirms an. Das Programm kann deshalb auch auf kleineren Bildschirmen gestartet werden. Weniger Platz bleibt allerdings weniger Platz: In diesem Fall können nicht alle Tabellen, Filter und Zusatzinformationen gleichzeitig in sinnvoller Größe dargestellt werden.
|
||||
Beispiel: `/CQ DL5ASG pse sked 144.205?`
|
||||
|
||||
### Java
|
||||
|
||||
Für die fertigen Pakete aus den [GitHub Releases](https://github.com/praktimarc/kst4contest/releases/latest) ist keine separate Java-Installation erforderlich. Die benötigte Laufzeitumgebung wird zusammen mit KST4Contest ausgeliefert.
|
||||
|
||||
Eine Java-Entwicklungsumgebung wird nur benötigt, wenn KST4Contest selbst aus dem Quellcode gebaut werden soll. Bei den AUR-Paketen `kst4contest` und `kst4contest-git` werden Java 21 und Maven als Build-Abhängigkeiten durch den Paketmanager berücksichtigt.
|
||||
|
||||
---
|
||||
|
||||
## Stable, Beta oder Nightly?
|
||||
|
||||
KST4Contest wird in drei Entwicklungsständen bereitgestellt:
|
||||
|
||||
| Kanal | Geeignet für | Einordnung |
|
||||
|---|---|---|
|
||||
| **Stable** | Normaler Contestbetrieb | Veröffentlichte und für den regulären Einsatz vorgesehene Version |
|
||||
| **Beta** | Gezielte Tests vor einem Stable-Release | Vorabversion mit weitgehend festgelegtem Funktionsumfang |
|
||||
| **Nightly** | Frühe Tests neuer Funktionen | Aktueller Entwicklungsstand aus dem `main`-Branch |
|
||||
|
||||
Für den normalen Einsatz wird die **Stable-Version** empfohlen.
|
||||
|
||||
Beta- und Nightly-Versionen können Funktionen enthalten, die im Stable-Release noch nicht verfügbar sind. Sie können aber ebenso unfertige Bedienabläufe, geänderte Einstellungen oder neue Fehler enthalten. Das ist kein ungewöhnlicher Defekt im Veröffentlichungsverfahren, sondern der Zweck eines Entwicklungskanals.
|
||||
|
||||
Wenn in diesem Manual eine Funktion ausdrücklich mit **Nightly** gekennzeichnet ist, gehört sie noch nicht zwingend zum aktuellen Stable-Release.
|
||||
Bei starkem Chat-Verkehr (5–6 Nachrichten pro Sekunde im Contest) gehen öffentliche Nachrichten, die an ein bestimmtes Rufzeichen gerichtet sind, leicht unter. KST4Contest fängt solche Nachrichten aber auch dann ab, wenn sie fälschlicherweise öffentlich gepostet werden (siehe [Funktionen – PM-Abfang](Funktionen#catching-personal-messages)).
|
||||
|
||||
---
|
||||
|
||||
## Download
|
||||
|
||||
Die aktuelle Stable-Version ist hier verfügbar:
|
||||
### Windows
|
||||
|
||||
- [Aktuelles KST4Contest-Release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [Alle veröffentlichten Versionen](https://github.com/praktimarc/kst4contest/releases)
|
||||
Die aktuelle Version kann als ZIP-Datei heruntergeladen werden:
|
||||
|
||||
Die Release-Seite enthält die Programmpakete sowie die deutschen und englischen PDF-Handbücher.
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
### Welches Paket wird benötigt?
|
||||
Der Dateiname hat das Format `praktiKST-v<Versionsnummer>-windows-x64.zip `.
|
||||
|
||||
| Betriebssystem | Paket | Typischer Dateiname |
|
||||
|---|---|---|
|
||||
| Windows x64 | ZIP-Paket | `praktiKST-v<Version>-windows-x64.zip` |
|
||||
| Linux x86_64 | AppImage | `KST4Contest-v<Version>-linux-x86_64.AppImage` |
|
||||
| Debian/Ubuntu amd64 | DEB-Paket | `KST4Contest-v<Version>-debian-amd64.deb` |
|
||||
| Fedora/RPM x86_64 | RPM-Paket | `KST4Contest-v<Version>-fedora-x86_64.rpm` |
|
||||
| Arch Linux x86_64 | Arch-Paket | `KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst` |
|
||||
| Linux mit Flatpak | Flatpak-Referenz | `de.x08.KST4Contest.flatpakref` |
|
||||
| macOS Apple Silicon | DMG für ARM64 | `KST4Contest-v<Version>-macos-arm64.dmg` |
|
||||
| macOS Intel | DMG für x86_64 | `KST4Contest-v<Version>-macos-x86_64.dmg` |
|
||||
### Linux
|
||||
|
||||
Die aktuelle Version kann als AppImage heruntergeladen werden:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-linux-x86_64.AppImage`.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort-Support:** macOS-Builds werden als zusätzliche Option bereitgestellt, sind aber **nicht umfassend getestet**. Wir bauen und veröffentlichen macOS-Binaries mit jedem Release, können allerdings nicht alle Szenarien unter macOS testen. Bei Problemen freuen wir uns über eine Rückmeldung – wir versuchen unser Bestes, können aber nicht den gleichen Support-Umfang wie für Windows und Linux garantieren.
|
||||
|
||||
Die aktuelle Version kann als DMG-Disk-Image heruntergeladen werden (für Apple-Silicon- und Intel-Macs verfügbar):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-macos-<Architektur>.dmg`, wobei `<Architektur>` entweder `arm64` (Apple Silicon) oder `x86_64` (Intel) ist.
|
||||
|
||||
Lade Programmpakete nur aus den offiziellen GitHub Releases, dem KST4Contest-Flatpak-Repository oder den verlinkten AUR-Paketen herunter. Dateien aus anderen Quellen können anders gebaut, veraltet oder verändert worden sein.
|
||||
|
||||
---
|
||||
|
||||
## Installation unter Windows
|
||||
## Installation
|
||||
|
||||
KST4Contest wird unter Windows als ZIP-Paket bereitgestellt. Ein klassischer Installer ist nicht erforderlich.
|
||||
### Windows
|
||||
|
||||
1. Lade `praktiKST-v<Version>-windows-x64.zip` aus dem aktuellen Release herunter.
|
||||
2. Entpacke die ZIP-Datei vollständig in einen eigenen Ordner.
|
||||
3. Öffne den entpackten Ordner.
|
||||
4. Starte `praktiKST.exe`.
|
||||
1. ZIP-Datei herunterladen.
|
||||
2. ZIP-Datei in einen gewünschten Ordner entpacken.
|
||||
3. `praktiKST.exe` ausführen.
|
||||
|
||||
Starte das Programm nicht direkt aus der noch komprimierten ZIP-Datei. KST4Contest besteht aus mehreren Dateien und einer mitgelieferten Laufzeitumgebung. Windows kann diese Struktur nur zuverlässig verwenden, wenn das Archiv vorher vollständig entpackt wurde.
|
||||
Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` gespeichert.
|
||||
|
||||
Die Programmeinstellungen befinden sich nicht im entpackten Programmordner, sondern im Benutzerverzeichnis:
|
||||
### Linux
|
||||
1. AppImage herunterladen.
|
||||
2. AppImage in gewünschten Ordner entpacken.
|
||||
3. AppImage ausführbar machen (geht im Terminal mit `chmod +x KST4Contest-v<Versionsnummer>-linux-x86_64.AppImage`)
|
||||
4. AppImage ausführen.
|
||||
|
||||
```text
|
||||
%USERPROFILE%\.praktiKST\preferences.xml
|
||||
```
|
||||
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
|
||||
Dadurch können neue Programmversionen in einen anderen Ordner entpackt werden, ohne dass die vorhandenen Einstellungen verloren gehen.
|
||||
### macOS
|
||||
1. DMG-Datei für die eigene Architektur herunterladen (Apple Silicon oder Intel).
|
||||
2. DMG-Datei öffnen.
|
||||
3. `KST4Contest.app` in den **Programme**-Ordner ziehen.
|
||||
4. Beim ersten Start zeigt macOS ggf. eine Warnung, da die App nicht notarisiert ist. Zum Öffnen:
|
||||
- Rechtsklick (oder Ctrl-Klick) auf `KST4Contest.app` im Finder → **Öffnen** wählen.
|
||||
- Alternativ: **Systemeinstellungen → Datenschutz & Sicherheit** → **Trotzdem öffnen** klicken.
|
||||
5. KST4Contest aus dem Programme-Ordner oder dem Launchpad starten.
|
||||
|
||||
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## Installation unter Linux
|
||||
## Update
|
||||
|
||||
Unter Linux stehen mehrere Paketformate zur Verfügung. Welches davon sinnvoll ist, hängt weniger von KST4Contest als von der verwendeten Distribution und der gewünschten Update-Methode ab.
|
||||
KST4Contest enthält einen **automatischen Update-Hinweis-Dienst**: Sobald eine neue Version verfügbar ist, erscheint beim Start ein Fenster mit:
|
||||
- der Information, dass eine neue Version vorliegt,
|
||||
- einem Changelog,
|
||||
- dem Download-Link zur neuen Version.
|
||||
|
||||
| Installationsart | Sinnvoll, wenn … |
|
||||
|---|---|
|
||||
| **Flatpak** | Updates zentral verwaltet werden sollen und Flatpak bereits verwendet wird |
|
||||
| **AppImage** | KST4Contest ohne Installation als einzelne portable Datei gestartet werden soll |
|
||||
| **DEB/RPM** | die Paketverwaltung der Distribution verwendet werden soll |
|
||||
| **Arch-Paket/AUR** | Arch Linux, Manjaro oder EndeavourOS eingesetzt wird |
|
||||

|
||||
|
||||
### Flatpak
|
||||
### Update-Prozess
|
||||
|
||||
Flatpak ist für die meisten Linux-Anwender der einfachste Weg, KST4Contest installiert und aktualisierbar zu halten. Das KST4Contest-Repository ist GPG-signiert und enthält die Kanäle Stable, Beta und Nightly.
|
||||
#### Windows
|
||||
|
||||
#### Stable über die Release-Datei installieren
|
||||
Derzeit gibt es nur einen Weg zum Aktualisieren:
|
||||
|
||||
Lade `de.x08.KST4Contest.flatpakref` aus dem aktuellen Release herunter und öffne die Datei mit der Softwareverwaltung der verwendeten Desktop-Umgebung.
|
||||
1. Den alten Ordner löschen.
|
||||
2. Das neue ZIP entpacken.
|
||||
|
||||
Alternativ kann sie im Terminal installiert werden:
|
||||
Die Einstellungsdatei (`preferences.xml`) bleibt erhalten, da sie im Benutzerordner gespeichert ist – nicht im Programmordner.
|
||||
|
||||
```bash
|
||||
flatpak install ./de.x08.KST4Contest.flatpakref
|
||||
```
|
||||
#### Linux
|
||||
|
||||
#### KST4Contest-Repository hinzufügen
|
||||
Derzeit folgendermaßen:
|
||||
1. neues AppImage herunterladen
|
||||
2. neues AppImage ausführbar makieren
|
||||
3. (optional) altes AppImage löschen.
|
||||
|
||||
Das Repository muss nur einmal hinzugefügt werden:
|
||||
#### macOS
|
||||
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest \
|
||||
https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
```
|
||||
1. Neue DMG-Datei herunterladen.
|
||||
2. DMG öffnen.
|
||||
3. Die neue `KST4Contest.app` in den **Programme**-Ordner ziehen und die alte Version ersetzen.
|
||||
|
||||
Anschließend kann die Stable-Version installiert werden:
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//stable
|
||||
```
|
||||
|
||||
#### Beta installieren
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//beta
|
||||
```
|
||||
|
||||
#### Nightly installieren
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
KST4Contest verwendet für alle drei Kanäle dieselbe App-ID. Eine parallele Installation mehrerer KST4Contest-Kanäle ist deshalb nicht vorgesehen.
|
||||
|
||||
Zum Wechsel von Stable auf Nightly beispielsweise:
|
||||
|
||||
```bash
|
||||
flatpak uninstall de.x08.KST4Contest//stable
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
Die persönlichen Einstellungen im Verzeichnis `~/.praktiKST` werden dabei nicht automatisch gelöscht.
|
||||
|
||||
Installierte Flatpak-Anwendungen können mit folgendem Befehl aktualisiert werden:
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
Je nach Desktop-Umgebung kann die grafische Softwareverwaltung verfügbare Flatpak-Updates ebenfalls anzeigen oder automatisch installieren. Der Befehl `flatpak update` selbst führt das Update aus; er verspricht nicht, irgendwann von allein vorbeizukommen.
|
||||
|
||||
### AppImage
|
||||
|
||||
Das AppImage benötigt keine klassische Installation.
|
||||
|
||||
1. Lade `KST4Contest-v<Version>-linux-x86_64.AppImage` herunter.
|
||||
2. Öffne ein Terminal im Download-Verzeichnis.
|
||||
3. Mache die Datei ausführbar:
|
||||
|
||||
```bash
|
||||
chmod +x KST4Contest-v<Version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
4. Starte KST4Contest:
|
||||
|
||||
```bash
|
||||
./KST4Contest-v<Version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
Das AppImage kann anschließend an einen anderen Ort verschoben werden, beispielsweise nach `~/Applications`.
|
||||
|
||||
### Debian und Ubuntu
|
||||
|
||||
Installiere das DEB-Paket mit:
|
||||
|
||||
```bash
|
||||
sudo apt install ./KST4Contest-v<Version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
Alternativ kann die Datei in einer grafischen Paketverwaltung geöffnet werden.
|
||||
|
||||
### Fedora und kompatible RPM-Systeme
|
||||
|
||||
Installiere das RPM-Paket mit:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./KST4Contest-v<Version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
### Arch Linux: fertiges Release-Paket
|
||||
|
||||
Das aus dem GitHub Release heruntergeladene Arch-Paket kann direkt installiert werden:
|
||||
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
### Arch Linux: Installation über den AUR
|
||||
|
||||
Im AUR stehen drei Varianten bereit:
|
||||
|
||||
| Paket | Inhalt |
|
||||
|---|---|
|
||||
| [`kst4contest-bin`](https://aur.archlinux.org/packages/kst4contest-bin) | Vorgefertigtes Paket des aktuellen Stable-Releases |
|
||||
| [`kst4contest`](https://aur.archlinux.org/packages/kst4contest) | Stable-Release, das lokal aus dem Quellcode gebaut wird |
|
||||
| [`kst4contest-git`](https://aur.archlinux.org/packages/kst4contest-git) | Aktueller Entwicklungsstand aus dem `main`-Branch |
|
||||
|
||||
Für die meisten Anwender ist `kst4contest-bin` die naheliegende Variante:
|
||||
|
||||
```bash
|
||||
yay -S kst4contest-bin
|
||||
```
|
||||
|
||||
Stable aus dem Quellcode bauen:
|
||||
|
||||
```bash
|
||||
yay -S kst4contest
|
||||
```
|
||||
|
||||
Aktuellen Entwicklungsstand bauen:
|
||||
|
||||
```bash
|
||||
yay -S kst4contest-git
|
||||
```
|
||||
|
||||
Die drei Pakete stellen dieselbe Anwendung bereit und sind deshalb als gegenseitige Konflikte definiert. Installiere nur eine Variante gleichzeitig.
|
||||
|
||||
AUR-Updates werden berücksichtigt, wenn der verwendete AUR-Helper nach Paketaktualisierungen sucht, beispielsweise mit:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
Sie erfolgen nicht allein deshalb automatisch, weil das Paket aus dem AUR stammt.
|
||||
|
||||
---
|
||||
|
||||
## Installation unter macOS
|
||||
## Bekannte Probleme beim Start
|
||||
|
||||
> **Best-Effort-Support:** Die macOS-Pakete werden zusammen mit den übrigen Releases gebaut, aber nicht in demselben Umfang getestet wie die Windows- und Linux-Versionen. Rückmeldungen sind willkommen; eine vollständig geprüfte Unterstützung aller macOS-Versionen und Hardwarevarianten kann derzeit jedoch nicht zugesagt werden.
|
||||
### Norton 360
|
||||
|
||||
Für Apple-Silicon-Macs wird das Paket mit `arm64` benötigt. Für Intel-Macs ist das Paket mit `x86_64` vorgesehen.
|
||||
Norton 360 stuft `praktiKST.exe` als gefährlich ein (Fehlalarm). Es muss eine Ausnahme für die Datei eingerichtet werden:
|
||||
|
||||
1. Lade die passende DMG-Datei herunter.
|
||||
2. Öffne die DMG-Datei.
|
||||
3. Ziehe `KST4Contest.app` in den Ordner **Programme**.
|
||||
4. Starte KST4Contest aus dem Programme-Ordner oder über das Launchpad.
|
||||
1. Norton 360 öffnen.
|
||||
2. Sicherheit → Verlauf → Das entsprechende Ereignis suchen.
|
||||
3. „Wiederherstellen & Ausnahme hinzufügen" wählen.
|
||||
|
||||
Die Anwendung ist derzeit nicht von Apple notarisiert. macOS kann den ersten Start deshalb blockieren.
|
||||
|
||||
Falls die Anwendung aus dem offiziellen GitHub Release stammt:
|
||||
|
||||
1. Öffne den Programme-Ordner im Finder.
|
||||
2. Klicke mit der rechten Maustaste oder mit gedrückter Ctrl-Taste auf `KST4Contest.app`.
|
||||
3. Wähle **Öffnen**.
|
||||
4. Bestätige den Start im angezeigten Dialog.
|
||||
|
||||
Alternativ kann macOS unter **Systemeinstellungen → Datenschutz & Sicherheit** die Schaltfläche **Trotzdem öffnen** anbieten.
|
||||
|
||||
---
|
||||
|
||||
## Wo werden die Einstellungen gespeichert?
|
||||
|
||||
KST4Contest speichert seine Einstellungen und weitere lokale Arbeitsdateien im Benutzerverzeichnis. Der Programmordner und das Datenverzeichnis sind voneinander getrennt.
|
||||
|
||||
| Betriebssystem | Datenverzeichnis | Einstellungsdatei |
|
||||
|---|---|---|
|
||||
| Windows | `%USERPROFILE%\.praktiKST\` | `%USERPROFILE%\.praktiKST\preferences.xml` |
|
||||
| Linux | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||
| macOS | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||
|
||||
Beachte die Schreibweise `.praktiKST` mit großem `KST`. Unter Linux und macOS wird zwischen Groß- und Kleinschreibung unterschieden. `.praktikst` wäre dort schlicht ein anderes Verzeichnis.
|
||||
|
||||
Ein Programmupdate entfernt dieses Verzeichnis nicht. Trotzdem ist es sinnvoll, vor größeren Versionswechseln oder umfangreichen Änderungen an der Konfiguration eine Sicherung davon anzulegen.
|
||||
|
||||
---
|
||||
|
||||
## Updates
|
||||
|
||||
KST4Contest prüft beim Start, ob ein neueres Stable-Release verfügbar ist. Wenn eine neuere Version gefunden wird, erscheint ein Informationsfenster mit:
|
||||
|
||||
- der installierten Version,
|
||||
- der aktuellen Stable-Version,
|
||||
- einer kurzen Übersicht wesentlicher Änderungen,
|
||||
- dem Changelog,
|
||||
- bekannten Problemen,
|
||||
- einem Link zur passenden GitHub-Release-Seite.
|
||||
|
||||

|
||||
|
||||
Der Update-Checker installiert nichts selbst. Er informiert über die neue Version und öffnet die plattformneutrale Release-Seite. Dort muss das passende Paket für Windows, Linux oder macOS ausgewählt werden.
|
||||
|
||||
### Windows aktualisieren
|
||||
|
||||
1. Beende KST4Contest.
|
||||
2. Lade das neue Windows-ZIP herunter.
|
||||
3. Entpacke es in einen neuen oder leeren Ordner.
|
||||
4. Starte die neue Version.
|
||||
5. Prüfe, ob die bisherigen Einstellungen geladen wurden.
|
||||
6. Entferne den alten Programmordner erst danach.
|
||||
|
||||
Die Einstellungen bleiben erhalten, weil sie unter `%USERPROFILE%\.praktiKST` und nicht im Programmordner gespeichert werden.
|
||||
|
||||
### AppImage aktualisieren
|
||||
|
||||
1. Lade das neue AppImage herunter.
|
||||
2. Mache es ausführbar.
|
||||
3. Starte die neue Datei.
|
||||
4. Entferne das bisherige AppImage erst, wenn die neue Version funktioniert.
|
||||
|
||||
### Debian und Ubuntu aktualisieren
|
||||
|
||||
```bash
|
||||
sudo apt install ./KST4Contest-v<Version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
### Fedora aktualisieren
|
||||
|
||||
```bash
|
||||
sudo dnf upgrade ./KST4Contest-v<Version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
### Arch-Paket aktualisieren
|
||||
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<Version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
### AUR-Paket aktualisieren
|
||||
|
||||
Aktualisiere das installierte Paket über den verwendeten AUR-Helper, beispielsweise:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
### Flatpak aktualisieren
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
### macOS aktualisieren
|
||||
|
||||
1. Lade die neue DMG-Datei für die vorhandene Architektur herunter.
|
||||
2. Beende KST4Contest.
|
||||
3. Öffne die DMG-Datei.
|
||||
4. Ersetze `KST4Contest.app` im Programme-Ordner.
|
||||
5. Starte die neue Version und prüfe die vorhandenen Einstellungen.
|
||||
|
||||
Die Konfiguration unter `~/.praktiKST` bleibt davon unberührt.
|
||||
|
||||
---
|
||||
|
||||
## Probleme beim ersten Start
|
||||
|
||||
### Windows meldet eine unbekannte Anwendung
|
||||
|
||||
KST4Contest ist derzeit nicht mit einem kommerziellen Windows-Code-Signing-Zertifikat signiert. Windows oder ein zusätzlich installiertes Sicherheitsprodukt kann deshalb vor einer unbekannten oder selten heruntergeladenen Anwendung warnen.
|
||||
|
||||
Prüfe in diesem Fall zuerst:
|
||||
|
||||
- Stammt die Datei aus dem [offiziellen KST4Contest-Release](https://github.com/praktimarc/kst4contest/releases/latest)?
|
||||
- Passt der Dateiname zum veröffentlichten Release?
|
||||
- Wurde die Datei vollständig heruntergeladen?
|
||||
- Nennt das Sicherheitsprogramm einen konkreten Erkennungsnamen oder nur eine allgemeine Reputationswarnung?
|
||||
|
||||
Ein Warnhinweis ist allein weder ein sicherer Beweis für Schadsoftware noch automatisch ein Fehlalarm. Wenn die Herkunft der Datei unklar ist, sollte sie nicht gestartet oder aus der Quarantäne wiederhergestellt werden.
|
||||
|
||||
Einige Anwender haben insbesondere Quarantäne-Meldungen von Norton 360 gemeldet. Wenn sich eine Meldung reproduzieren lässt, erstelle bitte einen [GitHub-Issue](https://github.com/praktimarc/kst4contest/issues) und nenne:
|
||||
|
||||
- die KST4Contest-Version,
|
||||
- den vollständigen Dateinamen,
|
||||
- das verwendete Sicherheitsprodukt und dessen Version,
|
||||
- den angezeigten Erkennungsnamen,
|
||||
- möglichst einen Screenshot der Meldung.
|
||||
|
||||
### Das AppImage startet nicht
|
||||
|
||||
Prüfe zuerst das Ausführungsrecht:
|
||||
|
||||
```bash
|
||||
chmod +x KST4Contest-v<Version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
Starte die Datei anschließend aus einem Terminal. Fehlermeldungen sind dort meist aussagekräftiger als ein Doppelklick, der lediglich nichts Sichtbares tut.
|
||||
|
||||
### 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.
|
||||
|
||||
### Das Problem bleibt bestehen
|
||||
|
||||
Prüfe zunächst, ob das Problem bereits unter [GitHub Issues](https://github.com/praktimarc/kst4contest/issues) beschrieben wurde. Falls nicht, erstelle einen neuen Issue mit:
|
||||
|
||||
- Betriebssystem und Version,
|
||||
- verwendeter KST4Contest-Version,
|
||||
- Installationsart,
|
||||
- genauer Fehlermeldung,
|
||||
- den Schritten, mit denen sich das Problem reproduzieren lässt.
|
||||
|
||||
„Geht nicht“ beschreibt den Zustand meistens korrekt, hilft bei der Fehlersuche aber nur begrenzt.
|
||||
*(Gemeldet von PE0WGA, Franz van Velzen – danke!)*
|
||||
|
||||
@@ -14,7 +14,7 @@ Nach dem ersten Start öffnet sich das **Einstellungsfenster** – dieses ist de
|
||||
|
||||
### Login und Chat-Kategorien
|
||||
|
||||
Hier werden die Zugangsdaten für den ON4KST-Chat eingetragen (Rufzeichen und Passwort).
|
||||
Hier werden die Zugangsdaten für den ON4KST-Chat eingetragen (Rufzeichen und Passwort).
|
||||
Zudem wird die **primäre Chat-Kategorie** (z. B. IARU Region 1 VHF/Microwave) ausgewählt.
|
||||
|
||||
Mit der Option für einen **zweiten Chat** (Multi-Channel-Login) kann man sich gleichzeitig in eine weitere Kategorie (z. B. UHF/SHF) einloggen. Beide Chats werden dann parallel überwacht. Hier kann optional auch ein abweichender Login-Name für den zweiten Chat vergeben werden (nützlich für Opposite Station Multi-Callsign Logging).
|
||||
@@ -25,83 +25,31 @@ Eigenes Rufzeichen und Maidenhead-Locator (6-stellig, z. B. `JN49IJ`) eintragen.
|
||||
|
||||
### Aktivierte Bänder
|
||||
|
||||
Über die Checkboxen **My station uses …** wird festgelegt, auf welchen Bändern die eigene Station im aktuellen Setup arbeiten kann. Unterstützt werden 50 MHz, 70 MHz, 144 MHz, 432 MHz, 1296 MHz, 2320 MHz, 3400 MHz, 5760 MHz und 10 GHz.
|
||||
|
||||
Die Auswahl steuert nicht nur die sichtbaren Bandspalten. Sie wird außerdem verwendet für:
|
||||
|
||||
- die bandbezogenen Worked- und NOT-QRV-Filter,
|
||||
- die im **Further Info**-Bereich sichtbaren NOT-QRV-Schalter,
|
||||
- die Herleitung von `a`- und `B+`-Bandmöglichkeiten,
|
||||
- den Filter **New bands**,
|
||||
- den Band-Upgrade-Hinweis nach einem Logeintrag und
|
||||
- bandbezogene Prioritäts- und Reachability-Funktionen.
|
||||
|
||||
Nach einer Änderung **Save Settings** verwenden und KST4Contest neu starten. Die Bandspalten und mehrere zugehörige Bedienelemente werden beim Aufbau der Benutzeroberfläche erzeugt und deshalb nicht vollständig in der laufenden Sitzung ergänzt oder entfernt.
|
||||
Über die **„my station uses band"**-Checkboxen werden die aktiven Bänder ausgewählt. Nur für ausgewählte Bänder erscheinen Schaltflächen und Tabellenzeilen in der Benutzeroberfläche. Nach Änderungen muss die Software neu gestartet werden.
|
||||
|
||||
### Antennen-Öffnungswinkel (Antenna Beamwidth)
|
||||
|
||||
Trage den vollständigen horizontalen Öffnungswinkel der eigenen Antenne in Grad ein. KST4Contest verwendet jeweils die Hälfte dieses Werts links und rechts der gewählten beziehungsweise hergeleiteten Antennenrichtung. Ein eingetragener Wert von `70°` entspricht daher einem Korridor von `±35°`.
|
||||
Einen realistischen Wert für den Öffnungswinkel der eigenen Antenne eintragen (in Grad). Dieser Wert wird für die [Sked-Richtungs-Hervorhebung](Funktionen#sked-richtungs-hervorhebung) verwendet. Ein Testwert von 50° hat sich bewährt; DM5M nutzt Quads mit 69°.
|
||||
|
||||
Der Wert wird an mehreren Stellen verwendet:
|
||||
|
||||
- für den QTF-Filter der Benutzerliste,
|
||||
- für die Darstellung des eigenen Antennenkorridors,
|
||||
- als angenommener Öffnungswinkel einer fremden Station bei der [Herleitung von Richtungsgelegenheiten](de-Funktionen#richtungsgelegenheiten-aus-gerichteten-nachrichten).
|
||||
|
||||
Der letzte Punkt ist bewusst eine Näherung. ON4KST überträgt weder die verwendete Antenne noch deren Öffnungswinkel. KST4Contest verwendet deshalb den eigenen Wert als praktikable Annahme für die Gegenstation.
|
||||
|
||||
Wähle einen realistischen Wert. Ein zu großer Öffnungswinkel erzeugt viele geometrische Treffer, die praktisch kaum noch eine Aussage haben. Ein zu kleiner Wert kann dagegen brauchbare Richtungsgelegenheiten ausblenden.
|
||||
> **Keinesfalls** Fantasy-Werte eintragen – die Richtungsberechnungen werden sonst unbrauchbar.
|
||||
|
||||
### Standard-Maximum-QRB
|
||||
|
||||
Trage die maximale Entfernung in Kilometern ein, innerhalb der KST4Contest Richtungsgelegenheiten berücksichtigen soll. Maßgeblich ist die Entfernung zwischen der eigenen Station und dem Absender der gerichteten Nachricht – nicht die Entfernung zwischen Absender und Empfänger.
|
||||
|
||||
Liegt der Absender weiter entfernt, wird die Situation auch dann nicht hervorgehoben und nicht als Richtungsgelegenheit an den lokalen DX-Cluster-Server weitergegeben, wenn der berechnete Winkel passen würde.
|
||||
|
||||
Der Wert sollte zum eigenen Stationsaufbau und zum vorgesehenen Contestbetrieb passen. Ein unnötig großer Bereich erzeugt Hinweise für Stationen, die praktisch nicht mehr zum Arbeitsbereich gehören; ein zu kleiner Bereich blendet mögliche Kandidaten bereits vor der Richtungsbewertung aus.
|
||||
Maximale Entfernung (in km), für die Richtungs-Warnungen ausgelöst werden sollen. Realistischer Wert für DM5M: 900 km. Stationen, die weiter entfernt sind, werden für Highlighting-Zwecke ignoriert.
|
||||
|
||||
---
|
||||
|
||||
### Streckenanalyse und Link-Budget
|
||||
## Server-Einstellungen (ab v1.31)
|
||||
|
||||
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.
|
||||
Der Chat-Server-DNS und -Port sind in den Preferences konfigurierbar:
|
||||
|
||||
| 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 |
|
||||
- **Server-DNS**: Standard `www.on4kst.org` (ab v1.31 geändert von `www.on4kst.info`).
|
||||
- **Port**: Standardport des ON4KST-Servers.
|
||||
|
||||
**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.
|
||||
Eine Änderung ist nur notwendig, wenn der Server umzieht oder ein alternativer Endpunkt genutzt wird.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Log-Sync-Einstellungen
|
||||
|
||||
Drei Methoden stehen zur Verfügung, um gearbeitete Stationen automatisch zu markieren. Details: [Log-Synchronisation](de-Log-Synchronisation).
|
||||
@@ -139,121 +87,17 @@ Konfiguration der Schnittstelle zu AirScout für die Flugzeug-Scatter-Erkennung.
|
||||
|
||||
## Notification Settings (Benachrichtigungen)
|
||||
|
||||

|
||||
Drei Benachrichtigungstypen stehen zur Wahl:
|
||||
|
||||
Im Reiter **Notification** werden nicht nur akustische Hinweise konfiguriert. Hier befinden sich auch die Einstellungen für den lokalen DX-Cluster-Server, den Band-Upgrade-Hinweis und das QSO-Monitoring.
|
||||
|
||||
### Akustische Hinweise
|
||||
|
||||
Die drei Audiofunktionen arbeiten unabhängig voneinander:
|
||||
|
||||
- **Play notification sounds …** aktiviert kurze Hinweistöne für neue Privatnachrichten, erkannte Richtungsgelegenheiten, Sked-Erinnerungen und Band-Upgrade-Hinweise.
|
||||
- **Spell the sender's callsign in CW …** gibt das Rufzeichen des Absenders einer neuen Privatnachricht als CW-Signal aus.
|
||||
- **Speak the sender's callsign phonetically …** spricht das Rufzeichen des Absenders phonetisch aus.
|
||||
|
||||
CW- und Sprachausgabe können gleichzeitig aktiviert werden. Das ist technisch möglich, im Contest aber nicht zwingend hilfreich. In der Praxis sollte nur die Ausgabe eingeschaltet werden, die im eigenen Stationsbetrieb tatsächlich wahrgenommen werden kann, ohne den Operator dauerhaft zu beschäftigen.
|
||||
|
||||
### Fallback-Band für relative QRG-Erkennung
|
||||
|
||||
Das Dropdown **Fallback band for relative QRG detection** legt fest, welches Band KST4Contest verwendet, wenn eine relative QRG keinem aktuellen Stationskontext zugeordnet werden kann.
|
||||
|
||||
Zur Auswahl stehen ausschließlich die vom Frequenzparser unterstützten Bandpräfixe:
|
||||
|
||||
```text
|
||||
50 MHz
|
||||
70 MHz
|
||||
144 MHz
|
||||
432 MHz
|
||||
1296 MHz
|
||||
2320 MHz
|
||||
3400 MHz
|
||||
5760 MHz
|
||||
10368 MHz (10G)
|
||||
24048 MHz (24G)
|
||||
```
|
||||
|
||||
Das Dropdown ist kein Filter und keine Vorgabe für vollständig angegebene Frequenzen. `432.088` wird unabhängig von der Auswahl als Frequenz im 432-MHz-Band erkannt. Benötigt wird das Fallback bei relativen Angaben wie `.205`, `,205` oder `qrg 205`.
|
||||
|
||||
Bevor KST4Contest auf das Fallback zurückgreift, prüft es den Bandkontext des Absenders. Wurde für dieselbe Station innerhalb der letzten 30 Minuten bereits eine passende vollständige Frequenz erkannt, hat dieses Band Vorrang. Ein Fallback von `144 MHz` macht aus `.100` daher `432.100 MHz`, wenn die Station kurz zuvor beispielsweise `432.088` genannt hat.
|
||||
|
||||
Die Einstellung befindet sich im Notification-Bereich, wirkt aber auf die gesamte QRG-Erkennung. Damit beeinflusst sie nicht nur mögliche DX-Cluster-Spots, sondern auch die QRG-Spalte, erkannte aktive Bänder, Priorisierung, Band-Upgrade-Hinweise und Funktionen, die eine bekannte Stationsfrequenz verwenden.
|
||||
|
||||
Mehr zur Erkennungslogik und zu absichtlich ignorierten Zahlen: [QRG-Erkennung](de-Funktionen#qrg-erkennung).
|
||||
|
||||
### Local DX Cluster output
|
||||
|
||||
KST4Contest kann erkannte Richtungsgelegenheiten als DX-Cluster-Spots an ein Logprogramm weitergeben. Eine im Chat erkannte Frequenz erscheint dadurch direkt in der Bandmap des Logprogramms und muss nicht erst von Hand übertragen werden.
|
||||
|
||||
Die Checkbox **Enable the local DX Cluster server …** startet beziehungsweise beendet den lokalen TCP-Server. Bei einer laufenden Chat-Verbindung wird die Änderung sofort wirksam.
|
||||
|
||||
Folgende Einstellungen und Schaltflächen gehören zur lokalen DX-Cluster-Ausgabe:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Die Prüfung verwendet dieselbe Bandherleitung wie die `a`- und `B+`-Anzeige:
|
||||
|
||||
1. die in den Stationseinstellungen aktivierten eigenen Bänder,
|
||||
2. höchstens 30 Minuten alte QRG-Erkennungen der Gegenstation,
|
||||
3. eindeutige Bandangaben im Namensfeld ihrer aktiven Chat-Einträge,
|
||||
4. die pro Band gespeicherten Worked-Markierungen und
|
||||
5. manuell gesetzte NOT-QRV-Tags.
|
||||
|
||||
Aktive Chat-Varianten desselben normalisierten Rufzeichens werden gemeinsam ausgewertet. NOT-QRV hat Vorrang vor einer automatisch erkannten QRG oder Bandangabe.
|
||||
|
||||
Bleibt mindestens ein gemeinsames, noch nicht gearbeitetes Band übrig, erscheint im Hauptfenster für ungefähr zwölf Sekunden ein blinkender **BAND+**-Hinweis mit Rufzeichen und den noch offenen Bändern. Der Tooltip zeigt die vollständige Herleitung. Ist die allgemeine Soundausgabe aktiviert, wird zusätzlich ein kurzer Hinweiston abgespielt.
|
||||
|
||||
Die beiden Optionen haben unterschiedliche Aufgaben:
|
||||
|
||||
- **Blink + sound …** aktiviert den Hinweis nach einem passenden Logeintrag.
|
||||
- **Priority boost …** erhöht zusätzlich den Score von Stationen, die bereits auf mindestens einem Band gearbeitet wurden, aber noch ein weiteres gemeinsames und nicht gearbeitetes Band anbieten.
|
||||
|
||||
Der Priority Boost ist nur ein Faktor innerhalb der gesamten Berechnung. Entfernung, Antennenrichtung, aktuelle Aktivität, AirScout-Daten, Skeds und negative Hinweise können den endgültigen Listenplatz weiterhin verändern. Die aktivierte Option garantiert deshalb weder einen bestimmten Score noch einen bestimmten Platz in der Prioritätsliste.
|
||||
|
||||
Die übrigen Score-Gewichte besitzen derzeit keine eigenen Bedienelemente. Mehrere vorhandene Einstellungen liefern jedoch Eingangsdaten für die Berechnung, insbesondere die [aktivierten Bänder](#aktivierte-bänder), der [Antennen-Öffnungswinkel](#antennen-öffnungswinkel-antenna-beamwidth), der [Standard-Maximum-QRB](#standard-maximum-qrb) und die [AirScout-Einstellungen](#airscout-einstellungen).
|
||||
|
||||
Die vollständige Herleitung ist unter [Prioritätsscore und Prioritätsliste](de-Funktionen#prioritätsscore-und-prioritätsliste-ab-v140) beschrieben.
|
||||
|
||||
Der Hinweis setzt eine Log-Synchronisation mit Bandinformation voraus. Der einfache dateibasierte Callsign-Interpreter erkennt lediglich Rufzeichen und liefert deshalb keine sichere Information über das Band des gerade geloggten QSOs.
|
||||
|
||||
Weitere Hintergründe: [Band-Upgrade-Hinweis nach einem Logeintrag](de-Funktionen#band-upgrade-hinweis-nach-einem-logeintrag).
|
||||
|
||||
### 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.
|
||||
|
||||
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.
|
||||
|
||||
Überwachte Nachrichten werden in der PM-Tabelle eindeutig gekennzeichnet:
|
||||
|
||||
```text
|
||||
Sniffed: (SENDER > RECEIVER) Nachrichtentext
|
||||
```
|
||||
|
||||
So wird sichtbar, dass die Nachricht nicht an die eigene Station gerichtet war.
|
||||
|
||||
Rufzeichen werden folgendermaßen verwaltet:
|
||||
|
||||
1. Mit **Add monitored callsign** ein neues Rufzeichen hinzufügen.
|
||||
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.
|
||||
1. **Einfache Sounds**: TADA-Sound für eingehende Nachrichten, Tick für Sked-Richtungserkennung usw.
|
||||
2. **CW-Ansage**: Das Rufzeichen einer Station, die eine Privatnachricht sendet, wird als CW-Signal ausgegeben.
|
||||
3. **Phonetische Ansage**: Das Rufzeichen wird phonetisch ausgesprochen.
|
||||
|
||||
---
|
||||
|
||||
## Shortcut Settings (Schnellzugriff-Schaltflächen)
|
||||
|
||||
Konfiguration von Schnellzugriff-Schaltflächen, die direkt im Hauptfenster erscheinen. Ein Klick auf eine Schaltfläche fügt den konfigurierten Text in das Sendfeld ein. Alle [Variablen](de-Makros-und-Variablen#variablen) können verwendet werden.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -273,155 +117,38 @@ Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als D
|
||||
|
||||
## Beacon Settings (Automatischer Beacon)
|
||||
|
||||

|
||||
Konfiguration eines automatischen Intervall-Beacons im öffentlichen Chat-Kanal. Empfohlen: Variable `MYQRG` im Text verwenden, damit die aktuelle Frequenz immer aktuell ist. Intervall und Text sind frei konfigurierbar.
|
||||
|
||||
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.
|
||||
> **Tipp**: Beacon beim CQ-Rufen aktivieren und im Einstellungsfenster schnell deaktivieren, wenn kein CQ gerufen wird.
|
||||
|
||||
KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivierung und Nachrichtentext werden trotzdem getrennt konfiguriert:
|
||||
|
||||
- **Enable CQ beacon** aktiviert den Beacon der betreffenden Kategorie.
|
||||
- **Beacon message** enthält den öffentlichen Nachrichtentext dieser Kategorie.
|
||||
- **Shared beacon interval** legt das gemeinsame Intervall für beide Kategorien fest.
|
||||
|
||||
Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in ihren jeweiligen Kategorien gesendet. Der zweite Beacon wird nur berücksichtigt, wenn auch der zweite Chat aktiviert und verbunden ist.
|
||||
|
||||
### Intervall und Timer-Verhalten
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
Im Beacon können alle [globalen Variablen](de-Makros-und-Variablen#variablen-im-beacon) verwendet werden, beispielsweise:
|
||||
|
||||
```text
|
||||
calling cq at MYQRG, 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.
|
||||
|
||||
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.
|
||||
|
||||
### Wann sollte der Beacon ausgeschaltet werden?
|
||||
|
||||
Der Beacon ist nur dann hilfreich, wenn seine QRG-Angabe zum tatsächlichen Betrieb passt. Bleibt er beim Absuchen oder häufigen Wechseln von Frequenzen aktiviert, können andere Stationen auf einer inzwischen falschen Frequenz nach der eigenen Station suchen.
|
||||
|
||||
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)
|
||||
|
||||

|
||||
Neuer Einstellungsbereich mit folgenden Optionen:
|
||||
|
||||
Die wichtigste Anwendung der allgemeinen automatischen Antwort betrifft Stationen, die zwar im ON4KST-Chat eingeloggt sind, den laufenden Contest aber nicht mitfunken. Gerade während größerer Contests werden Sked-Anfragen teilweise unkoordiniert und in großer Zahl an eingeloggte Stationen verteilt, ohne vorher zu prüfen, ob sie überhaupt teilnehmen. Die Empfänger müssten sonst immer wieder dieselbe Absage schreiben.
|
||||
|
||||
KST4Contest kann darauf mit einem vorher festgelegten Text reagieren. Davon getrennt steht eine gezielte QRG-Auskunft zur Verfügung. Beide Funktionen können unabhängig voneinander aktiviert werden.
|
||||
|
||||
### Allgemeine automatische Antwort
|
||||
|
||||
**Enable automatic reply to all private messages** beantwortet eingehende Privatnachrichten mit dem Text im Feld rechts daneben. Es gibt einen gemeinsamen Text für beide Chat-Kategorien. Die beim Eingeben verwendete Groß- und Kleinschreibung bleibt erhalten.
|
||||
|
||||
Eine zweckmäßige Nachricht ist beispielsweise:
|
||||
|
||||
```text
|
||||
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.
|
||||
|
||||
### Automatische QRG-Antwort
|
||||
|
||||
**Enable automatic QRG replies** reagiert auf typische QRG-Anfragen. Die Erkennung unterscheidet nicht zwischen Groß- und Kleinschreibung und sucht nach folgenden Textbestandteilen:
|
||||
|
||||
```text
|
||||
ur qrg?
|
||||
your qrg?
|
||||
qrg?
|
||||
freq?
|
||||
pse qrg
|
||||
```
|
||||
|
||||
Die Antwort enthält nur 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 |
|
||||
|
||||
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.
|
||||
|
||||
### Schutz vor wiederholten Antworten
|
||||
|
||||
Jede automatisch erzeugte Nachricht trägt das feste Präfix:
|
||||
|
||||
```text
|
||||
[KST4C Automsg]
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
Die Sperrzeit beginnt nur, wenn KST4Contest tatsächlich eine Antwort sendet.
|
||||
|
||||
> **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.
|
||||
|
||||
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung und Text nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
|
||||
|
||||
Weitere Hintergründe: [Automatische Antworten auf Privatnachrichten](de-Funktionen#automatische-antworten-auf-privatnachrichten-ab-v125).
|
||||
- **Auto-Antwort auf alle eingehenden Nachrichten**: Automatische Antwort auf Privatnachrichten konfigurierbar.
|
||||
- **Auto-Antwort mit eigener CQ-QRG**: Wenn jemand nach der eigenen QRG fragt, antwortet KST4Contest automatisch mit dem Inhalt der `MYQRG`-Variable.
|
||||
- **Standard-Filter für das Userinfo-Fenster**: Voreingestellter Nachrichtenfilter für das Stationsinfo-Fenster konfigurierbar *(für Gianluca :-) )*.
|
||||
|
||||
---
|
||||
|
||||
## Win-Test-Netzwerk-Listener (ab v1.31)
|
||||
|
||||
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:
|
||||
Dedizierter Empfänger für Win-Test-spezifische UDP-Pakete. Ermöglicht:
|
||||
|
||||
- QSOs einschließlich Band- und Locatorinformation auswerten,
|
||||
- STATUS-Pakete für die eigene QRG verarbeiten und
|
||||
- Skeds an das Win-Test-Netzwerk übergeben.
|
||||
- **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.
|
||||
|
||||
### Einstellungen unter Log sync
|
||||
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.
|
||||
|
||||
| Einstellung | Funktion |
|
||||
|---|---|
|
||||
| **Receive Win-Test network based UDP log messages** | Aktiviert den Win-Test-Netzwerk-Listener. Bei aktiviertem Listener wird nach **Create sked** auch die Sked-Übergabe versucht. |
|
||||
| **UDP-Port for Win-Test listener** | Port des Win-Test-Netzwerks. Standard ist `9871`. Der Port wird auch für die Sked-Übergabe verwendet. |
|
||||
| **KST station name in Win-Test network (src of SKED packets)** | Stationsname, unter dem KST4Contest die Sked-Pakete sendet. In einem Netzwerk mit mehreren Clients sollte ein eindeutiger Name verwendet werden. |
|
||||
| **Win-Test network broadcast address** | Zieladresse für ausgehende Win-Test-Netzwerkpakete. Bei lokalem Netzwerkbetrieb muss hier eine vom Win-Test-Rechner erreichbare Broadcast-Adresse eingetragen sein. |
|
||||
|
||||
Die Broadcast-Adresse ist konfigurierbar, weil `255.255.255.255` nicht in jedem Stationsnetz und nicht über jede Netzwerkschnittstelle zuverlässig weitergeleitet wird. Bei mehreren Rechnern kann stattdessen die zum Stationsnetz gehörende gerichtete Broadcast-Adresse erforderlich sein.
|
||||
|
||||
### Einstellungen unter TRX sync
|
||||
|
||||
| Einstellung | Funktion |
|
||||
|---|---|
|
||||
| **Win-Test STATUS QRG Sync** | Übernimmt die aktuelle Frequenz aus Win-Test-STATUS-Paketen als eigene QRG. |
|
||||
| **Use pass frequency from Win-Test STATUS** | Verwendet die übertragene Pass-Frequenz anstelle der normalen TRX-QRG. |
|
||||
| **Win-Test station name filter** | Verarbeitet nur STATUS-Pakete der angegebenen Win-Test-Station. Ein leeres Feld akzeptiert alle Stationsnamen. |
|
||||
|
||||
Der Stationsfilter ist insbesondere bei mehreren Win-Test-Clients sinnvoll. Ohne Filter kann die zuletzt eingegangene STATUS-Meldung eines anderen Arbeitsplatzes die eigene QRG in KST4Contest überschreiben.
|
||||
|
||||
### Sked-Übergabe
|
||||
|
||||
Für die Sked-Übergabe gibt es keinen davon getrennten internen Sked-Modus. Ist der Listener aktiviert, versucht **Create sked** zusätzlich zur internen Anlage die Übertragung an Win-Test.
|
||||
|
||||
KST4Contest sendet nur dann ein `ADDSKED`-Paket, wenn eine QRG ermittelt wurde, die zum ausdrücklich ausgewählten Band gehört. Kann keine passende QRG gefunden werden, bleibt der interne Sked bestehen und die Win-Test-Übergabe wird ausgelassen.
|
||||
|
||||
Die Auswahl `SSB` oder `CW` erfolgt direkt im Further-Info-Bereich beim Anlegen des Skeds. Eine automatische Mode-Ableitung wird nicht verwendet.
|
||||
|
||||
Nach Änderungen **Save Settings** verwenden, damit Port, Stationsname, Broadcast-Adresse und TRX-Optionen beim nächsten Programmstart wiederhergestellt werden.
|
||||
|
||||
Datenbehandlung und QRG-Auswahl: [Log-Synchronisation – Win-Test](de-Log-Synchronisation#win-test)
|
||||
> **Hinweis**: Der Win-Test-Listener ist ein **zusätzlicher** Listener – der Standard-QSO-UDP-Broadcast-Listener auf Port 12060 bleibt davon unabhängig.
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator-Einstellungen (ab v1.31)
|
||||
|
||||
@@ -436,37 +163,25 @@ Einstellungen:
|
||||
|
||||
---
|
||||
|
||||
## GUI Settings: Hinweise in den Bandspalten
|
||||
## Sniffer-Einstellungen (ab v1.31)
|
||||
|
||||
Im Reiter **GUI** lassen sich zwei Zusatzinformationen der Bandspalten ein- oder ausblenden:
|
||||
Der QSO-Sniffer filtert Chat-Nachrichten von konfigurierbaren Rufzeichen und leitet sie ins PM-Fenster weiter.
|
||||
|
||||
- **Show "o" in band columns …** zeigt ein `o`, wenn das vierstellige Großfeld auf dem betreffenden Band bereits gearbeitet wurde. Das Abschalten entfernt keine Daten aus der Datenbank; nur die zusätzliche Anzeige in den Bandspalten wird ausgeblendet. `wkdany` bleibt davon unberührt.
|
||||
- **Show "a" in band columns …** unterscheidet ein vollständig neues Rufzeichen von einer Bandmöglichkeit mit einem bereits auf einem anderen Band gearbeiteten Rufzeichen. Ist die Option ausgeschaltet, werden beide Fälle als `B+` dargestellt. Die Bandherleitung selbst ändert sich dadurch nicht.
|
||||
Einstellungen:
|
||||
- **Rufzeichen-Liste**: Kommagetrennte Liste von Rufzeichen, deren Nachrichten immer in das PM-Fenster weitergeleitet werden sollen.
|
||||
|
||||
Änderungen werden in der laufenden Benutzeroberfläche unmittelbar sichtbar. Damit sie nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
|
||||
|
||||

|
||||
Anwendungsfall: Wichtige Stationen (z. B. DX-Peditionen oder feste Verbündete im Contest) im Auge behalten, ohne den Haupt-Chat ständig zu beobachten.
|
||||
|
||||
---
|
||||
|
||||
## Worked Station Database Settings (Gearbeitete-Stationen-Datenbank)
|
||||
|
||||
Die interne SQLite-Datenbank speichert die contestbezogenen Zustände unabhängig von der Datenbank des Logprogramms:
|
||||
Die interne Worked-Datenbank enthält:
|
||||
|
||||
- globaler Worked-Status eines Rufzeichens,
|
||||
- Worked-Status pro Band,
|
||||
- manuell gesetzte NOT-QRV-Tags pro Band und
|
||||
- gearbeitete vierstellige Großfelder pro Band.
|
||||
- Worked-Status aller Stationen (pro Band)
|
||||
- NOT-QRV-Tags (seit v1.2)
|
||||
|
||||
Als Schlüssel wird das normalisierte Rufzeichen ohne sichtbare Chat-Klammern oder Kategorieformatierung verwendet. Dadurch können aktive Varianten desselben Rufzeichens konsistent ausgewertet werden.
|
||||
|
||||
Worked- und NOT-QRV-Informationen laufen drei Tage nach ihrer letzten Änderung automatisch ab. Gespeicherte Großfelder laufen drei Tage nach dem zugehörigen Logeintrag ab. Ein manuelles Zurücksetzen vor jedem Contest ist deshalb normalerweise nicht erforderlich.
|
||||
|
||||
Die Schaltfläche **Reset worked, NOT-QRV and grid data...** entfernt sämtliche Worked-Markierungen, NOT-QRV-Tags und gespeicherten Großfelder. Vor dem Reset erscheint eine Sicherheitsabfrage. Die bekannten Rufzeichenzeilen bleiben erhalten; zurückgesetzt werden nur die contestbezogenen Zustände.
|
||||
|
||||
Ein Reset ist sinnvoll, wenn bewusst mit einem leeren Conteststand begonnen werden soll oder Testdaten eingelesen wurden. Als tägliche Wartungsmaßnahme ist er nicht vorgesehen.
|
||||
|
||||
Anzeige und Herleitung: [Gearbeitete Rufzeichen, neue Bänder und neue Großfelder](de-Funktionen#gearbeitete-rufzeichen-neue-bänder-und-neue-großfelder).
|
||||
**Ab v1.40**: Einträge haben eine automatische Lebensdauer von **3 Tagen** – ein manuelles Zurücksetzen vor jedem Contest ist nicht mehr zwingend notwendig. Für ein vollständiges Reset kann trotzdem die Schaltfläche **„Reinitialize"** verwendet werden.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 🇬🇧 [English version](en-Log-Sync) | 🇩🇪 Du liest gerade die deutsche Version
|
||||
|
||||
KST4Contest übernimmt gearbeitete Stationen aus dem Logprogramm und stellt daraus den globalen Worked-Status, bandbezogene Worked-Markierungen und – sofern ein Locator übertragen wurde – gearbeitete Großfelder bereit. Dafür gibt es drei Wege: den dateibasierten Simplelogfile-Interpreter, den allgemeinen QSO-UDP-Listener und den eigenen Win-Test-Netzwerk-Listener.
|
||||
KST4Contest markiert gearbeitete Stationen automatisch in der Chat-Benutzerliste. Dafür gibt es zwei grundlegende Methoden:
|
||||
|
||||
---
|
||||
|
||||
@@ -10,25 +10,24 @@ KST4Contest übernimmt gearbeitete Stationen aus dem Logprogramm und stellt dara
|
||||
|
||||
## Methode 1: Universal File Based Callsign Interpreter (Simplelogfile)
|
||||
|
||||
KST4Contest liest eine Logdatei und sucht mit einem konfigurierbaren regulären Ausdruck nach Rufzeichen. Die Datei wird ausschließlich gelesen und nicht verändert. Auch binäre Logdateien können verwendet werden; nicht als Text interpretierbare Inhalte werden übersprungen.
|
||||
KST4Contest liest eine Log-Datei und sucht mittels regulärem Ausdruck nach Rufzeichen-Mustern. Dabei werden auch binäre Logdateien unterstützt – unlesbarer Binärinhalt wird einfach ignoriert.
|
||||
|
||||
Der Vorteil liegt in der breiten Kompatibilität: Die Funktion benötigt keine besondere Netzwerkschnittstelle des Logprogramms.
|
||||
**Vorteil**: Funktioniert mit nahezu jedem Logprogramm, das eine Datei schreibt.
|
||||
**Nachteil**: Keine Bandinformation möglich – es wird nur „gearbeitet" markiert, nicht auf welchem Band.
|
||||
|
||||
Die Grenze ist ebenso eindeutig: Aus einem reinen Rufzeichentreffer lassen sich weder Band noch Locator zuverlässig ableiten. Der Simplelogfile-Interpreter kann deshalb nur den globalen Worked-Status setzen. Er erzeugt keine bandbezogene `X`-Markierung, kein Worked-Großfeld und keine belastbare Grundlage für den Band-Upgrade-Hinweis nach einem Logeintrag.
|
||||
Pfad der Log-Datei in den Preferences eintragen. Die Datei wird nur gelesen, nie verändert (read-only).
|
||||
|
||||
Den Pfad der Logdatei und den regulären Ausdruck im Reiter **Log sync** eintragen. Für bandbezogene Auswertungen sollte nach Möglichkeit eine der Netzwerkschnittstellen verwendet werden.
|
||||
> **Tipp**: Die Simplelogfile-Funktion kann auch genutzt werden, um Stationen zu markieren, die definitiv nicht erreichbar sind (z. B. eigene Notizen). Das wird in einer späteren Version durch ein besseres Tagging-System ersetzt.
|
||||
|
||||
---
|
||||
|
||||
## Methode 2: Netzwerk-Listener für QSO-UDP-Pakete – empfohlen
|
||||
## Methode 2: Netzwerk-Listener (UDP-Broadcast) – Empfohlen
|
||||
|
||||
UCXLog, QARTest, N1MM+ und DXLog.net können beim Speichern eines QSOs ein UDP-Paket senden. KST4Contest empfängt diese Pakete standardmäßig auf Port `12060` und übernimmt das Rufzeichen sowie die enthaltenen Band- und Locatorinformationen.
|
||||
Das Logprogramm sendet beim Speichern eines QSOs ein UDP-Paket an die Broadcast-Adresse des Heimnetzwerks. KST4Contest empfängt dieses Paket und markiert die Station inklusive **Bandinformation** in der internen SQLite-Datenbank.
|
||||
|
||||
Liegt eine Bandinformation vor, wird das Rufzeichen für dieses Band als gearbeitet markiert. Enthält das Paket zusätzlich einen gültigen Locator, speichert KST4Contest dessen vierstelliges Großfeld für das betreffende Band. Fehlende Informationen werden nicht aus anderen Feldern geraten.
|
||||
> **Wichtig**: KST4Contest muss **parallel zum Logprogramm laufen**. QSOs, die während einer Abwesenheit von KST4Contest geloggt werden, werden nicht erfasst – außer bei QARTest (kann das komplette Log senden).
|
||||
|
||||
KST4Contest muss zum Zeitpunkt der Übertragung laufen. Einige Logprogramme können jedoch das vorhandene Log erneut senden: QARTest bietet dafür **Invia log completo**; DXLog.net sendet beim Broadcast des vollständigen Logs `contactreplace`-Pakete, die KST4Contest ebenfalls verarbeitet.
|
||||
|
||||
**Standardport:** `12060`
|
||||
**Standard UDP-Port**: 12060 (entspricht dem Standard der meisten Logprogramme)
|
||||
|
||||
---
|
||||
|
||||
@@ -82,80 +81,34 @@ Für den integrierten DX-Cluster-Server: N1MM+ als DX-Cluster-Client konfigurier
|
||||
- IP des KST4Contest-Computers eintragen (grün markierte Felder)
|
||||
- Port: 12060
|
||||
|
||||
Beim Broadcast des vollständigen Logbuchs verwendet DXLog.net `contactreplace` anstelle von `contactinfo`. KST4Contest verarbeitet beide Pakettypen. Damit können auch ältere QSOs übernommen werden, wenn der vollständige Broadcast ausgelöst wird, während KST4Contest läuft.
|
||||
|
||||
### Win-Test
|
||||
|
||||
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.
|
||||
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
|
||||
|
||||
#### QSO- und Worked-Synchronisation
|
||||
**Vorteile der Win-Test Integration:**
|
||||
- Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen.
|
||||
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern 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.
|
||||
|
||||
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)
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
## TRX-Frequenz-Synchronisation
|
||||
|
||||
@@ -191,16 +144,6 @@ Für DM5M-typische Setups (2 Radios, 2 Computer, eine KST4Contest-Instanz oder z
|
||||
|
||||
## Interne Datenbank
|
||||
|
||||
KST4Contest speichert Worked-, NOT-QRV- und Großfeldinformationen in einer eigenen SQLite-Datenbank. Sie ist von der Datenbank des Logprogramms unabhängig.
|
||||
KST4Contest speichert die Worked-Information in einer internen **SQLite-Datenbank**. Diese ist von der Logprogramm-Datenbank unabhängig und wird nur über den UDP-Broadcast befüllt.
|
||||
|
||||
Die Datenquellen liefern unterschiedlich genaue Informationen:
|
||||
|
||||
| Quelle | Rufzeichen global | Bandbezogen | Großfeld |
|
||||
|---|---:|---:|---:|
|
||||
| Simplelogfile | ja | nein | nein |
|
||||
| QSO-UDP-Listener | ja | ja, wenn im Paket enthalten | ja, wenn Band und Locator enthalten sind |
|
||||
| Win-Test-Netzwerk-Listener | ja | ja | ja, wenn ein Locator enthalten ist |
|
||||
|
||||
Die Daten werden beim Programmstart wieder geladen und bei neuen Logeinträgen während des Betriebs aktualisiert. Sie laufen nach drei Tagen automatisch ab. Ein Reset vor jedem Contest ist daher normalerweise nicht erforderlich.
|
||||
|
||||
Ein vollständiger manueller Reset entfernt Worked-Markierungen, NOT-QRV-Tags und Worked-Großfelder gemeinsam. Weitere Einzelheiten: [Worked Station Database Settings](de-Konfiguration#worked-station-database-settings-gearbeitete-stationen-datenbank).
|
||||
Vor jedem neuen Contest: Datenbank zurücksetzen! → [Konfiguration – Worked Station Database Settings](Konfiguration#worked-station-database-settings)
|
||||
|
||||
@@ -138,34 +138,18 @@ Wird durch die aktuelle Antennenrichtung in Worten ersetzt (z. B. `north`, `nort
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
| 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 |
|
||||
|
||||
`QRZNAME`, `FIRSTAP` und `SECONDAP` benötigen eine ausgewählte Station. In einem öffentlichen Beacon werden sie daher nicht aufgelöst.
|
||||
|
||||
Eine zweckmäßige Konfiguration ist beispielsweise:
|
||||
Alle Variablen können auch im **automatischen Beacon** (Intervall-Nachrichten) verwendet werden. Empfohlene Beacon-Konfiguration:
|
||||
|
||||
```
|
||||
calling cq at MYQRG, loc MYLOCATOR, GL all!
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
Da KST4Contest QRG-Daten automatisch aus Chat-Nachrichten ausliest: Wenn andere Stationen ebenfalls KST4Contest nutzen, sehen sie die eigene QRG sofort in der QRG-Spalte der Benutzerliste.
|
||||
|
||||
---
|
||||
|
||||
## Beispiel-Workflow mit Makros im Contest
|
||||
|
||||
1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt.
|
||||
|
||||
|
Before Width: | Height: | Size: 961 KiB |
@@ -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](en-Macros-and-Variables#variables)
|
||||
Details: [Macros and Variables](Macros-and-Variables#variables)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,169 +4,9 @@
|
||||
|
||||
Version history of KST4Contest / PraktiKST.
|
||||
|
||||
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: 10 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
|
||||
|
||||
- **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
|
||||
|
||||
- **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
|
||||
|
||||
- **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.
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
For the latest changelog, please refer to GitHub. The previous changelog is below.
|
||||
|
||||
## v1.40 (2026-02-16)
|
||||
**Major Feature Release: Score System, AP Timeline, Win-Test, PSTRotator**
|
||||
|
||||
@@ -25,22 +25,11 @@ Enter your own callsign and Maidenhead locator (6 characters, e.g., `JN49IJ`). T
|
||||
|
||||
### Active Bands
|
||||
|
||||
The **My station uses …** checkboxes define the bands available in the current local station setup. Supported choices are 50 MHz, 70 MHz, 144 MHz, 432 MHz, 1296 MHz, 2320 MHz, 3400 MHz, 5760 MHz and 10 GHz.
|
||||
|
||||
This selection controls more than the visible band columns. It is also used for:
|
||||
|
||||
- the per-band Worked and NOT-QRV filters,
|
||||
- the NOT-QRV controls visible in the **Further Info** panel,
|
||||
- the `a` and `B+` band-opportunity calculation,
|
||||
- the **New bands** filter,
|
||||
- the band-upgrade hint after a log entry, and
|
||||
- band-specific priority and Reachability functions.
|
||||
|
||||
After a change, click **Save Settings** and restart KST4Contest. Band columns and several related controls are created while the user interface is being built and are therefore not added or removed completely during the current session.
|
||||
Use the **"my station uses band"** checkboxes to select the active bands. Buttons and table rows will only appear in the user interface for selected bands. The software must be restarted after making changes.
|
||||
|
||||
### Antenna Beamwidth
|
||||
|
||||
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°.
|
||||
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°.
|
||||
|
||||
> **Do not** enter fantasy values – the direction calculations will become useless.
|
||||
|
||||
@@ -48,84 +37,34 @@ 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 connection details for the ON4KST chat server are shown at the top of the **Station** tab. They normally do not need to be changed.
|
||||
The chat server DNS and port are configurable in the Preferences:
|
||||
|
||||
| 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` |
|
||||
- **Server DNS**: Default `www.on4kst.org` (changed from `www.on4kst.info` in v1.31 hotfix).
|
||||
- **Port**: Default port of the ON4KST server.
|
||||
|
||||
`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.
|
||||
A change is only needed if the server moves or an alternative endpoint is used.
|
||||
|
||||
---
|
||||
|
||||
## Log Sync Settings
|
||||
|
||||
The **Log sync** tab selects the sources from which KST4Contest imports worked stations. The three input paths provide different levels of detail:
|
||||
Three methods are available for automatically marking worked stations. Details: [Log Synchronisation](en-Log-Sync).
|
||||
|
||||

|
||||
### Universal File Based Callsign Interpreter (Simplelogfile)
|
||||
|
||||
| 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 |
|
||||
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.
|
||||
|
||||
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.
|
||||
### Network Listener for Logger's QSO UDP Broadcast
|
||||
|
||||
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.
|
||||
**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.).
|
||||
|
||||
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.
|
||||
### Win-Test Network Listener (Additional UDP Listener)
|
||||
|
||||
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).
|
||||
A dedicated network listener for Win-Test. KST4Contest receives and processes Win-Test-specific UDP packets (including sked handovers) on the configured port.
|
||||
|
||||
---
|
||||
|
||||
@@ -148,76 +87,17 @@ Configuration of the interface to AirScout for aircraft scatter detection. Detai
|
||||
|
||||
## Notification Settings
|
||||
|
||||

|
||||
|
||||
Three notification types are available:
|
||||
|
||||
1. **Simple sounds**: TADA sound for incoming messages, tick for sked direction detection, etc.
|
||||
2. **CW announcement**: The callsign of a station sending a private message is output as a CW signal.
|
||||
3. **Phonetic announcement**: The callsign is pronounced phonetically.
|
||||
|
||||
### Fallback Band for Relative QRG Detection
|
||||
|
||||
The **Fallback band for relative QRG detection** dropdown selects the band used when a relative QRG cannot be assigned to a recent station-specific band context.
|
||||
|
||||
Only band prefixes supported by the frequency parser are available:
|
||||
|
||||
```text
|
||||
50 MHz
|
||||
70 MHz
|
||||
144 MHz
|
||||
432 MHz
|
||||
1296 MHz
|
||||
2320 MHz
|
||||
3400 MHz
|
||||
5760 MHz
|
||||
10368 MHz (10G)
|
||||
24048 MHz (24G)
|
||||
```
|
||||
|
||||
The dropdown is neither a filter nor an override for complete frequencies. `432.088` is recognised as a frequency in the 432 MHz band regardless of the selection. The fallback is needed for relative values such as `.205`, `,205` or `qrg 205`.
|
||||
|
||||
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.
|
||||
|
||||
### Band Upgrade Hint after a Log Entry
|
||||
|
||||
After receiving a log entry from UCXLog or Win-Test, KST4Contest can check whether the station which has just been worked still offers another common and unworked band.
|
||||
|
||||
The check uses the same derivation as the `a` and `B+` display:
|
||||
|
||||
1. the bands enabled in the local station settings,
|
||||
2. QRGs detected for the remote station during the previous 30 minutes,
|
||||
3. explicit band designators in the name fields of its active chat entries,
|
||||
4. stored per-band Worked marks, and
|
||||
5. manually assigned NOT-QRV marks.
|
||||
|
||||
Active chat variants of the same normalised callsign are evaluated together. NOT-QRV takes precedence over an automatically detected QRG or band designator.
|
||||
|
||||
If at least one common and unworked band remains, the main window displays a blinking **BAND+** hint for approximately twelve seconds. The callsign and remaining bands are included in the button text; the tooltip contains the complete derivation. If general notification sounds are enabled, KST4Contest also plays a short sound.
|
||||
|
||||
The two options serve different purposes:
|
||||
|
||||
- **Blink + sound …** enables the hint after a matching log entry.
|
||||
- **Priority boost …** additionally raises the score of stations which have already been worked on at least one band but still offer another common and unworked band.
|
||||
|
||||
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 [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).
|
||||
|
||||
|
||||
The hint requires a log-synchronisation source which provides band information. The file-based callsign interpreter sees callsigns only and cannot reliably identify the band of the QSO which has just been logged.
|
||||
|
||||
Further explanation: [Band Upgrade Hint after a Log Entry](en-Features#band-upgrade-hint-after-a-log-entry).
|
||||
|
||||
---
|
||||
|
||||
## 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](en-Macros-and-Variables#variables) can be used.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -255,44 +135,18 @@ New settings section with the following options:
|
||||
|
||||
## Win-Test Network Listener (from v1.31)
|
||||
|
||||
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:
|
||||
A dedicated listener for Win-Test-specific UDP packets. Enables:
|
||||
|
||||
- processing QSOs including band and locator information,
|
||||
- processing STATUS packets for the local QRG, and
|
||||
- handing skeds over to the Win-Test network.
|
||||
- **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.
|
||||
|
||||
### Log sync settings
|
||||
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.
|
||||
|
||||
| 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)
|
||||
> **Note**: The Win-Test listener is an **additional** listener – the standard QSO UDP broadcast listener on port 12060 remains independent.
|
||||
|
||||
---
|
||||
|
||||
@@ -320,37 +174,14 @@ Use case: Keep track of important stations (e.g. DX expeditions or trusted conte
|
||||
|
||||
---
|
||||
|
||||
## GUI Settings: Band-Column Hints
|
||||
|
||||
Two optional additions to the band columns can be enabled or disabled in the **GUI** tab:
|
||||
|
||||
- **Show "o" in band columns …** displays an `o` if the four-character grid square has already been worked on the relevant band. Disabling the option does not delete any database records; it only hides the additional indicator in the band columns. `wkdany` is unaffected.
|
||||
- **Show "a" in band columns …** distinguishes a completely new callsign from a band opportunity involving a callsign already worked elsewhere. When disabled, both cases are displayed as `B+`. The band-opportunity calculation itself remains unchanged.
|
||||
|
||||
Changes are reflected in the current user interface immediately. Click **Save Settings** afterwards if they should persist after the next start.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Worked Station Database Settings
|
||||
|
||||
The internal SQLite database stores contest-related state independently of the logging application's database:
|
||||
The internal worked database contains:
|
||||
|
||||
- the global Worked status of a callsign,
|
||||
- Worked status per band,
|
||||
- manually assigned NOT-QRV marks per band, and
|
||||
- worked four-character grid squares per band.
|
||||
- Worked status of all stations (per band)
|
||||
- NOT-QRV tags (since v1.2)
|
||||
|
||||
The normalised callsign, without visible chat brackets or category formatting, is used as the key. This allows active variants of the same callsign to be evaluated consistently.
|
||||
|
||||
Worked and NOT-QRV information expires automatically three days after its most recent change. Stored grid squares expire three days after the corresponding log entry. A manual reset before every contest is therefore normally unnecessary.
|
||||
|
||||
The **Reset worked, NOT-QRV and grid data...** button removes every Worked mark, NOT-QRV mark and stored worked grid square. A confirmation dialog is displayed first. Known callsign rows remain in the database; only the contest-related state is reset.
|
||||
|
||||
A reset is useful when you deliberately want to start with an empty contest state or have imported test data. It is not intended as a daily maintenance step.
|
||||
|
||||
Display and derivation: [Worked Callsigns, New Bands and New Grid Squares](en-Features#worked-callsigns-new-bands-and-new-grid-squares).
|
||||
**From v1.40**: Entries have an automatic lifetime of **3 days** – manually resetting before each contest is no longer strictly necessary. For a full reset, the **"Reinitialize"** button is still available.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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](en-Configuration#antenna-beamwidth)
|
||||
> Configuration: [Configuration – Antenna Beamwidth](Configuration#antenna-beamwidth)
|
||||
|
||||
---
|
||||
|
||||
@@ -48,92 +48,26 @@ Recognised formats: `144.205`, `432.088`, `.205` (with configured band assumptio
|
||||
|
||||
---
|
||||
|
||||
## Worked Callsigns, New Bands and New Grid Squares
|
||||
## Worked Marking
|
||||
|
||||
KST4Contest distinguishes between three pieces of information which may look similar during a contest but answer different questions:
|
||||
Worked stations are visually marked in the user list – per band. Based on [Log Synchronisation](en-Log-Sync) via UDP or Simplelogfile.
|
||||
|
||||
1. Has this callsign been worked before?
|
||||
2. Has this callsign been worked on a particular band?
|
||||
3. Has the four-character Maidenhead grid square already been worked, possibly with a different station?
|
||||
Reset the database before each contest: [Configuration – Worked Station Database Settings](Configuration#worked-station-database-settings).
|
||||
|
||||
This distinction matters. A callsign already worked on one band may still be useful on another. Conversely, a new callsign may be located in a grid square which is already in the log.
|
||||
---
|
||||
|
||||
### Worked information from the log
|
||||
## NOT-QRV Tags (from v1.2)
|
||||
|
||||
[Log Synchronisation](en-Log-Sync) imports new QSOs from the logging application. The amount of information available depends on the interface being used:
|
||||
|
||||
- The file-based Simplelogfile interpreter detects callsigns only. It can therefore set only the global Worked status.
|
||||
- The QSO UDP interfaces and the Win-Test network listener can also provide the band.
|
||||
- If the log packet contains a valid locator, KST4Contest additionally stores the worked four-character grid square for that band.
|
||||
|
||||
Missing information is not guessed. A QSO without a locator does not create a worked-grid record, and a Simplelogfile match does not create a band-specific Worked mark.
|
||||
|
||||
### Meaning of the band columns
|
||||
|
||||
The shared **worked** column contains only the bands enabled under **Station → My station uses …**. Its cells deliberately use short status codes because a full description would leave very little room for the actual user list.
|
||||
|
||||
| Display | Meaning |
|
||||
|---|---|
|
||||
| `X` | The callsign has been worked on this band. |
|
||||
| `a` | The station offers this band, the band has not been worked yet, and the callsign has not been worked on any band. |
|
||||
| `B+` | The station offers this band and it has not been worked yet. The callsign has already been worked on another band. If the separate `a` display is disabled, a completely new callsign is also shown as `B+`. |
|
||||
| `o` | The station's four-character grid square has already been worked on this band, regardless of callsign. |
|
||||
| empty | No matching information is available for this band. This does not mean that the station is not QRV. |
|
||||
|
||||
The `o` is an independent overlay and can therefore be combined with the other codes. Examples include `Xo`, `ao` and `B+o`. A single `o` means that the grid square has been worked on this band, while the displayed callsign has neither a Worked mark nor a current band opportunity.
|
||||
|
||||

|
||||
|
||||
### How is a band opportunity derived?
|
||||
|
||||
KST4Contest displays `a` or `B+` only if it can derive an open band opportunity. The calculation combines:
|
||||
|
||||
1. the bands enabled for the local station,
|
||||
2. QRGs detected for the remote station during the previous 30 minutes,
|
||||
3. explicit band designators in the remote station's name field,
|
||||
4. stored per-band Worked marks, and
|
||||
5. manually assigned NOT-QRV marks.
|
||||
|
||||
Active chat entries with the same normalised callsign are evaluated together. This is particularly relevant when the station appears in several chat categories or with different visible callsign variants. An explicit band designator in the name field remains useful while the corresponding chat entry is active. A band derived from a detected QRG expires after 30 minutes.
|
||||
|
||||
The remaining set contains only bands which are enabled locally, known for the remote station and not yet worked. A manual NOT-QRV mark overrides automatically detected evidence. The chat category alone is not sufficient evidence that an individual station is QRV on a particular band.
|
||||
|
||||
The global Worked mark does not decide whether a band opportunity exists. It merely selects `a` or `B+` for the display. The actual opportunity calculation uses per-band Worked information.
|
||||
|
||||
### Meaning of `wkdany`
|
||||
|
||||
The **wkdany** subcolumn combines the global callsign and grid-square status:
|
||||
|
||||
| Display | Meaning |
|
||||
|---|---|
|
||||
| empty | Neither the callsign nor the four-character grid square has been worked. |
|
||||
| `x` | The callsign has been worked on at least one band. |
|
||||
| `o` | The four-character grid square has been worked on at least one band. |
|
||||
| `xo` | Both the callsign and the grid square have been worked. |
|
||||
|
||||
`wkdany` is deliberately band-independent. The lower-case `x` must therefore not be confused with the upper-case `X` in a band column. The global status is used for the overview and the global **wkd** filter; it is not a substitute for per-band Worked information.
|
||||
|
||||
### NOT-QRV marks
|
||||
|
||||
If a station reports that it is not QRV on a particular band, mark this in the selected station's **Further Info** panel:
|
||||
When a station indicates it is not QRV on a specific band, this can be manually marked:
|
||||
|
||||
1. Select the station in the user list.
|
||||
2. Enable the relevant band under **Not QRV**.
|
||||
3. Use **tag not qrv all** only if the station should not be requested on any supported band.
|
||||
2. Right-click → Set NOT-QRV for the appropriate band.
|
||||
|
||||
The individual NOT-QRV controls are shown for the bands enabled at the local station. **tag not qrv all**, however, marks every supported band, including bands which are not currently visible in the user interface. The state is stored per band under the normalised callsign and propagated to its active chat variants.
|
||||
These tags are stored in the internal database and persist after a KST4Contest restart. Can be reset via the settings.
|
||||
|
||||

|
||||
**Benefit**: Prevents repeated sked requests on bands where the station is not active – saves time for both sides.
|
||||
|
||||
NOT-QRV is a manual correction and therefore takes precedence over detected QRGs and band designators in the name field. The affected band is no longer offered as `a` or `B+`, is not counted as an opportunity by the **New bands** filter and is excluded by the corresponding band filter.
|
||||
|
||||
In plain terms: an automatically detected hint means "probably active on this band". A manual NOT-QRV mark means "do not request this station on this band". The next detected number must not silently reverse that decision.
|
||||
|
||||
### Storage and lifetime
|
||||
|
||||
Worked, NOT-QRV and worked-grid information is stored in the internal SQLite database and restored on the next start. Entries expire automatically after three days, so a reset before every contest is normally unnecessary.
|
||||
|
||||
A manual reset under **Workedstn database** removes all Worked marks, NOT-QRV marks and stored worked grid squares. The known callsign rows remain in the database. See [Worked Station Database Settings](en-Configuration#worked-station-database-settings) for details.
|
||||
---
|
||||
|
||||
## Direction Filter
|
||||
|
||||
@@ -149,19 +83,9 @@ Hide stations beyond a maximum distance. The **"Show only QRB [km] <="** button
|
||||
|
||||
---
|
||||
|
||||
## Filters for Worked Status, New Bands and New Grid Squares
|
||||
## Worked and NOT-QRV Filter
|
||||
|
||||
The filters above the user list use the same information as the Worked columns:
|
||||
|
||||
- **wkd** hides callsigns which have been worked on at least one band.
|
||||
- The individual band buttons hide a station if the callsign has already been worked on that band or has manually been marked NOT QRV there.
|
||||
- **New bands** shows only stations for which at least one locally enabled and unworked band is known. It evaluates recent QRG detections and band designators in the name field; NOT-QRV takes precedence.
|
||||
- **Only new grids** shows only stations whose four-character grid square has not been worked on any band. Stations without a valid locator do not pass this filter.
|
||||
- **Grid color** does not filter the list. When enabled, it gives the QRA cell of an already worked grid square a slightly darker background. New grid squares retain the normal table colour.
|
||||
|
||||
Several active filters are applied together. A station remains visible only if it satisfies every selected condition. The filters react immediately to new log entries and changed NOT-QRV marks.
|
||||
|
||||
Operation and layout of the filter bar: [User Interface – Filters](en-User-Interface#filters).
|
||||
Toggle buttons (one per band) to hide already-worked stations and/or NOT-QRV-tagged stations. The filter takes effect **immediately** without manually reactivating (live since v1.22).
|
||||
|
||||
---
|
||||
|
||||
@@ -211,86 +135,19 @@ For selected stations in the user list, there are direct buttons to open the **Q
|
||||
|
||||
---
|
||||
|
||||
## Skeds and Sked Reminders
|
||||
## Sked Reminders with ALERT (from v1.40)
|
||||
|
||||
> Available from v1.40; band, callsign and Win-Test handling extended in Nightly / v1.42.
|
||||
A sked reminder service with automatic messages can be activated for each chat member. Configurable interval patterns:
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
KST4Contest therefore treats three tasks separately:
|
||||
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.
|
||||
|
||||
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)
|
||||
Activate from the FurtherInfo panel of the corresponding station.
|
||||
|
||||
---
|
||||
|
||||
## QSO Sniffer (from v1.31)
|
||||
|
||||
@@ -300,22 +157,17 @@ Configuration: [Configuration – Sniffer Settings](en-Configuration#sniffer-set
|
||||
|
||||
---
|
||||
|
||||
## Win-Test Integration
|
||||
## Win-Test Integration (from v1.31, fully configurable from v1.40)
|
||||
|
||||
KST4Contest uses a dedicated listener for the native Win-Test network protocol. It provides three separate functions:
|
||||
KST4Contest fully supports [Win-Test](https://www.win-test.com/) as a logging programme:
|
||||
|
||||
- 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.
|
||||
- **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.
|
||||
|
||||
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)
|
||||
Details: [Configuration – Win-Test Network Listener](en-Configuration#win-test-network-listener)
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator Interface (from v1.31, fully configurable from v1.40)
|
||||
|
||||
@@ -325,459 +177,59 @@ Configuration: [Configuration – PSTRotator Settings](en-Configuration#pstrotat
|
||||
|
||||
---
|
||||
|
||||
## Band Upgrade Hint after a Log Entry
|
||||
## Band Alert for New QSOs (from v1.40)
|
||||
|
||||
When UCXLog or Win-Test reports a new log entry with band information, KST4Contest checks whether the worked station still offers another common band.
|
||||
|
||||
The calculation follows the same rules as `a`, `B+` and the **New bands** filter: locally enabled bands, recent QRG detections, band designators in the name field, per-band Worked marks and NOT-QRV marks. Active chat variants of the same normalised callsign are evaluated together.
|
||||
|
||||
If at least one common and unworked band remains, a blinking hint appears for approximately twelve seconds. It includes the callsign and the remaining bands, for example `BAND+ DL0ABC 432, 1296`. Its tooltip also lists the enabled, worked and NOT-QRV bands used for the decision. If general notification sounds are enabled, KST4Contest also plays a short sound.
|
||||
|
||||
The Simplelogfile interpreter cannot trigger this hint reliably because it provides no band information for the QSO which has just been logged.
|
||||
|
||||
Configuration: [Band Upgrade Hint after a Log Entry](en-Configuration#band-upgrade-hint-after-a-log-entry).
|
||||
|
||||
Worked, NOT-QRV and worked-grid data expire automatically after three days. See [Worked Station Database Settings](en-Configuration#worked-station-database-settings) for the lifetime and manual reset behaviour.
|
||||
When a station is logged, KST4Contest automatically checks whether that station has shown any other active bands in the chat that you are also QRV on. If so, a **hint alert** appears so no multi-band opportunity is missed.
|
||||
|
||||
---
|
||||
|
||||
## Priority Score and Priority List (from v1.40)
|
||||
## Worked Tag Lifetime (from v1.40)
|
||||
|
||||
### Why is a score needed at all?
|
||||
|
||||
A conventional chat user list initially tells the operator only which stations are logged in. That is not enough during a contest. The operator must also consider which stations have not yet been worked, which bands may still be available, where the antenna is pointing, whether a suitable aircraft is approaching and whether an agreed sked is about to begin.
|
||||
|
||||
With a short list, much of this can still be handled mentally. As the contest continues, several bands are used and two chat categories are monitored at the same time, the same decision has to be reconstructed over and over again.
|
||||
|
||||
KST4Contest therefore combines the available information into a priority score. The score does not answer whether a QSO will definitely be possible. It supports the more practical question:
|
||||
|
||||
> Which of the currently visible stations should I examine next?
|
||||
|
||||
### When is a station excluded?
|
||||
|
||||
Before applying the weighted factors, KST4Contest checks whether a known band opportunity exists. All active chat entries belonging to the same normalised base callsign are evaluated together.
|
||||
|
||||
The calculation uses:
|
||||
|
||||
1. the bands enabled in the local station settings,
|
||||
2. QRGs detected for the remote station during the previous 30 minutes,
|
||||
3. explicit band designators in the name fields of its active chat entries,
|
||||
4. stored per-band Worked marks, and
|
||||
5. manually assigned NOT-QRV marks.
|
||||
|
||||
NOT QRV takes precedence over automatically detected frequencies and band designators.
|
||||
|
||||
If the remote station’s bands are known but none of them is both enabled locally and still available, the station receives a score of `0`. The same applies when every common band opportunity has already been worked.
|
||||
|
||||
A station is not excluded merely because all band information is missing. An unknown band opportunity is not the same as a known incompatibility. In this case, the station is removed from consideration only if all locally enabled bands have been manually marked NOT QRV for that station.
|
||||
|
||||
Stations with a score of `0` remain visible in the user list but are not included in the priority list.
|
||||
|
||||
### Which information raises or lowers the score?
|
||||
|
||||
The score combines several independent hints. One factor will therefore not normally determine the final position on its own.
|
||||
|
||||
| Factor | Effect on priority |
|
||||
|---|---|
|
||||
| Worked status | A callsign which has not been worked on any supported band receives a higher initial priority. A station which has already been worked is ranked lower but remains a candidate when another band opportunity is available. |
|
||||
| Available bands | Several common and unworked bands raise the priority. An additional boost for band-upgrade cases can be enabled separately. |
|
||||
| Distance | Distances below 200 km are weighted lower. The range between 200 km and the configured maximum QRB is preferred. Stations beyond the maximum QRB are reduced substantially. If the QRB is unavailable, this factor is omitted. |
|
||||
| Antenna direction | The score rises when the QTF to the station lies within half of the configured antenna beamwidth around the current local QTF. The closer both directions are, the stronger the effect. |
|
||||
| AirScout | At least one currently reachable aircraft raises the score. An expected AP opportunity in zero, one or two minutes receives an additional time-dependent weighting. |
|
||||
| Recent chat activity | A message received during the previous minute has a stronger effect than one received during the previous three minutes. Several incoming lines within the activity window raise the score further. |
|
||||
| Positive signals | Detected terms such as `QRV`, `READY`, `RGR`, `OK`, `TNX` or comparable configured text patterns are treated as a positive hint for several minutes. |
|
||||
| Reply behaviour | If another visible chat line from the station follows an outgoing `/cq` message quickly, the averaged reaction time raises the score. If no such line arrives before the configured timeout, a negative mark is added. |
|
||||
| Skeds | A scheduled contact initially adds a small amount of priority. During the final 15 minutes, its influence increases continuously. From three minutes before until one minute after the scheduled time, the sked receives very high priority. |
|
||||
| Failed attempt | **Sked fail** strongly reduces the station’s score until the mark is removed with **Reset fail** or KST4Contest is restarted. |
|
||||
|
||||
The default activity window for counting incoming messages is 180 seconds. A message received during the previous 60 seconds is evaluated separately as current activity. The default no-reply timeout is 13 minutes.
|
||||
|
||||
For reply behaviour, KST4Contest cannot prove that a later public or private line is actually a reply to the operator’s request. Any subsequent line received from the same station therefore ends the pending response-time measurement. This is a practical approximation, not a statistically reliable response rate.
|
||||
|
||||
### What does a scheduled contact mean for the score?
|
||||
|
||||
A sked is a time-dependent operating commitment. An imminent sked must therefore take precedence over most normal activity and distance hints. Without this weighting, a station which happens to be very active in the chat could displace an agreed contact from the priority list.
|
||||
|
||||
The strongest sked boost is deliberately limited to the period from three minutes before until one minute after the scheduled time. A sked further in the future remains relevant but should not yet dominate current operation.
|
||||
|
||||
The score is calculated for the normalised base callsign. A sked entered for an active variant such as `9A0BB-23` therefore affects the common score of the chat entries belonging to `9A0BB`.
|
||||
|
||||
### How are multiple suffixes and chat categories handled?
|
||||
|
||||
Active callsigns such as `9A0BB-2`, `9A0BB-70`, `9A0BB-23` and `9A0BB-13` remain separate chat members. Messages can therefore still be addressed to the complete callsign in the correct chat category.
|
||||
|
||||
Worked, band, NOT-QRV and score information belongs to the common base callsign `9A0BB`. The score is calculated once and projected to all active variants. The user list may consequently contain several separate rows with the same score, while the priority list contains only one entry for the base station.
|
||||
|
||||
KST4Contest uses the most recently suitable active login in the last relevant chat category as the concrete message target. Selecting a priority candidate then resolves the complete callsign, including its suffix and chat category.
|
||||
|
||||
### Updating and displaying the score
|
||||
|
||||
New messages, AirScout data, skeds, Worked information and manual NOT-QRV or Sked-fail changes request a new calculation immediately. The score is also refreshed periodically because activity, AP and sked information changes with time even when no new event is received.
|
||||
|
||||
A delay of a few seconds between an event and the visible new order is therefore normal.
|
||||
|
||||
The user interface displays the score in three places:
|
||||
|
||||
- the numerically sortable **Score** column in the user list,
|
||||
- the **Further Info** section for the selected station, and
|
||||
- the compact list of the two highest-ranked candidates, with a separate window containing up to 15 candidates.
|
||||
|
||||
Operation: [Priority List in the User Interface](en-User-Interface#priority-list).
|
||||
|
||||
### What does the score not tell you?
|
||||
|
||||
The numerical value is neither a success probability nor a signal prediction. A score which is twice as high does not mean that the QSO is twice as likely.
|
||||
|
||||
Among other things, the calculation does not know:
|
||||
|
||||
- the actual antenna direction of the remote station,
|
||||
- its current operating situation,
|
||||
- local interference,
|
||||
- short-term propagation changes,
|
||||
- terrain obstruction outside the connected assessment functions, or
|
||||
- whether a station which is active in the chat is currently sitting at the radio.
|
||||
|
||||
Known input data may also be outdated or ambiguous. A detected frequency, for example, proves only that the QRG recently appeared in connection with that station.
|
||||
|
||||
In practical terms, the score does not replace the operator’s decision. It prevents the information already available to KST4Contest from having to be reconstructed mentally for every candidate.
|
||||
|
||||
Related settings:
|
||||
|
||||
- [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)
|
||||
- [Band Upgrade Hint and Priority Boost](en-Configuration#band-upgrade-hint-after-a-log-entry)
|
||||
Worked stations are automatically removed from the database after **3 days**. Manually resetting the worked database before each contest is therefore no longer strictly necessary – the database keeps itself up to date.
|
||||
|
||||
---
|
||||
|
||||
## AP and Sked Timeline
|
||||
## Chatmember Score System / Priority List (from v1.40)
|
||||
|
||||
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:
|
||||
KST4Contest automatically calculates a **priority score** for each active chat member. The score is derived from:
|
||||
|
||||
- When is an interesting AP opportunity expected?
|
||||
- Which previously agreed sked is approaching independently of that opportunity?
|
||||
- Antenna direction of the remote station (is it pointing towards me?)
|
||||
- QRB (distance)
|
||||
- Activity time and message count
|
||||
- Active bands and frequencies
|
||||
- AP availability (AirScout)
|
||||
- Sked direction (degrees)
|
||||
- Sked success rate and skedfail markings
|
||||
|
||||
Events further in the future appear on the right. As time passes, they move left towards the current time.
|
||||
The top candidates are highlighted in a dedicated priority list, helping you not to miss the most important contacts during contest stress.
|
||||
|
||||

|
||||
Stations with a failed sked can be marked using the **Skedfail button** in the FurtherInfo panel – this temporarily lowers their score.
|
||||
|
||||
### 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.
|
||||
## AP Timeline (from v1.40)
|
||||
|
||||
The colour of an AP marker represents the reflection potential:
|
||||
A visual timeline shows up to 4 highly-scored stations per minute slot that should be workable via aircraft scatter. Prioritisation criteria:
|
||||
|
||||
| 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.
|
||||
- **Highest reflection potential** is preferred (not necessarily the fastest arrival).
|
||||
- Stations towards which your antenna is not pointing are shown **transparently**.
|
||||
|
||||
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](en-Configuration#beacon-settings).
|
||||
Automatic CQ messages in the public channel at a configurable interval. Recommended: use the `MYQRG` variable so the current frequency is always accurate. Details: [Configuration – Beacon Settings](Configuration#beacon-settings).
|
||||
|
||||
---
|
||||
|
||||
## Simplelogfile
|
||||
|
||||
File-based log evaluation using regex. Details: [Log Synchronisation](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.
|
||||
File-based log evaluation using regex. Details: [Log Synchronisation](Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
|
||||
|
||||
---
|
||||
|
||||
## Station Map and Path Analysis (from v1.41)
|
||||
## Cluster & QSO of Others
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Screen-Aware Main Window Sizing (from v1.41)
|
||||
|
||||
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.
|
||||
|
||||
KST4Contest therefore checks the stored size against the usable area of the primary screen during startup.
|
||||
|
||||
### How is the startup size determined?
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
This catches two different cases:
|
||||
|
||||
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.
|
||||
|
||||
### What happens to the layout?
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
@@ -1,129 +1,51 @@
|
||||
# KST4Contest – User Manual
|
||||
# KST4Contest – Wiki
|
||||
|
||||
> You are reading the English version | [Deutsche Version](de-Home)
|
||||
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Home)
|
||||
|
||||
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 brings chat, candidate selection, sked planning, aircraft scatter data and external station software together in a single operating interface.
|
||||
**KST4Contest** (also known as *PraktiKST*) is a Java-based chat client for the [ON4KST Chat](http://www.on4kst.info/chat/), specifically designed for contest operation on the VHF/UHF/SHF bands (144 MHz and above).
|
||||
|
||||
KST4Contest is developed by **DO5AMF (Marc Fröhlich)**, operator at DM5M and (since May 2025) **DN9APW (Philipp Wagner)**. The source code is publicly available on [GitHub](https://github.com/praktimarc/kst4contest).
|
||||
Developed by **DO5AMF (Marc Fröhlich)**, operator at DM5M.
|
||||
|
||||
---
|
||||
|
||||
## Why use a dedicated ON4KST client?
|
||||
|
||||
During a contest, the ON4KST Chat provides a considerable amount of information: active stations, locators, frequencies, sked requests and indications of current activity. The actual problem is not seeing this data. It is turning it into the next useful contact in time.
|
||||
|
||||
KST4Contest evaluates the available information, puts it into context and presents it as part of a contest-oriented workflow. This includes antenna direction, distance, known bands and frequencies, worked status, chat activity and aircraft scatter timing.
|
||||
|
||||
The program does not decide which QSO is actually possible. Priority scores, the AP timeline and visual highlights are decision aids. The final judgement remains with the operator – not least because even a very convincing computer screen cannot complete a radio contact.
|
||||
|
||||
---
|
||||
|
||||
## How KST4Contest supports contest operation
|
||||
|
||||
- **Observe and organise chat activity:** KST4Contest can display two ON4KST chat categories at the same time. Messages, frequency information and known band activity are assigned to the corresponding stations.
|
||||
|
||||
- **Reduce the station list:** Direction, distance, worked and NOT-QRV filters help limit the user list to stations that are relevant to the current operating situation.
|
||||
|
||||
- **Prioritise candidates:** The score system evaluates active chat members using several known criteria. The currently most relevant candidates are also shown in a separate priority list.
|
||||
|
||||
- **Prepare skeds and keep them visible:** Sked reminders, automatic advance messages and the AP timeline help prevent scheduled contacts from disappearing somewhere between chat, logging and ongoing CQ operation.
|
||||
|
||||
- **Include aircraft scatter information:** The AirScout interface brings suitable aircraft and expected reflection times into candidate evaluation and sked planning.
|
||||
|
||||
- **Connect the logger and station equipment:** KST4Contest synchronises worked stations and frequency information with supported logging software. It also provides interfaces for Win-Test, PSTRotator and a built-in DX Cluster server.
|
||||
|
||||
- **Display stations and radio paths:** The station map shows active chat members, locator squares, antenna directions and the path to the selected station. A terrain profile can also be calculated for selected paths.
|
||||
|
||||
These functions share information. A detected frequency can indicate an active band, worked status comes from the logger, aircraft scatter data adds timing information and the resulting score affects the priority list. Missing or outdated input data can therefore affect the result as well.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
A registered ON4KST account is required. Registration and login are available from the [official ON4KST login page](https://www.on4kst.org/chat/login.php).
|
||||
|
||||
English is the official language of the ON4KST Chat. This also applies when communicating with stations from your own country. Common amateur radio abbreviations such as `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` and `73` are normal and usually considerably faster than carefully written prose.
|
||||
|
||||
Downloads, supported operating systems and installation methods are described in [Installation](en-Installation).
|
||||
|
||||
---
|
||||
|
||||
## Manual version
|
||||
|
||||
This manual describes the current stable release of KST4Contest.
|
||||
|
||||
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 Stable, Beta and Nightly builds](https://kst4contest.hamradioonline.de/download/)
|
||||
- [GitHub releases](https://github.com/praktimarc/kst4contest/releases)
|
||||
- [Version history](en-Changelog)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Quick navigation
|
||||
## Quick Navigation
|
||||
|
||||
| Page | Contents |
|
||||
|---|---|
|
||||
| [Installation](en-Installation) | ON4KST account, downloads, installation and updates |
|
||||
| [Configuration](en-Configuration) | Login, station, bands, user interface and external connections |
|
||||
| [Log Synchronisation](en-Log-Sync) | Simplelogfile, UCXLog, N1MM+, QARTest, DXLog.net and Win-Test |
|
||||
| [AirScout Integration](en-AirScout-Integration) | Connecting AirScout and evaluating aircraft scatter timing |
|
||||
| [DX Cluster Server](en-DX-Cluster-Server) | Passing detected opportunities to logging software |
|
||||
| [Features](en-Features) | Operation, reasoning and limitations of individual functions |
|
||||
| [Macros and Variables](en-Macros-and-Variables) | Reusable messages, shortcuts and automatically substituted values |
|
||||
| [User Interface](en-User-Interface) | Interface layout and operation during a contest |
|
||||
| [Changelog](en-Changelog) | Releases, Nightly changes and resolved issues |
|
||||
| [Installation](en-Installation) | Download, Java requirements, update |
|
||||
| [Configuration](en-Configuration) | All settings in detail |
|
||||
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
|
||||
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
|
||||
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
|
||||
| [Features](en-Features) | All features at a glance |
|
||||
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
|
||||
| [User Interface](en-User-Interface) | UI explanation and operation |
|
||||
| [Changelog](en-Changelog) | Version history |
|
||||
|
||||
---
|
||||
|
||||
## Contact and support
|
||||
## What is KST4Contest?
|
||||
|
||||
- **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
|
||||
Please use this address only for KST4Contest-related topics.
|
||||
The ON4KST Chat is the de-facto standard for skeds on the 144 MHz and higher bands. KST4Contest enhances the chat experience with contest-specific features:
|
||||
|
||||
### Reporting a bug
|
||||
- **Worked marking**: Stations already worked are highlighted visually, synchronised directly from your logging software via UDP.
|
||||
- **Sked direction detection**: When a station calls another one from your direction, it is highlighted green and bold.
|
||||
- **QRG detection**: KST4Contest automatically reads frequencies from the chat traffic and shows them in the user list.
|
||||
- **AirScout interface**: Reflectable aircraft are shown directly in the user list.
|
||||
- **Built-in DX cluster server**: Spots are sent directly to your logging software.
|
||||
- **Dark mode** (from v1.26): Easy on the eyes during night-time operation.
|
||||
- **Multi-channel login** (from v1.26): Simultaneously logged into two chat categories.
|
||||
|
||||
A problem is considerably easier to reproduce when the report contains at least:
|
||||
---
|
||||
|
||||
1. the KST4Contest version,
|
||||
2. the operating system and installation method,
|
||||
3. the exact steps leading to the problem,
|
||||
4. the expected and observed behaviour,
|
||||
5. a screenshot where appropriate,
|
||||
6. the error log file.
|
||||
## Contact & Support
|
||||
|
||||
The error log is stored at:
|
||||
|
||||
| Operating system | Path |
|
||||
|---|---|
|
||||
| Linux / macOS | `~/.praktiKST/kst4contest-errors.log` |
|
||||
| Windows | `C:\Users\<YourName>\.praktiKST\kst4contest-errors.log` |
|
||||
|
||||
Please inspect the file briefly before uploading it. An error log mainly contains technical information, but depending on the problem it may also include local file paths or other contextual data.
|
||||
|
||||
Some file and directory names still use the technical name `praktiKST`. They refer to the same program.
|
||||
- **Email**: praktimarc+kst4contest@gmail.com *(for kst4contest topics only)*
|
||||
- **GitHub**: https://github.com/praktimarc/kst4contest
|
||||
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
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) 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.
|
||||
Special thanks to: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX cluster idea), DC9DJ (Konrad Neitzel, project structure), DO5ALF (Andreas, webmaster funkerportal.de), PE0WGA (Franz van Velzen, tester) and all other testers and contributors.
|
||||
|
||||
@@ -2,422 +2,140 @@
|
||||
|
||||
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Installation)
|
||||
|
||||
KST4Contest is distributed as a ready-to-run application package for Windows, Linux and macOS. The official release packages do not require a separate Java installation: the required Java runtime is already included.
|
||||
## Prerequisites
|
||||
|
||||
In practical terms, you need a supported operating system, an internet connection and an ON4KST account. That sounds manageable—and usually it is.
|
||||
An resolution of 1200px by 720px is recommended
|
||||
|
||||
## Requirements
|
||||
### ON4KST Account
|
||||
|
||||
### ON4KST account
|
||||
To use the chat, a registered account with the ON4KST chat service is required:
|
||||
|
||||
KST4Contest is an independent client for the ON4KST chat, but it does not replace the corresponding user account.
|
||||
- Register at: http://www.on4kst.info/chat/register.php
|
||||
|
||||
If you do not have an account yet, you can create one on the official ON4KST website:
|
||||
### Chat Etiquette
|
||||
|
||||
- [ON4KST login and registration](https://www.on4kst.org/chat/login.php)
|
||||
The official language in the ON4KST Chat is **English**. Please use English even when communicating with stations from your own country. Common HAM abbreviations (agn, dir, pse, rrr, tnx, 73 …) are widely used and understood.
|
||||
|
||||
ON4KST specifies English as the common language for the chat. This also applies when both stations happen to share another language. During contests, the usual amateur radio abbreviations such as `pse`, `agn`, `qrg`, `dir`, `rrr`, `tnx` and `73` are widely used.
|
||||
### Personal Messages
|
||||
|
||||
Using the chat and sending personal messages are covered in the user interface chapter. For the installation, the only thing that matters at this point is that the account works.
|
||||
To send a private message to another station, always use the following format:
|
||||
|
||||
### Screen size
|
||||
```
|
||||
/CQ CALLSIGN message text
|
||||
```
|
||||
|
||||
A usable screen area of approximately **1200 × 720 pixels** or more is recommended.
|
||||
Example: `/CQ DL5ASG pse sked 144.205?`
|
||||
|
||||
KST4Contest automatically adapts the main window to the available area of the primary screen. The application can therefore also be started on smaller displays. Less space is still less space, however: not all tables, filters and additional information can then be displayed at a useful size at the same time.
|
||||
|
||||
### Java
|
||||
|
||||
The ready-made packages from the [GitHub Releases](https://github.com/praktimarc/kst4contest/releases/latest) do not require a separate Java installation. The required runtime is distributed with KST4Contest.
|
||||
|
||||
A Java development environment is only required if you want to build KST4Contest from source. For the AUR packages `kst4contest` and `kst4contest-git`, Java 21 and Maven are handled as build dependencies by the package manager.
|
||||
|
||||
---
|
||||
|
||||
## Stable, Beta or Nightly?
|
||||
|
||||
KST4Contest is distributed through three development channels:
|
||||
|
||||
| Channel | Intended use | Classification |
|
||||
|---|---|---|
|
||||
| **Stable** | Normal contest operation | Published version intended for regular use |
|
||||
| **Beta** | Focused testing before a Stable release | Pre-release version with a largely defined feature set |
|
||||
| **Nightly** | Early testing of new features | Current development state from the `main` branch |
|
||||
|
||||
The **Stable version** is recommended for normal operation.
|
||||
|
||||
Beta and Nightly builds may contain features that are not yet available in the Stable release. They may also contain unfinished workflows, changed settings or new defects. That is not an unusual failure of the release process; it is the purpose of a development channel.
|
||||
|
||||
If a function in this manual is explicitly marked as **Nightly**, it is not necessarily part of the current Stable release yet.
|
||||
During heavy chat traffic (5–6 messages per second in a contest), public messages directed at a specific callsign are easily missed. However, KST4Contest also catches such messages if they are accidentally posted publicly (see [Features – PM Catching](Features#catching-personal-messages)).
|
||||
|
||||
---
|
||||
|
||||
## Download
|
||||
|
||||
The current Stable version is available here:
|
||||
### Windows
|
||||
|
||||
- [Latest KST4Contest release](https://github.com/praktimarc/kst4contest/releases/latest)
|
||||
- [All published releases](https://github.com/praktimarc/kst4contest/releases)
|
||||
The latest version can be downloaded as a ZIP file:
|
||||
|
||||
The release page contains the application packages as well as the English and German PDF manuals.
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
### Which package do I need?
|
||||
The filename has the format `praktiKST-v<version_number>-windows-x64.zip`.
|
||||
|
||||
| Operating system | Package | Typical filename |
|
||||
|---|---|---|
|
||||
| Windows x64 | ZIP package | `praktiKST-v<version>-windows-x64.zip` |
|
||||
| Linux x86_64 | AppImage | `KST4Contest-v<version>-linux-x86_64.AppImage` |
|
||||
| Debian/Ubuntu amd64 | DEB package | `KST4Contest-v<version>-debian-amd64.deb` |
|
||||
| Fedora/RPM x86_64 | RPM package | `KST4Contest-v<version>-fedora-x86_64.rpm` |
|
||||
| Arch Linux x86_64 | Arch package | `KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst` |
|
||||
| Linux with Flatpak | Flatpak reference | `de.x08.KST4Contest.flatpakref` |
|
||||
| macOS Apple Silicon | DMG for ARM64 | `KST4Contest-v<version>-macos-arm64.dmg` |
|
||||
| macOS Intel | DMG for x86_64 | `KST4Contest-v<version>-macos-x86_64.dmg` |
|
||||
### Linux
|
||||
|
||||
The latest version can be downloaded as an AppImage:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
The filename has the format `KST4Contest-v<version_number>-linux-x86_64.AppImage`.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort Support:** macOS builds are provided as a convenience but are **not fully tested**. We build and release macOS binaries with every release, but we cannot test every scenario on macOS. If you encounter issues, please report them – we will do our best to address them, but cannot guarantee the same level of support as for Windows and Linux.
|
||||
|
||||
The latest version can be downloaded as a DMG disk image (available for both Apple Silicon and Intel Macs):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
The filename has the format `KST4Contest-v<version_number>-macos-<arch>.dmg`, where `<arch>` is `arm64` (Apple Silicon) or `x86_64` (Intel).
|
||||
|
||||
Only download application packages from the official GitHub Releases, the KST4Contest Flatpak repository or the linked AUR packages. Files from other sources may have been built differently, may be outdated or may have been modified.
|
||||
|
||||
---
|
||||
|
||||
## Installing on Windows
|
||||
## Installation
|
||||
|
||||
KST4Contest is distributed as a ZIP package for Windows. A conventional installer is not required.
|
||||
### Windows
|
||||
|
||||
1. Download `praktiKST-v<version>-windows-x64.zip` from the latest release.
|
||||
2. Extract the ZIP file completely into a dedicated folder.
|
||||
3. Open the extracted folder.
|
||||
4. Start `praktiKST.exe`.
|
||||
1. Download the ZIP file.
|
||||
2. Unzip the ZIP file into a folder of your choice.
|
||||
3. Run `praktiKST.exe`.
|
||||
|
||||
Do not start the application directly from the compressed ZIP file. KST4Contest consists of several files and a bundled runtime. Windows can only use this structure reliably after the archive has been extracted completely.
|
||||
Settings are stored at `%USERPROFILE%\.praktikst\preferences.xml`.
|
||||
|
||||
The application settings are not stored in the extracted program directory. They are stored in your user profile:
|
||||
### Linux
|
||||
1. Download the AppImage.
|
||||
2. Unzip the AppImage into a folder of your choice.
|
||||
3. Make the AppImage executable (in the terminal with `chmod +x KST4Contest-v<version_number>-linux-x86_64.AppImage`)
|
||||
4. Run the AppImage.
|
||||
|
||||
```text
|
||||
%USERPROFILE%\.praktiKST\preferences.xml
|
||||
```
|
||||
Settings are stored at `~/.praktikst/preferences.xml`.
|
||||
|
||||
This allows a new application version to be extracted into a different directory without losing the existing settings.
|
||||
|
||||
---
|
||||
|
||||
## Installing on Linux
|
||||
|
||||
Several Linux package formats are available. The appropriate choice depends less on KST4Contest itself than on your distribution and the way you prefer to manage updates.
|
||||
|
||||
| Installation method | Useful when … |
|
||||
|---|---|
|
||||
| **Flatpak** | updates should be managed centrally and Flatpak is already in use |
|
||||
| **AppImage** | KST4Contest should run as a portable file without package installation |
|
||||
| **DEB/RPM** | the distribution’s native package manager should be used |
|
||||
| **Arch package/AUR** | Arch Linux, Manjaro or EndeavourOS is being used |
|
||||
|
||||
### Flatpak
|
||||
|
||||
For most Linux users, Flatpak is the simplest way to keep KST4Contest installed and manageable through the system’s update tools. The KST4Contest repository is GPG-signed and contains the Stable, Beta and Nightly channels.
|
||||
|
||||
#### Installing Stable from the release file
|
||||
|
||||
Download `de.x08.KST4Contest.flatpakref` from the latest release and open it with the software manager provided by your desktop environment.
|
||||
|
||||
Alternatively, install it from a terminal:
|
||||
|
||||
```bash
|
||||
flatpak install ./de.x08.KST4Contest.flatpakref
|
||||
```
|
||||
|
||||
#### Adding the KST4Contest repository
|
||||
|
||||
The repository only needs to be added once:
|
||||
|
||||
```bash
|
||||
flatpak remote-add --if-not-exists kst4contest \
|
||||
https://praktimarc.github.io/kst4contest/kst4contest.flatpakrepo
|
||||
```
|
||||
|
||||
You can then install the Stable version:
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//stable
|
||||
```
|
||||
|
||||
#### Installing Beta
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//beta
|
||||
```
|
||||
|
||||
#### Installing Nightly
|
||||
|
||||
```bash
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
All three KST4Contest channels use the same application ID. Installing multiple KST4Contest channels in parallel is therefore not supported.
|
||||
|
||||
For example, to switch from Stable to Nightly:
|
||||
|
||||
```bash
|
||||
flatpak uninstall de.x08.KST4Contest//stable
|
||||
flatpak install kst4contest de.x08.KST4Contest//nightly
|
||||
```
|
||||
|
||||
This does not automatically remove the personal settings stored in `~/.praktiKST`.
|
||||
|
||||
Installed Flatpak applications can be updated with:
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
Depending on the desktop environment, the graphical software manager may also display or automatically install available Flatpak updates. The `flatpak update` command itself performs an update; it does not promise to turn up and run by itself one day.
|
||||
|
||||
### AppImage
|
||||
|
||||
The AppImage does not require a conventional installation.
|
||||
|
||||
1. Download `KST4Contest-v<version>-linux-x86_64.AppImage`.
|
||||
2. Open a terminal in the download directory.
|
||||
3. Make the file executable:
|
||||
|
||||
```bash
|
||||
chmod +x KST4Contest-v<version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
4. Start KST4Contest:
|
||||
|
||||
```bash
|
||||
./KST4Contest-v<version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
The AppImage can then be moved to another location, such as `~/Applications`.
|
||||
|
||||
### Debian and Ubuntu
|
||||
|
||||
Install the DEB package with:
|
||||
|
||||
```bash
|
||||
sudo apt install ./KST4Contest-v<version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
Alternatively, open the file with a graphical package manager.
|
||||
|
||||
### Fedora and compatible RPM systems
|
||||
|
||||
Install the RPM package with:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./KST4Contest-v<version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
### Arch Linux: installing the release package
|
||||
|
||||
The Arch package downloaded from the GitHub Release can be installed directly:
|
||||
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
### Arch Linux: installing from the AUR
|
||||
|
||||
Three variants are available in the AUR:
|
||||
|
||||
| Package | Content |
|
||||
|---|---|
|
||||
| [`kst4contest-bin`](https://aur.archlinux.org/packages/kst4contest-bin) | Pre-built package from the current Stable release |
|
||||
| [`kst4contest`](https://aur.archlinux.org/packages/kst4contest) | Stable release built locally from source |
|
||||
| [`kst4contest-git`](https://aur.archlinux.org/packages/kst4contest-git) | Current development state from the `main` branch |
|
||||
|
||||
For most users, `kst4contest-bin` is the straightforward option:
|
||||
|
||||
```bash
|
||||
yay -S kst4contest-bin
|
||||
```
|
||||
|
||||
To build the Stable release from source:
|
||||
|
||||
```bash
|
||||
yay -S kst4contest
|
||||
```
|
||||
|
||||
To build the current development state:
|
||||
|
||||
```bash
|
||||
yay -S kst4contest-git
|
||||
```
|
||||
|
||||
All three packages provide the same application and are therefore defined as conflicting with one another. Install only one variant at a time.
|
||||
|
||||
AUR updates are included when the selected AUR helper checks for package updates, for example:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
They do not happen automatically merely because the package came from the AUR.
|
||||
|
||||
---
|
||||
|
||||
## Installing on macOS
|
||||
|
||||
> **Best-effort support:** The macOS packages are built together with the other releases, but they are not tested to the same extent as the Windows and Linux versions. Feedback is welcome; fully tested support for every macOS version and hardware variant cannot currently be guaranteed.
|
||||
|
||||
Apple Silicon Macs require the package marked `arm64`. Intel Macs require the package marked `x86_64`.
|
||||
|
||||
1. Download the appropriate DMG file.
|
||||
### macOS
|
||||
1. Download the DMG file for your architecture (Apple Silicon or Intel).
|
||||
2. Open the DMG file.
|
||||
3. Drag `KST4Contest.app` into the **Applications** folder.
|
||||
4. Start KST4Contest from the Applications folder or Launchpad.
|
||||
3. Drag `KST4Contest.app` into your **Applications** folder.
|
||||
4. On first launch, macOS may show a warning because the app is not notarised. To open it:
|
||||
- Right-click (or Control-click) on `KST4Contest.app` in Finder and choose **Open**.
|
||||
- Alternatively, go to **System Settings → Privacy & Security** and click **Open Anyway**.
|
||||
5. Run KST4Contest from your Applications folder or Launchpad.
|
||||
|
||||
The application is not currently notarized by Apple. macOS may therefore block the first launch.
|
||||
|
||||
If the application came from the official GitHub Release:
|
||||
|
||||
1. Open the Applications folder in Finder.
|
||||
2. Right-click or Control-click `KST4Contest.app`.
|
||||
3. Select **Open**.
|
||||
4. Confirm the launch in the dialog that appears.
|
||||
|
||||
Alternatively, macOS may provide an **Open Anyway** button under **System Settings → Privacy & Security**.
|
||||
Settings are stored at `~/.praktikst/preferences.xml`.
|
||||
|
||||
---
|
||||
|
||||
## Where are the settings stored?
|
||||
## Updating
|
||||
|
||||
KST4Contest stores its settings and other local working files in the user’s home directory. The application directory and the data directory are separate.
|
||||
KST4Contest includes an **automatic update notification service**: as soon as a new version is available, a window will appear at startup with:
|
||||
- information that a new version is available,
|
||||
- a changelog,
|
||||
- the download link for the new version.
|
||||
|
||||
| Operating system | Data directory | Settings file |
|
||||
|---|---|---|
|
||||
| Windows | `%USERPROFILE%\.praktiKST\` | `%USERPROFILE%\.praktiKST\preferences.xml` |
|
||||
| Linux | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||
| macOS | `~/.praktiKST/` | `~/.praktiKST/preferences.xml` |
|
||||

|
||||
|
||||
Note the spelling `.praktiKST` with an uppercase `KST`. Linux and macOS distinguish between uppercase and lowercase letters. `.praktikst` would simply be a different directory.
|
||||
### Update Process
|
||||
|
||||
#### Windows
|
||||
|
||||
Currently, there is only one way to update:
|
||||
|
||||
1. Delete the old folder.
|
||||
2. Unzip the new ZIP file.
|
||||
|
||||
The settings file (`preferences.xml`) is preserved because it is stored in the user folder, not the program folder.
|
||||
|
||||
#### Linux
|
||||
|
||||
Currently as follows:
|
||||
1. Download the new AppImage
|
||||
2. Mark the new AppImage as executable
|
||||
3. (optional) Delete the old AppImage.
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Download the new DMG file.
|
||||
2. Open the DMG.
|
||||
3. Drag the new `KST4Contest.app` into your **Applications** folder, replacing the old version.
|
||||
|
||||
An application update does not remove this directory. Even so, creating a backup before a major version change or extensive configuration work is sensible.
|
||||
|
||||
---
|
||||
|
||||
## Updates
|
||||
## Known Issues at Startup
|
||||
|
||||
When KST4Contest starts, it checks whether a newer Stable release is available. If it finds one, it displays an information window containing:
|
||||
### Norton 360
|
||||
|
||||
- the installed version,
|
||||
- the latest Stable version,
|
||||
- a short overview of the main changes,
|
||||
- the changelog,
|
||||
- known issues,
|
||||
- a link to the corresponding GitHub Release page.
|
||||
Norton 360 classifies `praktiKST.exe` as dangerous (false positive). An exception must be created for the file:
|
||||
|
||||

|
||||
1. Open Norton 360.
|
||||
2. Security → History → Find the corresponding event.
|
||||
3. Select "Restore & Add Exception".
|
||||
|
||||
The update checker does not install anything. It reports the new version and opens the platform-neutral release page. From there, you must select the appropriate package for Windows, Linux or macOS.
|
||||
|
||||
### Updating Windows
|
||||
|
||||
1. Close KST4Contest.
|
||||
2. Download the new Windows ZIP package.
|
||||
3. Extract it into a new or empty directory.
|
||||
4. Start the new version.
|
||||
5. Check that the existing settings have been loaded.
|
||||
6. Remove the old application directory only after that.
|
||||
|
||||
The settings are preserved because they are stored under `%USERPROFILE%\.praktiKST`, not in the application directory.
|
||||
|
||||
### Updating an AppImage
|
||||
|
||||
1. Download the new AppImage.
|
||||
2. Make it executable.
|
||||
3. Start the new file.
|
||||
4. Remove the previous AppImage only after the new version works.
|
||||
|
||||
### Updating Debian and Ubuntu
|
||||
|
||||
```bash
|
||||
sudo apt install ./KST4Contest-v<version>-debian-amd64.deb
|
||||
```
|
||||
|
||||
### Updating Fedora
|
||||
|
||||
```bash
|
||||
sudo dnf upgrade ./KST4Contest-v<version>-fedora-x86_64.rpm
|
||||
```
|
||||
|
||||
### Updating the Arch package
|
||||
|
||||
```bash
|
||||
sudo pacman -U KST4Contest-v<version>-archlinux-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
### Updating an AUR package
|
||||
|
||||
Update the installed package through the selected AUR helper, for example:
|
||||
|
||||
```bash
|
||||
yay -Syu
|
||||
```
|
||||
|
||||
### Updating Flatpak
|
||||
|
||||
```bash
|
||||
flatpak update de.x08.KST4Contest
|
||||
```
|
||||
|
||||
### Updating macOS
|
||||
|
||||
1. Download the new DMG file for the appropriate architecture.
|
||||
2. Close KST4Contest.
|
||||
3. Open the DMG file.
|
||||
4. Replace `KST4Contest.app` in the Applications folder.
|
||||
5. Start the new version and check the existing settings.
|
||||
|
||||
The configuration under `~/.praktiKST` is not affected.
|
||||
|
||||
---
|
||||
|
||||
## Problems during the first launch
|
||||
|
||||
### Windows reports an unknown application
|
||||
|
||||
KST4Contest is not currently signed with a commercial Windows code-signing certificate. Windows or an additional security product may therefore warn about an unknown or rarely downloaded application.
|
||||
|
||||
Check the following first:
|
||||
|
||||
- Did the file come from the [official KST4Contest release](https://github.com/praktimarc/kst4contest/releases/latest)?
|
||||
- Does the filename match the published release?
|
||||
- Was the file downloaded completely?
|
||||
- Does the security product report a specific detection name or only a general reputation warning?
|
||||
|
||||
A warning alone is neither reliable proof of malware nor automatically a false positive. If the origin of the file is unclear, do not run it or restore it from quarantine.
|
||||
|
||||
Some users have reported quarantine messages from Norton 360 in particular. If the warning can be reproduced, please create a [GitHub issue](https://github.com/praktimarc/kst4contest/issues) and include:
|
||||
|
||||
- the KST4Contest version,
|
||||
- the complete filename,
|
||||
- the security product and its version,
|
||||
- the reported detection name,
|
||||
- a screenshot of the warning, if possible.
|
||||
|
||||
### The AppImage does not start
|
||||
|
||||
Check the executable permission first:
|
||||
|
||||
```bash
|
||||
chmod +x KST4Contest-v<version>-linux-x86_64.AppImage
|
||||
```
|
||||
|
||||
Then start the file from a terminal. Error messages shown there are usually more useful than a double-click that simply produces no visible result.
|
||||
|
||||
### 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.
|
||||
|
||||
### The problem remains
|
||||
|
||||
First check whether the problem has already been reported under [GitHub Issues](https://github.com/praktimarc/kst4contest/issues). If it has not, create a new issue containing:
|
||||
|
||||
- the operating system and version,
|
||||
- the KST4Contest version,
|
||||
- the installation method,
|
||||
- the exact error message,
|
||||
- the steps required to reproduce the problem.
|
||||
|
||||
“It does not work” usually describes the situation accurately, but it is of limited value during diagnosis.
|
||||
*(Reported by PE0WGA, Franz van Velzen – thank you!)*
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Log-Synchronisation)
|
||||
|
||||
KST4Contest imports worked stations from the logging application and derives the global Worked status, per-band Worked marks and – where a locator is available – worked grid squares. Three input paths are available: the file-based Simplelogfile interpreter, the general QSO UDP listener and the dedicated Win-Test network listener.
|
||||
KST4Contest automatically marks worked stations in the chat user list. Two basic methods are available:
|
||||
|
||||
---
|
||||
|
||||
@@ -10,25 +10,24 @@ KST4Contest imports worked stations from the logging application and derives the
|
||||
|
||||
## Method 1: Universal File Based Callsign Interpreter (Simplelogfile)
|
||||
|
||||
KST4Contest reads a log file and searches it for callsigns using a configurable regular expression. The file is read only and is never modified. Binary log files can also be used; content which cannot be interpreted as text is skipped.
|
||||
KST4Contest reads a log file and searches for callsign patterns using a regular expression. Binary log files are also supported – unreadable binary content is simply ignored.
|
||||
|
||||
The advantage is broad compatibility: no dedicated network interface is required from the logging application.
|
||||
**Advantage**: Works with almost any logging program that writes a file.
|
||||
**Disadvantage**: No band information available – stations are only marked as "worked", not on which band.
|
||||
|
||||
The limitation is equally clear. A callsign match alone provides neither a reliable band nor a locator. The Simplelogfile interpreter can therefore set only the global Worked status. It does not create a per-band `X`, a worked-grid record or a reliable basis for the band-upgrade hint after a log entry.
|
||||
Enter the path to the log file in the Preferences. The file is only read, never modified (read-only).
|
||||
|
||||
Configure the log-file path and regular expression in the **Log sync** tab. Use one of the network interfaces where possible if band-specific information is required.
|
||||
> **Tip**: The Simplelogfile function can also be used to mark stations that are definitely unreachable (e.g. personal notes). This will be replaced in a later version by a better tagging system.
|
||||
|
||||
---
|
||||
|
||||
# Method 2: Network Listener for QSO UDP Packets – Recommended
|
||||
## Method 2: Network Listener (UDP Broadcast) – Recommended
|
||||
|
||||
UCXLog, QARTest, N1MM+ and DXLog.net can transmit a UDP packet when a QSO is saved. KST4Contest receives these packets on port `12060` by default and imports the callsign together with any band and locator information they contain.
|
||||
When saving a QSO, the logging software sends a UDP packet to the broadcast address of the home network. KST4Contest receives this packet and marks the station including **band information** in its internal SQLite database.
|
||||
|
||||
If a band is available, the callsign is marked as worked on that band. If the packet also contains a valid locator, KST4Contest stores its four-character grid square for that band. Missing information is not inferred from unrelated fields.
|
||||
> **Important**: KST4Contest must be **running in parallel with the logging software**. QSOs logged while KST4Contest is not running will not be captured – except with QARTest (which can send the complete log).
|
||||
|
||||
KST4Contest must be running when the packet is transmitted. Some logging applications can, however, resend an existing log: QARTest provides **Invia log completo**, while DXLog.net sends `contactreplace` packets when broadcasting the complete log. KST4Contest processes both mechanisms.
|
||||
|
||||
**Default port:** `12060`
|
||||
**Default UDP port**: 12060 (matches the default of most logging programs)
|
||||
|
||||
---
|
||||
|
||||
@@ -82,81 +81,34 @@ For the built-in DX cluster server: configure N1MM+ as a DX cluster client (serv
|
||||
- Enter the IP of the KST4Contest computer (green-highlighted fields)
|
||||
- Port: 12060
|
||||
|
||||
When broadcasting the complete logbook, DXLog.net uses `contactreplace` instead of `contactinfo`. KST4Contest processes both packet types. Older QSOs can therefore be imported by starting a complete-log broadcast while KST4Contest is running.
|
||||
|
||||
### Win-Test
|
||||
|
||||
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`.
|
||||
Win-Test is supported with a dedicated UDP network listener that understands the native Win-Test network protocol.
|
||||
|
||||
#### QSO and Worked synchronisation
|
||||
**Advantages of Win-Test Integration:**
|
||||
- Automatic QSO synchronization to mark worked stations.
|
||||
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet* – 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.
|
||||
|
||||
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 the "TRX Synchronisation" tab:**
|
||||
- `Win-Test STATUS QRG Sync`: When enabled, KST4Contest takes the current transceiver frequency from the Win-Test STATUS packet and uses it as your own QRG (MYQRG).
|
||||
- `Use pass frequency from Win-Test STATUS`: Instead of the main TRX frequency, the pass frequency contained in the STATUS packet is used as MYQRG (useful for multi-op setups that operate with a dedicated pass QRG).
|
||||
- `Win-Test station name filter`: If a name is entered here (e.g. "STN1"), KST4Contest only processes packets from that specific Win-Test instance. Leave empty to accept all.
|
||||
|
||||
**Settings in Win-Test:**
|
||||
- The network in Win-Test must be active.
|
||||
- Win-Test must be configured to send/receive its broadcasts on the corresponding port (default 9871).
|
||||
|
||||
---
|
||||
|
||||
## TRX Frequency Synchronisation
|
||||
|
||||
@@ -192,16 +144,6 @@ For DM5M-style setups (2 radios, 2 computers, one KST4Contest instance or two se
|
||||
|
||||
## Internal Database
|
||||
|
||||
KST4Contest stores Worked, NOT-QRV and worked-grid information in its own SQLite database. This database is independent of the logging application's database.
|
||||
KST4Contest stores worked information in an internal **SQLite database**. This is independent of the logging program's database and is only populated via the UDP broadcast.
|
||||
|
||||
The input sources provide different levels of detail:
|
||||
|
||||
| Source | Global callsign status | Per-band status | Grid square |
|
||||
|---|---:|---:|---:|
|
||||
| Simplelogfile | yes | no | no |
|
||||
| QSO UDP listener | yes | yes, if included in the packet | yes, if both band and locator are available |
|
||||
| Win-Test network listener | yes | yes | yes, if a locator is available |
|
||||
|
||||
The information is restored when KST4Contest starts and updated during operation when new log entries arrive. It expires automatically after three days, so a reset before every contest is normally unnecessary.
|
||||
|
||||
A complete manual reset removes Worked marks, NOT-QRV marks and worked grid squares together. See [Worked Station Database Settings](en-Configuration#worked-station-database-settings) for details.
|
||||
Before each new contest: reset the database! → [Configuration – Worked Station Database Settings](Configuration#worked-station-database-settings)
|
||||
|
||||
@@ -26,29 +26,14 @@ The central table of all currently active chat users. Columns (depending on conf
|
||||
|
||||
| Column | Content |
|
||||
|---|---|
|
||||
| Callsign | Station callsign |
|
||||
| Name | Name and additional information from the chat name field |
|
||||
| QRA | Maidenhead locator |
|
||||
| Call | Station's callsign |
|
||||
| Name | Name from the chat name field |
|
||||
| Loc | Maidenhead locator |
|
||||
| QRB | Distance in km |
|
||||
| QTF | Direction in degrees |
|
||||
| QRG | Most recent frequency detected in a chat message |
|
||||
| Tropo | Result of the band-specific tropo or path assessment |
|
||||
| Score | Current, numerically sortable priority score of the normalised base callsign |
|
||||
| Act | Minutes since the most recent activity |
|
||||
| AP | AirScout aircraft data, when enabled |
|
||||
| worked | Per-band Worked, band-opportunity and grid-square status, plus `wkdany` |
|
||||
| NOT QRV @ | Bands on which the station has manually been marked not QRV |
|
||||
| Category | Chat category of this entry |
|
||||
|
||||
### Worked, band and grid-square status
|
||||
|
||||
The subcolumns under **worked** use compact codes because several enabled bands leave little room for full descriptions. `X` marks a callsign worked on that band. `a` and `B+` identify an offered band which has not yet been worked. An appended `o` means that the four-character grid square has already been worked on this band.
|
||||
|
||||
The **wkdany** subcolumn is band-independent: `x` means that the callsign has been worked, `o` means that the grid square has been worked on any band, and `xo` means both.
|
||||
|
||||
Each status cell has a tooltip containing the legend and the state derived for that station. For the complete calculation, including NOT-QRV precedence, see [Worked Callsigns, New Bands and New Grid Squares](en-Features#worked-callsigns-new-bands-and-new-grid-squares).
|
||||
|
||||

|
||||
| QRG | Automatically detected frequency |
|
||||
| AP | AirScout aircraft data (when active) |
|
||||
| Band colours | Worked / NOT-QRV status per band |
|
||||
|
||||
**Sorting**: Click column headers. QRB sorting is numerical (corrected in v1.22).
|
||||
|
||||
@@ -68,210 +53,53 @@ Input field for the current antenna direction. Used for the planned `MYQTF` vari
|
||||
|
||||
## Filters
|
||||
|
||||
The filter bar is located above the chat-member table and groups related controls:
|
||||
The filter bar (from v1.21 as a flowpane for small screens):
|
||||
|
||||
- **Show only QTF** limits the list to a selected antenna direction.
|
||||
- **Show only QRB [km] <=** sets a maximum distance.
|
||||
- **Find** searches for a callsign.
|
||||
- **wkd** hides callsigns which have already been worked on at least one band.
|
||||
- The individual band buttons hide a station if it has already been worked on that band or has been marked NOT QRV there. Only bands enabled for the local station are shown.
|
||||
- **Only new grids** shows only stations in four-character grid squares which have not been worked on any band.
|
||||
- **Grid color** is not a filter. It marks the QRA cell of an already worked grid square without hiding stations.
|
||||
- **New bands** shows stations with at least one detected, locally enabled and unworked band opportunity. NOT-QRV marks take precedence.
|
||||
- **Reachability**, **Tropo >=0dB** and **AS next 5m** limit the list according to the selected path or AirScout criteria.
|
||||
|
||||
The filter bar has no fixed width. QTF, Worked and Reachability controls initially use the available space in their respective rows. When the horizontal divider is moved to the right and the chat-member area becomes narrower, controls wrap only when their actual required width no longer fits.
|
||||
|
||||

|
||||
|
||||
In plain terms: the filters determine the table contents, but no longer enforce the minimum width of the entire right-hand side. The bar remains compact in the normal layout and uses additional height only when the view becomes genuinely narrow. Moving the divider back to the left immediately returns the controls to the available rows.
|
||||
- **Show only QTF**: Activate direction filter (N/NE/E/… buttons or degree input)
|
||||
- **Show only QRB [km] <=**: Activate distance filter (toggle button)
|
||||
- **Hide Worked [Band]**: Hide worked stations per band (one toggle per band)
|
||||
- **Hide NOT-QRV [Band]**: Hide NOT-QRV-tagged stations per band
|
||||
|
||||
---
|
||||
|
||||
## Station Info Panel (Further Info)
|
||||
|
||||
The lower-right panel combines the messages associated with the selected station. This includes public messages, private messages to the local station and, where visible in the chat, private messages addressed to other stations.
|
||||
Bottom right: Shows all messages of a selected station (CQ messages and PMs in one panel). A message filter can be pre-configured via the default filter in the Preferences.
|
||||
|
||||
The selected filter controls which of these messages are displayed. Under **Settings → GUI**, the default filter can be set to:
|
||||
|
||||
- all messages,
|
||||
- private messages to the local station,
|
||||
- private messages to other stations, or
|
||||
- public messages.
|
||||
|
||||
This setting changes the Further Info display only. Messages are neither discarded nor removed from the other message tables, and the filter can be changed at any time for the currently selected station.
|
||||
|
||||
The lower part of the panel contains per-band **Not QRV** marks for the selected station. Individual controls are shown for the bands enabled in the local station settings. **tag not qrv all** sets or removes the mark for every supported band, including bands which are not currently visible.
|
||||
|
||||
The change immediately affects the **NOT QRV @** column, band opportunities and the corresponding filters. It is stored in the internal database and restored after a restart.
|
||||
|
||||

|
||||
|
||||
The current **Priority score** of the selected station is displayed in the same section.
|
||||
|
||||
**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.
|
||||
|
||||
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).
|
||||
**Sked reminders** can also be activated here.
|
||||
|
||||
---
|
||||
|
||||
## Priority List
|
||||
|
||||
The compact priority bar is located on the right-hand side between the user list and the Further Info section. It displays the two currently highest-ranked candidates directly in the main window:
|
||||
|
||||
```text
|
||||
Priority: 1 CALLSIGN SCORE 2 CALLSIGN SCORE more
|
||||
```
|
||||
|
||||
Clicking either candidate selects the corresponding active chat member. The complete callsign, including its suffix and chat category, is used.
|
||||
|
||||
The **more** button opens a separate window containing up to 15 candidates. The list is sorted by descending score. Double-clicking an entry selects the candidate and closes the window.
|
||||
|
||||

|
||||
|
||||
Stations with a score of `0` are not included in the priority list. They remain visible in the user list so that the reason for their exclusion can be examined and, for example, an incorrect NOT-QRV mark can be changed.
|
||||
|
||||
The score is calculated for the normalised base callsign. Several active variants such as `9A0BB-2` and `9A0BB-70` may therefore display the same value in the user list. They nevertheless remain separate message targets.
|
||||
|
||||
New messages, AirScout data, skeds and status changes request a new calculation. A periodic refresh also runs in the background. A briefly outdated order is therefore not an error.
|
||||
|
||||
Calculation and limitations: [Priority Score and Priority List](en-Features#priority-score-and-priority-list-from-v140).
|
||||
Shows the top candidates calculated by the Score Service. Updates automatically in the background based on direction, distance and AP availability.
|
||||
|
||||
---
|
||||
|
||||
## Station Map
|
||||
## Cluster & QSO of Others
|
||||
|
||||
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.
|
||||
Separate window (can be minimised). Shows the communication flow between other stations – interesting during quieter contest periods.
|
||||
|
||||
---
|
||||
|
||||
## Menu
|
||||
|
||||
### 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
|
||||
- **Use Dark Mode** (from v1.26): Toggle dark colour scheme on/off.
|
||||
|
||||
---
|
||||
|
||||
## Window Sizes and Dividers
|
||||
|
||||
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.
|
||||
From **v1.21**, clicking **"Save Settings"** also saves window sizes and divider positions of all panels in the configuration file, which are restored on the next start.
|
||||
|
||||
If you encounter display problems: delete the configuration file → KST4Contest creates new default values.
|
||||
|
||||
---
|
||||
|
||||
## Operating Tips
|
||||
|
||||
- **Keep the settings window open**: Quick access to enable/disable the beacon.
|
||||
- **Right-click in the user list**: Opens the snippet menu and other context actions.
|
||||
- **Mark a station NOT QRV**: Select the station and use the per-band controls in the **Further Info** panel.
|
||||
- **Right-click in the user list**: Opens the snippet menu and further actions (QRZ.com profile, set NOT-QRV tags).
|
||||
- **Enter from anywhere**: When text is in the send field, Enter sends directly – even if the focus is elsewhere.
|
||||
- **Stop the beacon**: Switch off the beacon while scanning frequencies to avoid flooding the chat with messages.
|
||||
|
||||
|
Before Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 100 KiB |
@@ -1,16 +0,0 @@
|
||||
pkgbase = kst4contest-bin
|
||||
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation (pre-built)
|
||||
pkgver = 1.41.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/praktimarc/kst4contest
|
||||
arch = x86_64
|
||||
license = GPL-3.0-only
|
||||
depends = gst-plugins-base
|
||||
depends = gst-plugins-good
|
||||
provides = kst4contest
|
||||
conflicts = kst4contest
|
||||
conflicts = kst4contest-git
|
||||
source = KST4Contest-v1.41.1-archlinux-x86_64.pkg.tar.zst::https://github.com/praktimarc/kst4contest/releases/download/v1.41.1/KST4Contest-v1.41.1-archlinux-x86_64.pkg.tar.zst
|
||||
sha256sums = 8e9a53ff832920c9ef2733635b90c5a4ffcd57a2958aaa251e92bd031142c614
|
||||
|
||||
pkgname = kst4contest-bin
|
||||
@@ -1,17 +0,0 @@
|
||||
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
|
||||
pkgname=kst4contest-bin
|
||||
pkgver=1.41.1
|
||||
pkgrel=1
|
||||
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation (pre-built)"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest' 'kst4contest-git')
|
||||
source=("KST4Contest-v${pkgver}-archlinux-${CARCH}.pkg.tar.zst::https://github.com/praktimarc/kst4contest/releases/download/v${pkgver}/KST4Contest-v${pkgver}-archlinux-${CARCH}.pkg.tar.zst")
|
||||
sha256sums=('8e9a53ff832920c9ef2733635b90c5a4ffcd57a2958aaa251e92bd031142c614')
|
||||
|
||||
package() {
|
||||
cp -a "${srcdir}/usr" "${pkgdir}/"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
pkgbase = kst4contest-git
|
||||
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation (git)
|
||||
pkgver = 1.42.0.r145.gd885924
|
||||
pkgrel = 1
|
||||
url = https://github.com/praktimarc/kst4contest
|
||||
arch = x86_64
|
||||
license = GPL-3.0-only
|
||||
makedepends = java-environment=21
|
||||
makedepends = maven
|
||||
makedepends = git
|
||||
depends = gst-plugins-base
|
||||
depends = gst-plugins-good
|
||||
provides = kst4contest
|
||||
conflicts = kst4contest
|
||||
conflicts = kst4contest-bin
|
||||
source = kst4contest::git+https://github.com/praktimarc/kst4contest.git
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = kst4contest-git
|
||||
@@ -1,76 +0,0 @@
|
||||
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
|
||||
pkgname=kst4contest-git
|
||||
pkgver=1.42.0.r145.gd885924
|
||||
pkgrel=1
|
||||
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation (git)"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
makedepends=('java-environment=21' 'maven' 'git')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest' 'kst4contest-bin')
|
||||
source=("kst4contest::git+https://github.com/praktimarc/kst4contest.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "${srcdir}/kst4contest"
|
||||
BASE=$(grep -m1 '<version>' pom.xml \
|
||||
| sed 's/.*<version>\(.*\)<\/version>.*/\1/' | sed 's/[-.]nightly//')
|
||||
printf '%s.r%s.g%s' "${BASE}" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/kst4contest"
|
||||
|
||||
export JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name 'java-21-*' -type d | head -n 1)
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
|
||||
mvn -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 dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/kst4contest"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/lib/KST4Contest"
|
||||
cp -a dist/KST4Contest/. "${pkgdir}/usr/lib/KST4Contest/"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/bin"
|
||||
printf '#!/bin/sh\nexec /usr/lib/KST4Contest/bin/KST4Contest "$@"\n' \
|
||||
> "${pkgdir}/usr/bin/KST4Contest"
|
||||
chmod 755 "${pkgdir}/usr/bin/KST4Contest"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/share/applications"
|
||||
cat > "${pkgdir}/usr/share/applications/KST4Contest.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=KST4Contest
|
||||
Comment=ON4KST Chat Client for VHF/UHF contest operation
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [[ -f "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" ]]; then
|
||||
install -Dm644 "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" \
|
||||
"${pkgdir}/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
pkgbase = kst4contest
|
||||
pkgdesc = ON4KST Chat Client for VHF/UHF contest operation
|
||||
pkgver = 1.41.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/praktimarc/kst4contest
|
||||
arch = x86_64
|
||||
license = GPL-3.0-only
|
||||
makedepends = java-environment=21
|
||||
makedepends = maven
|
||||
depends = gst-plugins-base
|
||||
depends = gst-plugins-good
|
||||
provides = kst4contest
|
||||
conflicts = kst4contest-bin
|
||||
conflicts = kst4contest-git
|
||||
source = kst4contest-1.41.1.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v1.41.1.tar.gz
|
||||
sha256sums = e96207a2d3fee19d35e34717f5312beb28bb087c040164e352337e749ce53b8d
|
||||
|
||||
pkgname = kst4contest
|
||||
@@ -1,69 +0,0 @@
|
||||
# Maintainer: Philipp Wagner <philipp@wagnersnetz.de>
|
||||
pkgname=kst4contest
|
||||
pkgver=1.41.1
|
||||
pkgrel=1
|
||||
pkgdesc="ON4KST Chat Client for VHF/UHF contest operation"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
makedepends=('java-environment=21' 'maven')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest-bin' 'kst4contest-git')
|
||||
source=("${pkgname}-${pkgver}.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v${pkgver}.tar.gz")
|
||||
sha256sums=('e96207a2d3fee19d35e34717f5312beb28bb087c040164e352337e749ce53b8d')
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/kst4contest-${pkgver}"
|
||||
|
||||
export JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name 'java-21-*' -type d | head -n 1)
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
|
||||
mvn -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 dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql,java.net.http,jdk.crypto.ec \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/kst4contest-${pkgver}"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/lib/KST4Contest"
|
||||
cp -a dist/KST4Contest/. "${pkgdir}/usr/lib/KST4Contest/"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/bin"
|
||||
printf '#!/bin/sh\nexec /usr/lib/KST4Contest/bin/KST4Contest "$@"\n' \
|
||||
> "${pkgdir}/usr/bin/KST4Contest"
|
||||
chmod 755 "${pkgdir}/usr/bin/KST4Contest"
|
||||
|
||||
install -dm755 "${pkgdir}/usr/share/applications"
|
||||
cat > "${pkgdir}/usr/share/applications/KST4Contest.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=KST4Contest
|
||||
Comment=ON4KST Chat Client for VHF/UHF contest operation
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [[ -f "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" ]]; then
|
||||
install -Dm644 "${pkgdir}/usr/lib/KST4Contest/lib/KST4Contest.png" \
|
||||
"${pkgdir}/usr/share/icons/hicolor/256x256/apps/KST4Contest.png"
|
||||
fi
|
||||
|
||||
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>de.x08</groupId>
|
||||
<artifactId>praktiKST</artifactId>
|
||||
<version>1.42.0-nightly</version>
|
||||
<version>1.41.0-nightly</version>
|
||||
|
||||
<name>praktiKST</name>
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
<launcher>${project.artifactId}</launcher>
|
||||
<appName>${project.artifactId}</appName>
|
||||
<main.class>kst4contest.view.Kst4ContestApplication</main.class>
|
||||
<java.version>21</java.version>
|
||||
<java.version>17</java.version>
|
||||
<required.maven.version>3.6.3</required.maven.version>
|
||||
<jar.filename>${project.artifactId}-${project.version}</jar.filename>
|
||||
|
||||
<!-- Dependency versions -->
|
||||
<javafx.version>21.0.5</javafx.version>
|
||||
<javafx.version>19.0.2.1</javafx.version>
|
||||
<jetbrains.annotations.version>24.0.1</jetbrains.annotations.version>
|
||||
<junit.version>5.10.1</junit.version>
|
||||
<lombok.version>1.18.44</lombok.version>
|
||||
@@ -50,8 +50,8 @@
|
||||
<maven.wrapper.plugin>3.2.0</maven.wrapper.plugin>
|
||||
<moditect.maven.plugin>1.0.0.RC2</moditect.maven.plugin>
|
||||
<jpackage.maven.plugin>0.1.3</jpackage.maven.plugin>
|
||||
<maven.pmd.version>3.28.0</maven.pmd.version>
|
||||
<pmd.version>7.17.0</pmd.version>
|
||||
<maven.pmd.version>3.21.2</maven.pmd.version>
|
||||
<pmd.version>6.55.0</pmd.version>
|
||||
<codehaus.version.plugin>2.16.1</codehaus.version.plugin>
|
||||
<javafx.maven.plugin>0.0.8</javafx.maven.plugin>
|
||||
<spotbugs.maven.plugin>4.9.8.2</spotbugs.maven.plugin>
|
||||
@@ -93,13 +93,6 @@
|
||||
<version>${javafx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JLayer: pure-Java MP3 decoder, used instead of JavaFX Media for Flatpak audio compatibility -->
|
||||
<dependency>
|
||||
<groupId>javazoom</groupId>
|
||||
<artifactId>jlayer</artifactId>
|
||||
<version>1.0.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SQLite -->
|
||||
<dependency>
|
||||
<groupId>org.xerial</groupId>
|
||||
@@ -150,32 +143,20 @@
|
||||
<version>${jetbrains.annotations.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<version>5.17.0</version>
|
||||
<scope>compile</scope>
|
||||
<groupId>xml-apis</groupId>
|
||||
<artifactId>xml-apis</artifactId>
|
||||
<version>2.0.2</version>
|
||||
</dependency>
|
||||
|
||||
<!--
|
||||
<dependency>
|
||||
<groupId>xml-apis</groupId>
|
||||
<artifactId>xml-apis</artifactId>
|
||||
<version>2.0.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.xml.parsers</groupId>
|
||||
<artifactId>jaxp-api</artifactId>
|
||||
<version>1.4.5</version>
|
||||
</dependency>
|
||||
-->
|
||||
<dependency>
|
||||
<groupId>javax.xml.parsers</groupId>
|
||||
<artifactId>jaxp-api</artifactId>
|
||||
<version>1.4.5</version>
|
||||
</dependency>
|
||||
-->
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -266,15 +247,6 @@
|
||||
<version>${maven.surfire.plugin}</version>
|
||||
<configuration>
|
||||
<testFailureIgnore>true</testFailureIgnore>
|
||||
<!--
|
||||
Without this, Surefire's automatic module-path detection splits the
|
||||
JUnit Platform jars across classpath and module-path inconsistently
|
||||
(module org.junit.platform.commons ends up loaded while a class from
|
||||
junit-platform-engine stays in the unnamed module), which fails with
|
||||
an IllegalAccessError before any test can run. The project itself has
|
||||
no module-info.java, so plain classpath execution is correct here.
|
||||
-->
|
||||
<useModulePath>false</useModulePath>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@@ -308,7 +280,30 @@
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-pmd-plugin</artifactId>
|
||||
<version>${maven.pmd.version}</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-core</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-java</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-javascript</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.sourceforge.pmd</groupId>
|
||||
<artifactId>pmd-jsp</artifactId>
|
||||
<version>${pmd.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<configuration>
|
||||
<sourceEncoding>${project.build.sourceEncoding}</sourceEncoding>
|
||||
<minimumTokens>100</minimumTokens>
|
||||
<targetJdk>${java.version}</targetJdk>
|
||||
<linkXRef>false</linkXRef>
|
||||
@@ -441,11 +436,7 @@
|
||||
<addmodule>javafx.graphics</addmodule>
|
||||
<addmodule>javafx.fxml</addmodule>
|
||||
<addmodule>javafx.web</addmodule>
|
||||
<addmodule>javafx.media</addmodule>
|
||||
<addmodule>java.sql</addmodule>
|
||||
<addmodule>java.net.http</addmodule>
|
||||
<addmodule>jdk.crypto.ec</addmodule>
|
||||
<addmodule>jdk.net</addmodule>
|
||||
</addmodules>
|
||||
<mainclass>${main.class}</mainclass>
|
||||
<input>${project.build.directory}/modules</input>
|
||||
|
||||
@@ -18,18 +18,11 @@ public class ApplicationConstants {
|
||||
public static final String APPLICATION_NAME = "praktiKST";
|
||||
|
||||
/**
|
||||
* Version shown to the user and used for semantic version comparison.
|
||||
* Name of file to store preferences in.
|
||||
*/
|
||||
public static final String APPLICATION_CURRENT_VERSION = "1.42";
|
||||
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.41;
|
||||
|
||||
/**
|
||||
* Legacy numeric representation used only while older update feeds and
|
||||
* application versions still exist.
|
||||
*/
|
||||
@Deprecated
|
||||
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.42;
|
||||
|
||||
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://kst4contest.hamradioonline.de/kst4ContestVersionInfo.xml";
|
||||
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml";
|
||||
public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
|
||||
|
||||
public static final String STYLECSSFILE_DEFAULT_DAYLIGHT = "KST4ContestDefaultDay.css";
|
||||
@@ -43,26 +36,9 @@ public class ApplicationConstants {
|
||||
|
||||
public static final String DISCONNECT_RDR_POISONPILL = "UNKNOWN: KST4C KILL POISONPILL_KILLTHREAD=: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
|
||||
|
||||
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg]"; // hard-coded marker (user cannot remove it)
|
||||
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg] "; // hard-coded marker (user can't remove it)
|
||||
|
||||
/**
|
||||
* UI message retention limits.
|
||||
*
|
||||
* The global chat message list is the backing list for several FilteredLists
|
||||
* and TableViews. It must not grow without limit during long contest runs.
|
||||
*
|
||||
* The list is kept in newest-first order:
|
||||
* index 0 = newest message
|
||||
* last index = oldest message
|
||||
*/
|
||||
public static final int CHAT_MESSAGE_STORE_MAX_SIZE = 30000;
|
||||
public static final int CHAT_MESSAGE_STORE_TRIM_TO_SIZE = 25000;
|
||||
|
||||
/**
|
||||
* DXCluster table retention limits.
|
||||
*/
|
||||
public static final int CLUSTER_MESSAGE_STORE_MAX_SIZE = 10000;
|
||||
public static final int CLUSTER_MESSAGE_STORE_TRIM_TO_SIZE = 8000;
|
||||
|
||||
/**
|
||||
* generates a unique runtime id per session. Its used to feed the poison pill in order to kill only this one and
|
||||
|
||||
@@ -1,315 +1,147 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
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.net.NoRouteToHostException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.TimerTask;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import javafx.collections.ObservableList;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
|
||||
/**
|
||||
* Sends periodical path requests and an AirScout watchlist for the currently
|
||||
* active ON4KST stations.
|
||||
*/
|
||||
public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(
|
||||
AirScoutPeriodicalAPReflectionInquirerTask.class.getName()
|
||||
);
|
||||
private ChatController client;
|
||||
|
||||
private static final String BROADCAST_ADDRESS = "255.255.255.255";
|
||||
public AirScoutPeriodicalAPReflectionInquirerTask(ChatController client) {
|
||||
|
||||
|
||||
|
||||
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
|
||||
) {
|
||||
this.client = client;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName(
|
||||
"AirscoutPeriodicalReflectionInquirerTask"
|
||||
);
|
||||
|
||||
Thread.currentThread().setName("AirscoutPeriodicalReflectionInquirierTask");
|
||||
|
||||
/*
|
||||
* Keep the scheduled task installed so that AirScout can be enabled at
|
||||
* runtime, but do not send anything while the integration is disabled.
|
||||
String KSTClientsNameForQuery = this.client.getChatPreferences().getAirScout_asClientNameString();
|
||||
String ASServerNameStringForAnswer = this.client.getChatPreferences().getAirScout_asServerNameString();
|
||||
|
||||
//TODO: Manage prefixes kst and as via preferences file and instance
|
||||
//TODO: Check if locator is changeable via the preferences object, need to be correct if it changes
|
||||
DatagramSocket dsocket;
|
||||
|
||||
// String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" "; //working original
|
||||
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
|
||||
|
||||
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
||||
String prefix_asWatchList = "ASWATCHLIST: \""+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
|
||||
|
||||
String bandString = "1440000"; //TODO: this must variable in case of higher bands! ... default: 1440000
|
||||
// String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //before fix 1.266
|
||||
|
||||
|
||||
String ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
|
||||
try {
|
||||
if (this.client.getChatPreferences().getStn_loginCallSign().contains("-")) {
|
||||
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign().split("-")[0];
|
||||
} else {
|
||||
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
|
||||
}
|
||||
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
|
||||
|
||||
|
||||
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
|
||||
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
|
||||
String asWatchListStringSuffix = asWatchListString;
|
||||
|
||||
String host = "255.255.255.255";
|
||||
// int port = 9872;
|
||||
|
||||
int port = client.getChatPreferences().getAirScout_asCommunicationPort();
|
||||
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
|
||||
InetAddress address;
|
||||
|
||||
|
||||
/**
|
||||
* Iterate over chatmemberlist and asking airscout for plane reflection information
|
||||
* To avoid a concurrentmodifyexception, we have to convert the original list to an array at first
|
||||
* since the iterator brakes if the list changing during the iteration time
|
||||
*/
|
||||
if (!client.getChatPreferences().isAirScout_asUDPListenerEnabled()) {
|
||||
return;
|
||||
}
|
||||
ObservableList<ChatMember> praktiKSTActiveUserList = this.client.getLst_chatMemberList();
|
||||
|
||||
|
||||
ChatMember[] ary_threadSafeChatMemberArray = new ChatMember[praktiKSTActiveUserList.size()];
|
||||
praktiKSTActiveUserList.toArray(ary_threadSafeChatMemberArray);
|
||||
|
||||
for (ChatMember i : ary_threadSafeChatMemberArray) {
|
||||
|
||||
String clientIdentifier =
|
||||
client.getChatPreferences().getAirScout_asClientNameString();
|
||||
String serverIdentifier =
|
||||
client.getChatPreferences().getAirScout_asServerNameString();
|
||||
String ownCallSign = normalizeOwnCallSign(
|
||||
client.getChatPreferences().getStn_loginCallSign()
|
||||
);
|
||||
String ownLocator =
|
||||
client.getChatPreferences().getStn_loginLocatorMainCat();
|
||||
if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault())
|
||||
//Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower!
|
||||
//this counts for AS request and Aswatchlist
|
||||
{
|
||||
suffix = i.getCallSign() + "," + i.getQra() + " ";
|
||||
|
||||
if (ownCallSign == null
|
||||
|| ownCallSign.isBlank()
|
||||
|| ownLocator == null
|
||||
|| ownLocator.isBlank()) {
|
||||
LOGGER.warning(
|
||||
"AirScout queries were skipped because the own callsign "
|
||||
+ "or locator is missing."
|
||||
);
|
||||
return;
|
||||
}
|
||||
String queryStringToAirScout = "";
|
||||
|
||||
String setPathPrefix =
|
||||
"ASSETPATH: \"" + clientIdentifier
|
||||
+ "\" \"" + serverIdentifier + "\" ";
|
||||
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
|
||||
|
||||
String watchListPrefix =
|
||||
"ASWATCHLIST: \"" + clientIdentifier
|
||||
+ "\" \"" + serverIdentifier + "\" ";
|
||||
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
|
||||
|
||||
String ownStation = ownCallSign + "," + ownLocator;
|
||||
List<ChatMember> activeMembers = client.snapshotChatMembers();
|
||||
List<String> watchListTargets = new ArrayList<>();
|
||||
Set<String> processedCallsigns = new LinkedHashSet<>();
|
||||
String watchListBandValue = null;
|
||||
int port = client.getChatPreferences()
|
||||
.getAirScout_asCommunicationPort();
|
||||
|
||||
try (
|
||||
DatagramSocket socket = new DatagramSocket()
|
||||
) {
|
||||
socket.setBroadcast(true);
|
||||
InetAddress broadcastAddress =
|
||||
InetAddress.getByName(BROADCAST_ADDRESS);
|
||||
|
||||
for (ChatMember member : activeMembers) {
|
||||
if (!isUsableAirScoutTarget(member)) {
|
||||
continue;
|
||||
try {
|
||||
address = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (UnknownHostException e1) {
|
||||
e1.printStackTrace();
|
||||
} catch (NoRouteToHostException e) {
|
||||
e.printStackTrace();
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
|
||||
|
||||
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();
|
||||
|
||||
String pathQuery =
|
||||
setPathPrefix
|
||||
+ bandValue
|
||||
+ ","
|
||||
+ ownStation
|
||||
+ ","
|
||||
+ targetStation
|
||||
+ " ";
|
||||
|
||||
sendPacket(
|
||||
socket,
|
||||
broadcastAddress,
|
||||
port,
|
||||
pathQuery
|
||||
);
|
||||
|
||||
watchListTargets.add(targetStation);
|
||||
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra();
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Could not send the periodical AirScout queries.",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/**
|
||||
* As next we will set the ASWatchlist. All stations in chat will be watched by airscout causing following code.\n\n
|
||||
* ASWATCHLIST: "KST" "AS" 4320000,DO5AMF,JN49GL,DF9QX,JO42HD,DG2KBC,JN58MI,DJ0PY,JO32MF,DL1YDI,JO42FA,DL6BF,JO32QI,F1NZC,JN15MR,F4TXU,JN23CX,F5GHP,IN96LE,F6HTJ,JN12KQ,G0GGG,IO81VE,G0JCC,IO82MA,G0JDL,JO02SI,G0MBL,JO01QH,G4AEP,IO91MB,G4CLA,IO92JL,G4DCV,IO91OF,G4LOH,IO70JC,G4MKF,IO91HJ,G4TRA,IO81WN,G8GXP,IO93FQ,G8VHI,IO92FM,GW0RHC,IO71UN,HA4ND,JN97MJ,I5/HB9SJV/P,JN52JS,IW2DAL,JN45NN,OK1FPR,JO80CE,OK6M,JN99CR,OV3T,JO46CM,OZ2M,JO65FR,PA0V,JO33II,PA2RU,JO32LT,PA3DOL,JO22MT,PA9R,JO22JK,PE1EVX,JO22MP,S51AT,JN75GW,SM7KOJ,JO66ND,SP9TTG,JO90KW�
|
||||
* The watchlist-String is bult by the for loop which builds the AP queries
|
||||
*/
|
||||
asWatchListStringSuffix += " ";
|
||||
|
||||
byte[] queryStringToAirScoutMSG = asWatchListStringSuffix.getBytes();
|
||||
|
||||
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;
|
||||
address = InetAddress.getByName("255.255.255.255");
|
||||
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
|
||||
dsocket = new DatagramSocket();
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.send(packet);
|
||||
dsocket.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// System.out.println("[ASUDPTask, info:] set watchlist: " + asWatchListStringSuffix);
|
||||
|
||||
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.
|
||||
*
|
||||
* @param callSign configured ON4KST login callsign
|
||||
* @return callsign without an ON4KST login suffix
|
||||
*/
|
||||
private String normalizeOwnCallSign(String callSign) {
|
||||
if (callSign == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalizedCallSign = callSign.trim();
|
||||
int suffixSeparator = normalizedCallSign.indexOf("-");
|
||||
|
||||
if (suffixSeparator > 0) {
|
||||
return normalizedCallSign.substring(0, suffixSeparator);
|
||||
}
|
||||
|
||||
return normalizedCallSign;
|
||||
}
|
||||
|
||||
private boolean isUsableAirScoutTarget(ChatMember member) {
|
||||
return member != null
|
||||
&& member.getCallSign() != null
|
||||
&& !member.getCallSign().isBlank()
|
||||
&& member.getQra() != null
|
||||
&& !member.getQra().isBlank();
|
||||
}
|
||||
|
||||
private void sendPacket(
|
||||
DatagramSocket socket,
|
||||
InetAddress address,
|
||||
int port,
|
||||
String message
|
||||
) throws IOException {
|
||||
byte[] payload = message.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
DatagramPacket packet = new DatagramPacket(
|
||||
payload,
|
||||
payload.length,
|
||||
address,
|
||||
port
|
||||
);
|
||||
|
||||
socket.send(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,178 +1,121 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
/**
|
||||
* Sends the configured public-chat beacons for both active chat categories.
|
||||
* This class is for sending beacons intervalled to the public chat. Gets all
|
||||
* preferences and instances via the Chatpreferences-object of the
|
||||
* Chatcontroller.
|
||||
* <br/><br/>
|
||||
* The task will be runned out of the singleton ChatController instance in an
|
||||
* intervall as specified by the Chatpreferences-instance (typically as
|
||||
* configured in the xml file.
|
||||
*
|
||||
*
|
||||
* @author prakt
|
||||
*
|
||||
* <p>Both categories deliberately share one timer and therefore the same
|
||||
* 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>
|
||||
*/
|
||||
public class BeaconTask extends TimerTask {
|
||||
|
||||
private static final String THREAD_NICKNAME = "MyBeacon";
|
||||
private ChatController chatController;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "MyBeacon";
|
||||
|
||||
private final ChatController chatController;
|
||||
private final ThreadStatusCallback callbackToController;
|
||||
public BeaconTask(ChatController client, ThreadStatusCallback callback) {
|
||||
this.callBackToController = callback;
|
||||
this.chatController = client;
|
||||
|
||||
/**
|
||||
* Creates one execution of the shared beacon timer.
|
||||
*
|
||||
* @param chatController controller providing preferences and the TX queue
|
||||
* @param callbackToController callback used by the thread-status display
|
||||
*/
|
||||
public BeaconTask(
|
||||
ChatController chatController,
|
||||
ThreadStatusCallback callbackToController
|
||||
) {
|
||||
this.chatController = chatController;
|
||||
this.callbackToController = callbackToController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
reportStatus(THREAD_NICKNAME, true, "initialized", false);
|
||||
|
||||
MessageVariableResolver variableResolver =
|
||||
new MessageVariableResolver(chatController.getChatPreferences());
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
sendMainCategoryBeacon(variableResolver);
|
||||
sendSecondCategoryBeacon(variableResolver);
|
||||
}
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
/**
|
||||
* 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);
|
||||
return;
|
||||
|
||||
ChatMessage beaconMSG = new ChatMessage();
|
||||
|
||||
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
|
||||
|
||||
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
|
||||
|
||||
beaconMSG.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG.setMessageDirectedToServer(true);
|
||||
|
||||
|
||||
|
||||
|
||||
ChatMessage beaconMSG2 = new ChatMessage();
|
||||
|
||||
String replaceVariables2 = this.chatController.getChatPreferences().getBcn_beaconTextSecondCat();
|
||||
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables2 = replaceVariables2.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
replaceVariables2 = replaceVariables2.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
|
||||
|
||||
|
||||
beaconMSG2.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG2.setMessageDirectedToServer(true);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* beacon 1st Chatcategory
|
||||
*/
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
|
||||
this.chatController.getMessageTXBus().add(beaconMSG);
|
||||
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} else {
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextMainCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategoryMain()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
"main category"
|
||||
);
|
||||
/**
|
||||
* beacon 2nd Chatcategory
|
||||
*/
|
||||
if (this.chatController.getChatPreferences().isLoginToSecondChatEnabled()) { //only send if 2nd cat enabled
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 1", false, "invalid text", true);
|
||||
return;
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledSecondCat()) {
|
||||
|
||||
beaconMSG2.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables2 + "|0|");
|
||||
beaconMSG2.setMessageDirectedToServer(true);
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText());
|
||||
this.chatController.getMessageTXBus().add(beaconMSG2);
|
||||
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} else {
|
||||
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println(
|
||||
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending main-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
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()
|
||||
|| !chatController.getChatPreferences()
|
||||
.isBcn_beaconsEnabledSecondCat()) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "off", false);
|
||||
return;
|
||||
}
|
||||
|
||||
String resolvedText = variableResolver.resolveGlobalVariables(
|
||||
chatController.getChatPreferences().getBcn_beaconTextSecondCat()
|
||||
);
|
||||
ChatMessage beaconMessage = buildBeaconMessage(
|
||||
chatController.getChatPreferences()
|
||||
.getLoginChatCategorySecond()
|
||||
.getCategoryNumber(),
|
||||
resolvedText,
|
||||
"second category"
|
||||
);
|
||||
|
||||
if (beaconMessage == null) {
|
||||
reportStatus(THREAD_NICKNAME + " 2", false, "invalid text", true);
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println(
|
||||
new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
+ " [BeaconTask, Info]: Sending second-category CQ: "
|
||||
+ beaconMessage.getMessageText()
|
||||
);
|
||||
chatController.getMessageTXBus().add(beaconMessage);
|
||||
reportStatus(THREAD_NICKNAME + " 2", true, "on", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the server-directed message after validating the resolved payload.
|
||||
*
|
||||
* <p>The resolved text is checked rather than only the configured template
|
||||
* because inserted values can increase the final message length.</p>
|
||||
*
|
||||
* @param categoryNumber ON4KST category number
|
||||
* @param resolvedText fully resolved beacon payload
|
||||
* @param categoryDescription text used in diagnostic output
|
||||
* @return prepared message, or {@code null} if the payload is invalid
|
||||
*/
|
||||
private ChatMessage buildBeaconMessage(
|
||||
int categoryNumber,
|
||||
String resolvedText,
|
||||
String categoryDescription
|
||||
) {
|
||||
if (resolvedText == null
|
||||
|| resolvedText.length() > ChatController.MAX_BEACON_TEXT_LENGTH) {
|
||||
int actualLength = resolvedText == null ? 0 : resolvedText.length();
|
||||
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
|
||||
+ "."
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
ChatMessage beaconMessage = new ChatMessage();
|
||||
beaconMessage.setMessageText(
|
||||
"MSG|" + categoryNumber + "|0|" + resolvedText + "|0|"
|
||||
);
|
||||
beaconMessage.setMessageDirectedToServer(true);
|
||||
return beaconMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards one state update to the existing thread-status display.
|
||||
*/
|
||||
private void reportStatus(
|
||||
String threadName,
|
||||
boolean running,
|
||||
String information,
|
||||
boolean criticalState
|
||||
) {
|
||||
ThreadStateMessage stateMessage = new ThreadStateMessage(
|
||||
threadName,
|
||||
running,
|
||||
information,
|
||||
criticalState
|
||||
);
|
||||
callbackToController.onThreadStatus(THREAD_NICKNAME, stateMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,6 @@ import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.utils.ApplicationFileUtils;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
|
||||
public class DBController {
|
||||
|
||||
/**
|
||||
@@ -36,14 +30,14 @@ public class DBController {
|
||||
* Number of milliseconds after which worked/not-QRV data is considered outdated
|
||||
* and therefore automatically reset.
|
||||
*/
|
||||
private static final long WORKED_DATA_EXPIRATION_IN_MILLISECONDS =
|
||||
3L * 24L * 60L * 60L * 1000L;
|
||||
private static final long WORKED_DATA_EXPIRATION_IN_MILLISECONDS = 65L * 60L * 60L * 1000L;
|
||||
|
||||
/**
|
||||
* Database schema version that includes the raw-callsign normalization migration
|
||||
* marker. The marker is stored in SQLite PRAGMA user_version so the expensive
|
||||
* normalization rebuild is executed only once per database file.
|
||||
*/
|
||||
private static final int CURRENT_DATABASE_SCHEMA_VERSION = 14;
|
||||
private static final int CURRENT_DATABASE_SCHEMA_VERSION = 13;
|
||||
|
||||
/**
|
||||
* Minimum interval between two expiration cleanup runs. This avoids repeated full
|
||||
@@ -142,10 +136,8 @@ public class DBController {
|
||||
*/
|
||||
private synchronized void ensureChatMemberTableCompatibility() {
|
||||
createChatMemberTableIfRequired();
|
||||
createWorkedGrossFieldTableIfRequired();
|
||||
versionUpdateOfDBCheckAndChangeV11ToV12();
|
||||
versionUpdateOfDBCheckAndChangeV12ToV13();
|
||||
versionUpdateOfDBCheckAndChangeV13ToV14();
|
||||
|
||||
if (helper_isDatabaseSchemaVersionOlderThanCurrent() || helper_isCallsignNormalizationMigrationRequired()) {
|
||||
normalizeStoredCallsignsToRawCallsigns();
|
||||
@@ -231,8 +223,6 @@ public class DBController {
|
||||
+ "worked3400 BOOLEAN DEFAULT 0, "
|
||||
+ "worked5600 BOOLEAN DEFAULT 0, "
|
||||
+ "worked10G BOOLEAN DEFAULT 0, "
|
||||
+ "worked50 BOOLEAN DEFAULT 0, "
|
||||
+ "worked70 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV144 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV432 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV1240 BOOLEAN DEFAULT 0, "
|
||||
@@ -240,8 +230,6 @@ public class DBController {
|
||||
+ "notQRV3400 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV5600 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV10G BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV50 BOOLEAN DEFAULT 0, "
|
||||
+ "notQRV70 BOOLEAN DEFAULT 0, "
|
||||
+ "lastFlagsChangeEpochMs INTEGER DEFAULT 0"
|
||||
+ ");";
|
||||
|
||||
@@ -252,30 +240,6 @@ public class DBController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the persistent gross-field table used by the new-locator filter.
|
||||
* The primary key is band + gross field because the filter only needs to know
|
||||
* whether a large locator square has already been worked on a band.
|
||||
*/
|
||||
private synchronized void createWorkedGrossFieldTableIfRequired() {
|
||||
|
||||
String createTableSql =
|
||||
"CREATE TABLE IF NOT EXISTS WorkedGrossField ("
|
||||
+ "band TEXT NOT NULL, "
|
||||
+ "grossField TEXT NOT NULL, "
|
||||
+ "locator TEXT, "
|
||||
+ "callsign TEXT, "
|
||||
+ "source TEXT, "
|
||||
+ "lastWorkedEpochMs INTEGER NOT NULL, "
|
||||
+ "PRIMARY KEY (band, grossField)"
|
||||
+ ");";
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(createTableSql);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("[DBH, ERROR:] Could not create WorkedGrossField table", e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Updates old v1.1 databases to the v1.2 schema by adding the not-QRV fields if
|
||||
* they are missing.
|
||||
@@ -308,22 +272,6 @@ public class DBController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates old v1.3 databases to the v1.4 schema by adding the worked/not-QRV
|
||||
* columns for the 50 MHz and 70 MHz bands if they are missing.
|
||||
*/
|
||||
public synchronized void versionUpdateOfDBCheckAndChangeV13ToV14() {
|
||||
|
||||
try {
|
||||
ensureColumnExists("ChatMember", "worked50", "BOOLEAN DEFAULT 0");
|
||||
ensureColumnExists("ChatMember", "worked70", "BOOLEAN DEFAULT 0");
|
||||
ensureColumnExists("ChatMember", "notQRV50", "BOOLEAN DEFAULT 0");
|
||||
ensureColumnExists("ChatMember", "notQRV70", "BOOLEAN DEFAULT 0");
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("[DBH, ERROR:] Could not migrate database from v1.3 to v1.4", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a missing column to an existing table. This method is used for safe schema
|
||||
* upgrades on customer systems which still contain older database files.
|
||||
@@ -473,11 +421,7 @@ public class DBController {
|
||||
targetChatMember.setWorked3400(targetChatMember.isWorked3400() || sourceChatMember.isWorked3400());
|
||||
targetChatMember.setWorked5600(targetChatMember.isWorked5600() || sourceChatMember.isWorked5600());
|
||||
targetChatMember.setWorked10G(targetChatMember.isWorked10G() || sourceChatMember.isWorked10G());
|
||||
targetChatMember.setWorked50(targetChatMember.isWorked50() || sourceChatMember.isWorked50());
|
||||
targetChatMember.setWorked70(targetChatMember.isWorked70() || sourceChatMember.isWorked70());
|
||||
|
||||
targetChatMember.setQrv50(targetChatMember.isQrv50() && sourceChatMember.isQrv50());
|
||||
targetChatMember.setQrv70(targetChatMember.isQrv70() && sourceChatMember.isQrv70());
|
||||
targetChatMember.setQrv144(targetChatMember.isQrv144() && sourceChatMember.isQrv144());
|
||||
targetChatMember.setQrv432(targetChatMember.isQrv432() && sourceChatMember.isQrv432());
|
||||
targetChatMember.setQrv1240(targetChatMember.isQrv1240() && sourceChatMember.isQrv1240());
|
||||
@@ -516,8 +460,6 @@ public class DBController {
|
||||
+ "worked3400 = 0, "
|
||||
+ "worked5600 = 0, "
|
||||
+ "worked10G = 0, "
|
||||
+ "worked50 = 0, "
|
||||
+ "worked70 = 0, "
|
||||
+ "notQRV144 = 0, "
|
||||
+ "notQRV432 = 0, "
|
||||
+ "notQRV1240 = 0, "
|
||||
@@ -525,8 +467,6 @@ public class DBController {
|
||||
+ "notQRV3400 = 0, "
|
||||
+ "notQRV5600 = 0, "
|
||||
+ "notQRV10G = 0, "
|
||||
+ "notQRV50 = 0, "
|
||||
+ "notQRV70 = 0, "
|
||||
+ "lastFlagsChangeEpochMs = 0 "
|
||||
+ "WHERE lastFlagsChangeEpochMs > 0 AND lastFlagsChangeEpochMs < ?;";
|
||||
|
||||
@@ -537,15 +477,7 @@ public class DBController {
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("[DBH, ERROR:] Could not reset expired worked data", e);
|
||||
}
|
||||
|
||||
try (PreparedStatement deleteGrossFieldsStatement = connection.prepareStatement(
|
||||
"DELETE FROM WorkedGrossField WHERE lastWorkedEpochMs > 0 AND lastWorkedEpochMs < ?;")) {
|
||||
deleteGrossFieldsStatement.setLong(1, expirationThresholdEpochMs);
|
||||
deleteGrossFieldsStatement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a chatmember with its metadata in the database. The unique key is always
|
||||
@@ -563,9 +495,9 @@ public class DBController {
|
||||
|
||||
String insertOrUpdateSql =
|
||||
"INSERT INTO ChatMember ("
|
||||
+ "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, worked50, worked70, "
|
||||
+ "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, notQRV50, notQRV70, lastFlagsChangeEpochMs"
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, "
|
||||
+ "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, lastFlagsChangeEpochMs"
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "ON CONFLICT(callsign) DO UPDATE SET "
|
||||
+ "qra = excluded.qra, "
|
||||
+ "name = excluded.name, "
|
||||
@@ -586,18 +518,14 @@ public class DBController {
|
||||
preparedStatement.setInt(10, helper_booleanIntConverter(chatMemberToStore.isWorked3400()));
|
||||
preparedStatement.setInt(11, helper_booleanIntConverter(chatMemberToStore.isWorked5600()));
|
||||
preparedStatement.setInt(12, helper_booleanIntConverter(chatMemberToStore.isWorked10G()));
|
||||
preparedStatement.setInt(13, helper_booleanIntConverter(chatMemberToStore.isWorked50()));
|
||||
preparedStatement.setInt(14, helper_booleanIntConverter(chatMemberToStore.isWorked70()));
|
||||
preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv144()));
|
||||
preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv432()));
|
||||
preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv1240()));
|
||||
preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv2300()));
|
||||
preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
|
||||
preparedStatement.setInt(20, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
|
||||
preparedStatement.setInt(21, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
|
||||
preparedStatement.setInt(22, helper_booleanIntConverter(!chatMemberToStore.isQrv50()));
|
||||
preparedStatement.setInt(23, helper_booleanIntConverter(!chatMemberToStore.isQrv70()));
|
||||
preparedStatement.setLong(24, resolvedLastFlagsChangeEpochMs);
|
||||
preparedStatement.setInt(13, helper_booleanIntConverter(!chatMemberToStore.isQrv144()));
|
||||
preparedStatement.setInt(14, helper_booleanIntConverter(!chatMemberToStore.isQrv432()));
|
||||
preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv1240()));
|
||||
preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv2300()));
|
||||
preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
|
||||
preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
|
||||
preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
|
||||
preparedStatement.setLong(20, resolvedLastFlagsChangeEpochMs);
|
||||
preparedStatement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
System.err.println("[DBH, ERROR:] Chatmember could not been stored.");
|
||||
@@ -679,19 +607,13 @@ public class DBController {
|
||||
|
||||
String resetAllWorkedDataSql =
|
||||
"UPDATE ChatMember SET "
|
||||
+ "worked = 0, worked144 = 0, worked432 = 0, worked1240 = 0, worked2300 = 0, worked3400 = 0, worked5600 = 0, worked10G = 0, worked50 = 0, worked70 = 0, "
|
||||
+ "notQRV144 = 0, notQRV432 = 0, notQRV1240 = 0, notQRV2300 = 0, notQRV3400 = 0, notQRV5600 = 0, notQRV10G = 0, notQRV50 = 0, notQRV70 = 0, "
|
||||
+ "worked = 0, worked144 = 0, worked432 = 0, worked1240 = 0, worked2300 = 0, worked3400 = 0, worked5600 = 0, worked10G = 0, "
|
||||
+ "notQRV144 = 0, notQRV432 = 0, notQRV1240 = 0, notQRV2300 = 0, notQRV3400 = 0, notQRV5600 = 0, notQRV10G = 0, "
|
||||
+ "lastFlagsChangeEpochMs = 0;";
|
||||
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
// return statement.executeUpdate(resetAllWorkedDataSql);
|
||||
int affectedRows = statement.executeUpdate(resetAllWorkedDataSql);
|
||||
statement.executeUpdate("DELETE FROM WorkedGrossField;");
|
||||
return affectedRows;
|
||||
|
||||
|
||||
return statement.executeUpdate(resetAllWorkedDataSql);
|
||||
} catch (SQLException e) {
|
||||
|
||||
System.err.println("[DBH, ERROR:] Couldn't reset the worked data");
|
||||
e.printStackTrace();
|
||||
return -1;
|
||||
@@ -715,30 +637,16 @@ public class DBController {
|
||||
String workedBandColumnName = helper_resolveWorkedBandColumnName(chatMemberToStore);
|
||||
|
||||
if (workedBandColumnName == null) {
|
||||
return helper_updateWorkedAnyOnChatMember(chatMemberToStore);
|
||||
System.out.println("[DBCtrl, Error]: unknown at which band the qso had been!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// String updateWorkedSql =
|
||||
// "UPDATE ChatMember SET worked = 1, " + workedBandColumnName + " = 1, lastFlagsChangeEpochMs = ? WHERE callsign = ?;";
|
||||
|
||||
String updateWorkedSql =
|
||||
"UPDATE ChatMember SET worked = 1, "
|
||||
+ workedBandColumnName
|
||||
+ " = 1, "
|
||||
+ "qra = CASE WHEN ? IS NOT NULL AND TRIM(?) <> '' AND LOWER(TRIM(?)) <> 'unknown' THEN ? ELSE qra END, "
|
||||
+ "lastFlagsChangeEpochMs = ? WHERE callsign = ?;";
|
||||
"UPDATE ChatMember SET worked = 1, " + workedBandColumnName + " = 1, lastFlagsChangeEpochMs = ? WHERE callsign = ?;";
|
||||
|
||||
try (PreparedStatement preparedStatement = connection.prepareStatement(updateWorkedSql)) {
|
||||
// preparedStatement.setLong(1, System.currentTimeMillis());
|
||||
// preparedStatement.setString(2, chatMemberToStore.getCallSignRaw());
|
||||
|
||||
String qra = chatMemberToStore.getQra();
|
||||
preparedStatement.setString(1, qra);
|
||||
preparedStatement.setString(2, qra);
|
||||
preparedStatement.setString(3, qra);
|
||||
preparedStatement.setString(4, qra);
|
||||
preparedStatement.setLong(5, System.currentTimeMillis());
|
||||
preparedStatement.setString(6, chatMemberToStore.getCallSignRaw());
|
||||
preparedStatement.setLong(1, System.currentTimeMillis());
|
||||
preparedStatement.setString(2, chatMemberToStore.getCallSignRaw());
|
||||
|
||||
int affectedRows = preparedStatement.executeUpdate();
|
||||
return affectedRows > 0;
|
||||
@@ -749,47 +657,6 @@ public class DBController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates only the global worked-any flag of a stored chatmember row.
|
||||
*
|
||||
* <p>This is used when an external logger confirms that a station was worked,
|
||||
* but the software cannot reliably map the QSO to one of the persisted band
|
||||
* columns. The UI status "x" is based on this global worked flag, so this method
|
||||
* makes no-band or unsupported-band log entries persistent as worked-any.</p>
|
||||
*
|
||||
* @param chatMemberToStore chatmember that contains the worked call and optional locator
|
||||
* @return true if an existing database row was updated
|
||||
* @throws SQLException if the database write fails
|
||||
*/
|
||||
private synchronized boolean helper_updateWorkedAnyOnChatMember(ChatMember chatMemberToStore) throws SQLException {
|
||||
|
||||
if (chatMemberToStore == null
|
||||
|| chatMemberToStore.getCallSignRaw() == null
|
||||
|| chatMemberToStore.getCallSignRaw().isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String updateWorkedAnySql =
|
||||
"UPDATE ChatMember SET worked = 1, "
|
||||
+ "qra = CASE WHEN ? IS NOT NULL AND TRIM(?) <> '' AND LOWER(TRIM(?)) <> 'unknown' THEN ? ELSE qra END, "
|
||||
+ "lastFlagsChangeEpochMs = ? WHERE callsign = ?;";
|
||||
|
||||
try (PreparedStatement preparedStatement = connection.prepareStatement(updateWorkedAnySql)) {
|
||||
String qra = chatMemberToStore.getQra();
|
||||
|
||||
preparedStatement.setString(1, qra);
|
||||
preparedStatement.setString(2, qra);
|
||||
preparedStatement.setString(3, qra);
|
||||
preparedStatement.setString(4, qra);
|
||||
preparedStatement.setLong(5, System.currentTimeMillis());
|
||||
preparedStatement.setString(6, chatMemberToStore.getCallSignRaw());
|
||||
|
||||
int affectedRows = preparedStatement.executeUpdate();
|
||||
return affectedRows > 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates all not-QRV flags for a chatmember row. The method uses the normalized
|
||||
* raw callsign and updates the timestamp so that automatic contest cleanup can
|
||||
@@ -814,8 +681,6 @@ public class DBController {
|
||||
+ "notQRV3400 = ?, "
|
||||
+ "notQRV5600 = ?, "
|
||||
+ "notQRV10G = ?, "
|
||||
+ "notQRV50 = ?, "
|
||||
+ "notQRV70 = ?, "
|
||||
+ "lastFlagsChangeEpochMs = ? "
|
||||
+ "WHERE callsign = ?;";
|
||||
|
||||
@@ -827,10 +692,8 @@ public class DBController {
|
||||
preparedStatement.setInt(5, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
|
||||
preparedStatement.setInt(6, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
|
||||
preparedStatement.setInt(7, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
|
||||
preparedStatement.setInt(8, helper_booleanIntConverter(!chatMemberToStore.isQrv50()));
|
||||
preparedStatement.setInt(9, helper_booleanIntConverter(!chatMemberToStore.isQrv70()));
|
||||
preparedStatement.setLong(10, System.currentTimeMillis());
|
||||
preparedStatement.setString(11, chatMemberToStore.getCallSignRaw());
|
||||
preparedStatement.setLong(8, System.currentTimeMillis());
|
||||
preparedStatement.setString(9, chatMemberToStore.getCallSignRaw());
|
||||
|
||||
int affectedRows = preparedStatement.executeUpdate();
|
||||
|
||||
@@ -858,9 +721,9 @@ public class DBController {
|
||||
|
||||
String upsertCompleteRowSql =
|
||||
"INSERT INTO ChatMember ("
|
||||
+ "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, worked50, worked70, "
|
||||
+ "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, notQRV50, notQRV70, lastFlagsChangeEpochMs"
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "callsign, qra, name, lastActivityDateTime, worked, worked144, worked432, worked1240, worked2300, worked3400, worked5600, worked10G, "
|
||||
+ "notQRV144, notQRV432, notQRV1240, notQRV2300, notQRV3400, notQRV5600, notQRV10G, lastFlagsChangeEpochMs"
|
||||
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
+ "ON CONFLICT(callsign) DO UPDATE SET "
|
||||
+ "qra = excluded.qra, "
|
||||
+ "name = excluded.name, "
|
||||
@@ -873,8 +736,6 @@ public class DBController {
|
||||
+ "worked3400 = excluded.worked3400, "
|
||||
+ "worked5600 = excluded.worked5600, "
|
||||
+ "worked10G = excluded.worked10G, "
|
||||
+ "worked50 = excluded.worked50, "
|
||||
+ "worked70 = excluded.worked70, "
|
||||
+ "notQRV144 = excluded.notQRV144, "
|
||||
+ "notQRV432 = excluded.notQRV432, "
|
||||
+ "notQRV1240 = excluded.notQRV1240, "
|
||||
@@ -882,8 +743,6 @@ public class DBController {
|
||||
+ "notQRV3400 = excluded.notQRV3400, "
|
||||
+ "notQRV5600 = excluded.notQRV5600, "
|
||||
+ "notQRV10G = excluded.notQRV10G, "
|
||||
+ "notQRV50 = excluded.notQRV50, "
|
||||
+ "notQRV70 = excluded.notQRV70, "
|
||||
+ "lastFlagsChangeEpochMs = excluded.lastFlagsChangeEpochMs;";
|
||||
|
||||
try (PreparedStatement preparedStatement = connection.prepareStatement(upsertCompleteRowSql)) {
|
||||
@@ -899,18 +758,14 @@ public class DBController {
|
||||
preparedStatement.setInt(10, helper_booleanIntConverter(chatMemberToStore.isWorked3400()));
|
||||
preparedStatement.setInt(11, helper_booleanIntConverter(chatMemberToStore.isWorked5600()));
|
||||
preparedStatement.setInt(12, helper_booleanIntConverter(chatMemberToStore.isWorked10G()));
|
||||
preparedStatement.setInt(13, helper_booleanIntConverter(chatMemberToStore.isWorked50()));
|
||||
preparedStatement.setInt(14, helper_booleanIntConverter(chatMemberToStore.isWorked70()));
|
||||
preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv144()));
|
||||
preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv432()));
|
||||
preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv1240()));
|
||||
preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv2300()));
|
||||
preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
|
||||
preparedStatement.setInt(20, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
|
||||
preparedStatement.setInt(21, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
|
||||
preparedStatement.setInt(22, helper_booleanIntConverter(!chatMemberToStore.isQrv50()));
|
||||
preparedStatement.setInt(23, helper_booleanIntConverter(!chatMemberToStore.isQrv70()));
|
||||
preparedStatement.setLong(24, chatMemberToStore.getLastFlagsChangeEpochMs());
|
||||
preparedStatement.setInt(13, helper_booleanIntConverter(!chatMemberToStore.isQrv144()));
|
||||
preparedStatement.setInt(14, helper_booleanIntConverter(!chatMemberToStore.isQrv432()));
|
||||
preparedStatement.setInt(15, helper_booleanIntConverter(!chatMemberToStore.isQrv1240()));
|
||||
preparedStatement.setInt(16, helper_booleanIntConverter(!chatMemberToStore.isQrv2300()));
|
||||
preparedStatement.setInt(17, helper_booleanIntConverter(!chatMemberToStore.isQrv3400()));
|
||||
preparedStatement.setInt(18, helper_booleanIntConverter(!chatMemberToStore.isQrv5600()));
|
||||
preparedStatement.setInt(19, helper_booleanIntConverter(!chatMemberToStore.isQrv10G()));
|
||||
preparedStatement.setLong(20, chatMemberToStore.getLastFlagsChangeEpochMs());
|
||||
preparedStatement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("[DBH, ERROR:] Could not rebuild normalized ChatMember row", e);
|
||||
@@ -939,8 +794,6 @@ public class DBController {
|
||||
builtChatMember.setWorked3400(helper_IntToBooleanConverter(resultSet.getInt("worked3400")));
|
||||
builtChatMember.setWorked5600(helper_IntToBooleanConverter(resultSet.getInt("worked5600")));
|
||||
builtChatMember.setWorked10G(helper_IntToBooleanConverter(resultSet.getInt("worked10G")));
|
||||
builtChatMember.setWorked50(helper_IntToBooleanConverter(resultSet.getInt("worked50")));
|
||||
builtChatMember.setWorked70(helper_IntToBooleanConverter(resultSet.getInt("worked70")));
|
||||
builtChatMember.setQrv144(!helper_IntToBooleanConverter(resultSet.getInt("notQRV144")));
|
||||
builtChatMember.setQrv432(!helper_IntToBooleanConverter(resultSet.getInt("notQRV432")));
|
||||
builtChatMember.setQrv1240(!helper_IntToBooleanConverter(resultSet.getInt("notQRV1240")));
|
||||
@@ -948,8 +801,6 @@ public class DBController {
|
||||
builtChatMember.setQrv3400(!helper_IntToBooleanConverter(resultSet.getInt("notQRV3400")));
|
||||
builtChatMember.setQrv5600(!helper_IntToBooleanConverter(resultSet.getInt("notQRV5600")));
|
||||
builtChatMember.setQrv10G(!helper_IntToBooleanConverter(resultSet.getInt("notQRV10G")));
|
||||
builtChatMember.setQrv50(!helper_IntToBooleanConverter(resultSet.getInt("notQRV50")));
|
||||
builtChatMember.setQrv70(!helper_IntToBooleanConverter(resultSet.getInt("notQRV70")));
|
||||
builtChatMember.setLastFlagsChangeEpochMs(resultSet.getLong("lastFlagsChangeEpochMs"));
|
||||
|
||||
return builtChatMember;
|
||||
@@ -971,8 +822,6 @@ public class DBController {
|
||||
targetChatMember.setWorked3400(sourceChatMember.isWorked3400());
|
||||
targetChatMember.setWorked5600(sourceChatMember.isWorked5600());
|
||||
targetChatMember.setWorked10G(sourceChatMember.isWorked10G());
|
||||
targetChatMember.setWorked50(sourceChatMember.isWorked50());
|
||||
targetChatMember.setWorked70(sourceChatMember.isWorked70());
|
||||
targetChatMember.setQrv144(sourceChatMember.isQrv144());
|
||||
targetChatMember.setQrv432(sourceChatMember.isQrv432());
|
||||
targetChatMember.setQrv1240(sourceChatMember.isQrv1240());
|
||||
@@ -980,8 +829,6 @@ public class DBController {
|
||||
targetChatMember.setQrv3400(sourceChatMember.isQrv3400());
|
||||
targetChatMember.setQrv5600(sourceChatMember.isQrv5600());
|
||||
targetChatMember.setQrv10G(sourceChatMember.isQrv10G());
|
||||
targetChatMember.setQrv50(sourceChatMember.isQrv50());
|
||||
targetChatMember.setQrv70(sourceChatMember.isQrv70());
|
||||
targetChatMember.setLastFlagsChangeEpochMs(sourceChatMember.getLastFlagsChangeEpochMs());
|
||||
}
|
||||
|
||||
@@ -1007,10 +854,6 @@ public class DBController {
|
||||
return "worked5600";
|
||||
} else if (chatMemberToStore.isWorked10G()) {
|
||||
return "worked10G";
|
||||
} else if (chatMemberToStore.isWorked50()) {
|
||||
return "worked50";
|
||||
} else if (chatMemberToStore.isWorked70()) {
|
||||
return "worked70";
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1053,17 +896,13 @@ public class DBController {
|
||||
|| chatMemberToStore.isWorked3400()
|
||||
|| chatMemberToStore.isWorked5600()
|
||||
|| chatMemberToStore.isWorked10G()
|
||||
|| chatMemberToStore.isWorked50()
|
||||
|| chatMemberToStore.isWorked70()
|
||||
|| !chatMemberToStore.isQrv144()
|
||||
|| !chatMemberToStore.isQrv432()
|
||||
|| !chatMemberToStore.isQrv1240()
|
||||
|| !chatMemberToStore.isQrv2300()
|
||||
|| !chatMemberToStore.isQrv3400()
|
||||
|| !chatMemberToStore.isQrv5600()
|
||||
|| !chatMemberToStore.isQrv10G()
|
||||
|| !chatMemberToStore.isQrv50()
|
||||
|| !chatMemberToStore.isQrv70();
|
||||
|| !chatMemberToStore.isQrv10G();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1116,79 +955,4 @@ public class DBController {
|
||||
// dbc.storeChatMember(dummy);
|
||||
// dbc.updateWkdInfoOnChatMember(dummy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates a worked gross field for the new-locator filter.
|
||||
*
|
||||
* @param band worked band
|
||||
* @param locator6 six-character Maidenhead locator
|
||||
* @param callsignRaw normalized raw callsign or null
|
||||
* @param source source identifier such as UCXLOG or WINTEST
|
||||
*/
|
||||
public synchronized void upsertWorkedGrossField(Band band, String locator6, String callsignRaw, String source) {
|
||||
|
||||
String grossField = WorkedGrossFieldCache.extractGrossField(locator6);
|
||||
String normalizedLocator6 = WorkedGrossFieldCache.extractLocator6(locator6);
|
||||
|
||||
if (band == null || grossField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String upsertSql =
|
||||
"INSERT INTO WorkedGrossField (band, grossField, locator, callsign, source, lastWorkedEpochMs) "
|
||||
+ "VALUES (?, ?, ?, ?, ?, ?) "
|
||||
+ "ON CONFLICT(band, grossField) DO UPDATE SET "
|
||||
+ "locator = COALESCE(excluded.locator, WorkedGrossField.locator), "
|
||||
+ "callsign = COALESCE(excluded.callsign, WorkedGrossField.callsign), "
|
||||
+ "source = excluded.source, "
|
||||
+ "lastWorkedEpochMs = excluded.lastWorkedEpochMs;";
|
||||
|
||||
try (PreparedStatement preparedStatement = connection.prepareStatement(upsertSql)) {
|
||||
preparedStatement.setString(1, band.name());
|
||||
preparedStatement.setString(2, grossField);
|
||||
preparedStatement.setString(3, normalizedLocator6);
|
||||
preparedStatement.setString(4, callsignRaw);
|
||||
preparedStatement.setString(5, source == null ? "UNKNOWN" : source);
|
||||
preparedStatement.setLong(6, System.currentTimeMillis());
|
||||
preparedStatement.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("[DBH, ERROR:] Could not upsert worked gross field", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all non-expired worked gross fields from the database.
|
||||
*
|
||||
* @return map of band to worked gross fields
|
||||
*/
|
||||
public synchronized Map<Band, Set<String>> fetchWorkedGrossFieldsFromDB() {
|
||||
|
||||
resetExpiredWorkedDataIfRequired();
|
||||
|
||||
Map<Band, Set<String>> result = new EnumMap<>(Band.class);
|
||||
|
||||
try (Statement statement = connection.createStatement();
|
||||
ResultSet resultSet = statement.executeQuery("SELECT band, grossField FROM WorkedGrossField ORDER BY band, grossField;")) {
|
||||
|
||||
while (resultSet.next()) {
|
||||
Band band;
|
||||
try {
|
||||
band = Band.valueOf(resultSet.getString("band"));
|
||||
} catch (Exception ignored) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String grossField = WorkedGrossFieldCache.extractGrossField(resultSet.getString("grossField"));
|
||||
if (grossField == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.computeIfAbsent(band, ignored -> new HashSet<>()).add(grossField);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("[DBH, ERROR:] Could not fetch worked gross fields", e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,363 +1,237 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.*;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class DXClusterThreadPooledServer implements Runnable {
|
||||
public class DXClusterThreadPooledServer implements Runnable{
|
||||
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(DXClusterThreadPooledServer.class.getName());
|
||||
private static final String THREAD_NICKNAME = "DXCluster-Server";
|
||||
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
|
||||
|
||||
private final List<Socket> clientSockets =
|
||||
Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
private final ChatController chatController;
|
||||
private final ThreadStatusCallback callBackToController;
|
||||
private final int serverPort;
|
||||
|
||||
private final ExecutorService threadPool =
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "DXCluster-Server";
|
||||
ChatController chatController = null;
|
||||
protected int serverPort = 8080;
|
||||
protected ServerSocket serverSocket = null;
|
||||
protected boolean isStopped = false;
|
||||
protected Thread runningThread= null;
|
||||
protected ExecutorService threadPool =
|
||||
Executors.newFixedThreadPool(10);
|
||||
Socket clientSocket;
|
||||
|
||||
private final ScheduledExecutorService keepAliveExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
private volatile boolean stopped;
|
||||
private ServerSocket serverSocket;
|
||||
|
||||
public DXClusterThreadPooledServer(
|
||||
int port,
|
||||
ChatController chatController,
|
||||
ThreadStatusCallback callback
|
||||
) {
|
||||
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
|
||||
this.serverPort = port;
|
||||
this.chatController = chatController;
|
||||
this.callBackToController = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("DXCluster-thread-pooled-server");
|
||||
public void run(){
|
||||
|
||||
try {
|
||||
serverSocket = new ServerSocket(serverPort);
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
if (stopped) {
|
||||
return;
|
||||
synchronized(this){
|
||||
this.runningThread = Thread.currentThread();
|
||||
runningThread.setName("DXCluster-thread-pooled-server");
|
||||
}
|
||||
openServerSocket();
|
||||
while(! isStopped()){
|
||||
clientSocket = null;
|
||||
try {
|
||||
clientSocket = this.serverSocket.accept();
|
||||
|
||||
synchronized(clientSockets) {
|
||||
clientSockets.add(clientSocket); // add dx cluster client to the "clients list" for broadcasting
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
if(isStopped()) {
|
||||
System.out.println("Server Stopped.") ;
|
||||
break;
|
||||
}
|
||||
throw new RuntimeException(
|
||||
"Error accepting client connection", e);
|
||||
}
|
||||
|
||||
callBackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
new ThreadStateMessage(
|
||||
THREAD_NICKNAME,
|
||||
true,
|
||||
"Listening on TCP port " + serverPort,
|
||||
false
|
||||
)
|
||||
);
|
||||
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController);
|
||||
|
||||
keepAliveExecutor.scheduleAtFixedRate(
|
||||
this::sendKeepAlive,
|
||||
30,
|
||||
30,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
this.threadPool.execute(worker);
|
||||
|
||||
while (!stopped) {
|
||||
try {
|
||||
Socket clientSocket = serverSocket.accept();
|
||||
clientSockets.add(clientSocket);
|
||||
}
|
||||
this.threadPool.shutdown();
|
||||
System.out.println("Server Stopped.") ;
|
||||
}
|
||||
|
||||
threadPool.execute(
|
||||
new DXClusterServerWorkerRunnable(
|
||||
clientSocket,
|
||||
clientSockets
|
||||
)
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
if (!stopped) {
|
||||
LOGGER.log(
|
||||
Level.SEVERE,
|
||||
"Error accepting DX Cluster client connection",
|
||||
exception
|
||||
);
|
||||
}
|
||||
private synchronized boolean isStopped() {
|
||||
return this.isStopped;
|
||||
}
|
||||
|
||||
public synchronized void stop(){
|
||||
this.isStopped = true;
|
||||
try {
|
||||
this.serverSocket.close();
|
||||
synchronized(clientSockets) {
|
||||
for (Socket socket : clientSockets) {
|
||||
socket.close(); // close all client connections
|
||||
}
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
if (!stopped) {
|
||||
LOGGER.log(
|
||||
Level.SEVERE,
|
||||
"Cannot open DX Cluster TCP port " + serverPort,
|
||||
exception
|
||||
);
|
||||
|
||||
callBackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
new ThreadStateMessage(
|
||||
THREAD_NICKNAME,
|
||||
false,
|
||||
"Cannot open TCP port "
|
||||
+ serverPort
|
||||
+ ": "
|
||||
+ exception.getMessage(),
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
closeServerSocket();
|
||||
closeClientSockets();
|
||||
keepAliveExecutor.shutdownNow();
|
||||
threadPool.shutdownNow();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("DXCCSERVER Error closing server", e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
stopped = true;
|
||||
closeServerSocket();
|
||||
closeClientSockets();
|
||||
keepAliveExecutor.shutdownNow();
|
||||
threadPool.shutdownNow();
|
||||
}
|
||||
|
||||
public boolean hasConnectedClients() {
|
||||
synchronized (clientSockets) {
|
||||
removeClosedClients();
|
||||
return !clientSockets.isEmpty();
|
||||
private void openServerSocket() {
|
||||
try {
|
||||
this.serverSocket = new ServerSocket(this.serverPort);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("DXCCSERVER Cannot open port ", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends one DX Cluster spot to all currently connected clients.
|
||||
* Sends a DX cluster message to ALL connected log programs via telnet, returns true if sent
|
||||
*
|
||||
* @return true if the spot was delivered to at least one client
|
||||
* @param aChatMember
|
||||
* @return boolean true if message had been sent
|
||||
*/
|
||||
public boolean broadcastSingleDXClusterEntryToLoggers(
|
||||
ChatMember chatMember
|
||||
) {
|
||||
final String clusterMessage;
|
||||
public boolean broadcastSingleDXClusterEntryToLoggers(ChatMember aChatMember) {
|
||||
synchronized(clientSockets) {
|
||||
|
||||
try {
|
||||
String frequency = Utils4KST.normalizeFrequencyString(
|
||||
chatMember.getFrequency().getValue(),
|
||||
chatController
|
||||
.getChatPreferences()
|
||||
.getNotify_optionalFrequencyPrefix()
|
||||
);
|
||||
System.out.println("DXClusterSrvr: broadcasting message to clients: " + clientSockets.size());
|
||||
|
||||
clusterMessage =
|
||||
"DX de "
|
||||
+ chatController
|
||||
.getChatPreferences()
|
||||
.getNotify_DXCSrv_SpottersCallSign()
|
||||
.getValue()
|
||||
+ ": "
|
||||
+ frequency
|
||||
+ " "
|
||||
+ chatMember.getCallSign().toUpperCase()
|
||||
+ " "
|
||||
+ chatMember.getQra().toUpperCase()
|
||||
+ " "
|
||||
+ new Utils4KST()
|
||||
.time_generateCurrenthhmmZTimeStringForClusterMessage()
|
||||
+ ((char) 7)
|
||||
+ ((char) 7)
|
||||
+ "\r\n";
|
||||
} catch (Exception exception) {
|
||||
LOGGER.log(
|
||||
Level.SEVERE,
|
||||
"Cannot build DX Cluster message",
|
||||
exception
|
||||
);
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
|
||||
int deliveredClients = 0;
|
||||
|
||||
synchronized (clientSockets) {
|
||||
Iterator<Socket> iterator = clientSockets.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Socket socket = iterator.next();
|
||||
|
||||
if (socket == null || socket.isClosed()) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
output.write(
|
||||
clusterMessage.getBytes(
|
||||
StandardCharsets.US_ASCII
|
||||
)
|
||||
);
|
||||
output.flush();
|
||||
deliveredClients++;
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"DX Cluster client disconnected while sending a spot",
|
||||
exception
|
||||
);
|
||||
|
||||
closeSocket(socket);
|
||||
iterator.remove();
|
||||
}
|
||||
System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue());
|
||||
System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ");
|
||||
} catch (Exception e) {
|
||||
System.out.println("DXCThPooledServer: Error accessing value in chatmember object: " + e.getMessage());
|
||||
// e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (deliveredClients > 0) {
|
||||
callBackToController.onThreadStatus(
|
||||
THREAD_NICKNAME,
|
||||
new ThreadStateMessage(
|
||||
THREAD_NICKNAME,
|
||||
true,
|
||||
"Last spot sent to "
|
||||
+ deliveredClients
|
||||
+ " DX Cluster client(s):\n"
|
||||
+ clusterMessage,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return deliveredClients > 0;
|
||||
}
|
||||
|
||||
private void sendKeepAlive() {
|
||||
synchronized (clientSockets) {
|
||||
Iterator<Socket> iterator = clientSockets.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Socket socket = iterator.next();
|
||||
|
||||
if (socket == null || socket.isClosed()) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
output.write(
|
||||
"\r\n".getBytes(StandardCharsets.US_ASCII)
|
||||
);
|
||||
output.flush();
|
||||
} catch (IOException exception) {
|
||||
closeSocket(socket);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeClosedClients() {
|
||||
clientSockets.removeIf(
|
||||
socket -> socket == null || socket.isClosed()
|
||||
);
|
||||
}
|
||||
|
||||
private synchronized void closeServerSocket() {
|
||||
if (serverSocket == null || serverSocket.isClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
serverSocket.close();
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Error closing DX Cluster server socket",
|
||||
exception
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void closeClientSockets() {
|
||||
synchronized (clientSockets) {
|
||||
for (Socket socket : clientSockets) {
|
||||
closeSocket(socket);
|
||||
|
||||
try {
|
||||
|
||||
OutputStream output = socket.getOutputStream();
|
||||
|
||||
String singleDXClusterMessage = "DX de ";
|
||||
|
||||
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
|
||||
|
||||
|
||||
|
||||
|
||||
singleDXClusterMessage += this.chatController.getChatPreferences().getNotify_DXCSrv_SpottersCallSign().getValue() + ": ";
|
||||
singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
|
||||
singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " "; //we need such an amount of spaces for n1mm to work, otherwise bullshit happens
|
||||
singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
|
||||
singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
|
||||
|
||||
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
|
||||
// singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
|
||||
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
|
||||
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
|
||||
|
||||
output.write((singleDXClusterMessage).getBytes());
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last msg to " + clientSockets.size() + " Cluster Clients:\n" + singleDXClusterMessage, false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
clientSockets.clear();
|
||||
}
|
||||
return true; //if message had been sent, return true for "ok"
|
||||
}
|
||||
|
||||
private static void closeSocket(Socket socket) {
|
||||
if (socket == null || socket.isClosed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ignored) {
|
||||
// The connection is already unusable.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DXClusterServerWorkerRunnable implements Runnable {
|
||||
class DXClusterServerWorkerRunnable implements Runnable{
|
||||
|
||||
private static final Logger LOGGER =
|
||||
Logger.getLogger(DXClusterServerWorkerRunnable.class.getName());
|
||||
protected Socket clientSocket = null;
|
||||
protected String serverText = null;
|
||||
private ChatController client = null;
|
||||
private List<Socket> dxClusterClientSocketsConnectedList;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "DXCluster-Server";
|
||||
|
||||
private final Socket clientSocket;
|
||||
private final List<Socket> clientSockets;
|
||||
|
||||
DXClusterServerWorkerRunnable(
|
||||
Socket clientSocket,
|
||||
List<Socket> clientSockets
|
||||
) {
|
||||
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) {
|
||||
this.clientSocket = clientSocket;
|
||||
this.clientSockets = clientSockets;
|
||||
this.serverText = serverText;
|
||||
this.client = chatController;
|
||||
this.dxClusterClientSocketsConnectedList = clientSockets;
|
||||
this.callBackToController = callback;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
OutputStream output = clientSocket.getOutputStream();
|
||||
output.write(
|
||||
"login: ".getBytes(StandardCharsets.US_ASCII)
|
||||
);
|
||||
output.flush();
|
||||
dxClusterClientSocketsConnectedList.add(clientSocket);
|
||||
|
||||
System.out.println(
|
||||
"[DXClusterServer] New client connected: "
|
||||
+ clientSocket.getInetAddress()
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
LOGGER.log(
|
||||
Level.WARNING,
|
||||
"Cannot initialise DX Cluster client connection",
|
||||
exception
|
||||
);
|
||||
Timer dXCkeepAliveTimer = new Timer();
|
||||
dXCkeepAliveTimer.schedule(new TimerTask() {
|
||||
|
||||
synchronized (clientSockets) {
|
||||
clientSockets.remove(clientSocket);
|
||||
}
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
clientSocket.close();
|
||||
} catch (IOException ignored) {
|
||||
// The connection is already unusable.
|
||||
StringBuilder connectedClients = new StringBuilder(); //only for statistics
|
||||
|
||||
for (Socket socket : dxClusterClientSocketsConnectedList) {
|
||||
|
||||
connectedClients.append(socket.getInetAddress()).append("\n");
|
||||
|
||||
try {
|
||||
OutputStream output = socket.getOutputStream();
|
||||
output.write(("\r\n").getBytes());
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
|
||||
dXCkeepAliveTimer.purge();
|
||||
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
finally {
|
||||
this.cancel();
|
||||
}
|
||||
dxClusterClientSocketsConnectedList.remove(socket); //if socket is closed by client, remove it from the broadcast list and close it
|
||||
}
|
||||
}
|
||||
|
||||
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
|
||||
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
|
||||
}
|
||||
}, 30000, 30000);
|
||||
|
||||
|
||||
output.write(("login: ").getBytes()); //say hello to the client, it will answer with a callsign
|
||||
System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
synchronized(dxClusterClientSocketsConnectedList) {
|
||||
dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package kst4contest.controller;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import kst4contest.model.ChatMessage;
|
||||
|
||||
@@ -15,7 +13,6 @@ import kst4contest.model.ChatMessage;
|
||||
* No need for it as it´s not longer a console application
|
||||
*/
|
||||
public class InputReaderThread extends Thread {
|
||||
private static final Logger LOGGER = Logger.getLogger(InputReaderThread.class.getName());
|
||||
private PrintWriter writer;
|
||||
private Socket socket;
|
||||
private ChatController client;
|
||||
@@ -42,7 +39,8 @@ public class InputReaderThread extends Thread {
|
||||
try {
|
||||
sendThisMessage23001 = reader.readLine();
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.SEVERE, "Error reading from stdin", e);
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
ownMSG.setMessageText("MSG|" + this.client.getChatCategoryMain().getCategoryNumber() + "|0|" + sendThisMessage23001 + "|0|");
|
||||
@@ -55,8 +53,8 @@ public class InputReaderThread extends Thread {
|
||||
try {
|
||||
this.sleep(500);
|
||||
} catch (InterruptedException e) {
|
||||
LOGGER.log(Level.SEVERE, "InputReaderThread interrupted", e);
|
||||
Thread.currentThread().interrupt();
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
//package kst4contest.controller;
|
||||
//
|
||||
//import kst4contest.model.Band;
|
||||
//import org.junit.jupiter.api.Test;
|
||||
//import org.junit.jupiter.params.ParameterizedTest;
|
||||
//import org.junit.jupiter.params.provider.ValueSource;
|
||||
//
|
||||
//import java.util.regex.Matcher;
|
||||
//import java.util.regex.Pattern;
|
||||
//
|
||||
//import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
//import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
//import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
//import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
//import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
//
|
||||
//class MessageBusManagementThreadFrequencyContextTest {
|
||||
//
|
||||
// private static final Pattern THREE_DIGIT_VALUE =
|
||||
// Pattern.compile("\\b\\d{3}\\b");
|
||||
//
|
||||
// @ParameterizedTest
|
||||
// @ValueSource(strings = {
|
||||
// "qrg 210",
|
||||
// "QRG: 210",
|
||||
// "freq is 210",
|
||||
// "frequency = 210",
|
||||
// "on 210",
|
||||
// "210 MHz",
|
||||
// "210 qrg"
|
||||
// })
|
||||
// void acceptsBareThreeDigitValueWithFrequencyContext(String messageText) {
|
||||
// Matcher matcher = findThreeDigitValue(messageText);
|
||||
//
|
||||
// assertTrue(
|
||||
// MessageBusManagementThread
|
||||
// .hasExplicitBareFrequencyContext(
|
||||
// messageText,
|
||||
// matcher.start(),
|
||||
// matcher.end()
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// @ParameterizedTest
|
||||
// @ValueSource(strings = {
|
||||
// "599",
|
||||
// "144",
|
||||
// "serial 210",
|
||||
// "score 210",
|
||||
// "worked 210 stations"
|
||||
// })
|
||||
// void rejectsBareThreeDigitValueWithoutFrequencyContext(String messageText) {
|
||||
// Matcher matcher = findThreeDigitValue(messageText);
|
||||
//
|
||||
// assertFalse(
|
||||
// MessageBusManagementThread
|
||||
// .hasExplicitBareFrequencyContext(
|
||||
// messageText,
|
||||
// matcher.start(),
|
||||
// matcher.end()
|
||||
// )
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// @Test
|
||||
// void resolvesOnlySupportedFallbackPrefixes() {
|
||||
// assertEquals(Band.B_144, Band.fromPrefix("144"));
|
||||
// assertEquals(Band.B_432, Band.fromPrefix(" 432 "));
|
||||
// assertEquals(Band.B_10G, Band.fromPrefix("10368"));
|
||||
// assertNull(Band.fromPrefix("999"));
|
||||
// assertNull(Band.fromPrefix(null));
|
||||
// }
|
||||
//
|
||||
// private Matcher findThreeDigitValue(String messageText) {
|
||||
// Matcher matcher = THREE_DIGIT_VALUE.matcher(messageText);
|
||||
// assertTrue(matcher.find(), "Test message must contain a three-digit value");
|
||||
// assertNotNull(matcher.group());
|
||||
// return matcher;
|
||||
// }
|
||||
//}
|
||||
@@ -1,154 +0,0 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import kst4contest.model.AirPlane;
|
||||
import kst4contest.model.AirPlaneReflectionInfo;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
/**
|
||||
* Resolves variables used in operator messages, shortcuts, snippets and
|
||||
* beacons.
|
||||
*
|
||||
* <p>Global variables only depend on the local station configuration and can
|
||||
* therefore be used in every message context. Station variables additionally
|
||||
* require a selected remote station. Keeping both groups in one resolver
|
||||
* prevents the send field and the beacon tasks from implementing different
|
||||
* replacement rules.</p>
|
||||
*/
|
||||
public final class MessageVariableResolver {
|
||||
|
||||
private static final int SHORT_VALUE_LENGTH = 7;
|
||||
|
||||
private final ChatPreferences chatPreferences;
|
||||
|
||||
/**
|
||||
* Creates a resolver backed by the live application preferences.
|
||||
*
|
||||
* @param chatPreferences preferences that provide current station values
|
||||
*/
|
||||
public MessageVariableResolver(ChatPreferences chatPreferences) {
|
||||
this.chatPreferences = Objects.requireNonNull(chatPreferences, "chatPreferences");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves variables which do not require a selected remote station.
|
||||
*
|
||||
* <p>The replacement is literal rather than regular-expression based.
|
||||
* Callsigns, locators and frequencies are data, not regular expressions.</p>
|
||||
*
|
||||
* @param template text that may contain variables
|
||||
* @return text with all available global variables resolved, or {@code null}
|
||||
* when the supplied template is {@code null}
|
||||
*/
|
||||
public String resolveGlobalVariables(String template) {
|
||||
if (template == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String primaryFrequency = valueOrEmpty(chatPreferences.getMYQRGFirstCat().getValue());
|
||||
String secondaryFrequency = valueOrEmpty(chatPreferences.getMYQRGSecondCat().getValue());
|
||||
String ownLocator = valueOrEmpty(chatPreferences.getStn_loginLocatorMainCat());
|
||||
|
||||
String resolvedText = template;
|
||||
resolvedText = resolvedText.replace("MYQRGSHORT", abbreviate(primaryFrequency));
|
||||
resolvedText = resolvedText.replace("MYQRG", primaryFrequency);
|
||||
resolvedText = resolvedText.replace("SECONDQRG", secondaryFrequency);
|
||||
resolvedText = resolvedText.replace("MYLOCATORSHORT", abbreviateLocator(ownLocator));
|
||||
resolvedText = resolvedText.replace("MYLOCATOR", ownLocator);
|
||||
resolvedText = resolvedText.replace("MYCALL", valueOrEmpty(chatPreferences.getStn_loginCallSign()));
|
||||
resolvedText = resolvedText.replace("MYQTF", formatHeading(chatPreferences.getActualQTF().getValue().doubleValue()));
|
||||
return resolvedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves global variables and variables derived from a selected station.
|
||||
*
|
||||
* <p>If no station is selected, station-specific placeholders remain visible.
|
||||
* This is intentional: silently removing {@code QRZNAME}, {@code FIRSTAP} or
|
||||
* {@code SECONDAP} could create a plausible-looking but incomplete message.</p>
|
||||
*
|
||||
* @param template text that may contain variables
|
||||
* @param selectedStation currently selected remote station, may be {@code null}
|
||||
* @return resolved message text
|
||||
*/
|
||||
public String resolveForSelectedStation(String template, ChatMember selectedStation) {
|
||||
String resolvedText = resolveGlobalVariables(template);
|
||||
|
||||
if (resolvedText == null || selectedStation == null) {
|
||||
return resolvedText;
|
||||
}
|
||||
|
||||
resolvedText = resolvedText.replace("QRZNAME", resolveStationName(selectedStation));
|
||||
resolvedText = resolvedText.replace("FIRSTAP", resolveFirstAirPlane(selectedStation));
|
||||
resolvedText = resolvedText.replace("SECONDAP", resolveSecondAirPlane(selectedStation));
|
||||
return resolvedText;
|
||||
}
|
||||
|
||||
private String resolveStationName(ChatMember selectedStation) {
|
||||
String stationName = valueOrEmpty(selectedStation.getName()).trim();
|
||||
|
||||
if (!stationName.isEmpty()) {
|
||||
return stationName;
|
||||
}
|
||||
|
||||
return valueOrEmpty(selectedStation.getCallSign());
|
||||
}
|
||||
|
||||
private String resolveFirstAirPlane(ChatMember selectedStation) {
|
||||
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
|
||||
|
||||
if (risingAirPlanes.isEmpty()) {
|
||||
return "no ap available";
|
||||
}
|
||||
|
||||
AirPlane firstAirPlane = risingAirPlanes.get(0);
|
||||
return "a " + firstAirPlane.getPotencialDescriptionAsWord()
|
||||
+ " in " + firstAirPlane.getArrivingDurationMinutes() + " min";
|
||||
}
|
||||
|
||||
private String resolveSecondAirPlane(ChatMember selectedStation) {
|
||||
List<AirPlane> risingAirPlanes = getRisingAirPlanes(selectedStation);
|
||||
|
||||
if (risingAirPlanes.size() < 2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
AirPlane secondAirPlane = risingAirPlanes.get(1);
|
||||
return "Next " + secondAirPlane.getPotencialDescriptionAsWord()
|
||||
+ " in " + secondAirPlane.getArrivingDurationMinutes() + " min";
|
||||
}
|
||||
|
||||
private List<AirPlane> getRisingAirPlanes(ChatMember selectedStation) {
|
||||
AirPlaneReflectionInfo reflectionInfo = selectedStation.getAirPlaneReflectInfo();
|
||||
|
||||
if (reflectionInfo == null || reflectionInfo.getRisingAirplanes() == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return reflectionInfo.getRisingAirplanes();
|
||||
}
|
||||
|
||||
private String abbreviate(String value) {
|
||||
return value.substring(0, Math.min(value.length(), SHORT_VALUE_LENGTH));
|
||||
}
|
||||
|
||||
private String abbreviateLocator(String locator) {
|
||||
return locator.substring(0, Math.min(locator.length(), 4));
|
||||
}
|
||||
|
||||
private String formatHeading(double headingDegrees) {
|
||||
if (!Double.isFinite(headingDegrees)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return BigDecimal.valueOf(headingDegrees).stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
private String valueOrEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
}
|
||||
@@ -1,837 +0,0 @@
|
||||
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 (Exception exception) {
|
||||
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 exception) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
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;
|
||||
|
||||
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|"));
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
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,671 +0,0 @@
|
||||
package kst4contest.controller;
|
||||
import kst4contest.logic.BandOpportunityResolver;
|
||||
import kst4contest.view.map.MapCallsignRawSnapshot;
|
||||
import kst4contest.logic.PropagationFrequencyResolver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import kst4contest.locatorUtils.Location;
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.view.map.GeometryOnlyPathAnalysisService;
|
||||
|
||||
import kst4contest.view.map.OpenMeteoTerrainProfileProvider;
|
||||
import kst4contest.view.map.PathAnalysisRequest;
|
||||
import kst4contest.view.map.PathAnalysisResult;
|
||||
import kst4contest.view.map.PathAnalysisService;
|
||||
import kst4contest.view.map.PathGeometryUtils;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Central service for tropo/path reachability calculations.
|
||||
*
|
||||
* <p>This service is now the single calculation path for:
|
||||
* <ul>
|
||||
* <li>the station table Tropo column</li>
|
||||
* <li>the Tropo filter/sorter</li>
|
||||
* <li>the station map path-analysis detail panel</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>The service always calculates a full {@link PathAnalysisResult}. When the
|
||||
* result contains a usable link budget, the bidirectional SSB margin is copied
|
||||
* into all matching {@link ChatMember} objects. The UI can then sort/filter by
|
||||
* a simple number while the map still receives the full path result.</p>
|
||||
*/
|
||||
public final class ReachabilityService {
|
||||
|
||||
private static final long FAILED_ANALYSIS_RETRY_DELAY_MS = 5L * 60L * 1000L;
|
||||
|
||||
private final ChatController chatController;
|
||||
private final PathAnalysisService pathAnalysisService;
|
||||
private final ExecutorService executor;
|
||||
|
||||
/**
|
||||
* Prevents duplicate jobs for the same path while a calculation is already
|
||||
* queued or running.
|
||||
*/
|
||||
private final Set<String> queuedCalculationKeys = ConcurrentHashMap.newKeySet();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Stores completed usable path-analysis results. Failed/no-budget results are
|
||||
* not permanently cached so transient terrain/network problems can be retried.
|
||||
*/
|
||||
private final Map<String, PathAnalysisResult> pathAnalysisResultCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Temporary callback list for map requests waiting for an already running
|
||||
* background calculation.
|
||||
*/
|
||||
private final Map<String, List<Consumer<PathAnalysisResult>>> pendingMapCallbacksByKey = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Prevents immediate retry loops for failed/no-budget analyses, especially
|
||||
* when a TableView cell repeatedly asks for the same value.
|
||||
*/
|
||||
private final Map<String, Long> failedCalculationRetryAfterEpochMs = new ConcurrentHashMap<>();
|
||||
|
||||
public ReachabilityService(ChatController chatController) {
|
||||
this.chatController = Objects.requireNonNull(chatController, "chatController");
|
||||
this.pathAnalysisService = new GeometryOnlyPathAnalysisService(new OpenMeteoTerrainProfileProvider());
|
||||
this.executor = Executors.newSingleThreadExecutor(new ReachabilityThreadFactory());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the automatically selected reachability band is calculated.
|
||||
*
|
||||
* @param member chatmember to evaluate
|
||||
*/
|
||||
public void ensureAutoTropoMarginCalculated(ChatMember member) {
|
||||
ensureTropoMarginCalculated(member, resolveAutoBand(member));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that a station/band SSB-margin value exists if it is already cached.
|
||||
*
|
||||
* <p>This method deliberately does not start online terrain analysis anymore.
|
||||
* It is safe to call from table cells and filters because it will not consume
|
||||
* API quota. Full calculations are now started only by explicit map/manual
|
||||
* requests.</p>
|
||||
*
|
||||
* @param member chatmember to evaluate
|
||||
* @param band band to evaluate
|
||||
*/
|
||||
public void ensureTropoMarginCalculated(ChatMember member, Band band) {
|
||||
if (member == null || band == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (member.hasFiniteTropoSsbMarginDb(band)) {
|
||||
return;
|
||||
}
|
||||
|
||||
PathAnalysisRequest request = buildRequestForChatMember(member, band);
|
||||
if (request == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PathAnalysisResult cachedResult = pathAnalysisResultCache.get(buildCalculationKey(request));
|
||||
if (cachedResult != null) {
|
||||
storePathAnalysisResultInChatMembers(member, band, cachedResult);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Requests a full path analysis for the station map.
|
||||
*
|
||||
* <p>This is the only normal automatic entry point for online/full terrain
|
||||
* analysis. The map needs the full PathAnalysisResult, and the station table
|
||||
* needs the SSB margin. Both are produced here through the same calculation.</p>
|
||||
*
|
||||
* @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());
|
||||
|
||||
if (selectedSnapshot == null) {
|
||||
dispatchFxCallback(fxCallback, PathAnalysisResult.waitingForSelection(ownLocator6));
|
||||
return;
|
||||
}
|
||||
|
||||
String targetLocator6 = normalizeLocator6(selectedSnapshot.locator6());
|
||||
|
||||
if (ownLocator6.length() != 6) {
|
||||
dispatchFxCallback(fxCallback,
|
||||
PathAnalysisResult.waitingForValidHomeLocator(ownLocator6, targetLocator6));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedSnapshot.hasUsablePosition()) {
|
||||
dispatchFxCallback(fxCallback,
|
||||
PathAnalysisResult.waitingForValidTarget(ownLocator6, targetLocator6));
|
||||
return;
|
||||
}
|
||||
|
||||
Band analysisBand;
|
||||
double analysisFrequencyMHz;
|
||||
|
||||
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 (frequencyResolution == null) {
|
||||
dispatchFxCallback(
|
||||
fxCallback,
|
||||
PathAnalysisResult.waitingForUsableBand(
|
||||
ownLocator6,
|
||||
targetLocator6,
|
||||
selectedSnapshot.callSignRaw()
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
analysisBand = frequencyResolution.getBand();
|
||||
analysisFrequencyMHz =
|
||||
frequencyResolution.getAnalysisFrequencyMHz();
|
||||
}
|
||||
|
||||
PathAnalysisRequest request = buildRequest(
|
||||
ownLocator6,
|
||||
selectedSnapshot.callSignRaw(),
|
||||
targetLocator6,
|
||||
selectedSnapshot.latitudeDeg(),
|
||||
selectedSnapshot.longitudeDeg(),
|
||||
analysisFrequencyMHz
|
||||
);
|
||||
|
||||
requestPathAnalysisAndStore(member, analysisBand, request, fxCallback);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Starts a full on-demand reachability calculation for one station row.
|
||||
*
|
||||
* <p>Use this for explicit operator actions only. It may call the online terrain
|
||||
* API and therefore must not be used from TableView cell factories or automatic
|
||||
* chat-join processing.</p>
|
||||
*
|
||||
* @param member station to calculate
|
||||
* @param band selected reachability band
|
||||
*/
|
||||
public void calculateSelectedStationOnDemand(ChatMember member, Band band) {
|
||||
if (member == null || band == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PathAnalysisRequest request = buildRequestForChatMember(member, band);
|
||||
if (request == null) {
|
||||
member.setTropoSsbMarginDb(band, Double.NaN);
|
||||
chatController.fireUserListUpdate("Reachability unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
requestPathAnalysisAndStore(member, band, request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
PropagationFrequencyResolver.Resolution resolution =
|
||||
resolveAutomaticPropagationFrequency(member);
|
||||
return resolution == null ? null : resolution.getBand();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active own bands configured in the station preferences. High bands
|
||||
* above 10 GHz are intentionally ignored for the first version.
|
||||
*
|
||||
* @return set of enabled bands
|
||||
*/
|
||||
public EnumSet<Band> getEnabledStationBands() {
|
||||
return BandOpportunityResolver.getEnabledStationBands(
|
||||
chatController.getChatPreferences()
|
||||
);
|
||||
}
|
||||
|
||||
private List<ChatMember> resolveCallsignVariants(ChatMember member) {
|
||||
if (member == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
String rawCall = member.getCallSignRaw() != null
|
||||
? member.getCallSignRaw()
|
||||
: member.getCallSign();
|
||||
|
||||
List<ChatMember> variants = chatController.findActiveChatMembersByRawCall(rawCall);
|
||||
return variants.isEmpty() ? List.of(member) : variants;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Stops the background executor.
|
||||
*/
|
||||
public void shutdown() {
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts or reuses one shared path-analysis calculation.
|
||||
*
|
||||
* @param member matching ChatMember, may be null for map-only snapshots
|
||||
* @param band band under which the value is stored in ChatMember
|
||||
* @param request full path-analysis request
|
||||
* @param fxCallback optional map callback
|
||||
*/
|
||||
private void requestPathAnalysisAndStore(ChatMember member,
|
||||
Band band,
|
||||
PathAnalysisRequest request,
|
||||
Consumer<PathAnalysisResult> fxCallback) {
|
||||
|
||||
if (request == null || band == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String calculationKey = buildCalculationKey(request);
|
||||
|
||||
PathAnalysisResult cachedResult = pathAnalysisResultCache.get(calculationKey);
|
||||
if (cachedResult != null) {
|
||||
storePathAnalysisResultInChatMembers(member, band, cachedResult);
|
||||
dispatchFxCallback(fxCallback, cachedResult);
|
||||
return;
|
||||
}
|
||||
|
||||
Long retryAfterEpochMs = failedCalculationRetryAfterEpochMs.get(calculationKey);
|
||||
if (retryAfterEpochMs != null && System.currentTimeMillis() < retryAfterEpochMs) {
|
||||
dispatchFxCallback(fxCallback, createNoProfileResult(
|
||||
request,
|
||||
"Previous path analysis failed or the terrain API limit was reached. Retry is delayed briefly."
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
addPendingCallback(calculationKey, fxCallback);
|
||||
|
||||
if (!queuedCalculationKeys.add(calculationKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
executor.submit(() -> {
|
||||
PathAnalysisResult result;
|
||||
|
||||
try {
|
||||
result = pathAnalysisService.analyze(request);
|
||||
if (result == null) {
|
||||
result = createNoProfileResult(request, "Path analysis returned no result.");
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
result = createNoProfileResult(
|
||||
request,
|
||||
"Path analysis failed: " + exception.getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
boolean usableBudget = hasUsableLinkBudget(result);
|
||||
|
||||
if (usableBudget) {
|
||||
pathAnalysisResultCache.put(calculationKey, result);
|
||||
failedCalculationRetryAfterEpochMs.remove(calculationKey);
|
||||
} else {
|
||||
failedCalculationRetryAfterEpochMs.put(
|
||||
calculationKey,
|
||||
System.currentTimeMillis() + FAILED_ANALYSIS_RETRY_DELAY_MS
|
||||
);
|
||||
}
|
||||
|
||||
queuedCalculationKeys.remove(calculationKey);
|
||||
|
||||
PathAnalysisResult finalResult = result;
|
||||
Runnable updateTask = () -> {
|
||||
storePathAnalysisResultInChatMembers(member, band, finalResult);
|
||||
dispatchAndClearPendingCallbacks(calculationKey, finalResult);
|
||||
chatController.fireUserListUpdate("Reachability calculated");
|
||||
};
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
updateTask.run();
|
||||
} else {
|
||||
Platform.runLater(updateTask);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a path-analysis request from a ChatMember row.
|
||||
*
|
||||
* @param member station row
|
||||
* @param band selected reachability band
|
||||
* @return request or null if locators are not usable
|
||||
*/
|
||||
private PathAnalysisRequest buildRequestForChatMember(ChatMember member, Band band) {
|
||||
String ownLocator6 = normalizeLocator6(chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
String targetLocator6 = normalizeLocator6(member.getQra());
|
||||
|
||||
if (ownLocator6.length() != 6 || targetLocator6.length() != 6) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Location targetLocation = new Location(targetLocator6);
|
||||
double analysisFrequencyMHz = resolveAnalysisFrequencyForBand(member, band);
|
||||
|
||||
return buildRequest(
|
||||
ownLocator6,
|
||||
member.getCallSignRaw(),
|
||||
targetLocator6,
|
||||
targetLocation.getLatitude().toDegrees(),
|
||||
targetLocation.getLongitude().toDegrees(),
|
||||
analysisFrequencyMHz
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the shared PathAnalysisRequest used by map and table/manual requests.
|
||||
*/
|
||||
private PathAnalysisRequest buildRequest(String ownLocator6,
|
||||
String targetCallsignRaw,
|
||||
String targetLocator6,
|
||||
double targetLatitudeDeg,
|
||||
double targetLongitudeDeg,
|
||||
double analysisFrequencyMHz) {
|
||||
|
||||
Location homeLocation = new Location(ownLocator6);
|
||||
|
||||
return new PathAnalysisRequest(
|
||||
ownLocator6,
|
||||
homeLocation.getLatitude().toDegrees(),
|
||||
homeLocation.getLongitude().toDegrees(),
|
||||
targetCallsignRaw,
|
||||
targetLocator6,
|
||||
targetLatitudeDeg,
|
||||
targetLongitudeDeg,
|
||||
analysisFrequencyMHz,
|
||||
chatController.getChatPreferences().getStn_pathAnalysisOwnAntennaHeightMeters(),
|
||||
chatController.getChatPreferences().getStn_pathAnalysisDefaultTargetAntennaHeightMeters(),
|
||||
PathGeometryUtils.DEFAULT_EFFECTIVE_EARTH_RADIUS_FACTOR,
|
||||
chatController.getChatPreferences().buildPathLinkBudgetSettings()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the SSB margin from a completed analysis in all matching ChatMember
|
||||
* objects. Matching uses raw callsign and locator.
|
||||
*/
|
||||
private void storePathAnalysisResultInChatMembers(ChatMember primaryMember,
|
||||
Band band,
|
||||
PathAnalysisResult result) {
|
||||
|
||||
if (band == null || result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
double marginDb = hasUsableLinkBudget(result)
|
||||
? result.linkBudgetSummary().bidirectionalSsbMarginDb()
|
||||
: Double.NaN;
|
||||
|
||||
if (primaryMember != null) {
|
||||
primaryMember.setTropoSsbMarginDb(band, marginDb);
|
||||
}
|
||||
|
||||
String resultCallSignRaw = normalizeCallsignRaw(result.toCallsignRaw());
|
||||
String resultLocator6 = normalizeLocator6(result.toLocator6());
|
||||
|
||||
for (ChatMember member : chatController.snapshotChatMembers()) {
|
||||
if (member == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String memberCallSignRaw = normalizeCallsignRaw(member.getCallSignRaw());
|
||||
if (!resultCallSignRaw.isBlank() && !resultCallSignRaw.equals(memberCallSignRaw)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String memberLocator6 = normalizeLocator6(member.getQra());
|
||||
if (!resultLocator6.isBlank() && !resultLocator6.equals(memberLocator6)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
member.setTropoSsbMarginDb(band, marginDb);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Resolves the analysis frequency for one member/band pair.
|
||||
*
|
||||
* <p>Preference order:
|
||||
* <ol>
|
||||
* <li>knownActiveBands frequency for the band</li>
|
||||
* <li>current displayed QRG if it belongs to the same band</li>
|
||||
* <li>band default frequency</li>
|
||||
* </ol>
|
||||
*/
|
||||
private double resolveAnalysisFrequencyForBand(ChatMember member, Band 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
|
||||
&& (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) {
|
||||
Band parsedBand = Band.fromFrequency(parsedFrequencyMHz);
|
||||
if (parsedBand == null || parsedBand == band) {
|
||||
return parsedFrequencyMHz;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return band.getDefaultAnalysisFrequencyMHz();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a map callback to the pending callback list for one calculation key.
|
||||
*
|
||||
* @param calculationKey path key
|
||||
* @param fxCallback callback to add
|
||||
*/
|
||||
private void addPendingCallback(String calculationKey, Consumer<PathAnalysisResult> fxCallback) {
|
||||
if (fxCallback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
pendingMapCallbacksByKey
|
||||
.computeIfAbsent(
|
||||
calculationKey,
|
||||
ignored -> Collections.synchronizedList(new ArrayList<>())
|
||||
)
|
||||
.add(fxCallback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches and removes all callbacks waiting for one completed calculation.
|
||||
*
|
||||
* @param calculationKey path key
|
||||
* @param result completed result
|
||||
*/
|
||||
private void dispatchAndClearPendingCallbacks(String calculationKey, PathAnalysisResult result) {
|
||||
List<Consumer<PathAnalysisResult>> callbacks = pendingMapCallbacksByKey.remove(calculationKey);
|
||||
if (callbacks == null || callbacks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Consumer<PathAnalysisResult>> callbackSnapshot;
|
||||
synchronized (callbacks) {
|
||||
callbackSnapshot = new ArrayList<>(callbacks);
|
||||
}
|
||||
|
||||
for (Consumer<PathAnalysisResult> callback : callbackSnapshot) {
|
||||
dispatchFxCallback(callback, result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a map callback on the JavaFX application thread.
|
||||
*
|
||||
* @param fxCallback callback to execute
|
||||
* @param result result to pass
|
||||
*/
|
||||
private void dispatchFxCallback(Consumer<PathAnalysisResult> fxCallback, PathAnalysisResult result) {
|
||||
if (fxCallback == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
fxCallback.accept(result);
|
||||
} else {
|
||||
Platform.runLater(() -> fxCallback.accept(result));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the result contains a usable link-budget summary.
|
||||
*
|
||||
* @param result path-analysis result
|
||||
* @return true if SSB margin can be read
|
||||
*/
|
||||
private boolean hasUsableLinkBudget(PathAnalysisResult result) {
|
||||
return result != null
|
||||
&& result.linkBudgetSummary() != null
|
||||
&& result.linkBudgetSummary().hasUsableBudget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a no-profile/no-budget result for failed service calculations.
|
||||
*
|
||||
* @param request original request
|
||||
* @param statusText status shown in the map detail panel
|
||||
* @return placeholder path result
|
||||
*/
|
||||
private PathAnalysisResult createNoProfileResult(PathAnalysisRequest request, String statusText) {
|
||||
return PathAnalysisResult.noProfile(
|
||||
"Reachability",
|
||||
request.fromLocator6(),
|
||||
request.toLocator6(),
|
||||
request.toCallsignRaw(),
|
||||
Double.NaN,
|
||||
Double.NaN,
|
||||
request.homeAntennaHeightMeters(),
|
||||
request.targetAntennaHeightMeters(),
|
||||
request.frequencyMHz(),
|
||||
statusText
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a stable calculation key.
|
||||
*
|
||||
* <p>The key includes station, locators, frequency, antenna heights and link
|
||||
* budget settings. If any relevant input changes, a new calculation is allowed.</p>
|
||||
*/
|
||||
private String buildCalculationKey(PathAnalysisRequest request) {
|
||||
return normalizeLocator6(request.fromLocator6())
|
||||
+ "|"
|
||||
+ normalizeCallsignRaw(request.toCallsignRaw())
|
||||
+ "|"
|
||||
+ normalizeLocator6(request.toLocator6())
|
||||
+ "|"
|
||||
+ String.format(Locale.US, "%.5f", request.toLatitudeDeg())
|
||||
+ "|"
|
||||
+ String.format(Locale.US, "%.5f", request.toLongitudeDeg())
|
||||
+ "|"
|
||||
+ String.format(Locale.US, "%.3f", request.frequencyMHz())
|
||||
+ "|"
|
||||
+ String.format(Locale.US, "%.1f", request.homeAntennaHeightMeters())
|
||||
+ "|"
|
||||
+ String.format(Locale.US, "%.1f", request.targetAntennaHeightMeters())
|
||||
+ "|"
|
||||
+ request.linkBudgetSettings();
|
||||
}
|
||||
|
||||
private String normalizeCallsignRaw(String callSignRaw) {
|
||||
return callSignRaw == null ? "" : callSignRaw.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeLocator6(String locator) {
|
||||
return locator == null ? "" : locator.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static final class ReachabilityThreadFactory implements ThreadFactory {
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable, "reachability-service");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,130 +1,118 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.Socket;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
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;
|
||||
|
||||
|
||||
/**
|
||||
* Reads exactly one immutable ON4KST connection session.
|
||||
* 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.
|
||||
*
|
||||
* <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>
|
||||
* @author www.codejava.net
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
public void setAccidentalDisconnected(boolean accidentalDisconnected) {
|
||||
this.accidentalDisconnected = accidentalDisconnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// private boolean readingFinished = true; //kst4contest.test 4 23001
|
||||
private boolean readingFinished = true;
|
||||
|
||||
InputStream input;
|
||||
|
||||
public ReadThread(Socket socket, ChatController client) {
|
||||
this.socket = socket;
|
||||
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("ReadFromOn4Kst-" + sessionId);
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST reader started for session {0}", sessionId);
|
||||
|
||||
this.client = client;
|
||||
|
||||
try {
|
||||
while (!isInterrupted() && sessionIsActive.test(sessionId)) {
|
||||
String response = reader.readLine();
|
||||
if (response == null) {
|
||||
throw new EOFException("ON4KST closed the TCP connection");
|
||||
}
|
||||
|
||||
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);
|
||||
input = socket.getInputStream();
|
||||
reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
|
||||
|
||||
} catch (IOException ex) {
|
||||
System.out.println("Error getting input stream: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void run() {
|
||||
Thread.currentThread().setName("ReadFromTelnetThread");
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
interrupt();
|
||||
reader.close();
|
||||
socket.close();
|
||||
return true;
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
catch (Exception sexc) {
|
||||
System.out.println("[ReadThread, CRITICAL: ] Socket geschlossen: " + sexc.getMessage());
|
||||
try {
|
||||
this.client.getSocket().close();
|
||||
this.interrupt();
|
||||
break;
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public boolean terminateConnection() throws IOException {
|
||||
this.reader.close();
|
||||
this.input.close();
|
||||
this.socket.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isReadingFinished() {
|
||||
return readingFinished;
|
||||
}
|
||||
|
||||
public void setReadingFinished(boolean readingReady) {
|
||||
this.readingFinished = readingReady;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package kst4contest.controller;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
import kst4contest.view.GuiUtils;
|
||||
@@ -287,74 +286,6 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* Resolves the project Band enum from Win-Test band IDs.
|
||||
*
|
||||
* <p>Only bands that exist in the current Band enum are returned. 50/70 MHz are
|
||||
* still represented as worked flags in ChatMember, but they are not part of the
|
||||
* current Reachability/New-Locator band enum.</p>
|
||||
*
|
||||
* @param bandId Win-Test band id from ADDQSO
|
||||
* @return matching Band or null
|
||||
*/
|
||||
private Band helper_resolveBandFromWinTestBandId(String bandId) {
|
||||
if (bandId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (bandId.trim()) {
|
||||
case "10" -> Band.B_50;
|
||||
case "11" -> Band.B_70;
|
||||
case "12" -> Band.B_144;
|
||||
case "14" -> Band.B_432;
|
||||
case "16" -> Band.B_1296;
|
||||
case "17" -> Band.B_2320;
|
||||
case "18" -> Band.B_3400;
|
||||
case "19" -> Band.B_5760;
|
||||
case "20" -> Band.B_10G;
|
||||
case "21" -> Band.B_24G;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the locator from a Win-Test ADDQSO packet.
|
||||
*
|
||||
* <p>Current parser model based on the existing split-by-quotes code:
|
||||
* <ul>
|
||||
* <li>{@code split("\"")[7]} = callsign</li>
|
||||
* <li>{@code split("\"")[11]} = received exchange, e.g. 599001</li>
|
||||
* <li>{@code split("\"")[13]} = locator, e.g. JO51UM</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>If the dedicated locator field is empty, the exchange is used as fallback.</p>
|
||||
*
|
||||
* @param msg raw ADDQSO message
|
||||
* @return normalized six-character locator or null
|
||||
*/
|
||||
private String helper_resolveLocatorFromWinTestAddQso(String msg) {
|
||||
if (msg == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] quotedParts = msg.split("\"");
|
||||
|
||||
if (quotedParts.length > 13) {
|
||||
String locator = WorkedGrossFieldCache.extractLocator6(quotedParts[13]);
|
||||
if (locator != null) {
|
||||
return locator;
|
||||
}
|
||||
}
|
||||
|
||||
if (quotedParts.length > 11) {
|
||||
return WorkedGrossFieldCache.extractLocator6(quotedParts[11]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Catches add-qso messages of wintest if a new qso gets into the log<br/>
|
||||
*
|
||||
@@ -379,22 +310,16 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
// receivedQsos.put(qsoNumber, msg);
|
||||
// lastKnownQso = Math.max(lastKnownQso, qsoNumber);
|
||||
String callSignCatched = msg.split("\"") [7];
|
||||
String locatorFromLogger = helper_resolveLocatorFromWinTestAddQso(msg);
|
||||
|
||||
ChatMember workedCall = new ChatMember();
|
||||
workedCall.setCallSign(callSignCatched);
|
||||
workedCall.setWorked(true); //its worked at this place, for sure!
|
||||
|
||||
if (locatorFromLogger != null) {
|
||||
workedCall.setQra(locatorFromLogger);
|
||||
}
|
||||
|
||||
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
|
||||
|
||||
String bandId;
|
||||
bandId = msg.split("\"")[6].split(" ")[4].trim();
|
||||
|
||||
Band workedBand = helper_resolveBandFromWinTestBandId(bandId);
|
||||
switch (bandId) {
|
||||
case "10" -> workedCall.setWorked50(true);
|
||||
case "11" -> workedCall.setWorked70(true);
|
||||
@@ -411,10 +336,6 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
default -> System.out.println("[WinTestUDPRcvr: warning] Unbekannte Band-ID: " + bandId);
|
||||
}
|
||||
|
||||
if (workedBand != null && locatorFromLogger != null) {
|
||||
this.client.registerWorkedGrossField(workedBand, locatorFromLogger, workedCall, "WINTEST");
|
||||
}
|
||||
|
||||
if (!markTheseChattersAsWorked.isEmpty()) {
|
||||
//Worked call is part of the current chatmember list
|
||||
|
||||
@@ -424,13 +345,6 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
|
||||
modifyThat.setWorked(true); //worked its for sure
|
||||
|
||||
if (locatorFromLogger != null
|
||||
&& (modifyThat.getQra() == null
|
||||
|| modifyThat.getQra().isBlank()
|
||||
|| "unknown".equalsIgnoreCase(modifyThat.getQra()))) {
|
||||
modifyThat.setQra(locatorFromLogger);
|
||||
}
|
||||
|
||||
if (workedCall.isWorked50()) {
|
||||
modifyThat.setWorked50(true);
|
||||
} else if (workedCall.isWorked70()) {
|
||||
@@ -484,11 +398,7 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
if (!isInChat) {
|
||||
|
||||
workedCall.setName("unknown");
|
||||
|
||||
if (workedCall.getQra() == null || workedCall.getQra().isBlank()) {
|
||||
workedCall.setQra("unknown");
|
||||
}
|
||||
|
||||
workedCall.setQra("unknown");
|
||||
workedCall.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
||||
this.client.getDbHandler().storeChatMember(workedCall);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -25,245 +21,188 @@ import kst4contest.model.ThreadStateMessage;
|
||||
* @author www.codejava.net
|
||||
*/
|
||||
public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
private BufferedReader reader;
|
||||
private Socket socket;
|
||||
private ChatController client;
|
||||
private int localPort;
|
||||
private String ASIdentificator, ChatClientIdentificator;
|
||||
private ThreadStatusCallback callBackToController;
|
||||
private String ThreadNickName = "AirScout msg";
|
||||
// public ReadUDPbyAirScoutMessageThread(int localPort) {
|
||||
// this.localPort = localPort;
|
||||
// }
|
||||
|
||||
private final ChatController client;
|
||||
private final int localPort;
|
||||
private final ThreadStatusCallback callBackToController;
|
||||
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator,
|
||||
String ChatClientIdentificator, ThreadStatusCallback callback) {
|
||||
|
||||
private final String threadNickName = "AirScout msg";
|
||||
|
||||
private DatagramSocket socket;
|
||||
|
||||
public ReadUDPbyAirScoutMessageThread(
|
||||
int localPort,
|
||||
ChatController client,
|
||||
ThreadStatusCallback callback
|
||||
) {
|
||||
this.callBackToController = callback;
|
||||
this.localPort = localPort;
|
||||
this.client = client;
|
||||
this.callBackToController = callback;
|
||||
this.ASIdentificator = ASIdentificator;
|
||||
this.ChatClientIdentificator = ChatClientIdentificator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void interrupt() {
|
||||
System.out.println("ReadUDP");
|
||||
super.interrupt();
|
||||
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
try {
|
||||
if (this.socket != null) {
|
||||
|
||||
this.socket.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an AirScout response is addressed to the currently
|
||||
* configured server and client identifiers.
|
||||
*
|
||||
* Outgoing message:
|
||||
* ASSETPATH: "client" "server" ...
|
||||
*
|
||||
* Corresponding response:
|
||||
* ASNEAREST: "server" "client" ...
|
||||
*
|
||||
* The comparison is deliberately case-sensitive. In a setup with several
|
||||
* clients, KST-A and kst-a must not silently become the same destination.
|
||||
*
|
||||
* @param message received UDP message
|
||||
* @return true if the response belongs to this KST4Contest instance
|
||||
*/
|
||||
private boolean isMessageForConfiguredClient(String message) {
|
||||
if (message == null || !message.startsWith("ASNEAREST:")) {
|
||||
return false;
|
||||
}
|
||||
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) {
|
||||
if (callBackToController != null) {
|
||||
//update the visual control of running thread
|
||||
callBackToController.onThreadStatus("AirScout", threadStateMessage);
|
||||
}
|
||||
}
|
||||
|
||||
String[] quotedParts = message.split("\"");
|
||||
if (quotedParts.length < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String receivedServerIdentifier = quotedParts[1].trim();
|
||||
String receivedClientIdentifier = quotedParts[3].trim();
|
||||
|
||||
String configuredServerIdentifier =
|
||||
client.getChatPreferences()
|
||||
.getAirScout_asServerNameString();
|
||||
String configuredClientIdentifier =
|
||||
client.getChatPreferences()
|
||||
.getAirScout_asClientNameString();
|
||||
|
||||
if (configuredServerIdentifier == null
|
||||
|| configuredClientIdentifier == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return configuredServerIdentifier.equals(
|
||||
receivedServerIdentifier
|
||||
) && configuredClientIdentifier.equals(
|
||||
receivedClientIdentifier
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
public void run() {
|
||||
Thread.currentThread().setName(
|
||||
"ReadUDPByAirScoutThread"
|
||||
);
|
||||
Thread.currentThread().setName("ReadUDPByAirScoutThread");
|
||||
|
||||
DatagramSocket socket = null;
|
||||
boolean running;
|
||||
byte[] buf = new byte[1777];
|
||||
DatagramPacket packet;
|
||||
// DatagramPacket packet = new DatagramPacket(buf, buf.length); //changed due to save memory
|
||||
packet = new DatagramPacket(buf, buf.length);
|
||||
|
||||
try {
|
||||
|
||||
socket = new DatagramSocket(null);
|
||||
socket.setReuseAddress(true);
|
||||
socket.bind(new InetSocketAddress(localPort));
|
||||
socket.receive(packet);
|
||||
socket.setSoTimeout(3000);
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
if (client.isDisconnectionPerformedByUser()) {
|
||||
break;
|
||||
} catch (SocketException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
|
||||
} catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
// e.printStackTrace();
|
||||
}
|
||||
|
||||
|
||||
while (true) {
|
||||
// packet = new DatagramPacket(buf, buf.length);
|
||||
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH);
|
||||
try {
|
||||
if (this.client.isDisconnectionPerformedByUser()) {
|
||||
break;//TODO: what if it´s not the finally closage but a band channel change?
|
||||
}
|
||||
|
||||
socket.receive(packet);
|
||||
|
||||
byte[] buffer = new byte[1777];
|
||||
DatagramPacket packet = new DatagramPacket(
|
||||
buffer,
|
||||
buffer.length
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} catch (SocketTimeoutException e2) {
|
||||
// this will catch the repeating Sockettimeoutexception...nothing to do
|
||||
// e2.printStackTrace();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// TODO Auto-generated catch block
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
InetAddress address = packet.getAddress();
|
||||
int port = packet.getPort();
|
||||
// packet = new DatagramPacket(buf, buf.length, address, port);
|
||||
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
|
||||
received = received.trim();
|
||||
|
||||
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
|
||||
System.out.println("ReadUdpByASMsgTh, Info: got poison, now dieing....");
|
||||
try {
|
||||
socket.receive(packet);
|
||||
} catch (SocketTimeoutException timeoutException) {
|
||||
/*
|
||||
* AirScout may remain silent for some time. Continue with a
|
||||
* new packet instead of processing the previous packet again.
|
||||
*/
|
||||
continue;
|
||||
terminateConnection();
|
||||
} catch (Exception e) {
|
||||
System.out.println("ASUDPRDR: catched error " + e.getMessage());
|
||||
}
|
||||
|
||||
String received = new String(
|
||||
packet.getData(),
|
||||
packet.getOffset(),
|
||||
packet.getLength()
|
||||
).trim();
|
||||
|
||||
if (received.contains(
|
||||
ApplicationConstants.DISCONNECT_RDR_POISONPILL
|
||||
)) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, info]: Received shutdown packet."
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
/*
|
||||
* The socket remains bound so that AirScout can be enabled
|
||||
* without reconnecting. Disabled means that received data is
|
||||
* discarded and no station state is changed.
|
||||
*/
|
||||
if (!client.getChatPreferences()
|
||||
.isAirScout_asUDPListenerEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isMessageForConfiguredClient(received)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processAirScoutResponse(received);
|
||||
}
|
||||
} catch (SocketException exception) {
|
||||
if (!Thread.currentThread().isInterrupted()) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, error]: Could not use UDP port "
|
||||
+ localPort
|
||||
+ ": "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
if (!Thread.currentThread().isInterrupted()) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, error]: Communication failed: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
break;
|
||||
}
|
||||
|
||||
socket = null;
|
||||
|
||||
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
|
||||
// do nothing, that is your own message
|
||||
} else if (received.contains("ASNEAREST:")) { //answer by airscout
|
||||
|
||||
// processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
|
||||
|
||||
AirPlaneReflectionInfo apReflectInfoForChatMember;
|
||||
|
||||
apReflectInfoForChatMember = processASUDPMessage(received);
|
||||
if (!this.client.getLst_chatMemberList().isEmpty()) {
|
||||
|
||||
try {
|
||||
|
||||
// this.client.getLst_chatMemberList()
|
||||
// .get(this.client.checkListForChatMemberIndexByCallSign(
|
||||
// apReflectInfoForChatMember.getReceiver()))
|
||||
// .setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
|
||||
// // the central instance of
|
||||
// // chatmember list .... -1 is a
|
||||
// // problem!
|
||||
|
||||
ArrayList<Integer> addApInfoToThese = this.client.checkListForChatMemberIndexesByCallSign(apReflectInfoForChatMember.getReceiver());
|
||||
addApInfoToThese.forEach((integerIndex) -> {this.client.getLst_chatMemberList().get(integerIndex).setAirPlaneReflectInfo(apReflectInfoForChatMember); });
|
||||
|
||||
// AirScout availability strongly affects priority => request recompute the score of the chatmember
|
||||
this.client.getScoreService().requestRecompute("airscout-update");
|
||||
|
||||
/**
|
||||
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10"
|
||||
* java.util.ConcurrentModificationException at
|
||||
* java.base/java.util.AbstractList$Itr.checkForComodification(AbstractList.java:399)
|
||||
* at java.base/java.util.AbstractList$Itr.next(AbstractList.java:368) at
|
||||
* kst4contest.controller.ChatController.checkListForChatMemberIndexByCallSign(ChatController.java:173)
|
||||
* at
|
||||
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
|
||||
*
|
||||
*/
|
||||
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
|
||||
// }
|
||||
} catch (Exception e) {
|
||||
|
||||
System.out.println("ReadUdpByAsMsgTh, Warning:"
|
||||
+ apReflectInfoForChatMember.getReceiver().getCallSign()
|
||||
+ " is not in the Chatmemberlist or the Chatmemberlist is modified by another Thread");
|
||||
// TODO: handle exception
|
||||
}
|
||||
|
||||
// String[] newState = new String[3];
|
||||
// newState[0] = "On";
|
||||
// newState[1] = "received line";
|
||||
// newState[2] = apReflectInfoForChatMember.toString();
|
||||
// callThreadStateToUi(newState);
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + apReflectInfoForChatMember.toString(), false);
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
}
|
||||
|
||||
}
|
||||
// packet = null; //reset packet
|
||||
buf = new byte[1777]; // reset buffer for future smaller packets
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an AirScout response and applies it to every active category
|
||||
* instance of the reported station.
|
||||
*
|
||||
* @param received received ASNEAREST message
|
||||
*/
|
||||
private void processAirScoutResponse(String received) {
|
||||
try {
|
||||
AirPlaneReflectionInfo reflectionInfo =
|
||||
processASUDPMessage(received);
|
||||
|
||||
if (reflectionInfo == null
|
||||
|| reflectionInfo.getReceiver() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String receiverCallSign =
|
||||
reflectionInfo.getReceiver().getCallSignRaw();
|
||||
|
||||
if (receiverCallSign == null
|
||||
|| receiverCallSign.isBlank()) {
|
||||
receiverCallSign =
|
||||
reflectionInfo.getReceiver().getCallSign();
|
||||
}
|
||||
|
||||
if (receiverCallSign == null
|
||||
|| receiverCallSign.isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ChatMember> matchingMembers =
|
||||
client.findActiveChatMembersByRawCall(
|
||||
receiverCallSign
|
||||
);
|
||||
|
||||
for (ChatMember matchingMember : matchingMembers) {
|
||||
matchingMember.setAirPlaneReflectInfo(
|
||||
reflectionInfo
|
||||
);
|
||||
}
|
||||
|
||||
if (!matchingMembers.isEmpty()) {
|
||||
client.getScoreService().requestRecompute(
|
||||
"airscout-update"
|
||||
);
|
||||
}
|
||||
|
||||
if (callBackToController != null) {
|
||||
ThreadStateMessage threadStateMessage =
|
||||
new ThreadStateMessage(
|
||||
threadNickName,
|
||||
true,
|
||||
"Received AirScout response\n"
|
||||
+ reflectionInfo,
|
||||
false
|
||||
);
|
||||
|
||||
callBackToController.onThreadStatus(
|
||||
threadNickName,
|
||||
threadStateMessage
|
||||
);
|
||||
}
|
||||
} catch (RuntimeException exception) {
|
||||
System.out.println(
|
||||
"[AirScout UDP, warning]: Could not process response: "
|
||||
+ exception.getMessage()
|
||||
);
|
||||
}
|
||||
}
|
||||
public AirPlaneReflectionInfo processASUDPMessage(String udpStringToProcess) {
|
||||
|
||||
// System.out.println("RDUDPAS RECV: " + udpStringToProcess);
|
||||
|
||||
|
||||
// TODO: filter messages which are directed to another client
|
||||
|
||||
/*
|
||||
* Example mesage: ASNEAREST: "AS" "KST"
|
||||
@@ -365,8 +304,11 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
|
||||
}
|
||||
|
||||
public boolean terminateConnection() {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (Exception e) {
|
||||
System.out.println("udpbyas: catched " + e.getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -22,9 +22,6 @@ import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.Band;
|
||||
|
||||
import javafx.application.Platform;
|
||||
|
||||
/**
|
||||
* This thread is responsible for reading server's input and printing it to the
|
||||
@@ -66,120 +63,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
System.out.println("UCXUDPRDR: catched error " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips binary logger framing bytes before the XML payload. Some UCXLog packets
|
||||
* contain transport bytes before the XML declaration.
|
||||
*
|
||||
* @param rawPacket raw UDP payload
|
||||
* @return cleaned XML string or trimmed original text
|
||||
*/
|
||||
private String helper_extractXmlPayload(String rawPacket) {
|
||||
if (rawPacket == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
int xmlStart = rawPacket.indexOf("<?xml");
|
||||
if (xmlStart < 0) {
|
||||
xmlStart = rawPacket.indexOf("<contactinfo");
|
||||
}
|
||||
if (xmlStart < 0) {
|
||||
xmlStart = rawPacket.indexOf("<contactreplace");
|
||||
}
|
||||
if (xmlStart < 0) {
|
||||
xmlStart = rawPacket.indexOf("<RadioInfo");
|
||||
}
|
||||
|
||||
return xmlStart >= 0
|
||||
? rawPacket.substring(xmlStart).trim()
|
||||
: rawPacket.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an optional XML child node.
|
||||
*
|
||||
* @param element parent element
|
||||
* @param tagName tag to read
|
||||
* @return trimmed value or empty string
|
||||
*/
|
||||
private String helper_getOptionalElementText(Element element, String tagName) {
|
||||
if (element == null || tagName == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
NodeList nodeList = element.getElementsByTagName(tagName);
|
||||
if (nodeList == null || nodeList.getLength() == 0 || nodeList.item(0) == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String textContent = nodeList.item(0).getTextContent();
|
||||
return textContent == null ? "" : textContent.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the QSO locator from UCXLog contactinfo. gridsquare is preferred,
|
||||
* rcvnr is used as fallback for exchanges such as 001JO41HK.
|
||||
*
|
||||
* @param element contactinfo XML element
|
||||
* @return normalized six-character locator or null
|
||||
*/
|
||||
private String helper_resolveLocatorFromContactInfo(Element element) {
|
||||
String gridSquare = WorkedGrossFieldCache.extractLocator6(helper_getOptionalElementText(element, "gridsquare"));
|
||||
if (gridSquare != null) {
|
||||
return gridSquare;
|
||||
}
|
||||
|
||||
return WorkedGrossFieldCache.extractLocator6(helper_getOptionalElementText(element, "rcvnr"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the project Band enum from logger band values.
|
||||
*
|
||||
* @param band logger band text
|
||||
* @return matching Band or null
|
||||
*/
|
||||
private Band helper_resolveBandFromLoggerBand(String band) {
|
||||
if (band == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (band.trim()) {
|
||||
case "50":
|
||||
case "6m":
|
||||
return Band.B_50;
|
||||
case "70":
|
||||
case "4m":
|
||||
return Band.B_70;
|
||||
case "144":
|
||||
case "2m":
|
||||
return Band.B_144;
|
||||
case "432":
|
||||
case "70cm":
|
||||
return Band.B_432;
|
||||
case "1240":
|
||||
case "1296":
|
||||
case "23cm":
|
||||
return Band.B_1296;
|
||||
case "2300":
|
||||
case "2320":
|
||||
case "13cm":
|
||||
return Band.B_2320;
|
||||
case "3400":
|
||||
case "9cm":
|
||||
return Band.B_3400;
|
||||
case "5600":
|
||||
case "5760":
|
||||
case "6cm":
|
||||
return Band.B_5760;
|
||||
case "10G":
|
||||
case "10368":
|
||||
case "3cm":
|
||||
return Band.B_10G;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void run() {
|
||||
|
||||
System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
|
||||
@@ -293,10 +177,18 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
File logUDPMessageToThisFile;
|
||||
|
||||
String udpMsg = helper_extractXmlPayload(udpPacketToProcess);
|
||||
String udpMsg = udpPacketToProcess;
|
||||
|
||||
ChatMember modifyThat = null;
|
||||
|
||||
// System.out.println("ReadUDPByUCX, message catched: " + udpMsg);
|
||||
|
||||
// String[] threadStatusMessage = new String[2];
|
||||
// threadStatusMessage = new String[3];
|
||||
// threadStatusMessage[0] = "on";
|
||||
// threadStatusMessage[1] = "received message:";
|
||||
// threadStatusMessage[2] = udpMsg;
|
||||
|
||||
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);
|
||||
|
||||
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
|
||||
@@ -305,7 +197,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
try {
|
||||
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
|
||||
} catch (ParserConfigurationException e1) {
|
||||
|
||||
// TODO Auto-generated catch block
|
||||
e1.printStackTrace();
|
||||
}
|
||||
|
||||
@@ -314,26 +206,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
Document doc = db.parse(new InputSource(new StringReader(udpMsg)));
|
||||
|
||||
/**
|
||||
* case Log-QSO-Packet in ucxlog / DXLog and compatible
|
||||
* case Log-QSO-Packet in ucxlog
|
||||
*
|
||||
*/
|
||||
NodeList list = doc.getElementsByTagName("contactinfo");
|
||||
|
||||
if (list.getLength() == 0) {
|
||||
list = doc.getElementsByTagName("contactreplace");
|
||||
//DXlog will send contactreplace instead of contactinfo on clicking "broadcast whole logbook"
|
||||
}
|
||||
|
||||
if (list.getLength() != 0) {
|
||||
|
||||
/*
|
||||
* QSO and TRX information share the same UDP receiver. The log-sync
|
||||
* preference therefore controls processing, not ownership of the socket.
|
||||
*/
|
||||
if (!client.getChatPreferences().isLogsynch_ucxUDPWkdCallListenerEnabled()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
|
||||
Node node = list.item(temp);
|
||||
@@ -343,9 +221,10 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
Element element = (Element) node;
|
||||
|
||||
String call = element.getElementsByTagName("call").item(0).getTextContent();
|
||||
String band = helper_getOptionalElementText(element, "band");
|
||||
String gridSquare = helper_resolveLocatorFromContactInfo(element);
|
||||
String points = helper_getOptionalElementText(element, "points");
|
||||
// call = call.toLowerCase();
|
||||
String band = element.getElementsByTagName("band").item(0).getTextContent();
|
||||
|
||||
String points = element.getElementsByTagName("points").item(0).getTextContent();
|
||||
|
||||
System.out.println("[Readudp, info ]: received Current Element :" + node.getNodeName()
|
||||
+ "call: " + call + " / " + band + " ----> " + points + " POINTS");
|
||||
@@ -356,83 +235,89 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
workedCall.setCallSign(call);
|
||||
workedCall.setWorked(true);
|
||||
|
||||
if (gridSquare != null) {
|
||||
workedCall.setQra(gridSquare);
|
||||
}
|
||||
|
||||
Band workedBand = helper_resolveBandFromLoggerBand(band);
|
||||
|
||||
switch (band) {
|
||||
case "50":
|
||||
case "6m":
|
||||
{
|
||||
workedCall.setWorked50(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "70":
|
||||
case "4m":
|
||||
{
|
||||
workedCall.setWorked70(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "144":
|
||||
case "2m": //minos contest logger
|
||||
{
|
||||
case "144": {
|
||||
workedCall.setWorked144(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "432":
|
||||
case "70cm":
|
||||
{
|
||||
workedCall.setWorked432(true);
|
||||
break;
|
||||
}
|
||||
case "432": {
|
||||
workedCall.setWorked432(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "1240": //ucxlog style
|
||||
case "1296": //used for n1mm / Dxlog
|
||||
case "23cm": //minos contest logger
|
||||
{
|
||||
case "1240": {
|
||||
workedCall.setWorked1240(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "2300":
|
||||
case "13cm":
|
||||
{
|
||||
case "2300": {
|
||||
workedCall.setWorked2300(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "3400":
|
||||
case "9cm":
|
||||
{
|
||||
case "3400": {
|
||||
workedCall.setWorked3400(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "5600":
|
||||
case "6cm":
|
||||
{
|
||||
case "5600": {
|
||||
workedCall.setWorked5600(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "10G":
|
||||
case "3cm":
|
||||
{
|
||||
case "10G": {
|
||||
workedCall.setWorked10G(true);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
System.out.println("[ReadUDPFromUCX, Error:] unexpected band value: \"" + band + "\"");
|
||||
break;
|
||||
/**
|
||||
* cases hotfix for MINOS logger, which tells band like "2m", not "144"
|
||||
*/
|
||||
case "2m": {
|
||||
workedCall.setWorked144(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "70cm": {
|
||||
workedCall.setWorked432(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "23cm": {
|
||||
workedCall.setWorked1240(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "13cm": {
|
||||
workedCall.setWorked2300(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "9cm": {
|
||||
workedCall.setWorked3400(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "6cm": {
|
||||
workedCall.setWorked5600(true);
|
||||
break;
|
||||
}
|
||||
|
||||
case "3cm": {
|
||||
workedCall.setWorked10G(true);
|
||||
|
||||
}
|
||||
|
||||
default:
|
||||
System.out.println("[ReadUDPFromUCX, Error:] unexpected band value: \"" + band + "\"");
|
||||
break;
|
||||
}
|
||||
|
||||
// if (!client.getMap_ucxLogInfoWorkedCalls().containsKey("call")) {
|
||||
// client.getMap_ucxLogInfoWorkedCalls().put(call, workedCall);
|
||||
|
||||
// } else
|
||||
{
|
||||
/**
|
||||
* That means, the station is worked already but maybe at another band. So we
|
||||
@@ -454,13 +339,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
|
||||
modifyThat.setWorked(true);
|
||||
|
||||
if (workedCall.isWorked50()) {
|
||||
modifyThat.setWorked50(true);
|
||||
|
||||
} else if (workedCall.isWorked70()) {
|
||||
modifyThat.setWorked70(true);
|
||||
|
||||
} else if (workedCall.isWorked144()) {
|
||||
if (workedCall.isWorked144()) {
|
||||
modifyThat.setWorked144(true);
|
||||
|
||||
} else if (workedCall.isWorked432()) {
|
||||
@@ -573,22 +452,14 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
*/
|
||||
}
|
||||
|
||||
if (workedBand != null && gridSquare != null) {
|
||||
this.client.registerWorkedGrossField(workedBand, gridSquare, workedCall, "UCXLOG");
|
||||
}
|
||||
|
||||
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
|
||||
// This will update the worked info on a worked chatmember. DBHandler will
|
||||
// check, if an entry at the db had been modified. If not, then the worked
|
||||
// station had not been stored. DBHandler will store the information then.
|
||||
if (!isInChat) {
|
||||
|
||||
|
||||
workedCall.setName("unknown");
|
||||
|
||||
if (workedCall.getQra() == null || workedCall.getQra().isBlank()) {
|
||||
workedCall.setQra("unknown");
|
||||
}
|
||||
|
||||
workedCall.setQra("unknown");
|
||||
workedCall.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
|
||||
this.client.getDbHandler().storeChatMember(workedCall);
|
||||
}
|
||||
@@ -626,15 +497,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
} else {
|
||||
list = doc.getElementsByTagName("RadioInfo");
|
||||
|
||||
/*
|
||||
* RadioInfo packets may arrive on the shared UDP port even when automatic
|
||||
* QRG synchronization is disabled. Ignore them unless TRX sync is enabled.
|
||||
*/
|
||||
if (list.getLength() != 0
|
||||
&& !client.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
|
||||
Node node = list.item(temp);
|
||||
@@ -688,13 +550,8 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
// System.out.println("Radio Mode: " + mode);
|
||||
// System.out.println("[ReadUDPFromUCX, Info:] Setted QRG pref to: \"" + qrg + "\"" );
|
||||
|
||||
// this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
|
||||
|
||||
final String finalFormattedQRG = formattedQRG;
|
||||
helper_runOnFxThread(() ->
|
||||
this.client.getChatPreferences().getMYQRGFirstCat().set(finalFormattedQRG)
|
||||
);
|
||||
|
||||
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
|
||||
|
||||
// System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
|
||||
}
|
||||
}
|
||||
@@ -747,28 +604,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
this.socket.close();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs UI-bound changes on the JavaFX application thread.
|
||||
*
|
||||
* <p>UCXLog UDP packets are processed in a background thread. Some preference
|
||||
* properties are bound to JavaFX controls, so setting them directly from this
|
||||
* thread can crash JavaFX with "Not on FX application thread".</p>
|
||||
*
|
||||
* @param runnable UI-bound update
|
||||
*/
|
||||
private void helper_runOnFxThread(Runnable runnable) {
|
||||
if (runnable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
Platform.runLater(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -140,9 +140,7 @@ public final class ScoreService {
|
||||
controller.getStationMetricsService().snapshot(nowEpochMs, prefs);
|
||||
|
||||
// 1) Choose one representative per callsignRaw
|
||||
Map<String, List<ChatMember>> variantsByCallRaw = groupMembersByCallRaw(members);
|
||||
Map<String, ChatMember> representativeByCallRaw =
|
||||
chooseRepresentativeMembers(variantsByCallRaw, lastInbound);
|
||||
Map<String, ChatMember> representativeByCallRaw = chooseRepresentativeMembers(members, lastInbound);
|
||||
|
||||
// 2) Compute score once per callsignRaw
|
||||
Map<String, Double> scoreByCallRaw = new HashMap<>(representativeByCallRaw.size());
|
||||
@@ -156,7 +154,6 @@ public final class ScoreService {
|
||||
|
||||
double score = priorityCalculator.calculatePriority(
|
||||
representative,
|
||||
variantsByCallRaw.getOrDefault(callRaw, List.of(representative)),
|
||||
prefs,
|
||||
activeSkeds,
|
||||
metricsSnapshot,
|
||||
@@ -166,15 +163,7 @@ public final class ScoreService {
|
||||
scoreByCallRaw.put(callRaw, score);
|
||||
preferredCategoryByCallRaw.put(callRaw, representative.getChatCategory());
|
||||
|
||||
if (Double.isFinite(score) && score > 0.0) {
|
||||
topAll.add(new TopCandidate(
|
||||
callRaw,
|
||||
representative.getCallSign(),
|
||||
representative.getChatCategory(),
|
||||
score
|
||||
));
|
||||
}
|
||||
|
||||
topAll.add(new TopCandidate(callRaw, representative.getCallSign(), representative.getChatCategory(), score));
|
||||
}
|
||||
|
||||
// 3) Build Top-N
|
||||
@@ -193,28 +182,12 @@ public final class ScoreService {
|
||||
|
||||
// 4) Publish to UI in ONE batched runLater
|
||||
Platform.runLater(() -> {
|
||||
applyScoreSnapshotToChatMembers(snap);
|
||||
topCandidatesFx.setAll(snap.getTopCandidates());
|
||||
updateSelectedScoreFromSnapshot(snap);
|
||||
uiPulse.set(uiPulse.get() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, List<ChatMember>> groupMembersByCallRaw(List<ChatMember> members) {
|
||||
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
|
||||
|
||||
for (ChatMember member : members) {
|
||||
if (member == null) continue;
|
||||
|
||||
String callRaw = normalizeCallRaw(member.getCallSignRaw());
|
||||
if (callRaw == null || callRaw.isEmpty()) continue;
|
||||
|
||||
byCallRaw.computeIfAbsent(callRaw, ignored -> new ArrayList<>()).add(member);
|
||||
}
|
||||
|
||||
return byCallRaw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks one ChatMember object per callsignRaw.
|
||||
* Preference order:
|
||||
@@ -222,9 +195,18 @@ public final class ScoreService {
|
||||
* 2) Most recently active variant (fallback)
|
||||
*/
|
||||
private Map<String, ChatMember> chooseRepresentativeMembers(
|
||||
Map<String, List<ChatMember>> byCallRaw,
|
||||
List<ChatMember> members,
|
||||
Map<String, ChatCategory> lastInboundCategoryByCallRaw
|
||||
) {
|
||||
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
|
||||
|
||||
for (ChatMember m : members) {
|
||||
if (m == null) continue;
|
||||
String callRaw = normalizeCallRaw(m.getCallSignRaw());
|
||||
if (callRaw == null || callRaw.isEmpty()) continue;
|
||||
byCallRaw.computeIfAbsent(callRaw, k -> new ArrayList<>()).add(m);
|
||||
}
|
||||
|
||||
Map<String, ChatMember> representative = new HashMap<>(byCallRaw.size());
|
||||
|
||||
for (Map.Entry<String, List<ChatMember>> entry : byCallRaw.entrySet()) {
|
||||
@@ -235,11 +217,12 @@ public final class ScoreService {
|
||||
ChatMember chosen = null;
|
||||
|
||||
if (preferredCat != null) {
|
||||
chosen = variants.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(v -> isSameChatCategory(v.getChatCategory(), preferredCat))
|
||||
.max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
|
||||
.orElse(null);
|
||||
for (ChatMember v : variants) {
|
||||
if (v != null && v.getChatCategory() == preferredCat) {
|
||||
chosen = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chosen == null) {
|
||||
@@ -255,39 +238,6 @@ public final class ScoreService {
|
||||
return representative;
|
||||
}
|
||||
|
||||
private static boolean isSameChatCategory(ChatCategory left, ChatCategory right) {
|
||||
return left != null
|
||||
&& right != null
|
||||
&& left.getCategoryNumber() == right.getCategoryNumber();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Projects the immutable score snapshot back into ChatMember display fields so
|
||||
* the normal station table can sort/filter by score without knowing the score
|
||||
* calculation internals.
|
||||
*
|
||||
* @param snap latest score snapshot
|
||||
*/
|
||||
private void applyScoreSnapshotToChatMembers(ScoreSnapshot snap) {
|
||||
if (snap == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Double> scoreByCallSignRaw = snap.getScoreByCallSignRaw();
|
||||
|
||||
for (ChatMember member : controller.snapshotChatMembers()) {
|
||||
if (member == null || member.getCallSignRaw() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Double score = scoreByCallSignRaw.get(normalizeCallRaw(member.getCallSignRaw()));
|
||||
member.setCurrentPriorityScore(score == null ? 0.0 : score);
|
||||
}
|
||||
|
||||
controller.fireUserListUpdate("Priority scores projected to ChatMember");
|
||||
}
|
||||
|
||||
private void updateSelectedScoreFromSnapshot(ScoreSnapshot snap) {
|
||||
if (snap == null || selectedCallSignRaw == null) {
|
||||
selectedCallPriorityScore.set(Double.NaN);
|
||||
|
||||
@@ -14,8 +14,8 @@ import java.util.TimerTask;
|
||||
* The task will be runned out of the singleton ChatController instance in an
|
||||
* intervall as specified by the Chatpreferences-instance (typically as
|
||||
* configured in the xml file.
|
||||
*
|
||||
*
|
||||
*
|
||||
*
|
||||
* @author prakt
|
||||
*
|
||||
*/
|
||||
@@ -34,20 +34,22 @@ public class ScoreboardUpdateTask extends TimerTask {
|
||||
Thread.currentThread().setName("BeaconTask");
|
||||
|
||||
ChatMessage beaconMSG = new ChatMessage();
|
||||
|
||||
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
|
||||
// replaceVariables = bcn_beaconText;
|
||||
|
||||
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
|
||||
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
|
||||
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
|
||||
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
|
||||
|
||||
MessageVariableResolver variableResolver =
|
||||
new MessageVariableResolver(this.chatController.getChatPreferences());
|
||||
String replaceVariables = variableResolver.resolveGlobalVariables(
|
||||
this.chatController.getChatPreferences().getBcn_beaconTextMainCat()
|
||||
);
|
||||
|
||||
|
||||
|
||||
beaconMSG.setMessageText(
|
||||
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
|
||||
beaconMSG.setMessageDirectedToServer(true);
|
||||
|
||||
|
||||
// System.out.println("########### " + replaceVariables);
|
||||
|
||||
|
||||
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
|
||||
|
||||
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
|
||||
@@ -56,8 +58,8 @@ public class ScoreboardUpdateTask extends TimerTask {
|
||||
} else {
|
||||
//do nothing, CQ is disabled
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package kst4contest.controller;
|
||||
|
||||
import kst4contest.logic.SignalDetector;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
@@ -22,7 +21,7 @@ import java.util.regex.Pattern;
|
||||
public final class StationMetricsService {
|
||||
|
||||
/** /cq <CALL> ... */
|
||||
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/-]+)\\b.*");
|
||||
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/]+)\\b.*");
|
||||
|
||||
/** Rolling window timestamps for momentum scoring. */
|
||||
private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32;
|
||||
@@ -196,7 +195,7 @@ public final class StationMetricsService {
|
||||
|
||||
private static String normalizeCallRaw(String s) {
|
||||
if (s == null) return null;
|
||||
return ChatMember.normalizeCallSignToBaseCallSign(s);
|
||||
return s.trim().toUpperCase();
|
||||
}
|
||||
|
||||
private static final class StationMetrics {
|
||||
|
||||
@@ -17,20 +17,4 @@ 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.
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,16 +108,6 @@ public class UpdateChecker {
|
||||
|
||||
Element element = (Element) node;
|
||||
updateInfos.setLatestVersionNumberOnServer(Double.parseDouble(element.getElementsByTagName("versionNumber").item(0).getTextContent()));
|
||||
|
||||
NodeList semanticVersionNodes =
|
||||
element.getElementsByTagName("semanticVersion");
|
||||
|
||||
if (semanticVersionNodes.getLength() > 0) {
|
||||
updateInfos.setLatestSemanticVersionOnServer(
|
||||
semanticVersionNodes.item(0).getTextContent()
|
||||
);
|
||||
}
|
||||
|
||||
updateInfos.setAdminMessage(element.getElementsByTagName("adminMessage").item(0).getTextContent());
|
||||
updateInfos.setMajorChanges(element.getElementsByTagName("majorChanges").item(0)
|
||||
.getTextContent());
|
||||
|
||||
@@ -26,14 +26,6 @@ public class UserActualizationTask extends TimerTask {
|
||||
|
||||
Thread.currentThread().setName("UserActualizationTask");
|
||||
|
||||
/*
|
||||
* File-based log synchronization is optional. Do not create, open or parse the
|
||||
* configured file while the feature is disabled.
|
||||
*/
|
||||
if (!client.getChatPreferences().isLogsynch_fileBasedWkdCallInterpreterEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// System.out.println("[Useract: ] Thread runned now");
|
||||
|
||||
// System.out.println("***********************Useract started");
|
||||
|
||||
@@ -101,143 +101,102 @@ public class Utils4KST {
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a frequency detected in a chat message into the kHz representation
|
||||
* used by the DX Cluster protocol.
|
||||
* 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>
|
||||
*
|
||||
* <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
|
||||
* @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."
|
||||
*/
|
||||
public static String normalizeFrequencyString(
|
||||
String qrgString,
|
||||
SimpleStringProperty optionalPrefix
|
||||
) {
|
||||
if (qrgString == null || qrgString.isBlank()) {
|
||||
return "";
|
||||
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();
|
||||
}
|
||||
|
||||
/*
|
||||
* 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(',', '.');
|
||||
|
||||
/*
|
||||
* 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);
|
||||
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
|
||||
|
||||
if (completeFrequencyMatcher.matches()) {
|
||||
return formatDxClusterFrequency(
|
||||
completeFrequencyMatcher.group(1),
|
||||
completeFrequencyMatcher.group(2),
|
||||
completeFrequencyMatcher.group(3)
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
return stringAggregation; //if nothing else helps
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -39,63 +39,26 @@ public class WinTestSkedSender {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a ContestSked into Win-Test by sending the
|
||||
* LOCKSKED / ADDSKED / UNLOCKSKED sequence.
|
||||
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
|
||||
* sequence via UDP broadcast.
|
||||
*
|
||||
* @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
|
||||
* @param sked the sked to push
|
||||
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0)
|
||||
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST")
|
||||
*/
|
||||
public void pushSkedToWinTest(ContestSked sked,
|
||||
String targetCallsign,
|
||||
double frequencyKHz,
|
||||
String notes,
|
||||
int mode) {
|
||||
|
||||
public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) {
|
||||
try {
|
||||
sendLockSked();
|
||||
|
||||
sendAddSked(
|
||||
sked,
|
||||
targetCallsign,
|
||||
frequencyKHz,
|
||||
notes,
|
||||
mode
|
||||
);
|
||||
|
||||
sendAddSked(sked, frequencyKHz, notes, modeOverride);
|
||||
sendUnlockSked();
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,53 +86,46 @@ public class WinTestSkedSender {
|
||||
|
||||
/**
|
||||
* Sends an ADDSKED message with the sked details.
|
||||
*
|
||||
* <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>
|
||||
* <p>
|
||||
* Win-Test ADDSKED data format (from wtKST):
|
||||
* <pre>
|
||||
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
|
||||
* </pre>
|
||||
* <p>
|
||||
* Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch).
|
||||
* The C# code adds 60 seconds to compensate.
|
||||
*/
|
||||
private void sendAddSked(ContestSked sked,
|
||||
String targetCallsign,
|
||||
double frequencyKHz,
|
||||
String notes,
|
||||
int mode) throws Exception {
|
||||
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;
|
||||
|
||||
long wtTimestamp =
|
||||
sked.getSkedTimeEpoch() / 1000L;
|
||||
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
|
||||
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
|
||||
|
||||
// Frequency in 0.1 kHz units.
|
||||
long frequencyTenthKHz =
|
||||
Math.round(frequencyKHz * 10.0);
|
||||
// Win-Test band ID
|
||||
int bandId = toWinTestBandId(sked.getBand());
|
||||
|
||||
int bandId =
|
||||
toWinTestBandId(sked.getBand());
|
||||
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
|
||||
int mode;
|
||||
if (modeOverride >= 0) {
|
||||
mode = modeOverride;
|
||||
} else {
|
||||
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Accept only the mode IDs supported by this UI.
|
||||
* Any unexpected value falls back to SSB.
|
||||
*/
|
||||
int winTestMode =
|
||||
mode == 0
|
||||
? 0
|
||||
: 1;
|
||||
String data = wtTimestamp
|
||||
+ " " + freqTenthKHz
|
||||
+ " " + bandId
|
||||
+ " " + mode
|
||||
+ " \"" + sked.getTargetCallsign() + "\""
|
||||
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||
|
||||
String data =
|
||||
wtTimestamp
|
||||
+ " " + frequencyTenthKHz
|
||||
+ " " + bandId
|
||||
+ " " + winTestMode
|
||||
+ " \"" + targetCallsign + "\""
|
||||
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||
|
||||
WinTestMessage message = new WinTestMessage(
|
||||
WinTestMessage msg = new WinTestMessage(
|
||||
WinTestMessage.MessageType.ADDSKED,
|
||||
stationName,
|
||||
"",
|
||||
data
|
||||
);
|
||||
|
||||
sendUdp(message);
|
||||
stationName, "",
|
||||
data);
|
||||
sendUdp(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,8 +155,6 @@ public class WinTestSkedSender {
|
||||
public static int toWinTestBandId(Band band) {
|
||||
if (band == null) return 12; // default to 144 MHz
|
||||
return switch (band) {
|
||||
case B_50 -> 10;
|
||||
case B_70 -> 11;
|
||||
case B_144 -> 12;
|
||||
case B_432 -> 14;
|
||||
case B_1296 -> 16;
|
||||
@@ -212,6 +166,18 @@ 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,202 +0,0 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Runtime cache for worked Maidenhead gross fields per band.
|
||||
*
|
||||
* <p>The cache is used by the "new locator" station filter. It is kept in memory
|
||||
* for fast UI predicates and rebuilt from SQLite on startup/refresh. New QSO log
|
||||
* packets update both SQLite and this cache immediately.</p>
|
||||
*/
|
||||
public final class WorkedGrossFieldCache {
|
||||
|
||||
private static final Pattern MAIDENHEAD_6_PATTERN =
|
||||
Pattern.compile("(?i)([A-R]{2}[0-9]{2}[A-X]{2})");
|
||||
|
||||
private final Map<Band, Set<String>> workedGrossFieldsByBand = new EnumMap<>(Band.class);
|
||||
|
||||
/**
|
||||
* Replaces the full cache content with data read from the database.
|
||||
*
|
||||
* @param databaseSnapshot map of band to worked gross fields
|
||||
*/
|
||||
public synchronized void rebuildFromDatabaseSnapshot(Map<Band, Set<String>> databaseSnapshot) {
|
||||
workedGrossFieldsByBand.clear();
|
||||
|
||||
if (databaseSnapshot == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<Band, Set<String>> entry : databaseSnapshot.entrySet()) {
|
||||
Band band = entry.getKey();
|
||||
Set<String> grossFields = entry.getValue();
|
||||
|
||||
if (band == null || grossFields == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Set<String> normalizedGrossFields = workedGrossFieldsByBand.computeIfAbsent(band, ignored -> new HashSet<>());
|
||||
for (String grossField : grossFields) {
|
||||
String normalizedGrossField = normalizeGrossField(grossField);
|
||||
if (normalizedGrossField != null) {
|
||||
normalizedGrossFields.add(normalizedGrossField);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds one worked locator to the cache.
|
||||
*
|
||||
* @param band worked band
|
||||
* @param locatorOrGrossField six-character locator or four-character gross field
|
||||
*/
|
||||
public synchronized void addWorked(Band band, String locatorOrGrossField) {
|
||||
String grossField = extractGrossField(locatorOrGrossField);
|
||||
if (band == null || grossField == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
workedGrossFieldsByBand
|
||||
.computeIfAbsent(band, ignored -> new HashSet<>())
|
||||
.add(grossField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all worked-band flags from stored ChatMember rows. This is only a fallback
|
||||
* for legacy data before WorkedGrossField existed or when logger packets did not
|
||||
* provide a locator.
|
||||
*
|
||||
* @param storedMembers stored ChatMember rows
|
||||
*/
|
||||
public synchronized void addWorkedBandsFromStoredChatMembers(Collection<ChatMember> storedMembers) {
|
||||
if (storedMembers == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ChatMember member : storedMembers) {
|
||||
if (member == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String locator = member.getQra();
|
||||
if (member.isWorked144()) addWorked(Band.B_144, locator);
|
||||
if (member.isWorked432()) addWorked(Band.B_432, locator);
|
||||
if (member.isWorked1240()) addWorked(Band.B_1296, locator);
|
||||
if (member.isWorked2300()) addWorked(Band.B_2320, locator);
|
||||
if (member.isWorked3400()) addWorked(Band.B_3400, locator);
|
||||
if (member.isWorked5600()) addWorked(Band.B_5760, locator);
|
||||
if (member.isWorked10G()) addWorked(Band.B_10G, locator);
|
||||
if (member.isWorked50()) addWorked(Band.B_50, locator);
|
||||
if (member.isWorked70()) addWorked(Band.B_70, locator);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a locator gross field is already worked on a band.
|
||||
*
|
||||
* @param band band to check
|
||||
* @param locatorOrGrossField six-character locator or four-character gross field
|
||||
* @return true if the gross field is already worked on that band
|
||||
*/
|
||||
public synchronized boolean isGrossFieldWorked(Band band, String locatorOrGrossField) {
|
||||
String grossField = extractGrossField(locatorOrGrossField);
|
||||
if (band == null || grossField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return workedGrossFieldsByBand
|
||||
.getOrDefault(band, Set.of())
|
||||
.contains(grossField);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a locator gross field has already been worked on any band.
|
||||
*
|
||||
* <p>The UI grid status and the "Only new grids" filter use this any-band logic.
|
||||
* This avoids user errors caused by wrong active-band settings and fits single
|
||||
* band contests such as NAC better.</p>
|
||||
*
|
||||
* @param locatorOrGrossField six-character locator or four-character gross field
|
||||
* @return true if the gross field exists in the cache on any band
|
||||
*/
|
||||
public synchronized boolean isGrossFieldWorkedAny(String locatorOrGrossField) {
|
||||
String grossField = extractGrossField(locatorOrGrossField);
|
||||
if (grossField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Set<String> workedGrossFields : workedGrossFieldsByBand.values()) {
|
||||
if (workedGrossFields != null && workedGrossFields.contains(grossField)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to normalize a locator. Accepts a plain six-character locator or extracts
|
||||
* one from exchange strings such as "001JO41HK".
|
||||
*
|
||||
* @param rawLocatorOrExchange raw locator/exchange text
|
||||
* @return normalized six-character locator, or null if none was found
|
||||
*/
|
||||
public static String extractLocator6(String rawLocatorOrExchange) {
|
||||
if (rawLocatorOrExchange == null || rawLocatorOrExchange.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Matcher matcher = MAIDENHEAD_6_PATTERN.matcher(rawLocatorOrExchange.trim());
|
||||
if (!matcher.find()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return matcher.group(1).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and normalizes the four-character gross field.
|
||||
*
|
||||
* @param rawLocatorOrGrossField six-character locator, four-character gross field or exchange text
|
||||
* @return normalized gross field, or null if no valid value is available
|
||||
*/
|
||||
public static String extractGrossField(String rawLocatorOrGrossField) {
|
||||
if (rawLocatorOrGrossField == null || rawLocatorOrGrossField.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String trimmed = rawLocatorOrGrossField.trim().toUpperCase(Locale.ROOT);
|
||||
|
||||
if (trimmed.matches("[A-R]{2}[0-9]{2}")) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
String locator6 = extractLocator6(trimmed);
|
||||
if (locator6 == null || locator6.length() < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return locator6.substring(0, 4);
|
||||
}
|
||||
|
||||
private static String normalizeGrossField(String grossField) {
|
||||
if (grossField == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized = grossField.trim().toUpperCase(Locale.ROOT);
|
||||
return normalized.matches("[A-R]{2}[0-9]{2}") ? normalized : null;
|
||||
}
|
||||
}
|
||||
@@ -1,173 +1,289 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.Socket;
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
import java.nio.charset.Charset;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Serializes and writes exactly one immutable ON4KST connection session.
|
||||
* This thread is responsible for sending content to the chat. As we only use
|
||||
* the tx function, there is no content in run() method
|
||||
*
|
||||
*
|
||||
* <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 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;
|
||||
private PrintWriter writer;
|
||||
private Socket socket;
|
||||
private ChatController client;
|
||||
private OutputStream output;
|
||||
|
||||
/**
|
||||
* Compatibility constructor for the pre-session controller path.
|
||||
*
|
||||
* @deprecated new connections should be created by
|
||||
* {@link On4KstConnectionManager}
|
||||
*/
|
||||
@Deprecated
|
||||
public WriteThread(Socket socket, ChatController client) throws IOException {
|
||||
this(0L, socket, client.getMessageTXBus(),
|
||||
client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber(),
|
||||
ignored -> true,
|
||||
ignored -> { }, System.out::println);
|
||||
}
|
||||
private ChatMessage messageToBeSend;
|
||||
|
||||
/**
|
||||
* 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 WriteThread(
|
||||
long sessionId,
|
||||
Socket socket,
|
||||
LinkedBlockingQueue<ChatMessage> transmitQueue,
|
||||
int defaultCategory,
|
||||
LongPredicate sessionIsActive,
|
||||
Consumer<Throwable> connectionFailure,
|
||||
Consumer<String> rejectedFrame
|
||||
) throws IOException {
|
||||
this.sessionId = sessionId;
|
||||
public WriteThread(Socket socket, ChatController client) throws InterruptedException {
|
||||
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));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Thread.currentThread().setName("WriteToOn4Kst-" + sessionId);
|
||||
LOGGER.log(Level.FINE,
|
||||
"ON4KST writer started for session {0}", sessionId);
|
||||
this.client = client;
|
||||
|
||||
try {
|
||||
while (!isInterrupted() && sessionIsActive.test(sessionId)) {
|
||||
ChatMessage message = transmitQueue.take();
|
||||
if (isPoisonPill(message)) {
|
||||
break;
|
||||
}
|
||||
if (!sessionIsActive.test(sessionId)) {
|
||||
break;
|
||||
}
|
||||
output = socket.getOutputStream();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
writer = new PrintWriter(output, true, StandardCharsets.UTF_8);
|
||||
|
||||
} catch (IOException ex) {
|
||||
System.out.println("Error getting output stream: " + ex.getMessage());
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
* 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>
|
||||
*
|
||||
* @param messageToServer
|
||||
* @throws InterruptedException
|
||||
*/
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void txKSTFormatted(ChatMessage messageToServer) throws InterruptedException {
|
||||
|
||||
// writer.println(messageToServer.getMessageText());
|
||||
messageToBeSend = messageToServer;
|
||||
|
||||
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 {
|
||||
interrupt();
|
||||
writer.close();
|
||||
socket.close();
|
||||
|
||||
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();
|
||||
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();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
// 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();
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// while (true) {
|
||||
//
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,6 @@ 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;
|
||||
@@ -23,14 +16,13 @@ public class keepAliveMessageSenderTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
|
||||
Thread.currentThread().setName("KeepAliveMessageSenderTask");
|
||||
|
||||
|
||||
// System.out.println("[keepalive: ] Thread runned now");
|
||||
|
||||
|
||||
ChatMessage keepAliveMSG = new ChatMessage();
|
||||
// WriteThread appends exactly one CRLF. An empty frame is the protocol reply.
|
||||
keepAliveMSG.setMessageText("");
|
||||
keepAliveMSG.setMessageText("\r");
|
||||
keepAliveMSG.setMessageDirectedToServer(true);
|
||||
|
||||
// System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString() + " [keepaliveTask]: Sending keepalive: "
|
||||
@@ -41,4 +33,4 @@ public class keepAliveMessageSenderTask extends TimerTask {
|
||||
this.client.getMessageTXBus().add(keepAliveMSG);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
package kst4contest.logic;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Resolves band availability and band-upgrade opportunities from one or more
|
||||
* active {@link ChatMember} variants of the same base callsign.
|
||||
*
|
||||
* <p>The resolver deliberately separates a band hint from an exact frequency:
|
||||
* {@code knownActiveBands} remains the source for detected QRGs with timestamps,
|
||||
* while the station name may add a band without inventing a frequency.</p>
|
||||
*
|
||||
* <p>A manual NOT-QRV flag always overrides automatic evidence. Worked flags are
|
||||
* evaluated separately because an offered band may still be useful for display,
|
||||
* even when it is no longer a band-upgrade opportunity.</p>
|
||||
*/
|
||||
public final class BandOpportunityResolver {
|
||||
|
||||
public static final long RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS = 30L * 60L * 1000L;
|
||||
|
||||
private static final Map<Band, Pattern> STATION_NAME_BAND_PATTERNS = createStationNameBandPatterns();
|
||||
|
||||
private BandOpportunityResolver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the common band state using the application-wide 30-minute window
|
||||
* for frequency evidence. Name-derived band hints remain valid while the
|
||||
* ChatMember is present in the active chat model.
|
||||
*/
|
||||
public static Resolution resolve(Collection<ChatMember> variants, long nowEpochMs) {
|
||||
return resolve(variants, nowEpochMs, RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves offered, worked and manually excluded bands across all supplied
|
||||
* category/callsign variants.
|
||||
*/
|
||||
public static Resolution resolve(Collection<ChatMember> variants,
|
||||
long nowEpochMs,
|
||||
long dynamicEvidenceMaxAgeMs) {
|
||||
|
||||
EnumSet<Band> offeredBands = EnumSet.noneOf(Band.class);
|
||||
EnumSet<Band> workedBands = EnumSet.noneOf(Band.class);
|
||||
EnumSet<Band> notQrvBands = EnumSet.noneOf(Band.class);
|
||||
|
||||
if (variants == null) {
|
||||
return new Resolution(offeredBands, workedBands, notQrvBands);
|
||||
}
|
||||
|
||||
for (ChatMember member : variants) {
|
||||
if (member == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
collectRecentFrequencyBands(
|
||||
member,
|
||||
offeredBands,
|
||||
nowEpochMs,
|
||||
dynamicEvidenceMaxAgeMs
|
||||
);
|
||||
offeredBands.addAll(detectBandsFromStationName(member.getName()));
|
||||
collectWorkedBands(member, workedBands);
|
||||
collectNotQrvBands(member, notQrvBands);
|
||||
}
|
||||
|
||||
return new Resolution(offeredBands, workedBands, notQrvBands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bands enabled in the local station setup. Bands above 10 GHz
|
||||
* remain excluded because the current preferences do not provide active-band
|
||||
* flags for them.
|
||||
*/
|
||||
public static EnumSet<Band> getEnabledStationBands(ChatPreferences preferences) {
|
||||
EnumSet<Band> enabledBands = EnumSet.noneOf(Band.class);
|
||||
if (preferences == null) {
|
||||
return enabledBands;
|
||||
}
|
||||
|
||||
if (preferences.isStn_bandActive50()) enabledBands.add(Band.B_50);
|
||||
if (preferences.isStn_bandActive70()) enabledBands.add(Band.B_70);
|
||||
if (preferences.isStn_bandActive144()) enabledBands.add(Band.B_144);
|
||||
if (preferences.isStn_bandActive432()) enabledBands.add(Band.B_432);
|
||||
if (preferences.isStn_bandActive1240()) enabledBands.add(Band.B_1296);
|
||||
if (preferences.isStn_bandActive2300()) enabledBands.add(Band.B_2320);
|
||||
if (preferences.isStn_bandActive3400()) enabledBands.add(Band.B_3400);
|
||||
if (preferences.isStn_bandActive5600()) enabledBands.add(Band.B_5760);
|
||||
if (preferences.isStn_bandActive10G()) enabledBands.add(Band.B_10G);
|
||||
|
||||
return enabledBands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects explicit amateur-band designators in a station name.
|
||||
*
|
||||
* <p>The boundary rules are intentionally stricter than simple token splitting.
|
||||
* In particular, the trailing {@code 2} in {@code 1.2 cm} must not be mistaken
|
||||
* for the 2 m band.</p>
|
||||
*/
|
||||
public static EnumSet<Band> detectBandsFromStationName(String stationName) {
|
||||
EnumSet<Band> detectedBands = EnumSet.noneOf(Band.class);
|
||||
if (stationName == null || stationName.isBlank()) {
|
||||
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;
|
||||
}
|
||||
|
||||
private static void collectRecentFrequencyBands(ChatMember member,
|
||||
EnumSet<Band> target,
|
||||
long nowEpochMs,
|
||||
long dynamicEvidenceMaxAgeMs) {
|
||||
if (member.getKnownActiveBands() == null || member.getKnownActiveBands().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry
|
||||
: member.getKnownActiveBands().entrySet()) {
|
||||
|
||||
Band band = entry.getKey();
|
||||
ChatMember.ActiveFrequencyInfo info = entry.getValue();
|
||||
if (band == null || info == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
long ageMs = nowEpochMs - info.timestampEpoch;
|
||||
boolean ageAccepted = dynamicEvidenceMaxAgeMs <= 0L
|
||||
? ageMs >= 0L
|
||||
: ageMs >= 0L && ageMs <= dynamicEvidenceMaxAgeMs;
|
||||
|
||||
if (ageAccepted) {
|
||||
target.add(band);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void collectWorkedBands(ChatMember member, EnumSet<Band> target) {
|
||||
if (member.isWorked50()) target.add(Band.B_50);
|
||||
if (member.isWorked70()) target.add(Band.B_70);
|
||||
if (member.isWorked144()) target.add(Band.B_144);
|
||||
if (member.isWorked432()) target.add(Band.B_432);
|
||||
if (member.isWorked1240()) target.add(Band.B_1296);
|
||||
if (member.isWorked2300()) target.add(Band.B_2320);
|
||||
if (member.isWorked3400()) target.add(Band.B_3400);
|
||||
if (member.isWorked5600()) target.add(Band.B_5760);
|
||||
if (member.isWorked10G()) target.add(Band.B_10G);
|
||||
if (member.isWorked24G()) target.add(Band.B_24G);
|
||||
}
|
||||
|
||||
private static void collectNotQrvBands(ChatMember member, EnumSet<Band> target) {
|
||||
if (!member.isQrv50()) target.add(Band.B_50);
|
||||
if (!member.isQrv70()) target.add(Band.B_70);
|
||||
if (!member.isQrv144()) target.add(Band.B_144);
|
||||
if (!member.isQrv432()) target.add(Band.B_432);
|
||||
if (!member.isQrv1240()) target.add(Band.B_1296);
|
||||
if (!member.isQrv2300()) target.add(Band.B_2320);
|
||||
if (!member.isQrv3400()) target.add(Band.B_3400);
|
||||
if (!member.isQrv5600()) target.add(Band.B_5760);
|
||||
if (!member.isQrv10G()) target.add(Band.B_10G);
|
||||
// There is currently no persisted NOT-QRV flag for 24 GHz.
|
||||
}
|
||||
|
||||
private static Map<Band, Pattern> createStationNameBandPatterns() {
|
||||
Map<Band, Pattern> patterns = new EnumMap<>(Band.class);
|
||||
|
||||
// Bare "70" and bare "6" are already claimed by the 70cm/6cm shorthand below
|
||||
// (their "CM" suffix is optional), so 4m/6m must require an explicit MHz/"M"
|
||||
// suffix here to avoid misreading a cm-band shorthand as 70/50 MHz.
|
||||
patterns.put(Band.B_50, bandPattern("50(?:\\s*MHZ)?|6\\s*M"));
|
||||
patterns.put(Band.B_70, bandPattern("70\\s*MHZ|4\\s*M"));
|
||||
patterns.put(Band.B_144, bandPattern("144(?:\\s*MHZ)?|2(?:\\s*M)?"));
|
||||
patterns.put(Band.B_432, bandPattern("432(?:\\s*MHZ)?|70(?:\\s*CM)?"));
|
||||
patterns.put(Band.B_1296, bandPattern("1296(?:\\s*MHZ)?|23(?:\\s*CM)?"));
|
||||
patterns.put(Band.B_2320, bandPattern("(?:2300|2320)(?:\\s*MHZ)?|13(?:\\s*CM)?"));
|
||||
patterns.put(Band.B_3400, bandPattern("3400(?:\\s*MHZ)?|9(?:\\s*CM)?"));
|
||||
patterns.put(Band.B_5760, bandPattern("(?:5600|5760)(?:\\s*MHZ)?|6(?:\\s*CM)?"));
|
||||
patterns.put(Band.B_10G, bandPattern("10368(?:\\s*MHZ)?|10\\s*G(?:HZ)?|3(?:\\s*CM)?"));
|
||||
patterns.put(Band.B_24G, bandPattern("24048(?:\\s*MHZ)?|24\\s*G(?:HZ)?|1[.,]2(?:\\s*CM)?"));
|
||||
|
||||
return Collections.unmodifiableMap(patterns);
|
||||
}
|
||||
|
||||
private static Pattern bandPattern(String alternatives) {
|
||||
return Pattern.compile(
|
||||
"(?<![A-Z0-9.,])(?:" + alternatives + ")(?![A-Z0-9.,])",
|
||||
Pattern.CASE_INSENSITIVE
|
||||
);
|
||||
}
|
||||
|
||||
/** Immutable result of one callsign-wide band resolution. */
|
||||
public static final class Resolution {
|
||||
|
||||
private final EnumSet<Band> offeredBands;
|
||||
private final EnumSet<Band> workedBands;
|
||||
private final EnumSet<Band> notQrvBands;
|
||||
|
||||
private Resolution(EnumSet<Band> offeredBands,
|
||||
EnumSet<Band> workedBands,
|
||||
EnumSet<Band> notQrvBands) {
|
||||
this.offeredBands = copyOf(offeredBands);
|
||||
this.workedBands = copyOf(workedBands);
|
||||
this.notQrvBands = copyOf(notQrvBands);
|
||||
}
|
||||
|
||||
/** Returns all recent/name-derived bands before NOT-QRV is applied. */
|
||||
public EnumSet<Band> getOfferedBands() {
|
||||
return copyOf(offeredBands);
|
||||
}
|
||||
|
||||
public EnumSet<Band> getWorkedBands() {
|
||||
return copyOf(workedBands);
|
||||
}
|
||||
|
||||
public EnumSet<Band> getNotQrvBands() {
|
||||
return copyOf(notQrvBands);
|
||||
}
|
||||
|
||||
/** Returns offered bands after manual NOT-QRV exclusions. */
|
||||
public EnumSet<Band> getAvailableBands() {
|
||||
EnumSet<Band> availableBands = copyOf(offeredBands);
|
||||
availableBands.removeAll(notQrvBands);
|
||||
return availableBands;
|
||||
}
|
||||
|
||||
/** Returns offered, QRV, enabled and not-yet-worked bands. */
|
||||
public EnumSet<Band> getUnworkedEnabledBands(EnumSet<Band> enabledBands) {
|
||||
EnumSet<Band> opportunities = getAvailableBands();
|
||||
if (enabledBands == null || enabledBands.isEmpty()) {
|
||||
opportunities.clear();
|
||||
return opportunities;
|
||||
}
|
||||
|
||||
opportunities.retainAll(enabledBands);
|
||||
opportunities.removeAll(workedBands);
|
||||
return opportunities;
|
||||
}
|
||||
|
||||
public boolean hasBandEvidence() {
|
||||
return !offeredBands.isEmpty();
|
||||
}
|
||||
|
||||
private static EnumSet<Band> copyOf(EnumSet<Band> source) {
|
||||
return source == null || source.isEmpty()
|
||||
? EnumSet.noneOf(Band.class)
|
||||
: EnumSet.copyOf(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,9 @@ package kst4contest.logic;
|
||||
import kst4contest.controller.StationMetricsService;
|
||||
import kst4contest.model.*;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Priority score calculation (off FX-thread).
|
||||
@@ -16,8 +16,10 @@ import java.util.List;
|
||||
*/
|
||||
public class PriorityCalculator {
|
||||
|
||||
/** Max age for "known active bands" (derived from chat history). */
|
||||
private static final long RX_BANDS_MAX_AGE_MS = 30L * 60L * 1000L; // 30 minutes
|
||||
|
||||
public double calculatePriority(ChatMember member,
|
||||
Collection<ChatMember> callsignVariants,
|
||||
ChatPreferences prefs,
|
||||
List<ContestSked> activeSkeds,
|
||||
StationMetricsService.Snapshot metricsSnapshot,
|
||||
@@ -31,51 +33,41 @@ public class PriorityCalculator {
|
||||
// --------------------------------------------------------------------
|
||||
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
|
||||
// --------------------------------------------------------------------
|
||||
Collection<ChatMember> variants = callsignVariants == null || callsignVariants.isEmpty()
|
||||
? List.of(member)
|
||||
: callsignVariants;
|
||||
// --------------------------------------------------------------------
|
||||
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
|
||||
// --------------------------------------------------------------------
|
||||
EnumSet<Band> myEnabledBands = getMyEnabledBands(prefs);
|
||||
|
||||
BandOpportunityResolver.Resolution bandResolution =
|
||||
BandOpportunityResolver.resolve(variants, nowEpochMs);
|
||||
|
||||
EnumSet<Band> myEnabledBands =
|
||||
BandOpportunityResolver.getEnabledStationBands(prefs);
|
||||
|
||||
// ChatMember.worked remains UI-only. Scoring uses only per-band worked flags.
|
||||
EnumSet<Band> workedBandsForScoring = bandResolution.getWorkedBands();
|
||||
// "worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
|
||||
// IMPORTANT: ChatMember.worked is UI-only and NOT used in scoring.
|
||||
EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
|
||||
|
||||
// Remaining bands that are:
|
||||
// - recently offered by the station (from knownActiveBands history)
|
||||
// - enabled at our station
|
||||
// - NOT worked yet (per-band flags)
|
||||
// If we do not know offered bands (history empty), this remains empty.
|
||||
EnumSet<Band> unworkedPossible = EnumSet.noneOf(Band.class);
|
||||
|
||||
EnumSet<Band> stationOfferedBands = bandResolution.getOfferedBands();
|
||||
EnumSet<Band> stationAvailableBands = bandResolution.getAvailableBands();
|
||||
EnumSet<Band> stationOfferedBands = getStationOfferedBandsFromHistory(member, nowEpochMs);
|
||||
EnumSet<Band> possibleBands = stationOfferedBands.isEmpty()
|
||||
? EnumSet.noneOf(Band.class)
|
||||
: EnumSet.copyOf(stationAvailableBands);
|
||||
? EnumSet.noneOf(Band.class) // unknown => don't hard-filter
|
||||
: EnumSet.copyOf(stationOfferedBands);
|
||||
|
||||
if (!stationOfferedBands.isEmpty()) {
|
||||
if (!possibleBands.isEmpty()) {
|
||||
possibleBands.retainAll(myEnabledBands);
|
||||
if (possibleBands.isEmpty()) {
|
||||
// Known bands are disabled locally or manually marked NOT QRV.
|
||||
// We know their bands, but none of them are enabled at our station.
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
unworkedPossible = EnumSet.copyOf(possibleBands);
|
||||
unworkedPossible.removeAll(workedBandsForScoring);
|
||||
|
||||
// If already worked on all possible bands => no priority on them anymore (contest logic).
|
||||
if (unworkedPossible.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Missing band evidence is not automatically negative. A complete manual
|
||||
* NOT-QRV exclusion is different: if every enabled own band is excluded,
|
||||
* this station cannot be a current contest candidate.
|
||||
*/
|
||||
EnumSet<Band> notExplicitlyExcluded = EnumSet.copyOf(myEnabledBands);
|
||||
notExplicitlyExcluded.removeAll(bandResolution.getNotQrvBands());
|
||||
if (!myEnabledBands.isEmpty() && notExplicitlyExcluded.isEmpty()) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
@@ -83,13 +75,12 @@ public class PriorityCalculator {
|
||||
// --------------------------------------------------------------------
|
||||
double score = 100.0;
|
||||
|
||||
/*
|
||||
* 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 (!member.isWorked()) {
|
||||
// score += 200.0;
|
||||
// }
|
||||
|
||||
//"worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
|
||||
// EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
|
||||
|
||||
if (workedBandsForScoring.isEmpty()) {
|
||||
score += 200.0; // never worked on any supported band -> higher priority
|
||||
@@ -191,11 +182,9 @@ public class PriorityCalculator {
|
||||
}
|
||||
|
||||
// Manual sked fail (path likely bad) => strong, permanent penalty until reset
|
||||
/*
|
||||
* Remember the explicit failure state here. The actual penalty is applied
|
||||
* after the sked contribution has been calculated.
|
||||
*/
|
||||
manualSkedFailed = mx.manualSkedFailed;
|
||||
if (mx.manualSkedFailed) {
|
||||
score *= 0.15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,28 +218,53 @@ 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);
|
||||
}
|
||||
|
||||
private static EnumSet<Band> getMyEnabledBands(ChatPreferences prefs) {
|
||||
EnumSet<Band> out = EnumSet.noneOf(Band.class);
|
||||
if (prefs.isStn_bandActive144()) out.add(Band.B_144);
|
||||
if (prefs.isStn_bandActive432()) out.add(Band.B_432);
|
||||
if (prefs.isStn_bandActive1240()) out.add(Band.B_1296);
|
||||
if (prefs.isStn_bandActive2300()) out.add(Band.B_2320);
|
||||
if (prefs.isStn_bandActive3400()) out.add(Band.B_3400);
|
||||
if (prefs.isStn_bandActive5600()) out.add(Band.B_5760);
|
||||
if (prefs.isStn_bandActive10G()) out.add(Band.B_10G);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static EnumSet<Band> getStationOfferedBandsFromHistory(ChatMember member, long nowEpochMs) {
|
||||
EnumSet<Band> out = EnumSet.noneOf(Band.class);
|
||||
Map<Band, ChatMember.ActiveFrequencyInfo> map = member.getKnownActiveBands();
|
||||
if (map == null || map.isEmpty()) return out;
|
||||
|
||||
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> e : map.entrySet()) {
|
||||
if (e == null || e.getKey() == null || e.getValue() == null) continue;
|
||||
long age = nowEpochMs - e.getValue().timestampEpoch;
|
||||
if (age <= RX_BANDS_MAX_AGE_MS) {
|
||||
out.add(e.getKey());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static EnumSet<Band> getWorkedBands(ChatMember member) {
|
||||
EnumSet<Band> out = EnumSet.noneOf(Band.class);
|
||||
if (member.isWorked144()) out.add(Band.B_144);
|
||||
if (member.isWorked432()) out.add(Band.B_432);
|
||||
if (member.isWorked1240()) out.add(Band.B_1296);
|
||||
if (member.isWorked2300()) out.add(Band.B_2320);
|
||||
if (member.isWorked3400()) out.add(Band.B_3400);
|
||||
if (member.isWorked5600()) out.add(Band.B_5760);
|
||||
if (member.isWorked10G()) out.add(Band.B_10G);
|
||||
if (member.isWorked24G()) out.add(Band.B_24G);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int findNextAirplaneArrivingMinutes(AirPlaneReflectionInfo apInfo) {
|
||||
try {
|
||||
if (apInfo.getRisingAirplanes() == null || apInfo.getRisingAirplanes().isEmpty()) return -1;
|
||||
|
||||
@@ -1,381 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ package kst4contest.model;
|
||||
* Used for plausibility checks in the Smart Parser.
|
||||
*/
|
||||
public enum Band {
|
||||
B_50(50.000, 54.000, "50"),
|
||||
B_70(70.000, 70.500, "70"),
|
||||
B_144(144.000, 146.000, "144"),
|
||||
B_432(432.000, 434.000, "432"),
|
||||
B_1296(1296.000, 1298.000, "1296"),
|
||||
@@ -31,66 +29,6 @@ public enum Band {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Resolves a configured MHz prefix to one of the bands supported by the
|
||||
* frequency parser.
|
||||
*
|
||||
* <p>The former free-text preference accepted arbitrary numeric values even
|
||||
* though only prefixes represented by this enum can be used for a plausible
|
||||
* frequency. Keeping the lookup here gives the UI and the parser one common
|
||||
* definition of a valid fallback band.</p>
|
||||
*
|
||||
* @param prefix configured MHz prefix, for example {@code 144} or {@code 10368}
|
||||
* @return matching band, or {@code null} if the prefix is not supported
|
||||
*/
|
||||
public static Band fromPrefix(String prefix) {
|
||||
if (prefix == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalizedPrefix = prefix.trim();
|
||||
|
||||
for (Band band : values()) {
|
||||
if (band.prefix.equals(normalizedPrefix)) {
|
||||
return band;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lower edge used as practical analysis frequency when only the band
|
||||
* is known. This keeps the batch reachability calculation deterministic.
|
||||
*
|
||||
* @return frequency in MHz
|
||||
*/
|
||||
public double getDefaultAnalysisFrequencyMHz() {
|
||||
return minFreq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a compact label for table display and filter controls.
|
||||
*
|
||||
* @return human readable band label
|
||||
*/
|
||||
public String getDisplayLabel() {
|
||||
switch (this) {
|
||||
case B_50: return "50";
|
||||
case B_70: return "70";
|
||||
case B_144: return "144";
|
||||
case B_432: return "432";
|
||||
case B_1296: return "1296";
|
||||
case B_2320: return "2320";
|
||||
case B_3400: return "3400";
|
||||
case B_5760: return "5760";
|
||||
case B_10G: return "10G";
|
||||
case B_24G: return "24G";
|
||||
default: return prefix;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a specific frequency falls within this band's limits.
|
||||
*/
|
||||
|
||||
@@ -10,13 +10,10 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public class ChatMember {
|
||||
|
||||
|
||||
|
||||
|
||||
long lastFlagsChangeEpochMs; // timestamp of the last worked/not-QRV flag change in the internal DB
|
||||
|
||||
// private final BooleanProperty workedInfoChangeFireListEventTrigger = new SimpleBooleanProperty();
|
||||
@@ -26,16 +23,9 @@ public class ChatMember {
|
||||
String name;
|
||||
String callSignRaw; //without -2 or -70 etc.
|
||||
|
||||
/**
|
||||
* A directional opportunity inferred from a directed chat message remains
|
||||
* relevant for five minutes. The timestamp is used instead of a permanent
|
||||
* boolean so an old antenna-direction assumption cannot remain visible
|
||||
* indefinitely.
|
||||
*/
|
||||
static final long DIRECTION_OPPORTUNITY_VALIDITY_MILLIS = 5L * 60L * 1000L;
|
||||
|
||||
private volatile long directionOpportunityValidUntilEpochMs;
|
||||
|
||||
boolean isInAngleAndRange; //if he tries a sked in my dir, he is in range, will process that in the messages
|
||||
|
||||
// String frequency; // last known qrg of the station
|
||||
|
||||
@@ -72,8 +62,6 @@ public class ChatMember {
|
||||
/**
|
||||
* Chatmember is qrv at all band except we initialize anything other, depending to user entry
|
||||
*/
|
||||
boolean qrv50 = true;
|
||||
boolean qrv70 = true;
|
||||
boolean qrv144 = true;
|
||||
boolean qrv432 = true;
|
||||
boolean qrv1240 = true;
|
||||
@@ -86,10 +74,6 @@ public class ChatMember {
|
||||
// Stores the last known frequency per band (Context History)
|
||||
private final Map<Band, ActiveFrequencyInfo> knownActiveBands = new ConcurrentHashMap<>();
|
||||
|
||||
// Stores the calculated bidirectional SSB tropo margin per band.
|
||||
// Values are calculated by the reachability backend and used only for UI sorting/filtering.
|
||||
private final Map<Band, Double> tropoSsbMarginDbByBand = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
// --- INNER CLASS FOR QRG HISTORY ---
|
||||
public class ActiveFrequencyInfo {
|
||||
@@ -117,37 +101,12 @@ public class ChatMember {
|
||||
this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the most recently inferred directional opportunity is
|
||||
* still valid.
|
||||
*
|
||||
* @return {@code true} until the five-minute validity period has expired
|
||||
*/
|
||||
public boolean isInAngleAndRange() {
|
||||
return isInAngleAndRangeAt(System.currentTimeMillis());
|
||||
return isInAngleAndRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Time-aware variant used by the public getter and by unit tests.
|
||||
*
|
||||
* @param nowEpochMs time against which the validity is checked
|
||||
* @return {@code true} while the stored validity timestamp is still in the future
|
||||
*/
|
||||
boolean isInAngleAndRangeAt(long nowEpochMs) {
|
||||
return directionOpportunityValidUntilEpochMs > nowEpochMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new five-minute validity period or removes the current
|
||||
* directional opportunity immediately.
|
||||
*
|
||||
* @param inAngleAndRange {@code true} for a newly detected opportunity;
|
||||
* {@code false} to clear it
|
||||
*/
|
||||
public void setInAngleAndRange(boolean inAngleAndRange) {
|
||||
directionOpportunityValidUntilEpochMs = inAngleAndRange
|
||||
? System.currentTimeMillis() + DIRECTION_OPPORTUNITY_VALIDITY_MILLIS
|
||||
: 0L;
|
||||
isInAngleAndRange = inAngleAndRange;
|
||||
}
|
||||
|
||||
public AirPlaneReflectionInfo getAirPlaneReflectInfo() {
|
||||
@@ -230,22 +189,6 @@ public class ChatMember {
|
||||
worked10G = worked10g;
|
||||
}
|
||||
|
||||
public boolean isQrv50() {
|
||||
return qrv50;
|
||||
}
|
||||
|
||||
public void setQrv50(boolean qrv50) {
|
||||
this.qrv50 = qrv50;
|
||||
}
|
||||
|
||||
public boolean isQrv70() {
|
||||
return qrv70;
|
||||
}
|
||||
|
||||
public void setQrv70(boolean qrv70) {
|
||||
this.qrv70 = qrv70;
|
||||
}
|
||||
|
||||
public boolean isQrv144() {
|
||||
return qrv144;
|
||||
}
|
||||
@@ -518,7 +461,6 @@ public class ChatMember {
|
||||
this.frequency = frequency;
|
||||
}
|
||||
|
||||
|
||||
public long getActivityTimeLastInEpoch() {
|
||||
return activityTimeLastInEpoch;
|
||||
}
|
||||
@@ -596,40 +538,6 @@ 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
|
||||
@@ -660,8 +568,6 @@ public class ChatMember {
|
||||
public void resetQRVInformationAtAllBands() {
|
||||
|
||||
this.setQrvAny(true);
|
||||
this.setQrv50(true);
|
||||
this.setQrv70(true);
|
||||
this.setQrv144(true);
|
||||
this.setQrv432(true);
|
||||
this.setQrv1240(true);
|
||||
@@ -714,108 +620,6 @@ public class ChatMember {
|
||||
return knownActiveBands;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the calculated bidirectional SSB tropo margin for one band.
|
||||
*
|
||||
* <p>The value is deliberately stored in ChatMember because the station table
|
||||
* can then sort and filter directly without triggering a new RF calculation.
|
||||
* The actual calculation stays outside ChatMember.</p>
|
||||
*
|
||||
* @param band band for which the margin was calculated
|
||||
* @param marginDb bidirectional SSB margin in dB; NaN marks an attempted but failed analysis
|
||||
*/
|
||||
public void setTropoSsbMarginDb(Band band, double marginDb) {
|
||||
if (band == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tropoSsbMarginDbByBand.put(band, marginDb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a tropo analysis result already exists for the given band.
|
||||
*
|
||||
* <p>A stored NaN also counts as an existing result because it means the analysis
|
||||
* was attempted and failed. This prevents endless retry loops when a terrain
|
||||
* provider is temporarily unavailable.</p>
|
||||
*
|
||||
* @param band band to check
|
||||
* @return true if a value or NaN marker is stored
|
||||
*/
|
||||
public boolean hasTropoSsbMarginDb(Band band) {
|
||||
return band != null && this.tropoSsbMarginDbByBand.containsKey(band);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Returns true only when a finite, usable tropo SSB margin is stored for the band.
|
||||
*
|
||||
* <p>This is intentionally different from {@link #hasTropoSsbMarginDb(Band)}:
|
||||
* a stored NaN means that an analysis was attempted but did not produce a usable
|
||||
* link budget. Such failed values should not permanently block later retries.</p>
|
||||
*
|
||||
* @param band band to check
|
||||
* @return true if a finite SSB margin exists
|
||||
*/
|
||||
public boolean hasFiniteTropoSsbMarginDb(Band band) {
|
||||
if (band == null || !this.tropoSsbMarginDbByBand.containsKey(band)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Double storedValue = this.tropoSsbMarginDbByBand.get(band);
|
||||
return storedValue != null && Double.isFinite(storedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the calculated tropo SSB margin for one band.
|
||||
*
|
||||
* @param band band to read
|
||||
* @return OptionalDouble with the stored value, or empty if no calculation exists yet
|
||||
*/
|
||||
public OptionalDouble getTropoSsbMarginDb(Band band) {
|
||||
if (band == null || !this.tropoSsbMarginDbByBand.containsKey(band)) {
|
||||
return OptionalDouble.empty();
|
||||
}
|
||||
|
||||
Double storedValue = this.tropoSsbMarginDbByBand.get(band);
|
||||
if (storedValue == null) {
|
||||
return OptionalDouble.empty();
|
||||
}
|
||||
|
||||
return OptionalDouble.of(storedValue);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Formats the calculated tropo margin for direct table display.
|
||||
*
|
||||
* <p>The method distinguishes three states:
|
||||
* <ul>
|
||||
* <li>{@code ... @144}: analysis has not run yet</li>
|
||||
* <li>{@code ? @144}: analysis ran, but no usable link budget was produced</li>
|
||||
* <li>{@code +8.4 dB @144}: finite SSB margin exists</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param band selected/auto-resolved reachability band
|
||||
* @return display text such as "+8.4 dB @144", "? @144" or "... @144"
|
||||
*/
|
||||
public String formatTropoSsbMarginForBand(Band band) {
|
||||
String bandLabel = band == null ? "?" : band.getDisplayLabel();
|
||||
|
||||
if (band == null || !this.tropoSsbMarginDbByBand.containsKey(band)) {
|
||||
return "... @" + bandLabel;
|
||||
}
|
||||
|
||||
Double storedValue = this.tropoSsbMarginDbByBand.get(band);
|
||||
|
||||
if (storedValue == null || !Double.isFinite(storedValue)) {
|
||||
return "? @" + bandLabel;
|
||||
}
|
||||
|
||||
return String.format(Locale.US, "%+.1f dB @%s", storedValue, bandLabel);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If a sked fails and the user tells this to the client, this counter will be increased to give the station a
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class ChatMemberDirectionOpportunityTest {
|
||||
|
||||
@Test
|
||||
void directionalOpportunityExpiresAfterConfiguredValidityPeriod() {
|
||||
ChatMember member = new ChatMember();
|
||||
|
||||
member.setInAngleAndRange(true);
|
||||
long afterActivationEpochMs = System.currentTimeMillis();
|
||||
|
||||
assertTrue(member.isInAngleAndRangeAt(afterActivationEpochMs));
|
||||
assertFalse(member.isInAngleAndRangeAt(
|
||||
afterActivationEpochMs
|
||||
+ ChatMember.DIRECTION_OPPORTUNITY_VALIDITY_MILLIS
|
||||
+ 1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void directionalOpportunityCanBeClearedImmediately() {
|
||||
ChatMember member = new ChatMember();
|
||||
|
||||
member.setInAngleAndRange(true);
|
||||
assertTrue(member.isInAngleAndRange());
|
||||
|
||||
member.setInAngleAndRange(false);
|
||||
assertFalse(member.isInAngleAndRange());
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class ChatPreferences {
|
||||
* Reading must stay backwards compatible: missing/unknown tags should fall back to defaults.
|
||||
*/
|
||||
// private static final int CONFIG_VERSION = 2;
|
||||
public static final int CONFIG_VERSION = 5;
|
||||
public static final int CONFIG_VERSION = 3;
|
||||
|
||||
// Prefer writing tag names that mirror variable names (human readable). Keep legacy tags for compatibility.
|
||||
private static final String TAG_CONFIG_VERSION = "configVersion";
|
||||
@@ -159,8 +159,6 @@ public class ChatPreferences {
|
||||
int stn_on4kstServersPort = 23001;
|
||||
|
||||
boolean stn_pstRotatorEnabled = false;
|
||||
String stn_pstRotatorHost = "127.0.0.1";
|
||||
int stn_pstRotatorPort = 12000;
|
||||
|
||||
boolean stn_loginAFKState = false; //always start as here
|
||||
String stn_loginCallSign = "do5amf";
|
||||
@@ -175,30 +173,11 @@ public class ChatPreferences {
|
||||
double stn_maxQRBDefault = 900;
|
||||
double stn_qtfDefault = 135;
|
||||
|
||||
double stn_pathAnalysisOwnAntennaHeightMeters = 10.0;
|
||||
double stn_pathAnalysisDefaultTargetAntennaHeightMeters = 10.0;
|
||||
String stn_pathAnalysisDemRootDirectory = "";
|
||||
String stn_pathAnalysisDemDatasetId = "copernicus_glo_30";
|
||||
double stn_pathAnalysisOwnTxPowerWatts = 750.0;
|
||||
double stn_pathAnalysisOwnAntennaGainDbi = 8.0;
|
||||
double stn_pathAnalysisDefaultTargetTxPowerWatts = 100.0;
|
||||
double stn_pathAnalysisDefaultTargetAntennaGainDbi = 8.0;
|
||||
|
||||
double stn_pathAnalysisVhfFeederLossPerStationDb = 2.0;
|
||||
double stn_pathAnalysisFeederLossIncreaseDbPer200MHz = 2.0;
|
||||
double stn_pathAnalysisMaxEstimatedFeederLossPerStationDb = 20.0;
|
||||
|
||||
double stn_pathAnalysisRequiredSsbSignalDbm = -126.0;
|
||||
double stn_pathAnalysisRequiredCwSignalDbm = -132.0;
|
||||
double stn_pathAnalysisContestMarginDb = 6.0;
|
||||
|
||||
ChatCategory loginChatCategoryMain = new ChatCategory(2);
|
||||
ChatCategory loginChatCategorySecond = new ChatCategory(3);
|
||||
boolean loginToSecondChatEnabled;
|
||||
DoubleProperty actualQTF = new SimpleDoubleProperty(360); // will be updated by user at runtime!
|
||||
|
||||
boolean stn_bandActive50;
|
||||
boolean stn_bandActive70;
|
||||
boolean stn_bandActive144;
|
||||
boolean stn_bandActive432;
|
||||
boolean stn_bandActive1240;
|
||||
@@ -224,7 +203,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"; // Supported values: SSB or CW
|
||||
String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO
|
||||
boolean logsynch_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet
|
||||
boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet
|
||||
|
||||
@@ -242,12 +221,9 @@ public class ChatPreferences {
|
||||
/**
|
||||
* AirScout prefs
|
||||
*/
|
||||
boolean AirScout_asUDPListenerEnabled = true;
|
||||
String AirScout_asServerNameString = "AS";
|
||||
String AirScout_asClientNameString = "KST";
|
||||
boolean AirScout_autoBandSelectionEnabled = true;
|
||||
String AirScout_asBandString = "1440000";
|
||||
int AirScout_asCommunicationPort = 9872;
|
||||
boolean AirScout_asUDPListenerEnabled;
|
||||
String AirScout_asServerNameString, AirScout_asClientNameString, AirScout_asBandString;
|
||||
int AirScout_asCommunicationPort;
|
||||
|
||||
/**
|
||||
* Notification prefs
|
||||
@@ -270,9 +246,6 @@ public class ChatPreferences {
|
||||
boolean notify_DXClusterServerTriggerBearing;
|
||||
boolean notify_DXClusterServerTriggerOnQRGDetect;
|
||||
|
||||
ObservableList<String>
|
||||
lstNotify_QSOSniffer_sniffedCallSignList =
|
||||
FXCollections.observableArrayList();
|
||||
// ObservableList<String> lstNotify_QSOSniffer_sniffedCallSignList = FXCollections.observableArrayList();
|
||||
ObservableList<String> lstNotify_QSOSniffer_sniffedWordsList = FXCollections.observableArrayList();
|
||||
ObservableList<String> lstNotify_QSOSniffer_sniffedPrefixLocList = FXCollections.observableArrayList();
|
||||
@@ -317,8 +290,8 @@ public class ChatPreferences {
|
||||
String messageHandling_unworkedStnRequesterBeaconsText;
|
||||
String messageHandling_beaconUnworkedstationsPrefix;
|
||||
|
||||
String messageHandling_autoAnswerTextMainCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENT_VERSION;
|
||||
String messageHandling_autoAnswerTextSecondCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENT_VERSION;
|
||||
String messageHandling_autoAnswerTextMainCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
|
||||
String messageHandling_autoAnswerTextSecondCat = "Hi, sry I am not qrv, just testing new features of KST4Contest " + ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
|
||||
|
||||
boolean messageHandling_autoAnswerEnabled = false;
|
||||
boolean messageHandling_autoAnswerEnabledSecondCat = false;
|
||||
@@ -335,12 +308,6 @@ public class ChatPreferences {
|
||||
boolean guiOptions_defaultFilterPmToMe;
|
||||
boolean guiOptions_defaultFilterPmToOther;
|
||||
boolean guiOptions_defaultFilterPublicMsgs;
|
||||
boolean guiOptions_showGrossFieldWorkedHintInBandColumns = true; // show "o" (grid square already worked on this band) in the band columns
|
||||
boolean guiOptions_showFreshCallHintInBandColumns = true; // show "a" (band available, call not worked on any band yet) instead of always "B+" in the band columns
|
||||
|
||||
private double[] GUIstationMapStageSceneSizeHW = new double[] { 1000, 800 };
|
||||
private double[] GUIstationMapStagePositionXY = new double[] { Double.NaN, Double.NaN };
|
||||
private boolean GUIstationMapPathAnalysisVisible = true;
|
||||
|
||||
|
||||
/*********************************************************************************
|
||||
@@ -404,42 +371,6 @@ public class ChatPreferences {
|
||||
this.MYQRGFirstCat.set(MYQRGFirstCat);
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisOwnAntennaHeightMeters() {
|
||||
return stn_pathAnalysisOwnAntennaHeightMeters;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisOwnAntennaHeightMeters(double stn_pathAnalysisOwnAntennaHeightMeters) {
|
||||
this.stn_pathAnalysisOwnAntennaHeightMeters = Math.max(0.0, stn_pathAnalysisOwnAntennaHeightMeters);
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisDefaultTargetAntennaHeightMeters() {
|
||||
return stn_pathAnalysisDefaultTargetAntennaHeightMeters;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisDefaultTargetAntennaHeightMeters(double stn_pathAnalysisDefaultTargetAntennaHeightMeters) {
|
||||
this.stn_pathAnalysisDefaultTargetAntennaHeightMeters = Math.max(0.0, stn_pathAnalysisDefaultTargetAntennaHeightMeters);
|
||||
}
|
||||
|
||||
public String getStn_pathAnalysisDemRootDirectory() {
|
||||
return stn_pathAnalysisDemRootDirectory;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisDemRootDirectory(String stn_pathAnalysisDemRootDirectory) {
|
||||
this.stn_pathAnalysisDemRootDirectory = stn_pathAnalysisDemRootDirectory == null
|
||||
? ""
|
||||
: stn_pathAnalysisDemRootDirectory.trim();
|
||||
}
|
||||
|
||||
public String getStn_pathAnalysisDemDatasetId() {
|
||||
return stn_pathAnalysisDemDatasetId;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisDemDatasetId(String stn_pathAnalysisDemDatasetId) {
|
||||
this.stn_pathAnalysisDemDatasetId = (stn_pathAnalysisDemDatasetId == null || stn_pathAnalysisDemDatasetId.isBlank())
|
||||
? "copernicus_glo_30"
|
||||
: stn_pathAnalysisDemDatasetId.trim().toLowerCase();
|
||||
}
|
||||
|
||||
public String getStn_loginNameSecondCat() {
|
||||
return stn_loginNameSecondCat;
|
||||
}
|
||||
@@ -464,11 +395,6 @@ public class ChatPreferences {
|
||||
this.stn_on4kstServersDns = stn_on4kstServersDns;
|
||||
}
|
||||
|
||||
public ObservableList<String>
|
||||
getLstNotify_QSOSniffer_sniffedCallSignList() {
|
||||
return lstNotify_QSOSniffer_sniffedCallSignList;
|
||||
}
|
||||
|
||||
public ObservableList<String> getLstNotify_QSOSniffer_sniffedWordsList() {
|
||||
return lstNotify_QSOSniffer_sniffedWordsList;
|
||||
}
|
||||
@@ -621,30 +547,6 @@ public class ChatPreferences {
|
||||
this.loginToSecondChatEnabled = loginToSecondChatEnabled;
|
||||
}
|
||||
|
||||
public double[] getGUIstationMapStageSceneSizeHW() {
|
||||
return GUIstationMapStageSceneSizeHW;
|
||||
}
|
||||
|
||||
public void setGUIstationMapStageSceneSizeHW(double[] GUIstationMapStageSceneSizeHW) {
|
||||
this.GUIstationMapStageSceneSizeHW = GUIstationMapStageSceneSizeHW;
|
||||
}
|
||||
|
||||
public double[] getGUIstationMapStagePositionXY() {
|
||||
return GUIstationMapStagePositionXY;
|
||||
}
|
||||
|
||||
public void setGUIstationMapStagePositionXY(double[] GUIstationMapStagePositionXY) {
|
||||
this.GUIstationMapStagePositionXY = GUIstationMapStagePositionXY;
|
||||
}
|
||||
|
||||
public boolean isGUIstationMapPathAnalysisVisible() {
|
||||
return GUIstationMapPathAnalysisVisible;
|
||||
}
|
||||
|
||||
public void setGUIstationMapPathAnalysisVisible(boolean GUIstationMapPathAnalysisVisible) {
|
||||
this.GUIstationMapPathAnalysisVisible = GUIstationMapPathAnalysisVisible;
|
||||
}
|
||||
|
||||
public boolean isGuiOptions_defaultFilterNothing() {
|
||||
return guiOptions_defaultFilterNothing;
|
||||
}
|
||||
@@ -661,22 +563,6 @@ public class ChatPreferences {
|
||||
this.guiOptions_defaultFilterPmToMe = guiOptions_defaultFilterPmToMe;
|
||||
}
|
||||
|
||||
public boolean isGuiOptions_showGrossFieldWorkedHintInBandColumns() {
|
||||
return guiOptions_showGrossFieldWorkedHintInBandColumns;
|
||||
}
|
||||
|
||||
public void setGuiOptions_showGrossFieldWorkedHintInBandColumns(boolean guiOptions_showGrossFieldWorkedHintInBandColumns) {
|
||||
this.guiOptions_showGrossFieldWorkedHintInBandColumns = guiOptions_showGrossFieldWorkedHintInBandColumns;
|
||||
}
|
||||
|
||||
public boolean isGuiOptions_showFreshCallHintInBandColumns() {
|
||||
return guiOptions_showFreshCallHintInBandColumns;
|
||||
}
|
||||
|
||||
public void setGuiOptions_showFreshCallHintInBandColumns(boolean guiOptions_showFreshCallHintInBandColumns) {
|
||||
this.guiOptions_showFreshCallHintInBandColumns = guiOptions_showFreshCallHintInBandColumns;
|
||||
}
|
||||
|
||||
public boolean isGuiOptions_defaultFilterPmToOther() {
|
||||
return guiOptions_defaultFilterPmToOther;
|
||||
}
|
||||
@@ -803,30 +689,6 @@ public class ChatPreferences {
|
||||
this.stn_pstRotatorEnabled = stn_pstRotatorEnabled;
|
||||
}
|
||||
|
||||
public String getStn_pstRotatorHost() {
|
||||
return stn_pstRotatorHost;
|
||||
}
|
||||
|
||||
public void setStn_pstRotatorHost(String stn_pstRotatorHost) {
|
||||
if (stn_pstRotatorHost == null || stn_pstRotatorHost.isBlank()) {
|
||||
this.stn_pstRotatorHost = "127.0.0.1";
|
||||
} else {
|
||||
this.stn_pstRotatorHost = stn_pstRotatorHost.trim();
|
||||
}
|
||||
}
|
||||
|
||||
public int getStn_pstRotatorPort() {
|
||||
return stn_pstRotatorPort;
|
||||
}
|
||||
|
||||
public void setStn_pstRotatorPort(int stn_pstRotatorPort) {
|
||||
if (stn_pstRotatorPort < 1 || stn_pstRotatorPort > 65535) {
|
||||
this.stn_pstRotatorPort = 12000;
|
||||
} else {
|
||||
this.stn_pstRotatorPort = stn_pstRotatorPort;
|
||||
}
|
||||
}
|
||||
|
||||
public SimpleStringProperty getNotify_optionalFrequencyPrefix() {
|
||||
return notify_optionalFrequencyPrefix;
|
||||
}
|
||||
@@ -839,16 +701,8 @@ public class ChatPreferences {
|
||||
return notify_dxclusterServerPort;
|
||||
}
|
||||
|
||||
public void setNotify_dxclusterServerPort(
|
||||
int notify_dxclusterServerPort
|
||||
) {
|
||||
if (notify_dxclusterServerPort < 1
|
||||
|| notify_dxclusterServerPort > 65535) {
|
||||
this.notify_dxclusterServerPort = 8000;
|
||||
} else {
|
||||
this.notify_dxclusterServerPort =
|
||||
notify_dxclusterServerPort;
|
||||
}
|
||||
public void setNotify_dxclusterServerPort(int notify_dxclusterServerPort) {
|
||||
this.notify_dxclusterServerPort = notify_dxclusterServerPort;
|
||||
}
|
||||
|
||||
public double[] getGUIscn_ChatwindowMainSceneSizeHW() {
|
||||
@@ -969,110 +823,35 @@ public class ChatPreferences {
|
||||
return stn_loginCallSignRaw;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes an AirScout routing identifier.
|
||||
*
|
||||
* AirScout encloses the identifiers in quotation marks. Empty identifiers,
|
||||
* quotation marks and line breaks would therefore produce an invalid protocol
|
||||
* message and are replaced with the supplied default value.
|
||||
*
|
||||
* @param identifier configured identifier
|
||||
* @param defaultIdentifier fallback value
|
||||
* @return normalized identifier
|
||||
*/
|
||||
private String normalizeAirScoutIdentifier(
|
||||
String identifier,
|
||||
String defaultIdentifier
|
||||
) {
|
||||
if (identifier == null) {
|
||||
return defaultIdentifier;
|
||||
}
|
||||
|
||||
String normalizedIdentifier = identifier.trim();
|
||||
|
||||
if (normalizedIdentifier.isEmpty()
|
||||
|| normalizedIdentifier.contains("\"")
|
||||
|| normalizedIdentifier.contains("\r")
|
||||
|| normalizedIdentifier.contains("\n")) {
|
||||
return defaultIdentifier;
|
||||
}
|
||||
|
||||
return normalizedIdentifier;
|
||||
}
|
||||
|
||||
public String getAirScout_asBandString() {
|
||||
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";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
long parsedBandValue = Long.parseLong(
|
||||
airScout_asBandString.trim()
|
||||
);
|
||||
|
||||
AirScout_asBandString = parsedBandValue > 0
|
||||
? Long.toString(parsedBandValue)
|
||||
: "1440000";
|
||||
} catch (NumberFormatException exception) {
|
||||
AirScout_asBandString = "1440000";
|
||||
}
|
||||
AirScout_asBandString = airScout_asBandString;
|
||||
}
|
||||
|
||||
public String getAirScout_asServerNameString() {
|
||||
return AirScout_asServerNameString;
|
||||
}
|
||||
|
||||
public void setAirScout_asServerNameString(
|
||||
String airScout_asServerNameString
|
||||
) {
|
||||
AirScout_asServerNameString = normalizeAirScoutIdentifier(
|
||||
airScout_asServerNameString,
|
||||
"AS"
|
||||
);
|
||||
public void setAirScout_asServerNameString(String airScout_asServerNameString) {
|
||||
AirScout_asServerNameString = airScout_asServerNameString;
|
||||
}
|
||||
|
||||
public String getAirScout_asClientNameString() {
|
||||
return AirScout_asClientNameString;
|
||||
}
|
||||
|
||||
public void setAirScout_asClientNameString(
|
||||
String airScout_asClientNameString
|
||||
) {
|
||||
AirScout_asClientNameString = normalizeAirScoutIdentifier(
|
||||
airScout_asClientNameString,
|
||||
"KST"
|
||||
);
|
||||
public void setAirScout_asClientNameString(String airScout_asClientNameString) {
|
||||
AirScout_asClientNameString = airScout_asClientNameString;
|
||||
}
|
||||
|
||||
public int getAirScout_asCommunicationPort() {
|
||||
return AirScout_asCommunicationPort;
|
||||
}
|
||||
|
||||
public void setAirScout_asCommunicationPort(
|
||||
int airScout_asCommunicationPort
|
||||
) {
|
||||
if (airScout_asCommunicationPort < 1
|
||||
|| airScout_asCommunicationPort > 65535) {
|
||||
AirScout_asCommunicationPort = 9872;
|
||||
return;
|
||||
}
|
||||
|
||||
public void setAirScout_asCommunicationPort(int airScout_asCommunicationPort) {
|
||||
AirScout_asCommunicationPort = airScout_asCommunicationPort;
|
||||
}
|
||||
|
||||
@@ -1080,13 +859,10 @@ public class ChatPreferences {
|
||||
return AirScout_asUDPListenerEnabled;
|
||||
}
|
||||
|
||||
public void setAirScout_asUDPListenerEnabled(
|
||||
boolean airScout_asUDPListenerEnabled
|
||||
) {
|
||||
public void setAirScout_asUDPListenerEnabled(boolean airScout_asUDPListenerEnabled) {
|
||||
AirScout_asUDPListenerEnabled = airScout_asUDPListenerEnabled;
|
||||
}
|
||||
|
||||
|
||||
public String getChatState() {
|
||||
return chatState;
|
||||
}
|
||||
@@ -1469,71 +1245,6 @@ public class ChatPreferences {
|
||||
stn_qtfDefault.setTextContent(this.stn_qtfDefault+"");
|
||||
station.appendChild(stn_qtfDefault);
|
||||
|
||||
Element stn_pathAnalysisOwnAntennaHeightMeters = doc.createElement("stn_pathAnalysisOwnAntennaHeightMeters");
|
||||
stn_pathAnalysisOwnAntennaHeightMeters.setTextContent(this.stn_pathAnalysisOwnAntennaHeightMeters + "");
|
||||
station.appendChild(stn_pathAnalysisOwnAntennaHeightMeters);
|
||||
|
||||
Element stn_pathAnalysisDefaultTargetAntennaHeightMeters = doc.createElement("stn_pathAnalysisDefaultTargetAntennaHeightMeters");
|
||||
stn_pathAnalysisDefaultTargetAntennaHeightMeters.setTextContent(this.stn_pathAnalysisDefaultTargetAntennaHeightMeters + "");
|
||||
station.appendChild(stn_pathAnalysisDefaultTargetAntennaHeightMeters);
|
||||
|
||||
Element stn_pathAnalysisDemRootDirectory = doc.createElement("stn_pathAnalysisDemRootDirectory");
|
||||
stn_pathAnalysisDemRootDirectory.setTextContent(this.stn_pathAnalysisDemRootDirectory);
|
||||
station.appendChild(stn_pathAnalysisDemRootDirectory);
|
||||
|
||||
Element stn_pathAnalysisDemDatasetId = doc.createElement("stn_pathAnalysisDemDatasetId");
|
||||
stn_pathAnalysisDemDatasetId.setTextContent(this.stn_pathAnalysisDemDatasetId);
|
||||
station.appendChild(stn_pathAnalysisDemDatasetId);
|
||||
Element stn_pathAnalysisOwnTxPowerWatts = doc.createElement("stn_pathAnalysisOwnTxPowerWatts");
|
||||
stn_pathAnalysisOwnTxPowerWatts.setTextContent(this.stn_pathAnalysisOwnTxPowerWatts + "");
|
||||
station.appendChild(stn_pathAnalysisOwnTxPowerWatts);
|
||||
|
||||
Element stn_pathAnalysisOwnAntennaGainDbi = doc.createElement("stn_pathAnalysisOwnAntennaGainDbi");
|
||||
stn_pathAnalysisOwnAntennaGainDbi.setTextContent(this.stn_pathAnalysisOwnAntennaGainDbi + "");
|
||||
station.appendChild(stn_pathAnalysisOwnAntennaGainDbi);
|
||||
|
||||
Element stn_pathAnalysisDefaultTargetTxPowerWatts = doc.createElement("stn_pathAnalysisDefaultTargetTxPowerWatts");
|
||||
stn_pathAnalysisDefaultTargetTxPowerWatts.setTextContent(this.stn_pathAnalysisDefaultTargetTxPowerWatts + "");
|
||||
station.appendChild(stn_pathAnalysisDefaultTargetTxPowerWatts);
|
||||
|
||||
Element stn_pathAnalysisDefaultTargetAntennaGainDbi = doc.createElement("stn_pathAnalysisDefaultTargetAntennaGainDbi");
|
||||
stn_pathAnalysisDefaultTargetAntennaGainDbi.setTextContent(this.stn_pathAnalysisDefaultTargetAntennaGainDbi + "");
|
||||
station.appendChild(stn_pathAnalysisDefaultTargetAntennaGainDbi);
|
||||
|
||||
Element stn_pathAnalysisVhfFeederLossPerStationDb = doc.createElement("stn_pathAnalysisVhfFeederLossPerStationDb");
|
||||
stn_pathAnalysisVhfFeederLossPerStationDb.setTextContent(this.stn_pathAnalysisVhfFeederLossPerStationDb + "");
|
||||
station.appendChild(stn_pathAnalysisVhfFeederLossPerStationDb);
|
||||
|
||||
Element stn_pathAnalysisFeederLossIncreaseDbPer200MHz = doc.createElement("stn_pathAnalysisFeederLossIncreaseDbPer200MHz");
|
||||
stn_pathAnalysisFeederLossIncreaseDbPer200MHz.setTextContent(this.stn_pathAnalysisFeederLossIncreaseDbPer200MHz + "");
|
||||
station.appendChild(stn_pathAnalysisFeederLossIncreaseDbPer200MHz);
|
||||
|
||||
Element stn_pathAnalysisMaxEstimatedFeederLossPerStationDb = doc.createElement("stn_pathAnalysisMaxEstimatedFeederLossPerStationDb");
|
||||
stn_pathAnalysisMaxEstimatedFeederLossPerStationDb.setTextContent(this.stn_pathAnalysisMaxEstimatedFeederLossPerStationDb + "");
|
||||
station.appendChild(stn_pathAnalysisMaxEstimatedFeederLossPerStationDb);
|
||||
|
||||
Element stn_pathAnalysisRequiredSsbSignalDbm = doc.createElement("stn_pathAnalysisRequiredSsbSignalDbm");
|
||||
stn_pathAnalysisRequiredSsbSignalDbm.setTextContent(this.stn_pathAnalysisRequiredSsbSignalDbm + "");
|
||||
station.appendChild(stn_pathAnalysisRequiredSsbSignalDbm);
|
||||
|
||||
Element stn_pathAnalysisRequiredCwSignalDbm = doc.createElement("stn_pathAnalysisRequiredCwSignalDbm");
|
||||
stn_pathAnalysisRequiredCwSignalDbm.setTextContent(this.stn_pathAnalysisRequiredCwSignalDbm + "");
|
||||
station.appendChild(stn_pathAnalysisRequiredCwSignalDbm);
|
||||
|
||||
Element stn_pathAnalysisContestMarginDb = doc.createElement("stn_pathAnalysisContestMarginDb");
|
||||
stn_pathAnalysisContestMarginDb.setTextContent(this.stn_pathAnalysisContestMarginDb + "");
|
||||
station.appendChild(stn_pathAnalysisContestMarginDb);
|
||||
|
||||
|
||||
|
||||
Element stn_bandActive50 = doc.createElement("stn_bandActive50");
|
||||
stn_bandActive50.setTextContent(this.stn_bandActive50+"");
|
||||
station.appendChild(stn_bandActive50);
|
||||
|
||||
Element stn_bandActive70 = doc.createElement("stn_bandActive70");
|
||||
stn_bandActive70.setTextContent(this.stn_bandActive70+"");
|
||||
station.appendChild(stn_bandActive70);
|
||||
|
||||
Element stn_bandActive144 = doc.createElement("stn_bandActive144");
|
||||
stn_bandActive144.setTextContent(this.stn_bandActive144+"");
|
||||
station.appendChild(stn_bandActive144);
|
||||
@@ -1578,13 +1289,6 @@ public class ChatPreferences {
|
||||
stn_pstRotatorEnabled.setTextContent(this.stn_pstRotatorEnabled + "");
|
||||
station.appendChild(stn_pstRotatorEnabled);
|
||||
|
||||
Element stn_pstRotatorHost = doc.createElement("stn_pstRotatorHost");
|
||||
stn_pstRotatorHost.setTextContent(this.stn_pstRotatorHost);
|
||||
station.appendChild(stn_pstRotatorHost);
|
||||
|
||||
Element stn_pstRotatorPort = doc.createElement("stn_pstRotatorPort");
|
||||
stn_pstRotatorPort.setTextContent(Integer.toString(this.stn_pstRotatorPort));
|
||||
station.appendChild(stn_pstRotatorPort);
|
||||
|
||||
|
||||
/**
|
||||
@@ -1706,13 +1410,6 @@ 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);
|
||||
@@ -1808,18 +1505,6 @@ public class ChatPreferences {
|
||||
snifferWords.appendChild(temp);
|
||||
}
|
||||
|
||||
Element snifferCallSigns =
|
||||
doc.createElement("snifferCallSigns");
|
||||
rootElement.appendChild(snifferCallSigns);
|
||||
|
||||
for (String callSign
|
||||
: lstNotify_QSOSniffer_sniffedCallSignList) {
|
||||
Element temp = doc.createElement("callSign");
|
||||
temp.setTextContent(callSign);
|
||||
snifferCallSigns.appendChild(temp);
|
||||
}
|
||||
|
||||
|
||||
Element snifferPrefixes = doc.createElement("snifferPrefixes");
|
||||
rootElement.appendChild(snifferPrefixes);
|
||||
|
||||
@@ -1990,14 +1675,6 @@ public class ChatPreferences {
|
||||
guiOptions_defaultFilterPublicMsgs.setTextContent(this.isGuiOptions_defaultFilterPublicMsgs()+"");
|
||||
guiSaveableOptions.appendChild(guiOptions_defaultFilterPublicMsgs);
|
||||
|
||||
Element guiOptions_showGrossFieldWorkedHintInBandColumns = doc.createElement("guiOptions_showGrossFieldWorkedHintInBandColumns");
|
||||
guiOptions_showGrossFieldWorkedHintInBandColumns.setTextContent(this.isGuiOptions_showGrossFieldWorkedHintInBandColumns()+"");
|
||||
guiSaveableOptions.appendChild(guiOptions_showGrossFieldWorkedHintInBandColumns);
|
||||
|
||||
Element guiOptions_showFreshCallHintInBandColumns = doc.createElement("guiOptions_showFreshCallHintInBandColumns");
|
||||
guiOptions_showFreshCallHintInBandColumns.setTextContent(this.isGuiOptions_showFreshCallHintInBandColumns()+"");
|
||||
guiSaveableOptions.appendChild(guiOptions_showFreshCallHintInBandColumns);
|
||||
|
||||
Element guiOptions_darkModeActive = doc.createElement("guiOptions_darkModeActive");
|
||||
guiOptions_darkModeActive.setTextContent(this.GUI_darkModeActive + "");
|
||||
guiSaveableOptions.appendChild(guiOptions_darkModeActive);
|
||||
@@ -2063,24 +1740,6 @@ public class ChatPreferences {
|
||||
GUIpnl_directedMSGWin_dividerpositionDefault.setTextContent(doubleArrayToCSVString(getGUIpnl_directedMSGWin_dividerpositionDefault()));
|
||||
guiOptions.appendChild(GUIpnl_directedMSGWin_dividerpositionDefault);
|
||||
|
||||
Element GUIstationMapStageSceneSizeHW = doc.createElement("GUIstationMapStageSceneSizeHW");
|
||||
GUIstationMapStageSceneSizeHW.setTextContent(
|
||||
this.getGUIstationMapStageSceneSizeHW()[0] + ";" + this.getGUIstationMapStageSceneSizeHW()[1]
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapStageSceneSizeHW);
|
||||
|
||||
Element GUIstationMapStagePositionXY = doc.createElement("GUIstationMapStagePositionXY");
|
||||
GUIstationMapStagePositionXY.setTextContent(
|
||||
this.getGUIstationMapStagePositionXY()[0] + ";" + this.getGUIstationMapStagePositionXY()[1]
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapStagePositionXY);
|
||||
|
||||
Element GUIstationMapPathAnalysisVisible = doc.createElement("GUIstationMapPathAnalysisVisible");
|
||||
GUIstationMapPathAnalysisVisible.setTextContent(
|
||||
String.valueOf(this.isGUIstationMapPathAnalysisVisible())
|
||||
);
|
||||
guiOptions.appendChild(GUIstationMapPathAnalysisVisible);
|
||||
|
||||
/****************************************************************************************
|
||||
****************************** now write this XML! *************************************
|
||||
****************************************************************************************/
|
||||
@@ -2197,93 +1856,7 @@ public class ChatPreferences {
|
||||
stn_maxQRBDefault = getDouble(stationEl, stn_maxQRBDefault, "stn_maxQRBDefault");
|
||||
stn_qtfDefault = getDouble(stationEl, stn_qtfDefault, "stn_qtfDefault");
|
||||
|
||||
stn_pathAnalysisOwnAntennaHeightMeters = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisOwnAntennaHeightMeters,
|
||||
"stn_pathAnalysisOwnAntennaHeightMeters"
|
||||
);
|
||||
|
||||
stn_pathAnalysisDefaultTargetAntennaHeightMeters = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisDefaultTargetAntennaHeightMeters,
|
||||
"stn_pathAnalysisDefaultTargetAntennaHeightMeters"
|
||||
);
|
||||
|
||||
stn_pathAnalysisDemRootDirectory = getText(
|
||||
stationEl,
|
||||
stn_pathAnalysisDemRootDirectory,
|
||||
"stn_pathAnalysisDemRootDirectory"
|
||||
);
|
||||
|
||||
stn_pathAnalysisDemDatasetId = getText(
|
||||
stationEl,
|
||||
stn_pathAnalysisDemDatasetId,
|
||||
"stn_pathAnalysisDemDatasetId"
|
||||
);
|
||||
|
||||
stn_pathAnalysisOwnTxPowerWatts = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisOwnTxPowerWatts,
|
||||
"stn_pathAnalysisOwnTxPowerWatts"
|
||||
);
|
||||
|
||||
stn_pathAnalysisOwnAntennaGainDbi = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisOwnAntennaGainDbi,
|
||||
"stn_pathAnalysisOwnAntennaGainDbi"
|
||||
);
|
||||
|
||||
stn_pathAnalysisDefaultTargetTxPowerWatts = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisDefaultTargetTxPowerWatts,
|
||||
"stn_pathAnalysisDefaultTargetTxPowerWatts"
|
||||
);
|
||||
|
||||
stn_pathAnalysisDefaultTargetAntennaGainDbi = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisDefaultTargetAntennaGainDbi,
|
||||
"stn_pathAnalysisDefaultTargetAntennaGainDbi"
|
||||
);
|
||||
|
||||
stn_pathAnalysisVhfFeederLossPerStationDb = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisVhfFeederLossPerStationDb,
|
||||
"stn_pathAnalysisVhfFeederLossPerStationDb"
|
||||
);
|
||||
|
||||
stn_pathAnalysisFeederLossIncreaseDbPer200MHz = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisFeederLossIncreaseDbPer200MHz,
|
||||
"stn_pathAnalysisFeederLossIncreaseDbPer200MHz"
|
||||
);
|
||||
|
||||
stn_pathAnalysisMaxEstimatedFeederLossPerStationDb = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisMaxEstimatedFeederLossPerStationDb,
|
||||
"stn_pathAnalysisMaxEstimatedFeederLossPerStationDb"
|
||||
);
|
||||
|
||||
stn_pathAnalysisRequiredSsbSignalDbm = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisRequiredSsbSignalDbm,
|
||||
"stn_pathAnalysisRequiredSsbSignalDbm"
|
||||
);
|
||||
|
||||
stn_pathAnalysisRequiredCwSignalDbm = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisRequiredCwSignalDbm,
|
||||
"stn_pathAnalysisRequiredCwSignalDbm"
|
||||
);
|
||||
|
||||
stn_pathAnalysisContestMarginDb = getDouble(
|
||||
stationEl,
|
||||
stn_pathAnalysisContestMarginDb,
|
||||
"stn_pathAnalysisContestMarginDb"
|
||||
);
|
||||
|
||||
// Band activity flags (introduced later; if missing -> keep defaults)
|
||||
stn_bandActive50 = getBoolean(stationEl, stn_bandActive50, "stn_bandActive50");
|
||||
stn_bandActive70 = getBoolean(stationEl, stn_bandActive70, "stn_bandActive70");
|
||||
stn_bandActive144 = getBoolean(stationEl, stn_bandActive144, "stn_bandActive144");
|
||||
stn_bandActive432 = getBoolean(stationEl, stn_bandActive432, "stn_bandActive432");
|
||||
stn_bandActive1240 = getBoolean(stationEl, stn_bandActive1240, "stn_bandActive1240");
|
||||
@@ -2294,22 +1867,6 @@ public class ChatPreferences {
|
||||
|
||||
stn_pstRotatorEnabled = getBoolean(stationEl, stn_pstRotatorEnabled, "stn_pstRotatorEnabled");
|
||||
|
||||
setStn_pstRotatorHost(
|
||||
getText(
|
||||
stationEl,
|
||||
stn_pstRotatorHost,
|
||||
"stn_pstRotatorHost"
|
||||
)
|
||||
);
|
||||
|
||||
setStn_pstRotatorPort(
|
||||
getInt(
|
||||
stationEl,
|
||||
stn_pstRotatorPort,
|
||||
"stn_pstRotatorPort"
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2434,13 +1991,7 @@ public class ChatPreferences {
|
||||
notify_dxClusterServerEnabled = getBoolean(notificationsEl, notify_dxClusterServerEnabled, "notify_dxClusterServerEnabled");
|
||||
notify_DXClusterServerTriggerBearing = getBoolean(notificationsEl, notify_DXClusterServerTriggerBearing, "notify_DXClusterServerTriggerBearing");
|
||||
notify_DXClusterServerTriggerOnQRGDetect = getBoolean(notificationsEl, notify_DXClusterServerTriggerOnQRGDetect, "notify_DXClusterServerTriggerOnQRGDetect");
|
||||
setNotify_dxclusterServerPort(
|
||||
getInt(
|
||||
notificationsEl,
|
||||
notify_dxclusterServerPort,
|
||||
"notify_dxclusterServerPort"
|
||||
)
|
||||
);
|
||||
notify_dxclusterServerPort = getInt(notificationsEl, notify_dxclusterServerPort, "notify_dxclusterServerPort");
|
||||
|
||||
String spotter = getText(notificationsEl, null, "notify_DXCSrv_SpottersCallSign");
|
||||
if (spotter != null) {
|
||||
@@ -2456,11 +2007,8 @@ public class ChatPreferences {
|
||||
notify_noReplyPenaltyMinutes = noReply;
|
||||
}
|
||||
|
||||
Integer momentum = getInt(
|
||||
notificationsEl,
|
||||
notify_momentumWindowSeconds,
|
||||
"notify_momentumWindowSeconds"
|
||||
); if (momentum != null) {
|
||||
Integer momentum = getInt(notificationsEl, 666, "notify_momentumWindowSeconds");
|
||||
if (momentum != null) {
|
||||
notify_momentumWindowSeconds = momentum;
|
||||
}
|
||||
|
||||
@@ -2494,68 +2042,15 @@ public class ChatPreferences {
|
||||
|
||||
Element airScoutEl = getFirstElement(doc, "AirScoutQuerier");
|
||||
if (airScoutEl != null) {
|
||||
setAirScout_asUDPListenerEnabled(
|
||||
getBoolean(
|
||||
airScoutEl,
|
||||
AirScout_asUDPListenerEnabled,
|
||||
"asQry_airScoutCommunicationEnabled"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asServerNameString(
|
||||
getText(
|
||||
airScoutEl,
|
||||
AirScout_asServerNameString,
|
||||
"asQry_airScoutServerName"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asClientNameString(
|
||||
getText(
|
||||
airScoutEl,
|
||||
AirScout_asClientNameString,
|
||||
"asQry_airScoutClientName"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asCommunicationPort(
|
||||
getInt(
|
||||
airScoutEl,
|
||||
AirScout_asCommunicationPort,
|
||||
"asQry_airScoutUDPPort"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_autoBandSelectionEnabled(
|
||||
getBoolean(
|
||||
airScoutEl,
|
||||
AirScout_autoBandSelectionEnabled,
|
||||
"asQry_airScoutAutoBandSelectionEnabled"
|
||||
)
|
||||
);
|
||||
|
||||
setAirScout_asBandString(
|
||||
getText(
|
||||
airScoutEl,
|
||||
AirScout_asBandString,
|
||||
"asQry_airScoutBandValue"
|
||||
)
|
||||
);
|
||||
AirScout_asUDPListenerEnabled = getBoolean(airScoutEl, AirScout_asUDPListenerEnabled, "asQry_airScoutCommunicationEnabled");
|
||||
AirScout_asServerNameString = getText(airScoutEl, AirScout_asServerNameString, "asQry_airScoutServerName");
|
||||
AirScout_asClientNameString = getText(airScoutEl, AirScout_asClientNameString, "asQry_airScoutClientName");
|
||||
AirScout_asCommunicationPort = getInt(airScoutEl, AirScout_asCommunicationPort, "asQry_airScoutUDPPort");
|
||||
AirScout_asBandString = getText(airScoutEl, AirScout_asBandString, "asQry_airScoutBandValue");
|
||||
|
||||
System.out.println(
|
||||
"[ChatPreferences, info]: AirScout integration enabled="
|
||||
+ AirScout_asUDPListenerEnabled
|
||||
+ ", server identifier="
|
||||
+ AirScout_asServerNameString
|
||||
+ ", client identifier="
|
||||
+ AirScout_asClientNameString
|
||||
+ ", port="
|
||||
+ AirScout_asCommunicationPort
|
||||
+ ", automatic band selection="
|
||||
+ AirScout_autoBandSelectionEnabled
|
||||
+ ", band="
|
||||
+ AirScout_asBandString
|
||||
);
|
||||
"[ChatPreferences, info]: AirScout querier enabled=" + AirScout_asUDPListenerEnabled
|
||||
+ ", band=" + AirScout_asBandString);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2605,45 +2100,6 @@ public class ChatPreferences {
|
||||
/**
|
||||
* Case QSO-sniffer lists (added later; older configs won't have them)
|
||||
*/
|
||||
|
||||
list = doc.getElementsByTagName("snifferCallSigns");
|
||||
|
||||
if (list != null && list.getLength() != 0) {
|
||||
lstNotify_QSOSniffer_sniffedCallSignList.clear();
|
||||
|
||||
for (int temp = 0; temp < list.getLength(); temp++) {
|
||||
Node node = list.item(temp);
|
||||
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
NodeList children = node.getChildNodes();
|
||||
|
||||
for (int i = 0; i < children.getLength(); i++) {
|
||||
Node child = children.item(i);
|
||||
|
||||
if (child.getNodeType()
|
||||
== Node.ELEMENT_NODE) {
|
||||
String callSign =
|
||||
child.getTextContent();
|
||||
|
||||
if (callSign != null
|
||||
&& !callSign.isBlank()) {
|
||||
String normalizedCallSign =
|
||||
callSign
|
||||
.trim()
|
||||
.toUpperCase();
|
||||
|
||||
if (!lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.contains(normalizedCallSign)) {
|
||||
lstNotify_QSOSniffer_sniffedCallSignList
|
||||
.add(normalizedCallSign);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list = doc.getElementsByTagName("snifferWords");
|
||||
if (list != null && list.getLength() != 0) {
|
||||
// reset to avoid duplicates when reloading
|
||||
@@ -2776,14 +2232,6 @@ public class ChatPreferences {
|
||||
messageHandling_autoAnswerEnabledSecondCat,
|
||||
"messageHandling_autoAnswerEnabledSecondCat",
|
||||
"autoAnswerEnabledSecondCat");
|
||||
|
||||
/*
|
||||
* The user interface intentionally exposes one shared generic auto-answer
|
||||
* setting for both chat categories. Keep the legacy second-category XML
|
||||
* fields synchronized so existing configuration files remain compatible.
|
||||
*/
|
||||
messageHandling_autoAnswerTextSecondCat = messageHandling_autoAnswerTextMainCat;
|
||||
messageHandling_autoAnswerEnabledSecondCat = messageHandling_autoAnswerEnabled;
|
||||
}
|
||||
|
||||
|
||||
@@ -2816,27 +2264,6 @@ public class ChatPreferences {
|
||||
parseSemicolonDoublesInto(getText(element, null, "GUIstage_updateStage_SceneSizeHW"), this.getGUIstage_updateStage_SceneSizeHW());
|
||||
parseSemicolonDoublesInto(getText(element, null, "GUIsettingsStageSceneSizeHW"), this.getGUIsettingsStageSceneSizeHW());
|
||||
|
||||
parseSemicolonDoublesInto(
|
||||
getText(element, null, "GUIstationMapStageSceneSizeHW"),
|
||||
this.getGUIstationMapStageSceneSizeHW()
|
||||
);
|
||||
|
||||
parseSemicolonDoublesInto(
|
||||
getText(element, null, "GUIstationMapStagePositionXY"),
|
||||
this.getGUIstationMapStagePositionXY()
|
||||
);
|
||||
|
||||
/*
|
||||
* Files written before config version 5 do not contain this value.
|
||||
* Keep the default true in that case so existing users discover the
|
||||
* path-analysis feature before choosing to hide it themselves.
|
||||
*/
|
||||
this.setGUIstationMapPathAnalysisVisible(getBoolean(
|
||||
element,
|
||||
this.isGUIstationMapPathAnalysisVisible(),
|
||||
"GUIstationMapPathAnalysisVisible"
|
||||
));
|
||||
|
||||
// Splitpane divider positions
|
||||
String s1 = getText(element, null, "GUIselectedCallSignSplitPane_dividerposition");
|
||||
if (s1 != null) {
|
||||
@@ -2910,8 +2337,6 @@ public class ChatPreferences {
|
||||
this.setGuiOptions_defaultFilterPmToMe(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPmToMe(), "guiOptions_defaultFilterPmToMe"));
|
||||
this.setGuiOptions_defaultFilterPmToOther(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPmToOther(), "guiOptions_defaultFilterPmToOther"));
|
||||
this.setGuiOptions_defaultFilterPublicMsgs(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_defaultFilterPublicMsgs(), "guiOptions_defaultFilterPublicMsgs"));
|
||||
this.setGuiOptions_showGrossFieldWorkedHintInBandColumns(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_showGrossFieldWorkedHintInBandColumns(), "guiOptions_showGrossFieldWorkedHintInBandColumns"));
|
||||
this.setGuiOptions_showFreshCallHintInBandColumns(getBoolean(guiSaveableOptionsEl, this.isGuiOptions_showFreshCallHintInBandColumns(), "guiOptions_showFreshCallHintInBandColumns"));
|
||||
|
||||
// Added in later versions: dark mode flags
|
||||
this.GUI_darkModeActive = getBoolean(guiSaveableOptionsEl, this.GUI_darkModeActive, "guiOptions_darkModeActive");
|
||||
@@ -2965,22 +2390,6 @@ public class ChatPreferences {
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isStn_bandActive50() {
|
||||
return stn_bandActive50;
|
||||
}
|
||||
|
||||
public void setStn_bandActive50(boolean stn_bandActive50) {
|
||||
this.stn_bandActive50 = stn_bandActive50;
|
||||
}
|
||||
|
||||
public boolean isStn_bandActive70() {
|
||||
return stn_bandActive70;
|
||||
}
|
||||
|
||||
public void setStn_bandActive70(boolean stn_bandActive70) {
|
||||
this.stn_bandActive70 = stn_bandActive70;
|
||||
}
|
||||
|
||||
public boolean isStn_bandActive144() {
|
||||
return stn_bandActive144;
|
||||
}
|
||||
@@ -3251,100 +2660,6 @@ public class ChatPreferences {
|
||||
}
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisOwnTxPowerWatts() {
|
||||
return stn_pathAnalysisOwnTxPowerWatts;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisOwnTxPowerWatts(double stn_pathAnalysisOwnTxPowerWatts) {
|
||||
this.stn_pathAnalysisOwnTxPowerWatts = stn_pathAnalysisOwnTxPowerWatts;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisOwnAntennaGainDbi() {
|
||||
return stn_pathAnalysisOwnAntennaGainDbi;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisOwnAntennaGainDbi(double stn_pathAnalysisOwnAntennaGainDbi) {
|
||||
this.stn_pathAnalysisOwnAntennaGainDbi = stn_pathAnalysisOwnAntennaGainDbi;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisDefaultTargetTxPowerWatts() {
|
||||
return stn_pathAnalysisDefaultTargetTxPowerWatts;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisDefaultTargetTxPowerWatts(double stn_pathAnalysisDefaultTargetTxPowerWatts) {
|
||||
this.stn_pathAnalysisDefaultTargetTxPowerWatts = stn_pathAnalysisDefaultTargetTxPowerWatts;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisDefaultTargetAntennaGainDbi() {
|
||||
return stn_pathAnalysisDefaultTargetAntennaGainDbi;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisDefaultTargetAntennaGainDbi(double stn_pathAnalysisDefaultTargetAntennaGainDbi) {
|
||||
this.stn_pathAnalysisDefaultTargetAntennaGainDbi = stn_pathAnalysisDefaultTargetAntennaGainDbi;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisVhfFeederLossPerStationDb() {
|
||||
return stn_pathAnalysisVhfFeederLossPerStationDb;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisVhfFeederLossPerStationDb(double stn_pathAnalysisVhfFeederLossPerStationDb) {
|
||||
this.stn_pathAnalysisVhfFeederLossPerStationDb = stn_pathAnalysisVhfFeederLossPerStationDb;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisFeederLossIncreaseDbPer200MHz() {
|
||||
return stn_pathAnalysisFeederLossIncreaseDbPer200MHz;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisFeederLossIncreaseDbPer200MHz(double stn_pathAnalysisFeederLossIncreaseDbPer200MHz) {
|
||||
this.stn_pathAnalysisFeederLossIncreaseDbPer200MHz = stn_pathAnalysisFeederLossIncreaseDbPer200MHz;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisMaxEstimatedFeederLossPerStationDb() {
|
||||
return stn_pathAnalysisMaxEstimatedFeederLossPerStationDb;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisMaxEstimatedFeederLossPerStationDb(double stn_pathAnalysisMaxEstimatedFeederLossPerStationDb) {
|
||||
this.stn_pathAnalysisMaxEstimatedFeederLossPerStationDb = stn_pathAnalysisMaxEstimatedFeederLossPerStationDb;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisRequiredSsbSignalDbm() {
|
||||
return stn_pathAnalysisRequiredSsbSignalDbm;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisRequiredSsbSignalDbm(double stn_pathAnalysisRequiredSsbSignalDbm) {
|
||||
this.stn_pathAnalysisRequiredSsbSignalDbm = stn_pathAnalysisRequiredSsbSignalDbm;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisRequiredCwSignalDbm() {
|
||||
return stn_pathAnalysisRequiredCwSignalDbm;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisRequiredCwSignalDbm(double stn_pathAnalysisRequiredCwSignalDbm) {
|
||||
this.stn_pathAnalysisRequiredCwSignalDbm = stn_pathAnalysisRequiredCwSignalDbm;
|
||||
}
|
||||
|
||||
public double getStn_pathAnalysisContestMarginDb() {
|
||||
return stn_pathAnalysisContestMarginDb;
|
||||
}
|
||||
|
||||
public void setStn_pathAnalysisContestMarginDb(double stn_pathAnalysisContestMarginDb) {
|
||||
this.stn_pathAnalysisContestMarginDb = stn_pathAnalysisContestMarginDb;
|
||||
}
|
||||
|
||||
public kst4contest.view.map.PathLinkBudgetSettings buildPathLinkBudgetSettings() {
|
||||
return new kst4contest.view.map.PathLinkBudgetSettings(
|
||||
stn_pathAnalysisOwnTxPowerWatts,
|
||||
stn_pathAnalysisOwnAntennaGainDbi,
|
||||
stn_pathAnalysisDefaultTargetTxPowerWatts,
|
||||
stn_pathAnalysisDefaultTargetAntennaGainDbi,
|
||||
stn_pathAnalysisVhfFeederLossPerStationDb,
|
||||
stn_pathAnalysisFeederLossIncreaseDbPer200MHz,
|
||||
stn_pathAnalysisMaxEstimatedFeederLossPerStationDb,
|
||||
stn_pathAnalysisRequiredSsbSignalDbm,
|
||||
stn_pathAnalysisRequiredCwSignalDbm,
|
||||
stn_pathAnalysisContestMarginDb
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,58 +3,25 @@ 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 String targetChatCallsign;
|
||||
private ChatCategory targetChatCategory;
|
||||
private double targetAzimuth;
|
||||
private long skedTimeEpoch;
|
||||
private double targetAzimuth; // Required for Antenna-Visuals
|
||||
private long skedTimeEpoch; // The peak time (e.g., AP)
|
||||
private Band band;
|
||||
|
||||
// Opportunity potential (0..100). -1 means "unknown".
|
||||
int opportunityPotentialPercent = -1;
|
||||
|
||||
// Status flags to prevent spamming alarms.
|
||||
// Status flags to prevent spamming alarms
|
||||
private boolean warning3MinSent = false;
|
||||
private boolean warningNowSent = false;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
public ContestSked(String call, double azimuth, long time, Band b) {
|
||||
this.targetCallsign = call;
|
||||
this.targetAzimuth = azimuth;
|
||||
this.skedTimeEpoch = time;
|
||||
this.band = band;
|
||||
this.band = b;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,54 +32,15 @@ public class ContestSked {
|
||||
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
// Getters and Setters...
|
||||
public String getTargetCallsign() { return targetCallsign; }
|
||||
public double getTargetAzimuth() { return targetAzimuth; }
|
||||
public long getSkedTimeEpoch() { return skedTimeEpoch; }
|
||||
public Band getBand() { return band; }
|
||||
public boolean isWarning3MinSent() { return warning3MinSent; }
|
||||
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
|
||||
public boolean isWarningNowSent() { return warningNowSent; }
|
||||
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
|
||||
|
||||
public int getOpportunityPotentialPercent() {
|
||||
return opportunityPotentialPercent;
|
||||
|
||||