mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-04-09 09:41:35 +02:00
Compare commits
13 Commits
db031bb5e3
...
wintest-im
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fc0e7d02d | |||
| e4648b596f | |||
| b3b7bc6a43 | |||
| 104cc07317 | |||
|
|
071ea800ae | ||
| e01cc3ca11 | |||
| aaa5c1088a | |||
| 178783aa8c | |||
| 68d171e793 | |||
| e4501e848a | |||
| b16dd1303a | |||
| e5b30c3049 | |||
| 318f3720b8 |
258
.github/latex-manual/manual-template.tex
vendored
Normal file
258
.github/latex-manual/manual-template.tex
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
%% ============================================================
|
||||
%% KST4Contest – pandoc LaTeX manual template
|
||||
%% PDF engine: XeLaTeX
|
||||
%% Usage: pandoc --template=manual-template.tex --pdf-engine=xelatex
|
||||
%% ============================================================
|
||||
\documentclass[11pt,a4paper]{article}
|
||||
|
||||
%% ─── Font / encoding ──────────────────────────────────────────────────────
|
||||
\usepackage{fontspec}
|
||||
% Latin Modern handles all Western European characters (umlauts etc.)
|
||||
\defaultfontfeatures{Ligatures=TeX,Scale=MatchLowercase}
|
||||
|
||||
%% ─── Language ─────────────────────────────────────────────────────────────
|
||||
\usepackage{polyglossia}
|
||||
$if(polyglossia-lang)$
|
||||
\setmainlanguage{$polyglossia-lang$}
|
||||
$else$
|
||||
\setmainlanguage{english}
|
||||
$endif$
|
||||
|
||||
%% ─── Page layout ──────────────────────────────────────────────────────────
|
||||
\usepackage[a4paper, top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm]{geometry}
|
||||
|
||||
%% ─── Text decorations (strikethrough via ~~...~~ in Markdown → \st{}) ────
|
||||
\usepackage{soul}
|
||||
|
||||
%% ─── Colors ───────────────────────────────────────────────────────────────
|
||||
\usepackage[dvipsnames,svgnames,x11names]{xcolor}
|
||||
\definecolor{brand-green}{RGB}{7,166,54}
|
||||
\definecolor{link-blue}{RGB}{0,86,163}
|
||||
\definecolor{code-bg}{RGB}{245,247,250}
|
||||
\definecolor{code-border}{RGB}{180,200,225}
|
||||
\definecolor{blockquote-line}{RGB}{7,166,54}
|
||||
|
||||
%% ─── Hyperlinks ───────────────────────────────────────────────────────────
|
||||
\usepackage{hyperref}
|
||||
\hypersetup{
|
||||
colorlinks = true,
|
||||
linkcolor = link-blue,
|
||||
urlcolor = link-blue,
|
||||
filecolor = link-blue,
|
||||
citecolor = link-blue,
|
||||
pdftitle = {$title$},
|
||||
pdfauthor = {DO5AMF (Marc Fröhlich), DN9APW (Philipp Wagner)},
|
||||
pdfsubject = {KST4Contest User Manual},
|
||||
pdfkeywords = {KST4Contest, pratiKST, VHF, Contest, Ham Radio},
|
||||
bookmarks = true,
|
||||
bookmarksnumbered = true,
|
||||
bookmarksopen = true,
|
||||
bookmarksopenlevel = 2,
|
||||
pdfpagemode = UseOutlines,
|
||||
}
|
||||
|
||||
%% ─── Graphics ─────────────────────────────────────────────────────────────
|
||||
\usepackage{graphicx}
|
||||
\graphicspath{{./}{./github_docs/}}
|
||||
\makeatletter
|
||||
\def\maxwidth{\ifdim\Gin@nat@width>\linewidth\linewidth\else\Gin@nat@width\fi}
|
||||
\def\maxheight{\ifdim\Gin@nat@height>0.65\textheight 0.65\textheight\else\Gin@nat@height\fi}
|
||||
\makeatother
|
||||
\setkeys{Gin}{width=\maxwidth,height=\maxheight,keepaspectratio}
|
||||
|
||||
%% ─── Tables ───────────────────────────────────────────────────────────────
|
||||
\usepackage{longtable}
|
||||
\usepackage{booktabs}
|
||||
\usepackage{array}
|
||||
\usepackage{calc}
|
||||
\usepackage{multirow}
|
||||
\setlength{\tabcolsep}{8pt}
|
||||
\renewcommand{\arraystretch}{1.35}
|
||||
% Pandoc pipe-table helpers
|
||||
\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}}
|
||||
\newcolumntype{C}[1]{>{\centering\arraybackslash}p{#1}}
|
||||
\newcolumntype{R}[1]{>{\raggedleft\arraybackslash}p{#1}}
|
||||
|
||||
%% ─── Lists ────────────────────────────────────────────────────────────────
|
||||
\providecommand{\tightlist}{%
|
||||
\setlength{\itemsep}{2pt}\setlength{\parskip}{0pt}}
|
||||
|
||||
%% ─── Code blocks (--listings flag) ───────────────────────────────────
|
||||
\usepackage{listings}
|
||||
\lstset{
|
||||
basicstyle = \ttfamily\small,
|
||||
backgroundcolor = \color{code-bg},
|
||||
frame = single,
|
||||
framesep = 4pt,
|
||||
rulecolor = \color{code-border},
|
||||
breaklines = true,
|
||||
breakatwhitespace= false,
|
||||
showstringspaces = false,
|
||||
extendedchars = true,
|
||||
xleftmargin = 6pt,
|
||||
xrightmargin = 6pt,
|
||||
aboveskip = 8pt,
|
||||
belowskip = 8pt,
|
||||
literate = {→}{{\ensuremath{\rightarrow}}}1
|
||||
{←}{{\ensuremath{\leftarrow}}}1
|
||||
{↔}{{\ensuremath{\leftrightarrow}}}1,
|
||||
}
|
||||
|
||||
%% ─── Blockquotes ──────────────────────────────────────────────────────────
|
||||
\usepackage{mdframed}
|
||||
\newmdenv[
|
||||
topline = false,
|
||||
rightline = false,
|
||||
bottomline = false,
|
||||
leftline = true,
|
||||
linewidth = 3pt,
|
||||
linecolor = blockquote-line,
|
||||
backgroundcolor = code-bg,
|
||||
leftmargin = 0pt,
|
||||
rightmargin = 0pt,
|
||||
innerleftmargin = 12pt,
|
||||
innerrightmargin = 8pt,
|
||||
innertopmargin = 6pt,
|
||||
innerbottommargin= 6pt,
|
||||
skipabove = 8pt,
|
||||
skipbelow = 8pt,
|
||||
]{blockquotebox}
|
||||
|
||||
\renewenvironment{quote}
|
||||
{\begin{blockquotebox}\small\itshape}
|
||||
{\end{blockquotebox}}
|
||||
|
||||
%% ─── Section styling ──────────────────────────────────────────────────────
|
||||
\usepackage{titlesec}
|
||||
|
||||
\titleformat{\section}
|
||||
{\Large\bfseries\color{brand-green}}
|
||||
{}
|
||||
{0em}
|
||||
{}
|
||||
[\color{brand-green}\titlerule]
|
||||
\titlespacing{\section}{0pt}{20pt}{10pt}
|
||||
|
||||
\titleformat{\subsection}
|
||||
{\large\bfseries\color{brand-green}}
|
||||
{}
|
||||
{0em}
|
||||
{}
|
||||
\titlespacing{\subsection}{0pt}{14pt}{6pt}
|
||||
|
||||
\titleformat{\subsubsection}
|
||||
{\normalsize\bfseries}
|
||||
{}
|
||||
{0em}
|
||||
{}
|
||||
\titlespacing{\subsubsection}{0pt}{10pt}{4pt}
|
||||
|
||||
% Level 4 (####): displayed as a named block heading in dark-grey
|
||||
\titleformat{\paragraph}
|
||||
{\normalsize\bfseries\color{brand-green}}
|
||||
{}
|
||||
{0em}
|
||||
{}
|
||||
\titlespacing{\paragraph}{0pt}{8pt}{2pt}
|
||||
|
||||
% Level 5 (#####): slightly smaller, italic, lighter grey
|
||||
\titleformat{\subparagraph}
|
||||
{\small\bfseries\itshape\color{brand-green!85!black}}
|
||||
{}
|
||||
{0em}
|
||||
{}
|
||||
\titlespacing{\subparagraph}{0pt}{6pt}{1pt}
|
||||
|
||||
% Reserve two additional section levels for future use (###### and deeper).
|
||||
% Pandoc currently maps up to \subparagraph for standard Markdown headings.
|
||||
\titleclass{\subsubsubsection}{straight}[\subparagraph]
|
||||
\newcounter{subsubsubsection}[subparagraph]
|
||||
\renewcommand\thesubsubsubsection{\thesubparagraph.\arabic{subsubsubsection}}
|
||||
\titleformat{\subsubsubsection}
|
||||
{\small\bfseries\color{brand-green!75!black}}
|
||||
{}
|
||||
{0em}
|
||||
{}
|
||||
\titlespacing{\subsubsubsection}{0pt}{5pt}{1pt}
|
||||
|
||||
\titleclass{\subsubsubsubsection}{straight}[\subsubsubsection]
|
||||
\newcounter{subsubsubsubsection}[subsubsubsection]
|
||||
\renewcommand\thesubsubsubsubsection{\thesubsubsubsection.\arabic{subsubsubsubsection}}
|
||||
\titleformat{\subsubsubsubsection}
|
||||
{\small\itshape\color{brand-green!65!black}}
|
||||
{}
|
||||
{0em}
|
||||
{}
|
||||
\titlespacing{\subsubsubsubsection}{0pt}{4pt}{1pt}
|
||||
|
||||
\setcounter{secnumdepth}{6}
|
||||
\setcounter{tocdepth}{6}
|
||||
|
||||
%% ─── Header / Footer ──────────────────────────────────────────────────────
|
||||
\usepackage{fancyhdr}
|
||||
\pagestyle{fancy}
|
||||
\fancyhf{}
|
||||
\fancyhead[L]{\small\color{brand-green}\textbf{KST4Contest}}
|
||||
\fancyhead[R]{\small\color{brand-green}$if(version)$$version$$endif$}
|
||||
\fancyfoot[L]{\small\color{gray}DO5AMF \textbar\ DN9APW}
|
||||
\fancyfoot[C]{\small\color{gray}\thepage}
|
||||
\fancyfoot[R]{\small\color{gray}$title$}
|
||||
\renewcommand{\headrulewidth}{0.4pt}
|
||||
\renewcommand{\footrulewidth}{0.3pt}
|
||||
\renewcommand{\headrule}{\color{brand-green}\hrule width\headwidth height\headrulewidth}
|
||||
|
||||
%% ─── Paragraph spacing ────────────────────────────────────────────────────
|
||||
\usepackage{parskip}
|
||||
\setlength{\parskip}{6pt}
|
||||
\setlength{\parindent}{0pt}
|
||||
|
||||
%% ─── TOC styling ──────────────────────────────────────────────────────────
|
||||
\usepackage{tocloft}
|
||||
\renewcommand{\cfttoctitlefont}{\Large\bfseries\color{brand-green}}
|
||||
\renewcommand{\cftsecfont}{\bfseries\color{brand-green}}
|
||||
\renewcommand{\cftsecpagefont}{\bfseries\color{brand-green}}
|
||||
\renewcommand{\cftsubsecfont}{\color{brand-green}}
|
||||
\renewcommand{\cftsubsecpagefont}{\color{brand-green}}
|
||||
\renewcommand{\cftsubsubsecfont}{\color{brand-green!85!black}}
|
||||
\renewcommand{\cftsubsubsecpagefont}{\color{brand-green!85!black}}
|
||||
\renewcommand{\cftparafont}{\color{brand-green!75!black}}
|
||||
\renewcommand{\cftparapagefont}{\color{brand-green!75!black}}
|
||||
\renewcommand{\cftsubparafont}{\color{brand-green!65!black}}
|
||||
\renewcommand{\cftsubparapagefont}{\color{brand-green!65!black}}
|
||||
\setlength{\cftbeforesecskip}{4pt}
|
||||
|
||||
%% ─── Misc ─────────────────────────────────────────────────────────────────
|
||||
\usepackage{amsmath}
|
||||
\usepackage{microtype}
|
||||
% Pandoc helper macros
|
||||
\newcommand{\passthrough}[1]{#1}
|
||||
|
||||
%% ══════════════════════════════════════════════════════════════════════════
|
||||
\begin{document}
|
||||
|
||||
%% ─── Title page ───────────────────────────────────────────────────────────
|
||||
\begin{titlepage}
|
||||
\pagecolor{brand-green}
|
||||
\centering
|
||||
\vspace*{3.5cm}
|
||||
{\fontsize{52}{62}\selectfont\bfseries\color{white}KST4Contest}\\[0.4cm]
|
||||
{\fontsize{22}{28}\selectfont\color{white!75!brand-green}pratiKST (ON4KST Chat Client)}\\[2.8cm]
|
||||
\color{white!40!brand-green}\rule{10cm}{0.6pt}\\[1.8cm]
|
||||
{\LARGE\bfseries\color{white}$title$}\\[1cm]
|
||||
$if(version)${\large\color{white!80!brand-green}Version:\space$version$}\\[0.6cm]$endif$
|
||||
\vfill
|
||||
{\large\color{white}DO5AMF · Marc Fröhlich · DM5M · DN9APW · Philipp Wagner}\\[0.4cm]
|
||||
{\color{white!70!brand-green}\today}\\[2cm]
|
||||
\end{titlepage}
|
||||
|
||||
\pagecolor{white}
|
||||
\newpage
|
||||
|
||||
%% ─── Table of Contents ────────────────────────────────────────────────────
|
||||
\tableofcontents
|
||||
\newpage
|
||||
|
||||
%% ─── Main content ─────────────────────────────────────────────────────────
|
||||
$body$
|
||||
|
||||
\end{document}
|
||||
156
.github/latex-manual/strip-wiki-links.lua
vendored
Normal file
156
.github/latex-manual/strip-wiki-links.lua
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
--[[
|
||||
strip-wiki-links.lua – pandoc Lua filter for KST4Contest documentation
|
||||
-----------------------------------------------------------------------
|
||||
1. Removes language-switch blockquotes (GitHub Wiki navigation) that
|
||||
are not relevant in the printed PDF manual.
|
||||
2. Converts internal GitHub-wiki-style links to in-document anchors
|
||||
so links jump within the generated PDF.
|
||||
3. Replaces flag emoji and other symbols that XeLaTeX cannot render with
|
||||
plain-text equivalents.
|
||||
--]]
|
||||
|
||||
local PAGE_ANCHOR_MAP = {
|
||||
["de-Home"] = "kst4contest-wiki",
|
||||
["de-Installation"] = "installation",
|
||||
["de-Konfiguration"] = "konfiguration",
|
||||
["de-Funktionen"] = "funktionen",
|
||||
["de-Benutzeroberflaeche"] = "benutzeroberflache",
|
||||
["de-Makros-und-Variablen"] = "makros-und-variablen",
|
||||
["de-Log-Synchronisation"] = "log-synchronisation",
|
||||
["de-AirScout-Integration"] = "airscout-integration",
|
||||
["de-DX-Cluster-Server"] = "integrierter-dx-cluster-server",
|
||||
["de-Changelog"] = "changelog",
|
||||
|
||||
["en-Home"] = "kst4contest-wiki",
|
||||
["en-Installation"] = "installation",
|
||||
["en-Configuration"] = "configuration",
|
||||
["en-Features"] = "features",
|
||||
["en-User-Interface"] = "user-interface",
|
||||
["en-Macros-and-Variables"] = "macros-and-variables",
|
||||
["en-Log-Sync"] = "log-synchronisation",
|
||||
["en-AirScout-Integration"] = "airscout-integration",
|
||||
["en-DX-Cluster-Server"] = "built-in-dx-cluster-server",
|
||||
["en-Changelog"] = "changelog",
|
||||
|
||||
["Installation"] = "installation",
|
||||
["Konfiguration"] = "konfiguration",
|
||||
["Funktionen"] = "funktionen",
|
||||
["Benutzeroberflaeche"] = "benutzeroberflache",
|
||||
["Makros-und-Variablen"] = "makros-und-variablen",
|
||||
["Log-Synchronisation"] = "log-synchronisation",
|
||||
["AirScout-Integration"] = "airscout-integration",
|
||||
["DX-Cluster-Server"] = "integrierter-dx-cluster-server",
|
||||
["Changelog"] = "changelog",
|
||||
|
||||
["Configuration"] = "configuration",
|
||||
["Features"] = "features",
|
||||
["User-Interface"] = "user-interface",
|
||||
["Macros-and-Variables"] = "macros-and-variables",
|
||||
["Log-Sync"] = "log-synchronisation",
|
||||
}
|
||||
|
||||
local function normalize_anchor(text)
|
||||
local s = text:lower()
|
||||
s = s:gsub("%%20", "-")
|
||||
s = s:gsub("ä", "a"):gsub("ö", "o"):gsub("ü", "u"):gsub("ß", "ss")
|
||||
s = s:gsub("[^%w%s%-_]", "")
|
||||
s = s:gsub("[_%s]+", "-")
|
||||
s = s:gsub("%-+", "-")
|
||||
s = s:gsub("^%-", ""):gsub("%-$", "")
|
||||
return s
|
||||
end
|
||||
|
||||
local function normalize_page_key(page)
|
||||
local key = page:gsub("^%./", ""):gsub("^/", "")
|
||||
key = key:gsub("^github_docs/", "")
|
||||
key = key:gsub("%.md$", "")
|
||||
return key
|
||||
end
|
||||
|
||||
local function resolve_page_anchor(page)
|
||||
local key = normalize_page_key(page)
|
||||
return PAGE_ANCHOR_MAP[key] or normalize_anchor(key)
|
||||
end
|
||||
|
||||
local function convert_url_token(token)
|
||||
local url, trailing = token:match("^(https?://%S-)([%.%,%;%:%!%?]?)$")
|
||||
if not url then
|
||||
return nil
|
||||
end
|
||||
|
||||
local link = pandoc.Link({pandoc.Str(url)}, url)
|
||||
if trailing ~= "" then
|
||||
return {link, pandoc.Str(trailing)}
|
||||
end
|
||||
return link
|
||||
end
|
||||
|
||||
-- Map of emoji / special Unicode sequences → plain-text replacements.
|
||||
-- Add more entries here as needed.
|
||||
local EMOJI_MAP = {
|
||||
-- Flag sequences
|
||||
["\xF0\x9F\x87\xAC\xF0\x9F\x87\xA7"] = "[EN]", -- 🇬🇧
|
||||
["\xF0\x9F\x87\xA9\xF0\x9F\x87\xAA"] = "[DE]", -- 🇩🇪
|
||||
-- Status symbols
|
||||
["\xE2\x9C\x85"] = "[OK]", -- ✅
|
||||
["\xE2\x9D\x8C"] = "[--]", -- ❌
|
||||
-- Misc symbols used in tables / text
|
||||
["\xF0\x9F\x94\xB4"] = "[red]", -- 🔴
|
||||
["\xF0\x9F\x9F\xA1"] = "[yellow]", -- 🟡
|
||||
["\xF0\x9F\x9F\xA2"] = "[green]", -- 🟢
|
||||
}
|
||||
|
||||
--- Replace emoji in a plain string.
|
||||
local function replace_emoji(text)
|
||||
for pattern, replacement in pairs(EMOJI_MAP) do
|
||||
text = text:gsub(pattern, replacement)
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
--- Filter: remove language-switch blockquotes from PDF output.
|
||||
-- These blockquotes appear in every wiki page for GitHub navigation
|
||||
-- but are not needed in the printed manual.
|
||||
function BlockQuote(el)
|
||||
local text = pandoc.utils.stringify(el)
|
||||
if text:find("Du liest gerade die deutsche Version") or
|
||||
text:find("You are reading the English version") then
|
||||
return {}
|
||||
end
|
||||
return el
|
||||
end
|
||||
|
||||
--- Filter: convert internal wiki links to in-PDF anchor links.
|
||||
function Link(el)
|
||||
local target = el.target
|
||||
-- Keep external URLs unchanged.
|
||||
if target:match("^https?://") or target:match("^mailto:") then
|
||||
return el
|
||||
end
|
||||
|
||||
if target:match("^#") then
|
||||
local fragment = target:gsub("^#", "")
|
||||
return pandoc.Link(el.content, "#" .. normalize_anchor(fragment), el.title, el.attr)
|
||||
end
|
||||
|
||||
local page, fragment = target:match("^([^#]+)#(.+)$")
|
||||
if page and fragment then
|
||||
return pandoc.Link(el.content, "#" .. normalize_anchor(fragment), el.title, el.attr)
|
||||
end
|
||||
|
||||
return pandoc.Link(el.content, "#" .. resolve_page_anchor(target), el.title, el.attr)
|
||||
end
|
||||
|
||||
--- Filter: replace emoji sequences in plain Str elements.
|
||||
function Str(el)
|
||||
local linkified = convert_url_token(el.text)
|
||||
if linkified then
|
||||
return linkified
|
||||
end
|
||||
|
||||
local replaced = replace_emoji(el.text)
|
||||
if replaced ~= el.text then
|
||||
return pandoc.Str(replaced)
|
||||
end
|
||||
return el
|
||||
end
|
||||
107
.github/workflows/docs-pdf.yml
vendored
Normal file
107
.github/workflows/docs-pdf.yml
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
name: Build Documentation PDF
|
||||
|
||||
# Runs when documentation changes are pushed to main, or on manual trigger.
|
||||
# Also triggered as a dependency from the tagged-release workflow.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- github_docs/**
|
||||
- .github/latex-manual/**
|
||||
- .github/workflows/docs-pdf.yml
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-docs-pdf:
|
||||
name: Build Documentation PDF
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Resolve version string
|
||||
run: |
|
||||
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
|
||||
VERSION="${{ github.ref_name }}"
|
||||
else
|
||||
VERSION="$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')-${GITHUB_SHA::7}"
|
||||
fi
|
||||
echo "DOC_VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install pandoc and LaTeX toolchain
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pandoc \
|
||||
texlive-xetex \
|
||||
texlive-fonts-recommended \
|
||||
texlive-latex-extra \
|
||||
texlive-plain-generic
|
||||
|
||||
- name: Build English PDF manual
|
||||
run: |
|
||||
mkdir -p dist
|
||||
pandoc \
|
||||
--from=markdown-yaml_metadata_block \
|
||||
--template=.github/latex-manual/manual-template.tex \
|
||||
--pdf-engine=xelatex \
|
||||
--lua-filter=.github/latex-manual/strip-wiki-links.lua \
|
||||
--resource-path=.:github_docs \
|
||||
--listings \
|
||||
--toc \
|
||||
--toc-depth=6 \
|
||||
-V title="User Manual" \
|
||||
-V polyglossia-lang=english \
|
||||
-V version="${DOC_VERSION}" \
|
||||
-o dist/KST4Contest-${DOC_VERSION}-manual-en.pdf \
|
||||
github_docs/en-Home.md \
|
||||
github_docs/en-Installation.md \
|
||||
github_docs/en-Configuration.md \
|
||||
github_docs/en-Features.md \
|
||||
github_docs/en-User-Interface.md \
|
||||
github_docs/en-Macros-and-Variables.md \
|
||||
github_docs/en-Log-Sync.md \
|
||||
github_docs/en-AirScout-Integration.md \
|
||||
github_docs/en-DX-Cluster-Server.md \
|
||||
github_docs/en-Changelog.md
|
||||
|
||||
- name: Build German PDF manual
|
||||
run: |
|
||||
pandoc \
|
||||
--from=markdown-yaml_metadata_block \
|
||||
--template=.github/latex-manual/manual-template.tex \
|
||||
--pdf-engine=xelatex \
|
||||
--lua-filter=.github/latex-manual/strip-wiki-links.lua \
|
||||
--resource-path=.:github_docs \
|
||||
--listings \
|
||||
--toc \
|
||||
--toc-depth=6 \
|
||||
-V title="Benutzerhandbuch" \
|
||||
-V polyglossia-lang=german \
|
||||
-V version="${DOC_VERSION}" \
|
||||
-o dist/KST4Contest-${DOC_VERSION}-manual-de.pdf \
|
||||
github_docs/de-Home.md \
|
||||
github_docs/de-Installation.md \
|
||||
github_docs/de-Konfiguration.md \
|
||||
github_docs/de-Funktionen.md \
|
||||
github_docs/de-Benutzeroberflaeche.md \
|
||||
github_docs/de-Makros-und-Variablen.md \
|
||||
github_docs/de-Log-Synchronisation.md \
|
||||
github_docs/de-AirScout-Integration.md \
|
||||
github_docs/de-DX-Cluster-Server.md \
|
||||
github_docs/de-Changelog.md
|
||||
|
||||
- name: Upload PDF artifacts
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: docs-pdf
|
||||
path: dist/KST4Contest-*-manual-*.pdf
|
||||
retention-days: 30
|
||||
96
.github/workflows/nightly-artifacts.yml
vendored
96
.github/workflows/nightly-artifacts.yml
vendored
@@ -85,7 +85,7 @@ jobs:
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=praktiKST-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name praktiKST \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
@@ -116,39 +116,105 @@ jobs:
|
||||
|
||||
- name: Create AppDir metadata
|
||||
run: |
|
||||
rm -rf target/praktiKST.AppDir
|
||||
cp -a dist/praktiKST target/praktiKST.AppDir
|
||||
rm -rf target/KST4Contest.AppDir
|
||||
cp -a dist/KST4Contest target/KST4Contest.AppDir
|
||||
|
||||
cat > target/praktiKST.AppDir/AppRun << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/AppRun << 'EOF'
|
||||
#!/bin/sh
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/bin/praktiKST" "$@"
|
||||
exec "$HERE/bin/KST4Contest" "$@"
|
||||
EOF
|
||||
chmod +x target/praktiKST.AppDir/AppRun
|
||||
chmod +x target/KST4Contest.AppDir/AppRun
|
||||
|
||||
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/KST4Contest.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=praktiKST
|
||||
Exec=praktiKST
|
||||
Icon=praktiKST
|
||||
Name=KST4Contest
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
|
||||
cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
|
||||
if [ -f target/KST4Contest.AppDir/lib/KST4Contest.png ]; then
|
||||
cp target/KST4Contest.AppDir/lib/KST4Contest.png target/KST4Contest.AppDir/KST4Contest.png
|
||||
fi
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x target/appimagetool.AppImage
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/praktiKST.AppDir "dist/${ASSET_BASENAME}-linux-x86_64.AppImage"
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/KST4Contest.AppDir "dist/${ASSET_BASENAME}-linux-x86_64.AppImage"
|
||||
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-appimage
|
||||
path: dist/praktiKST-*-linux-x86_64.AppImage
|
||||
path: dist/KST4Contest-*-linux-x86_64.AppImage
|
||||
retention-days: 14
|
||||
|
||||
build-macos-dmg:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, macos-15-intel]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Resolve nightly version info
|
||||
run: |
|
||||
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
|
||||
SHORT_SHA="${GITHUB_SHA::7}"
|
||||
ARCH=$(uname -m)
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
|
||||
echo "ASSET_BASENAME=KST4Contest-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
|
||||
echo "ARCH=$ARCH" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build macOS DMG with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type dmg \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
|
||||
- name: Rename DMG artifact
|
||||
run: |
|
||||
DMG=$(ls dist/*.dmg | head -n 1)
|
||||
if [ -z "$DMG" ]; then
|
||||
echo "No DMG produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$DMG" "dist/${ASSET_BASENAME}-macos-${ARCH}.dmg"
|
||||
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.os }}
|
||||
path: dist/KST4Contest-*-macos-*.dmg
|
||||
retention-days: 14
|
||||
|
||||
187
.github/workflows/tagged-release.yml
vendored
187
.github/workflows/tagged-release.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name praktiKST \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
@@ -98,41 +98,174 @@ jobs:
|
||||
|
||||
- name: Create AppDir metadata
|
||||
run: |
|
||||
rm -rf target/praktiKST.AppDir
|
||||
cp -a dist/praktiKST target/praktiKST.AppDir
|
||||
rm -rf target/KST4Contest.AppDir
|
||||
cp -a dist/KST4Contest target/KST4Contest.AppDir
|
||||
|
||||
cat > target/praktiKST.AppDir/AppRun << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/AppRun << 'EOF'
|
||||
#!/bin/sh
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/bin/praktiKST" "$@"
|
||||
exec "$HERE/bin/KST4Contest" "$@"
|
||||
EOF
|
||||
chmod +x target/praktiKST.AppDir/AppRun
|
||||
chmod +x target/KST4Contest.AppDir/AppRun
|
||||
|
||||
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
|
||||
cat > target/KST4Contest.AppDir/KST4Contest.desktop << 'EOF'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=praktiKST
|
||||
Exec=praktiKST
|
||||
Icon=praktiKST
|
||||
Name=KST4Contest
|
||||
Exec=KST4Contest
|
||||
Icon=KST4Contest
|
||||
Categories=Network;HamRadio;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
if [ -f target/praktiKST.AppDir/lib/praktiKST.png ]; then
|
||||
cp target/praktiKST.AppDir/lib/praktiKST.png target/praktiKST.AppDir/praktiKST.png
|
||||
if [ -f target/KST4Contest.AppDir/lib/KST4Contest.png ]; then
|
||||
cp target/KST4Contest.AppDir/lib/KST4Contest.png target/KST4Contest.AppDir/KST4Contest.png
|
||||
fi
|
||||
|
||||
- name: Build AppImage
|
||||
run: |
|
||||
wget -q -O target/appimagetool.AppImage https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||
chmod +x target/appimagetool.AppImage
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/praktiKST.AppDir dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 target/appimagetool.AppImage target/KST4Contest.AppDir dist/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: linux-appimage
|
||||
path: dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
|
||||
build-macos-dmg:
|
||||
name: Build macOS DMG (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, macos-15-intel]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@v4.1.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: Ensure mvnw is executable
|
||||
run: chmod +x mvnw
|
||||
|
||||
- name: Build JAR and copy runtime dependencies
|
||||
run: |
|
||||
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
|
||||
- name: Build macOS DMG with jpackage
|
||||
run: |
|
||||
mkdir -p dist
|
||||
jpackage \
|
||||
--type dmg \
|
||||
--name KST4Contest \
|
||||
--input target/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
|
||||
--dest dist
|
||||
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "13.0"
|
||||
|
||||
- name: Rename DMG artifact
|
||||
run: |
|
||||
ARCH=$(uname -m)
|
||||
DMG=$(ls dist/*.dmg | head -n 1)
|
||||
if [ -z "$DMG" ]; then
|
||||
echo "No DMG produced by jpackage" && exit 1
|
||||
fi
|
||||
mv "$DMG" "dist/KST4Contest-${{ github.ref_name }}-macos-${ARCH}.dmg"
|
||||
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: macos-dmg-${{ matrix.os }}
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-macos-*.dmg
|
||||
|
||||
build-docs-pdf:
|
||||
name: Build Documentation PDF
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Install pandoc and LaTeX toolchain
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
pandoc \
|
||||
texlive-xetex \
|
||||
texlive-fonts-recommended \
|
||||
texlive-latex-extra \
|
||||
texlive-plain-generic
|
||||
|
||||
- name: Build English PDF manual
|
||||
run: |
|
||||
mkdir -p dist
|
||||
pandoc \
|
||||
--from=markdown-yaml_metadata_block \
|
||||
--template=.github/latex-manual/manual-template.tex \
|
||||
--pdf-engine=xelatex \
|
||||
--lua-filter=.github/latex-manual/strip-wiki-links.lua \
|
||||
--resource-path=.:github_docs \
|
||||
--listings \
|
||||
--toc \
|
||||
--toc-depth=3 \
|
||||
-V title="User Manual" \
|
||||
-V polyglossia-lang=english \
|
||||
-V version="${{ github.ref_name }}" \
|
||||
-o dist/KST4Contest-${{ github.ref_name }}-manual-en.pdf \
|
||||
github_docs/en-Home.md \
|
||||
github_docs/en-Installation.md \
|
||||
github_docs/en-Configuration.md \
|
||||
github_docs/en-Features.md \
|
||||
github_docs/en-User-Interface.md \
|
||||
github_docs/en-Macros-and-Variables.md \
|
||||
github_docs/en-Log-Sync.md \
|
||||
github_docs/en-AirScout-Integration.md \
|
||||
github_docs/en-DX-Cluster-Server.md \
|
||||
github_docs/en-Changelog.md
|
||||
|
||||
- name: Build German PDF manual
|
||||
run: |
|
||||
pandoc \
|
||||
--from=markdown-yaml_metadata_block \
|
||||
--template=.github/latex-manual/manual-template.tex \
|
||||
--pdf-engine=xelatex \
|
||||
--lua-filter=.github/latex-manual/strip-wiki-links.lua \
|
||||
--resource-path=.:github_docs \
|
||||
--listings \
|
||||
--toc \
|
||||
--toc-depth=3 \
|
||||
-V title="Benutzerhandbuch" \
|
||||
-V polyglossia-lang=german \
|
||||
-V version="${{ github.ref_name }}" \
|
||||
-o dist/KST4Contest-${{ github.ref_name }}-manual-de.pdf \
|
||||
github_docs/de-Home.md \
|
||||
github_docs/de-Installation.md \
|
||||
github_docs/de-Konfiguration.md \
|
||||
github_docs/de-Funktionen.md \
|
||||
github_docs/de-Benutzeroberflaeche.md \
|
||||
github_docs/de-Makros-und-Variablen.md \
|
||||
github_docs/de-Log-Synchronisation.md \
|
||||
github_docs/de-AirScout-Integration.md \
|
||||
github_docs/de-DX-Cluster-Server.md \
|
||||
github_docs/de-Changelog.md
|
||||
|
||||
- name: Upload PDF artifacts
|
||||
uses: actions/upload-artifact@v4.3.4
|
||||
with:
|
||||
name: docs-pdf
|
||||
path: dist/KST4Contest-${{ github.ref_name }}-manual-*.pdf
|
||||
|
||||
release-tag:
|
||||
name: Publish Tagged Release
|
||||
@@ -140,20 +273,35 @@ jobs:
|
||||
needs:
|
||||
- build-windows-zip
|
||||
- build-linux-appimage
|
||||
- build-macos-dmg
|
||||
- build-docs-pdf
|
||||
|
||||
steps:
|
||||
- name: Download Windows artifact
|
||||
uses: actions/download-artifact@v4.1.1
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: windows-zip
|
||||
path: release-assets/windows
|
||||
|
||||
- name: Download Linux artifact
|
||||
uses: actions/download-artifact@v4.1.1
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: linux-appimage
|
||||
path: release-assets/linux
|
||||
|
||||
- name: Download macOS artifacts
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
pattern: macos-dmg-*
|
||||
merge-multiple: true
|
||||
path: release-assets/macos
|
||||
|
||||
- name: Download PDF manuals
|
||||
uses: actions/download-artifact@v4.1.3
|
||||
with:
|
||||
name: docs-pdf
|
||||
path: release-assets/docs
|
||||
|
||||
- name: Create tagged release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
@@ -165,4 +313,9 @@ jobs:
|
||||
replacesArtifacts: false
|
||||
makeLatest: ${{ !startsWith(github.ref_name, 'beta-') }}
|
||||
generateReleaseNotes: true
|
||||
artifacts: release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
|
||||
artifacts: >-
|
||||
release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,
|
||||
release-assets/linux/KST4Contest-${{ github.ref_name }}-linux-x86_64.AppImage,
|
||||
release-assets/macos/KST4Contest-${{ github.ref_name }}-macos-*.dmg,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
|
||||
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,5 +30,8 @@ target/
|
||||
#builds
|
||||
build/
|
||||
|
||||
#pdf output directory
|
||||
dist/
|
||||
|
||||
#zip files for local backups
|
||||
*.zip
|
||||
40
README.md
40
README.md
@@ -1,2 +1,38 @@
|
||||
# kst4contest
|
||||
java based on4kst chatclient
|
||||
# KST4Contest
|
||||
|
||||
KST4Contest (also known as pratiKST) is a Java-based chat client for ON4KST, focused on VHF/UHF/SHF contest operation.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full user documentation is maintained in the project wiki:
|
||||
|
||||
- https://github.com/praktimarc/kst4contest/wiki
|
||||
|
||||
Direct entry points:
|
||||
|
||||
- German start page: https://github.com/praktimarc/kst4contest/wiki/de-Home
|
||||
- English start page: https://github.com/praktimarc/kst4contest/wiki/en-Home
|
||||
|
||||
## Build
|
||||
|
||||
Compile locally with Maven Wrapper:
|
||||
|
||||
```bash
|
||||
./mvnw -B -DskipTests compile
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Source code is under `src/`.
|
||||
- Documentation markdown pages for wiki/PDF are under `github_docs/`.
|
||||
|
||||
## 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:
|
||||
|
||||
[](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml)
|
||||
@@ -32,8 +32,10 @@ Für diesen Dienst ist ein Account erforderlich. Bitte eine Spende für Thomas i
|
||||
|
||||
1. AirScout starten.
|
||||
2. In den AirScout-Einstellungen den OV3T-Feed-Account eintragen (Benutzername, Passwort, URL).
|
||||
|
||||

|
||||

|
||||
|
||||
3. Verbindung testen.
|
||||
|
||||
### Schritt 2: UDP-Kommunikation für KST4Contest aktivieren
|
||||
@@ -48,7 +50,8 @@ In AirScout die UDP-Schnittstelle aktivieren:
|
||||
In den KST4Contest-Preferences → **AirScout Settings**:
|
||||
- AirScout-Kommunikation aktivieren
|
||||
- IP und Port auf Standardwerte lassen (sofern nicht geändert)
|
||||

|
||||
|
||||
{ width=85% }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,83 @@ Versionsverlauf von KST4Contest / PraktiKST.
|
||||
|
||||
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**
|
||||
|
||||
**Neu:**
|
||||
- **Chatmember Score-System**: Jeder Chatmember erhält automatisch eine Prioritätsbewertung anhand von Antennenrichtung, Aktivitätszeit, Nachrichtenanzahl, aktiven Bändern, Frequenzen, Sked-Richtung und anderen Faktoren. Die Top-Kandidaten werden in einer eigenen Liste hervorgehoben.
|
||||
- **AP-Timeline**: Für jeden möglichen AP-Ankunftsminuten-Slot werden bis zu 4 hochbewertete Stationen angezeigt, die erreichbar wären. Bevorzugt werden APs mit dem höchsten Potenzial, nicht die schnellste Ankunft. Stationen, auf die die eigene Antenne nicht zeigt, werden transparent dargestellt.
|
||||
- **Win-Test-Unterstützung** (ab v1.31 als Beta, jetzt vollständig konfigurierbar): Log-Synchronisation, Frequenzauswertung und **Sked-Übergabe via UDP** vollständig integriert. In den Preferences aktivier-/deaktivierbar.
|
||||
- **PSTRotator-Interface** (ab v1.31 als Beta, jetzt vollständig konfigurierbar): Aktualisierung der Rotatorposition direkt aus KST4Contest. In den Preferences aktivier-/deaktivierbar.
|
||||
- **QSO-Sniffer**: Nachrichten von konfigurierbaren Rufzeichen-Listen werden automatisch in das PM-Fenster weitergeleitet.
|
||||
- **Band-Alert bei gearbeiteten Stationen**: Wenn eine Station geloggt wird, erscheint ein Hinweis, wenn diese Station ein weiteres Band aktiv hat, auf dem man selbst ebenfalls QRV ist.
|
||||
- **Sked-Erinnerungs-ALERT**: Pro Chatmember kann ein Sked-Alarm mit automatischen Nachrichten in konfigurierbaren Intervallen (2+1 / 5+2+1 / 10+5+2+1 Minuten vor dem Sked) eingerichtet werden, plus akustische und optische Benachrichtigung.
|
||||
- **Chat-Historie beim Start laden**: Beim Verbindungsaufbau wird die Serverhistorie geladen, um aktive Chatmember und letzte Nachrichten sofort sichtbar zu machen.
|
||||
- **Skedfail-Button**: Im FurtherInfo-Panel kann ein Sked-Misserfolg für einen Chatmember markiert werden, was dessen Score senkt.
|
||||
|
||||
**Geändert:**
|
||||
- AP-Notizen in DX-Cluster-Spots integriert.
|
||||
- Scrolling der Chatmember-Tabelle folgt automatisch der aktuellen Nachrichtenauswahl.
|
||||
- Generic Auto-Antwort und QRG-Auto-Antwort senden max. einmal pro 45 Sekunden pro Rufzeichen (verhindert Spam-Schleifen).
|
||||
- Speicherbare Einstellungen erweitert: ServerDNS/Port, PSTRotator-Interface, Win-Test-Interface, Callsign-Sniffer, Dark-Mode-Standard.
|
||||
- Datum in der Chat-Tabelle entfernt (nur Uhrzeit verbleibt – spart Platz).
|
||||
|
||||
**Behoben:**
|
||||
- Benutzerliste wird jetzt bei jedem Neu-Login automatisch sortiert.
|
||||
- Posonpill-Nachrichten beenden jetzt nur genau eine Client-Instanz (nicht alle und nicht wtKST).
|
||||
- wtKST: Absturz bei KST4Contest-Trennung behoben.
|
||||
- Mehrere Probleme mit Rufzeichen-Suffixen wie `/p`, `-2` etc. behoben.
|
||||
- `QTFDefault` wurde nicht korrekt gespeichert → behoben.
|
||||
- AirScout-Watchlist (ASWATCHLIST) wurde nicht korrekt aktualisiert → behoben.
|
||||
- Dark Mode: QRG-Felder wurden nicht vollständig angezeigt → behoben.
|
||||
- Versionsnummer-Anzeige korrigiert.
|
||||
|
||||
---
|
||||
|
||||
## v1.31 (2025-12-13)
|
||||
**Win-Test + PSTRotator Beta, QSO-Sniffer, DNS-Hotfix**
|
||||
|
||||
**Neu:**
|
||||
- **Win-Test-Unterstützung** (Beta, noch nicht deaktivierbar): Log-Synchronisation und Frequenzauswertung.
|
||||
- **PSTRotator-Unterstützung** (Beta, noch nicht deaktivierbar).
|
||||
- **QSO-Sniffer**: Nachrichten von konfigurierbaren Rufzeichen werden ins PM-Fenster weitergeleitet.
|
||||
|
||||
**Geändert:**
|
||||
- **DNS-Server geändert**: Von `www.on4kst.info` auf `www.on4kst.org` (Hotfix). Der DNS-Server ist ab sofort in den Preferences änderbar.
|
||||
|
||||
**Behoben:**
|
||||
- Endlosschleife im Fehlerfall friert den Client ein → behoben.
|
||||
|
||||
---
|
||||
|
||||
## v1.266 (2025-10-03)
|
||||
**AirScout-Fix für Rufzeichen mit Suffix**
|
||||
|
||||
**Behoben:**
|
||||
- AirScout-Interface funktionierte nicht, wenn das Login-Rufzeichen einen Suffix enthielt (z. B. `9A1W-2`). AirScout kann mit diesem Format nicht umgehen – es wird jetzt nur noch das Basis-Rufzeichen ohne Suffix an AirScout übergeben.
|
||||
|
||||
*(Fehler gemeldet und getestet von 9A2HM / Kreso – herzlichen Dank!)*
|
||||
|
||||
---
|
||||
|
||||
## v1.265 (2025-09-28)
|
||||
**Richtungs-Buttons bleiben aktiviert eingefärbt**
|
||||
|
||||
**Behoben:**
|
||||
- Richtungs-Buttons (N / NE / E usw.) behalten jetzt ihre Farbe, wenn sie aktiviert sind, sodass der Aktivierungsstatus auf einen Blick erkennbar ist.
|
||||
|
||||
---
|
||||
|
||||
## v1.264 (2025-08-02)
|
||||
**Simplelogfile: Rufzeichen-Erkennung verbessert**
|
||||
|
||||
**Behoben:**
|
||||
- Rufzeichen wie `S53CC`, `S51A` usw. wurden in der SimpleLogFile-Auswertung nicht als gearbeitet markiert → Erkennungsmuster verbessert.
|
||||
|
||||
*(Fehler gemeldet von Boris, S53CC – danke!)*
|
||||
|
||||
---
|
||||
|
||||
## v1.263 (2025-06-08)
|
||||
**AirScout-Kommunikation und Login-Name**
|
||||
|
||||
@@ -153,6 +230,6 @@ Erste öffentlich veröffentlichte Version. Grundfunktionen:
|
||||
## Geplante Features
|
||||
|
||||
- `MYQTF`-Variable (eigene Antennenrichtung als Text)
|
||||
- Lebensdauer für den Worked-Status (automatisches Zurücksetzen)
|
||||
- ~~Lebensdauer für den Worked-Status (automatisches Zurücksetzen)~~ ✅ **Umgesetzt in v1.40** (3-Tage-Lebensdauer, kein manuelles Zurücksetzen mehr nötig)
|
||||
- Filterung des „Cluster & QSO der anderen"-Fensters auf eigenes QTF
|
||||
- Weitere Topografie-basierte Berechnungen für die Richtungswarnung
|
||||
|
||||
@@ -135,22 +135,86 @@ Für ausgewählte Stationen in der Benutzerliste gibt es direkte Buttons, um das
|
||||
|
||||
---
|
||||
|
||||
## Sked-Erinnerungen (Sked Reminder Service)
|
||||
## Sked-Erinnerungen mit ALERT (ab v1.40)
|
||||
|
||||
Für vereinbarte Skeds können automatische Erinnerungs-PMs konfiguriert werden, die X Minuten vor dem vereinbarten Zeitpunkt gesendet werden. Die Erinnerungen werden aus dem FurtherInfo-Panel heraus aktiviert.
|
||||
Für jeden Chatmember kann ein Sked-Erinnerungsdienst mit automatischen Nachrichten aktiviert werden. Konfigurierbare Intervallmuster:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
Aktivierung: FurtherInfo-Panel der entsprechenden Station.
|
||||
|
||||
---
|
||||
|
||||
## Prioritätsliste / Score-Service
|
||||
## QSO-Sniffer (ab v1.31)
|
||||
|
||||
KST4Contest berechnet automatisch eine **Prioritätsliste** der interessantesten Gesprächspartner, basierend auf:
|
||||
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.
|
||||
|
||||
- Richtungserkennung
|
||||
Konfiguration: [Konfiguration – Sniffer-Einstellungen](de-Konfiguration#sniffer-einstellungen-ab-v131)
|
||||
|
||||
---
|
||||
|
||||
## Win-Test-Integration (ab v1.31, vollständig ab v1.40)
|
||||
|
||||
KST4Contest unterstützt [Win-Test](https://www.win-test.com/) vollständig als Logprogramm:
|
||||
|
||||
- **Log-Synchronisation**: Gearbeitete Stationen werden automatisch aus Win-Test übernommen und in der Benutzerliste markiert.
|
||||
- **Frequenz-Auswertung**: Die aktuelle TRX-Frequenz wird aus Win-Test-UDP-Paketen ausgewertet und befüllt die `MYQRG`-Variable.
|
||||
- **Sked-Übergabe (SKED Push via UDP)**: Vereinbarte Skeds aus KST4Contest können direkt an Win-Test übertragen werden, sodass das Rufzeichen der Gegenstation im Win-Test-Sked-Fenster erscheint.
|
||||
|
||||
Details zur Konfiguration: [Konfiguration – Win-Test-Netzwerk-Listener](de-Konfiguration#win-test-netzwerk-listener)
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator-Interface (ab v1.31, vollständig ab v1.40)
|
||||
|
||||
KST4Contest kann die Antennenrichtung direkt über **PSTRotator** steuern. Wenn in der Benutzerliste eine Station ausgewählt wird, kann der Rotator automatisch auf den QTF zur ausgewählten Station gedreht werden.
|
||||
|
||||
Konfiguration: [Konfiguration – PSTRotator-Einstellungen](de-Konfiguration#pstrotator-einstellungen-ab-v131)
|
||||
|
||||
---
|
||||
|
||||
## Band-Alert bei neuen QSOs (ab v1.40)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Worked-Tag-Lebensdauer (ab v1.40)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
|
||||
- Antennenrichtung der Gegenstation (zeigt sie auf mich?)
|
||||
- QRB (Entfernung)
|
||||
- Aktivitätszeit und Nachrichtenanzahl
|
||||
- Aktive Bänder und Frequenzen
|
||||
- AP-Verfügbarkeit (AirScout)
|
||||
- Worked-Status
|
||||
- Sked-Richtung
|
||||
- Sked-Erfolgsrate und Skedfail-Markierungen
|
||||
|
||||
Die Top-Kandidaten werden in einer eigenen Liste angezeigt und helfen, im Contest-Stress die wichtigsten Stationen nicht zu übersehen.
|
||||
Die Top-Kandidaten werden in einer eigenen Prioritätsliste hervorgehoben und helfen, im Contest-Stress die wichtigsten Stationen nicht zu übersehen.
|
||||
|
||||
Stationen, bei denen ein Sked gescheitert ist, können über den **Skedfail-Button** im FurtherInfo-Panel markiert werden – das senkt ihren Score vorübergehend.
|
||||
|
||||
---
|
||||
|
||||
## AP-Timeline (ab v1.40)
|
||||
|
||||
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:
|
||||
|
||||
- Bevorzugt werden APs mit dem **höchsten Reflexionspotenzial** (nicht unbedingt die schnellste Ankunft).
|
||||
- Stationen, auf die die eigene Antenne nicht zeigt, werden **transparent** dargestellt.
|
||||
|
||||
So kann der Contest-Operator auf einem Blick sehen, welche Stationen wann und über welche Flugzeuge erreichbar sein werden.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
Es wird eine Mindestauflösung von 1200px mal 720px empfohlen
|
||||
|
||||
### ON4KST-Account
|
||||
|
||||
Um den Chat zu nutzen, ist ein registrierter Account beim ON4KST-Chat-Dienst erforderlich:
|
||||
@@ -44,7 +46,17 @@ Die aktuelle Version kann als AppImage heruntergeladen werden:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
Der Dateiname hat das Format `praktiKST-v<Versionsnummer>-linux-x86_64.AppImage`.
|
||||
Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-linux-x86_64.AppImage`.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort-Support:** macOS-Builds werden als zusätzliche Option bereitgestellt, sind aber **nicht umfassend getestet**. Wir bauen und veröffentlichen macOS-Binaries mit jedem Release, können allerdings nicht alle Szenarien unter macOS testen. Bei Problemen freuen wir uns über eine Rückmeldung – wir versuchen unser Bestes, können aber nicht den gleichen Support-Umfang wie für Windows und Linux garantieren.
|
||||
|
||||
Die aktuelle Version kann als DMG-Disk-Image heruntergeladen werden (für Apple-Silicon- und Intel-Macs verfügbar):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
Der Dateiname hat das Format `KST4Contest-v<Versionsnummer>-macos-<Architektur>.dmg`, wobei `<Architektur>` entweder `arm64` (Apple Silicon) oder `x86_64` (Intel) ist.
|
||||
|
||||
|
||||
---
|
||||
@@ -62,11 +74,22 @@ Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` gespei
|
||||
### Linux
|
||||
1. AppImage herunterladen.
|
||||
2. AppImage in gewünschten Ordner entpacken.
|
||||
3. AppImage ausführbar machen (geht im Terminal mit `chmod +x praktiKST-v<Versionsnummer>-linux-x86_64.AppImage`)
|
||||
3. AppImage ausführbar machen (geht im Terminal mit `chmod +x KST4Contest-v<Versionsnummer>-linux-x86_64.AppImage`)
|
||||
4. AppImage ausführen.
|
||||
|
||||
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
|
||||
### macOS
|
||||
1. DMG-Datei für die eigene Architektur herunterladen (Apple Silicon oder Intel).
|
||||
2. DMG-Datei öffnen.
|
||||
3. `KST4Contest.app` in den **Programme**-Ordner ziehen.
|
||||
4. Beim ersten Start zeigt macOS ggf. eine Warnung, da die App nicht notarisiert ist. Zum Öffnen:
|
||||
- Rechtsklick (oder Ctrl-Klick) auf `KST4Contest.app` im Finder → **Öffnen** wählen.
|
||||
- Alternativ: **Systemeinstellungen → Datenschutz & Sicherheit** → **Trotzdem öffnen** klicken.
|
||||
5. KST4Contest aus dem Programme-Ordner oder dem Launchpad starten.
|
||||
|
||||
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
|
||||
|
||||
---
|
||||
|
||||
## Update
|
||||
@@ -96,6 +119,12 @@ Derzeit folgendermaßen:
|
||||
2. neues AppImage ausführbar makieren
|
||||
3. (optional) altes AppImage löschen.
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Neue DMG-Datei herunterladen.
|
||||
2. DMG öffnen.
|
||||
3. Die neue `KST4Contest.app` in den **Programme**-Ordner ziehen und die alte Version ersetzen.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ Nach dem ersten Start öffnet sich das **Einstellungsfenster** – dieses ist de
|
||||
---
|
||||
|
||||
## Station Settings (Stationseinstellungen)
|
||||
|
||||

|
||||
|
||||
### Login und Chat-Kategorien
|
||||
|
||||
Hier werden die Zugangsdaten für den ON4KST-Chat eingetragen (Rufzeichen und Passwort).
|
||||
@@ -37,6 +39,17 @@ Maximale Entfernung (in km), für die Richtungs-Warnungen ausgelöst werden soll
|
||||
|
||||
---
|
||||
|
||||
## Server-Einstellungen (ab v1.31)
|
||||
|
||||
Der Chat-Server-DNS und -Port sind in den Preferences konfigurierbar:
|
||||
|
||||
- **Server-DNS**: Standard `www.on4kst.org` (ab v1.31 geändert von `www.on4kst.info`).
|
||||
- **Port**: Standardport des ON4KST-Servers.
|
||||
|
||||
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).
|
||||
@@ -120,14 +133,55 @@ Neuer Einstellungsbereich mit folgenden Optionen:
|
||||
|
||||
---
|
||||
|
||||
## Win-Test-Netzwerk-Listener (ab v1.31)
|
||||
|
||||
Dedizierter Empfänger für Win-Test-spezifische UDP-Pakete. Ermöglicht:
|
||||
|
||||
- **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:
|
||||
- **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.
|
||||
|
||||
> **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)
|
||||
|
||||
KST4Contest kann die Antennenrichtung über PSTRotator steuern.
|
||||
|
||||
Einstellungen:
|
||||
- **Aktivieren/Deaktivieren**: Checkbox in den Preferences (ab v1.40).
|
||||
- **IP-Adresse**: IP-Adresse des PSTRotator-Rechners (Standard: `127.0.0.1` bei Betrieb auf demselben PC).
|
||||
- **Port**: Kommunikationsport von PSTRotator.
|
||||
|
||||
> **Hinweis**: Nach einem Klick auf den Richtungs-Button wartet KST4Contest kurz auf die Rotatorantwort. Bei langsamen Rotoren (z. B. SPID) kann es zu einer kleinen Verzögerung kommen.
|
||||
|
||||
---
|
||||
|
||||
## Sniffer-Einstellungen (ab v1.31)
|
||||
|
||||
Der QSO-Sniffer filtert Chat-Nachrichten von konfigurierbaren Rufzeichen und leitet sie ins PM-Fenster weiter.
|
||||
|
||||
Einstellungen:
|
||||
- **Rufzeichen-Liste**: Kommagetrennte Liste von Rufzeichen, deren Nachrichten immer in das PM-Fenster weitergeleitet werden sollen.
|
||||
|
||||
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)
|
||||
|
||||
Vor jedem Contest die interne Worked-Datenbank zurücksetzen! Enthält:
|
||||
Die interne Worked-Datenbank enthält:
|
||||
|
||||
- Worked-Status aller Stationen (pro Band)
|
||||
- NOT-QRV-Tags (seit v1.2)
|
||||
|
||||
Schaltfläche **„Reinitialize"** unter der Tabelle verwenden. Eine geplante Funktion ist eine automatische Ablaufzeit für den Worked-Status.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
KST4Contest markiert gearbeitete Stationen automatisch in der Chat-Benutzerliste. Dafür gibt es zwei grundlegende Methoden:
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
## Methode 1: Universal File Based Callsign Interpreter (Simplelogfile)
|
||||
@@ -33,6 +34,7 @@ Das Logprogramm sendet beim Speichern eines QSOs ein UDP-Paket an die Broadcast-
|
||||
## Unterstützte Logprogramme
|
||||
|
||||
### UCXLog (DL7UCX)
|
||||
|
||||

|
||||
|
||||
UCXLog sendet QSO-UDP-Pakete und Transceiver-Frequenzpakete.
|
||||
@@ -81,20 +83,26 @@ Für den integrierten DX-Cluster-Server: N1MM+ als DX-Cluster-Client konfigurier
|
||||
|
||||
### Win-Test
|
||||
|
||||
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
|
||||
Win-Test wird mit einem dedizierten UDP-Netzwerk-Listener unterstützt, der das native Win-Test Netzwerkprotokoll versteht.
|
||||
|
||||
**Vorteile der Win-Test Integration:**
|
||||
- Automatische QSO-Synchronisation zur Markierung gearbeiteter Stationen.
|
||||
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieses auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet*.
|
||||
- **Sked-Übergabe (ADDSKED):** Über den Button "Create sked" im Stationsinfo-Panel wird nicht nur in KST4Contest ein Sked angelegt, sondern dieser auch *direkt per UDP an das Win-Test Netzwerk als ADDSKED-Paket gesendet* – automatisch, sobald der Listener aktiv ist.
|
||||
- Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden.
|
||||
- **Automatische QRG-Auflösung für SKEDs:** KST4Contest wählt die Sked-Frequenz intelligent:
|
||||
1. Hat die Gegenstation in einer Chat-Nachricht ihre QRG genannt, wird diese verwendet.
|
||||
2. Sonst wird die eigene aktuelle QRG verwendet (aus Win-Test STATUS oder manueller Eingabe).
|
||||
|
||||
**Notwendige Einstellungen in KST4Contest:**
|
||||
- `UDP-Port for Win-Test listener` (Standard: 9871).
|
||||
**Einstellungen im Reiter „Log-Synchronisation":**
|
||||
- `Receive Win-Test network based UDP log messages` aktivieren.
|
||||
- `Win-Test sked transmission (push via ADDSKED to Win-Test network)` aktivieren.
|
||||
- `UDP-Port for Win-Test listener` (Standard: 9871).
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST").
|
||||
- `Win-Test station name filter`: Wenn hier ein Name eingetragen wird (z.B. "STN1"), werden nur QSOs von dieser bestimmten Win-Test Instanz verarbeitet. Leer lassen, um alle zu akzeptieren.
|
||||
- `Win-Test network broadcast address`: Wird idR automatisch erkannt und ist erforderlich, um die Sked-Pakete ins Netzwerk zu senden.
|
||||
- `Win-Test network broadcast address`: Wird i.d.R. automatisch erkannt; erforderlich für das Senden von Sked-Paketen.
|
||||
|
||||
**Einstellungen im Reiter „TRX-Synchronisation":**
|
||||
- `Win-Test STATUS QRG Sync`: Wenn aktiviert, übernimmt KST4Contest die aktuelle Transceiverfrequenz aus dem Win-Test STATUS-Paket als eigene QRG (MYQRG).
|
||||
- `Use pass frequency from Win-Test STATUS`: Statt der eigenen TRX-QRG wird die im STATUS-Paket enthaltene Pass-Frequenz als MYQRG verwendet (für Multi-Op-Setups, bei denen mit einer Pass-QRG gearbeitet wird).
|
||||
- `Win-Test station name filter`: Wird hier ein Name eingetragen (z.B. "STN1"), verarbeitet KST4Contest nur Pakete dieser Win-Test-Instanz. Leer lassen, um alle zu akzeptieren.
|
||||
|
||||
**Einstellungen in Win-Test:**
|
||||
- Das Netzwerk in Win-Test muss aktiv sein.
|
||||
@@ -110,6 +118,11 @@ Neben der QSO-Synchronisation übertragen UCXLog und andere Programme auch die *
|
||||
|
||||
**Ergebnis**: Die eigene QRG muss im Chat nie mehr manuell eingegeben werden – ein Klick auf den MYQRG-Button oder die Verwendung der Variable im Beacon genügt.
|
||||
|
||||
**Quellen für die eigene QRG (MYQRG):**
|
||||
- UCXLog, N1MM+, DXLog.net, QARTest via UDP-Port 12060
|
||||
- Win-Test STATUS-Paket (optional, konfigurierbar im Reiter „TRX-Synchronisation" unter „Win-Test STATUS QRG Sync")
|
||||
- Manuelle Eingabe im QRG-Feld
|
||||
|
||||
> **Hinweis für Multi-Setup**: Bei zwei Logprogrammen an zwei Computern sollte nur **eines** die Frequenzpakete senden. KST4Contest kann nicht zwischen den Quellen unterscheiden und verarbeitet alle eingehenden Pakete.
|
||||
|
||||
---
|
||||
|
||||
@@ -32,8 +32,10 @@ An account is required for this service. Please consider donating to Thomas –
|
||||
|
||||
1. Start AirScout.
|
||||
2. Enter your OV3T feed account details (username, password, URL) in the AirScout settings.
|
||||
|
||||

|
||||

|
||||
|
||||
3. Test the connection.
|
||||
|
||||
### Step 2: Enable UDP Communication for KST4Contest
|
||||
@@ -48,7 +50,8 @@ In AirScout, enable the UDP interface:
|
||||
In KST4Contest Preferences → **AirScout Settings**:
|
||||
- Enable AirScout communication
|
||||
- Leave IP and port at their default values (unless changed)
|
||||

|
||||
|
||||
{ width=85% }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,83 @@ Version history of KST4Contest / PraktiKST.
|
||||
|
||||
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**
|
||||
|
||||
**New:**
|
||||
- **Chatmember Score System**: Every chat member is automatically scored based on antenna direction, activity time, message count, active bands, frequencies, sked direction (degrees), and other factors. Top candidates are highlighted in a dedicated list.
|
||||
- **AP Timeline**: For each minute of possible aircraft arrival, up to 4 highly-scored stations are shown that should be workable. Aircraft with the highest potential are preferred over the fastest arrival. Chat members whose antenna is not pointing towards you are shown transparently.
|
||||
- **Win-Test Support** (Beta since v1.31, now fully configurable): Log synchronisation, frequency parsing and **sked handover via UDP** fully integrated. Can be enabled/disabled in Preferences.
|
||||
- **PSTRotator Interface** (Beta since v1.31, now fully configurable): Rotator position updates directly from KST4Contest. Can be enabled/disabled in Preferences.
|
||||
- **QSO Sniffer**: Messages from configurable callsign lists are automatically forwarded to the PM window.
|
||||
- **Band Alert for logged stations**: When a station is logged, a hint appears if that station has another active band that you are also QRV on.
|
||||
- **Sked Reminder ALERT**: A sked alarm with automatic messages in configurable intervals (2+1 / 5+2+1 / 10+5+2+1 minutes before the sked) can be set up for each chat member, plus acoustic and visual notification.
|
||||
- **Load chat history on startup**: Chat server history is loaded on connect to immediately see active members and recent messages.
|
||||
- **Skedfail button**: In the FurtherInfo panel, a sked failure can be marked for a chat member, which lowers their priority score.
|
||||
|
||||
**Changed:**
|
||||
- AP notes added to internal DX cluster spots.
|
||||
- Chat member table scrolling follows the current message selection automatically.
|
||||
- Generic auto-reply and QRG auto-reply now fire a maximum of once every 45 seconds per callsign (prevents spam and message ping-pong).
|
||||
- New saveable settings: ServerDNS/Port, PSTRotator interface, Win-Test interface, callsign sniffer, Dark Mode on by default.
|
||||
- Date column removed from chat table (time only – saves space).
|
||||
|
||||
**Fixed:**
|
||||
- User list now automatically sorted on every new member sign-on.
|
||||
- Posonpill messages now terminate exactly one client instance (no longer affects all instances or wtKST).
|
||||
- wtKST: crash on KST4Contest disconnection fixed.
|
||||
- Multiple issues with callsign suffixes like `/p`, `-2`, etc. fixed throughout.
|
||||
- `QTFDefault` was not saved correctly → fixed.
|
||||
- AirScout watchlist (ASWATCHLIST) was not being updated → fixed.
|
||||
- Dark Mode: QRG fields not displayed at full size → fixed.
|
||||
- Version number display corrected.
|
||||
|
||||
---
|
||||
|
||||
## v1.31 (2025-12-13)
|
||||
**Win-Test + PSTRotator Beta, QSO Sniffer, DNS Hotfix**
|
||||
|
||||
**New:**
|
||||
- **Win-Test support** (Beta, not yet deactivatable): Log synchronisation and frequency parsing.
|
||||
- **PSTRotator support** (Beta, not yet deactivatable).
|
||||
- **QSO Sniffer**: Messages from configurable callsigns are forwarded to the PM window.
|
||||
|
||||
**Changed:**
|
||||
- **DNS server changed**: From `www.on4kst.info` to `www.on4kst.org` (hotfix). The DNS server is now configurable in Preferences.
|
||||
|
||||
**Fixed:**
|
||||
- Endless loop in error case freezes the client → fixed.
|
||||
|
||||
---
|
||||
|
||||
## v1.266 (2025-10-03)
|
||||
**AirScout Fix for Callsigns with Suffix**
|
||||
|
||||
**Fixed:**
|
||||
- AirScout interface did not work when the login callsign contained a suffix (e.g. `9A1W-2`). AirScout cannot handle this format – only the base callsign without suffix is now passed to AirScout.
|
||||
|
||||
*(Bug reported and tested by 9A2HM / Kreso – many thanks!)*
|
||||
|
||||
---
|
||||
|
||||
## v1.265 (2025-09-28)
|
||||
**Direction Buttons Stay Coloured When Active**
|
||||
|
||||
**Fixed:**
|
||||
- Direction buttons (N / NE / E etc.) now keep their highlight colour when activated, making the active state immediately visible.
|
||||
|
||||
---
|
||||
|
||||
## v1.264 (2025-08-02)
|
||||
**Simplelogfile: Improved Callsign Recognition**
|
||||
|
||||
**Fixed:**
|
||||
- Callsigns like `S53CC`, `S51A`, etc. were not being marked as worked in the SimpleLogFile interpreter → recognition pattern improved.
|
||||
|
||||
*(Bug reported by Boris, S53CC – thank you!)*
|
||||
|
||||
---
|
||||
|
||||
## v1.263 (2025-06-08)
|
||||
**AirScout Communication and Login Name**
|
||||
|
||||
@@ -153,6 +230,6 @@ First publicly released version. Core features:
|
||||
## Planned Features
|
||||
|
||||
- `MYQTF` variable (own antenna direction as text)
|
||||
- Lifetime for worked status (automatic reset)
|
||||
- ~~Lifetime for worked status (automatic reset)~~ ✅ **Implemented in v1.40** (3-day lifetime, no manual reset needed anymore)
|
||||
- Filtering the "Cluster & QSO of others" window to own QTF
|
||||
- Further topography-based calculations for direction warnings
|
||||
|
||||
@@ -9,7 +9,9 @@ After the first start, the **settings window** opens – this is the central sta
|
||||
---
|
||||
|
||||
## Station Settings
|
||||
|
||||

|
||||
|
||||
### Login and Chat Categories
|
||||
|
||||
Enter your ON4KST chat credentials here (callsign and password).
|
||||
@@ -37,9 +39,20 @@ Maximum distance (in km) for which direction warnings should be triggered. A rea
|
||||
|
||||
---
|
||||
|
||||
## Server Settings (from v1.31)
|
||||
|
||||
The chat server DNS and port are configurable in the Preferences:
|
||||
|
||||
- **Server DNS**: Default `www.on4kst.org` (changed from `www.on4kst.info` in v1.31 hotfix).
|
||||
- **Port**: Default port of the ON4KST server.
|
||||
|
||||
A change is only needed if the server moves or an alternative endpoint is used.
|
||||
|
||||
---
|
||||
|
||||
## Log Sync Settings
|
||||
|
||||
Two methods are available for automatically marking worked stations. Details: [Log Synchronisation](en-Log-Sync).
|
||||
Three methods are available for automatically marking worked stations. Details: [Log Synchronisation](en-Log-Sync).
|
||||
|
||||
### Universal File Based Callsign Interpreter (Simplelogfile)
|
||||
|
||||
@@ -120,14 +133,55 @@ New settings section with the following options:
|
||||
|
||||
---
|
||||
|
||||
## Win-Test Network Listener (from v1.31)
|
||||
|
||||
A dedicated listener for Win-Test-specific UDP packets. Enables:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
> **Note**: The Win-Test listener is an **additional** listener – the standard QSO UDP broadcast listener on port 12060 remains independent.
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator Settings (from v1.31)
|
||||
|
||||
KST4Contest can control antenna direction via PSTRotator.
|
||||
|
||||
Settings:
|
||||
- **Enable/Disable**: Checkbox in Preferences (from v1.40).
|
||||
- **IP address**: IP address of the PSTRotator computer (default: `127.0.0.1` when running on the same PC).
|
||||
- **Port**: Communication port of PSTRotator.
|
||||
|
||||
> **Note**: After clicking a direction button, KST4Contest waits briefly for the rotator response. With slow rotors (e.g. SPID) there may be a small delay.
|
||||
|
||||
---
|
||||
|
||||
## Sniffer Settings (from v1.31)
|
||||
|
||||
The QSO sniffer filters chat messages from configurable callsigns and forwards them to the PM window.
|
||||
|
||||
Settings:
|
||||
- **Callsign list**: Comma-separated list of callsigns whose messages are always forwarded to the PM window.
|
||||
|
||||
Use case: Keep track of important stations (e.g. DX expeditions or trusted contest allies) without constantly monitoring the main chat.
|
||||
|
||||
---
|
||||
|
||||
## Worked Station Database Settings
|
||||
|
||||
Reset the internal worked database before each contest! It contains:
|
||||
The internal worked database contains:
|
||||
|
||||
- Worked status of all stations (per band)
|
||||
- NOT-QRV tags (since v1.2)
|
||||
|
||||
Use the **"Reinitialize"** button below the table. A planned feature is an automatic expiration time for the worked status.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -135,22 +135,86 @@ For selected stations in the user list, there are direct buttons to open the **Q
|
||||
|
||||
---
|
||||
|
||||
## Sked Reminders (Sked Reminder Service)
|
||||
## Sked Reminders with ALERT (from v1.40)
|
||||
|
||||
For agreed skeds, automatic reminder PMs can be configured, sent X minutes before the agreed time. Reminders are activated from the FurtherInfo panel.
|
||||
A sked reminder service with automatic messages can be activated for each chat member. Configurable interval patterns:
|
||||
|
||||
- **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.
|
||||
|
||||
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.
|
||||
|
||||
Activate from the FurtherInfo panel of the corresponding station.
|
||||
|
||||
---
|
||||
|
||||
## Priority List / Score Service
|
||||
## QSO Sniffer (from v1.31)
|
||||
|
||||
KST4Contest automatically calculates a **priority list** of the most interesting contacts, based on:
|
||||
The QSO sniffer monitors the chat for messages from a configurable callsign list and automatically forwards them to the **PM window**. This prevents relevant messages from being lost in the general chat traffic.
|
||||
|
||||
- Direction detection
|
||||
Configuration: [Configuration – Sniffer Settings](en-Configuration#sniffer-settings-from-v131)
|
||||
|
||||
---
|
||||
|
||||
## Win-Test Integration (from v1.31, fully configurable from v1.40)
|
||||
|
||||
KST4Contest fully supports [Win-Test](https://www.win-test.com/) as a logging programme:
|
||||
|
||||
- **Log synchronisation**: Worked stations are automatically retrieved from Win-Test and marked in the user list.
|
||||
- **Frequency parsing**: The current TRX frequency is read from Win-Test UDP packets and populates the `MYQRG` variable.
|
||||
- **Sked handover (SKED push via UDP)**: Agreed skeds from KST4Contest can be pushed directly to Win-Test, so the remote callsign appears in Win-Test's sked window.
|
||||
|
||||
Details: [Configuration – Win-Test Network Listener](en-Configuration#win-test-network-listener)
|
||||
|
||||
---
|
||||
|
||||
## PSTRotator Interface (from v1.31, fully configurable from v1.40)
|
||||
|
||||
KST4Contest can control antenna direction directly via **PSTRotator**. When a station is selected in the user list, the rotator can automatically be turned to the QTF of the selected station.
|
||||
|
||||
Configuration: [Configuration – PSTRotator Settings](en-Configuration#pstrotator-settings-from-v131)
|
||||
|
||||
---
|
||||
|
||||
## Band Alert for New QSOs (from v1.40)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Worked Tag Lifetime (from v1.40)
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Chatmember Score System / Priority List (from v1.40)
|
||||
|
||||
KST4Contest automatically calculates a **priority score** for each active chat member. The score is derived from:
|
||||
|
||||
- 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)
|
||||
- Worked status
|
||||
- Sked direction (degrees)
|
||||
- Sked success rate and skedfail markings
|
||||
|
||||
The top candidates are shown in a separate list, helping you not to miss the most important stations during contest stress.
|
||||
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 Timeline (from v1.40)
|
||||
|
||||
A visual timeline shows up to 4 highly-scored stations per minute slot that should be workable via aircraft scatter. Prioritisation criteria:
|
||||
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
## Prerequisites
|
||||
|
||||
An resolution of 1200px by 720px is recommended
|
||||
|
||||
### ON4KST Account
|
||||
|
||||
To use the chat, a registered account with the ON4KST chat service is required:
|
||||
@@ -44,7 +46,17 @@ The latest version can be downloaded as an AppImage:
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
The filename has the format `praktiKST-v<version_number>-linux-x86_64.AppImage`.
|
||||
The filename has the format `KST4Contest-v<version_number>-linux-x86_64.AppImage`.
|
||||
|
||||
### macOS
|
||||
|
||||
> ⚠️ **Best-Effort Support:** macOS builds are provided as a convenience but are **not fully tested**. We build and release macOS binaries with every release, but we cannot test every scenario on macOS. If you encounter issues, please report them – we will do our best to address them, but cannot guarantee the same level of support as for Windows and Linux.
|
||||
|
||||
The latest version can be downloaded as a DMG disk image (available for both Apple Silicon and Intel Macs):
|
||||
|
||||
**https://github.com/praktimarc/kst4contest/releases/latest**
|
||||
|
||||
The filename has the format `KST4Contest-v<version_number>-macos-<arch>.dmg`, where `<arch>` is `arm64` (Apple Silicon) or `x86_64` (Intel).
|
||||
|
||||
|
||||
---
|
||||
@@ -62,11 +74,22 @@ Settings are stored at `%USERPROFILE%\.praktikst\preferences.xml`.
|
||||
### Linux
|
||||
1. Download the AppImage.
|
||||
2. Unzip the AppImage into a folder of your choice.
|
||||
3. Make the AppImage executable (in the terminal with `chmod +x praktiKST-v<version_number>-linux-x86_64.AppImage`)
|
||||
3. Make the AppImage executable (in the terminal with `chmod +x KST4Contest-v<version_number>-linux-x86_64.AppImage`)
|
||||
4. Run the AppImage.
|
||||
|
||||
Settings are stored at `~/.praktikst/preferences.xml`.
|
||||
|
||||
### macOS
|
||||
1. Download the DMG file for your architecture (Apple Silicon or Intel).
|
||||
2. Open the DMG file.
|
||||
3. Drag `KST4Contest.app` into your **Applications** folder.
|
||||
4. On first launch, macOS may show a warning because the app is not notarised. To open it:
|
||||
- Right-click (or Control-click) on `KST4Contest.app` in Finder and choose **Open**.
|
||||
- Alternatively, go to **System Settings → Privacy & Security** and click **Open Anyway**.
|
||||
5. Run KST4Contest from your Applications folder or Launchpad.
|
||||
|
||||
Settings are stored at `~/.praktikst/preferences.xml`.
|
||||
|
||||
---
|
||||
|
||||
## Updating
|
||||
@@ -96,6 +119,12 @@ Currently as follows:
|
||||
2. Mark the new AppImage as executable
|
||||
3. (optional) Delete the old AppImage.
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Download the new DMG file.
|
||||
2. Open the DMG.
|
||||
3. Drag the new `KST4Contest.app` into your **Applications** folder, replacing the old version.
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
KST4Contest automatically marks worked stations in the chat user list. Two basic methods are available:
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||
## Method 1: Universal File Based Callsign Interpreter (Simplelogfile)
|
||||
@@ -33,6 +34,7 @@ When saving a QSO, the logging software sends a UDP packet to the broadcast addr
|
||||
## Supported Logging Software
|
||||
|
||||
### UCXLog (DL7UCX)
|
||||
|
||||

|
||||
|
||||
UCXLog sends QSO UDP packets and transceiver frequency packets.
|
||||
@@ -85,16 +87,22 @@ Win-Test is supported with a dedicated UDP network listener that understands the
|
||||
|
||||
**Advantages of Win-Test Integration:**
|
||||
- Automatic QSO synchronization to mark worked stations.
|
||||
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet*.
|
||||
- **Sked Handover (ADDSKED):** Using the "Create sked" button in the station info panel not only creates a sked in KST4Contest but also *sends it directly via UDP to the Win-Test network as an ADDSKED packet* – automatically, as soon as the listener is active. No separate toggle is needed.
|
||||
- You can choose between "AUTO", "SSB", or "CW" sked modes.
|
||||
- **Automatic QRG resolution for SKEDs:** KST4Contest selects the sked frequency intelligently:
|
||||
1. If the other station mentioned their QRG in a recent chat message, that frequency is used.
|
||||
2. Otherwise, your own current QRG is used (from Win-Test STATUS or manual entry).
|
||||
|
||||
**Required Settings in KST4Contest:**
|
||||
- `UDP-Port for Win-Test listener` (Default: 9871).
|
||||
**Settings in the "Log Synchronisation" tab:**
|
||||
- Enable `Receive Win-Test network based UDP log messages`.
|
||||
- Enable `Win-Test sked transmission (push via ADDSKED to Win-Test network)`.
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Defines the station name KST4Contest uses in the WT network (e.g., "KST").
|
||||
- `Win-Test station name filter`: If a name is entered here (e.g., "STN1"), only QSOs from that specific Win-Test instance will be processed. Leave empty to accept all.
|
||||
- `Win-Test network broadcast address`: Is usually detected automatically and is required to send sked packets to the network.
|
||||
- `UDP-Port for Win-Test listener` (default: 9871).
|
||||
- `KST station name in Win-Test network (src of SKED packets)`: Defines the station name KST4Contest uses in the WT network (e.g. "KST").
|
||||
- `Win-Test network broadcast address`: Usually detected automatically; required to send sked packets to the network.
|
||||
|
||||
**Settings in the "TRX Synchronisation" tab:**
|
||||
- `Win-Test STATUS QRG Sync`: When enabled, KST4Contest takes the current transceiver frequency from the Win-Test STATUS packet and uses it as your own QRG (MYQRG).
|
||||
- `Use pass frequency from Win-Test STATUS`: Instead of the main TRX frequency, the pass frequency contained in the STATUS packet is used as MYQRG (useful for multi-op setups that operate with a dedicated pass QRG).
|
||||
- `Win-Test station name filter`: If a name is entered here (e.g. "STN1"), KST4Contest only processes packets from that specific Win-Test instance. Leave empty to accept all.
|
||||
|
||||
**Settings in Win-Test:**
|
||||
- The network in Win-Test must be active.
|
||||
@@ -110,6 +118,11 @@ In addition to QSO synchronisation, UCXLog and other programs also transmit the
|
||||
|
||||
**Result**: Your own QRG never needs to be typed manually in the chat – clicking the MYQRG button or using the variable in the beacon is sufficient.
|
||||
|
||||
**Sources for your own QRG (MYQRG):**
|
||||
- UCXLog, N1MM+, DXLog.net, QARTest via UDP port 12060
|
||||
- Win-Test STATUS packet (optional, configurable in the "TRX Synchronisation" tab under "Win-Test STATUS QRG Sync")
|
||||
- Manual entry in the QRG field
|
||||
|
||||
> **Note for multi-setup**: With two logging programs on two computers, only **one** should send frequency packets. KST4Contest cannot distinguish between sources and processes all incoming packets.
|
||||
|
||||
---
|
||||
|
||||
528
src/kstsimulator.py
Normal file
528
src/kstsimulator.py
Normal file
@@ -0,0 +1,528 @@
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# =====================================
|
||||
# KST-Server-Simulator / DO5AMF
|
||||
# Usage: change configuration below and
|
||||
# run. Enter 127.0.0.1 : 23001 as a
|
||||
# target in KST4Contest or another
|
||||
# KST chat client.
|
||||
# =====================================
|
||||
|
||||
# ==========================================
|
||||
# KONFIGURATION
|
||||
# ==========================================
|
||||
|
||||
PORT = 23001
|
||||
HOST = '127.0.0.1'
|
||||
|
||||
MSG_TO_USER_INTERVAL = 300.0
|
||||
LOGIN_LOGOUT_INTERVAL = 60.0
|
||||
KEEP_ALIVE_INTERVAL = 10.0
|
||||
CLIENT_WARMUP_TIME = 5.0
|
||||
|
||||
PROB_INACTIVE = 0.10
|
||||
PROB_REACTIVE = 0.20
|
||||
|
||||
# QSY Wahrscheinlichkeit (Wie oft wechselt ein User seine Frequenz?)
|
||||
# 0.05 = 5% Chance pro Nachricht, dass er die Frequenz ändert. Sonst bleibt er stabil.
|
||||
PROB_QSY = 0.05
|
||||
|
||||
BANDS_VHF = { "2m": (144.150, 144.400), "70cm": (432.100, 432.300) }
|
||||
BANDS_UHF = { "23cm": (1296.100, 1296.300), "3cm": (10368.100, 10368.250) }
|
||||
|
||||
CHANNELS_SETUP = {
|
||||
"2": {
|
||||
"NAME": "144/432 MHz",
|
||||
"NUM_USERS": 777,
|
||||
"BANDS": BANDS_VHF,
|
||||
"RATES": {"PUBLIC": 0.5, "DIRECTED": 3.0},
|
||||
"PERMANENT": [
|
||||
{"call": "DK5EW", "name": "Erwin", "loc": "JN47NX"},
|
||||
{"call": "DL1TEST", "name": "TestOp", "loc": "JO50XX"}
|
||||
]
|
||||
},
|
||||
"3": {
|
||||
"NAME": "Microwave",
|
||||
"NUM_USERS": 333,
|
||||
"BANDS": BANDS_UHF,
|
||||
"RATES": {"PUBLIC": 0.2, "DIRECTED": 0.5},
|
||||
"PERMANENT": [
|
||||
{"call": "ON4KST", "name": "Alain", "loc": "JO20HI"},
|
||||
{"call": "G4CBW", "name": "MwTest", "loc": "IO83AA"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
COUNTRY_MAPPING = {
|
||||
"DL": ["JO", "JN"], "DA": ["JO", "JN"], "DF": ["JO", "JN"], "DJ": ["JO", "JN"], "DK": ["JO", "JN"], "DO": ["JO", "JN"],
|
||||
"F": ["JN", "IN", "JO"], "G": ["IO", "JO"], "M": ["IO", "JO"], "2E": ["IO", "JO"],
|
||||
"PA": ["JO"], "ON": ["JO"], "OZ": ["JO"], "SM": ["JO", "JP"], "LA": ["JO", "JP"],
|
||||
"OH": ["KP"], "SP": ["JO", "KO"], "OK": ["JO", "JN"], "OM": ["JN", "KN"],
|
||||
"HA": ["JN", "KN"], "S5": ["JN"], "9A": ["JN"], "HB9": ["JN"], "OE": ["JN"],
|
||||
"I": ["JN", "JM"], "IK": ["JN", "JM"], "IU": ["JN", "JM"], "EA": ["IN", "IM"],
|
||||
"CT": ["IM"], "EI": ["IO"], "GM": ["IO"], "GW": ["IO"], "YO": ["KN"],
|
||||
"YU": ["KN"], "LZ": ["KN"], "SV": ["KM", "KN"], "UR": ["KO", "KN"],
|
||||
"LY": ["KO"], "YL": ["KO"], "ES": ["KO"]
|
||||
}
|
||||
|
||||
NAMES = ["Hans", "Peter", "Jo", "Alain", "Mike", "Sven", "Ole", "Jean", "Bob", "Tom", "Giovanni", "Mario", "Frank", "Steve", "Dave"]
|
||||
|
||||
MSG_TEMPLATES_WITH_FREQ = [
|
||||
"QSY {freq}", "PSE QSY {freq}", "Calling CQ on {freq}", "I am QRV on {freq}",
|
||||
"Listening on {freq}", "Can you try {freq}?", "Signals strong on {freq}",
|
||||
"Scattering on {freq}", "Please go to {freq}", "Running test on {freq}",
|
||||
"Any takers for {freq}?", "Back to {freq}", "QRG {freq}?", "Aircraft scatter {freq}"
|
||||
]
|
||||
|
||||
MSG_TEMPLATES_TEXT_ONLY = [
|
||||
"TNX for QSO", "73 all", "Anyone for sked?", "Good conditions",
|
||||
"Nothing heard", "Rain scatter?", "Waiting for moonrise", "CQ Contest",
|
||||
"QRZ?", "My locator is {loc}", "Band is open"
|
||||
]
|
||||
|
||||
REPLY_TEMPLATES = [
|
||||
"Hello {user}, 599 here", "Rgr {user}, tnx for report", "Yes {user}, QSY?",
|
||||
"Sorry {user}, no copy", "Pse wait 5 min {user}", "Ok {user}, 73",
|
||||
"Locator is {loc}", "Go to {freq} please", "Rgr {user}, gl"
|
||||
]
|
||||
|
||||
# ==========================================
|
||||
# CLIENT WRAPPER
|
||||
# ==========================================
|
||||
|
||||
class ConnectedClient:
|
||||
def __init__(self, sock, addr):
|
||||
self.sock = sock
|
||||
self.addr = addr
|
||||
self.call = f"GUEST_{random.randint(1000,9999)}"
|
||||
self.channels = {"2"}
|
||||
self.login_time = time.time()
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def send_safe(self, data_str):
|
||||
if not data_str: return True
|
||||
with self.lock:
|
||||
try:
|
||||
self.sock.sendall(data_str.encode('latin-1', errors='replace'))
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
try: self.sock.close()
|
||||
except: pass
|
||||
|
||||
# ==========================================
|
||||
# LOGIK KLASSEN
|
||||
# ==========================================
|
||||
|
||||
class MessageFactory:
|
||||
@staticmethod
|
||||
def get_stable_frequency(user, band_name, min_f, max_f):
|
||||
"""Liefert eine stabile Frequenz für diesen User auf diesem Band"""
|
||||
# Wenn noch keine Frequenz da ist ODER Zufall zuschlägt (QSY)
|
||||
if band_name not in user['freqs'] or random.random() < PROB_QSY:
|
||||
freq_val = round(random.uniform(min_f, max_f), 3)
|
||||
user['freqs'][band_name] = f"{freq_val:.3f}"
|
||||
|
||||
return user['freqs'][band_name]
|
||||
|
||||
@staticmethod
|
||||
def get_chat_message(bands_config, user):
|
||||
try:
|
||||
# Entscheidung: Text mit Frequenz oder ohne?
|
||||
if random.random() < 0.7:
|
||||
# Wähle zufälliges Band aus den verfügbaren
|
||||
band_name = random.choice(list(bands_config.keys()))
|
||||
min_f, max_f = bands_config[band_name]
|
||||
|
||||
# Hole STABILE Frequenz für diesen User
|
||||
freq_str = MessageFactory.get_stable_frequency(user, band_name, min_f, max_f)
|
||||
|
||||
return random.choice(MSG_TEMPLATES_WITH_FREQ).format(freq=freq_str)
|
||||
else:
|
||||
return random.choice(MSG_TEMPLATES_TEXT_ONLY).format(loc=user['loc'])
|
||||
except: return "TNX 73"
|
||||
|
||||
@staticmethod
|
||||
def get_reply_msg(bands, target_call, my_loc):
|
||||
try:
|
||||
tmpl = random.choice(REPLY_TEMPLATES)
|
||||
freq_str = "QSY?"
|
||||
# Bei Replies simulieren wir oft nur "QSY?" ohne konkrete Frequenz,
|
||||
# oder nutzen eine zufällige, da der Kontext fehlt.
|
||||
if "{freq}" in tmpl and bands:
|
||||
band_name = random.choice(list(bands.keys()))
|
||||
min_f, max_f = bands[band_name]
|
||||
freq_str = f"{round(random.uniform(min_f, max_f), 3):.3f}"
|
||||
return tmpl.format(user=target_call, loc=my_loc, freq=freq_str)
|
||||
except: return "TNX 73"
|
||||
|
||||
class UserFactory:
|
||||
registry = {}
|
||||
|
||||
@classmethod
|
||||
def get_or_create_user(cls, channel_id, current_channel_users):
|
||||
# 1. Reuse existing
|
||||
candidates = [u for call, u in cls.registry.items() if call not in current_channel_users]
|
||||
if candidates and random.random() < 0.5:
|
||||
return random.choice(candidates)
|
||||
|
||||
# 2. Create new
|
||||
return cls._create_new_unique_user(channel_id, current_channel_users)
|
||||
|
||||
@classmethod
|
||||
def _create_new_unique_user(cls, channel_id, current_channel_users):
|
||||
while True:
|
||||
prefix = random.choice(list(COUNTRY_MAPPING.keys()))
|
||||
num = random.randint(0, 9)
|
||||
suffix = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=random.randint(1,3)))
|
||||
call = f"{prefix}{num}{suffix}"
|
||||
|
||||
if call in current_channel_users: continue
|
||||
if call in cls.registry: return cls.registry[call]
|
||||
|
||||
valid_grids = COUNTRY_MAPPING[prefix]
|
||||
grid_prefix = random.choice(valid_grids)
|
||||
sq_num = f"{random.randint(0,99):02d}"
|
||||
sub = "".join(random.choices("ABCDEFGHIJKLMNOPQRSTUVWXYZ", k=2))
|
||||
loc = f"{grid_prefix}{sq_num}{sub}"
|
||||
|
||||
name = random.choice(NAMES)
|
||||
rand = random.random()
|
||||
if rand < PROB_INACTIVE: role = "INACTIVE"
|
||||
elif rand < (PROB_INACTIVE + PROB_REACTIVE): role = "REACTIVE"
|
||||
else: role = "ACTIVE"
|
||||
|
||||
# Neu V31: Frequenz-Gedächtnis
|
||||
user_data = {
|
||||
"call": call,
|
||||
"name": name,
|
||||
"loc": loc,
|
||||
"role": role,
|
||||
"freqs": {} # Speicher für { '2m': '144.300' }
|
||||
}
|
||||
|
||||
cls.registry[call] = user_data
|
||||
return user_data
|
||||
|
||||
@classmethod
|
||||
def register_permanent(cls, user_data):
|
||||
# Sicherstellen, dass auch Permanent User Freq-Memory haben
|
||||
if "freqs" not in user_data:
|
||||
user_data["freqs"] = {}
|
||||
cls.registry[user_data['call']] = user_data
|
||||
|
||||
# ==========================================
|
||||
# CHANNEL INSTANCE
|
||||
# ==========================================
|
||||
|
||||
class ChannelInstance:
|
||||
def __init__(self, cid, config, server):
|
||||
self.id = cid
|
||||
self.config = config
|
||||
self.server = server
|
||||
|
||||
self.users_pool = []
|
||||
self.online_users = {}
|
||||
self.history_chat = []
|
||||
|
||||
self.last_pub = time.time()
|
||||
self.last_dir = time.time()
|
||||
self.last_me = time.time()
|
||||
self.last_login = time.time()
|
||||
|
||||
self.rate_pub = 1.0 / config["RATES"]["PUBLIC"]
|
||||
self.rate_dir = 1.0 / config["RATES"]["DIRECTED"]
|
||||
|
||||
self._init_data()
|
||||
|
||||
def _init_data(self):
|
||||
print(f"[*] Init Channel {self.id} ({self.config['NAME']})...")
|
||||
|
||||
for u in self.config["PERMANENT"]:
|
||||
u_full = u.copy()
|
||||
u_full["role"] = "ACTIVE"
|
||||
UserFactory.register_permanent(u_full)
|
||||
self.online_users[u['call']] = u_full
|
||||
|
||||
for _ in range(self.config["NUM_USERS"]):
|
||||
new_u = UserFactory.get_or_create_user(self.id, self.online_users.keys())
|
||||
self.users_pool.append(new_u)
|
||||
|
||||
fill = int(self.config["NUM_USERS"] * 0.9)
|
||||
for i in range(fill):
|
||||
u = self.users_pool[i]
|
||||
if u['call'] not in self.online_users:
|
||||
self.online_users[u['call']] = u
|
||||
|
||||
print(f"[*] Channel {self.id} ready: {len(self.online_users)} Users.")
|
||||
self._prefill_history()
|
||||
|
||||
def _prefill_history(self):
|
||||
actives = [u for u in self.online_users.values() if u['role'] == "ACTIVE"]
|
||||
if not actives: return
|
||||
start = datetime.now() - timedelta(minutes=15)
|
||||
for i in range(30):
|
||||
msg_time = start + timedelta(seconds=i*30)
|
||||
ts = str(int(msg_time.timestamp()))
|
||||
sender = random.choice(actives)
|
||||
if i % 2 == 0:
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], sender)
|
||||
frame = f"CH|{self.id}|{ts}|{sender['call']}|{sender['name']}|0|{text}|0|\r\n"
|
||||
else:
|
||||
target = random.choice(list(self.online_users.values()))
|
||||
text = MessageFactory.get_reply_msg(self.config["BANDS"], target['call'], sender['loc'])
|
||||
frame = f"CH|{self.id}|{ts}|{sender['call']}|{sender['name']}|0|{text}|{target['call']}|\r\n"
|
||||
self.history_chat.append(frame)
|
||||
|
||||
def tick(self, now):
|
||||
actives = [u for u in self.online_users.values() if u['role'] == "ACTIVE"]
|
||||
if not actives: return
|
||||
|
||||
# PUBLIC
|
||||
if now - self.last_pub > self.rate_pub:
|
||||
self.last_pub = now
|
||||
u = random.choice(actives)
|
||||
# V31: Nutzt jetzt get_chat_message, das das Freq-Memory abfragt
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], u)
|
||||
ts = str(int(now))
|
||||
frame = f"CH|{self.id}|{ts}|{u['call']}|{u['name']}|0|{text}|0|\r\n"
|
||||
self._add_hist(frame)
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
|
||||
# DIRECTED
|
||||
if now - self.last_dir > self.rate_dir:
|
||||
self.last_dir = now
|
||||
if len(actives) > 5:
|
||||
u1 = random.choice(actives)
|
||||
u2 = random.choice(list(self.online_users.values()))
|
||||
if u1 != u2:
|
||||
if random.random() < 0.5:
|
||||
# Auch hier Frequenzstabilität beachten
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], u1)
|
||||
else:
|
||||
text = MessageFactory.get_reply_msg(self.config["BANDS"], u2['call'], u1['loc'])
|
||||
ts = str(int(now))
|
||||
frame = f"CH|{self.id}|{ts}|{u1['call']}|{u1['name']}|0|{text}|{u2['call']}|\r\n"
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
if u2['role'] != "INACTIVE":
|
||||
threading.Thread(target=self._schedule_reply, args=(u2['call'], u1['call']), daemon=True).start()
|
||||
|
||||
# MSG TO YOU
|
||||
if now - self.last_me > MSG_TO_USER_INTERVAL:
|
||||
self.last_me = now
|
||||
target_client = self.server.get_random_subscriber(self.id)
|
||||
if target_client and actives:
|
||||
if not target_client.call.startswith("GUEST"):
|
||||
sender = random.choice(actives)
|
||||
text = MessageFactory.get_chat_message(self.config["BANDS"], sender)
|
||||
print(f"[SIM Ch{self.id}] MSG TO YOU ({target_client.call})")
|
||||
self.process_msg(sender['call'], sender['name'], text, target_client.call)
|
||||
|
||||
# LOGIN/LOGOUT
|
||||
if now - self.last_login > LOGIN_LOGOUT_INTERVAL:
|
||||
self.last_login = now
|
||||
if random.choice(['IN', 'OUT']) == 'OUT' and len(self.online_users) > 20:
|
||||
cands = [c for c in self.online_users if c not in [p['call'] for p in self.config["PERMANENT"]]]
|
||||
if cands:
|
||||
l = random.choice(cands)
|
||||
del self.online_users[l]
|
||||
self.server.broadcast_to_channel(self.id, f"UR6|{self.id}|{l}|\r\n")
|
||||
else:
|
||||
candidates = [u for u in self.users_pool if u['call'] not in self.online_users]
|
||||
if candidates:
|
||||
n = random.choice(candidates)
|
||||
self.online_users[n['call']] = n
|
||||
self.server.broadcast_to_channel(self.id, f"UA5|{self.id}|{n['call']}|{n['name']}|{n['loc']}|2|\r\n")
|
||||
|
||||
def process_msg(self, sender, name, text, target):
|
||||
ts = str(int(time.time()))
|
||||
frame = f"CH|{self.id}|{ts}|{sender}|{name}|0|{text}|{target}|\r\n"
|
||||
if target == "0": self._add_hist(frame)
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
if target in self.online_users:
|
||||
threading.Thread(target=self._schedule_reply, args=(target, sender), daemon=True).start()
|
||||
|
||||
def _schedule_reply(self, sim_sender, real_target):
|
||||
if sim_sender not in self.online_users: return
|
||||
u = self.online_users[sim_sender]
|
||||
if u['role'] == "INACTIVE": return
|
||||
|
||||
time.sleep(random.uniform(2.0, 5.0))
|
||||
if sim_sender in self.online_users:
|
||||
text = MessageFactory.get_reply_msg(self.config["BANDS"], real_target, u['loc'])
|
||||
ts = str(int(time.time()))
|
||||
|
||||
if self.server.is_real_user(real_target):
|
||||
print(f"[REPLY Ch{self.id}] {sim_sender} -> {real_target}")
|
||||
|
||||
frame = f"CH|{self.id}|{ts}|{sim_sender}|{u['name']}|0|{text}|{real_target}|\r\n"
|
||||
self.server.broadcast_to_channel(self.id, frame)
|
||||
|
||||
def _add_hist(self, frame):
|
||||
self.history_chat.append(frame)
|
||||
if len(self.history_chat) > 50: self.history_chat.pop(0)
|
||||
|
||||
def get_full_init_blob(self):
|
||||
blob = ""
|
||||
for u in self.online_users.values():
|
||||
blob += f"UA0|{self.id}|{u['call']}|{u['name']}|{u['loc']}|0|\r\n"
|
||||
for h in self.history_chat: blob += h
|
||||
blob += f"UE|{self.id}|{len(self.online_users)}|\r\n"
|
||||
return blob.encode('latin-1', errors='replace')
|
||||
|
||||
# ==========================================
|
||||
# SERVER
|
||||
# ==========================================
|
||||
|
||||
class KSTServerV31:
|
||||
def __init__(self):
|
||||
self.lock = threading.Lock()
|
||||
self.running = True
|
||||
self.clients = {}
|
||||
self.channels = {}
|
||||
|
||||
for cid, cfg in CHANNELS_SETUP.items():
|
||||
self.channels[cid] = ChannelInstance(cid, cfg, self)
|
||||
|
||||
def start(self):
|
||||
threading.Thread(target=self._sim_loop, daemon=True).start()
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
try:
|
||||
s.bind((HOST, PORT))
|
||||
s.listen(5)
|
||||
s.settimeout(1.0)
|
||||
print(f"[*] ON4KST V31 (Stable Frequencies) running on {HOST}:{PORT}")
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
sock, addr = s.accept()
|
||||
print(f"[*] CONNECT: {addr}")
|
||||
threading.Thread(target=self._handle_client, args=(sock,), daemon=True).start()
|
||||
except socket.timeout: continue
|
||||
except OSError: break
|
||||
except KeyboardInterrupt:
|
||||
print("\n[!] Stop.")
|
||||
finally:
|
||||
self.running = False
|
||||
try: s.close()
|
||||
except: pass
|
||||
|
||||
def _handle_client(self, sock):
|
||||
client_obj = ConnectedClient(sock, None)
|
||||
with self.lock:
|
||||
self.clients[sock] = client_obj
|
||||
|
||||
buffer = ""
|
||||
try:
|
||||
while self.running:
|
||||
try: data = sock.recv(2048)
|
||||
except: break
|
||||
if not data: break
|
||||
|
||||
buffer += data.decode('latin-1', errors='replace')
|
||||
while '\n' in buffer:
|
||||
line, buffer = buffer.split('\n', 1)
|
||||
line = line.strip()
|
||||
if not line: continue
|
||||
|
||||
parts = line.split('|')
|
||||
cmd = parts[0]
|
||||
|
||||
if cmd == 'LOGIN' or cmd == 'LOGINC':
|
||||
if len(parts) > 1:
|
||||
client_obj.call = parts[1].strip().upper()
|
||||
print(f"[LOGIN] {client_obj.call} (Ch 2)")
|
||||
|
||||
client_obj.send_safe(f"LOGSTAT|100|2|PySimV31|KEY|Conf|3|\r\n")
|
||||
if cmd == 'LOGIN':
|
||||
self._send_channel_init(client_obj, "2")
|
||||
|
||||
elif cmd == 'SDONE':
|
||||
self._send_channel_init(client_obj, "2")
|
||||
|
||||
elif cmd.startswith('ACHAT'):
|
||||
if len(parts) >= 2:
|
||||
new_chan = parts[1]
|
||||
if new_chan in self.channels:
|
||||
client_obj.channels.add(new_chan)
|
||||
print(f"[ACHAT] {client_obj.call} -> Ch {new_chan}")
|
||||
self._send_channel_init(client_obj, new_chan)
|
||||
|
||||
elif cmd == 'MSG':
|
||||
if len(parts) >= 4:
|
||||
cid = parts[1]
|
||||
target = parts[2]
|
||||
text = parts[3]
|
||||
if text.lower().startswith("/cq"):
|
||||
spl = text.split(' ', 2)
|
||||
if len(spl) >= 3:
|
||||
target = spl[1]; text = spl[2]
|
||||
if cid in self.channels:
|
||||
self.channels[cid].process_msg(client_obj.call, "Me", text, target)
|
||||
|
||||
elif cmd == 'CK': pass
|
||||
except Exception as e:
|
||||
print(f"[!] Err: {e}")
|
||||
finally:
|
||||
with self.lock:
|
||||
if sock in self.clients: del self.clients[sock]
|
||||
client_obj.close()
|
||||
|
||||
def _send_channel_init(self, client_obj, cid):
|
||||
if cid in self.channels:
|
||||
full_blob = self.channels[cid].get_full_init_blob()
|
||||
client_obj.send_safe(full_blob.decode('latin-1'))
|
||||
|
||||
def broadcast_to_channel(self, cid, frame):
|
||||
now = time.time()
|
||||
with self.lock:
|
||||
targets = list(self.clients.values())
|
||||
|
||||
for c in targets:
|
||||
if cid in c.channels:
|
||||
if now - c.login_time > CLIENT_WARMUP_TIME:
|
||||
c.send_safe(frame)
|
||||
|
||||
def get_random_subscriber(self, cid):
|
||||
with self.lock:
|
||||
subs = [c for c in self.clients.values() if cid in c.channels and not c.call.startswith("GUEST")]
|
||||
return random.choice(subs) if subs else None
|
||||
|
||||
def is_real_user(self, call):
|
||||
with self.lock:
|
||||
for c in self.clients.values():
|
||||
if c.call.upper() == call.upper() and not c.call.startswith("GUEST"):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _sim_loop(self):
|
||||
print("[*] Sim Loop running...")
|
||||
last_ka = time.time()
|
||||
while self.running:
|
||||
now = time.time()
|
||||
time.sleep(0.02)
|
||||
|
||||
for c in self.channels.values():
|
||||
c.tick(now)
|
||||
|
||||
if now - last_ka > KEEP_ALIVE_INTERVAL:
|
||||
last_ka = now
|
||||
self.broadcast_global("CK|\r\n")
|
||||
|
||||
def broadcast_global(self, frame):
|
||||
with self.lock:
|
||||
targets = list(self.clients.values())
|
||||
for c in targets:
|
||||
c.send_safe(frame)
|
||||
|
||||
if __name__ == '__main__':
|
||||
KSTServerV31().start()
|
||||
@@ -810,6 +810,24 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** Tracks the last time WE sent a message containing a QRG to a specific callsign (UPPERCASE).
|
||||
* Compared against knownActiveBands.timestampEpoch to decide whose QRG to use in a SKED. */
|
||||
private final Map<String, Long> lastSentQRGToCallsign =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** Call this whenever we send a PM to {@code receiverCallsign} that contains our QRG. */
|
||||
public void recordOutboundQRG(String receiverCallsign) {
|
||||
if (receiverCallsign == null) return;
|
||||
lastSentQRGToCallsign.put(receiverCallsign.trim().toUpperCase(), System.currentTimeMillis());
|
||||
System.out.println("[ChatController] Recorded outbound QRG to: " + receiverCallsign);
|
||||
}
|
||||
|
||||
/** Returns epoch-ms of when we last sent our QRG to this callsign, or 0 if never. */
|
||||
public long getLastSentQRGTimestamp(String callsign) {
|
||||
if (callsign == null) return 0L;
|
||||
return lastSentQRGToCallsign.getOrDefault(callsign.trim().toUpperCase(), 0L);
|
||||
}
|
||||
|
||||
private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
|
||||
private ScheduledExecutorService scoreScheduler;
|
||||
private final StationMetricsService stationMetricsService = new StationMetricsService();
|
||||
@@ -827,8 +845,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
});
|
||||
|
||||
// Push sked to Win-Test via UDP if enabled
|
||||
if (chatPreferences.isLogsynch_wintestNetworkSkedPushEnabled()
|
||||
&& chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
||||
if (chatPreferences.isLogsynch_wintestNetworkListenerEnabled()) {
|
||||
pushSkedToWinTest(sked);
|
||||
}
|
||||
}
|
||||
@@ -847,16 +864,69 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
|
||||
WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this);
|
||||
|
||||
// Get current frequency from QRG property (set by Win-Test STATUS or user)
|
||||
double freqKHz = 144300.0; // fallback default
|
||||
try {
|
||||
String qrgStr = chatPreferences.getMYQRGFirstCat().get();
|
||||
if (qrgStr != null && !qrgStr.isBlank()) {
|
||||
// QRG is in display format like "144.300.00" – strip dots → "14430000" → / 100 → 144300.0 kHz
|
||||
String cleaned = qrgStr.trim().replace(".", "");
|
||||
freqKHz = Double.parseDouble(cleaned) / 100.0;
|
||||
// Frequency resolution:
|
||||
// Compare WHO sent a QRG most recently in the PM conversation:
|
||||
// - OM sent their QRG last → use OM's Last Known QRG (ChatMember.frequency)
|
||||
// - WE sent our QRG last → use our own Win-Test QRG (MYQRG)
|
||||
// Fallback chain if no timestamps exist: OM's Last Known QRG → hardcoded default
|
||||
double freqKHz = -1.0;
|
||||
final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes
|
||||
|
||||
ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign());
|
||||
|
||||
// Collect timestamps: when did the OM last mention their QRG? When did WE last send ours?
|
||||
long omLastQRGTimestamp = 0L;
|
||||
double omLastQRGMhz = 0.0;
|
||||
if (targetMember != null && sked.getBand() != null) {
|
||||
ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand());
|
||||
if (fi != null && fi.frequency > 0
|
||||
&& (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) {
|
||||
omLastQRGTimestamp = fi.timestampEpoch;
|
||||
omLastQRGMhz = fi.frequency;
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
long ourLastQRGTimestamp = getLastSentQRGTimestamp(sked.getTargetCallsign());
|
||||
|
||||
// Decision: who was more recent?
|
||||
if (omLastQRGTimestamp > 0 && omLastQRGTimestamp >= ourLastQRGTimestamp) {
|
||||
// OM mentioned their QRG MORE RECENTLY (or at same time) → use their QRG
|
||||
freqKHz = omLastQRGMhz * 1000.0;
|
||||
System.out.println("[ChatController] SKED freq: OM sent last → "
|
||||
+ omLastQRGMhz + " MHz → " + freqKHz + " kHz");
|
||||
|
||||
} else if (ourLastQRGTimestamp > 0) {
|
||||
// WE sent our QRG more recently → use our Win-Test QRG
|
||||
try {
|
||||
String qrgStr = chatPreferences.getMYQRGFirstCat().get();
|
||||
if (qrgStr != null && !qrgStr.isBlank()) {
|
||||
String cleaned = qrgStr.trim().replace(".", "");
|
||||
double parsed = Double.parseDouble(cleaned) / 100.0;
|
||||
if (parsed > 50000) {
|
||||
freqKHz = parsed;
|
||||
System.out.println("[ChatController] SKED freq: WE sent last → "
|
||||
+ freqKHz + " kHz (raw: " + qrgStr + ")");
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
|
||||
// Fallback A: OM's Last Known QRG from KST field (if no PM QRG exchange found at all)
|
||||
if (freqKHz < 0 && targetMember != null) {
|
||||
try {
|
||||
String memberQrg = targetMember.getFrequency().get();
|
||||
if (memberQrg != null && !memberQrg.isBlank()) {
|
||||
double mhz = Double.parseDouble(memberQrg.trim());
|
||||
freqKHz = mhz * 1000.0;
|
||||
System.out.println("[ChatController] SKED freq: fallback Last Known QRG → "
|
||||
+ mhz + " MHz → " + freqKHz + " kHz");
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
|
||||
// Fallback B: hardcoded default
|
||||
if (freqKHz < 0) {
|
||||
freqKHz = 144300.0;
|
||||
}
|
||||
|
||||
// Build notes string with target locator/azimuth info like reference: [JO02OB - 279°]
|
||||
String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign());
|
||||
@@ -883,6 +953,22 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
}, "WinTestSkedPush").start();
|
||||
}
|
||||
|
||||
private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
||||
synchronized (getLst_chatMemberList()) {
|
||||
for (ChatMember member : getLst_chatMemberList()) {
|
||||
if (member == null || member.getCallSignRaw() == null) continue;
|
||||
if (normalizeCallRaw(member.getCallSignRaw()).equals(normalizedTargetCall)) {
|
||||
return member;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
return null;
|
||||
|
||||
@@ -962,6 +962,13 @@ public class MessageBusManagementThread extends Thread {
|
||||
.setMessageText("(>" + newMessageArrived.getReceiver().getCallSign() + ")" + originalMessage);
|
||||
this.client.getLst_globalChatMessageList().add(0,newMessageArrived);
|
||||
|
||||
// If our message contained a frequency (e.g. "QRG is: 144.375"), record that
|
||||
// WE sent our QRG to this OM – used by SKED frequency resolution.
|
||||
if (originalMessage != null && newMessageArrived.getReceiver() != null
|
||||
&& originalMessage.matches(".*\\b\\d{3,5}[.,]\\d{1,3}.*")) {
|
||||
this.client.recordOutboundQRG(newMessageArrived.getReceiver().getCallSign());
|
||||
}
|
||||
|
||||
// if you sent the message to another station, it will be sorted in to
|
||||
// the "to me message list" with modified messagetext, added rxers callsign
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import kst4contest.ApplicationConstants;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
@@ -75,9 +76,10 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
socket = new DatagramSocket(null); //first init with null, then make ready for reuse
|
||||
socket.setReuseAddress(true);
|
||||
// socket = new DatagramSocket(PORT);
|
||||
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_wintestNetworkPort()));
|
||||
int boundPort = client.getChatPreferences().getLogsynch_wintestNetworkPort();
|
||||
socket.bind(new InetSocketAddress(boundPort));
|
||||
socket.setSoTimeout(3000);
|
||||
System.out.println("[WinTest UDP listener] started at port: " + PORT);
|
||||
System.out.println("[WinTest UDP listener] started at port: " + boundPort);
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
@@ -224,9 +226,43 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
} else {
|
||||
formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback
|
||||
}
|
||||
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
|
||||
// Parse pass frequency from parts[11] if available (WT STATUS format)
|
||||
String formattedPassQRG = null;
|
||||
if (parts.size() > 11) {
|
||||
try {
|
||||
String passFreqRaw = parts.get(11);
|
||||
double passFreqFloat = Integer.parseInt(passFreqRaw) / 10.0;
|
||||
if (passFreqFloat > 100) { // Must be a valid radio frequency (> 100 kHz), protects against parsing boolean flag tokens
|
||||
long passFreqHzTimes100 = Math.round(passFreqFloat * 100.0);
|
||||
String passHzStr = String.valueOf(passFreqHzTimes100);
|
||||
if (passHzStr.length() == 8) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 3), passHzStr.substring(3, 6), passHzStr.substring(6, 8));
|
||||
} else if (passHzStr.length() == 9) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 4), passHzStr.substring(4, 7), passHzStr.substring(7, 9));
|
||||
} else if (passHzStr.length() == 7) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 2), passHzStr.substring(2, 5), passHzStr.substring(5, 7));
|
||||
} else if (passHzStr.length() == 6) {
|
||||
formattedPassQRG = String.format("%s.%s.%s", passHzStr.substring(0, 1), passHzStr.substring(1, 4), passHzStr.substring(4, 6));
|
||||
} else {
|
||||
formattedPassQRG = String.format(Locale.US, "%.1f", passFreqFloat);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// parts[11] not a valid frequency, leave formattedPassQRG as null
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG);
|
||||
if (this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) {
|
||||
final String qrgToSet = (this.client.getChatPreferences().isLogsynch_wintestUsePassQrg() && formattedPassQRG != null)
|
||||
? formattedPassQRG
|
||||
: formattedQRG;
|
||||
// JavaFX StringProperty must be updated on the FX Application Thread
|
||||
Platform.runLater(() -> this.client.getChatPreferences().getMYQRGFirstCat().set(qrgToSet));
|
||||
}
|
||||
|
||||
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG
|
||||
+ (formattedPassQRG != null ? ", passQrg=" + formattedPassQRG : "")
|
||||
+ ", syncActive=" + this.client.getChatPreferences().isLogsynch_wintestQrgSyncEnabled());
|
||||
} catch (Exception e) {
|
||||
System.out.println("[WinTest] STATUS parsing error: " + e.getMessage());
|
||||
}
|
||||
|
||||
@@ -204,6 +204,8 @@ public class ChatPreferences {
|
||||
String logsynch_wintestNetworkBroadcastAddress = "255.255.255.255"; // UDP broadcast address for sending to Win-Test
|
||||
boolean logsynch_wintestNetworkSkedPushEnabled = false; // push SKEDs to Win-Test via UDP
|
||||
String logsynch_wintestSkedMode = "SSB"; // CW, SSB or AUTO
|
||||
boolean logsynch_wintestQrgSyncEnabled = true; // sync QRG from Win-Test STATUS packet
|
||||
boolean logsynch_wintestUsePassQrg = false; // use pass frequency instead of main QRG from STATUS packet
|
||||
|
||||
|
||||
|
||||
@@ -481,6 +483,22 @@ public class ChatPreferences {
|
||||
this.logsynch_wintestSkedMode = logsynch_wintestSkedMode;
|
||||
}
|
||||
|
||||
public boolean isLogsynch_wintestQrgSyncEnabled() {
|
||||
return logsynch_wintestQrgSyncEnabled;
|
||||
}
|
||||
|
||||
public void setLogsynch_wintestQrgSyncEnabled(boolean logsynch_wintestQrgSyncEnabled) {
|
||||
this.logsynch_wintestQrgSyncEnabled = logsynch_wintestQrgSyncEnabled;
|
||||
}
|
||||
|
||||
public boolean isLogsynch_wintestUsePassQrg() {
|
||||
return logsynch_wintestUsePassQrg;
|
||||
}
|
||||
|
||||
public void setLogsynch_wintestUsePassQrg(boolean logsynch_wintestUsePassQrg) {
|
||||
this.logsynch_wintestUsePassQrg = logsynch_wintestUsePassQrg;
|
||||
}
|
||||
|
||||
public String getStn_loginLocatorSecondCat() {
|
||||
return stn_loginLocatorSecondCat;
|
||||
}
|
||||
@@ -1338,6 +1356,14 @@ public class ChatPreferences {
|
||||
logsynch_wintestSkedMode.setTextContent(this.logsynch_wintestSkedMode);
|
||||
logsynch.appendChild(logsynch_wintestSkedMode);
|
||||
|
||||
Element logsynch_wintestQrgSyncEnabled = doc.createElement("logsynch_wintestQrgSyncEnabled");
|
||||
logsynch_wintestQrgSyncEnabled.setTextContent(this.logsynch_wintestQrgSyncEnabled + "");
|
||||
logsynch.appendChild(logsynch_wintestQrgSyncEnabled);
|
||||
|
||||
Element logsynch_wintestUsePassQrg = doc.createElement("logsynch_wintestUsePassQrg");
|
||||
logsynch_wintestUsePassQrg.setTextContent(this.logsynch_wintestUsePassQrg + "");
|
||||
logsynch.appendChild(logsynch_wintestUsePassQrg);
|
||||
|
||||
|
||||
/**
|
||||
* trxSynchUCX
|
||||
@@ -1912,6 +1938,16 @@ public class ChatPreferences {
|
||||
logsynch_wintestSkedMode,
|
||||
"logsynch_wintestSkedMode");
|
||||
|
||||
logsynch_wintestQrgSyncEnabled = getBoolean(
|
||||
logsynchEl,
|
||||
logsynch_wintestQrgSyncEnabled,
|
||||
"logsynch_wintestQrgSyncEnabled");
|
||||
|
||||
logsynch_wintestUsePassQrg = getBoolean(
|
||||
logsynchEl,
|
||||
logsynch_wintestUsePassQrg,
|
||||
"logsynch_wintestUsePassQrg");
|
||||
|
||||
System.out.println(
|
||||
"[ChatPreferences, info]: file based worked-call interpreter: " + logsynch_fileBasedWkdCallInterpreterEnabled);
|
||||
System.out.println(
|
||||
|
||||
@@ -3582,7 +3582,57 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
Menu fileMenu = new Menu("File");
|
||||
|
||||
// create menuitems
|
||||
// build "Connect to <configured chat>" label from saved preferences
|
||||
ChatCategory mainCat = chatcontroller.getChatPreferences().getLoginChatCategoryMain();
|
||||
String connectLabel = "Connect to " + mainCat.getChatCategoryName(mainCat.getCategoryNumber());
|
||||
if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()) {
|
||||
ChatCategory secCat = chatcontroller.getChatPreferences().getLoginChatCategorySecond();
|
||||
if (secCat != null) {
|
||||
connectLabel += " & " + secCat.getChatCategoryName(secCat.getCategoryNumber());
|
||||
}
|
||||
}
|
||||
menuItemFileConnect = new MenuItem(connectLabel);
|
||||
menuItemFileConnect.setDisable(false);
|
||||
|
||||
if (chatcontroller.isConnectedAndLoggedIn() || chatcontroller.isConnectedAndNOTLoggedIn()) {
|
||||
menuItemFileConnect.setDisable(true);
|
||||
}
|
||||
|
||||
menuItemFileConnect.setOnAction(event -> {
|
||||
System.out.println("[Info] File menu: Connect clicked, using saved preferences");
|
||||
|
||||
String call = chatcontroller.getChatPreferences().getStn_loginCallSign();
|
||||
String pass = chatcontroller.getChatPreferences().getStn_loginPassword();
|
||||
|
||||
if (call == null || call.isBlank() || pass == null || pass.isBlank()) {
|
||||
Alert alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Cannot connect");
|
||||
alert.setHeaderText("Login credentials missing");
|
||||
alert.setContentText("Please configure your callsign and password in Settings first.");
|
||||
alert.show();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
chatcontroller.execute();
|
||||
|
||||
menuItemFileConnect.setDisable(true);
|
||||
menuItemFileDisconnect.setDisable(false);
|
||||
menuItemOptionsAwayBack.setDisable(false);
|
||||
menuItemOptionsSetFrequencyAsName.setDisable(false);
|
||||
|
||||
chatcontroller.setConnectedAndLoggedIn(true);
|
||||
chatcontroller.setDisconnected(false);
|
||||
|
||||
} catch (InterruptedException | IOException e) {
|
||||
e.printStackTrace();
|
||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||
alert.setTitle("Connection failed");
|
||||
alert.setContentText("Could not connect: " + e.getMessage());
|
||||
alert.show();
|
||||
}
|
||||
});
|
||||
|
||||
menuItemFileDisconnect = new MenuItem("Disconnect");
|
||||
menuItemFileDisconnect.setDisable(true);
|
||||
|
||||
@@ -3595,6 +3645,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
public void handle(ActionEvent event) {
|
||||
chatcontroller.disconnect(ApplicationConstants.DISCSTRING_DISCONNECTONLY);
|
||||
menuItemFileDisconnect.setDisable(true);
|
||||
menuItemFileConnect.setDisable(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -3607,6 +3658,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
});
|
||||
|
||||
// add menu items to menu
|
||||
fileMenu.getItems().add(menuItemFileConnect);
|
||||
fileMenu.getItems().add(menuItemFileDisconnect);
|
||||
fileMenu.getItems().add(m10);
|
||||
|
||||
@@ -4010,6 +4062,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
Scene clusterAndQSOMonScene;
|
||||
Scene settingsScene;
|
||||
|
||||
MenuItem menuItemFileConnect;
|
||||
MenuItem menuItemFileDisconnect;
|
||||
MenuItem menuItemOptionsAwayBack;
|
||||
|
||||
@@ -4170,10 +4223,15 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
timer_updatePrivatemessageTable.purge();
|
||||
timer_updatePrivatemessageTable.cancel();
|
||||
chatcontroller.disconnect("CLOSEALL");
|
||||
|
||||
try {
|
||||
chatcontroller.disconnect("CLOSEALL");
|
||||
} catch (Exception e) {
|
||||
System.out.println("[Main.java, Warning:] Exception during disconnect: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Platform.exit();
|
||||
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
private Queue<Media> musicList = new LinkedList<Media>();
|
||||
@@ -5382,7 +5440,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
FlowPane chatMemberTableFilterQRBHBox = new FlowPane();
|
||||
chatMemberTableFilterQRBHBox.setAlignment(Pos.CENTER_LEFT);
|
||||
chatMemberTableFilterQRBHBox.setHgap(2);
|
||||
chatMemberTableFilterQRBHBox.setPrefWidth(210);
|
||||
chatMemberTableFilterQRBHBox.setPrefWidth(225);
|
||||
|
||||
TextField chatMemberTableFilterMaxQrbTF = new TextField(chatcontroller.getChatPreferences().getStn_maxQRBDefault() + "");
|
||||
chatMemberTableFilterMaxQrbTF.setFocusTraversable(false);
|
||||
@@ -5431,7 +5489,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
// HBox chatMemberTableFilterQTFHBox = new HBox();
|
||||
FlowPane chatMemberTableFilterQTFHBox = new FlowPane();
|
||||
chatMemberTableFilterQTFHBox.setAlignment(Pos.CENTER_LEFT);
|
||||
chatMemberTableFilterQTFHBox.setPrefWidth(490);
|
||||
chatMemberTableFilterQTFHBox.setPrefWidth(525);
|
||||
chatMemberTableFilterQTFHBox.setHgap(2);
|
||||
|
||||
CheckBox chatMemberTableFilterQtfEnableChkbx = new CheckBox("Show only QTF:");
|
||||
@@ -6212,7 +6270,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
*
|
||||
****************************************************************************/
|
||||
settingsStage = new Stage();
|
||||
settingsStage.setTitle("Change Client seetings");
|
||||
settingsStage.setTitle("Change Client Settings");
|
||||
|
||||
BorderPane optionsPanel = new BorderPane();
|
||||
|
||||
@@ -6273,11 +6331,14 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
});
|
||||
|
||||
boolean isSecondChatEnabled = this.chatcontroller.getChatPreferences().isLoginToSecondChatEnabled();
|
||||
Label lblNameSecondCat = new Label("Name in Chat 2:");
|
||||
lblNameSecondCat.setVisible(false);
|
||||
lblNameSecondCat.setVisible(isSecondChatEnabled);
|
||||
lblNameSecondCat.setDisable(!isSecondChatEnabled);
|
||||
TextField txtFldNameInChatSecondCat = new TextField(this.chatcontroller.getChatPreferences().getStn_loginNameSecondCat());
|
||||
txtFldNameInChatSecondCat.setFocusTraversable(false);
|
||||
txtFldNameInChatSecondCat.setVisible(false);
|
||||
txtFldNameInChatSecondCat.setVisible(isSecondChatEnabled);
|
||||
txtFldNameInChatSecondCat.setDisable(!isSecondChatEnabled);
|
||||
|
||||
txtFldNameInChatSecondCat.textProperty().addListener(new ChangeListener<String>() {
|
||||
|
||||
@@ -6397,11 +6458,12 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
|
||||
CheckBox station_chkBxEnableSecondChat = new CheckBox("2nd Chat: ");
|
||||
station_chkBxEnableSecondChat.setSelected(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
boolean isSecondChatEnabledForCheckbox = chatcontroller.getChatPreferences().isLoginToSecondChatEnabled();
|
||||
station_chkBxEnableSecondChat.setSelected(isSecondChatEnabledForCheckbox);
|
||||
|
||||
|
||||
|
||||
stn_choiceBxChatChategorySecond.setDisable(true);
|
||||
stn_choiceBxChatChategorySecond.setDisable(!isSecondChatEnabledForCheckbox);
|
||||
station_chkBxEnableSecondChat.selectedProperty().addListener(new ChangeListener<Boolean>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
@@ -6431,12 +6493,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
});
|
||||
|
||||
if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled()) {
|
||||
stn_choiceBxChatChategorySecond.setVisible(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
stn_choiceBxChatChategorySecond.setDisable(!chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
txtFldNameInChatSecondCat.setVisible(chatcontroller.getChatPreferences().isLoginToSecondChatEnabled());
|
||||
|
||||
}
|
||||
|
||||
TextField txtFldstn_antennaBeamWidthDeg = new TextField(this.chatcontroller.getChatPreferences().getStn_antennaBeamWidthDeg() + "");
|
||||
txtFldstn_antennaBeamWidthDeg.setFocusTraversable(false);
|
||||
@@ -6667,7 +6724,6 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
grdPnlStation_bands.add(settings_chkbx_QRV3400, 1, 2);
|
||||
grdPnlStation_bands.add(settings_chkbx_QRV5600, 2, 2);
|
||||
grdPnlStation_bands.add(settings_chkbx_QRV10G, 0, 3);
|
||||
grdPnlStation_bands.setMaxWidth(555.0);
|
||||
|
||||
grdPnlStation_bands.setStyle(" -fx-border-color: lightgray;\n" +
|
||||
" -fx-vgap: 5;\n" +
|
||||
@@ -6882,15 +6938,32 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
grdPnlLog.add(lblUDPByWintest, 0, 8);
|
||||
grdPnlLog.add(txtFldUDPPortforWintest, 1, 8);
|
||||
|
||||
// --- Win-Test SKED push settings ---
|
||||
Label lblEnableSkedPush = new Label("Push SKEDs to Win-Test via UDP (ADDSKED)");
|
||||
CheckBox chkBxEnableSkedPush = new CheckBox();
|
||||
chkBxEnableSkedPush.setSelected(
|
||||
this.chatcontroller.getChatPreferences().isLogsynch_wintestNetworkSkedPushEnabled()
|
||||
// --- QRG sync from Win-Test STATUS ---
|
||||
Label lblWtQrgSync = new Label("Win-Test STATUS QRG Sync (updates own QRG from Win-Test transceiver frequency)");
|
||||
CheckBox chkBxWtQrgSync = new CheckBox();
|
||||
chkBxWtQrgSync.setSelected(
|
||||
this.chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()
|
||||
);
|
||||
chkBxEnableSkedPush.selectedProperty().addListener((obs, oldVal, newVal) -> {
|
||||
chatcontroller.getChatPreferences().setLogsynch_wintestNetworkSkedPushEnabled(newVal);
|
||||
System.out.println("[Main.java, Info]: Win-Test SKED push enabled: " + newVal);
|
||||
chkBxWtQrgSync.selectedProperty().addListener((obs, oldVal, newVal) -> {
|
||||
chatcontroller.getChatPreferences().setLogsynch_wintestQrgSyncEnabled(newVal);
|
||||
System.out.println("[Main.java, Info]: Win-Test QRG sync enabled: " + newVal);
|
||||
boolean anyActive = chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled() || newVal;
|
||||
if (!anyActive) {
|
||||
txt_ownqrgMainCategory.textProperty().unbind();
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by hand (watch prefs!)"));
|
||||
} else {
|
||||
txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat());
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)"));
|
||||
}
|
||||
});
|
||||
Label lblWtUsePassQrg = new Label("Use pass frequency from Win-Test STATUS (instead of own QRG)");
|
||||
CheckBox chkBxWtUsePassQrg = new CheckBox();
|
||||
chkBxWtUsePassQrg.setSelected(
|
||||
this.chatcontroller.getChatPreferences().isLogsynch_wintestUsePassQrg()
|
||||
);
|
||||
chkBxWtUsePassQrg.selectedProperty().addListener((obs, oldVal, newVal) -> {
|
||||
chatcontroller.getChatPreferences().setLogsynch_wintestUsePassQrg(newVal);
|
||||
System.out.println("[Main.java, Info]: Win-Test use pass QRG: " + newVal);
|
||||
});
|
||||
|
||||
Label lblWtStationName = new Label("KST station name in Win-Test network (src of SKED packets)");
|
||||
@@ -6935,13 +7008,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
}
|
||||
});
|
||||
|
||||
grdPnlLog.add(lblEnableSkedPush, 0, 9);
|
||||
grdPnlLog.add(chkBxEnableSkedPush, 1, 9);
|
||||
|
||||
grdPnlLog.add(lblWtStationName, 0, 11);
|
||||
grdPnlLog.add(txtFldWtStationName, 1, 11);
|
||||
grdPnlLog.add(lblWtStationFilter, 0, 12);
|
||||
grdPnlLog.add(txtFldWtStationFilter, 1, 12);
|
||||
grdPnlLog.add(lblWtStationName, 0, 9);
|
||||
grdPnlLog.add(txtFldWtStationName, 1, 9);
|
||||
|
||||
// Auto-detect subnet broadcast if preference is still the default
|
||||
String currentBroadcast = this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress();
|
||||
@@ -6959,8 +7027,8 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
// Re-read (may have been auto-detected)
|
||||
txtFldWtBroadcastAddr.setText(this.chatcontroller.getChatPreferences().getLogsynch_wintestNetworkBroadcastAddress());
|
||||
|
||||
grdPnlLog.add(lblWtBroadcastAddr, 0, 13);
|
||||
grdPnlLog.add(txtFldWtBroadcastAddr, 1, 13);
|
||||
grdPnlLog.add(lblWtBroadcastAddr, 0, 10);
|
||||
grdPnlLog.add(txtFldWtBroadcastAddr, 1, 10);
|
||||
|
||||
VBox vbxLog = new VBox();
|
||||
vbxLog.setPadding(new Insets(10, 10, 10, 10));
|
||||
@@ -6995,51 +7063,45 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
chkBxEnableTRXMsgbyUCX.selectedProperty().addListener(new ChangeListener<Boolean>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
// chk2.setSelected(!newValue);
|
||||
if (!newValue) {
|
||||
chatcontroller.getChatPreferences()
|
||||
.setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected());
|
||||
chatcontroller.getChatPreferences().setTrxSynch_ucxLogUDPListenerEnabled(newValue);
|
||||
boolean anyActive = newValue || chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled();
|
||||
if (!anyActive) {
|
||||
txt_ownqrgMainCategory.textProperty().unbind();
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by hand (watch prefs!)"));
|
||||
System.out.println("[Main.java, Info]: MYQRG will be changed only by User input");
|
||||
System.out.println("[Main.java, Info]: setted the trx-frequency updated by ucxlog to: "
|
||||
+ chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled());
|
||||
|
||||
} else {
|
||||
chatcontroller.getChatPreferences()
|
||||
.setTrxSynch_ucxLogUDPListenerEnabled(chkBxEnableTRXMsgbyUCX.isSelected());
|
||||
txt_ownqrgMainCategory.textProperty().bind(chatcontroller.getChatPreferences().getMYQRGFirstCat());
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)"));
|
||||
System.out.println("[Main.java, Info]: setted the trx-frequency updated by ucxlog to: "
|
||||
+ chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Thats the default behaviour of the myqrg textfield
|
||||
if (this.chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled()) {
|
||||
// Unconditionally add listener to manually sync the textfield input to the button
|
||||
// (this listener also fires correctly when the value is updated by the binding)
|
||||
txt_ownqrgMainCategory.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
MYQRGButton.textProperty().set(newValue);
|
||||
});
|
||||
|
||||
// That's the default behaviour of the myqrg textfield
|
||||
if (this.chatcontroller.getChatPreferences().isTrxSynch_ucxLogUDPListenerEnabled() || this.chatcontroller.getChatPreferences().isLogsynch_wintestQrgSyncEnabled()) {
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("Your cq qrg will be updated by the log program (watch prefs!)"));
|
||||
txt_ownqrgMainCategory.textProperty().bind(this.chatcontroller.getChatPreferences().getMYQRGFirstCat());// TODO: Bind darf nur
|
||||
// gemacht werden, wenn
|
||||
// ucxlog-Frequenznachrichten
|
||||
// ausgewerttet werden!
|
||||
// System.out.println("[Main.java, Info]: MYQRG will be changed only by UCXListener");
|
||||
txt_ownqrgMainCategory.textProperty().bind(this.chatcontroller.getChatPreferences().getMYQRGFirstCat());
|
||||
} else {
|
||||
txt_ownqrgMainCategory.setTooltip(new Tooltip("enter your cq qrg here"));
|
||||
// System.out.println("[Main.java, Info]: MYQRG will be changed only by User input");
|
||||
txt_ownqrgMainCategory.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
System.out.println(
|
||||
"[Main.java, Info]: MYQRG Text changed from " + oldValue + " to " + newValue + " by hand");
|
||||
MYQRGButton.textProperty().set(newValue);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
grdPnltrx.add(generateLabeledSeparator(100, "Receive UCXLog TRX info"), 0, 0, 2, 1);
|
||||
grdPnltrx.add(lblEnableTRXMsgbyUCX, 0, 1);
|
||||
grdPnltrx.add(chkBxEnableTRXMsgbyUCX, 1, 1);
|
||||
|
||||
grdPnltrx.add(generateLabeledSeparator(100, "Win-Test TRX sync"), 0, 2, 2, 1);
|
||||
grdPnltrx.add(lblWtQrgSync, 0, 3);
|
||||
grdPnltrx.add(chkBxWtQrgSync, 1, 3);
|
||||
grdPnltrx.add(lblWtUsePassQrg, 0, 4);
|
||||
grdPnltrx.add(chkBxWtUsePassQrg, 1, 4);
|
||||
grdPnltrx.add(lblWtStationFilter, 0, 5);
|
||||
grdPnltrx.add(txtFldWtStationFilter, 1, 5);
|
||||
|
||||
VBox vbxTRXSynch = new VBox();
|
||||
vbxTRXSynch.setPadding(new Insets(10, 10, 10, 10));
|
||||
vbxTRXSynch.getChildren().addAll(grdPnltrx);
|
||||
@@ -8124,6 +8186,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
else if (chatcontroller.isConnectedAndLoggedIn()) {
|
||||
btnOptionspnlDisconnectOnly.setDisable(false);
|
||||
menuItemFileDisconnect.setDisable(false);
|
||||
menuItemFileConnect.setDisable(true);
|
||||
menuItemOptionsAwayBack.setDisable(false);
|
||||
}
|
||||
|
||||
@@ -8147,13 +8210,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
txtFldstn_maxQRBDefault.setDisable(false);
|
||||
menuItemOptionsSetFrequencyAsName.setDisable(true);
|
||||
menuItemOptionsAwayBack.setDisable(true);
|
||||
menuItemFileConnect.setDisable(false);
|
||||
station_chkBxEnableSecondChat.setDisable(false);
|
||||
stn_choiceBxChatChategorySecond.setDisable(false);
|
||||
}
|
||||
});
|
||||
|
||||
btnOptionspnlConnect = new Button("Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain()
|
||||
.getChatCategoryName(choiceBxChatChategory.getSelectionModel().getSelectedItem().getCategoryNumber()));
|
||||
String btnText = "Connect to " + chatcontroller.getChatPreferences().getLoginChatCategoryMain()
|
||||
.getChatCategoryName(choiceBxChatChategory.getSelectionModel().getSelectedItem().getCategoryNumber());
|
||||
ChatCategory secCat = chatcontroller.getChatPreferences().getLoginChatCategorySecond();
|
||||
if (chatcontroller.getChatPreferences().isLoginToSecondChatEnabled() && secCat != null) {
|
||||
btnText += " & " + secCat.getChatCategoryName(secCat.getCategoryNumber());
|
||||
}
|
||||
btnOptionspnlConnect = new Button(btnText);
|
||||
btnOptionspnlConnect.setOnAction(new EventHandler<ActionEvent>() {
|
||||
@Override
|
||||
public void handle(ActionEvent event) {
|
||||
@@ -8185,6 +8254,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
btnOptionspnlDisconnectOnly.setDisable(false);
|
||||
menuItemFileDisconnect.setDisable(false);
|
||||
menuItemFileConnect.setDisable(true);
|
||||
menuItemOptionsAwayBack.setDisable(false);
|
||||
menuItemOptionsSetFrequencyAsName.setDisable(false);
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module praktiKST {
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires javafx.web;
|
||||
requires jdk.xml.dom;
|
||||
requires java.sql;
|
||||
requires javafx.media;
|
||||
|
||||
Reference in New Issue
Block a user