33 Commits

Author SHA1 Message Date
aaa5c1088a fix Wrapping of Buttons in Linux when wrapping is not needed 2026-03-28 23:58:45 +01:00
178783aa8c fix Typo and add in JavaFX as requirement 2026-03-28 23:12:47 +01:00
68d171e793 fix Nightly Documentation 2026-03-27 23:16:59 +01:00
e4501e848a UpdateWiki 27.03.2026 2026-03-27 23:08:33 +01:00
b16dd1303a Added recommended minimal Resolution of Display 2026-03-27 23:08:33 +01:00
e5b30c3049 README.MD Updated 2026-03-27 01:23:18 +01:00
318f3720b8 PDF User Manual Pipeline for use in Release and nightly Build 2026-03-27 01:23:18 +01:00
db031bb5e3 Docs EN changed to include new Changes 2026-03-26 01:50:49 +01:00
d635ee3fef Doku DE changed to include newest features. 2026-03-26 01:50:49 +01:00
f93f3acbb0 Wiki Sync Pipelines
Wiki Sync fix

Wiki no longer empty commits

Add files via upload

a
2026-03-26 00:28:00 +01:00
9ec17332e2 pre-Release-Pipeline 2026-03-26 00:28:00 +01:00
6716751697 fix nightly pipelines 2026-03-26 00:28:00 +01:00
d5b8508aa6 Win-Test: improve broadcast defaults and SKED workflow
Co-authored-by: GitHub Copilot <github-copilot[bot]@users.noreply.github.com>
2026-03-25 23:35:35 +01:00
c0b8aa61a9 Add Win-Test SKED push via UDP (ported from wtKST)
Implements sending SKEDs to Win-Test via the LOCKSKED/ADDSKED/UNLOCKSKED
UDP protocol sequence, ported from the C# wtSked class in wtKST.

New files:
- WinTestMessage.java: Win-Test network message format with checksum
- WinTestSkedSender.java: UDP broadcast sender for SKED messages

Modified:
- ChatController: addSked() now pushes to Win-Test when enabled
- ChatPreferences: new settings for broadcast address and sked push toggle

The feature is disabled by default (logsynch_wintestNetworkSkedPushEnabled=false).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-25 23:35:35 +01:00
Marc Froehlich
1f3aa031c3 Fixed database lag issue at startup. Added lifetime of 3 days to the worked tags in the database. Now no reset by hand needed any more 2026-03-23 22:36:41 +01:00
6be44bbea2 Fix WT Compile issues and prepare auto release pipeline (#13)
* Fixup WinTest Error

* Prepare CI/CD Auto Release Pipeline

* Fix CI/CD Components not running

* CI/CD Pre-Check n nighty and produce zip for more intutive User Design

* fix Version

* App Version Update.

* More Similar Naming to old Convention
2026-03-23 18:58:00 +01:00
6b311c3907 win-Test Frequency parsing (#12)
tnx dn9apw
2026-03-20 17:35:36 +01:00
Marc Froehlich
7f9b1bfc4d Integrate latest local development state and clean repository artifacts 2026-03-20 11:24:28 +01:00
Marc Froehlich
ee5ee535bb <changeLog>
<changedVersionNumber>1.263</changedVersionNumber>
<date>2025-06-08</date>
<description>Airscout communication and Loginname</description>
<added> nothing </added>
<changed> The shema of AS-calculation messages from KST4Contest to Airscout had been changed due to some reports of Mats Helgöstam and other nice OMs who wasn´t able to track AP by the button. Before: EACH entry of the userlist had been sent to AirScout for AS plane path calculation in 12s intervals. EACH entry of the userlist had been added to the AirScout map. New in v1.263: Only entries of the userlist with a QRB lower than the setted max-QRB will be sent to AirScout for AS plane path calculation in 60s intervals. Only entries of the userlist with a QRB lower than the setted max-QRB will be added to the AS map. </changed>
<fixed> First: By changing the AS communication there is a huge decrease of the amount of messages which have to be queued by AS and also a huge decrease of the calculation operations of AS. That most likely will speed up also the issue of the very laggy "track in AS"-button. And also it will save a lot of computing power. The new 1min interval seems to be enough. Second: Name in chat is now saveable, whoohoo! Third: Some beauty fixes before and after login (visibiity of fields) 4th: Fixed issue which 9A2HM / Kreso told me. The name of the AS client had been hard wired to "KST" and the AS servers name had been hard wired to "AS", which results out of a time where I never mentioned to publish this client. It´s now fixed, so that the name-preferences will have a real effect.... 73 / DO5AMF </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.262</changedVersionNumber>
<date>2025-05-21</date>
<description>Freezes caused by getting messages before user login should be fixed now</description>
<added> nothing </added>
<changed> nothing </changed>
<fixed> ON4KST is delivering messages of stations which are not logged in already. That caused an error at the message processing engine which now should be fixed. </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.26</changedVersionNumber>
<date>2025-05</date>
<description>Login to multiple Channels via single signon / spend some colors</description>
<added> 1. UI: There is a dark mode, now. Switch in "Window -> use dark mode" 2. Usage of two Chatcategories at the same time. 3. opposite station multi-callsign login-tagging 73 / DO5AMF </added>
<changed> - coloring mechanic of the software. Modify colors via css by yourself... </changed>
<fixed> - Station tagging fixed completely </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.251</changedVersionNumber>
<date>2025-02</date>
<description>BUGFIX of 1.25, tnx Steve Clements!</description>
<added> - Steve spotted a problem in udp broadcast spot info reading, it´s now fixed! 73 / DO5AMF </added>
<changed> </changed>
<fixed> - Station tagging </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.25</changedVersionNumber>
<date>2025-02</date>
<description>Wishlist-time</description>
<added> - New configuration Tab: Messagehandling You can find options to wether auto-answering all messages which arriving or answer your CQ qrg automatically if someone asks you for it. - New configuration Tab: Messagehandling There you can configure the default userinfo-window message filter [for my friend Gianluca :-)] - There is a big amount of planned new features during april. Stay tuned! 73 / DO5AMF </added>
- Added coloured lines: new personal message rows will appear in red and changes its colours every 30 seconds as they get older, rainbow like via yellow to white (tnx Gianluca, good idea)
<changed> </changed>
<fixed> - Users with suffixes like "-2 and -70" had not been marked as worked. These will now be ignored and the stations will be marked correctly </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.24</changedVersionNumber>
<date>2024-11</date>
<description>Wishlist-time</description>
<added> - Button to show qrz.com profile of a selected station - Button to show qrzcq.com profile of a selected station </added>
- Added coloured lines: new personal message rows will appear in red and changes its colours every 30 seconds as they get older, rainbow like via yellow to white (tnx Gianluca, good idea)
<changed> </changed>
<fixed> - Users with suffixes like "-2 and -70" had not been marked as worked. These will now be ignored and the stations will be marked correctly </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.23</changedVersionNumber>
<date>2024-10</date>
<description>DXCluster Server is now implemented</description>
<added>- DXCluster Server (tnx OMAAO): KST4Contest inhibts a DXCluster server now. It generates a DXCluster message to feed your log client with station-reachable warnings. As a default, the dxcluster warnings will only be sent if a chatter writes to another and due to this is most likely pointing it´s antenna to your direction. For correct spot processing of your log program, you must use another spotters callsign than your contest callsign. Otherwise the filter will not work. </added>
<changed> </changed>
<fixed> </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.22</changedVersionNumber>
<date>2024-05</date>
<description>Increase usability, fixed AS button</description>
<added>- Variables (tnx OMAAO): * MYLOCATORSHORT * MYQRGSHORT * QRZNAME </added>
<changed>- Sendtext-field focus Focus is now on the text field when clicking on the list of people in the chat to avoid double clicking. You can just begin to type after clicking a callsign. </changed>
<fixed>- Worked-station-filter (tnx Gianluca) Filter is now live, if you activate the worked-filter, the worked(and user tagged not-qrv-for-this-band) will disappear without manually reactivating the filter - Chatters list sorting by QRB (tnx Alessandro) Fixed sorting, was lexicographically, now it´s handled as numbers - Airscout-showpath-button The button inhibits an arrow, directed to the selected station in the chatmembers list. A click to this button will now maximize AirScout which then shows the path and the airplanes which are reflectable to reach the selected station </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.21</changedVersionNumber>
<date>2024-04</date>
<description>Increase usability</description>
<added> </added>
<changed> - GUI-behaviour After a click to the save button, the sizes of all windows will be stored in the configfile and restored at the next startup of the client. Also the dividers of the splitpanels will be stored and restored. On problems delete config-file! Further the filters section is now a flowpane to make the software viewable at lower resolutions. </changed>
<fixed> </fixed>
<removed/>
</changeLog>
<changeLog>
<changedVersionNumber>1.2</changedVersionNumber>
<date>2024-04</date>
<description>Increase usability</description>
<added> - Selectable bands Its now possible to select which bands you want to activate. Please select your bands and click save and restart the software. There will only appear buttonds and field which are related to the bands which you have choosen. - Unworkable tags for each callsign. It´s now possible to set NOT-QRV tags for each station for each band. If an OM tells you for example, that he is not QRV at 144 MHz, you can set the "unworkable" flag for him and able to filter his callsign out of the chatmember-list - QTF-Arrow The button "show path in AS" now got an arrow which shows the QTF of the selected station while the button is still out of function (will work that out some time) </added>
<changed> </changed>
<fixed/>
<removed/>
</changeLog>
2025-07-21 23:52:37 +02:00
Marc Froehlich
5cca2923c2 * New variables: MYLOCATORSHORT, MYQRGSHORT, QRZNAME (Viliam Petrik)
* Sendtext-field focus is now on the text field when clicking on the list of people in the chat. You can just begin to type after clicking a callsign (Gian Luca)
* Worked-station-filter (tnx Gianluca) Filter is now live, if you activate the worked-filter, the worked (and user tagged not-qrv-for-this-band) will disappear without manually reactivating the filter
* Chatters list sorting by QRB (tnx Alessandro); sorting, was lexicographically, now it's handled as numbers
* Airscout-showpath-button works now
2024-05-16 10:08:47 +02:00
Marc Froehlich
1663b0fd7f - Windows-sizes and dividers of the panels will be saved now in the xml file
- changed the filter panel to a flowpanel to increase usability at smaller screens
2024-05-02 22:50:20 +02:00
Marc Froehlich
3e8783d7cd - There had been changes at the preferences xml and the database. Thatswhy for the DB there is an update method now and some checks if the XML is valid. That are simple checks, just to prevent crashing...
- Selectable bands
Its now possible to select which bands you want to activate. Please select your bands and
click save and restart the software. There will only appear buttonds and field which are
related to the bands which you have choosen.

- Unworkable tags for each callsign.
It´s now possible to set NOT-QRV tags for each station for each band. If an OM tells
you for example, that he is not QRV at 144 MHz, you can set the "unworkable" flag for him
and able to filter his callsign out of the chatmember-list

- QTF-Arrow
The button "show path in AS" now got an arrow which shows the QTF of the selected station
while the button is still out of function (will work that out some time)
2024-04-14 23:09:37 +02:00
Marc Froehlich
8bea4111f0 - introduced qrv tags for callsigns, only UI so far 2024-03-30 00:50:16 +01:00
Marc Froehlich
136cf08f08 - reachable function: If a message-sender writes another to ask a sked, I assume that his antenna is directed to this receiver-chatter.
If this causes that the sender-antenna is directed most likely in my direction (with a difference of ~25deg), the callsign will appear fat and green in the userlist. As the sender often propagates his frequency at the chat (that means, we have saved this already), there is a high probability to work him at this short term opportunity
- mark new connected stations
- made some UI improvements (Behaviour of messagefilter-radiobutton corrected)
- removed UI bug, caused if you send a message to your onwn station....
2024-03-17 23:35:13 +01:00
Marc Froehlich
eb04ad3f33 implemented all filters to the chatmemberlist, activity-displays in chatmember table and userinfopanel, linked selected messages to the userinfopanel for better UI feeling, begin of AS-Showpath-function (not yet ready) 2024-02-27 01:53:24 +01:00
Marc Froehlich
51712a1f85 implemented some of the new filters to the chatmemberlist, changed list-subtype to make it sortable again 2024-02-20 23:59:46 +01:00
Marc Froehlich
4a605f54ba Changed lists mechanic: not 3 messagelists any more but one oversable messagelist for all messages. The 3 categories of messages are now filteredlists, derived from this global messagelist.
Added a new panel down of the userlist which will be dynamically generated and shows filtered messages to a selected callsign
2024-02-18 02:39:37 +01:00
Marc Froehlich
037dc8a05b Update information service mechanic implemented 2024-02-08 23:52:08 +01:00
Marc Froehlich
476b4a7dd1 Some bugfixes to make the client robust against crashes after deconnects 2024-02-06 23:56:04 +01:00
Marc Froehlich
bd687dc50f Chat is now disconnectable and reconnectable without closing. Made some changes in the thread management to make that possible 2024-02-01 22:35:06 +01:00
Marc Froehlich
7bce7be2ba added contextmenu to cq-message-table 2024-01-26 22:42:45 +01:00
Marc Froehlich
3286a34a08 added audio support 2024-01-26 11:09:15 +01:00
Marc Froehlich
c2086a73b0 added audio support 2024-01-16 22:51:30 +01:00
197 changed files with 39364 additions and 2206 deletions

258
.github/latex-manual/manual-template.tex vendored Normal file
View 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}

View 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
View 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

21
.github/workflows/github-wiki.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Publish wiki
on:
push:
branches: [main]
paths:
- github_docs/**
- .github/workflows/github-wiki.yml
concurrency:
group: publish-wiki
cancel-in-progress: true
permissions:
contents: write
jobs:
publish-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: github_docs
disable-empty-commits: true

154
.github/workflows/nightly-artifacts.yml vendored Normal file
View File

@@ -0,0 +1,154 @@
name: Nightly Runtime Artifacts
on:
push:
branches:
- main
schedule:
- cron: "20 2 * * *"
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build-windows-zip:
name: Build Windows ZIP
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Resolve nightly version info
shell: pwsh
run: |
$xml = [xml](Get-Content pom.xml)
$version = $xml.project.version
$shortSha = "${{ github.sha }}".Substring(0, 7)
Add-Content -Path $env:GITHUB_ENV -Value "VERSION=$version"
Add-Content -Path $env:GITHUB_ENV -Value "SHORT_SHA=$shortSha"
Add-Content -Path $env:GITHUB_ENV -Value "ASSET_BASENAME=praktiKST-$version-$shortSha"
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Install WiX Toolset
shell: pwsh
run: choco install wixtoolset --no-progress -y
- name: Build JAR and copy runtime dependencies
shell: pwsh
run: |
.\mvnw.cmd -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
$jar = Get-ChildItem -Path target -Filter 'praktiKST-*.jar' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $jar) {
throw "No project JAR produced"
}
Copy-Item $jar.FullName target/dist-libs/app.jar
- name: Build app-image with jpackage
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
- name: Create Windows ZIP
shell: pwsh
run: |
if (-not (Test-Path dist/praktiKST)) {
throw "No Windows app-image produced by jpackage"
}
Compress-Archive -Path dist/praktiKST -DestinationPath "dist/$env:ASSET_BASENAME-windows-x64.zip" -Force
- name: Upload Windows artifact
uses: actions/upload-artifact@v4.3.4
with:
name: windows-zip
path: dist/praktiKST-*-windows-x64.zip
retention-days: 14
build-linux-appimage:
name: Build Linux AppImage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Resolve nightly version info
run: |
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
SHORT_SHA="${GITHUB_SHA::7}"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "SHORT_SHA=$SHORT_SHA" >> "$GITHUB_ENV"
echo "ASSET_BASENAME=praktiKST-${VERSION}-${SHORT_SHA}" >> "$GITHUB_ENV"
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Ensure mvnw is executable
run: chmod +x mvnw
- name: Build JAR and copy runtime dependencies
run: |
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
- name: Build app-image with jpackage
run: |
mkdir -p dist
jpackage \
--type app-image \
--name praktiKST \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
--module-path target/dist-libs \
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
--dest dist
- name: Create AppDir metadata
run: |
rm -rf target/praktiKST.AppDir
cp -a dist/praktiKST target/praktiKST.AppDir
cat > target/praktiKST.AppDir/AppRun << 'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/bin/praktiKST" "$@"
EOF
chmod +x target/praktiKST.AppDir/AppRun
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=praktiKST
Exec=praktiKST
Icon=praktiKST
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
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"
- name: Upload Linux artifact
uses: actions/upload-artifact@v4.3.4
with:
name: linux-appimage
path: dist/praktiKST-*-linux-x86_64.AppImage
retention-days: 14

31
.github/workflows/pr-compile-check.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: PR Compile Check
on:
pull_request:
branches:
- main
workflow_dispatch:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
compile:
name: Compile (Java 17)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Ensure mvnw is executable
run: chmod +x mvnw
- name: Compile
run: ./mvnw -B -DskipTests compile

256
.github/workflows/tagged-release.yml vendored Normal file
View File

@@ -0,0 +1,256 @@
name: Tagged Release Build
on:
push:
tags:
- "*"
workflow_dispatch:
permissions:
contents: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
build-windows-zip:
name: Build Windows ZIP
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Install WiX Toolset
shell: pwsh
run: choco install wixtoolset --no-progress -y
- name: Build JAR and copy runtime dependencies
shell: pwsh
run: |
.\mvnw.cmd -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
$jar = Get-ChildItem -Path target -Filter 'praktiKST-*.jar' | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $jar) {
throw "No project JAR produced"
}
Copy-Item $jar.FullName target/dist-libs/app.jar
- name: Build app-image with jpackage
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
jpackage --type app-image --name praktiKST --input target/dist-libs --main-jar app.jar --main-class kst4contest.view.Kst4ContestApplication --module-path target/dist-libs --add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql --dest dist
- name: Create Windows ZIP
shell: pwsh
run: |
if (-not (Test-Path dist/praktiKST)) {
throw "No Windows app-image produced by jpackage"
}
Compress-Archive -Path dist/praktiKST -DestinationPath dist/praktiKST-${{ github.ref_name }}-windows-x64.zip -Force
- name: Upload Windows artifact
uses: actions/upload-artifact@v4.3.4
with:
name: windows-zip
path: dist/praktiKST-${{ github.ref_name }}-windows-x64.zip
build-linux-appimage:
name: Build Linux AppImage
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
- name: Set up Java 17
uses: actions/setup-java@v4.1.0
with:
distribution: temurin
java-version: "17"
- name: Ensure mvnw is executable
run: chmod +x mvnw
- name: Build JAR and copy runtime dependencies
run: |
./mvnw -B -DskipTests package dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
- name: Build app-image with jpackage
run: |
mkdir -p dist
jpackage \
--type app-image \
--name praktiKST \
--input target/dist-libs \
--main-jar app.jar \
--main-class kst4contest.view.Kst4ContestApplication \
--module-path target/dist-libs \
--add-modules javafx.controls,javafx.graphics,javafx.fxml,javafx.web,javafx.media,java.sql \
--dest dist
- name: Create AppDir metadata
run: |
rm -rf target/praktiKST.AppDir
cp -a dist/praktiKST target/praktiKST.AppDir
cat > target/praktiKST.AppDir/AppRun << 'EOF'
#!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/bin/praktiKST" "$@"
EOF
chmod +x target/praktiKST.AppDir/AppRun
cat > target/praktiKST.AppDir/praktiKST.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=praktiKST
Exec=praktiKST
Icon=praktiKST
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
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
- 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
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
runs-on: ubuntu-latest
needs:
- build-windows-zip
- build-linux-appimage
- build-docs-pdf
steps:
- name: Download Windows artifact
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.3
with:
name: linux-appimage
path: release-assets/linux
- 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:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
name: ${{ startsWith(github.ref_name, 'beta-') && format('Beta {0}', github.ref_name) || format('Release {0}', github.ref_name) }}
prerelease: ${{ startsWith(github.ref_name, 'beta-') }}
allowUpdates: false
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,
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-en.pdf,
release-assets/docs/KST4Contest-${{ github.ref_name }}-manual-de.pdf

19
.gitignore vendored
View File

@@ -16,3 +16,22 @@ target
debug.out
.DS_Store
#Logfiles
SimpleLogFile.txt
udpReaderBackup.txt
#tempfiles
.idea/
out/
#targetfiles - mvn wrapper
target/
#builds
build/
#pdf output directory
dist/
#zip files for local backups
*.zip

View File

@@ -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:
[![Publish wiki](https://github.com/praktimarc/kst4contest/actions/workflows/github-wiki.yml/badge.svg)](https://github.com/praktimarc/kst4contest/actions/workflows/github-wiki.yml)
[![Docs PDF](https://github.com/praktimarc/kst4contest/actions/workflows/docs-pdf.yml/badge.svg)](https://github.com/praktimarc/kst4contest/actions/workflows/docs-pdf.yml)
Builds:
[![Nightly Runtime Artifacts](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml/badge.svg)](https://github.com/praktimarc/kst4contest/actions/workflows/nightly-artifacts.yml)

View File

@@ -1 +1,2 @@
do5sa
dr2x
oe3cin

15832
bugsept24.txt Normal file

File diff suppressed because it is too large Load Diff

53
github_docs/Home.md Normal file
View File

@@ -0,0 +1,53 @@
# KST4Contest Wiki
**KST4Contest** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), speziell entwickelt für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern.
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
---
## 🌐 Sprache / Language
| 🇩🇪 Deutsch | 🇬🇧 English |
|---|---|
| [Startseite (Deutsch)](de-Home) | [Home (English)](en-Home) |
---
## 🇩🇪 Inhalt (Deutsch)
| Seite | Inhalt |
|---|---|
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
| [Changelog](de-Changelog) | Versionsgeschichte |
---
## 🇬🇧 Contents (English)
| Page | Contents |
|---|---|
| [Installation](en-Installation) | Download, Java requirements, updates |
| [Configuration](en-Configuration) | All settings in detail |
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
| [Features](en-Features) | All features at a glance |
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
| [User Interface](en-User-Interface) | UI explained and how to operate it |
| [Changelog](en-Changelog) | Version history |
---
## Schnellinfo / Quick Info
- **Download**: https://github.com/praktimarc/kst4contest/releases
- **GitHub**: https://github.com/praktimarc/kst4contest
- **Kontakt / Contact**: praktimarc+kst4contest@gmail.com

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -0,0 +1,116 @@
# AirScout-Integration
> 🇬🇧 [English version](en-AirScout-Integration) | 🇩🇪 Du liest gerade die deutsche Version
AirScout (von DL2ALF) ist ein Programm zur Erkennung von Flugzeugen für den Aircraft-Scatter-Betrieb. KST4Contest ist eng mit AirScout integriert und zeigt reflektierbare Flugzeuge direkt in der Benutzerliste an.
> **Aircraft Scatter** ermöglicht sehr weitreichende Verbindungen auf VHF und höher auch für Stationen mit geringer Höhe über NN oder ungünstigen topografischen Verhältnissen.
---
## AirScout herunterladen
Download von AirScout:
- http://airscout.eu/index.php/download
---
## Flugzeugdaten-Feeds (ADSB)
Öffentliche Flugzeugdaten-Feeds im Internet sind oft unzuverlässig und begrenzt nutzbar. Eine empfohlene Alternative bietet **OV3T (Thomas)** mit einem dedizierten ADSB-Feed-Dienst:
- https://airscatter.dk/
- https://www.facebook.com/groups/825093981868542
Für diesen Dienst ist ein Account erforderlich. Bitte eine Spende für Thomas in Betracht ziehen der Server-Betrieb ist nicht kostenlos!
---
## AirScout einrichten
### Schritt 1: ADSB-Feed in AirScout konfigurieren
1. AirScout starten.
2. In den AirScout-Einstellungen den OV3T-Feed-Account eintragen (Benutzername, Passwort, URL).
![AirscoutSchritt1](as_plane_feed_1.png)
![AirscoutSchritt2](as_plane_feed_2.png)
3. Verbindung testen.
### Schritt 2: UDP-Kommunikation für KST4Contest aktivieren
In AirScout die UDP-Schnittstelle aktivieren:
- In den AirScout-Einstellungen die entsprechende Checkbox aktivieren (nur eine Checkbox notwendig).
- Standard-Ports nicht ändern, wenn kein besonderer Grund vorliegt.
### Schritt 3: KST4Contest-Einstellungen
In den KST4Contest-Preferences → **AirScout Settings**:
- AirScout-Kommunikation aktivieren
- IP und Port auf Standardwerte lassen (sofern nicht geändert)
![AirscoutSchritt3](as_plane_feed_3.png){ width=85% }
---
## Kommunikation zwischen KST4Contest und AirScout (ab v1.263)
**Verbesserung in v1.263**: KST4Contest sendet nur noch Stationen an AirScout, deren QRB (Entfernung) kleiner als das eingestellte **Maximum-QRB** ist. Das Abfrageintervall wurde von 12 Sekunden auf **60 Sekunden** verlängert.
**Vorteile:**
- Deutlich weniger Berechnungsaufwand für AirScout
- Deutlich weniger Nachrichtenverkehr
- Das Tracking-Problem mit dem „Show Path in AirScout"-Button wurde dadurch deutlich verbessert
- Weniger Rechenleistung insgesamt
Außerdem: Der Name des KST4Contest-Clients und des AirScout-Servers war früher hartcodiert (`KST` und `AS`). Ab v1.263 werden die in den Preferences eingetragenen Namen verwendet.
---
## Mehrere KST4Contest-Instanzen und AirScout
> **Achtung**: Wenn mehrere KST4Contest-Instanzen gleichzeitig betrieben werden und bei beiden die AirScout-Kommunikation aktiviert ist, antwortet AirScout **an beide Instanzen**.
Das ist unproblematisch, wenn:
- Beide Instanzen denselben Locator verwenden, **oder**
- Beide Instanzen unterschiedliche Login-Rufzeichen haben.
Andernfalls kann es zu fehlerhaften AP-Daten kommen.
---
## AP-Spalte in der Benutzerliste
Nach der Einrichtung erscheint in der Benutzerliste eine **AP-Spalte** mit bis zu zwei reflektierbaren Flugzeugen pro Station.
Beispiel-Darstellung:
| Station | AP-Info |
|---|---|
| DF9QX | 2 Planes: 0 min / 0 min, je 100% |
| F5DYD | 2 Planes: 14 min / 31 min, je 50% |
Die AP-Informationen sind auch im **Privatnachrichten-Fenster** verfügbar.
Die Prozentzahl gibt das Reflexionspotenzial an (Größe des Flugzeugs, Höhe, Entfernung).
---
## AP-Variablen in Nachrichten
Die Flugzeugdaten können direkt in Nachrichten eingefügt werden:
- `FIRSTAP` → z. B. `a very big AP in 1 min`
- `SECONDAP` → z. B. `Next big AP in 9 min`
Details: [Makros und Variablen](Makros-und-Variablen#variablen)
---
## „Show Path in AirScout"-Button
In der Benutzerliste gibt es einen Button mit einem Pfeil, der die Richtung (QTF) zur ausgewählten Station anzeigt. Ein Klick maximiert AirScout und zeigt den Pfad mit reflektierbaren Flugzeugen zum ausgewählten Gesprächspartner.

View File

@@ -0,0 +1,105 @@
# Benutzeroberfläche
> 🇬🇧 [English version](en-User-Interface) | 🇩🇪 Du liest gerade die deutsche Version
## Verbinden mit dem Chat
1. Im Einstellungsfenster eine **Chat-Kategorie** auswählen (z. B. 144 MHz VHF, 432 MHz UHF, …).
2. **Connect**-Button klicken.
3. Warten bis die Verbindung aufgebaut ist.
> Trennen und Neu-Verbinden ist nur über das Einstellungsfenster möglich. Es empfiehlt sich daher, das Einstellungsfenster geöffnet zu lassen.
---
## Hauptfenster-Überblick
Das Hauptfenster besteht aus mehreren Bereichen:
### PM-Fenster (oben links)
Zeigt alle empfangenen **Privatnachrichten** sowie abgefangene öffentliche Nachrichten, die das eigene Rufzeichen enthalten. Neue Nachrichten erscheinen in **Rot** und faden alle 30 Sekunden über Gelb bis Weiß ab.
### Benutzerliste (Chat Members)
Die zentrale Tabelle aller aktuell aktiven Chat-Nutzer. Spalten (je nach Konfiguration):
| Spalte | Inhalt |
|---|---|
| Call | Rufzeichen der Station |
| Name | Name aus dem Chat-Namenfeld |
| Loc | Maidenhead-Locator |
| QRB | Entfernung in km |
| QTF | Richtung in Grad |
| QRG | Automatisch erkannte Frequenz |
| AP | AirScout-Flugzeugdaten (wenn aktiv) |
| Band-Farben | Worked/NOT-QRV-Status pro Band |
**Sortierung**: Klick auf Spaltenköpfe. QRB-Sortierung arbeitet numerisch (ab v1.22 korrigiert).
### Sendfeld
Texteingabe für ausgehende Nachrichten. Nach Klick auf ein Rufzeichen in der Benutzerliste erhält das Sendfeld automatisch den Fokus sofort tippen ohne Doppelklick (ab v1.22).
### MYQRG-Feld
Rechts neben dem Sendbutton. Zeigt die aktuelle eigene QRG an, kann auch manuell eingetragen werden.
### MYQTF-Feld *(für v1.3)*
Eingabefeld für die aktuelle Antennenrichtung. Wird für die geplante `MYQTF`-Variable verwendet.
---
## Filter
Die Filter-Leiste (ab v1.21 als Flowpane für kleine Bildschirme):
- **Show only QTF**: Richtungsfilter aktivieren (Buttons N/NE/E/… oder Grad-Eingabe)
- **Show only QRB [km] <=**: Entfernungsfilter aktivieren (Toggle-Button)
- **Hide Worked [Band]**: Gearbeitete Stationen pro Band ausblenden (je ein Toggle pro Band)
- **Hide NOT-QRV [Band]**: NOT-QRV-markierte Stationen pro Band ausblenden
---
## Stationsinfo-Panel (Further Info)
Rechts unten: Zeigt alle Nachrichten einer ausgewählten Station (CQ-Nachrichten und PMs in einem Panel). Ein Nachrichtenfilter lässt sich über den Standard-Filter in den Preferences vorbelegen.
Hier können auch **Sked-Erinnerungen** aktiviert werden.
---
## Prioritätsliste
Zeigt die vom Score-Service berechneten Top-Kandidaten. Aktualisiert sich automatisch im Hintergrund basierend auf Richtung, Entfernung und AP-Verfügbarkeit.
---
## Cluster & QSO der anderen
Separates Fenster (kann miniaturisiert werden). Zeigt den Kommunikationsfluss zwischen anderen Stationen interessant in ruhigeren Phasen.
---
## Menü
### Window
- **Use Dark Mode** (ab v1.26): Dunkles Farbschema aktivieren/deaktivieren.
---
## Fenstergrößen und Divider
Ab **v1.21** werden beim Klick auf **„Save Settings"** auch Fenstergrößen und Divider-Positionen aller Panels in der Konfigurationsdatei gespeichert und beim nächsten Start wiederhergestellt.
Bei Problemen mit der Darstellung: Konfigurationsdatei löschen → KST4Contest erstellt neue Standardwerte.
---
## Tipps zur Bedienung
- **Einstellungsfenster geöffnet lassen**: Schneller Zugriff auf Beacon-Aktivierung/Deaktivierung.
- **Rechtsklick in der Benutzerliste**: Öffnet das Snippet-Menü und weitere Aktionen (QRZ.com-Profil, NOT-QRV-Tags setzen).
- **Enter aus dem Chat heraus**: Wenn im Sendfeld Text steht, sendet Enter direkt auch wenn der Fokus woanders liegt.
- **Beacon stoppen**: Beim Scannen von Frequenzen den Beacon ausschalten, damit der Chat nicht mit Meldungen überflutet wird.

235
github_docs/de-Changelog.md Normal file
View File

@@ -0,0 +1,235 @@
# Changelog
> 🇬🇧 [English version](en-Changelog) | 🇩🇪 Du liest gerade die deutsche Version
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**
**Geändert:**
- AirScout-Kommunikation grundlegend überarbeitet: Nur noch Stationen mit QRB < max-QRB werden an AirScout gesendet.
- Abfrage-Intervall von 12 Sekunden auf **60 Sekunden** erhöht.
- Deutlich weniger Berechnungsaufwand und Nachrichtenverkehr → Stabileres AirScout-Tracking.
- Name des AS-Clients und AS-Servers ist jetzt aus den Preferences konfigurierbar (war vorher hartcodiert auf „KST" / „AS").
**Behoben:**
- „Track in AirScout"-Button war sehr träge → durch neue Kommunikationslogik deutlich verbessert.
- Name im Chat ist jetzt speicherbar (Fehler behoben).
- Visuelle Korrekturen vor und nach dem Login.
- Fehler behoben, der von 9A2HM (Kreso) gemeldet wurde.
---
## v1.262 (2025-05-21)
**Freeze-Fix bei vorzeitiger Nachrichtenlieferung**
**Behoben:**
- ON4KST liefert manchmal Nachrichten, bevor der Login abgeschlossen ist. Das verursachte Fehler in der Nachrichtenverarbeitung → jetzt behoben.
---
## v1.26 (2025-05)
**Multi-Channel-Login und Dark Mode**
**Neu:**
- **Dark Mode**: Umschaltbar über `Window → Use Dark Mode`.
- **Multi-Channel-Login**: Gleichzeitiger Login in zwei Chat-Kategorien.
- **Opposite Station Multi-Callsign Login-Tagging**: Unterstützung für Stationen mit mehreren Rufzeichen.
**Geändert:**
- Farbgebungs-Mechanismus überarbeitet: Farben können jetzt über CSS angepasst werden.
**Behoben:**
- Stationsmarkierung komplett überarbeitet und korrekt gestellt.
---
## v1.251 (2025-02)
**Bugfix für UDP-Broadcast-Spot-Info**
**Behoben:**
- Problem beim Lesen von UDP-Broadcast-Spot-Informationen behoben (gemeldet von Steve Clements danke!).
- Stationsmarkierung (erneut verbessert).
---
## v1.25 (2025-02)
**Wunschliste umgesetzt**
**Neu:**
- **Neuer Einstellungs-Tab: Messagehandling**
- Auto-Antwort auf eingehende Nachrichten konfigurierbar.
- Automatische Antwort mit eigener CQ-QRG, wenn jemand danach fragt.
- Konfigurierbarer Standard-Filter für das Userinfo-Fenster *(für Gianluca :-) )*.
- **Farbige PM-Zeilen**: Neue Privatnachrichten erscheinen rot und faden alle 30 Sekunden über Gelb bis Weiß ab *(Idee von IU3OAR, Gianluca)*.
**Behoben:**
- Stationen mit Suffixen wie „-2" und „-70" wurden nicht als gearbeitet markiert → werden jetzt ignoriert, Station wird korrekt markiert.
---
## v1.24 (2024-11)
**Wunschliste + DX-Cluster-Spots**
**Neu:**
- Button zum Öffnen des **QRZ.com-Profils** der ausgewählten Station.
- Button zum Öffnen des **QRZ-CQ-Profils** der ausgewählten Station.
- **DX-Cluster-Server-Integration**: Richtungs-Warnungen werden als Spots an das Logprogramm gesendet (wenn QRG bekannt).
*(Zusätzlich wurden Farbgebungen der PM-Zeilen hinzugefügt tnx Gianluca)*
---
## v1.23 (2024-10)
**Integrierter DX-Cluster-Server**
**Neu:**
- KST4Contest enthält jetzt einen **integrierten DX-Cluster-Server**.
- Generiert DX-Cluster-Spots und sendet sie an das Logprogramm, wenn eine Richtungs-Warnung ausgelöst und eine QRG bekannt ist.
- Spotter-Rufzeichen muss sich vom Contest-Rufzeichen unterscheiden (für korrekte Filterung im Logprogramm).
*(Idee von OM0AAO, Viliam Petrik danke!)*
---
## v1.22 (2024-05)
**Usability-Verbesserungen und AirScout-Button-Fix**
**Neu:**
- Neue Variablen (tnx OM0AAO, Viliam Petrik):
- `MYLOCATORSHORT`
- `MYQRGSHORT`
- `QRZNAME`
**Geändert:**
- Sendfeld-Fokus: Nach Klick auf Rufzeichen in der Benutzerliste erhält das Sendfeld sofort den Fokus kein Doppelklick notwendig *(tnx Gianluca)*.
**Behoben:**
- Worked-Station-Filter ist jetzt live-aktiv: Gearbeitete Stationen verschwinden sofort nach Aktivierung des Filters *(tnx Gianluca)*.
- QRB-Sortierung war lexikografisch → jetzt numerisch *(tnx Alessandro Murador)*.
- AirScout-„Show Path"-Button: Klick maximiert AirScout und zeigt den Pfad korrekt an.
---
## v1.21 (2024-04)
**Usability-Verbesserungen**
**Geändert:**
- Fenstergrößen und Divider-Positionen werden beim Klick auf „Save Settings" in der Konfigurationsdatei gespeichert und beim Start wiederhergestellt.
- Filter-Bereich als Flowpane → bessere Darstellung auf kleineren Bildschirmen.
---
## v1.2 (2024-04)
**Bandselektion und NOT-QRV-Tags**
**Neu:**
- **Bandselektion**: In den Preferences auswählbar, welche Bänder aktiv sind. Nur für gewählte Bänder erscheinen Buttons und Felder in der UI. Speichern und Neustart erforderlich.
- **NOT-QRV-Tags pro Station und Band**: Stationen können für jedes Band als „nicht QRV" markiert werden. Kombinierbar mit dem Userlist-Filter.
- **QTF-Pfeil**: Der „Show path in AS"-Button zeigt jetzt einen Pfeil mit dem QTF der ausgewählten Station an.
---
## Frühere Versionen
### v1.1
Erste öffentlich veröffentlichte Version. Grundfunktionen:
- Worked-Markierung via Simplelogfile und UDP
- Sked-Richtungs-Hervorhebung
- QRG-Erkennung
- Text-Snippets und Shortcuts
- AirScout-Interface (erste Version)
- Intervall-Beacon
- PM-Abfang für öffentliche Nachrichten mit eigenem Rufzeichen
- Update-Hinweis-Dienst
---
## Geplante Features
- `MYQTF`-Variable (eigene Antennenrichtung als Text)
- ~~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

View File

@@ -0,0 +1,76 @@
# Integrierter DX-Cluster-Server
> 🇬🇧 [English version](en-DX-Cluster-Server) | 🇩🇪 Du liest gerade die deutsche Version
Ab **Version 1.23** enthält KST4Contest einen integrierten DX-Cluster-Server. Dieser sendet Spots direkt an das Logprogramm, wenn eine Richtungs-Warnung ausgelöst wird.
*(Idee von OM0AAO, Viliam Petrik danke!)*
---
## Wozu dient der integrierte DX-Cluster-Server?
Wenn KST4Contest erkennt, dass eine Station aus der eigenen Richtung ein Sked anfragt und gleichzeitig eine QRG bekannt ist, wird **automatisch ein DX-Cluster-Spot generiert** und an den Cluster-Client des Logprogramms gesendet.
Das Logprogramm zeigt den Spot in der Bandkarte an. Ein Klick auf den Spot stellt Frequenz und Mode des Transceivers direkt ein ohne manuelles Eintippen.
---
## Einrichtung
### In KST4Contest
In den Preferences → **DX-Cluster-Server-Einstellungen**:
1. **Port** des internen Servers eintragen (z. B. 7300 oder 8000 muss mit dem Logprogramm übereinstimmen).
2. **Spotter-Rufzeichen** eintragen **unbedingt ein anderes Rufzeichen als das Contest-Rufzeichen verwenden!**
- Grund: Logprogramme filtern Spots, die vom eigenen Rufzeichen stammen, als „gearbeitet" heraus. Wenn der Spotter dasselbe Rufzeichen hat, werden die Spots nicht angezeigt.
3. **Angenommene MHz** eintragen: Bei Frequenzangaben wie „.205" im Chat muss KST4Contest entscheiden, ob 144.205, 432.205 oder 1296.205 gemeint ist. Bei Einband-Contests einfach die entsprechende Bandmitte eintragen. Vollständige Frequenzangaben wie „144.205" oder „1296.338" im Chat werden immer korrekt erkannt.
### In UCXLog
- Verbindung zu einem DX-Cluster-Server konfigurieren:
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
- Port: Wie in KST4Contest konfiguriert
- Passwort: kann leer bleiben
- Über die Schaltfläche **„Send a test message to your log"** kann die Verbindung getestet werden.
### In N1MM+
Ähnliche Einstellungen:
- Host: `127.0.0.1` (oder IP des KST4Contest-Computers)
- Port: Wie in KST4Contest konfiguriert
---
## Funktionsweise
Ein Spot wird generiert, wenn **beide** Bedingungen erfüllt sind:
1. Eine **Richtungs-Warnung** wurde ausgelöst (Station macht ein Sked in die eigene Richtung).
2. **QRG der Station ist bekannt** (aus dem Chat ausgelesen oder manuell eingetragen).
Der generierte Spot enthält:
- Rufzeichen der Station
- Frequenz
- Spotterzeit
Das Logprogramm kann den Spot dann in der Bandkarte anzeigen und den TRX per Mausklick auf die Frequenz abstimmen.
---
## Multi-Computer-Setup
Wenn KST4Contest auf einem separaten Computer läuft (nicht auf dem Log-Computer):
- Host im Logprogramm: IP des KST4Contest-Computers (nicht `127.0.0.1`)
- Entspricht der Konfiguration der QSO-UDP-Broadcast-Pakete (siehe [Log-Synchronisation](de-Log-Synchronisation))
---
## Getestete Logprogramme
- **UCXLog** ✓
- **N1MM+** ✓
Weitere Testergebnisse sind willkommen bitte per E-Mail an DO5AMF melden.

View File

@@ -0,0 +1,237 @@
# Funktionen
> 🇬🇧 [English version](en-Features) | 🇩🇪 Du liest gerade die deutsche Version
Übersicht aller Hauptfunktionen von KST4Contest.
---
## Sked-Richtungs-Hervorhebung
Eine der Kernfunktionen: Wenn eine Station ein Sked in die **eigene Richtung** sendet, wird sie in der Benutzerliste **grün und fett** hervorgehoben.
### Wie funktioniert das?
Die Berechnung basiert auf folgender Logik:
- Wenn Station A eine Sked-Anfrage an Station B sendet, wird angenommen, dass A ihre Antenne auf B ausrichtet.
- Wenn die daraus resultierende Richtung von A zur eigenen Station innerhalb des halben Öffnungswinkels der eigenen Antenne liegt, wird A hervorgehoben.
**Beispiel** (Öffnungswinkel 69°, Halbwinkel 34,5°):
| Situation | Ergebnis für DO5AMF in JN49 |
|---|---|
| Sked von F5FEN → DM5M | ✅ Hervorhebung (F5FEN zeigt Richtung DM5M, das liegt nahe JN49) |
| Sked von DM5M → F5FEN | ✅ Hervorhebung (DM5M antwortet in Richtung F5FEN) |
| F1DBN ist unbeteiligt | ❌ Keine Hervorhebung |
| DO5AMF/P (anderer Standort) | ❌ Keine Hervorhebung für Sked-Antwort |
Die Berechnung berücksichtigt keine topografischen Wegberechnungen das ist eine bewusste Vereinfachung. Möglicherweise wird das in einer späteren Version ergänzt.
> Konfiguration: [Konfiguration Antennen-Öffnungswinkel](Konfiguration#antennen-öffnungswinkel-antenna-beamwidth)
---
## Sked-Richtungs-Spots (Integrierter DX-Cluster)
Ab **v1.23**: Richtungs-Warnungen werden als DX-Cluster-Spots an das Logprogramm weitergeleitet, wenn eine QRG bekannt ist. Details: [DX-Cluster-Server](de-DX-Cluster-Server).
---
## QRG-Erkennung (QRG Reading)
KST4Contest verarbeitet jede Chat-Nachricht und extrahiert automatisch **Frequenzangaben**. Diese werden in der Benutzerliste in der **QRG-Spalte** angezeigt.
Erkannte Formate: `144.205`, `432.088`, `.205` (mit konfigurierter Bandannahme), etc.
**Nutzen**: Ohne nachzufragen kann man direkt auf die QRG einer Station schauen und entscheiden, ob eine Verbindung möglich ist.
---
## Worked-Markierung
Gearbeitete Stationen werden in der Benutzerliste visuell markiert pro Band. Grundlage ist die [Log-Synchronisation](de-Log-Synchronisation) via UDP oder Simplelogfile.
Vor jedem Contest die Datenbank zurücksetzen: [Konfiguration Worked Station Database Settings](Konfiguration#worked-station-database-settings).
---
## NOT-QRV-Tags (ab v1.2)
Wenn eine Station mitteilt, dass sie auf einem bestimmten Band nicht QRV ist, kann dies manuell markiert werden:
1. Station in der Benutzerliste auswählen.
2. Rechtsklick → NOT-QRV für das entsprechende Band setzen.
Diese Tags werden in der internen Datenbank gespeichert und bleiben nach einem Neustart von KST4Contest erhalten. Zurücksetzen über die Einstellungen möglich.
**Nutzen**: Verhindert wiederholte Sked-Anfragen auf Bändern, auf denen die Station nicht QRV ist schont sowohl die eigenen Nerven als auch die der Gegenstation.
---
## Richtungsfilter (Direction Filter)
Zeigt in der Benutzerliste nur Stationen an, die sich in einer bestimmten Richtung befinden. Aktivierbar über die Buttons N / NE / E / SE / S / SW / W / NW oder durch manuelle Eingabe von Grad.
Sinnvoll: Während man CQ in eine bestimmte Richtung ruft, nur Stationen in dieser Richtung anzeigen.
---
## Entfernungsfilter (Distance Filter)
Stationen jenseits einer maximalen Entfernung ausblenden. Schaltfläche **„Show only QRB [km] <="** ist ein Toggle-Button.
---
## Worked- und NOT-QRV-Filter
Toggle-Buttons (einer pro Band) zum Ausblenden bereits gearbeiteter Stationen und/oder NOT-QRV-markierter Stationen. Der Filter wirkt **sofort** ohne manuelles Neu-Aktivieren (ab v1.22 live).
---
## Farbige PM-Zeilen (ab v1.25)
Neue Privatnachrichten erscheinen in **Rot**. Die Farbe wechselt alle 30 Sekunden über Gelb bis Weiß wie ein Regenbogen-Fade. So ist auf einen Blick erkennbar, wie aktuell eine Nachricht ist.
*(Idee von IU3OAR, Gianluca Costantino danke!)*
---
## PM-Abfang (Catching Personal Messages)
Manche Nutzer senden Direktnachrichten versehentlich öffentlich, z. B.:
```
(DM5M) pse ur qrg
```
KST4Contest erkennt solche Nachrichten, die das eigene Rufzeichen enthalten, und sortiert sie automatisch in die **Privatnachrichten-Tabelle** ein. So gehen keine Nachrichten verloren.
---
## Multi-Channel-Login (ab v1.26)
Gleichzeitiger Login in **zwei Chat-Kategorien** (z. B. 144 MHz und 432 MHz). Beide Chats werden parallel überwacht.
---
## Dark Mode (ab v1.26)
Aktivierbar über: **Window → Use Dark Mode**
Für individuelle Farbanpassungen: CSS-Datei bearbeiten (Pfad in den Programmunterlagen).
---
## Opposite Station Multi-Callsign Login-Tagging (ab v1.26)
Unterstützung für Stationen, die mit mehreren Rufzeichen gleichzeitig im Chat aktiv sind (z. B. Expedition-Setups).
---
## QRZ.com und QRZ-CQ Profil-Buttons (ab v1.24)
Für ausgewählte Stationen in der Benutzerliste gibt es direkte Buttons, um das **QRZ.com-Profil** und das **QRZ-CQ-Profil** im Browser zu öffnen.
---
## Sked-Erinnerungen mit ALERT (ab v1.40)
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.
---
## QSO-Sniffer (ab v1.31)
Der QSO-Sniffer überwacht den Chat auf Nachrichten von einer konfigurierbaren Rufzeichen-Liste und leitet diese automatisch in das **PM-Fenster** weiter. So gehen keine relevanten Nachrichten im allgemeinen Chat-Rauschen unter.
Konfiguration: [Konfiguration Sniffer-Einstellungen](de-Konfiguration#sniffer-einstellungen-ab-v131)
---
## Win-Test-Integration (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)
- Sked-Richtung
- Sked-Erfolgsrate und Skedfail-Markierungen
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.
---
## Intervall-Beacon
Automatische CQ-Meldungen im öffentlichen Kanal in konfigurierbarem Intervall. Empfohlene Verwendung mit der Variable `MYQRG` für aktuelle Frequenzangabe. Details: [Konfiguration Beacon Settings](Konfiguration#beacon-settings-automatischer-beacon).
---
## Simplelogfile
Dateibasierte Log-Auswertung per Regex. Details: [Log-Synchronisation](Log-Synchronisation#methode-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Cluster & QSO der anderen
Ein separates Fenster zeigt den QSO-Fluss zwischen anderen Stationen. Besonders interessant in ruhigeren Nacht-Stunden während des Contests, wenn weniger Verkehr herrscht.
Dieses Fenster kann miniaturisiert werden, wenn es nicht benötigt wird. Zukünftig geplant: Filterung auf Stationen im ausgewählten QTF.

51
github_docs/de-Home.md Normal file
View File

@@ -0,0 +1,51 @@
# KST4Contest Wiki
> 🇬🇧 [English version](en-Home) | 🇩🇪 Du liest gerade die deutsche Version
**KST4Contest** (auch bekannt als *PraktiKST*) ist ein Java-basierter Chat-Client für den [ON4KST-Chat](http://www.on4kst.info/chat/), der speziell für den Contest-Betrieb auf den VHF/UHF/SHF-Bändern (144 MHz und aufwärts) entwickelt wurde.
Entwickelt von **DO5AMF (Marc Fröhlich)**, Operator bei DM5M.
---
## Schnellnavigation
| Seite | Inhalt |
|---|---|
| [Installation](de-Installation) | Download, Java-Voraussetzungen, Update |
| [Konfiguration](de-Konfiguration) | Alle Einstellungen im Detail |
| [Log-Synchronisation](de-Log-Synchronisation) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout-Integration](de-AirScout-Integration) | Flugzeug-Scatter-Erkennung |
| [DX-Cluster-Server](de-DX-Cluster-Server) | Integrierter DX-Cluster für das Log-Programm |
| [Funktionen](de-Funktionen) | Alle Features im Überblick |
| [Makros und Variablen](de-Makros-und-Variablen) | Text-Snippets, Shortcuts, Variablen |
| [Benutzeroberfläche](de-Benutzeroberflaeche) | UI-Erklärung und Bedienung |
| [Changelog](de-Changelog) | Versionsgeschichte |
---
## Was ist KST4Contest?
Der ON4KST-Chat ist der De-facto-Standard für Skeds auf den 144-MHz-und-höher-Bändern. KST4Contest erweitert die Chat-Nutzung um contest-spezifische Funktionen:
- **Worked-Markierung**: Bereits gearbeitete Stationen werden farblich markiert, direkt aus dem Logprogramm via UDP synchronisiert.
- **Sked-Richtungs-Erkennung**: Wenn eine Station eine andere aus deiner Richtung anruft, wird sie grün und fett hervorgehoben.
- **QRG-Erkennung**: KST4Contest liest Frequenzen automatisch aus dem Chat-Verkehr und zeigt sie in der Benutzerliste an.
- **AirScout-Interface**: Anzeige reflektierbarer Flugzeuge direkt in der Benutzerliste.
- **Integrierter DX-Cluster-Server**: Spots werden direkt an das Logprogramm gesendet.
- **Dark Mode** (ab v1.26): Schont die Augen in der Nacht.
- **Multi-Channel-Login** (ab v1.26): Gleichzeitig in zwei Chat-Kategorien einloggen.
---
## Kontakt & Support
- **E-Mail**: praktimarc+kst4contest@gmail.com *(nur für kst4contest-Themen)*
- **GitHub**: https://github.com/praktimarc/kst4contest
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest
---
## Danksagungen
Besonderer Dank gilt: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX-Cluster-Idee), DC9DJ (Konrad Neitzel, Projektstruktur), DO5ALF (Andreas, Webmaster funkerportal.de), PE0WGA (Franz van Velzen, Tester) sowie allen weiteren Testern und Ideengebern.

View File

@@ -0,0 +1,114 @@
# Installation
> 🇬🇧 [English version](en-Installation) | 🇩🇪 Du liest gerade die deutsche Version
## 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:
- Registrierung unter: http://www.on4kst.info/chat/register.php
### Verhaltensregeln im Chat
Die offizielle Sprache im ON4KST-Chat ist **Englisch**. Auch bei Kommunikation mit Stationen aus dem eigenen Land bitte Englisch verwenden. Übliche HAM-Abkürzungen (agn, dir, pse, rrr, tnx, 73 …) sind gang und gäbe.
### Persönliche Nachrichten
Um eine Privatnachricht an eine andere Station zu senden, immer folgendes Format verwenden:
```
/CQ RUFZEICHEN Nachrichtentext
```
Beispiel: `/CQ DL5ASG pse sked 144.205?`
Bei starkem Chat-Verkehr (56 Nachrichten pro Sekunde im Contest) gehen öffentliche Nachrichten, die an ein bestimmtes Rufzeichen gerichtet sind, leicht unter. KST4Contest fängt solche Nachrichten aber auch dann ab, wenn sie fälschlicherweise öffentlich gepostet werden (siehe [Funktionen PM-Abfang](Funktionen#catching-personal-messages)).
---
## Download
### Windows
Die aktuelle Version kann als ZIP-Datei heruntergeladen werden:
**https://github.com/praktimarc/kst4contest/releases/latest**
Der Dateiname hat das Format `praktiKST-v<Versionsnummer>-windows-x64.zip `.
### Linux
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`.
---
## Installation
### Windows
1. ZIP-Datei herunterladen.
2. ZIP-Datei in einen gewünschten Ordner entpacken.
3. `praktiKST.exe` ausführen.
Die Einstellungen werden unter `%USERPROFILE%\.praktikst\preferences.xml` gespeichert.
### 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`)
4. AppImage ausführen.
Die Einstellungen werden unter `~/.praktikst/preferences.xml` gespeichert.
---
## Update
KST4Contest enthält einen **automatischen Update-Hinweis-Dienst**: Sobald eine neue Version verfügbar ist, erscheint beim Start ein Fenster mit:
- der Information, dass eine neue Version vorliegt,
- einem Changelog,
- dem Download-Link zur neuen Version.
![Beispiel Update Fenster](update_window.png)
### Update-Prozess
#### Windows
Derzeit gibt es nur einen Weg zum Aktualisieren:
1. Den alten Ordner löschen.
2. Das neue ZIP entpacken.
Die Einstellungsdatei (`preferences.xml`) bleibt erhalten, da sie im Benutzerordner gespeichert ist nicht im Programmordner.
#### Linux
Derzeit folgendermaßen:
1. neues AppImage herunterladen
2. neues AppImage ausführbar makieren
3. (optional) altes AppImage löschen.
---
## Bekannte Probleme beim Start
### Norton 360
Norton 360 stuft `praktiKST.exe` als gefährlich ein (Fehlalarm). Es muss eine Ausnahme für die Datei eingerichtet werden:
1. Norton 360 öffnen.
2. Sicherheit → Verlauf → Das entsprechende Ereignis suchen.
3. „Wiederherstellen & Ausnahme hinzufügen" wählen.
*(Gemeldet von PE0WGA, Franz van Velzen danke!)*

View File

@@ -0,0 +1,200 @@
# Konfiguration
> 🇬🇧 [English version](en-Configuration) | 🇩🇪 Du liest gerade die deutsche Version
Nach dem ersten Start öffnet sich das **Einstellungsfenster** dieses ist der zentrale Ausgangspunkt für alle Konfigurationen. Es empfiehlt sich, das Einstellungsfenster während des Betriebs geöffnet zu lassen (z. B. um den Beacon schnell ein- und auszuschalten).
> **Wichtig**: Nach jeder Änderung unbedingt **„Save Settings"** klicken! Die Einstellungen werden unter Linux in `~/.praktikst/preferences.xml` und unter Windows in `%USERPROFILE%\.praktikst\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktikst\preferences.xml`) gespeichert. Ab v1.21 werden auch Fenstergrößen und Divider-Positionen beim Speichern gesichert.
---
## Station Settings (Stationseinstellungen)
![Stationseinstellungen](client_settings_window_station.png)
### Login und Chat-Kategorien
Hier werden die Zugangsdaten für den ON4KST-Chat eingetragen (Rufzeichen und Passwort).
Zudem wird die **primäre Chat-Kategorie** (z. B. IARU Region 1 VHF/Microwave) ausgewählt.
Mit der Option für einen **zweiten Chat** (Multi-Channel-Login) kann man sich gleichzeitig in eine weitere Kategorie (z. B. UHF/SHF) einloggen. Beide Chats werden dann parallel überwacht. Hier kann optional auch ein abweichender Login-Name für den zweiten Chat vergeben werden (nützlich für Opposite Station Multi-Callsign Logging).
### Rufzeichen und Locator
Eigenes Rufzeichen und Maidenhead-Locator (6-stellig, z. B. `JN49IJ`) eintragen. Diese Werte werden für Distanz- und Richtungsberechnungen benötigt.
### Aktivierte Bänder
Über die **„my station uses band"**-Checkboxen werden die aktiven Bänder ausgewählt. Nur für ausgewählte Bänder erscheinen Schaltflächen und Tabellenzeilen in der Benutzeroberfläche. Nach Änderungen muss die Software neu gestartet werden.
### Antennen-Öffnungswinkel (Antenna Beamwidth)
Einen realistischen Wert für den Öffnungswinkel der eigenen Antenne eintragen (in Grad). Dieser Wert wird für die [Sked-Richtungs-Hervorhebung](Funktionen#sked-richtungs-hervorhebung) verwendet. Ein Testwert von 50° hat sich bewährt; DM5M nutzt Quads mit 69°.
> **Keinesfalls** Fantasy-Werte eintragen die Richtungsberechnungen werden sonst unbrauchbar.
### Standard-Maximum-QRB
Maximale Entfernung (in km), für die Richtungs-Warnungen ausgelöst werden sollen. Realistischer Wert für DM5M: 900 km. Stationen, die weiter entfernt sind, werden für Highlighting-Zwecke ignoriert.
---
## 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).
### Universal File Based Callsign Interpreter (Simplelogfile)
Interpretiert beliebige Log-Dateien per Regex nach Rufzeichen-Mustern. Keine Bandinformation möglich. Geeignet als Fallback oder für nicht direkt unterstützte Logprogramme.
### Netzwerk-Listener für QSO-UDP-Broadcast
**Empfohlene Methode.** KST4Contest hört auf UDP-Pakete, die das Logprogramm beim Speichern eines QSOs an die Broadcast-Adresse sendet. Die Stationen werden mit Bandinformation markiert. UDP-Port: Standard **12060**. (Wird z. B. von UCXLog, N1MM+, QARTest, DXLog.net genutzt).
### Win-Test Network-Listener (Zusätzlicher UDP-Listener)
Dedizierter Netzwerk-Erkenner für Win-Test. KST4Contest empfängt und verarbeitet Win-Test-spezifische UDP-Pakete (inkl. Sked-Übergabe) auf dem dafür konfigurierten Port.
---
## TRX-Sync-Einstellungen
Empfängt die aktuelle Frequenz des Transceivers vom Logprogramm via UDP. Ermöglicht die automatische Befüllung der Variable `MYQRG`. Nützlich für:
- Schnelles Einfügen der eigenen QRG in Chat-Nachrichten.
- Automatische CQ-Baken mit aktueller Frequenz.
> **Hinweis für Multi-Setup**: Wenn zwei Logprogramme an zwei Computern betrieben werden, aber nur eine KST4Contest-Instanz, darf nur ein Logprogramm die Frequenzpakete senden. KST4Contest kann nicht zwischen den Quellen unterscheiden.
---
## AirScout-Einstellungen
Konfiguration der Schnittstelle zu AirScout für die Flugzeug-Scatter-Erkennung. Details: [AirScout-Integration](de-AirScout-Integration).
---
## Notification Settings (Benachrichtigungen)
Drei Benachrichtigungstypen stehen zur Wahl:
1. **Einfache Sounds**: TADA-Sound für eingehende Nachrichten, Tick für Sked-Richtungserkennung usw.
2. **CW-Ansage**: Das Rufzeichen einer Station, die eine Privatnachricht sendet, wird als CW-Signal ausgegeben.
3. **Phonetische Ansage**: Das Rufzeichen wird phonetisch ausgesprochen.
---
## Shortcut Settings (Schnellzugriff-Schaltflächen)
Konfiguration von Schnellzugriff-Schaltflächen, die direkt im Hauptfenster erscheinen. Ein Klick auf eine Schaltfläche fügt den konfigurierten Text in das Sendfeld ein. Alle [Variablen](Makros-und-Variablen#variablen) können verwendet werden.
---
## Snippet Settings (Text-Snippets)
Text-Snippets sind über folgende Wege abrufbar:
- **Rechtsklick** auf ein Rufzeichen in der Benutzerliste
- **Rechtsklick** in der CQ-Nachrichtentabelle
- **Rechtsklick** in der PM-Nachrichtentabelle
- **Tastenkombinationen**: `Ctrl+1` bis `Ctrl+0` für die ersten 10 Snippets
Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als Direktnachricht adressiert:
`/CQ RUFZEICHEN <Snippet-Text>`
---
## Beacon Settings (Automatischer Beacon)
Konfiguration eines automatischen Intervall-Beacons im öffentlichen Chat-Kanal. Empfohlen: Variable `MYQRG` im Text verwenden, damit die aktuelle Frequenz immer aktuell ist. Intervall und Text sind frei konfigurierbar.
> **Tipp**: Beacon beim CQ-Rufen aktivieren und im Einstellungsfenster schnell deaktivieren, wenn kein CQ gerufen wird.
---
## Messagehandling Settings (ab v1.25)
Neuer Einstellungsbereich mit folgenden Optionen:
- **Auto-Antwort auf alle eingehenden Nachrichten**: Automatische Antwort auf Privatnachrichten konfigurierbar.
- **Auto-Antwort mit eigener CQ-QRG**: Wenn jemand nach der eigenen QRG fragt, antwortet KST4Contest automatisch mit dem Inhalt der `MYQRG`-Variable.
- **Standard-Filter für das Userinfo-Fenster**: Voreingestellter Nachrichtenfilter für das Stationsinfo-Fenster konfigurierbar *(für Gianluca :-) )*.
---
## Win-Test-Netzwerk-Listener (ab v1.31)
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)
Die interne Worked-Datenbank enthält:
- Worked-Status aller Stationen (pro Band)
- NOT-QRV-Tags (seit v1.2)
**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.
---
## Dark Mode (ab v1.26)
Umschaltbar über das Menü: **Window → Use Dark Mode**. Die Farben können über CSS individuell angepasst werden.
---
## Einstellungen speichern
Nach **jeder** Änderung **„Save Settings"** klicken! Ohne Speichern gehen alle Änderungen beim nächsten Start verloren.
- Speicherort: unter Linux `~/.praktikst/preferences.xml` und unter Windows `%USERPROFILE%\.praktikst\preferences.xml` (bzw. `C:\Users\<Benutzername>\.praktikst\preferences.xml`)
- Ab v1.21: Fenstergrößen und Divider-Positionen werden ebenfalls gespeichert.
- Bei Problemen: Konfigurationsdatei löschen → KST4Contest erstellt eine neue mit Standardwerten.

View File

@@ -0,0 +1,138 @@
# Log-Synchronisation
> 🇬🇧 [English version](en-Log-Sync) | 🇩🇪 Du liest gerade die deutsche Version
KST4Contest markiert gearbeitete Stationen automatisch in der Chat-Benutzerliste. Dafür gibt es zwei grundlegende Methoden:
---
![Log-Synchronisation Einstellungsfenster](client_settings_window_logsync.png)
## Methode 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest liest eine Log-Datei und sucht mittels regulärem Ausdruck nach Rufzeichen-Mustern. Dabei werden auch binäre Logdateien unterstützt unlesbarer Binärinhalt wird einfach ignoriert.
**Vorteil**: Funktioniert mit nahezu jedem Logprogramm, das eine Datei schreibt.
**Nachteil**: Keine Bandinformation möglich es wird nur „gearbeitet" markiert, nicht auf welchem Band.
Pfad der Log-Datei in den Preferences eintragen. Die Datei wird nur gelesen, nie verändert (read-only).
> **Tipp**: Die Simplelogfile-Funktion kann auch genutzt werden, um Stationen zu markieren, die definitiv nicht erreichbar sind (z. B. eigene Notizen). Das wird in einer späteren Version durch ein besseres Tagging-System ersetzt.
---
## Methode 2: Netzwerk-Listener (UDP-Broadcast) Empfohlen
Das Logprogramm sendet beim Speichern eines QSOs ein UDP-Paket an die Broadcast-Adresse des Heimnetzwerks. KST4Contest empfängt dieses Paket und markiert die Station inklusive **Bandinformation** in der internen SQLite-Datenbank.
> **Wichtig**: KST4Contest muss **parallel zum Logprogramm laufen**. QSOs, die während einer Abwesenheit von KST4Contest geloggt werden, werden nicht erfasst außer bei QARTest (kann das komplette Log senden).
**Standard UDP-Port**: 12060 (entspricht dem Standard der meisten Logprogramme)
---
## Unterstützte Logprogramme
### UCXLog (DL7UCX)
![UCXLog Konfiguration](ucxlog_logsync.png)
UCXLog sendet QSO-UDP-Pakete und Transceiver-Frequenzpakete.
**Einstellungen in UCXLog:**
- UDP-Broadcast aktivieren
- IP-Adresse des KST4Contest-Computers eintragen (bei lokalem Betrieb: `127.0.0.1`)
- Port: 12060 (Standard)
Grün markierte Felder in den UCXLog-Einstellungen beachten: IP und Port müssen eingetragen werden.
Hinweis für Multi-Setup (2 Computer, 2 Radios, eine KST4Contest-Instanz): Beide Logprogramme müssen die QSO-Pakete an die IP des KST4Contest-Computers senden. Dann ist mindestens eine IP nicht `127.0.0.1`.
### QARTest (IK3QAR)
![QARTest Konfiguration](qartest_logsync.png)
**Besonderheit**: QARTest kann das **vollständige Log** an KST4Contest senden (Schaltfläche „Invia log completo" in den QARTest-Einstellungen). Damit werden auch QSOs erfasst, die vor dem Start von KST4Contest geloggt wurden.
**Einstellungen in QARTest:**
- UDP-Broadcast und IP/Port wie UCXLog konfigurieren
- „Invia log completo" für den vollständigen Log-Upload verwenden
*(„Buona funzionalità caro IK3QAR!" DO5AMF)*
### N1MM+
**Einstellungen in N1MM+:**
In N1MM+ unter `Config → Configure Ports, Mode Control, Winkey, etc. → Broadcast Data`:
- `Radio Info` aktivieren (für TRX-Sync/QRG)
- `Contact Info` aktivieren (für QSO-Sync)
- IP: `127.0.0.1` (oder IP des KST4Contest-Computers)
- Port: 12060
Für den integrierten DX-Cluster-Server: N1MM+ als DX-Cluster-Client konfigurieren (Server: `127.0.0.1`, Port wie in KST4Contest eingestellt).
### DXLog.net
![DXLog.net Konfiguration](dxlog_net_logsync.png)
**Einstellungen in DXLog.net:**
- UDP-Broadcast aktivieren
- IP des KST4Contest-Computers eintragen (grün markierte Felder)
- Port: 12060
### Win-Test
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*.
- Es kann zwischen den Sked-Modi "AUTO", "SSB" oder "CW" gewählt werden.
**Notwendige Einstellungen in KST4Contest:**
- `UDP-Port for Win-Test listener` (Standard: 9871).
- `Receive Win-Test network based UDP log messages` aktivieren.
- `Win-Test sked transmission (push via ADDSKED to Win-Test network)` aktivieren.
- `KST station name in Win-Test network (src of SKED packets)`: Legt fest, unter welchem Stationsnamen KST4Contest im WT-Netzwerk auftritt (z.B. "KST").
- `Win-Test station name filter`: Wenn hier ein Name eingetragen wird (z.B. "STN1"), werden nur QSOs von dieser bestimmten Win-Test Instanz verarbeitet. Leer lassen, um alle zu akzeptieren.
- `Win-Test network broadcast address`: Wird idR automatisch erkannt und ist erforderlich, um die Sked-Pakete ins Netzwerk zu senden.
**Einstellungen in Win-Test:**
- Das Netzwerk in Win-Test muss aktiv sein.
- Win-Test muss so konfiguriert sein, dass es seine Broadcasts an den entsprechenden Port (Standard 9871) sendet bzw. empfängt.
---
## TRX-Frequenz-Synchronisation
Neben der QSO-Synchronisation übertragen UCXLog und andere Programme auch die **aktuelle Transceiverfrequenz** via UDP. KST4Contest verarbeitet diese Information und stellt sie als Variable `MYQRG` bereit.
![FrequenzButtons](qrg_buttons.png)
**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.
> **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.
---
## Multi-Setup: 2 Radios, 2 Computer
Für DM5M-typische Setups (2 Radios, 2 Computer, eine KST4Contest-Instanz oder zwei separate):
**Variante A Eine gemeinsame KST4Contest-Instanz:**
- Beide Logprogramme senden QSO-Pakete an die IP des KST4Contest-Computers
- Nur ein Logprogramm sendet Frequenzpakete (empfohlen: das VHF-Logprogramm)
**Variante B Zwei separate KST4Contest-Instanzen (empfohlen):**
- Jedes Logprogramm kommuniziert mit seiner eigenen KST4Contest-Instanz via `127.0.0.1`
- Zwei separate Chat-Logins
- Bessere Trennung und weniger Konflikte
---
## Interne Datenbank
KST4Contest speichert die Worked-Information in einer internen **SQLite-Datenbank**. Diese ist von der Logprogramm-Datenbank unabhängig und wird nur über den UDP-Broadcast befüllt.
Vor jedem neuen Contest: Datenbank zurücksetzen! → [Konfiguration Worked Station Database Settings](Konfiguration#worked-station-database-settings)

View File

@@ -0,0 +1,162 @@
# Makros und Variablen
> 🇬🇧 [English version](en-Macros-and-Variables) | 🇩🇪 Du liest gerade die deutsche Version
KST4Contest bietet ein flexibles System aus Text-Snippets, Shortcuts und eingebauten Variablen, die den Chat-Workflow im Contest erheblich beschleunigen.
---
## Überblick
| Typ | Aufruf | Zweck |
|---|---|---|
| **Shortcuts** | Button in der Toolbar | Schneller Text-Insert ins Sendfeld |
| **Snippets** | Rechtsklick / Ctrl+1..0 | Text-Bausteine, optionaler PM-Versand |
| **Variablen** | In allen Text-Feldern verwendbar | Dynamische Werte (QRG, Locator, AP-Daten) |
---
## Shortcuts (Schnellzugriff-Schaltflächen)
Konfigurierbar in den Preferences → **Shortcut Settings**.
- Jeder konfigurierte Text erzeugt **einen Button** in der Benutzeroberfläche.
- Ein Klick fügt den Text in das **Sendfeld** ein.
- **Alle Variablen** können in Shortcuts verwendet werden und werden beim Einfügen sofort aufgelöst.
- Auch längere Texte möglich.
**Tipp**: Häufig verwendete Abkürzungen wie „pse", „rrr", „tnx", „73" als Shortcuts anlegen.
---
## Snippets (Text-Bausteine)
Konfigurierbar in den Preferences → **Snippet Settings**.
### Aufruf
- **Rechtsklick** auf ein Rufzeichen in der Benutzerliste
- **Rechtsklick** in der CQ-Nachrichtentabelle
- **Rechtsklick** in der PM-Nachrichtentabelle
- **Tastaturkürzel**: `Ctrl+1` bis `Ctrl+0` für die ersten 10 Snippets
### Verhalten mit ausgewähltem Rufzeichen
Wenn in der Benutzerliste ein Rufzeichen ausgewählt ist, wird der Snippet als **Privatnachricht** adressiert:
```
/CQ RUFZEICHEN <Snippet-Text>
```
Anschließend kann mit **Enter** direkt gesendet werden auch wenn das Sendfeld nicht den Fokus hat.
### Hardware-Makro-Tastatur
*(Idee von IU3OAR, Gianluca Costantino)*
Die Tastenkombinationen `Ctrl+1` bis `Ctrl+0` können auf einer programmierbaren Makro-Tastatur belegt werden. Ein weiterer Tastendruck (auf eine „Enter"-Taste) sendet den Text sofort. Im Contest-Betrieb spart das erheblich Zeit.
### Vordefinierte Standard-Snippets
Beim ersten Start werden einige Snippets vorbelegt, z. B.:
- `Hi OM, try sked?`
- `I am calling cq ur dir, pse lsn to me at MYQRG`
- `pse ur qrg?`
- `rrr, I move to your qrg nw, pse ant dir me`
Diese können in den Preferences angepasst oder gelöscht werden.
---
## Variablen
Variablen werden in geschriebenen Texten (Snippets, Shortcuts, Beacon, Sendfeld) durch ihre aktuellen Werte ersetzt. Einfach den Variablennamen **großgeschrieben** in den Text einfügen.
### MYQRG
Wird durch die aktuelle Transceiverfrequenz ersetzt.
- Quelle: TRX-Sync via UDP vom Logprogramm (wenn aktiviert)
- Fallback: Manuell eingetragener Wert im MYQRG-Textfeld rechts neben dem Sendbutton
- Format: `144.388.03`
**Beispiel**: `calling cq at MYQRG``calling cq at 144.388.03`
### MYQRGSHORT
Wie MYQRG, aber nur die ersten 7 Zeichen.
- Format: `144.388`
**Beispiel**: `qrg: MYQRGSHORT``qrg: 144.388`
### MYLOCATOR
Wird durch den eigenen Maidenhead-Locator (6-stellig) ersetzt.
- Format: `JO51IJ`
**Beispiel**: `my loc: MYLOCATOR``my loc: JO51IJ`
### MYLOCATORSHORT
Wie MYLOCATOR, aber nur die ersten 4 Zeichen.
- Format: `JO51`
**Beispiel**: `loc: MYLOCATORSHORT``loc: JO51`
### QRZNAME
Wird durch den **Namen** der aktuell ausgewählten Station aus dem Chat-Namenfeld ersetzt.
**Beispiel**: `Hi QRZNAME, sked?``Hi Gianluca, sked?`
### FIRSTAP
Wird durch Daten des ersten reflektierbaren Flugzeugs zur ausgewählten Station ersetzt (sofern vorhanden).
- Bedingung: AirScout ist aktiv und ein Flugzeug ist verfügbar.
- Format-Beispiel: `a very big AP in 1 min`
**Beispiel**: `AP info: FIRSTAP``AP info: a very big AP in 1 min`
### SECONDAP
Wie FIRSTAP, aber für das zweite verfügbare Flugzeug.
- Format-Beispiel: `Next big AP in 9 min`
**Beispiel**: `also: SECONDAP``also: Next big AP in 9 min`
### MYQTF *(geplant für v1.3)*
Wird durch die aktuelle Antennenrichtung in Worten ersetzt (z. B. `north`, `north east`, `east`, …).
- Quelle: Winkelwert im MYQTF-Eingabefeld (rechts neben dem MYQRG-Feld)
---
## Variablen im Beacon
Alle Variablen können auch im **automatischen Beacon** (Intervall-Nachrichten) verwendet werden. Empfohlene Beacon-Konfiguration:
```
calling cq at MYQRG, loc MYLOCATOR, GL all!
```
Da KST4Contest QRG-Daten automatisch aus Chat-Nachrichten ausliest: Wenn andere Stationen ebenfalls KST4Contest nutzen, sehen sie die eigene QRG sofort in der QRG-Spalte der Benutzerliste.
---
## Beispiel-Workflow mit Makros im Contest
1. Station in der Benutzerliste auswählen → Rufzeichen ist nun vorausgewählt.
2. `Ctrl+1` drücken → Snippet „Hi OM, try sked?" wird als PM adressiert.
3. Enter drücken → Nachricht wird gesendet.
4. Station antwortet mit Frequenz → QRG-Spalte wird automatisch befüllt.
5. `Ctrl+2` → Snippet „I am calling cq ur dir, pse lsn to me at 144.388" (MYQRG aufgelöst).
6. Enter → Gesendet.
Ohne manuelle Tipparbeit, ohne Fehler, ohne Unterbrechung des CQ-Rufens.

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,116 @@
# AirScout Integration
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-AirScout-Integration)
AirScout (by DL2ALF) is a program for detecting aircraft for aircraft scatter operation. KST4Contest is tightly integrated with AirScout and shows reflectable aircraft directly in the user list.
> **Aircraft Scatter** enables very long-distance communication on VHF and higher even for stations with low altitude above sea level or unfavourable topographic conditions.
---
## Downloading AirScout
Download AirScout from:
- http://airscout.eu/index.php/download
---
## Aircraft Data Feeds (ADSB)
Public aircraft data feeds on the internet are often unreliable and limited in use. A recommended alternative is the dedicated ADSB feed service provided by **OV3T (Thomas)**:
- https://airscatter.dk/
- https://www.facebook.com/groups/825093981868542
An account is required for this service. Please consider donating to Thomas the server costs are not free!
---
## Setting Up AirScout
### Step 1: Configure the ADSB Feed in AirScout
1. Start AirScout.
2. Enter your OV3T feed account details (username, password, URL) in the AirScout settings.
![AirscoutStep1](as_plane_feed_1.png)
![AirscoutStep2](as_plane_feed_2.png)
3. Test the connection.
### Step 2: Enable UDP Communication for KST4Contest
In AirScout, enable the UDP interface:
- Activate the corresponding checkbox in the AirScout settings (only one checkbox needed).
- Do not change the default ports unless there is a specific reason.
### Step 3: KST4Contest Settings
In KST4Contest Preferences → **AirScout Settings**:
- Enable AirScout communication
- Leave IP and port at their default values (unless changed)
![AirscoutStep3](as_plane_feed_3.png){ width=85% }
---
## Communication Between KST4Contest and AirScout (from v1.263)
**Improvement in v1.263**: KST4Contest now only sends stations to AirScout whose QRB (distance) is less than the configured **maximum QRB**. The query interval has been extended from 12 seconds to **60 seconds**.
**Benefits:**
- Significantly less computation load for AirScout
- Significantly less message traffic
- The tracking issue with the "Show Path in AirScout" button is greatly improved
- Less overall CPU usage
Additionally: The name of the KST4Contest client and AirScout server was previously hardcoded (`KST` and `AS`). From v1.263, the names configured in the Preferences are used.
---
## Multiple KST4Contest Instances and AirScout
> **Note**: If multiple KST4Contest instances are running simultaneously and AirScout communication is enabled on both, AirScout will respond **to both instances**.
This is not a problem if:
- Both instances use the same locator, **or**
- Both instances have different login callsigns.
Otherwise, it may result in incorrect AP data.
---
## AP Column in the User List
After setup, an **AP column** appears in the user list showing up to two reflectable aircraft per station.
Example display:
| Station | AP Info |
|---|---|
| DF9QX | 2 Planes: 0 min / 0 min, 100% each |
| F5DYD | 2 Planes: 14 min / 31 min, 50% each |
AP information is also available in the **private messages window**.
The percentage indicates the reflection potential (aircraft size, altitude, distance).
---
## AP Variables in Messages
Aircraft data can be inserted directly into messages:
- `FIRSTAP` → e.g. `a very big AP in 1 min`
- `SECONDAP` → e.g. `Next big AP in 9 min`
Details: [Macros and Variables](Macros-and-Variables#variables)
---
## "Show Path in AirScout" Button
In the user list there is a button with an arrow showing the direction (QTF) to the selected station. Clicking it maximises AirScout and shows the path with reflectable aircraft to the selected contact.

235
github_docs/en-Changelog.md Normal file
View File

@@ -0,0 +1,235 @@
# Changelog
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Changelog)
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**
**Changed:**
- AirScout communication fundamentally revised: Only stations with QRB < max-QRB are now sent to AirScout.
- Query interval extended from 12 seconds to **60 seconds**.
- Significantly less computation load and message traffic → more stable AirScout tracking.
- Name of the AS client and AS server is now configurable from the Preferences (was previously hardcoded to "KST" / "AS").
**Fixed:**
- "Track in AirScout" button was very sluggish → greatly improved by new communication logic.
- Name in chat is now saveable (bug fixed).
- Visual corrections before and after login.
- Bug fixed that was reported by 9A2HM (Kreso).
---
## v1.262 (2025-05-21)
**Freeze Fix for Early Message Delivery**
**Fixed:**
- ON4KST sometimes delivers messages before login is complete. This caused errors in the message processing engine → now fixed.
---
## v1.26 (2025-05)
**Multi-Channel Login and Dark Mode**
**New:**
- **Dark Mode**: Toggle via `Window → Use Dark Mode`.
- **Multi-channel login**: Simultaneous login to two chat categories.
- **Opposite station multi-callsign login tagging**: Support for stations with multiple callsigns.
**Changed:**
- Colouring mechanism revised: Colours can now be customised via CSS.
**Fixed:**
- Station tagging completely revised and corrected.
---
## v1.251 (2025-02)
**Bugfix for UDP Broadcast Spot Info**
**Fixed:**
- Problem reading UDP broadcast spot information fixed (reported by Steve Clements thank you!).
- Station tagging (further improved).
---
## v1.25 (2025-02)
**Wishlist Time**
**New:**
- **New settings tab: Messagehandling**
- Auto-reply to incoming messages configurable.
- Automatic reply with own CQ QRG when someone asks for it.
- Configurable default filter for the userinfo window *(for Gianluca :-) )*.
- **Coloured PM rows**: New private messages appear red and fade every 30 seconds from yellow to white *(idea by IU3OAR, Gianluca)*.
**Fixed:**
- Stations with suffixes like "-2" and "-70" were not being marked as worked → now ignored, station is correctly marked.
---
## v1.24 (2024-11)
**Wishlist + DX Cluster Spots**
**New:**
- Button to open the **QRZ.com profile** of the selected station.
- Button to open the **QRZ-CQ profile** of the selected station.
- **DX Cluster Server integration**: Direction warnings are sent as spots to the logging software (when QRG is known).
*(Coloured PM row feature also added tnx Gianluca)*
---
## v1.23 (2024-10)
**Built-in DX Cluster Server**
**New:**
- KST4Contest now contains a **built-in DX cluster server**.
- Generates DX cluster spots and sends them to the logging software when a direction warning is triggered and a QRG is known.
- Spotter callsign must differ from the contest callsign (for correct filtering in the logging software).
*(Idea by OM0AAO, Viliam Petrik thank you!)*
---
## v1.22 (2024-05)
**Usability Improvements and AirScout Button Fix**
**New:**
- New variables (tnx OM0AAO, Viliam Petrik):
- `MYLOCATORSHORT`
- `MYQRGSHORT`
- `QRZNAME`
**Changed:**
- Send field focus: After clicking a callsign in the user list, the send field immediately receives focus no double-click needed *(tnx Gianluca)*.
**Fixed:**
- Worked-station filter is now live: Worked stations disappear immediately when the filter is activated *(tnx Gianluca)*.
- QRB sorting was lexicographic → now numeric *(tnx Alessandro Murador)*.
- AirScout "Show Path" button: Click now maximises AirScout and correctly shows the path.
---
## v1.21 (2024-04)
**Usability Improvements**
**Changed:**
- Window sizes and divider positions are saved in the configuration file when clicking "Save Settings" and restored on startup.
- Filter section as flowpane → better display on smaller screens.
---
## v1.2 (2024-04)
**Band Selection and NOT-QRV Tags**
**New:**
- **Band selection**: Selectable in Preferences which bands are active. Only buttons and fields for selected bands appear in the UI. Save and restart required.
- **NOT-QRV tags per station and band**: Stations can be marked as "not QRV" for each band. Combinable with the user list filter.
- **QTF arrow**: The "Show path in AS" button now shows an arrow with the QTF of the selected station.
---
## Earlier Versions
### v1.1
First publicly released version. Core features:
- Worked marking via Simplelogfile and UDP
- Sked direction highlighting
- QRG detection
- Text snippets and shortcuts
- AirScout interface (first version)
- Interval beacon
- PM catching for public messages containing your own callsign
- Update notification service
---
## Planned Features
- `MYQTF` variable (own antenna direction as text)
- ~~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

View File

@@ -0,0 +1,200 @@
# Configuration
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Konfiguration)
After the first start, the **settings window** opens this is the central starting point for all configuration. It is recommended to keep the settings window open during operation (e.g. to quickly toggle the beacon on and off).
> **Important**: Always click **"Save Settings"** after any change! Settings are stored in `~/.praktikst/preferences.xml` on Linux and in `%USERPROFILE%\.praktikst\preferences.xml` (or `C:\Users\<Username>\.praktikst\preferences.xml`) on Windows. From v1.21 onwards, window sizes and divider positions are also saved when you click Save.
---
## Station Settings
![Station Settings](client_settings_window_station.png)
### Login and Chat Categories
Enter your ON4KST chat credentials here (callsign and password).
Also, select the **primary chat category** (e.g., IARU Region 1 VHF/Microwave).
With the option for a **second chat** (Multi-Channel Login), you can log in to another category simultaneously (e.g., UHF/SHF). Both chats will then be monitored in parallel. You can optionally specify a different login name for the second chat (useful for Opposite Station Multi-Callsign Logging).
### Callsign and Locator
Enter your own callsign and Maidenhead locator (6 characters, e.g., `JN49IJ`). These values are needed for distance and direction calculations.
### Active Bands
Use the **"my station uses band"** checkboxes to select the active bands. Buttons and table rows will only appear in the user interface for selected bands. The software must be restarted after making changes.
### Antenna Beamwidth
Enter a realistic value for your antenna's beamwidth (in degrees). This value is used for the [Sked Direction Highlighting](Features#sked-direction-highlighting). A test value of 50° has proven effective; DM5M uses quads with 69°.
> **Do not** enter fantasy values the direction calculations will become useless.
### Default Maximum QRB
Maximum distance (in km) for which direction warnings should be triggered. A realistic value for DM5M is 900 km. Stations farther away are ignored for highlighting purposes.
---
## 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
Three methods are available for automatically marking worked stations. Details: [Log Synchronisation](en-Log-Sync).
### Universal File Based Callsign Interpreter (Simplelogfile)
Interprets any log file using regex for callsign patterns. No band information is available. Suitable as a fallback or for log programs that are not directly supported.
### Network Listener for Logger's QSO UDP Broadcast
**Recommended method.** KST4Contest listens for UDP packets sent by the logging software to the broadcast address when a QSO is saved. Stations are marked with band information. UDP port: default **12060**. (Used by UCXLog, N1MM+, QARTest, DXLog.net, etc.).
### Win-Test Network Listener (Additional UDP Listener)
A dedicated network listener for Win-Test. KST4Contest receives and processes Win-Test-specific UDP packets (including sked handovers) on the configured port.
---
## TRX Sync Settings
Receives the current transceiver frequency from the logging software via UDP. This enables the automatic population of the `MYQRG` variable. Useful for:
- Quickly inserting your own QRG into chat messages.
- Automatic CQ beacon with current frequency.
> **Note for multi-setup**: When running two logging programs on two computers but only one KST4Contest instance, only one logging program should send frequency packets. KST4Contest cannot distinguish between sources.
---
## AirScout Settings
Configuration of the interface to AirScout for aircraft scatter detection. Details: [AirScout Integration](en-AirScout-Integration).
---
## Notification Settings
Three notification types are available:
1. **Simple sounds**: TADA sound for incoming messages, tick for sked direction detection, etc.
2. **CW announcement**: The callsign of a station sending a private message is output as a CW signal.
3. **Phonetic announcement**: The callsign is pronounced phonetically.
---
## Shortcut Settings
Configuration of quick-access buttons that appear directly in the main window. Clicking a button inserts the configured text into the send field. All [variables](Macros-and-Variables#variables) can be used.
---
## Snippet Settings
Text snippets are accessible via:
- **Right-click** on a callsign in the user list
- **Right-click** in the CQ message table
- **Right-click** in the PM message table
- **Keyboard shortcuts**: `Ctrl+1` to `Ctrl+0` for the first 10 snippets
If a callsign is selected in the user list, the snippet is addressed as a direct message:
`/CQ CALLSIGN <snippet text>`
---
## Beacon Settings
Configuration of an automatic interval beacon in the public chat channel. Recommended: use the `MYQRG` variable in the text so the current frequency is always up to date. Interval and text are freely configurable.
> **Tip**: Enable the beacon when calling CQ and quickly disable it in the settings window when not calling.
---
## Messagehandling Settings (from v1.25)
New settings section with the following options:
- **Auto-reply to all incoming messages**: Configurable automatic reply to private messages.
- **Auto-reply with own CQ QRG**: When someone asks for your QRG, KST4Contest automatically replies with the content of the `MYQRG` variable.
- **Default filter for the userinfo window**: Pre-configured message filter for the station info window *(for Gianluca :-) )*.
---
## 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
The internal worked database contains:
- Worked status of all stations (per band)
- NOT-QRV tags (since v1.2)
**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.
---
## Dark Mode (from v1.26)
Toggle via the menu: **Window → Use Dark Mode**. The colors can be individually customized via CSS.
---
## Saving Settings
Click **"Save Settings"** after **every** change! Without saving, all changes will be lost on the next start.
- Storage location: `~/.praktikst/preferences.xml` on Linux and `%USERPROFILE%\.praktikst\preferences.xml` (or `C:\Users\<Username>\.praktikst\preferences.xml`) on Windows
- From v1.21: Window sizes and divider positions are also saved.
- If you encounter problems: delete the configuration file → KST4Contest will create a new one with default values.

View File

@@ -0,0 +1,76 @@
# Built-in DX Cluster Server
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-DX-Cluster-Server)
From **version 1.23**, KST4Contest includes a built-in DX cluster server. It sends spots directly to the logging software whenever a direction warning is triggered.
*(Idea by OM0AAO, Viliam Petrik thank you!)*
---
## What is the Built-in DX Cluster Server For?
When KST4Contest detects that a station is requesting a sked from your direction and a QRG is known, it **automatically generates a DX cluster spot** and feeds it directly to the logging software's cluster client / band map.
The logging software then displays the spot in the band map. Clicking the spot sets the transceiver's frequency and mode directly without any manual typing.
---
## Setup
### In KST4Contest
In Preferences → **DX Cluster Server Settings**:
1. Enter the **port** of the internal server (e.g. 7300 or 8000 must match the logging software).
2. Enter a **spotter callsign** **this must be a different callsign than your contest callsign!**
- Reason: Logging programs filter spots from your own callsign as "already worked". If the spotter uses the same callsign, the spots will not be displayed.
3. Enter the **assumed MHz**: For frequency references like ".205" in the chat, KST4Contest needs to decide whether 144.205, 432.205 or 1296.205 is meant. For single-band contests, simply enter the corresponding band centre. Full frequency references like "144.205" or "1296.338" in the chat are always correctly identified.
### In UCXLog
- Configure a DX cluster server connection:
- Host: `127.0.0.1` (or IP of the KST4Contest computer)
- Port: As configured in KST4Contest
- Password: can be left empty
- Use the **"Send a test message to your log"** button to test the connection.
### In N1MM+
Similar settings:
- Host: `127.0.0.1` (or IP of the KST4Contest computer)
- Port: As configured in KST4Contest
---
## How It Works
A spot is generated when **both** conditions are met:
1. A **direction warning** has been triggered (station is making a sked in your direction).
2. The **station's QRG is known** (read from the chat or manually entered).
The generated spot contains:
- Station's callsign
- Frequency
- Spot time
The logging software can then display the spot in the band map and tune the TRX to that frequency with a mouse click.
---
## Multi-Computer Setup
If KST4Contest runs on a separate computer (not the logging computer):
- Host in the logging software: IP of the KST4Contest computer (not `127.0.0.1`)
- Same configuration as for the QSO UDP broadcast packets (see [Log Synchronisation](en-Log-Sync))
---
## Tested Logging Software
- **UCXLog** ✓
- **N1MM+** ✓
Further test reports are welcome please send by email to DO5AMF.

235
github_docs/en-Features.md Normal file
View File

@@ -0,0 +1,235 @@
# Features
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Funktionen)
Overview of all main features of KST4Contest.
---
## Sked Direction Highlighting
One of the core features: when a station makes a sked request **towards your direction**, it is highlighted **green and bold** in the user list.
### How does it work?
The calculation is based on the following logic:
- When station A sends a sked request to station B, it is assumed that A is pointing its antenna towards B.
- If the resulting direction from A to your own station is within half the beamwidth of your own antenna, A is highlighted.
**Example** (beamwidth 69°, half-angle 34.5°):
| Situation | Result for DO5AMF in JN49 |
|---|---|
| Sked from F5FEN → DM5M | ✅ Highlighted (F5FEN points towards DM5M, close to JN49) |
| Sked from DM5M → F5FEN | ✅ Highlighted (DM5M replies towards F5FEN) |
| F1DBN is uninvolved | ❌ No highlighting |
| DO5AMF/P (different location) | ❌ No highlighting for sked reply |
The calculation does not include topographic path calculations this is a deliberate simplification. It may be added in a future version.
> Configuration: [Configuration Antenna Beamwidth](Configuration#antenna-beamwidth)
---
## Sked Direction Spots (Built-in DX Cluster)
From **v1.23**: Direction warnings are forwarded as DX cluster spots to the logging software when a QRG is known. Details: [DX Cluster Server](en-DX-Cluster-Server).
---
## QRG Detection (QRG Reading)
KST4Contest processes every line of text flowing through the channel and automatically extracts **frequency references**. These are displayed in the user list in the **QRG column**.
Recognised formats: `144.205`, `432.088`, `.205` (with configured band assumption), etc.
**Benefit**: Without asking, you can directly look up a station's calling frequency and decide whether a contact is possible.
---
## Worked Marking
Worked stations are visually marked in the user list per band. Based on [Log Synchronisation](en-Log-Sync) via UDP or Simplelogfile.
Reset the database before each contest: [Configuration Worked Station Database Settings](Configuration#worked-station-database-settings).
---
## NOT-QRV Tags (from v1.2)
When a station indicates it is not QRV on a specific band, this can be manually marked:
1. Select the station in the user list.
2. Right-click → Set NOT-QRV for the appropriate band.
These tags are stored in the internal database and persist after a KST4Contest restart. Can be reset via the settings.
**Benefit**: Prevents repeated sked requests on bands where the station is not active saves time for both sides.
---
## Direction Filter
Shows only stations in the user list that are located in a specific direction. Toggle using the N / NE / E / SE / S / SW / W / NW buttons or by entering degrees manually.
Useful: While calling CQ in a specific direction, only show stations in that direction.
---
## Distance Filter
Hide stations beyond a maximum distance. The **"Show only QRB [km] <="** button is a toggle.
---
## Worked and NOT-QRV Filter
Toggle buttons (one per band) to hide already-worked stations and/or NOT-QRV-tagged stations. The filter takes effect **immediately** without manually reactivating (live since v1.22).
---
## Coloured PM Rows (from v1.25)
New private messages appear in **red**. The colour fades every 30 seconds from yellow to white like a rainbow fade. This makes it immediately clear how recent a message is.
*(Idea by IU3OAR, Gianluca Costantino thank you!)*
---
## PM Catching
Some users accidentally post direct messages publicly, e.g.:
```
(DM5M) pse ur qrg
```
KST4Contest detects such messages that contain your own callsign and automatically sorts them into the **private messages table**. No messages are missed this way.
---
## Multi-Channel Login (from v1.26)
Simultaneous login to **two chat categories** (e.g. 144 MHz and 432 MHz). Both chats are monitored in parallel.
---
## Dark Mode (from v1.26)
Toggle via: **Window → Use Dark Mode**
For individual colour adjustments: edit the CSS file (path in the program settings).
---
## Opposite Station Multi-Callsign Login Tagging (from v1.26)
Support for stations that are active in the chat with multiple callsigns simultaneously (e.g. expedition setups).
---
## QRZ.com and QRZ-CQ Profile Buttons (from v1.24)
For selected stations in the user list, there are direct buttons to open the **QRZ.com profile** and the **QRZ-CQ profile** in the browser.
---
## Sked Reminders with ALERT (from v1.40)
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.
---
## QSO Sniffer (from v1.31)
The QSO sniffer monitors the chat for messages from a configurable callsign list and automatically forwards them to the **PM window**. This prevents relevant messages from being lost in the general chat traffic.
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)
- Sked direction (degrees)
- Sked success rate and skedfail markings
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.
---
## Interval Beacon
Automatic CQ messages in the public channel at a configurable interval. Recommended: use the `MYQRG` variable so the current frequency is always accurate. Details: [Configuration Beacon Settings](Configuration#beacon-settings).
---
## Simplelogfile
File-based log evaluation using regex. Details: [Log Synchronisation](Log-Sync#method-1-universal-file-based-callsign-interpreter-simplelogfile).
---
## Cluster & QSO of Others
A separate window showing the QSO flow between other stations. Particularly interesting during quieter night-time hours of a contest. This window can be minimised when not needed. Future plan: filtering to stations in your selected QTF.

51
github_docs/en-Home.md Normal file
View File

@@ -0,0 +1,51 @@
# KST4Contest Wiki
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Home)
**KST4Contest** (also known as *PraktiKST*) is a Java-based chat client for the [ON4KST Chat](http://www.on4kst.info/chat/), specifically designed for contest operation on the VHF/UHF/SHF bands (144 MHz and above).
Developed by **DO5AMF (Marc Fröhlich)**, operator at DM5M.
---
## Quick Navigation
| Page | Contents |
|---|---|
| [Installation](en-Installation) | Download, Java requirements, update |
| [Configuration](en-Configuration) | All settings in detail |
| [Log Synchronisation](en-Log-Sync) | UCXLog, N1MM+, QARTest, DXLog.net, WinTest |
| [AirScout Integration](en-AirScout-Integration) | Aircraft scatter detection |
| [DX Cluster Server](en-DX-Cluster-Server) | Built-in DX cluster for your logging software |
| [Features](en-Features) | All features at a glance |
| [Macros and Variables](en-Macros-and-Variables) | Text snippets, shortcuts, variables |
| [User Interface](en-User-Interface) | UI explanation and operation |
| [Changelog](en-Changelog) | Version history |
---
## What is KST4Contest?
The ON4KST Chat is the de-facto standard for skeds on the 144 MHz and higher bands. KST4Contest enhances the chat experience with contest-specific features:
- **Worked marking**: Stations already worked are highlighted visually, synchronised directly from your logging software via UDP.
- **Sked direction detection**: When a station calls another one from your direction, it is highlighted green and bold.
- **QRG detection**: KST4Contest automatically reads frequencies from the chat traffic and shows them in the user list.
- **AirScout interface**: Reflectable aircraft are shown directly in the user list.
- **Built-in DX cluster server**: Spots are sent directly to your logging software.
- **Dark mode** (from v1.26): Easy on the eyes during night-time operation.
- **Multi-channel login** (from v1.26): Simultaneously logged into two chat categories.
---
## Contact & Support
- **Email**: praktimarc+kst4contest@gmail.com *(for kst4contest topics only)*
- **GitHub**: https://github.com/praktimarc/kst4contest
- **Download**: https://github.com/praktimarc/kst4contest/releases/latest
---
## Acknowledgements
Special thanks to: Gianluca Costantino (IU3OAR), Alessandro Murador (IZ3VTH), Reczetár István (HA1FV), OM0AAO (Viliam Petrik, DX cluster idea), DC9DJ (Konrad Neitzel, project structure), DO5ALF (Andreas, webmaster funkerportal.de), PE0WGA (Franz van Velzen, tester) and all other testers and contributors.

View File

@@ -0,0 +1,114 @@
# Installation
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Installation)
## 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:
- Register at: http://www.on4kst.info/chat/register.php
### Chat Etiquette
The official language in the ON4KST Chat is **English**. Please use English even when communicating with stations from your own country. Common HAM abbreviations (agn, dir, pse, rrr, tnx, 73 …) are widely used and understood.
### Personal Messages
To send a private message to another station, always use the following format:
```
/CQ CALLSIGN message text
```
Example: `/CQ DL5ASG pse sked 144.205?`
During heavy chat traffic (56 messages per second in a contest), public messages directed at a specific callsign are easily missed. However, KST4Contest also catches such messages if they are accidentally posted publicly (see [Features PM Catching](Features#catching-personal-messages)).
---
## Download
### Windows
The latest version can be downloaded as a ZIP file:
**https://github.com/praktimarc/kst4contest/releases/latest**
The filename has the format `praktiKST-v<version_number>-windows-x64.zip`.
### Linux
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`.
---
## Installation
### Windows
1. Download the ZIP file.
2. Unzip the ZIP file into a folder of your choice.
3. Run `praktiKST.exe`.
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`)
4. Run the AppImage.
Settings are stored at `~/.praktikst/preferences.xml`.
---
## Updating
KST4Contest includes an **automatic update notification service**: as soon as a new version is available, a window will appear at startup with:
- information that a new version is available,
- a changelog,
- the download link for the new version.
![Example Update Window](update_window.png)
### Update Process
#### Windows
Currently, there is only one way to update:
1. Delete the old folder.
2. Unzip the new ZIP file.
The settings file (`preferences.xml`) is preserved because it is stored in the user folder, not the program folder.
#### Linux
Currently as follows:
1. Download the new AppImage
2. Mark the new AppImage as executable
3. (optional) Delete the old AppImage.
---
## Known Issues at Startup
### Norton 360
Norton 360 classifies `praktiKST.exe` as dangerous (false positive). An exception must be created for the file:
1. Open Norton 360.
2. Security → History → Find the corresponding event.
3. Select "Restore & Add Exception".
*(Reported by PE0WGA, Franz van Velzen thank you!)*

138
github_docs/en-Log-Sync.md Normal file
View File

@@ -0,0 +1,138 @@
# Log Synchronisation
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Log-Synchronisation)
KST4Contest automatically marks worked stations in the chat user list. Two basic methods are available:
---
![Log Sync Settings Window](client_settings_window_logsync.png)
## Method 1: Universal File Based Callsign Interpreter (Simplelogfile)
KST4Contest reads a log file and searches for callsign patterns using a regular expression. Binary log files are also supported unreadable binary content is simply ignored.
**Advantage**: Works with almost any logging program that writes a file.
**Disadvantage**: No band information available stations are only marked as "worked", not on which band.
Enter the path to the log file in the Preferences. The file is only read, never modified (read-only).
> **Tip**: The Simplelogfile function can also be used to mark stations that are definitely unreachable (e.g. personal notes). This will be replaced in a later version by a better tagging system.
---
## Method 2: Network Listener (UDP Broadcast) Recommended
When saving a QSO, the logging software sends a UDP packet to the broadcast address of the home network. KST4Contest receives this packet and marks the station including **band information** in its internal SQLite database.
> **Important**: KST4Contest must be **running in parallel with the logging software**. QSOs logged while KST4Contest is not running will not be captured except with QARTest (which can send the complete log).
**Default UDP port**: 12060 (matches the default of most logging programs)
---
## Supported Logging Software
### UCXLog (DL7UCX)
![UCXLog Configuration](ucxlog_logsync.png)
UCXLog sends QSO UDP packets and transceiver frequency packets.
**Settings in UCXLog:**
- Enable UDP broadcast
- Enter the IP address of the KST4Contest computer (for local operation: `127.0.0.1`)
- Port: 12060 (default)
Note the green-highlighted fields in the UCXLog settings: IP and port must be filled in.
Note for multi-setup (2 computers, 2 radios, one KST4Contest instance): Both logging programs must send QSO packets to the IP of the KST4Contest computer. In this case, at least one IP is not `127.0.0.1`.
### QARTest (IK3QAR)
![QARTest Configuration](qartest_logsync.png)
**Special feature**: QARTest can send the **complete log** to KST4Contest (button "Invia log completo" in the QARTest settings). This means QSOs logged before KST4Contest was started are also captured.
**Settings in QARTest:**
- Configure UDP broadcast and IP/port as with UCXLog
- Use "Invia log completo" for a full log upload
*(„Buona funzionalità caro IK3QAR!" DO5AMF)*
### N1MM+
**Settings in N1MM+:**
In N1MM+ under `Config → Configure Ports, Mode Control, Winkey, etc. → Broadcast Data`:
- Enable `Radio Info` (for TRX sync / QRG)
- Enable `Contact Info` (for QSO sync)
- IP: `127.0.0.1` (or IP of the KST4Contest computer)
- Port: 12060
For the built-in DX cluster server: configure N1MM+ as a DX cluster client (server: `127.0.0.1`, port as set in KST4Contest).
### DXLog.net
![DXLog.net Configuration](dxlog_net_logsync.png)
**Settings in DXLog.net:**
- Enable UDP broadcast
- Enter the IP of the KST4Contest computer (green-highlighted fields)
- Port: 12060
### Win-Test
Win-Test is supported with a dedicated UDP network listener that understands the native Win-Test network protocol.
**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*.
- You can choose between "AUTO", "SSB", or "CW" sked modes.
**Required Settings in KST4Contest:**
- `UDP-Port for Win-Test listener` (Default: 9871).
- 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.
**Settings in Win-Test:**
- The network in Win-Test must be active.
- Win-Test must be configured to send/receive its broadcasts on the corresponding port (default 9871).
---
## TRX Frequency Synchronisation
In addition to QSO synchronisation, UCXLog and other programs also transmit the **current transceiver frequency** via UDP. KST4Contest processes this information and makes it available as the `MYQRG` variable.
![Frequency Buttons](qrg_buttons.png)
**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.
> **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.
---
## Multi-Setup: 2 Radios, 2 Computers
For DM5M-style setups (2 radios, 2 computers, one KST4Contest instance or two separate):
**Option A One shared KST4Contest instance:**
- Both logging programs send QSO packets to the IP of the KST4Contest computer
- Only one logging program sends frequency packets (recommended: the VHF logging program)
**Option B Two separate KST4Contest instances (recommended):**
- Each logging program communicates with its own KST4Contest instance via `127.0.0.1`
- Two separate chat logins
- Better separation and fewer conflicts
---
## Internal Database
KST4Contest stores worked information in an internal **SQLite database**. This is independent of the logging program's database and is only populated via the UDP broadcast.
Before each new contest: reset the database! → [Configuration Worked Station Database Settings](Configuration#worked-station-database-settings)

View File

@@ -0,0 +1,162 @@
# Macros and Variables
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Makros-und-Variablen)
KST4Contest offers a flexible system of text snippets, shortcuts and built-in variables that significantly speed up the chat workflow during contests.
---
## Overview
| Type | Access | Purpose |
|---|---|---|
| **Shortcuts** | Button in the toolbar | Quick text insert into the send field |
| **Snippets** | Right-click / Ctrl+1..0 | Text building blocks, optional PM sending |
| **Variables** | Usable in all text fields | Dynamic values (QRG, locator, AP data) |
---
## Shortcuts (Quick-Access Buttons)
Configurable in Preferences → **Shortcut Settings**.
- Each configured text creates **one button** in the user interface.
- Clicking a button inserts the text into the **send field**.
- **All variables** can be used in shortcuts and are resolved immediately when inserted.
- Longer texts are also possible.
**Tip**: Set up frequently used abbreviations like "pse", "rrr", "tnx", "73" as shortcuts.
---
## Snippets (Text Building Blocks)
Configurable in Preferences → **Snippet Settings**.
### Access
- **Right-click** on a callsign in the user list
- **Right-click** in the CQ message table
- **Right-click** in the PM message table
- **Keyboard shortcuts**: `Ctrl+1` to `Ctrl+0` for the first 10 snippets
### Behaviour with a Selected Callsign
When a callsign is selected in the user list, the snippet is addressed as a **private message**:
```
/CQ CALLSIGN <snippet text>
```
Then **Enter** can be pressed to send directly even if the send field does not have focus.
### Hardware Macro Keyboard
*(Idea by IU3OAR, Gianluca Costantino)*
The key combinations `Ctrl+1` to `Ctrl+0` can be assigned to a programmable macro keyboard. One key press triggers the snippet, another press (mapped to Enter) sends it immediately. In contest operation this saves considerable time.
### Predefined Default Snippets
On first start, some snippets are pre-configured, e.g.:
- `Hi OM, try sked?`
- `I am calling cq ur dir, pse lsn to me at MYQRG`
- `pse ur qrg?`
- `rrr, I move to your qrg nw, pse ant dir me`
These can be customised or deleted in the Preferences.
---
## Variables
Variables in written texts (snippets, shortcuts, beacon, send field) are replaced by their current values at runtime. Simply type the variable name in **uppercase** in the text.
### MYQRG
Replaced by the current transceiver frequency.
- Source: TRX sync via UDP from the logging software (if enabled)
- Fallback: Manually entered value in the MYQRG text field to the right of the send button
- Format: `144.388.03`
**Example**: `calling cq at MYQRG``calling cq at 144.388.03`
### MYQRGSHORT
Like MYQRG, but only the first 7 characters.
- Format: `144.388`
**Example**: `qrg: MYQRGSHORT``qrg: 144.388`
### MYLOCATOR
Replaced by your own Maidenhead locator (6 characters).
- Format: `JO51IJ`
**Example**: `my loc: MYLOCATOR``my loc: JO51IJ`
### MYLOCATORSHORT
Like MYLOCATOR, but only the first 4 characters.
- Format: `JO51`
**Example**: `loc: MYLOCATORSHORT``loc: JO51`
### QRZNAME
Replaced by the **name** of the currently selected station from the chat name field.
**Example**: `Hi QRZNAME, sked?``Hi Gianluca, sked?`
### FIRSTAP
Replaced by data of the first reflectable aircraft to the selected station (if available).
- Condition: AirScout is active and an aircraft is available.
- Example format: `a very big AP in 1 min`
**Example**: `AP info: FIRSTAP``AP info: a very big AP in 1 min`
### SECONDAP
Like FIRSTAP, but for the second available aircraft.
- Example format: `Next big AP in 9 min`
**Example**: `also: SECONDAP``also: Next big AP in 9 min`
### MYQTF *(planned for v1.3)*
Replaced by the current antenna direction in words (e.g. `north`, `north east`, `east`, …).
- Source: Degree value in the MYQTF input field (to the right of the MYQRG field)
---
## Variables in the Beacon
All variables can also be used in the **automatic beacon** (interval messages). Recommended beacon configuration:
```
calling cq at MYQRG, loc MYLOCATOR, GL all!
```
Since KST4Contest automatically reads QRG data from chat messages: if other stations also use KST4Contest, they will immediately see your QRG in the QRG column of their user list.
---
## Example Contest Workflow with Macros
1. Select a station in the user list → callsign is now pre-selected.
2. Press `Ctrl+1` → Snippet "Hi OM, try sked?" is addressed as a PM.
3. Press Enter → Message sent.
4. Station replies with frequency → QRG column is automatically filled.
5. Press `Ctrl+2` → Snippet "I am calling cq ur dir, pse lsn to me at 144.388" (MYQRG resolved).
6. Press Enter → Sent.
No manual typing, no errors, no interruption to CQ calling.

View File

@@ -0,0 +1,105 @@
# User Interface
> 🇬🇧 You are reading the English version | 🇩🇪 [Deutsche Version](de-Benutzeroberflaeche)
## Connecting to the Chat
1. Select a **chat category** in the settings window (e.g. 144 MHz VHF, 432 MHz UHF, …).
2. Click the **Connect** button.
3. Wait for the connection to be established.
> Disconnecting and reconnecting is only possible via the settings window. It is therefore recommended to keep the settings window open.
---
## Main Window Overview
The main window consists of several areas:
### PM Window (top left)
Shows all received **private messages** as well as intercepted public messages containing your own callsign. New messages appear in **red** and fade every 30 seconds from yellow to white.
### User List (Chat Members)
The central table of all currently active chat users. Columns (depending on configuration):
| Column | Content |
|---|---|
| Call | Station's callsign |
| Name | Name from the chat name field |
| Loc | Maidenhead locator |
| QRB | Distance in km |
| QTF | Direction in degrees |
| QRG | Automatically detected frequency |
| AP | AirScout aircraft data (when active) |
| Band colours | Worked / NOT-QRV status per band |
**Sorting**: Click column headers. QRB sorting is numerical (corrected in v1.22).
### Send Field
Text input for outgoing messages. After clicking a callsign in the user list, the send field automatically receives focus start typing immediately without double-clicking (from v1.22).
### MYQRG Field
To the right of the send button. Shows the current own QRG, can also be entered manually.
### MYQTF Field *(for v1.3)*
Input field for the current antenna direction. Used for the planned `MYQTF` variable.
---
## Filters
The filter bar (from v1.21 as a flowpane for small screens):
- **Show only QTF**: Activate direction filter (N/NE/E/… buttons or degree input)
- **Show only QRB [km] <=**: Activate distance filter (toggle button)
- **Hide Worked [Band]**: Hide worked stations per band (one toggle per band)
- **Hide NOT-QRV [Band]**: Hide NOT-QRV-tagged stations per band
---
## Station Info Panel (Further Info)
Bottom right: Shows all messages of a selected station (CQ messages and PMs in one panel). A message filter can be pre-configured via the default filter in the Preferences.
**Sked reminders** can also be activated here.
---
## Priority List
Shows the top candidates calculated by the Score Service. Updates automatically in the background based on direction, distance and AP availability.
---
## Cluster & QSO of Others
Separate window (can be minimised). Shows the communication flow between other stations interesting during quieter contest periods.
---
## Menu
### Window
- **Use Dark Mode** (from v1.26): Toggle dark colour scheme on/off.
---
## Window Sizes and Dividers
From **v1.21**, clicking **"Save Settings"** also saves window sizes and divider positions of all panels in the configuration file, which are restored on the next start.
If you encounter display problems: delete the configuration file → KST4Contest creates new default values.
---
## Operating Tips
- **Keep the settings window open**: Quick access to enable/disable the beacon.
- **Right-click in the user list**: Opens the snippet menu and further actions (QRZ.com profile, set NOT-QRV tags).
- **Enter from anywhere**: When text is in the send field, Enter sends directly even if the focus is elsewhere.
- **Stop the beacon**: Switch off the beacon while scanning frequencies to avoid flooding the chat with messages.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
github_docs/qrg_buttons.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

3
jdk.cmd Normal file
View File

@@ -0,0 +1,3 @@
@echo off
SET JAVA_HOME=C:\Program Files\Java\jdk-17
SET PATH=%JAVA_HOME%\bin,%PATH%

View File

@@ -6,7 +6,7 @@
<groupId>de.x08</groupId>
<artifactId>praktiKST</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.41.0-nightly</version>
<name>praktiKST</name>
@@ -32,7 +32,7 @@
<javafx.version>19.0.2.1</javafx.version>
<jetbrains.annotations.version>24.0.1</jetbrains.annotations.version>
<junit.version>5.10.1</junit.version>
<lombok.version>1.18.30</lombok.version>
<lombok.version>1.18.44</lombok.version>
<mockito.version>5.7.0</mockito.version>
<sqlite.version>3.43.2.2</sqlite.version>
@@ -54,8 +54,8 @@
<pmd.version>6.55.0</pmd.version>
<codehaus.version.plugin>2.16.1</codehaus.version.plugin>
<javafx.maven.plugin>0.0.8</javafx.maven.plugin>
<spotbugs.maven.plugin>4.8.1.0</spotbugs.maven.plugin>
<spotbugs.version>4.8.1</spotbugs.version>
<spotbugs.maven.plugin>4.9.8.2</spotbugs.maven.plugin>
<spotbugs.version>4.9.8</spotbugs.version>
<!-- other properties -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@@ -1,8 +1,54 @@
package kst4contest;
import java.util.Random;
public class ApplicationConstants {
/**
* default constructor generates runtime id
*/
ApplicationConstants() {
sessionRuntimeUniqueId = generateRuntimeId();
};
public static int sessionRuntimeUniqueId = generateRuntimeId();
/**
* Name of the Application.
*/
public static final String APPLICATION_NAME = "praktiKST";
/**
* Name of file to store preferences in.
*/
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.41;
public static final String VERSIONINFOURLFORUPDATES_KST4CONTEST = "https://do5amf.funkerportal.de/kst4ContestVersionInfo.xml";
public static final String VERSIONINFDOWNLOADEDLOCALFILE = "kst4ContestVersionInfo.xml";
public static final String STYLECSSFILE_DEFAULT_DAYLIGHT = "KST4ContestDefaultDay.css";
public static final String STYLECSSFILE_DEFAULT_EVENING = "KST4ContestDefaultEvening.css";
public static final String DISCSTRING_DISCONNECT_AND_CLOSE = "CLOSEALL";
public static final String DISCSTRING_DISCONNECT_DUE_PAWWORDERROR = "JUSTDSICCAUSEPWWRONG";
public static final String DISCSTRING_DISCONNECTONLY = "ONLYDISCONNECT";
// public static final String DISCONNECT_RDR_POISONPILL = "POISONPILL_KILLTHREAD: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String DISCONNECT_RDR_POISONPILL = "UNKNOWN: KST4C KILL POISONPILL_KILLTHREAD=: " + sessionRuntimeUniqueId; //whereever a (blocking) udp or tcp reader in an infinite loop gets this message, it will break this loop
public static final String AUTOANSWER_PREFIX = "[KST4C Automsg] "; // hard-coded marker (user can't remove it)
/**
* generates a unique runtime id per session. Its used to feed the poison pill in order to kill only this one and
* only instance if the program and not multiple instances
* @return
*/
public static int generateRuntimeId() {
Random ran = new Random();
return ran.nextInt(6) + 100;
}
}

View File

@@ -11,6 +11,7 @@ import java.net.UnknownHostException;
import java.util.TimerTask;
import javafx.collections.ObservableList;
import kst4contest.locatorUtils.Location;
import kst4contest.model.ChatMember;
@@ -28,22 +29,45 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
public void run() {
Thread.currentThread().setName("AirscoutPeriodicalReflectionInquirierTask");
String KSTClientsNameForQuery = this.client.getChatPreferences().getAirScout_asClientNameString();
String ASServerNameStringForAnswer = this.client.getChatPreferences().getAirScout_asServerNameString();
//TODO: Manage prefixes kst and as via preferences file and instance
//TODO: Check if locator is changeable via the preferences object, need to be correct if it changes
DatagramSocket dsocket;
String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" ";
String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" ";
String bandString = "1440000";
String myCallAndMyLocString = this.client.getChatPreferences().getLoginCallSign() + "," + this.client.getChatPreferences().getLoginLocator();
// String prefix_asSetpath ="ASSETPATH: \"KST\" \"AS\" "; //working original
// String prefix_asWatchList = "ASWATCHLIST: \"KST\" \"AS\" "; //working original
String prefix_asSetpath ="ASSETPATH: \"" + this.client.getChatPreferences().getAirScout_asClientNameString() + "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String prefix_asWatchList = "ASWATCHLIST: \""+ this.client.getChatPreferences().getAirScout_asClientNameString()+ "\" \"" + this.client.getChatPreferences().getAirScout_asServerNameString() + "\" ";
String bandString = "1440000"; //TODO: this must variable in case of higher bands! ... default: 1440000
// String myCallAndMyLocString = this.client.getChatPreferences().getStn_loginCallSign() + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //before fix 1.266
String ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
try {
if (this.client.getChatPreferences().getStn_loginCallSign().contains("-")) {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign().split("-")[0];
} else {
ownCallSign = this.client.getChatPreferences().getStn_loginCallSign();
}
} catch (Exception e) {
System.out.println("[ASPERIODICAL, Error]: " + e.getMessage());
}
String myCallAndMyLocString = ownCallSign + "," + this.client.getChatPreferences().getStn_loginLocatorMainCat(); //bugfix, Airscout do not process 9A1W-2 but 9A1W like formatted calls
String suffix = ""; //"FOREIGNCALL,FOREIGNLOC " -- dont forget the space at the end!!!
String asWatchListString = prefix_asWatchList + bandString + "," + myCallAndMyLocString;
String asWatchListStringSuffix = asWatchListString;
String host = "255.255.255.255";
int port = 9872;
// int port = 9872;
int port = client.getChatPreferences().getAirScout_asCommunicationPort();
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
InetAddress address;
@@ -60,113 +84,41 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
praktiKSTActiveUserList.toArray(ary_threadSafeChatMemberArray);
for (ChatMember i : ary_threadSafeChatMemberArray) {
suffix = i.getCallSign() + "," + i.getQra() + " ";
//
String queryStringToAirScout = "";
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
try {
address = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
dsocket = new DatagramSocket();
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (UnknownHostException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (NoRouteToHostException e) {
e.printStackTrace();
}
catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
if (i.getQrb() < this.client.getChatPreferences().getStn_maxQRBDefault())
//Here: check if maximum distance to the chatmember is reached, only ask AS if distance is lower!
//this counts for AS request and Aswatchlist
{
suffix = i.getCallSign() + "," + i.getQra() + " ";
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra();
String queryStringToAirScout = "";
queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
try {
address = InetAddress.getByName("255.255.255.255");
DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
dsocket = new DatagramSocket();
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (UnknownHostException e1) {
e1.printStackTrace();
} catch (NoRouteToHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
asWatchListStringSuffix += "," + i.getCallSign() + "," + i.getQra();
}
}
// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) {
// ChatMember chatMember = (ChatMember) iterator.next();
//
// suffix = chatMember.getCallSign() + "," + chatMember.getQra() + " ";
//
// String queryStringToAirScout = "";
//
// queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
//
// byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
//
// try {
// address = InetAddress.getByName("255.255.255.255");
// DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
// dsocket = new DatagramSocket();
// dsocket.setBroadcast(true);
// dsocket.send(packet);
// dsocket.close();
// } catch (UnknownHostException e1) {
// // TODO Auto-generated catch block
// e1.printStackTrace();
// } catch (SocketException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
//
//// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
//
// asWatchListStringSuffix += "," + chatMember.getCallSign() + "," + chatMember.getQra();
//
// }}
// for (Iterator iterator = praktiKSTActiveUserList.iterator(); iterator.hasNext();) {
// ChatMember chatMember = (ChatMember) iterator.next();
//
// suffix = chatMember.getCallSign() + "," + chatMember.getQra() + " ";
//
// String queryStringToAirScout = "";
//
// queryStringToAirScout += prefix_asSetpath + bandString + "," + myCallAndMyLocString + "," + suffix;
//
// byte[] queryStringToAirScoutMSG = queryStringToAirScout.getBytes();
//
// try {
// address = InetAddress.getByName("255.255.255.255");
// DatagramPacket packet = new DatagramPacket(queryStringToAirScoutMSG, queryStringToAirScoutMSG.length, address, port);
// dsocket = new DatagramSocket();
// dsocket.setBroadcast(true);
// dsocket.send(packet);
// dsocket.close();
// } catch (UnknownHostException e1) {
// // TODO Auto-generated catch block
// e1.printStackTrace();
// } catch (SocketException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
//
//// System.out.println("[ASUDPTask, info:] sent query " + queryStringToAirScout);
//
// asWatchListStringSuffix += "," + chatMember.getCallSign() + "," + chatMember.getQra();
//
// }
/**
* As next we will set the ASWatchlist. All stations in chat will be watched by airscout causing following code.\n\n
* ASWATCHLIST: "KST" "AS" 4320000,DO5AMF,JN49GL,DF9QX,JO42HD,DG2KBC,JN58MI,DJ0PY,JO32MF,DL1YDI,JO42FA,DL6BF,JO32QI,F1NZC,JN15MR,F4TXU,JN23CX,F5GHP,IN96LE,F6HTJ,JN12KQ,G0GGG,IO81VE,G0JCC,IO82MA,G0JDL,JO02SI,G0MBL,JO01QH,G4AEP,IO91MB,G4CLA,IO92JL,G4DCV,IO91OF,G4LOH,IO70JC,G4MKF,IO91HJ,G4TRA,IO81WN,G8GXP,IO93FQ,G8VHI,IO92FM,GW0RHC,IO71UN,HA4ND,JN97MJ,I5/HB9SJV/P,JN52JS,IW2DAL,JN45NN,OK1FPR,JO80CE,OK6M,JN99CR,OV3T,JO46CM,OZ2M,JO65FR,PA0V,JO33II,PA2RU,JO32LT,PA3DOL,JO22MT,PA9R,JO22JK,PE1EVX,JO22MP,S51AT,JN75GW,SM7KOJ,JO66ND,SP9TTG,JO90KW<4B>
@@ -183,14 +135,7 @@ public class AirScoutPeriodicalAPReflectionInquirerTask extends TimerTask {
dsocket.setBroadcast(true);
dsocket.send(packet);
dsocket.close();
} catch (UnknownHostException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (SocketException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

View File

@@ -1,8 +1,10 @@
package kst4contest.controller;
import java.util.Arrays;
import java.util.TimerTask;
import kst4contest.model.ChatMessage;
import kst4contest.model.ThreadStateMessage;
/**
* This class is for sending beacons intervalled to the public chat. Gets all
@@ -20,43 +22,99 @@ import kst4contest.model.ChatMessage;
public class BeaconTask extends TimerTask {
private ChatController chatController;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "MyBeacon";
public BeaconTask(ChatController client) {
public BeaconTask(ChatController client, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.chatController = client;
}
@Override
public void run() {
Thread.currentThread().setName("BeaconTask");
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("BeaconTask");
ChatMessage beaconMSG = new ChatMessage();
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconText();
// replaceVariables = bcn_beaconText;
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRG().getValue());
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getLoginCallSign());
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getLoginLocator());
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
replaceVariables = replaceVariables.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
beaconMSG.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategory().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
beaconMSG.setMessageDirectedToServer(true);
// System.out.println("########### " + replaceVariables);
if (this.chatController.getChatPreferences().isBcn_beaconsEnabled() ) {
ChatMessage beaconMSG2 = new ChatMessage();
String replaceVariables2 = this.chatController.getChatPreferences().getBcn_beaconTextSecondCat();
replaceVariables2 = replaceVariables2.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables2 = replaceVariables2.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables2 = replaceVariables2.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables2 = replaceVariables2.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
replaceVariables2 = replaceVariables2.replaceAll("SECONDQRG", this.chatController.getChatPreferences().getMYQRGSecondCat().getValue() + "");
beaconMSG2.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
beaconMSG2.setMessageDirectedToServer(true);
/**
* beacon 1st Chatcategory
*/
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
//do nothing, CQ is disabled
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 1", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
/**
* beacon 2nd Chatcategory
*/
if (this.chatController.getChatPreferences().isLoginToSecondChatEnabled()) { //only send if 2nd cat enabled
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledSecondCat()) {
beaconMSG2.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategorySecond().getCategoryNumber() + "|0|" + replaceVariables2 + "|0|");
beaconMSG2.setMessageDirectedToServer(true);
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending CQ 2nd Cat: " + beaconMSG2.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG2);
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", true, "on", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} else {
threadStateMessage = new ThreadStateMessage(this.ThreadNickName + " 2", false, "off", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,196 @@
//package kst4contest.controller;
//
//import kst4contest.model.ChatMember;
//import kst4contest.model.ChatMessage;
//
//import java.io.*;
//import java.net.ServerSocket;
//import java.net.Socket;
//import java.nio.channels.ServerSocketChannel;
//import java.nio.channels.SocketChannel;
//import java.time.Instant;
//
///**
// * This thread is responsible for providing DXCluster messages for a connected log program.
// *
// *
// */
////public class DXClusterController extends Thread {
// PrintWriter outTelnet;
// BufferedReader inTelnet;
// private Socket socket;
// private ChatController client;
//// private OutputStream output;
//// private InputStream input;
//
// private ChatMessage messageTextRaw;
//
// private static final int PORT = 23;
// private static final String USERNAME = "user";
// private static final String PASSWORD = "pass";
// private Socket clientSocket;
//
//
//
//
//// public DXClusterController(Socket clientSocket, ChatController client) throws InterruptedException {
////
//// this.client = client;
////
//// try {
//// outTelnet = new PrintWriter(clientSocket.getOutputStream(), true);
//// inTelnet = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//// } catch (IOException e) {
//// throw new RuntimeException(e);
//// }
//// System.out.println("defcons");
//// this.clientSocket = clientSocket;
////
//// }
//
// public DXClusterController(Socket clientSocket, ChatController chatController) {
//
// try {
// socket = clientSocket;
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// this.client = chatController;
//
// try {
// outTelnet = new PrintWriter(socket.getOutputStream(), true);
// inTelnet = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// System.out.println("[DXCCtrl, info:] DXCluster Controller created!");
// this.clientSocket = socket;
// }
//
// public DXClusterController(Socket clientSocket, ObjectOutputStream objectout, ChatController chatController) {
//
// try {
// socket = clientSocket;
// } catch (Exception e) {
// throw new RuntimeException(e);
// }
// this.client = chatController;
//
// try {
// outTelnet = new PrintWriter(socket.getOutputStream(), true);
// inTelnet = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// } catch (IOException e) {
// throw new RuntimeException(e);
// }
// System.out.println("[DXCCtrl, info:] DXCluster Controller created!");
// this.clientSocket = socket;
// }
//
//// public DXClusterController(ServerSocket clientSocket, ChatController client) throws InterruptedException {
//// //TODO: GOT FROM https://stackoverflow.com/questions/15541804/creating-the-serversocket-in-a-separate-thread
//// try {
//// socket = clientSocket.accept(2);
//// } catch (IOException e) {
//// throw new RuntimeException(e);
//// }
//// this.client = client;
////
//// try {
//// outTelnet = new PrintWriter(socket.getOutputStream(), true);
//// inTelnet = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//// } catch (IOException e) {
//// throw new RuntimeException(e);
//// }
//// System.out.println("defcons");
//// this.clientSocket = socket;
////
//// }
//
//// public DXClusterController(ServerSocketChannel serverSocketChannel, ChatController client) throws InterruptedException {
////
//// this.client = client;
////
////// clientSocketChannel.ac
////
//// try {
////
//// serverSocketChannel.accept();
//// serverSocketChannel.rea
////
//// outTelnet = new PrintWriter(clientSocket.getOutputStream(), true);
//// inTelnet = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
//// } catch (IOException e) {
//// throw new RuntimeException(e);
//// }
//// System.out.println("defcons");
//// this.clientSocket = clientSocket;
////
//// }
//
// public boolean terminateConnection() throws IOException {
//
//// this.output.close();
// this.socket.close();
//
// return true;
// }
//
// public void sendLocalClusterMessage() {
//
// }
//
// public void run() {
//// try (
//// PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
//// BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())))
//// {
//
//// out.println("Welcome to the Telnet Server");
// outTelnet.print("login: ");
// outTelnet.flush();
// try {
// String user = inTelnet.readLine();
// } catch (IOException ex) {
// throw new RuntimeException(ex);
// }
//// finally {
//// try {
//// clientSocket.close();
//// } catch (Exception e) {
//// System.out.println("Error closing client socket: " + e.getMessage());
//// }
//// }
//
//
//// for (int i = 0; i < 10; i++) {
////
//// outTelnet.println("DX de DM5M: 144222.0 DO5AMF JN49FL 2250Z\n");
//// }
//
//
//
// }
//
// /**
// * Sends a DX cluster message to the connected log programs via telnet, returns true if sent
// *
// * @param aChatMember
// * @return
// */
// public boolean propagateSingleDXClusterEntry(ChatMember aChatMember) {
//
// String singleDXClusterMessage = "DX de ";
//
// singleDXClusterMessage += client.getChatPreferences().getLoginCallSign() + " ";
// singleDXClusterMessage += aChatMember.getFrequency().getValue() + " ";
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + "\n";
//
// outTelnet.println(singleDXClusterMessage);
// outTelnet.flush();
// return true;
// }
//}

View File

@@ -0,0 +1,237 @@
package kst4contest.controller;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ThreadStateMessage;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class DXClusterThreadPooledServer implements Runnable{
private List<Socket> clientSockets = Collections.synchronizedList(new ArrayList<>()); //list of all connected clients
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
ChatController chatController = null;
protected int serverPort = 8080;
protected ServerSocket serverSocket = null;
protected boolean isStopped = false;
protected Thread runningThread= null;
protected ExecutorService threadPool =
Executors.newFixedThreadPool(10);
Socket clientSocket;
public DXClusterThreadPooledServer(int port, ChatController chatController, ThreadStatusCallback callback){
this.serverPort = port;
this.chatController = chatController;
this.callBackToController = callback;
}
public void run(){
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
synchronized(this){
this.runningThread = Thread.currentThread();
runningThread.setName("DXCluster-thread-pooled-server");
}
openServerSocket();
while(! isStopped()){
clientSocket = null;
try {
clientSocket = this.serverSocket.accept();
synchronized(clientSockets) {
clientSockets.add(clientSocket); // add dx cluster client to the "clients list" for broadcasting
}
} catch (IOException e) {
if(isStopped()) {
System.out.println("Server Stopped.") ;
break;
}
throw new RuntimeException(
"Error accepting client connection", e);
}
DXClusterServerWorkerRunnable worker = new DXClusterServerWorkerRunnable(clientSocket, "Thread Pooled DXCluster Server ", chatController, clientSockets, chatController);
this.threadPool.execute(worker);
}
this.threadPool.shutdown();
System.out.println("Server Stopped.") ;
}
private synchronized boolean isStopped() {
return this.isStopped;
}
public synchronized void stop(){
this.isStopped = true;
try {
this.serverSocket.close();
synchronized(clientSockets) {
for (Socket socket : clientSockets) {
socket.close(); // close all client connections
}
}
} catch (IOException e) {
throw new RuntimeException("DXCCSERVER Error closing server", e);
}
}
private void openServerSocket() {
try {
this.serverSocket = new ServerSocket(this.serverPort);
} catch (IOException e) {
throw new RuntimeException("DXCCSERVER Cannot open port ", e);
}
}
/**
* Sends a DX cluster message to ALL connected log programs via telnet, returns true if sent
*
* @param aChatMember
* @return boolean true if message had been sent
*/
public boolean broadcastSingleDXClusterEntryToLoggers(ChatMember aChatMember) {
synchronized(clientSockets) {
System.out.println("DXClusterSrvr: broadcasting message to clients: " + clientSockets.size());
try {
System.out.println("-------------> ORIGINALEE VAL: " + aChatMember.getFrequency().getValue());
System.out.println("-------------> NORMALIZED VAL: " + Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ");
} catch (Exception e) {
System.out.println("DXCThPooledServer: Error accessing value in chatmember object: " + e.getMessage());
// e.printStackTrace();
}
for (Socket socket : clientSockets) {
try {
OutputStream output = socket.getOutputStream();
String singleDXClusterMessage = "DX de ";
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
singleDXClusterMessage += this.chatController.getChatPreferences().getNotify_DXCSrv_SpottersCallSign().getValue() + ": ";
singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " "; //we need such an amount of spaces for n1mm to work, otherwise bullshit happens
singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
// singleDXClusterMessage += chatController.getChatPreferences().getLoginCallSign() + ": ";
// singleDXClusterMessage += Utils4KST.normalizeFrequencyString(aChatMember.getFrequency().getValue(), chatController.getChatPreferences().getNotify_optionalFrequencyPrefix()) + " ";
// singleDXClusterMessage += aChatMember.getCallSign().toUpperCase() + " ";
// singleDXClusterMessage += aChatMember.getQra().toUpperCase() + " ";
// singleDXClusterMessage += new Utils4KST().time_generateCurrenthhmmZTimeStringForClusterMessage() + ((char)7) + ((char)7) + "\r\n";
output.write((singleDXClusterMessage).getBytes());
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Last msg to " + clientSockets.size() + " Cluster Clients:\n" + singleDXClusterMessage, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (IOException e) {
e.printStackTrace();
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
return false;
}
}
}
return true; //if message had been sent, return true for "ok"
}
}
class DXClusterServerWorkerRunnable implements Runnable{
protected Socket clientSocket = null;
protected String serverText = null;
private ChatController client = null;
private List<Socket> dxClusterClientSocketsConnectedList;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "DXCluster-Server";
public DXClusterServerWorkerRunnable(Socket clientSocket, String serverText, ChatController chatController, List<Socket> clientSockets, ThreadStatusCallback callback) {
this.clientSocket = clientSocket;
this.serverText = serverText;
this.client = chatController;
this.dxClusterClientSocketsConnectedList = clientSockets;
this.callBackToController = callback;
}
public void run() {
try {
OutputStream output = clientSocket.getOutputStream();
dxClusterClientSocketsConnectedList.add(clientSocket);
Timer dXCkeepAliveTimer = new Timer();
dXCkeepAliveTimer.schedule(new TimerTask() {
@Override
public void run() {
StringBuilder connectedClients = new StringBuilder(); //only for statistics
for (Socket socket : dxClusterClientSocketsConnectedList) {
connectedClients.append(socket.getInetAddress()).append("\n");
try {
OutputStream output = socket.getOutputStream();
output.write(("\r\n").getBytes());
} catch (IOException e) {
e.printStackTrace();
System.out.println("[DXClusterSrvr, Error:] broadcasting DXC-message to clients went wrong!");
dXCkeepAliveTimer.purge();
try {
socket.close();
} catch (IOException ex) {
ex.printStackTrace();
}
finally {
this.cancel();
}
dxClusterClientSocketsConnectedList.remove(socket); //if socket is closed by client, remove it from the broadcast list and close it
}
}
// ThreadStateMessage threadStateMessage = new ThreadStateMessage(ThreadNickName, true, "Connected clients: " + connectedClients.toString(), false);
// callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
}, 30000, 30000);
output.write(("login: ").getBytes()); //say hello to the client, it will answer with a callsign
System.out.println("[DXClusterThreadPooledServer, Info:] New cluster client connected! "); //TODO: maybe integrate non blocking reader for client identification
} catch (IOException e) {
e.printStackTrace();
} finally {
synchronized(dxClusterClientSocketsConnectedList) {
dxClusterClientSocketsConnectedList.remove(clientSocket); // Entferne den Client nach Verarbeitung
}
}
}
}

View File

@@ -0,0 +1,44 @@
package kst4contest.controller;
import javafx.beans.property.SimpleStringProperty;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
public class DXClusterThreadPooledServerTest {
public static void main(String[] args) {
ChatController client = new ChatController();
ChatPreferences testPreferences = new ChatPreferences();
testPreferences.setStn_loginCallSign("DM5M");
client.setChatPreferences(testPreferences);
DXClusterThreadPooledServer dxClusterServer = new DXClusterThreadPooledServer(8000, client, client);
new Thread(dxClusterServer).start();
try {
Thread.sleep(10 * 1000);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>ready.....go!");
} catch (InterruptedException e) {
e.printStackTrace();
}
ChatMember test = new ChatMember();
test.setCallSign("DL5ASG");
test.setQra("JO51HK");
test.setFrequency(new SimpleStringProperty("144776.0"));
dxClusterServer.broadcastSingleDXClusterEntryToLoggers(test);
// try {
// Thread.sleep(20 * 3333);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println("Stopping Server");
// server.stop();
}
}

View File

@@ -43,7 +43,7 @@ public class InputReaderThread extends Thread {
e.printStackTrace();
}
ownMSG.setMessageText("MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + sendThisMessage23001 + "|0|");
ownMSG.setMessageText("MSG|" + this.client.getChatCategoryMain().getCategoryNumber() + "|0|" + sendThisMessage23001 + "|0|");
// System.out.println("inreader " + ownMSG.getMessage() + client.getMessageTXBus().size());

View File

@@ -0,0 +1,237 @@
package kst4contest.controller;
import kst4contest.controller.interfaces.PstRotatorEventListener;
import kst4contest.model.ThreadStateMessage;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
public class PstRotatorClient implements Runnable {
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "PSTRotator";
private static final Logger LOGGER = Logger.getLogger(PstRotatorClient.class.getName());
private static final int BUFFER_SIZE = 1024;
// Konfiguration
private final String host;
private final int remotePort; // Port, auf dem PSTRotator hört (z.B. 12060)
private final int localPort; // Port, auf dem wir hören (z.B. 12061)
private DatagramSocket socket;
private volatile boolean running = false;
private PstRotatorEventListener listener;
// Executor für Polling (Status-Abfrage)
private ScheduledExecutorService poller;
/**
* Konstruktor
* @param host IP Adresse von PSTRotator (meist "127.0.0.1")
* @param remotePort Der Port, der in PSTRotator eingestellt ist (User-Wunsch: 12060)
* @param listener Callback für den Chatcontroller
*/
public PstRotatorClient(String host, int remotePort, PstRotatorEventListener listener, ThreadStatusCallback callBack) {
this.callBackToController = callBack;
this.host = host;
this.remotePort = remotePort;
// Laut Manual antwortet PSTRotator oft auf Port+1.
// Wir binden uns also standardmäßig auf remotePort + 1.
this.localPort = remotePort + 1;
this.listener = listener;
}
/**
* alternative constructor for seting the remote port explicitely
*/
public PstRotatorClient(String host, int remotePort, int localPort, PstRotatorEventListener listener) {
this.host = host;
this.remotePort = remotePort;
this.localPort = localPort;
this.listener = listener;
}
/**
* Startet den Empfangs-Thread und das Polling
*/
public void start() {
try {
// Socket binden
// socket = new DatagramSocket(null);
// socket.setReuseAddress(true);
// socket = new DatagramSocket(localPort);
//
socket = new DatagramSocket(null);
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(localPort)); //bind to port
running = true;
// 1. Empfangs-Thread starten (dieses Runnable)
Thread thread = new Thread(this, "PSTRotator-Listener-" + remotePort);
thread.start();
// 2. Polling starten (z.B. alle 2 Sekunden Status abfragen)
poller = Executors.newSingleThreadScheduledExecutor();
poller.scheduleAtFixedRate(this::pollStatus, 1, 2, TimeUnit.SECONDS);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, running, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
LOGGER.info("PstRotatorClient started. Remote: " + remotePort + ", Local: " + localPort);
} catch (SocketException e) {
LOGGER.log(Level.SEVERE, "Fehler beim Öffnen des UDP Sockets", e);
}
}
/**
* Stopping threads and closing sockets of pstRotator communicator
*/
public void stop() {
running = false;
if (poller != null && !poller.isShutdown()) {
poller.shutdownNow();
}
if (socket != null && !socket.isClosed()) {
socket.close();
}
}
/**
* Main loop in thread which listens fpr PSTrotator packets
*/
@Override
public void run() {
byte[] buffer = new byte[BUFFER_SIZE];
while (running && !socket.isClosed()) {
try {
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
socket.receive(packet); // Blockiert bis Daten kommen
String received = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + received, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
parseResponse(received);
} catch (IOException e) {
if (running) {
LOGGER.log(Level.WARNING, "Fehler beim Empfangen des Pakets", e);
}
}
}
}
/**
* parses a pst rotatpr message to fit the PST listener interface
* @param msg
*/
private void parseResponse(String msg) {
// Debug
if (listener != null) listener.onMessageReceived(msg);
// Example answer: "AZ:145.0<CR>", "EL:010.0<CR>", "MODE:1<CR>"
msg = msg.replace("<CR>", "").trim();
try {
if (msg.startsWith("AZ:")) {
String val = msg.substring(3);
if (listener != null) listener.onAzimuthUpdate(Double.parseDouble(val));
}
else if (msg.startsWith("EL:")) {
String val = msg.substring(3);
if (listener != null) listener.onElevationUpdate(Double.parseDouble(val));
}
else if (msg.startsWith("MODE:")) {
// MODE:1 = Tracking, MODE:0 = Manual
String val = msg.substring(5);
boolean tracking = "1".equals(val);
if (listener != null) listener.onModeUpdate(tracking);
}
else if (msg.startsWith("OK:")) {
// Bestätigung von Befehlen, z.B. OK:STOP:1
LOGGER.fine("Befehl bestätigt: " + msg);
}
} catch (NumberFormatException e) {
LOGGER.warning("Konnte Wert nicht parsen: " + msg);
}
}
// --- Sende Methoden (API für den Chatcontroller) ---
private void sendUdp(String message) {
if (socket == null || socket.isClosed()) return;
try {
byte[] data = message.getBytes(StandardCharsets.US_ASCII);
InetAddress address = InetAddress.getByName(host);
DatagramPacket packet = new DatagramPacket(data, data.length, address, remotePort);
socket.send(packet);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Fehler beim Senden an PstRotator", e);
}
}
/**
* Sendet den generischen XML Befehl.
* Bsp: <PST><AZIMUTH>85</AZIMUTH></PST>
*/
private void sendCommand(String tag, String value) {
String xml = String.format("<PST><%s>%s</%s></PST>", tag, value, tag);
System.out.println("PSTRotatorClient: sent: " + xml);
sendUdp(xml);
}
// Öffentliche Steuermethoden
public void setAzimuth(double degrees) {
// Formatierung ohne unnötige Nachkommastellen, falls nötig
sendCommand("AZIMUTH", String.valueOf((int) degrees));
}
public void setElevation(double degrees) {
sendCommand("ELEVATION", String.valueOf(degrees));
}
public void stopRotor() {
sendCommand("STOP", "1");
}
public void park() {
sendCommand("PARK", "1");
}
public void setTrackingMode(boolean enable) {
sendCommand("TRACK", enable ? "1" : "0");
}
/**
* Method for polling rotators status via PSTRotator software. Asks only for AZ value!<br/>
* Scheduled in a fixed time by executor
*/
public void pollStatus() {
// PSTRotator Dokumentation:
// <PST>AZ?</PST>
// <PST>EL?</PST>
// <PST>MODE?</PST>
// Man kann mehrere Befehle in einem Paket senden
String query = "<PST><AZ?></AZ?><EL?></EL?><MODE?></MODE?></PST>";
// HINWEIS: Laut Doku ist die Syntax für Abfragen etwas anders: <PST>AZ?</PST>
// Daher bauen wir den String manuell, da sendCommand Tags schließt.
sendUdp("<PST>AZ?</PST>");
// sendUdp("<PST>EL?</PST>");
sendUdp("<PST>MODE?</PST>");
}
}

View File

@@ -2,6 +2,7 @@ package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import kst4contest.model.ChatMessage;
@@ -39,7 +40,7 @@ public class ReadThread extends Thread {
try {
input = socket.getInputStream();
reader = new BufferedReader(new InputStreamReader(input));
reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
} catch (IOException ex) {
System.out.println("Error getting input stream: " + ex.getMessage());
@@ -91,48 +92,7 @@ public class ReadThread extends Thread {
// TODO Auto-generated catch block
e.printStackTrace();
}
// try {
// sleep(3000);
// } catch (InterruptedException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// try {
// System.out.println("RDTH: try new socket");
// this.client.getSocket().close();
// this.client.getSocket().close();
// this.client.setSocket(new Socket(this.client.getHostname(), this.client.getPort()));
// socket.connect(new InetSocketAddress(this.client.getHostname(), this.client.getPort()));
// System.out.println("[Readthread, Warning:] new socket connected? -> " + socket.isConnected());
// input = socket.getInputStream();
// reader = new BufferedReader(new InputStreamReader(input));
//
// this.sleep(5000);
// } catch (IOException | InterruptedException e2) {
// // TODO Auto-generated catch block
// System.out.println("fucktah");
// e2.printStackTrace();
// }
// try {
// sleep(2000);
// } catch (InterruptedException e1) {
// // TODO Auto-generated catch block
// e1.printStackTrace();
// }
// try {
// this.client.getSocket().close();
// this.client.setSocket(new Socket(this.client.getHostname(), this.client.getPort()));
// } catch (UnknownHostException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// } catch (IOException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
}
}

View File

@@ -0,0 +1,407 @@
package kst4contest.controller;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ReadUDPByWintestThread extends Thread {
private static final Pattern STATUS_TOKEN_PATTERN = Pattern.compile("\"([^\"]*)\"|(\\S+)");
private DatagramSocket socket;
private ChatController client;
private volatile boolean running = true;
private int PORT = 9871; //default
private static final int BUFFER_SIZE = 4096;
private final Map<Integer, String> receivedQsos = new ConcurrentHashMap<>();
private long lastPacketTime = 0;
private String myStation = "DO5AMF";
private String targetStation = "";
private String stationID = "";
private int lastKnownQso = 0;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "Wintest-msg";
public ReadUDPByWintestThread(ChatController client, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.client = client;
this.myStation = client.getChatPreferences().getStn_loginCallSignRaw(); //callsign of the logging stn
this.PORT = client.getChatPreferences().getLogsynch_wintestNetworkPort();
}
@Override
public void interrupt() {
running = false;
if (socket != null && !socket.isClosed()) socket.close();
super.interrupt();
}
@Override
public void run() {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
Thread.currentThread().setName("ReadUDPByWintestThread");
byte[] buffer = new byte[BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
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()));
socket.setSoTimeout(3000);
System.out.println("[WinTest UDP listener] started at port: " + PORT);
} catch (SocketException e) {
e.printStackTrace();
return;
}
while (running) {
try {
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.US_ASCII).trim();
processWinTestMessage(msg);
} catch (SocketTimeoutException e) {
// checkForMissingQsos();
} catch (IOException e) {
//TODO: here is something to catch
}
}
}
private void processWinTestMessage(String msg) {
// System.out.println("Wintest-Message received: " + msg);
lastPacketTime = System.currentTimeMillis();
if (msg.startsWith("HELLO:")) { //Client Signon of wintest
parseHello(msg);
try {
// send_needqso();
}catch (Exception e) {
System.out.println("Error: ");
e.printStackTrace();
}
} else if (msg.startsWith("ADDQSO:")) { //adding qso to wintest log
try {
parseAddQso(msg);
} catch (Exception e) {
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "Parsing ERROR: " + Arrays.toString(e.getStackTrace()), true);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
} else if (msg.startsWith("STATUS")) {
parseStatus(msg);
} else if (msg.startsWith("IHAVE:")) { //periodical message of wintest, which qsos are in the log
// parseIHave(msg); //TODO
}
else if (msg.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
System.out.println("ReadUdpByWintest, Info: got poison, now dieing....");
socket.close();
running = false;
}
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "message received\n" + msg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
/**
* parsing of the hello message of wintest:
* "HELLO: "STN1" "" 6667 130 "SLAVE" 1 0 1762201985"
* @param msg
*/
private void parseHello(String msg) {
try {
String[] tokens = msg.split("\"");
if (tokens.length >= 2) {
targetStation = tokens[1];
System.out.println("[WinTest rcv: found logger instance: " + targetStation);
}
} catch (Exception e) {
System.out.println("[WinTest] ERROR on HELLO-Parsing: " + e.getMessage());
}
}
private byte util_calculateChecksum(byte[] bytes) {
int sum = 0;
for (byte b : bytes) sum += b;
return (byte) ((sum | 0x80) & 0xFF);
}
/**
* Parse Win-Test STATUS packets and update own QRG from WT station.
*
* Parsing model (tokenized with quotes preserved):
* parts[0] = "STATUS"
* parts[1] = station name (example: "STN1")
* parts[5] = val2 (used to derive mode: 1 => SSB, else CW)
* parts[7] = frequency in 0.1 kHz units (example: 1443210 => 144321.0)
*/
private void parseStatus(String msg) {
try {
ArrayList<String> parts = new ArrayList<>();
Matcher matcher = STATUS_TOKEN_PATTERN.matcher(msg);
while (matcher.find()) {
if (matcher.group(1) != null) {
parts.add(matcher.group(1));
} else {
parts.add(matcher.group(2));
}
}
if (parts.size() < 8) {
System.out.println("[WinTest] STATUS too short: " + msg);
return;
}
String stn = parts.get(1);
String stationFilter = client.getChatPreferences().getLogsynch_wintestNetworkStationNameOfWintestClient1();
if (stationFilter != null && !stationFilter.isBlank() && !stn.equalsIgnoreCase(stationFilter)) {
return;
}
String val2 = parts.get(5);
String freqRaw = parts.get(7);
double freqFloat = Integer.parseInt(freqRaw) / 10.0;
String mode;
if ("1".equals(val2)) {
mode = freqFloat > 10000.0 ? "usb" : "lsb";
} else {
mode = "cw";
}
// Format as MMM.KKK.HH display format (e.g. 144.300.00) consistent with UCX thread
// freqFloat is in kHz (e.g. 144300.0), convert to Hz-string for formatting
long freqHzTimes100 = Math.round(freqFloat * 100.0); // e.g. 14430000
String hzStr = String.valueOf(freqHzTimes100);
String formattedQRG;
if (hzStr.length() == 8) {
// 144MHz range: 14430000 -> 144.300.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 3), hzStr.substring(3, 6), hzStr.substring(6, 8));
} else if (hzStr.length() == 9) {
// 1296MHz range: 129600000 -> 1296.000.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 4), hzStr.substring(4, 7), hzStr.substring(7, 9));
} else if (hzStr.length() == 7) {
// 70MHz range: 7010000 -> 70.100.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 2), hzStr.substring(2, 5), hzStr.substring(5, 7));
} else if (hzStr.length() == 6) {
// 50MHz range: 5030000 but 6 digits: 503000 -> 5.030.00
formattedQRG = String.format("%s.%s.%s", hzStr.substring(0, 1), hzStr.substring(1, 4), hzStr.substring(4, 6));
} else {
formattedQRG = String.format(Locale.US, "%.1f", freqFloat); // fallback
}
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
System.out.println("[WinTest STATUS] stn=" + stn + ", mode=" + mode + ", qrg=" + formattedQRG);
} catch (Exception e) {
System.out.println("[WinTest] STATUS parsing error: " + e.getMessage());
}
}
// private void send_needqso() throws IOException {
// String payload = String.format("NEEDQSO:\"%s\" \"%s\" \"%s\" %d %d?\0",
// "DO5AMF", "STN1", stationID, 1, 9999);
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
// }
// private void send_hello() throws IOException {
// String payload = String.format("HELLO:\"%s\" \"%s\" \"%s\" %d %d?\0",
// "DO5AMF", "", stationID, "SLAVE", 1, 14);
// InetAddress broadcast = InetAddress.getByName("255.255.255.255");
// byte[] bytes = payload.getBytes(StandardCharsets.US_ASCII);
// bytes[bytes.length - 2] = util_calculateChecksum((bytes));
// socket.send(new DatagramPacket(bytes, bytes.length, broadcast, 9871));
// }
/**
* Catches add-qso messages of wintest if a new qso gets into the log<br/>
*
* String is like this:<br/><br/>
*ADDQSO: "STN1" "" "STN1" 1762202297 1440000 0 12 0 0 0 2 2 "DM2RN" "599" "599001" "JO51UM" "" "" 0 "" "" "" 44510
*
* ^^^^sentby<br/>
* ^^^^^^^^^^time<br/>
* ^^^^^^qrg<br/>
* ^^band<br/>
* ^^^^^callsign logged<br/>
* stn-id ^^^^
* @param msg
*/
private void parseAddQso(String msg) {
ChatMember modifyThat = null;
try {
// int qsoNumber = extractQsoNumber(msg);
// receivedQsos.put(qsoNumber, msg);
// lastKnownQso = Math.max(lastKnownQso, qsoNumber);
String callSignCatched = msg.split("\"") [7];
ChatMember workedCall = new ChatMember();
workedCall.setCallSign(callSignCatched);
workedCall.setWorked(true); //its worked at this place, for sure!
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
String bandId;
bandId = msg.split("\"")[6].split(" ")[4].trim();
switch (bandId) {
case "10" -> workedCall.setWorked50(true);
case "11" -> workedCall.setWorked70(true);
case "12" -> workedCall.setWorked144(true);
case "14" -> workedCall.setWorked432(true);
case "16" -> workedCall.setWorked1240(true);
case "17" -> workedCall.setWorked2300(true);
case "18" -> workedCall.setWorked3400(true);
case "19" -> workedCall.setWorked5600(true);
case "20" -> workedCall.setWorked10G(true);
case "21" -> workedCall.setWorked24G(true);
case "22" -> workedCall.setWorked47G(true);
case "23" -> workedCall.setWorked76G(true);
default -> System.out.println("[WinTestUDPRcvr: warning] Unbekannte Band-ID: " + bandId);
}
if (!markTheseChattersAsWorked.isEmpty()) {
//Worked call is part of the current chatmember list
for (int index : markTheseChattersAsWorked) {
//iterate through the logged in chatmembers callsigns and set the worked markers
modifyThat = client.getLst_chatMemberList().get(index);
modifyThat.setWorked(true); //worked its for sure
if (workedCall.isWorked50()) {
modifyThat.setWorked50(true);
} else if (workedCall.isWorked70()) {
modifyThat.setWorked70(true);
} else if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
} else if (workedCall.isWorked24G()) {
modifyThat.setWorked24G(true);
} else if (workedCall.isWorked47G()) {
modifyThat.setWorked47G(true);
} else if (workedCall.isWorked76G()) {
modifyThat.setWorked76G(true);
} else {
System.out.println("[WinTestUDPRcvr: warning] found no new worked-flag for this band: " + workedCall.getCallSignRaw() + bandId);
}
}
try {
GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
// trigger band-upgrade hint after log entry (Win-Test)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[WinTestUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
} catch (Exception IllegalStateException) {
//do nothing, as it works...
}
}
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
// This will update the worked info on a worked chatmember. DBHandler will
// check, if an entry at the db had been modified. If not, then the worked
// station had not been stored. DBHandler will store the information then.
if (!isInChat) {
workedCall.setName("unknown");
workedCall.setQra("unknown");
workedCall.setLastActivity(new Utils4KST().time_generateActualTimeInDateFormat());
this.client.getDbHandler().storeChatMember(workedCall);
}
File logUDPMessageToThisFile = new File(this.client.getChatPreferences()
.getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
FileWriter fileWriterPersistUDPToFile = null;
BufferedWriter bufwrtrRawMSGOut;
try {
fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);
} catch (IOException e1) {
e1.printStackTrace();
}
bufwrtrRawMSGOut = new BufferedWriter(fileWriterPersistUDPToFile);
if (modifyThat != null) {
bufwrtrRawMSGOut.write("\n" + modifyThat.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
} else {
bufwrtrRawMSGOut.write("\n" + workedCall.toString());
bufwrtrRawMSGOut.flush();
bufwrtrRawMSGOut.close();
}
System.out.println("[WinTest, Info: Marking Chatmember as worked: " + workedCall.toString());
// markChatMemberAsWorked(call, band); //TODO
} catch (Exception e) {
System.out.println("[WinTest] Fehler beim ADDQSO-Parsing: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,18 @@
package kst4contest.controller;
import kst4contest.model.ChatPreferences;
public class ReadUDPByWintestThreadTest {
public static void main(String[] args) {
ChatController ctrl1 = new ChatController();
ChatPreferences prefs = new ChatPreferences();
ctrl1.setChatPreferences(prefs);
// ReadUDPByWintestThread test = new ReadUDPByWintestThread(ctrl1);
// test.run();
}
}

View File

@@ -2,13 +2,16 @@ package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Comparator;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import kst4contest.ApplicationConstants;
import kst4contest.model.AirPlane;
import kst4contest.model.AirPlaneReflectionInfo;
import kst4contest.model.ChatMember;
import kst4contest.model.ThreadStateMessage;
/**
* This thread is responsible for reading server's input and printing it to the
@@ -23,13 +26,16 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
private ChatController client;
private int localPort;
private String ASIdentificator, ChatClientIdentificator;
public ReadUDPbyAirScoutMessageThread(int localPort) {
this.localPort = localPort;
}
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "AirScout msg";
// public ReadUDPbyAirScoutMessageThread(int localPort) {
// this.localPort = localPort;
// }
public ReadUDPbyAirScoutMessageThread(int localPort, ChatController client, String ASIdentificator,
String ChatClientIdentificator) {
String ChatClientIdentificator, ThreadStatusCallback callback) {
this.callBackToController = callback;
this.localPort = localPort;
this.client = client;
this.ASIdentificator = ASIdentificator;
@@ -38,6 +44,7 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
@Override
public void interrupt() {
System.out.println("ReadUDP");
super.interrupt();
try {
if (this.socket != null) {
@@ -50,7 +57,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
}
}
private void callThreadStateToUi (ThreadStateMessage threadStateMessage) {
if (callBackToController != null) {
//update the visual control of running thread
callBackToController.onThreadStatus("AirScout", threadStateMessage);
}
}
public void run() {
Thread.currentThread().setName("ReadUDPByAirScoutThread");
@@ -89,6 +102,12 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
}
socket.receive(packet);
} catch (SocketTimeoutException e2) {
// this will catch the repeating Sockettimeoutexception...nothing to do
// e2.printStackTrace();
@@ -104,29 +123,44 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
received = received.trim();
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
System.out.println("ReadUdpByASMsgTh, Info: got poison, now dieing....");
try {
terminateConnection();
} catch (Exception e) {
System.out.println("ASUDPRDR: catched error " + e.getMessage());
}
break;
}
if (received.contains("ASSETPATH") || received.contains("ASWATCHLIST")) {
// do nothing, that is your own message
} else if (received.contains("ASNEAREST:")) { //answer by airscout
processASUDPMessage(received);
// System.out.println("[ReadUSPASTh, info:] received AS String " + received);
// processASUDPMessage(received); //TODO: 2025-11-Zeile deaktiviert. Fand hier Doppelberechnung statt?!
AirPlaneReflectionInfo apReflectInfoForChatMember;
apReflectInfoForChatMember = processASUDPMessage(received);
if (this.client.getLst_chatMemberList().size() != 0) {
if (!this.client.getLst_chatMemberList().isEmpty()) {
try {
// if (this.client.checkListForChatMemberIndexByCallSign(apReflectInfoForChatMember.getReceiver()) != -1) {
// this.client.getLst_chatMemberList()
// .get(this.client.checkListForChatMemberIndexByCallSign(
// apReflectInfoForChatMember.getReceiver()))
// .setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
// // the central instance of
// // chatmember list .... -1 is a
// // problem!
ArrayList<Integer> addApInfoToThese = this.client.checkListForChatMemberIndexesByCallSign(apReflectInfoForChatMember.getReceiver());
addApInfoToThese.forEach((integerIndex) -> {this.client.getLst_chatMemberList().get(integerIndex).setAirPlaneReflectInfo(apReflectInfoForChatMember); });
// AirScout availability strongly affects priority => request recompute the score of the chatmember
this.client.getScoreService().requestRecompute("airscout-update");
this.client.getLst_chatMemberList()
.get(this.client.checkListForChatMemberIndexByCallSign(
apReflectInfoForChatMember.getReceiver()))
.setAirPlaneReflectInfo(apReflectInfoForChatMember); // TODO: here we set the ap info at
// the central instance of
// chatmember list .... -1 is a
// problem!
/**
* CK| MSGBUS BGFX Listactualizer Exception in thread "Thread-10"
* java.util.ConcurrentModificationException at
@@ -137,6 +171,7 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
* kst4contest.controller.ReadUDPbyAirScoutMessageThread.run(ReadUDPbyAirScoutMessageThread.java:93)
*
*/
// System.out.println("[ReadUdpByASth, AP-Info catched: ] " + apReflectInfoForChatMember.toString());
// }
} catch (Exception e) {
@@ -146,6 +181,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
// TODO: handle exception
}
// String[] newState = new String[3];
// newState[0] = "On";
// newState[1] = "received line";
// newState[2] = apReflectInfoForChatMember.toString();
// callThreadStateToUi(newState);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received line\n" + apReflectInfoForChatMember.toString(), false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
}
}
@@ -261,9 +303,13 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
return apInfo;
}
public boolean terminateConnection() throws IOException {
public boolean terminateConnection() {
this.socket.close();
try {
this.socket.close();
} catch (Exception e) {
System.out.println("udpbyas: catched " + e.getMessage());
}
return true;
}

View File

@@ -3,12 +3,17 @@ package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import kst4contest.ApplicationConstants;
import kst4contest.model.ThreadStateMessage;
import kst4contest.view.GuiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -29,13 +34,19 @@ public class ReadUDPbyUCXMessageThread extends Thread {
private BufferedReader reader;
private Socket socket;
private ChatController client;
private int udpPortNr = 12060;
private ThreadStatusCallback callBackToController;
private String ThreadNickName = "UDP-Log msg";
public ReadUDPbyUCXMessageThread(int localPort) {
// public ReadUDPbyUCXMessageThread(int localPort , ThreadStatusCallback callback) {
//
//// this.callBackToController = callback;
// }
}
public ReadUDPbyUCXMessageThread(int localPort, ChatController client) {
this.client = client;
public ReadUDPbyUCXMessageThread(int localPort, ChatController client, ThreadStatusCallback callback) {
this.udpPortNr = localPort;
this.client = client;
this.callBackToController = callback;
}
@Override
@@ -43,27 +54,35 @@ public class ReadUDPbyUCXMessageThread extends Thread {
super.interrupt();
try {
if (this.socket != null) {
this.socket.close();
System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket");
terminateConnection();
// callBackToController.onThreadStatus("UDPReceiver", new String[]);
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("UCXUDPRDR: catched error " + e.getMessage());
}
}
public void run() {
System.out.println("ReadUDPByUCXLogThread: started Thread for UCXLog getUDP");
Thread.currentThread().setName("ReadUDPByUCXLogThread");
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "initialized", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
DatagramSocket socket = null;
boolean running;
boolean running;
byte[] buf = new byte[1777];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
try {
socket = new DatagramSocket(12060);
socket.setSoTimeout(11000); //TODO try for end properly
// socket = new DatagramSocket(12060);
socket = new DatagramSocket(udpPortNr);
socket.setSoTimeout(2000); //TODO try for end properly
}
catch (SocketException e) {
@@ -75,22 +94,45 @@ public class ReadUDPbyUCXMessageThread extends Thread {
boolean timeOutIndicator = false;
if (this.client.isDisconnectionPerformedByUser()) {
break;//TODO: what if it´s not the finally closage but a band channel change?
}
// packet = new DatagramPacket(buf, buf.length); //TODO: Changed that due to memory leak, check if all works (seems like that)
// DatagramPacket packet = new DatagramPacket(SRPDefinitions.BYTE_BUFFER_MAX_LENGTH); //TODO: Changed that due to memory leak, check if all works (seems like that)
try {
socket.receive(packet);
} catch (SocketTimeoutException e2) {
timeOutIndicator = true;
// this will catch the repeating Sockettimeoutexception...nothing to do
// e2.printStackTrace();
}
}
catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NullPointerException nE) {
// TODO Auto-generated catch block
nE.printStackTrace();
System.out.println("ReadUdpByUCXTH: Socket not ready");
try {
socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort());
socket.setSoTimeout(2000);
} catch (SocketException e) {
System.out.println("[ReadUDPByUCSMsgTH, Error]: socket in use or something:");
e.printStackTrace();
try {
socket = new DatagramSocket(null);
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort()));
socket.receive(packet);
socket.setSoTimeout(3000);
} catch (Exception ex) {
System.out.println("ReadUDPByUCXMsgTh: Could not solve that. Program Restart needed.");
throw new RuntimeException(ex);
}
}
}
InetAddress address = packet.getAddress();
@@ -99,9 +141,26 @@ public class ReadUDPbyUCXMessageThread extends Thread {
String received = new String(packet.getData(), packet.getOffset(), packet.getLength());
received = received.trim();
// System.out.println("recvudpucx");
// System.out.println("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<recv " + received);
if (received.contains(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
System.out.println("ReadUdpByUCX, Info: got poison, now dieing....");
socket.close();
timeOutIndicator = true;
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "stopped";
// threadStatusMessage[1] = "by poisonpill message (disconnect on purpose)";
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "stopped by Poisonpill", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
break;
}
if (this.client.isDisconnectionPerformedByUser()) {
break;//TODO: what if it´s not the finally closage but a band channel change?
}
if (!timeOutIndicator) {
processUCXUDPMessage(received);
} else {
@@ -122,7 +181,17 @@ public class ReadUDPbyUCXMessageThread extends Thread {
ChatMember modifyThat = null;
// System.out.println(udpMsg);
// System.out.println("ReadUDPByUCX, message catched: " + udpMsg);
// String[] threadStatusMessage = new String[2];
// threadStatusMessage = new String[3];
// threadStatusMessage[0] = "on";
// threadStatusMessage[1] = "received message:";
// threadStatusMessage[2] = udpMsg;
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "received Message\n" + udpMsg, false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
@@ -155,8 +224,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
// call = call.toLowerCase();
String band = element.getElementsByTagName("band").item(0).getTextContent();
String points = element.getElementsByTagName("points").item(0).getTextContent();
System.out.println("[Readudp, info ]: received Current Element :" + node.getNodeName()
+ "call: " + call + " / " + band);
+ "call: " + call + " / " + band + " ----> " + points + " POINTS");
// client.getChatPreferences().setBcn_contestScoreSum(Long.parseLong(points));
ChatMember workedCall = new ChatMember();
workedCall.setCallSign(call);
@@ -195,6 +268,44 @@ public class ReadUDPbyUCXMessageThread extends Thread {
case "10G": {
workedCall.setWorked10G(true);
break;
}
/**
* cases hotfix for MINOS logger, which tells band like "2m", not "144"
*/
case "2m": {
workedCall.setWorked144(true);
break;
}
case "70cm": {
workedCall.setWorked432(true);
break;
}
case "23cm": {
workedCall.setWorked1240(true);
break;
}
case "13cm": {
workedCall.setWorked2300(true);
break;
}
case "9cm": {
workedCall.setWorked3400(true);
break;
}
case "6cm": {
workedCall.setWorked5600(true);
break;
}
case "3cm": {
workedCall.setWorked10G(true);
}
@@ -216,68 +327,135 @@ public class ReadUDPbyUCXMessageThread extends Thread {
// modifyThat = (ChatMember) client.getMap_ucxLogInfoWorkedCalls().get(call);
int indexOfChatMemberInTable = -1;
indexOfChatMemberInTable = client.checkListForChatMemberIndexByCallSign(workedCall);
// asd //TODO: Check if callsign and callsignraw is similar, then mark first and further via new checklistforchatmembermultiplemethod with array of indize
if (indexOfChatMemberInTable == -1) {
// do nothing
ArrayList<Integer> markTheseChattersAsWorked = client.checkListForChatMemberIndexesByCallSign(workedCall);
if (markTheseChattersAsWorked.isEmpty()) {
//Worked call is not part of the chatmember list
} else {
modifyThat = client.getLst_chatMemberList().get(indexOfChatMemberInTable);
// modifyThat.setWorked(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
for (int index : markTheseChattersAsWorked) {
modifyThat = client.getLst_chatMemberList().get(index);
if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
.setWorked144(true);
modifyThat.setWorked(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
.setWorked432(true);
if (workedCall.isWorked144()) {
modifyThat.setWorked144(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
.setWorked1240(true);
} else if (workedCall.isWorked432()) {
modifyThat.setWorked432(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
.setWorked2300(true);
} else if (workedCall.isWorked1240()) {
modifyThat.setWorked1240(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
.setWorked3400(true);
} else if (workedCall.isWorked2300()) {
modifyThat.setWorked2300(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
.setWorked5600(true);
} else if (workedCall.isWorked3400()) {
modifyThat.setWorked3400(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
client.getLst_chatMemberList()
.get(client.checkListForChatMemberIndexByCallSign(modifyThat))
.setWorked10G(true);
} else if (workedCall.isWorked5600()) {
modifyThat.setWorked5600(true);
} else if (workedCall.isWorked10G()) {
modifyThat.setWorked10G(true);
}
}
try {
GuiUtils.triggerGUIFilteredChatMemberListChange(this.client);
// BEGIN PATCH: trigger band-upgrade hint after log entry (UCXLog)
try {
client.onExternalLogEntryReceived(workedCall.getCallSignRaw());
} catch (Exception e) {
System.out.println("[UCXUDPRcvr, warning]: band-upgrade hint failed: " + e.getMessage());
}
} catch (Exception IllegalStateException) {
//do nothing, as it works...
}
}
/**
* old mechanic to markup worked stations in the chatmember table
*/
// int indexOfChatMemberInTable = -1; //chatmember not in table
// indexOfChatMemberInTable = client.checkListForChatMemberIndexByCallSign(workedCall);
//
// if (indexOfChatMemberInTable == -1) {
// // do nothing
// } else {
// modifyThat = client.getLst_chatMemberList().get(indexOfChatMemberInTable);
//
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat)).setWorked(true);
//
// if (workedCall.isWorked144()) {
// modifyThat.setWorked144(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked144(true);
//
// } else if (workedCall.isWorked432()) {
// modifyThat.setWorked432(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked432(true);
//
// } else if (workedCall.isWorked1240()) {
// modifyThat.setWorked1240(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked1240(true);
//
// } else if (workedCall.isWorked2300()) {
// modifyThat.setWorked2300(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked2300(true);
//
// } else if (workedCall.isWorked3400()) {
// modifyThat.setWorked3400(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked3400(true);
//
// } else if (workedCall.isWorked5600()) {
// modifyThat.setWorked5600(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked5600(true);
//
// } else if (workedCall.isWorked10G()) {
// modifyThat.setWorked10G(true);
// client.getLst_chatMemberList()
// .get(client.checkListForChatMemberIndexByCallSign(modifyThat))
// .setWorked10G(true);
// }
/**
* //TODO: following line is a quick fix to making disappear worked chatmembers of the list
* Thats uncomfortable due to this also causes selection changes,
* Better way is to change all worked and qrv values to observables and then trigger the underlying
* list to fire an invalidationevent. Really Todo!
*/
// try{
//
// GuiUtils.triggerGUIFilteredChatMemberListChange(client); //not clean at all
// } catch (Exception IllegalStateException) {
// //do nothing, as it works...
// }
// }
/**
* end -> old mechanic to markup worked stations in the chatmember table
*/
}
boolean isInChat = this.client.getDbHandler().updateWkdInfoOnChatMember(workedCall);
// This will update the worked info on a worked chatmember. DBHandler will
// check, if an entry at the db had been modified. If not, then the worked
// station had not been stored. DBHandler will store the informations then.
// station had not been stored. DBHandler will store the information then.
if (!isInChat) {
workedCall.setName("unknown");
@@ -297,7 +475,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
@@ -373,9 +550,9 @@ public class ReadUDPbyUCXMessageThread extends Thread {
// System.out.println("Radio Mode: " + mode);
// System.out.println("[ReadUDPFromUCX, Info:] Setted QRG pref to: \"" + qrg + "\"" );
this.client.getChatPreferences().getMYQRG().set(formattedQRG);
this.client.getChatPreferences().getMYQRGFirstCat().set(formattedQRG);
System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
// System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
}
}
@@ -385,9 +562,27 @@ public class ReadUDPbyUCXMessageThread extends Thread {
e.printStackTrace();
System.out.println(e.getCause());
System.out.println(e.getMessage());
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
threadStateMessage = new ThreadStateMessage(this.ThreadNickName, true, "CRASHED" + udpMsg, true);
threadStateMessage.setCriticalStateFurtherInfo(Arrays.toString(e.getStackTrace()));
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = Arrays.toString(e.getStackTrace());
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
}
// System.out.println("[ReadUDPbyUCXTh: ] worked size = " + this.client.getMap_ucxLogInfoWorkedCalls().size());
@@ -397,6 +592,14 @@ public class ReadUDPbyUCXMessageThread extends Thread {
}
public boolean terminateConnection() throws IOException {
// String[] threadStatusMessage = new String[2];
// threadStatusMessage = new String[2];
// threadStatusMessage[0] = "STOPPED";
// threadStatusMessage[1] = "Connection terminated for purpose.";
// callBackToController.onThreadStatus(ThreadNickName,threadStatusMessage);
ThreadStateMessage threadStateMessage = new ThreadStateMessage(this.ThreadNickName, false, "terminated", false);
callBackToController.onThreadStatus(ThreadNickName,threadStateMessage);
this.socket.close();

View File

@@ -0,0 +1,309 @@
package kst4contest.controller;
import javafx.application.Platform;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.SimpleLongProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import kst4contest.logic.PriorityCalculator;
import kst4contest.model.ChatCategory;
import kst4contest.model.ChatMember;
import kst4contest.model.ChatPreferences;
import kst4contest.model.ContestSked;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
/**
* Calculates priority scores off the JavaFX thread and publishes a small UI model.
*
* Design goals:
* - No per-member Platform.runLater flooding.
* - Score is computed once per callsignRaw (e.g. "SM6VTZ"), even if it exists in multiple chat categories.
* - A routing hint (preferred ChatCategory) is kept using "last inbound category" if available.
*/
public final class ScoreService {
public static final int DEFAULT_TOP_N = 15; //how many top places we have?
/** Force a refresh at least every X ms (some scoring inputs are time dependent). */
private static final long MAX_SNAPSHOT_AGE_MS = 10_000L;
private final ChatController controller;
private final PriorityCalculator priorityCalculator;
private final AtomicBoolean recomputeRequested = new AtomicBoolean(true);
private final AtomicReference<ScoreSnapshot> latestSnapshot = new AtomicReference<>(ScoreSnapshot.empty());
// UI outputs
private final ObservableList<TopCandidate> topCandidatesFx = FXCollections.observableArrayList();
private final ReadOnlyDoubleWrapper selectedCallPriorityScore = new ReadOnlyDoubleWrapper(Double.NaN);
private final LongProperty uiPulse = new SimpleLongProperty(0);
private volatile String selectedCallSignRaw;
private volatile long lastComputedEpochMs = 0L;
private final int topN;
private final ObjectProperty<ChatMember> selectedChatMember = new SimpleObjectProperty<>(null);
public ScoreService(ChatController controller, PriorityCalculator priorityCalculator, int topN) {
this.controller = Objects.requireNonNull(controller, "controller");
this.priorityCalculator = Objects.requireNonNull(priorityCalculator, "priorityCalculator");
this.topN = topN > 0 ? topN : DEFAULT_TOP_N;
}
public ObservableList<TopCandidate> getTopCandidatesFx() {
return topCandidatesFx;
}
public ReadOnlyDoubleProperty selectedCallPriorityScoreProperty() {
return selectedCallPriorityScore.getReadOnlyProperty();
}
/**
* A lightweight UI invalidation signal that increments after every published snapshot.
* Consumers can refresh small panels (timeline/toplist), but should avoid refreshing huge tables.
*/
public LongProperty uiPulseProperty() {
return uiPulse;
}
public ScoreSnapshot getLatestSnapshot() {
return latestSnapshot.get();
}
/** Coalesced recompute request (safe to call frequently from other threads). */
public void requestRecompute(String reason) {
recomputeRequested.set(true);
}
/** Called by UI when selection changes. */
public void setSelectedChatMember(ChatMember member) {
// keep a central selection for UI actions (FurtherInfo buttons, timeline clicks, etc.)
if (Platform.isFxApplicationThread()) {
selectedChatMember.set(member);
} else {
Platform.runLater(() -> selectedChatMember.set(member));
}
selectedCallSignRaw = member == null ? null : normalizeCallRaw(member.getCallSignRaw());
// Update score immediately from the latest snapshot
if (Platform.isFxApplicationThread()) {
updateSelectedScoreFromSnapshot(latestSnapshot.get());
} else {
Platform.runLater(() -> updateSelectedScoreFromSnapshot(latestSnapshot.get()));
}
}
/**
* Called periodically by the scheduler thread.
* Recomputes only if explicitly requested or if the snapshot is too old.
*/
public void tick() {
long now = System.currentTimeMillis();
boolean shouldRecompute = recomputeRequested.getAndSet(false) || (now - lastComputedEpochMs) > MAX_SNAPSHOT_AGE_MS;
if (!shouldRecompute) return;
try {
// Apply "no reply" strikes (operator pinged via /cq but no inbound line arrived)
controller.getStationMetricsService().evaluateNoReplyTimeouts(now, controller.getChatPreferences());
recompute(now);
} catch (Exception e) {
System.err.println("[ScoreService] CRITICAL error while recomputing scores");
e.printStackTrace();
}
}
private void recompute(long nowEpochMs) {
// Keep sked list clean (must happen on FX thread)
controller.requestRemoveExpiredSkeds(nowEpochMs);
final List<ChatMember> members = controller.snapshotChatMembers();
final List<ContestSked> activeSkeds = controller.snapshotActiveSkeds();
final ChatPreferences prefs = controller.getChatPreferences();
final Map<String, ChatCategory> lastInbound = controller.snapshotLastInboundCategoryMap();
StationMetricsService.Snapshot metricsSnapshot =
controller.getStationMetricsService().snapshot(nowEpochMs, prefs);
// 1) Choose one representative per callsignRaw
Map<String, ChatMember> representativeByCallRaw = chooseRepresentativeMembers(members, lastInbound);
// 2) Compute score once per callsignRaw
Map<String, Double> scoreByCallRaw = new HashMap<>(representativeByCallRaw.size());
Map<String, ChatCategory> preferredCategoryByCallRaw = new HashMap<>(representativeByCallRaw.size());
List<TopCandidate> topAll = new ArrayList<>(representativeByCallRaw.size());
for (Map.Entry<String, ChatMember> e : representativeByCallRaw.entrySet()) {
String callRaw = e.getKey();
ChatMember representative = e.getValue();
if (representative == null) continue;
double score = priorityCalculator.calculatePriority(
representative,
prefs,
activeSkeds,
metricsSnapshot,
nowEpochMs
);
scoreByCallRaw.put(callRaw, score);
preferredCategoryByCallRaw.put(callRaw, representative.getChatCategory());
topAll.add(new TopCandidate(callRaw, representative.getCallSign(), representative.getChatCategory(), score));
}
// 3) Build Top-N
topAll.sort(Comparator.comparingDouble(TopCandidate::getScore).reversed());
List<TopCandidate> topNList = topAll.size() <= topN ? topAll : new ArrayList<>(topAll.subList(0, topN));
ScoreSnapshot snap = new ScoreSnapshot(
nowEpochMs,
Collections.unmodifiableMap(scoreByCallRaw),
Collections.unmodifiableMap(preferredCategoryByCallRaw),
Collections.unmodifiableList(topNList)
);
latestSnapshot.set(snap);
lastComputedEpochMs = nowEpochMs;
// 4) Publish to UI in ONE batched runLater
Platform.runLater(() -> {
topCandidatesFx.setAll(snap.getTopCandidates());
updateSelectedScoreFromSnapshot(snap);
uiPulse.set(uiPulse.get() + 1);
});
}
/**
* Picks one ChatMember object per callsignRaw.
* Preference order:
* 1) Variant in last inbound chat category (stable reply routing)
* 2) Most recently active variant (fallback)
*/
private Map<String, ChatMember> chooseRepresentativeMembers(
List<ChatMember> members,
Map<String, ChatCategory> lastInboundCategoryByCallRaw
) {
Map<String, List<ChatMember>> byCallRaw = new HashMap<>();
for (ChatMember m : members) {
if (m == null) continue;
String callRaw = normalizeCallRaw(m.getCallSignRaw());
if (callRaw == null || callRaw.isEmpty()) continue;
byCallRaw.computeIfAbsent(callRaw, k -> new ArrayList<>()).add(m);
}
Map<String, ChatMember> representative = new HashMap<>(byCallRaw.size());
for (Map.Entry<String, List<ChatMember>> entry : byCallRaw.entrySet()) {
String callRaw = entry.getKey();
List<ChatMember> variants = entry.getValue();
ChatCategory preferredCat = lastInboundCategoryByCallRaw.get(callRaw);
ChatMember chosen = null;
if (preferredCat != null) {
for (ChatMember v : variants) {
if (v != null && v.getChatCategory() == preferredCat) {
chosen = v;
break;
}
}
}
if (chosen == null) {
chosen = variants.stream()
.filter(Objects::nonNull)
.max(Comparator.comparingLong(ChatMember::getActivityTimeLastInEpoch))
.orElse(null);
}
if (chosen != null) representative.put(callRaw, chosen);
}
return representative;
}
private void updateSelectedScoreFromSnapshot(ScoreSnapshot snap) {
if (snap == null || selectedCallSignRaw == null) {
selectedCallPriorityScore.set(Double.NaN);
return;
}
Double v = snap.getScoreByCallSignRaw().get(selectedCallSignRaw);
selectedCallPriorityScore.set(v == null ? Double.NaN : v);
}
private static String normalizeCallRaw(String callRaw) {
if (callRaw == null) return null;
return callRaw.trim().toUpperCase();
}
// ------------------------- DTOs -------------------------
public static final class TopCandidate {
private final String callSignRaw;
private final String displayCallSign;
private final ChatCategory preferredChatCategory;
private final double score;
public TopCandidate(String callSignRaw, String displayCallSign, ChatCategory preferredChatCategory, double score) {
this.callSignRaw = callSignRaw;
this.displayCallSign = displayCallSign;
this.preferredChatCategory = preferredChatCategory;
this.score = score;
}
public String getCallSignRaw() { return callSignRaw; }
public String getDisplayCallSign() { return displayCallSign; }
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
public double getScore() { return score; }
}
public static final class ScoreSnapshot {
private final long computedAtEpochMs;
private final Map<String, Double> scoreByCallSignRaw;
private final Map<String, ChatCategory> preferredCategoryByCallSignRaw;
private final List<TopCandidate> topCandidates;
public ScoreSnapshot(long computedAtEpochMs,
Map<String, Double> scoreByCallSignRaw,
Map<String, ChatCategory> preferredCategoryByCallSignRaw,
List<TopCandidate> topCandidates) {
this.computedAtEpochMs = computedAtEpochMs;
this.scoreByCallSignRaw = scoreByCallSignRaw;
this.preferredCategoryByCallSignRaw = preferredCategoryByCallSignRaw;
this.topCandidates = topCandidates;
}
public static ScoreSnapshot empty() {
return new ScoreSnapshot(System.currentTimeMillis(), Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList());
}
public long getComputedAtEpochMs() { return computedAtEpochMs; }
public Map<String, Double> getScoreByCallSignRaw() { return scoreByCallSignRaw; }
public Map<String, ChatCategory> getPreferredCategoryByCallSignRaw() { return preferredCategoryByCallSignRaw; }
public List<TopCandidate> getTopCandidates() { return topCandidates; }
}
public ReadOnlyObjectProperty<ChatMember> selectedChatMemberProperty() {
return selectedChatMember;
}
public ChatMember getSelectedChatMember() {
return selectedChatMember.get();
}
}

View File

@@ -0,0 +1,65 @@
package kst4contest.controller;
import kst4contest.model.ChatMessage;
import java.util.TimerTask;
/**
* This class is updateing the scoreboard at https://slovhf.net/claimed/. Gets scores of all bands out of the
* preferences which is updated via ReadUdpByUCXLog Thread.
*
* api description: https://slovhf.net/claimed-score-api/
*
* <br/><br/>
* The task will be runned out of the singleton ChatController instance in an
* intervall as specified by the Chatpreferences-instance (typically as
* configured in the xml file.
*
*
* @author prakt
*
*/
public class ScoreboardUpdateTask extends TimerTask {
private ChatController chatController;
public ScoreboardUpdateTask(ChatController client) {
this.chatController = client;
}
@Override
public void run() {
Thread.currentThread().setName("BeaconTask");
ChatMessage beaconMSG = new ChatMessage();
String replaceVariables = this.chatController.getChatPreferences().getBcn_beaconTextMainCat();
// replaceVariables = bcn_beaconText;
replaceVariables = replaceVariables.replaceAll("MYQRG", this.chatController.getChatPreferences().getMYQRGFirstCat().getValue());
replaceVariables = replaceVariables.replaceAll("MYCALL", this.chatController.getChatPreferences().getStn_loginCallSign());
replaceVariables = replaceVariables.replaceAll("MYLOCATOR", this.chatController.getChatPreferences().getStn_loginLocatorMainCat());
replaceVariables = replaceVariables.replaceAll("MYQTF", this.chatController.getChatPreferences().getActualQTF().getValue() + "");
beaconMSG.setMessageText(
"MSG|" + this.chatController.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|" + replaceVariables + "|0|");
beaconMSG.setMessageDirectedToServer(true);
// System.out.println("########### " + replaceVariables);
if (this.chatController.getChatPreferences().isBcn_beaconsEnabledMainCat() ) {
System.out.println(new Utils4KST().time_generateCurrentMMDDhhmmTimeString()
+ " [BeaconTask, Info]: Sending CQ: " + beaconMSG.getMessageText());
this.chatController.getMessageTXBus().add(beaconMSG);
} else {
//do nothing, CQ is disabled
}
}
}

View File

@@ -0,0 +1,124 @@
package kst4contest.controller;
import javafx.application.Platform;
import kst4contest.model.ChatCategory;
import kst4contest.model.ThreadStateMessage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
* Schedules PM reminders for a specific sked time.
*
* Requirements:
* - Reminder goes out as PM to the station (via "/cq CALL ...").
* - Reminders are armed manually from FurtherInfo.
*/
public final class SkedReminderService {
private final ChatController controller;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r);
t.setDaemon(true);
t.setName("SkedReminderService");
return t;
});
private final ConcurrentHashMap<String, List<ScheduledFuture<?>>> scheduledByCallRaw = new ConcurrentHashMap<>();
public SkedReminderService(ChatController controller) {
this.controller = controller;
}
/**
* Arms reminders for one sked. Existing reminders for this call are cancelled.
*
* @param callSignRaw target call
* @param preferredCategory where to send (if null, controller resolves via lastInbound category)
* @param skedTimeEpochMs sked time
* @param offsetsMinutes e.g. [5,2,1] => reminders 5,2,1 minutes before
*/
public void armReminders(String callSignRaw,
ChatCategory preferredCategory,
long skedTimeEpochMs,
List<Integer> offsetsMinutes) {
String callRaw = normalize(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
cancelReminders(callRaw);
long now = System.currentTimeMillis();
List<Integer> offsets = (offsetsMinutes == null) ? List.of() : offsetsMinutes;
List<ScheduledFuture<?>> futures = new ArrayList<>();
for (Integer offMin : offsets) {
if (offMin == null) continue;
long fireAt = skedTimeEpochMs - (offMin * 60_000L);
long delayMs = fireAt - now;
if (delayMs <= 0) continue;
ScheduledFuture<?> f = scheduler.schedule(
() -> fireReminder(callRaw, preferredCategory, offMin),
delayMs,
TimeUnit.MILLISECONDS
);
futures.add(f);
}
scheduledByCallRaw.put(callRaw, futures);
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", true,
"Armed for " + callRaw + " (" + offsets + " min before)", false));
}
public void cancelReminders(String callSignRaw) {
String callRaw = normalize(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
List<ScheduledFuture<?>> futures = scheduledByCallRaw.remove(callRaw);
if (futures != null) {
for (ScheduledFuture<?> f : futures) {
if (f != null) f.cancel(false);
}
}
}
private void fireReminder(String callRaw, ChatCategory preferredCategory, int minutesBefore) {
try {
controller.queuePrivateCqMessage(callRaw, preferredCategory, "[KST4C Autoreminder] sked in " + minutesBefore + " min");
controller.fireUiReminderEvent(callRaw, minutesBefore); //triggers some blingbling in the UI
///Local acoustic hint (reuse existing project audio utilities, no AWT, no extra JavaFX modules)
try {
if (controller.getChatPreferences().isNotify_playSimpleSounds()) {
controller.getPlayAudioUtils().playNoiseLauncher('!'); // choose a suitable char you already use
}
// Optional: voice/cw hint (short, not too intrusive)
// controller.getPlayAudioUtils().playCWLauncher(" SKED " + minutesBefore);
} catch (Exception ignore) {
// never block reminder sending because of audio issues
}
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", true,
"PM reminder sent to " + callRaw + " (" + minutesBefore + " min)", false));
} catch (Exception e) {
controller.onThreadStatus("SkedReminderService",
new ThreadStateMessage("SkedReminder", false,
"ERROR sending reminder to " + callRaw + ": " + e.getMessage(), true));
e.printStackTrace();
}
}
private static String normalize(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
}

View File

@@ -3,6 +3,9 @@
*/
package kst4contest.controller;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableStringValue;
import java.io.IOException;
/**
@@ -19,7 +22,9 @@ public class StartChat {
public static void main(String[] args) throws InterruptedException, IOException {
System.out.println("[Startchat:] Starting new Chat instance");
// ObservableStringValue messageBus = new SimpleStringProperty("");
ChatController client = new ChatController();
client.execute();

View File

@@ -0,0 +1,268 @@
package kst4contest.controller;
import kst4contest.logic.SignalDetector;
import kst4contest.model.ChatPreferences;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Thread-safe metrics store keyed by normalized callsignRaw (e.g. "SM6VTZ").
*
* Purpose:
* - Provide inputs for scoring (momentum, reply time, no-reply strikes, manual sked-fail, positive signals).
* - Decouple MessageBus / TX from ScoreService (only data flows, no UI calls here).
*/
public final class StationMetricsService {
/** /cq <CALL> ... */
private static final Pattern OUTBOUND_CQ_PATTERN = Pattern.compile("(?i)^\\s*/cq\\s+([A-Z0-9/]+)\\b.*");
/** Rolling window timestamps for momentum scoring. */
private static final int MAX_STORED_INBOUND_TIMESTAMPS = 32;
private final ConcurrentHashMap<String, StationMetrics> byCallRaw = new ConcurrentHashMap<>();
/**
* Called when the operator sends a message.
* If it is a "/cq CALL ..." message, this arms a pending ping for response-time / no-reply tracking.
*/
public Optional<String> tryRecordOutboundCq(String messageText, long nowEpochMs) {
if (messageText == null) return Optional.empty();
Matcher m = OUTBOUND_CQ_PATTERN.matcher(messageText.trim());
if (!m.matches()) return Optional.empty();
String callRaw = normalizeCallRaw(m.group(1));
if (callRaw == null || callRaw.isBlank()) return Optional.empty();
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.pendingCqSentAtEpochMs = nowEpochMs;
metrics.lastOutboundCqEpochMs = nowEpochMs;
}
return Optional.of(callRaw);
}
/**
* Called for EVERY inbound line from a station (CH or PM).
* "Any line counts as activity"
*/
public void onInboundMessage(String senderCallSignRaw,
long nowEpochMs,
String messageText,
ChatPreferences prefs,
String ownCallSignRaw) {
String callRaw = normalizeCallRaw(senderCallSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
// ignore own echoed messages
if (ownCallSignRaw != null && callRaw.equalsIgnoreCase(normalizeCallRaw(ownCallSignRaw))) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.lastInboundEpochMs = nowEpochMs;
// rolling timestamps (momentum)
metrics.recentInboundEpochMs.addLast(nowEpochMs);
while (metrics.recentInboundEpochMs.size() > MAX_STORED_INBOUND_TIMESTAMPS) {
metrics.recentInboundEpochMs.removeFirst();
}
// positive signal detection (extendable by prefs)
if (messageText != null && prefs != null) {
if (SignalDetector.containsPositiveSignal(messageText, prefs.getNotify_positiveSignalsPatterns())) {
metrics.lastPositiveSignalEpochMs = nowEpochMs;
}
}
// response time measurement: any inbound line ends a pending ping
if (metrics.pendingCqSentAtEpochMs > 0) {
long rttMs = Math.max(0, nowEpochMs - metrics.pendingCqSentAtEpochMs);
metrics.pendingCqSentAtEpochMs = 0;
// EWMA for response time (stable, no spikes)
final double alpha = 0.25;
if (metrics.avgResponseTimeMs <= 0) {
metrics.avgResponseTimeMs = rttMs;
} else {
metrics.avgResponseTimeMs = alpha * rttMs + (1.0 - alpha) * metrics.avgResponseTimeMs;
}
}
}
}
/**
* Called periodically (e.g. from ScoreService.tick()).
* Applies a "no reply" strike if the pending ping is older than prefs timeout.
*/
public void evaluateNoReplyTimeouts(long nowEpochMs, ChatPreferences prefs) {
if (prefs == null) return;
long timeoutMs = Math.max(1, prefs.getNotify_noReplyPenaltyMinutes()) * 60_000L;
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
StationMetrics metrics = e.getValue();
if (metrics == null) continue;
synchronized (metrics) {
if (metrics.pendingCqSentAtEpochMs <= 0) continue;
long age = nowEpochMs - metrics.pendingCqSentAtEpochMs;
if (age >= timeoutMs) {
metrics.pendingCqSentAtEpochMs = 0;
metrics.noReplyStrikes++;
metrics.lastNoReplyStrikeEpochMs = nowEpochMs;
}
}
}
}
/** Manual sked fail: permanent until reset. */
public void markManualSkedFail(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.manualSkedFailed = true;
metrics.manualSkedFailCount++;
}
}
public void resetManualSkedFail(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return;
StationMetrics metrics = byCallRaw.computeIfAbsent(callRaw, k -> new StationMetrics());
synchronized (metrics) {
metrics.manualSkedFailed = false;
metrics.manualSkedFailCount = 0;
}
}
public boolean isManualSkedFailed(String callSignRaw) {
String callRaw = normalizeCallRaw(callSignRaw);
if (callRaw == null || callRaw.isBlank()) return false;
StationMetrics metrics = byCallRaw.get(callRaw);
if (metrics == null) return false;
synchronized (metrics) {
return metrics.manualSkedFailed;
}
}
/** Immutable snapshot for scoring */
public Snapshot snapshot(long nowEpochMs, ChatPreferences prefs) {
long momentumWindowMs = (prefs != null ? prefs.getNotify_momentumWindowSeconds() : 180) * 1000L;
Snapshot snap = new Snapshot(nowEpochMs, momentumWindowMs);
for (Map.Entry<String, StationMetrics> e : byCallRaw.entrySet()) {
String callRaw = e.getKey();
StationMetrics m = e.getValue();
if (m == null) continue;
synchronized (m) {
snap.byCallRaw.put(callRaw, new Snapshot.Metrics(
m.lastInboundEpochMs,
countRecent(m.recentInboundEpochMs, nowEpochMs, momentumWindowMs),
m.avgResponseTimeMs,
m.noReplyStrikes,
m.manualSkedFailed,
m.manualSkedFailCount,
m.lastPositiveSignalEpochMs
));
}
}
return snap;
}
private static int countRecent(Deque<Long> timestamps, long nowEpochMs, long windowMs) {
if (timestamps == null || timestamps.isEmpty()) return 0;
int cnt = 0;
for (Long t : timestamps) {
if (t == null) continue;
if (nowEpochMs - t <= windowMs) cnt++;
}
return cnt;
}
private static String normalizeCallRaw(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
private static final class StationMetrics {
long lastInboundEpochMs;
long lastOutboundCqEpochMs;
long pendingCqSentAtEpochMs; // 0 = none
int noReplyStrikes;
long lastNoReplyStrikeEpochMs;
double avgResponseTimeMs; // EWMA
final Deque<Long> recentInboundEpochMs = new ArrayDeque<>();
long lastPositiveSignalEpochMs;
boolean manualSkedFailed;
int manualSkedFailCount;
}
public static final class Snapshot {
private final long snapshotEpochMs;
private final long momentumWindowMs;
private final ConcurrentHashMap<String, Metrics> byCallRaw = new ConcurrentHashMap<>();
private Snapshot(long snapshotEpochMs, long momentumWindowMs) {
this.snapshotEpochMs = snapshotEpochMs;
this.momentumWindowMs = momentumWindowMs;
}
public Metrics get(String callSignRaw) {
if (callSignRaw == null) return null;
return byCallRaw.get(normalizeCallRaw(callSignRaw));
}
public long getSnapshotEpochMs() {
return snapshotEpochMs;
}
public long getMomentumWindowMs() {
return momentumWindowMs;
}
public static final class Metrics {
public final long lastInboundEpochMs;
public final int inboundCountInWindow;
public final double avgResponseTimeMs;
public final int noReplyStrikes;
public final boolean manualSkedFailed;
public final int manualSkedFailCount;
public final long lastPositiveSignalEpochMs;
public Metrics(long lastInboundEpochMs,
int inboundCountInWindow,
double avgResponseTimeMs,
int noReplyStrikes,
boolean manualSkedFailed,
int manualSkedFailCount,
long lastPositiveSignalEpochMs) {
this.lastInboundEpochMs = lastInboundEpochMs;
this.inboundCountInWindow = inboundCountInWindow;
this.avgResponseTimeMs = avgResponseTimeMs;
this.noReplyStrikes = noReplyStrikes;
this.manualSkedFailed = manualSkedFailed;
this.manualSkedFailCount = manualSkedFailCount;
this.lastPositiveSignalEpochMs = lastPositiveSignalEpochMs;
}
}
}
}

View File

@@ -0,0 +1,20 @@
package kst4contest.controller;
import kst4contest.model.ThreadStateMessage;
public interface StatusUpdateListener {
/**
* Thread (key) will send update status (value) to the view via this interface.
*
*/
void onThreadStatusChanged(String key, ThreadStateMessage threadStateMessage);
/**
* Called on change if the userlist to update the UI (sort the chatmembers list)
*/
void onUserListUpdated(String reason);
// new: userlist-update
}

View File

@@ -0,0 +1,8 @@
package kst4contest.controller;
import kst4contest.model.ThreadStateMessage;
public interface ThreadStatusCallback {
void onThreadStatus(String threadName, ThreadStateMessage threadStateMessage);
}

View File

@@ -15,7 +15,10 @@ import kst4contest.model.ChatMember;
public class UCXLogFileToHashsetParser {
public BufferedReader fileReader;
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)";
// private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d{1}]?\\/)?(\\d{1}[a-zA-Z][\\d{1}][a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d{1}]?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)"; //OLD, S51AR for example will not work
private final String PTRN_CallSign = "(([a-zA-Z]{1,2}[\\d]{1}?\\/)?(\\d{1}[a-zA-Z][\\d]{1}[a-zA-Z]{1,3})((\\/p)|(\\/\\d))?)|(([a-zA-Z0-9]{1,2}[\\d]{1}?\\/)?(([a-zA-Z]{1,2}(\\d{1}[a-zA-Z]{1,4})))((\\/p)|(\\/\\d))?)|([A-Z]\\d{2}[A-Z]{1,3})";
public UCXLogFileToHashsetParser(String filePathAndName) {
@@ -37,7 +40,7 @@ public class UCXLogFileToHashsetParser {
*/
private ChatMember checkIfLineInhibitsCallSign(String line) {
Pattern pattern = Pattern.compile(PTRN_CallSign); // TODO: PTRN should depend to category-selection of own stn
Pattern pattern = Pattern.compile(PTRN_CallSign);
Matcher matcher = pattern.matcher(line);
String matchedString = "";

View File

@@ -0,0 +1,237 @@
package kst4contest.controller;
import java.io.InputStream;
import kst4contest.ApplicationConstants;
import kst4contest.model.UpdateInformation;
import kst4contest.utils.ApplicationFileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
public class UpdateChecker {
public static void main(String[] args) {
// new UpdateChecker(null).parseUpdateXMLFile();
if (new UpdateChecker(null).downloadLatestVersionInfoXML()) {
// ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
}
new UpdateChecker(null).parseUpdateXMLFile();
}
public UpdateChecker(ChatController chatController) {
System.out.println("[Updatechecker: checking for updates...]");
// double currentVersionNumber = ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
}
String versionInfoDownloadedFromServerFileName = ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
String versionInfoXMLURLAtServer = ApplicationConstants.VERSIONINFOURLFORUPDATES_KST4CONTEST;
// double currentVersion = ApplicationConstants.APPLICATION_CURRENTVERSIONNUMBER;
//DOWNLOAD from URL, then parse, then do anything with it...
/**
* Downloads the versioninfo-xml-file from a webserver to local. Returns true if download was successful, else false
*
* @return true if successful
*/
public boolean downloadLatestVersionInfoXML() {
try {
InputStream in = new URL(versionInfoXMLURLAtServer).openStream();
Files.copy(in, Paths.get(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/"+ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE)), StandardCopyOption.REPLACE_EXISTING);
in.close();
// System.out.println(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/"+ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE));
// ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME,ApplicationFileUtils.get,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
} catch (Exception e) {
System.out.println("ERROR DOWNLOADING!" + e);
return false;
}
return true;
}
public UpdateInformation parseUpdateXMLFile() {
UpdateInformation updateInfos = new UpdateInformation();
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME,"/"+ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE,ApplicationConstants.VERSIONINFDOWNLOADEDLOCALFILE);
// System.out.println("[Updatecker, Info]: restoring prefs from file " + versionInfoDownloadedFromServerFileName);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
} catch (ParserConfigurationException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
try {
File xmlConfigFile = new File(versionInfoDownloadedFromServerFileName);
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(xmlConfigFile);
/**
* latestVersion on server
*/
NodeList list = doc.getElementsByTagName("latestVersion");
if (list.getLength() != 0) {
for (int temp = 0; temp < list.getLength(); temp++) {
Node node = list.item(temp);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
updateInfos.setLatestVersionNumberOnServer(Double.parseDouble(element.getElementsByTagName("versionNumber").item(0).getTextContent()));
updateInfos.setAdminMessage(element.getElementsByTagName("adminMessage").item(0).getTextContent());
updateInfos.setMajorChanges(element.getElementsByTagName("majorChanges").item(0)
.getTextContent());
updateInfos.setLatestVersionPathOnWebserver(element.getElementsByTagName("latestVersionPathOnWebserver").item(0).getTextContent());
// System.out.println(updateInfos.toString());
}
}
}
/**
* Section changeLog
*/
list = doc.getElementsByTagName("changeLog");
ArrayList<String[]> changeLogArrayList = new ArrayList<String[]>();
if (list.getLength() != 0) {
for (int temp = 0; temp < list.getLength(); temp++) {
Node node = list.item(temp);
Element element = (Element) node;
int childNodeCounter = 0; //need an extra counter due to childnodes are counted...no idea, how
String[] aChangeLogEntry = new String[7];
aChangeLogEntry[0] = "";
aChangeLogEntry[1] = "Date: ";
aChangeLogEntry[2] = "Desc: ";
aChangeLogEntry[3] = "Added: ";
aChangeLogEntry[4] = "Changed: ";
aChangeLogEntry[5] = "Fixed: ";
aChangeLogEntry[6] = "Removed: ";
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
if (element.getChildNodes().item(i).getNodeType() == Node.ELEMENT_NODE) {
// System.out.println(element.getChildNodes().item(i).getTextContent() + " <<<<<<<<<<<<<<<<<< " + i + " / " + childNodeCounter);
// System.out.println(element.getChildNodes().item(i).getNodeName());
aChangeLogEntry[childNodeCounter] = aChangeLogEntry[childNodeCounter] + element.getChildNodes().item(i).getTextContent();
childNodeCounter++;
}
}
changeLogArrayList.add(aChangeLogEntry);
}
updateInfos.setChangeLog(changeLogArrayList);
}
/**
* Section Buglist
*/
list = doc.getElementsByTagName("bug");
ArrayList<String[]> bugFixArrayList = new ArrayList<String[]>();
if (list.getLength() != 0) {
for (int temp = 0; temp < list.getLength(); temp++) {
Node node = list.item(temp);
Element element = (Element) node;
int childNodeCounter = 0; //need an extra counter due to childnodes are counted...no idea, how
String[] aChangeLogEntry = new String[3];
aChangeLogEntry[0] = "";
aChangeLogEntry[1] = "State: ";
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
if (element.getChildNodes().item(i).getNodeType() == Node.ELEMENT_NODE) {
// System.out.println(element.getChildNodes().item(i).getTextContent() + " <<<<<<<<<<<<<<<<<< " + i + " / " + childNodeCounter);
// System.out.println(element.getChildNodes().item(i).getNodeName());
aChangeLogEntry[childNodeCounter] = aChangeLogEntry[childNodeCounter] + element.getChildNodes().item(i).getTextContent();
childNodeCounter++;
}
}
bugFixArrayList.add(aChangeLogEntry);
}
updateInfos.setBugList(bugFixArrayList);
}
} catch (Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
}
String[] testEntry = new String[7];
testEntry[0] = "0.99";
testEntry[1] = "2022-09";
testEntry[2] = "researched the Chatprotocol";
testEntry[3] = "addednothing";
testEntry[4] = "changedsome";
testEntry[5] = "fixedxed";
testEntry[6] = "removedYourMom";
String[] testEntry2 = new String[7];
testEntry2[0] = "0.29";
testEntry2[1] = "2033-09";
testEntry2[2] = "tested";
testEntry2[3] = "addednotashing";
testEntry2[4] = "changeasdsome";
testEntry2[5] = "fixedxeds";
testEntry2[6] = "removedYosssurMom";
// changeLogArrayList.add(testEntry);
// changeLogArrayList.add(testEntry2);
return updateInfos;
}
@Override
public String toString() {
String toString = "";
// toString += this.currentVersion;
return toString;
}
}

View File

@@ -9,6 +9,7 @@ import java.util.TimerTask;
import javafx.collections.ObservableList;
import kst4contest.model.ChatMember;
import kst4contest.model.ClusterMessage;
import kst4contest.view.GuiUtils;
public class UserActualizationTask extends TimerTask {
@@ -50,21 +51,10 @@ public class UserActualizationTask extends TimerTask {
UCXLogFileToHashsetParser getWorkedCallsignsOfUCXLogFile = new UCXLogFileToHashsetParser(
this.client.getChatPreferences().getLogsynch_fileBasedWkdCallInterpreterFileNameReadOnly());
// UCXLogFileToHashsetParser getWorkedCallsignsOfUDPBackupFile = new UCXLogFileToHashsetParser(
// this.client.getChatPreferences().getLogSynch_storeWorkedCallSignsFileNameUDPMessageBackup());
try {
fetchedWorkedSet = getWorkedCallsignsOfUCXLogFile.parse();
// fetchedWorkedSetUdpBckup = getWorkedCallsignsOfUDPBackupFile.parse();
// for (HashMap.Entry entry : fetchedWorkedSet.entrySet()) {
// String key = (String) entry.getKey();
// Object value = entry.getValue();
// System.out.println("key " + key);
// }
System.out.println("USERACT: fetchedWorkedSet size: " + fetchedWorkedSet.size());
// System.out.println("USERACT: fetchedWorkedSetudpbckup size: " + fetchedWorkedSetUdpBckup.size());
} catch (IOException e) {
// TODO Auto-generated catch block
@@ -89,6 +79,7 @@ public class UserActualizationTask extends TimerTask {
// chatMember.setWorked(true);
// System.out.println("[USERACT, info:] marking Chatuser " + chatMember.getCallSign() + " as worked, based on UDPLsnBackup-Logfile.");
// }
// GuiUtils.triggerGUIFilteredChatMemberListChange(this.client); //todo: quick and dirty gui fix
}
ObservableList<ClusterMessage> praktiKSTClusterList = this.client.getLst_clusterMemberList();

View File

@@ -1,5 +1,7 @@
package kst4contest.controller;
import javafx.beans.property.SimpleStringProperty;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.OffsetDateTime;
@@ -7,6 +9,8 @@ import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Utils4KST {
@@ -20,6 +24,14 @@ public class Utils4KST {
return millisecondsSinceEpoch;
}
public String time_generateCurrenthhmmZTimeStringForClusterMessage() {
OffsetDateTime currentTimeInUtc = OffsetDateTime.now(ZoneOffset.UTC);
System.out.println("Utils generated current time " + currentTimeInUtc + " --> " + currentTimeInUtc.format(DateTimeFormatter.ofPattern("HHmm"))+"Z");
return currentTimeInUtc.format(DateTimeFormatter.ofPattern("HHmm"))+"Z";
}
public String time_generateCurrentMMDDhhmmTimeString() {
OffsetDateTime currentTimeInUtc = OffsetDateTime.now(ZoneOffset.UTC);
@@ -40,7 +52,8 @@ public class Utils4KST {
// Instant instant = Instant.ofEpochSecond(epoch);
Date date = new Date(epoch * 1000L);
DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss");
// DateFormat format = new SimpleDateFormat("dd.MM HH:mm:ss"); //old value which is too long
DateFormat format = new SimpleDateFormat("H:mm:ss");
format.setTimeZone(TimeZone.getTimeZone("Etc/UTC"));
String formatted = format.format(date);
@@ -49,6 +62,21 @@ public class Utils4KST {
return formatted;
}
public static long time_getSecondsBetweenEpochAndNow(String epoch1) {
long epoch1Long = Long.parseLong(epoch1);
long epoch2Long = new Utils4KST().time_generateCurrentEpochTime();
// Instant instant = Instant.ofEpochSecond(epoch);
Date date = new Date(epoch1Long * 1000L);
Date date2 = new Date(epoch2Long * 1000L);
long seconds = Math.abs(date.getTime()-date2.getTime())/1000;
return seconds;
}
public Date time_generateActualTimeInDateFormat() {
Date date = new Date(time_generateCurrentEpochTime() * 1000L);
@@ -56,4 +84,119 @@ public class Utils4KST {
}
/**
* This method tests a regexp-pattern against a given string
*
* @param testString: check if this string matches a given pattern
* @param regExPattern: pattern which should be checked
* @return true if match, else false
*/
private static boolean testPattern(String testString, String regExPattern) {
Pattern pattern = Pattern.compile(regExPattern);
Matcher matcher = pattern.matcher(testString);
return matcher.find();
}
/**
* Normalizes a chatmembers frequency-string for cluster usage<br/>
* <b>returns a frequency String in KHz like = "144300" or "144300.0" to match DXC protocol needs</b>
*
* @param optionalPrefix: if there is a value like ".300", it have to be decided, wich ".300": 144.300, 432.300, 1296.300 .... prefix means for example "144."
*/
public static String normalizeFrequencyString(String qrgString, SimpleStringProperty optionalPrefix) {
// final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
try {
qrgString = qrgString.replace(" ","");
} catch (Exception e) {
System.out.println("UTILS: QRG NULL, nothing to convert");
// e.printStackTrace();
}
final String PTRN_QRG_CAT2_wholeQRGMHz4Digits = "(([0-9]{4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)"; //1296.300.3 etc
final String PTRN_QRG_CAT2_wholeQRGMHz3Digits = "(([0-9]{3}[\\.|,| ]?[0-9]{3})([\\.][\\d]{1,2})?)"; //144.300.3 etc
final String PTRN_QRG_CAT2_QRGwithoutPrefix = "((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; //144.300.3 etc
String stringAggregation = "";
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz4Digits)) {//case 1296.200 or 1296.200.2 etc.
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".","");
stringAggregation = stringAggregation.replace(",","");
stringAggregation = stringAggregation.replace(" ", "");
if (stringAggregation.length() == 8) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
stringAggregation = stringAggregationNew + ".0";
return stringAggregation;
} else if (stringAggregation.length() == 9) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
}
} else
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz3Digits)) { //case 144.300 or 144.300.2
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".","");
stringAggregation = stringAggregation.replace(",","");
stringAggregation = stringAggregation.replace(" ", "");
if (stringAggregation.length() == 6) {
stringAggregation = stringAggregation + ".0";
return stringAggregation;
}
if (stringAggregation.length() == 7) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
stringAggregation = stringAggregationNew + ".0";
return stringAggregation;
} else if (stringAggregation.length() == 8) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
}
}
else
if (testPattern(qrgString, PTRN_QRG_CAT2_QRGwithoutPrefix)) { //case ".050 or .300 or something like that"
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".", "");
stringAggregation = stringAggregation.replace(",", "");
stringAggregation = stringAggregation.replace(" ", "");
if (stringAggregation.length() == 3) { // like 050 or 300
String stringAggregationNew = optionalPrefix.getValue() + stringAggregation;
stringAggregation = stringAggregationNew + ".0";
return stringAggregation;
} else if (stringAggregation.length() == 4) { //like 050.2 --> 0502
stringAggregation = optionalPrefix.getValue() + stringAggregation;
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 1) + "." + stringAggregation.substring(stringAggregation.length() - 1, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
} else if (stringAggregation.length() == 5) { //like 050.20 --> 05020
stringAggregation = optionalPrefix.getValue() + stringAggregation;
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 2) + "." + stringAggregation.substring(stringAggregation.length() - 2, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
}
}
return stringAggregation; //if nothing else helps
}
}

View File

@@ -0,0 +1,80 @@
package kst4contest.controller;
import java.nio.charset.StandardCharsets;
/**
* Represents a Win-Test network protocol message.
* <p>
* Ported from the C# wtMessage class in wtKST.
* <p>
* Win-Test uses a simple ASCII-based UDP protocol with a checksum byte.
* Message format (for sending):
* <pre>
* MESSAGETYPE: "src" "dst" data{checksum}\0
* </pre>
* The checksum is calculated over all bytes before the checksum position,
* then OR'd with 0x80.
*/
public class WinTestMessage {
/** Win-Test message types relevant for SKED management. */
public enum MessageType {
LOCKSKED,
UNLOCKSKED,
ADDSKED,
DELETESKED,
UPDATESKED
}
private final MessageType type;
private final String src;
private final String dst;
private final String data;
public WinTestMessage(MessageType type, String src, String dst, String data) {
this.type = type;
this.src = src;
this.dst = dst;
this.data = data;
}
/**
* Serializes this message to bytes for UDP transmission.
* <p>
* Format: {@code MESSAGETYPE: "src" "dst" data{checksum}\0}
* <p>
* The '?' placeholder is replaced by the calculated checksum,
* followed by a NUL terminator.
* Degree signs (°) are escaped as \260 per Win-Test convention.
*/
public byte[] toBytes() {
String escapedData = data.replace("°", "\\260");
// Format: MESSAGETYPE: "src" "dst" data?\0
// The '?' is a placeholder for the checksum byte
String raw = type.name() + ": \"" + src + "\" \"" + dst + "\" " + escapedData + "?\0";
byte[] bytes = raw.getBytes(StandardCharsets.US_ASCII);
// Calculate checksum over everything before the checksum position (length - 2)
int sum = 0;
for (int i = 0; i < bytes.length - 2; i++) {
sum += (bytes[i] & 0xFF);
}
byte checksum = (byte) ((sum | 0x80) & 0xFF);
bytes[bytes.length - 2] = checksum;
return bytes;
}
// Getters for debugging/logging
public MessageType getType() { return type; }
public String getSrc() { return src; }
public String getDst() { return dst; }
public String getData() { return data; }
@Override
public String toString() {
return type.name() + ": src=" + src + " dst=" + dst + " data=" + data;
}
}

View File

@@ -0,0 +1,187 @@
package kst4contest.controller;
import kst4contest.model.Band;
import kst4contest.model.ContestSked;
import kst4contest.model.ThreadStateMessage;
import java.net.*;
import java.nio.charset.StandardCharsets;
/**
* Sends SKED entries to Win-Test via UDP broadcast.
* <p>
* Ported from the C# wtSked class in wtKST.
* <p>
* Win-Test expects a LOCKSKED / ADDSKED / UNLOCKSKED sequence
* to safely insert a new sked into its schedule window.
*/
public class WinTestSkedSender {
private final String stationName;
private final InetAddress broadcastAddress;
private final int port;
private final ThreadStatusCallback callback;
private static final String THREAD_NICKNAME = "WT-SkedSend";
/**
* @param stationName our station name in the Win-Test network (e.g. "KST4Contest")
* @param broadcastAddress UDP broadcast address (e.g. 255.255.255.255 or subnet broadcast)
* @param port Win-Test network port (default 9871)
* @param callback optional callback for status reporting (may be null)
*/
public WinTestSkedSender(String stationName, InetAddress broadcastAddress, int port,
ThreadStatusCallback callback) {
this.stationName = stationName;
this.broadcastAddress = broadcastAddress;
this.port = port;
this.callback = callback;
}
/**
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
* sequence via UDP broadcast.
*
* @param sked the sked to push
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0)
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST")
*/
public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) {
try {
sendLockSked();
sendAddSked(sked, frequencyKHz, notes, modeOverride);
sendUnlockSked();
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false);
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign()
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand());
} catch (Exception e) {
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage());
e.printStackTrace();
}
}
/**
* Sends a LOCKSKED message to lock the Win-Test sked window.
*/
private void sendLockSked() throws Exception {
WinTestMessage msg = new WinTestMessage(
WinTestMessage.MessageType.LOCKSKED,
stationName, "",
"\"" + stationName + "\"");
sendUdp(msg);
}
/**
* Sends an UNLOCKSKED message to unlock the Win-Test sked window.
*/
private void sendUnlockSked() throws Exception {
WinTestMessage msg = new WinTestMessage(
WinTestMessage.MessageType.UNLOCKSKED,
stationName, "",
"\"" + stationName + "\"");
sendUdp(msg);
}
/**
* Sends an ADDSKED message with the sked details.
* <p>
* Win-Test ADDSKED data format (from wtKST):
* <pre>
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
* </pre>
* <p>
* Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch).
* The C# code adds 60 seconds to compensate.
*/
private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception {
// Win-Test timestamp: epoch seconds with 60s offset
long epochSeconds = sked.getSkedTimeEpoch() / 1000;
long wtTimestamp = epochSeconds + 60;
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
// Win-Test band ID
int bandId = toWinTestBandId(sked.getBand());
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
int mode;
if (modeOverride >= 0) {
mode = modeOverride;
} else {
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
}
String data = wtTimestamp
+ " " + freqTenthKHz
+ " " + bandId
+ " " + mode
+ " \"" + sked.getTargetCallsign() + "\""
+ " \"" + (notes != null ? notes : "") + "\"";
WinTestMessage msg = new WinTestMessage(
WinTestMessage.MessageType.ADDSKED,
stationName, "",
data);
sendUdp(msg);
}
/**
* Sends a WinTestMessage via UDP broadcast.
*/
private void sendUdp(WinTestMessage msg) throws Exception {
try (DatagramSocket socket = new DatagramSocket()) {
socket.setBroadcast(true);
socket.setReuseAddress(true);
byte[] bytes = msg.toBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcastAddress, port);
socket.send(packet);
System.out.println("[WinTestSkedSender] sent: " + msg);
}
}
/**
* Maps the kst4contest Band enum to Win-Test band IDs.
* <p>
* Win-Test band IDs (reverse-engineered from wtKST):
* 10=50MHz, 11=70MHz, 12=144MHz, 14=432MHz, 16=1.2GHz,
* 17=2.3GHz, 18=3.4GHz, 19=5.7GHz, 20=10GHz, 21=24GHz,
* 22=47GHz, 23=76GHz
*/
public static int toWinTestBandId(Band band) {
if (band == null) return 12; // default to 144 MHz
return switch (band) {
case B_144 -> 12;
case B_432 -> 14;
case B_1296 -> 16;
case B_2320 -> 17;
case B_3400 -> 18;
case B_5760 -> 19;
case B_10G -> 20;
case B_24G -> 21;
};
}
/**
* Very simple SSB segment heuristic.
* A more complete implementation would check actual mode from Win-Test STATUS.
*/
private boolean isInSsbSegment(double frequencyKHz) {
// SSB segments (kHz ranges)
if (frequencyKHz >= 144300 && frequencyKHz <= 144399) return true; // 2m SSB
if (frequencyKHz >= 432200 && frequencyKHz <= 432399) return true; // 70cm SSB
if (frequencyKHz >= 1296200 && frequencyKHz <= 1296399) return true; // 23cm SSB
return false;
}
private void reportStatus(String text, boolean isError) {
if (callback != null) {
callback.onThreadStatus(THREAD_NICKNAME,
new ThreadStateMessage(THREAD_NICKNAME, !isError, text, isError));
}
}
}

View File

@@ -2,7 +2,10 @@ package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import kst4contest.ApplicationConstants;
import kst4contest.model.ChatMessage;
/**
@@ -17,7 +20,7 @@ public class WriteThread extends Thread {
private ChatController client;
private OutputStream output;
private ChatMessage messageTextRaw;
private ChatMessage messageToBeSend;
public WriteThread(Socket socket, ChatController client) throws InterruptedException {
this.socket = socket;
@@ -25,7 +28,9 @@ public class WriteThread extends Thread {
try {
output = socket.getOutputStream();
writer = new PrintWriter(output, true);
writer = new PrintWriter(output, true, StandardCharsets.UTF_8);
} catch (IOException ex) {
System.out.println("Error getting output stream: " + ex.getMessage());
ex.printStackTrace();
@@ -34,7 +39,8 @@ public class WriteThread extends Thread {
/**
* This method is used to send a message to the server, raw formatted. E.g. for
* the keepalive message.
* the keepalive message. This method sends only in the main message-Category. To send it in a category
* "defined by Chatmessage", use txByRxmsgCatOrigin(Chatmessage "toBeSend")
*
* @param messageToServer
* @throws InterruptedException
@@ -48,6 +54,33 @@ public class WriteThread extends Thread {
}
/**
* This method is used to send a message directly to a receiver in a special chatcategory. The receivers category
* will be read out of the Chatmessage.getChatCategory method. <b> The message text will be modified to fit kst
* messageformat</b>
*
* @param messageToServer
* @throws InterruptedException
*/
public void txByRxmsgCatOrigin(ChatMessage messageToServer) throws InterruptedException {
// writer.println(messageToServer.getMessage()); //kst4contest.test 4 23001
// writer.flush(); //kst4contest.test 4 23001
String originalMessageText = messageToServer.getMessageText() + "";
String newMessageText = "";
newMessageText = ("MSG|" + messageToServer.getChatCategory().getCategoryNumber()
+ "|0|" + originalMessageText + "|0|"); //original before 1.26
System.out.println(newMessageText + "< sended to the writer (DIRECTED REPLY)");
writer.println(newMessageText);
}
/**
* This method gets a textmessage to the chat and adds some characters to hit
* the neccessarry format to send a message in the on4kst chat either to another
@@ -59,29 +92,28 @@ public class WriteThread extends Thread {
public void txKSTFormatted(ChatMessage messageToServer) throws InterruptedException {
// writer.println(messageToServer.getMessageText());
messageTextRaw = messageToServer;
messageToBeSend = messageToServer;
try {
messageTextRaw = client.getMessageTXBus().take();
messageToBeSend = client.getMessageTXBus().take();
// this.client.getmesetChatsetServerready(true);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
String messageLine = messageTextRaw.getMessageText();
String messageLine = messageToBeSend.getMessageText();
if (messageTextRaw.isMessageDirectedToServer()) {
if (messageToBeSend.isMessageDirectedToServer()) {
/**
* We have to check if we only commands the server (keepalive) or want do talk
* to the community
*/
try {
tx(messageTextRaw);
System.out.println("BUS: tx: " + messageTextRaw.getMessageText());
tx(messageToBeSend);
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
@@ -95,8 +127,8 @@ public class WriteThread extends Thread {
// ownMSG.setMessageText(
// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
ownMSG.setMessageText("MSG|" + this.client.getChatPreferences().getLoginChatCategory().getCategoryNumber()
+ "|0|" + messageLine + "|0|");
ownMSG.setMessageText("MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber()
+ "|0|" + messageLine + "|0|"); //original before 1.26
try {
tx(ownMSG);
@@ -108,7 +140,7 @@ public class WriteThread extends Thread {
}
}
if (messageTextRaw.equals("/QUIT")) {
if (messageToBeSend.equals("/QUIT")) {
try {
this.client.getReadThread().terminateConnection();
this.client.getReadThread().interrupt();
@@ -117,7 +149,6 @@ public class WriteThread extends Thread {
this.interrupt();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@@ -137,59 +168,62 @@ public class WriteThread extends Thread {
while (true) {
try {
messageTextRaw = client.getMessageTXBus().take();
messageToBeSend = client.getMessageTXBus().take();
if (messageTextRaw.getMessageText().equals("POISONPILL_KILLTHREAD")
&& messageTextRaw.getMessageSenderName().equals("POISONPILL_KILLTHREAD")) {
if (messageToBeSend.getMessageText().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)
&& messageToBeSend.getMessageSenderName().equals(ApplicationConstants.DISCONNECT_RDR_POISONPILL)) {
client.getMessageRXBus().clear();
this.interrupt();
break;
} else {
String messageLine = messageTextRaw.getMessageText();
String messageLine = messageToBeSend.getMessageText();
if (messageTextRaw.isMessageDirectedToServer()) {
if (messageToBeSend.isMessageDirectedToServer()) {
/**
* We have to check if we only commands the server (keepalive) or want do talk
* to the community
*/
try {
tx(messageTextRaw);
System.out.println("BUS: tx: " + messageTextRaw.getMessageText());
tx(messageToBeSend);
System.out.println("BUS: tx: " + messageToBeSend.getMessageText());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
} else { //message is not directed to the server, it´s directed to all or to a station
ChatMessage ownMSG = new ChatMessage();
if (messageToBeSend.getChatCategory() == this.client.getChatCategoryMain() || messageToBeSend.getChatCategory() == this.client.getChatCategorySecondChat()) {
// ownMSG.setMessageText(
// "MSG|" + this.client.getCategory().getCategoryNumber() + "|0|" + messageLine + "|0|");
txByRxmsgCatOrigin(messageToBeSend);
ownMSG.setMessageText(
"MSG|" + this.client.getChatPreferences().getLoginChatCategory().getCategoryNumber() + "|0|"
+ messageLine + "|0|");
} else { //default bhv if destination cat is not detectable
try {
tx(ownMSG);
System.out.println("BUS: tx: " + ownMSG.getMessageText());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
ChatMessage ownMSG = new ChatMessage();
ownMSG.setMessageText(
"MSG|" + this.client.getChatPreferences().getLoginChatCategoryMain().getCategoryNumber() + "|0|"
+ messageLine + "|0|");
try {
tx(ownMSG);
System.out.println("WT: tx (raw): " + ownMSG.getMessageText());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
System.out.println("WritheTh: got message out of the queue: " + messageTextRaw.getMessageText());
System.out.println("WritheTh: got message out of the queue: " + messageToBeSend.getMessageText());
// this.client.getmesetChatsetServerready(true);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
client.getMessageTXBus().clear();
}

View File

@@ -0,0 +1,13 @@
package kst4contest.controller.interfaces;
public interface PstRotatorEventListener {
void onAzimuthUpdate(double azimuth);
void onElevationUpdate(double elevation);
void onModeUpdate(boolean isTracking); // true = Tracking, false = Manual
void onMessageReceived(String rawMessage); // Debugging usage
// void setRotorPosition(double azimuth);
}

View File

@@ -0,0 +1,110 @@
package kst4contest.locatorUtils;
public class DirectionUtils {
/**
* Checks wheter a sked-sender writes to a sked-receiver and is in my direction due he beams to this receiver
*
* @param myLocator
* @param locatorOfSkedSender
* @param locatorOfSekdReceiver
* @param maxRangeKm
* @param hisAntennaBeamWidth
* @return
*/
public static boolean isInAngleAndRange(String myLocator, String locatorOfSkedSender, String locatorOfSekdReceiver, double maxRangeKm, double hisAntennaBeamWidth) {
Location myLocation = new Location(myLocator);
Location skedSenderLocation = new Location(locatorOfSkedSender);
Location skedReceiverLocation = new Location(locatorOfSekdReceiver);
double distanceFromMeToLocSender = new Location(myLocator).getDistanceKm(new Location(locatorOfSkedSender));
// Check if distance exceeds my setted maximum range
if (distanceFromMeToLocSender > maxRangeKm) {
System.out.println("too far, " + distanceFromMeToLocSender + " km");
return false;
}
//check bearing of sender to receiver
double bearingOfSekdSenderToSkedReceiver = skedSenderLocation.getBearing(skedReceiverLocation);
// System.out.println("skedTX -> skedTX deg: " + bearingOfSekdSenderToSkedReceiver);
double bearingOfSekdSenderToMe = skedSenderLocation.getBearing(myLocation);
// System.out.println("skedTX -> me deg: " + bearingOfSekdSenderToMe);
/**
* simple mech works
*/
// if (bearingOfSekdSenderToMe >= bearingOfSekdSenderToSkedReceiver) {
// if (bearingOfSekdSenderToMe-bearingOfSekdSenderToSkedReceiver <= hisAntennaBeamWidth/2){
// System.out.println(bearingOfSekdSenderToMe-bearingOfSekdSenderToSkedReceiver + " <= " + hisAntennaBeamWidth);
// return true;
// }
// } else if ((bearingOfSekdSenderToMe <= bearingOfSekdSenderToSkedReceiver)) {
// if (bearingOfSekdSenderToSkedReceiver-bearingOfSekdSenderToMe <= hisAntennaBeamWidth/2){
// return true;
// }
// } else return false;
/**
* simple mech end
*/
if (DirectionUtils.isAngleInRange(bearingOfSekdSenderToSkedReceiver, bearingOfSekdSenderToMe, hisAntennaBeamWidth)) {
//I may should get "/2" because of 50% of the 3dB opening angle if txer is directed to sender exactly
// System.out.println("------------> isinangleandrange!");
return true;
} else {
// System.out.println("not in angle and reach");
return false;
}
}
/**
* Tests, if the angle (from me to) other station is in the range of the
* angle (qtf) in degrees where my antenna points to.
*
* @param toForeignAngle [degrees]
* @param mySelectedQTFAngle [degrees]
* @param antennaBeamwidth [degrees]
* @return
*/
public static boolean isAngleInRange(double toForeignAngle,
double mySelectedQTFAngle, double antennaBeamwidth) {
double beamwidth = antennaBeamwidth / 2; // half left, half right
double startAngle = mySelectedQTFAngle - beamwidth;
double endAngle = mySelectedQTFAngle + beamwidth;
// Normalize angles to be between 0 and 360 degrees
toForeignAngle = normalizeAngle(toForeignAngle);
startAngle = normalizeAngle(startAngle);
endAngle = normalizeAngle(endAngle);
// Check if the range wraps around 360 degrees
if (startAngle <= endAngle) {
return toForeignAngle >= startAngle && toForeignAngle <= endAngle;
} else {
// Range wraps around 360 degrees, so check if angle is within the
// range or outside the range
return toForeignAngle >= startAngle || toForeignAngle <= endAngle;
}
}
private static double normalizeAngle(double angle) {
if (angle < 0) {
angle += 360;
}
if (angle >= 360) {
angle -= 360;
}
return angle;
}
}

View File

@@ -1,5 +1,10 @@
package kst4contest.locatorUtils;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;
/**
* Location class with methods allowing conversion to and from Maidenhead
* locator (grid squares) based off of
@@ -203,6 +208,27 @@ public class Location {
return getDistanceKm(this, loc2);
}
/**
* @param locator1 6 letter location string
* @param locator2 6 letter location string
* @return great circle distance in kilometers
*/
public double getDistanceKmByTwoLocatorStrings(String locator1,String locator2 ) {
Location loc1 = new Location(locator1);
Location loc2 = new Location(locator2);
Locale locale = new Locale("en", "UK");
String pattern = "###.##";
DecimalFormat decimalFormat = (DecimalFormat)
NumberFormat.getNumberInstance(locale);
decimalFormat.applyPattern(pattern);
String format = decimalFormat.format(loc1.getDistanceKm(loc2));
return Double.parseDouble(format);
}
/**
* @param loc2
* second location
@@ -278,6 +304,19 @@ public class Location {
return getBearing(this, loc2);
}
/**
*
* @return bearing in degrees
*/
public double getBearingOfTwoLocatorStrings(String locator1, String locator2) {
Location loc1 = new Location(locator1);
Location loc2 = new Location(locator2);
return getBearing(loc1, loc2);
}
/**
* @param loc1
* source location
@@ -300,6 +339,21 @@ public class Location {
- Math.sin(loc1.getLatitude().getRadians())
* Math.cos(loc2.getLatitude().getRadians())
* Math.cos(dLon);
return (Angle.radiansToDegrees(Math.atan2(y, x)) + 360) % 360;
double bearing = (Angle.radiansToDegrees(Math.atan2(y, x)) + 360) % 360;
// return bearing;
Locale locale = new Locale("en", "UK");
String pattern = "###.##";
DecimalFormat decimalFormat = (DecimalFormat)
NumberFormat.getNumberInstance(locale);
decimalFormat.applyPattern(pattern);
String format = decimalFormat.format(bearing);
return Double.parseDouble(format);
}
}

View File

@@ -0,0 +1,43 @@
package kst4contest.locatorUtils;
public class TestLocatorUtils {
public static void main(String[] args) {
// isInAngle(myLocation, location1, location2);
System.out.println(isInAngleAndRange("JN49FL", "jo43xm", "jo30sa", 900, 50));
System.out.println(isInAngleAndRange("JN49FL", "jo51ij", "jn39oc", 900, 50));
System.out.println(isInAngleAndRange("JN49FL", "jn39oc", "jo51ij", 1100, 50));
}
public static boolean isInAngleAndRange(String myLocator, String locatorOfSkedSender, String locatorOfSekdReceiver, double maxRangeKm, double hisAntennaBeamWidth) {
Location myLocation = new Location(myLocator);
Location skedSenderLocation = new Location(locatorOfSkedSender);
Location skedReceiverLocation = new Location(locatorOfSekdReceiver);
double distanceFromMeToLocSender = new Location(myLocator).getDistanceKm(new Location(locatorOfSkedSender));
// Check if distance exceeds my setted maximum range
if (distanceFromMeToLocSender > maxRangeKm) {
System.out.println("too far, " + distanceFromMeToLocSender + " km");
return false;
}
//check bearing of sender to receiver
double bearingOfSekdSenderToSkedReceiver = skedSenderLocation.getBearing(skedReceiverLocation);
System.out.println("skedTX -> skedRX deg: " + bearingOfSekdSenderToSkedReceiver);
double bearingOfSekdSenderToMe = skedSenderLocation.getBearing(myLocation);
System.out.println("skedTX -> me deg: " + bearingOfSekdSenderToMe);
if (DirectionUtils.isAngleInRange(bearingOfSekdSenderToSkedReceiver, bearingOfSekdSenderToMe, hisAntennaBeamWidth)) {
//may I should get "/2" because of 50% of the 3dB opening angle if txer is directed to sender exactly
return true;
} else return false;
}
}

View File

@@ -0,0 +1,292 @@
package kst4contest.logic;
import kst4contest.controller.StationMetricsService;
import kst4contest.model.*;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
/**
* Priority score calculation (off FX-thread).
*
* Notes:
* - Score is computed once per callsignRaw by ScoreService.
* - This calculator MUST be pure (no UI calls) and fast.
*/
public class PriorityCalculator {
/** Max age for "known active bands" (derived from chat history). */
private static final long RX_BANDS_MAX_AGE_MS = 30L * 60L * 1000L; // 30 minutes
public double calculatePriority(ChatMember member,
ChatPreferences prefs,
List<ContestSked> activeSkeds,
StationMetricsService.Snapshot metricsSnapshot,
long nowEpochMs) {
if (member == null || prefs == null) return 0.0;
final String callRaw = normalize(member.getCallSignRaw());
if (callRaw == null || callRaw.isBlank()) return 0.0;
// --------------------------------------------------------------------
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
// --------------------------------------------------------------------
// --------------------------------------------------------------------
// 1) HARD FILTER: reachable hardware + "already worked on all possible bands"
// --------------------------------------------------------------------
EnumSet<Band> myEnabledBands = getMyEnabledBands(prefs);
// "worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
// IMPORTANT: ChatMember.worked is UI-only and NOT used in scoring.
EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
// Remaining bands that are:
// - recently offered by the station (from knownActiveBands history)
// - enabled at our station
// - NOT worked yet (per-band flags)
// If we do not know offered bands (history empty), this remains empty.
EnumSet<Band> unworkedPossible = EnumSet.noneOf(Band.class);
EnumSet<Band> stationOfferedBands = getStationOfferedBandsFromHistory(member, nowEpochMs);
EnumSet<Band> possibleBands = stationOfferedBands.isEmpty()
? EnumSet.noneOf(Band.class) // unknown => don't hard-filter
: EnumSet.copyOf(stationOfferedBands);
if (!possibleBands.isEmpty()) {
possibleBands.retainAll(myEnabledBands);
if (possibleBands.isEmpty()) {
// We know their bands, but none of them are enabled at our station.
return 0.0;
}
unworkedPossible = EnumSet.copyOf(possibleBands);
unworkedPossible.removeAll(workedBandsForScoring);
// If already worked on all possible bands => no priority on them anymore (contest logic).
if (unworkedPossible.isEmpty()) {
return 0.0;
}
}
// --------------------------------------------------------------------
// 2) BASE SCORE
// --------------------------------------------------------------------
double score = 100.0;
// if (!member.isWorked()) {
// score += 200.0;
// }
//"worked" for scoring is derived ONLY from per-band flags (worked144/432/...)
// EnumSet<Band> workedBandsForScoring = getWorkedBands(member);
if (workedBandsForScoring.isEmpty()) {
score += 200.0; // never worked on any supported band -> higher priority
} else {
score -= 150.0; // already worked on at least one band -> lower base priority
}
// Multi-band bonus: if they offer >1 possible band and we worked at least one, prefer them
if (!possibleBands.isEmpty()) {
int bandCount = possibleBands.size();
score += (bandCount - 1) * 80.0;
}
// Optional: band-upgrade visibility boost
// If the station is already worked on at least one band, but is still QRV on other unworked enabled band(s),
// we can optionally add a boost so it remains visible in the list.
if (prefs.isNotify_bandUpgradePriorityBoostEnabled()
&& !workedBandsForScoring.isEmpty()
&& !unworkedPossible.isEmpty()) {
score += 180.0; // tuned visibility boost
}
// --------------------------------------------------------------------
// 3) DISTANCE ("Goldilocks Zone")
// --------------------------------------------------------------------
double distKm = member.getQrb() == null ? 0.0 : member.getQrb();
if (distKm > 0) {
if (distKm < 200) {
score *= 0.7;
} else if (distKm > prefs.getStn_maxQRBDefault()) {
score *= 0.3;
} else {
score *= 1.15;
}
}
// --------------------------------------------------------------------
// 4) AIRSCOUT BOOST
// --------------------------------------------------------------------
AirPlaneReflectionInfo apInfo = member.getAirPlaneReflectInfo();
if (apInfo != null && apInfo.getAirPlanesReachableCntr() > 0) {
score += 200;
int nextMinutes = findNextAirplaneArrivingMinutes(apInfo);
if (nextMinutes == 0) score += 120;
else if (nextMinutes == 1) score += 60;
else if (nextMinutes == 2) score += 30;
}
// --------------------------------------------------------------------
// 5) BOOST IDEA #1: Beam direction match (within beamwidth)
// --------------------------------------------------------------------
if (member.getQTFdirection() != null) {
double myAz = prefs.getActualQTF().getValue();
double targetAz = member.getQTFdirection();
double diff = minimalAngleDiffDeg(myAz, targetAz);
double halfBeam = Math.max(1.0, prefs.getStn_antennaBeamWidthDeg()) / 2.0;
if (diff <= halfBeam) {
double centerFactor = 1.0 - (diff / halfBeam); // 1.0 center -> 0.0 edge
score += 80.0 + (120.0 * centerFactor);
}
}
// --------------------------------------------------------------------
// 6) BOOST IDEA #3: Conversation momentum (recent inbound burst)
// --------------------------------------------------------------------
if (metricsSnapshot != null) {
StationMetricsService.Snapshot.Metrics mx = metricsSnapshot.get(callRaw);
if (mx != null) {
long ageMs = mx.lastInboundEpochMs > 0 ? (nowEpochMs - mx.lastInboundEpochMs) : Long.MAX_VALUE;
// "Active now" bonus
if (ageMs < 60_000) score += 120;
else if (ageMs < 3 * 60_000) score += 60;
// Momentum bonus: multiple lines in the configured window
int cnt = mx.inboundCountInWindow;
if (cnt >= 6) score += 160;
else if (cnt >= 4) score += 110;
else if (cnt >= 2) score += 60;
// Positive signal (configurable)
if (mx.lastPositiveSignalEpochMs > 0 && (nowEpochMs - mx.lastPositiveSignalEpochMs) < 5 * 60_000) {
score += 120;
}
// Reply time: prefer fast responders
if (mx.avgResponseTimeMs > 0) {
if (mx.avgResponseTimeMs < 60_000) score += 80;
else if (mx.avgResponseTimeMs < 3 * 60_000) score += 40;
}
// No-reply penalty (automatic failed attempt)
if (mx.noReplyStrikes > 0) {
score /= (1.0 + (mx.noReplyStrikes * 0.6));
}
// Manual sked fail (path likely bad) => strong, permanent penalty until reset
if (mx.manualSkedFailed) {
score *= 0.15;
}
}
}
// --------------------------------------------------------------------
// 7) BOOST IDEA #4: Sked commitment ramp-up
// --------------------------------------------------------------------
if (activeSkeds != null && !activeSkeds.isEmpty()) {
for (ContestSked sked : activeSkeds) {
if (sked == null) continue;
if (!callRaw.equals(normalize(sked.getTargetCallsign()))) continue;
long seconds = sked.getTimeUntilSkedSeconds();
// Imminent sked: absolute priority (T-3min..T+1min)
if (seconds < 180 && seconds > -60) {
score += 5000;
continue;
}
// Ramp: 0..15 minutes before => up to +1200
if (seconds >= 0 && seconds <= 15 * 60) {
double t = (15 * 60 - seconds) / (15.0 * 60.0); // 0.0..1.0
score += 300 + (900 * t);
} else if (seconds > 15 * 60) {
score += 40;
}
}
}
// --------------------------------------------------------------------
// 8) Legacy penalty: failed attempts in ChatMember
// --------------------------------------------------------------------
if (member.getFailedQSOAttempts() > 0) {
score = score / (member.getFailedQSOAttempts() + 1);
}
return Math.max(0.0, score);
}
private static EnumSet<Band> getMyEnabledBands(ChatPreferences prefs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (prefs.isStn_bandActive144()) out.add(Band.B_144);
if (prefs.isStn_bandActive432()) out.add(Band.B_432);
if (prefs.isStn_bandActive1240()) out.add(Band.B_1296);
if (prefs.isStn_bandActive2300()) out.add(Band.B_2320);
if (prefs.isStn_bandActive3400()) out.add(Band.B_3400);
if (prefs.isStn_bandActive5600()) out.add(Band.B_5760);
if (prefs.isStn_bandActive10G()) out.add(Band.B_10G);
return out;
}
private static EnumSet<Band> getStationOfferedBandsFromHistory(ChatMember member, long nowEpochMs) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
Map<Band, ChatMember.ActiveFrequencyInfo> map = member.getKnownActiveBands();
if (map == null || map.isEmpty()) return out;
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> e : map.entrySet()) {
if (e == null || e.getKey() == null || e.getValue() == null) continue;
long age = nowEpochMs - e.getValue().timestampEpoch;
if (age <= RX_BANDS_MAX_AGE_MS) {
out.add(e.getKey());
}
}
return out;
}
private static EnumSet<Band> getWorkedBands(ChatMember member) {
EnumSet<Band> out = EnumSet.noneOf(Band.class);
if (member.isWorked144()) out.add(Band.B_144);
if (member.isWorked432()) out.add(Band.B_432);
if (member.isWorked1240()) out.add(Band.B_1296);
if (member.isWorked2300()) out.add(Band.B_2320);
if (member.isWorked3400()) out.add(Band.B_3400);
if (member.isWorked5600()) out.add(Band.B_5760);
if (member.isWorked10G()) out.add(Band.B_10G);
if (member.isWorked24G()) out.add(Band.B_24G);
return out;
}
private static int findNextAirplaneArrivingMinutes(AirPlaneReflectionInfo apInfo) {
try {
if (apInfo.getRisingAirplanes() == null || apInfo.getRisingAirplanes().isEmpty()) return -1;
int min = Integer.MAX_VALUE;
for (AirPlane ap : apInfo.getRisingAirplanes()) {
if (ap == null) continue;
min = Math.min(min, ap.getArrivingDurationMinutes());
}
return min == Integer.MAX_VALUE ? -1 : min;
} catch (Exception ignore) {
return -1;
}
}
private static double minimalAngleDiffDeg(double a, double b) {
double diff = Math.abs((a - b) % 360.0);
return diff > 180.0 ? 360.0 - diff : diff;
}
private static String normalize(String s) {
if (s == null) return null;
return s.trim().toUpperCase();
}
}

View File

@@ -0,0 +1,51 @@
package kst4contest.logic;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
/**
* Lightweight positive-signal detector.
*
* Patterns are configured via a single preference string, delimited by ';' or newlines.
* Examples: "QRV;READY;RX OK;RGR;TNX;TU;HRD"
*/
public final class SignalDetector {
private static final AtomicReference<String> lastPatterns = new AtomicReference<>("");
private static final AtomicReference<List<Pattern>> cached = new AtomicReference<>(List.of());
private SignalDetector() {}
public static boolean containsPositiveSignal(String messageText, String patternsDelimited) {
if (messageText == null || messageText.isBlank()) return false;
List<Pattern> patterns = compileIfChanged(patternsDelimited);
String txt = messageText.toUpperCase();
for (Pattern p : patterns) {
if (p.matcher(txt).find()) return true;
}
return false;
}
private static List<Pattern> compileIfChanged(String patternsDelimited) {
String p = patternsDelimited == null ? "" : patternsDelimited.trim();
String prev = lastPatterns.get();
if (p.equals(prev)) return cached.get();
List<Pattern> out = new ArrayList<>();
for (String token : p.split("[;\\n\\r]+")) {
String t = token.trim();
if (t.isEmpty()) continue;
// plain substring match, but regex-safe
String regex = Pattern.quote(t.toUpperCase());
out.add(Pattern.compile(regex));
}
lastPatterns.set(p);
cached.set(List.copyOf(out));
return out;
}
}

View File

@@ -3,6 +3,25 @@ package kst4contest.model;
public class AirPlane {
String apCallSign, apSizeCategory;
String potencialDescriptionAsWord;
public String getPotencialDescriptionAsWord() {
if (this.getPotential() <=50) {
return "small AP";
} else if (this.getPotential() <=75 && this.getPotential() > 50) {
return "big AP";
} else if (this.getPotential() > 75) {
return "very big AP";
}
return potencialDescriptionAsWord;
}
public void setPotencialDescriptionAsWord(String potencialDescriptionAsWord) {
this.potencialDescriptionAsWord = potencialDescriptionAsWord;
}
int distanceKm, potential, arrivingDurationMinutes;
public String getApCallSign() {
return apCallSign;

View File

@@ -0,0 +1,49 @@
package kst4contest.model;
/**
* Represents Amateur Radio Bands and their physical limits.
* Used for plausibility checks in the Smart Parser.
*/
public enum Band {
B_144(144.000, 146.000, "144"),
B_432(432.000, 434.000, "432"),
B_1296(1296.000, 1298.000, "1296"),
B_2320(2320.000, 2322.000, "2320"),
B_3400(3400.000, 3410.000, "3400"),
B_5760(5760.000, 5762.000, "5760"),
B_10G(10368.000, 10370.000, "10368"),
B_24G(24048.000, 24050.000, "24048");
// more space for future usage
private final double minFreq;
private final double maxFreq;
private final String prefix; // Default prefix for "short value" parsing (e.g., .210)
Band(double min, double max, String prefix) {
this.minFreq = min;
this.maxFreq = max;
this.prefix = prefix;
}
public String getPrefix() {
return prefix;
}
/**
* Checks if a specific frequency falls within this band's limits.
*/
public boolean isPlausible(double freq) {
return freq >= minFreq && freq <= maxFreq;
}
/**
* Helper to find the matching Band enum for a given frequency.
* Returns null if no band matches.
*/
public static Band fromFrequency(double freq) {
for (Band b : values()) {
if (b.isPlausible(freq)) return b;
}
return null;
}
}

View File

@@ -53,28 +53,17 @@ public class ChatCategory {
public ChatCategory(int setThiscategoryNumber) {
this.categoryNumber = setThiscategoryNumber;
setCategoryNumber(setThiscategoryNumber);
}
public int getCategoryNumber() {
return categoryNumber;
}
public void setCategoryNumber(int categoryNumber) {
this.categoryNumber = categoryNumber;
}
/**
* Returns an Array of int with possible frequency prefixes, due to in the chat
* normally the following format is used (not ever): <br/>

View File

@@ -1,31 +1,47 @@
package kst4contest.model;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class ChatMember {
long lastFlagsChangeEpochMs; // timestamp of the last worked/not-QRV flag change in the internal DB
// private final BooleanProperty workedInfoChangeFireListEventTrigger = new SimpleBooleanProperty();
AirPlaneReflectionInfo airPlaneReflectInfo;
String callSign;
String qra;
String name;
String callSignRaw; //without -2 or -70 etc.
boolean isInAngleAndRange; //if he tries a sked in my dir, he is in range, will process that in the messages
// String frequency; // last known qrg of the station
StringProperty frequency = new SimpleStringProperty();
String password; // only used by own instance of the chatmember instance to login to the chat
ChatCategory chatCategory; // only used by own instance of the chatmember instance to login to the chat
ChatCategory chatCategory; //Source category
// ChatCategory chatCategory;//only used by own instance of the chatmember instance to login to the chat
long activityCounter; // time of last activity in epochtimesec
long activityTimeLastInEpoch; // time of last activity in epochtimesec
Date lastActivity; // time of last activity in epochtimesec
Date lastActualizationTimeOfThisMember; // time of last state change if that member
int qrb;
Double qrb;
int state;
int QTFdirection; // antenna direction in deg
Double QTFdirection; // antenna direction in deg
int[] workedCategories; // Chatcategory where the station is in the log, see kst4contest.model.ChatCategory
boolean worked; // true if the callsign is logged already - for temporary worked processing
@@ -36,6 +52,62 @@ public class ChatMember {
boolean worked3400;
boolean worked5600;
boolean worked10G;
boolean Worked50;
boolean Worked70;
boolean Worked24G;
boolean Worked47G;
boolean Worked76G;
/**
* Chatmember is qrv at all band except we initialize anything other, depending to user entry
*/
boolean qrv144 = true;
boolean qrv432 = true;
boolean qrv1240 = true;
boolean qrv2300 = true;
boolean qrv3400 = true;
boolean qrv5600 = true;
boolean qrv10G = true;
boolean qrvAny = true;
// Stores the last known frequency per band (Context History)
private final Map<Band, ActiveFrequencyInfo> knownActiveBands = new ConcurrentHashMap<>();
// --- INNER CLASS FOR QRG HISTORY ---
public class ActiveFrequencyInfo {
public double frequency;
public long timestampEpoch;
public ActiveFrequencyInfo(double freq) {
this.frequency = freq;
this.timestampEpoch = System.currentTimeMillis();
}
}
// Counter for failed calls (Penalty Logic)
private int failedQSOAttempts = 0;
// Calculated Score for sorting the user list
private double currentPriorityScore = 0.0;
public long getLastFlagsChangeEpochMs() {
return lastFlagsChangeEpochMs;
}
public void setLastFlagsChangeEpochMs(long lastFlagsChangeEpochMs) {
this.lastFlagsChangeEpochMs = lastFlagsChangeEpochMs;
}
public boolean isInAngleAndRange() {
return isInAngleAndRange;
}
public void setInAngleAndRange(boolean inAngleAndRange) {
isInAngleAndRange = inAngleAndRange;
}
public AirPlaneReflectionInfo getAirPlaneReflectInfo() {
return airPlaneReflectInfo;
@@ -117,6 +189,70 @@ public class ChatMember {
worked10G = worked10g;
}
public boolean isQrv144() {
return qrv144;
}
public void setQrv144(boolean qrv144) {
this.qrv144 = qrv144;
}
public boolean isQrv432() {
return qrv432;
}
public void setQrv432(boolean qrv432) {
this.qrv432 = qrv432;
}
public boolean isQrv1240() {
return qrv1240;
}
public void setQrv1240(boolean qrv1240) {
this.qrv1240 = qrv1240;
}
public boolean isQrv2300() {
return qrv2300;
}
public void setQrv2300(boolean qrv2300) {
this.qrv2300 = qrv2300;
}
public boolean isQrv3400() {
return qrv3400;
}
public void setQrv3400(boolean qrv3400) {
this.qrv3400 = qrv3400;
}
public boolean isQrv5600() {
return qrv5600;
}
public void setQrv5600(boolean qrv5600) {
this.qrv5600 = qrv5600;
}
public boolean isQrv10G() {
return qrv10G;
}
public void setQrv10G(boolean qrv10G) {
this.qrv10G = qrv10G;
}
public boolean isQrvAny() {
return qrvAny;
}
public void setQrvAny(boolean qrvAny) {
this.qrvAny = qrvAny;
}
public int[] getWorkedCategories() {
return workedCategories;
}
@@ -125,8 +261,8 @@ public class ChatMember {
this.workedCategories = workedCategories;
}
public void setActivityCounter(long activityCounter) {
this.activityCounter = activityCounter;
public void setActivityTimeLastInEpoch(long activityTimeLastInEpoch) {
this.activityTimeLastInEpoch = activityTimeLastInEpoch;
}
public int getState() {
@@ -153,34 +289,149 @@ public class ChatMember {
this.lastActualizationTimeOfThisMember = lastActualizationTimeOfThisMember;
}
public int getQrb() {
public Double getQrb() {
return qrb;
}
public void setQrb(int qrb) {
public void setQrb(Double qrb) {
this.qrb = qrb;
}
public int getQTFdirection() {
public Double getQTFdirection() {
return QTFdirection;
}
public void setQTFdirection(int qTFdirection) {
public void setQTFdirection(Double qTFdirection) {
QTFdirection = qTFdirection;
}
// public int getWorkedCategory() {
// return workedCategory;
// }
// public void setWorkedCategory(int workedCategory) {
// this.workedCategory = workedCategory;
// }
public String getCallSign() {
return callSign;
}
/**
* Sets the original callsign and derives the normalized base callsign which is
* used as the database key. Prefixes like EA5/ and suffixes like /P or -70 are
* ignored for the raw-key handling.
*
* @param callSign callsign as received from chat or database
*/
public void setCallSign(String callSign) {
this.callSign = callSign;
if (callSign == null) {
this.callSign = null;
this.callSignRaw = null;
return;
}
this.callSign = callSign.trim().toUpperCase(Locale.ROOT);
this.callSignRaw = normalizeCallSignToBaseCallSign(this.callSign);
}
/**
* Normalizes a callsign to the base callsign which is used as the unique key in
* the internal database. The method removes KST suffixes like "-2", portable
* suffixes like "/P" and prefix additions like "EA5/".
*
* @param callSign callsign to normalize
* @return normalized base callsign in upper case
*/
public static String normalizeCallSignToBaseCallSign(String callSign) {
if (callSign == null) {
return null;
}
String normalizedCallSign = callSign.trim().toUpperCase(Locale.ROOT);
if (normalizedCallSign.isBlank()) {
return normalizedCallSign;
}
String callSignWithoutDashSuffix = normalizedCallSign.split("-", 2)[0].trim();
if (!callSignWithoutDashSuffix.contains("/")) {
return callSignWithoutDashSuffix;
}
String[] callSignParts = callSignWithoutDashSuffix.split("/");
String bestMatchingCallsignPart = helper_selectBestCallsignPart(callSignParts);
if (bestMatchingCallsignPart == null || bestMatchingCallsignPart.isBlank()) {
return callSignWithoutDashSuffix;
}
return bestMatchingCallsignPart;
}
/**
* Selects the most plausible base callsign segment from a slash-separated
* callsign. In strings like "EA5/G8MBI/P" the segment "G8MBI" is preferred over
* prefix or portable markers.
*
* @param callSignParts slash-separated callsign parts
* @return best matching base callsign segment
*/
private static String helper_selectBestCallsignPart(String[] callSignParts) {
String bestLikelyBaseCallsignPart = null;
int bestLikelyBaseCallsignLength = -1;
String bestFallbackCallsignPart = null;
int bestFallbackCallsignLength = -1;
for (String rawCallsignPart : callSignParts) {
String currentCallsignPart = rawCallsignPart == null ? "" : rawCallsignPart.trim().toUpperCase(Locale.ROOT);
if (currentCallsignPart.isBlank()) {
continue;
}
if (currentCallsignPart.length() > bestFallbackCallsignLength) {
bestFallbackCallsignPart = currentCallsignPart;
bestFallbackCallsignLength = currentCallsignPart.length();
}
if (helper_isLikelyBaseCallsignSegment(currentCallsignPart)
&& currentCallsignPart.length() > bestLikelyBaseCallsignLength) {
bestLikelyBaseCallsignPart = currentCallsignPart;
bestLikelyBaseCallsignLength = currentCallsignPart.length();
}
}
if (bestLikelyBaseCallsignPart != null) {
return bestLikelyBaseCallsignPart;
}
return bestFallbackCallsignPart;
}
/**
* Checks whether a slash-separated segment looks like a real base callsign. A
* normal amateur-radio callsign typically contains letters and digits and is
* longer than one-character postfix markers.
*
* @param callsignSegment segment to inspect
* @return true if the segment looks like a base callsign
*/
private static boolean helper_isLikelyBaseCallsignSegment(String callsignSegment) {
boolean containsLetter = false;
boolean containsDigit = false;
for (int currentIndex = 0; currentIndex < callsignSegment.length(); currentIndex++) {
char currentCharacter = callsignSegment.charAt(currentIndex);
if (Character.isLetter(currentCharacter)) {
containsLetter = true;
}
if (Character.isDigit(currentCharacter)) {
containsDigit = true;
}
}
return containsLetter && containsDigit && callsignSegment.length() >= 3;
}
public String getQra() {
@@ -210,22 +461,83 @@ public class ChatMember {
this.frequency = frequency;
}
public long getActivityCounter() {
return activityCounter;
public long getActivityTimeLastInEpoch() {
return activityTimeLastInEpoch;
}
public void setActivityCounter(int activityCounter) {
this.activityCounter = activityCounter;
this.activityTimeLastInEpoch = activityCounter;
}
public boolean isWorked() {
return worked;
}
public void setWorked(boolean worked) {
public boolean isWorked50() {
return Worked50;
}
public void setWorked50(boolean worked50) {
Worked50 = worked50;
}
public boolean isWorked70() {
return Worked70;
}
public void setWorked70(boolean worked70) {
Worked70 = worked70;
}
public boolean isWorked24G() {
return Worked24G;
}
public void setWorked24G(boolean worked24G) {
Worked24G = worked24G;
}
public boolean isWorked47G() {
return Worked47G;
}
public void setWorked47G(boolean worked47G) {
Worked47G = worked47G;
}
public boolean isWorked76G() {
return Worked76G;
}
public void setWorked76G(boolean worked76G) {
Worked76G = worked76G;
}
public void setWorked(boolean worked) {
this.worked = worked;
}
/**
*
* @return String (callsign) without -2 or -70 etc.
*/
public String getCallSignRaw() {
return callSignRaw;
// String raw = "";
//
// try {
// return this.getCallSign().split("-")[0]; //e.g. OK2M-70, returns only ok2m
// } catch (Exception e) {
// return getCallSign();
// }
}
/**
* Sets all worked information of this object to false. Scope: GUI, Reset Button
* for worked info, called by appcontroller
@@ -234,21 +546,44 @@ public class ChatMember {
this.setWorked(false);
this.setWorked144(false);
this.setWorked50(false);
this.setWorked70(false);
this.setWorked432(false);
this.setWorked1240(false);
this.setWorked2300(false);
this.setWorked3400(false);
this.setWorked5600(false);
this.setWorked10G(false);
this.setWorked24G(false);
this.setWorked47G(false);
this.setWorked76G(false);
}
/**
* Sets all worked information of this object to false. Scope: GUI, Reset Button
* for worked info, called by appcontroller
*/
public void resetQRVInformationAtAllBands() {
this.setQrvAny(true);
this.setQrv144(true);
this.setQrv432(true);
this.setQrv1240(true);
this.setQrv2300(true);
this.setQrv3400(true);
this.setQrv5600(true);
this.setQrv10G(true);
}
@Override
public String toString() {
String chatMemberSerialization = "";
chatMemberSerialization += callSign + ";" + name + ";" + qra + ";" + frequency + ";" + worked + ";" + worked144
+ ";" + worked432 + ";" + worked1240 + ";" + worked2300 + ";" + worked3400 + ";" + worked5600 + ";"
+ worked10G;
chatMemberSerialization += callSign + ";" + name + ";" + qra + ";" + frequency + "; wkd " + worked + "; wkd144 " + worked144
+ "; wkd432" + worked432 + "; wkd1240" + worked1240 + "; wkd2300" + worked2300 + "; wkd3400" + worked3400 + "; wkd5600" + worked5600 + "; wkd10G"
+ worked10G + " ; " + chatCategory;
return chatMemberSerialization;
}
@@ -267,4 +602,56 @@ public class ChatMember {
return false;
}
/**
* Adds a new recognized frequency by band to the internal band/qrg map
* @param band
* @param freq
*/
public void addKnownFrequency(Band band, double freq) {
this.knownActiveBands.put(band, new ActiveFrequencyInfo(freq));
}
/**
* represents a map of bands which are known of this chatmember
*
* @return Band
*/
public Map<Band, ActiveFrequencyInfo> getKnownActiveBands() {
return knownActiveBands;
}
/**
* If a sked fails and the user tells this to the client, this counter will be increased to give the station a
* lower score
*/
public void incrementFailedAttempts() {
this.failedQSOAttempts++;
}
public void resetFailedAttempts() {
this.failedQSOAttempts = 0;
}
public int getFailedQSOAttempts() {
return failedQSOAttempts;
}
/**
* Sets the working-priority score of a chatmember for the "Todo-List"
* @param score
*/
public void setCurrentPriorityScore(double score) {
this.currentPriorityScore = score;
}
/**
* Gets the working-priority score of a chatmember for the "Todo-List"
*
*/
public double getCurrentPriorityScore() {
return currentPriorityScore;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
package kst4contest.model;
/**
* Represents a scheduled event or an AirScout opportunity in the future.
* Used for the Timeline View and Priority Calculation.
*/
public class ContestSked {
private String targetCallsign;
private double targetAzimuth; // Required for Antenna-Visuals
private long skedTimeEpoch; // The peak time (e.g., AP)
private Band band;
// Opportunity potential (0..100). -1 means "unknown".
int opportunityPotentialPercent = -1;
// Status flags to prevent spamming alarms
private boolean warning3MinSent = false;
private boolean warningNowSent = false;
public ContestSked(String call, double azimuth, long time, Band b) {
this.targetCallsign = call;
this.targetAzimuth = azimuth;
this.skedTimeEpoch = time;
this.band = b;
}
/**
* Returns the seconds remaining until the event.
* Negative values mean the event is in the past.
*/
public long getTimeUntilSkedSeconds() {
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
}
// Getters and Setters...
public String getTargetCallsign() { return targetCallsign; }
public double getTargetAzimuth() { return targetAzimuth; }
public long getSkedTimeEpoch() { return skedTimeEpoch; }
public Band getBand() { return band; }
public boolean isWarning3MinSent() { return warning3MinSent; }
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
public boolean isWarningNowSent() { return warningNowSent; }
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
public int getOpportunityPotentialPercent() {
return opportunityPotentialPercent;
}
public void setOpportunityPotentialPercent(int opportunityPotentialPercent) {
this.opportunityPotentialPercent = opportunityPotentialPercent;
}
}

View File

@@ -0,0 +1,103 @@
package kst4contest.model;
/**
* Object for the description of the activity of a Thread to show these information in a View.
* <br/><br/>
* If state is critical, there could be used a further information field for the stacktrace
*/
public class ThreadStateMessage {
String threadNickName;
String threadDescription;
boolean running;
String runningInformationTextDescription;
String runningInformation;
boolean criticalState;
String criticalStateFurtherInfo;
public ThreadStateMessage(String threadNickName, boolean running, String runningInformation, boolean criticalState) {
this.threadNickName = threadNickName;
this.running = running;
this.criticalState = criticalState;
this.runningInformation = runningInformation;
}
/**
* This triggers the message for "Sked armed"
*
* @return
*/
public String getRunningInformationTextDescription() {
// If a custom description was set (e.g. for UI indicator buttons), prefer it.
if (runningInformationTextDescription != null && !runningInformationTextDescription.isBlank()) {
return runningInformationTextDescription;
}
// Fallback (legacy behavior)
if (isRunning()) {
return "on";
} else if (!isRunning() && isCriticalState()) {
return "FAILED";
} else {
return "off";
}
}
public void setRunningInformationTextDescription(String runningInformationTextDescription) {
this.runningInformationTextDescription = runningInformationTextDescription;
}
public String getThreadNickName() {
return threadNickName;
}
public void setThreadNickName(String threadNickName) {
this.threadNickName = threadNickName;
}
public String getThreadDescription() {
return threadDescription;
}
public void setThreadDescription(String threadDescription) {
this.threadDescription = threadDescription;
}
public boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
public String getRunningInformation() {
return runningInformation;
}
public void setRunningInformation(String runningInformation) {
this.runningInformation = runningInformation;
}
public boolean isCriticalState() {
return criticalState;
}
public void setCriticalState(boolean criticalState) {
this.criticalState = criticalState;
}
public String getCriticalStateFurtherInfo() {
return criticalStateFurtherInfo;
}
public void setCriticalStateFurtherInfo(String criticalStateFurtherInfo) {
this.criticalStateFurtherInfo = criticalStateFurtherInfo;
}
}

View File

@@ -0,0 +1,87 @@
package kst4contest.model;
import java.util.ArrayList;
public class UpdateInformation {
double latestVersionNumberOnServer = 1.26; //dummy value to prevent nullpointerexc
String adminMessage ="";
String majorChanges ="";
String latestVersionPathOnWebserver="";
ArrayList<String> needUpdateResourcesSinceLastVersion = new ArrayList<String>();
ArrayList<String[]> featureRequest = new ArrayList<String[]>();
ArrayList<String[]> bugRequests = new ArrayList<String[]>();
ArrayList<String[]> changeLog = new ArrayList<String[]>();
ArrayList<String[]> bugList = new ArrayList<String[]>();
public ArrayList<String[]> getBugList() {
return bugList;
}
public void setBugList(ArrayList<String[]> bugList) {
this.bugList = bugList;
}
public ArrayList<String[]> getChangeLog() {
return changeLog;
}
public void setChangeLog(ArrayList<String[]> changeLog) {
this.changeLog = changeLog;
}
public double getLatestVersionNumberOnServer() {
return latestVersionNumberOnServer;
}
public void setLatestVersionNumberOnServer(double latestVersionNumberOnServer) {
this.latestVersionNumberOnServer = latestVersionNumberOnServer;
}
public String getAdminMessage() {
return adminMessage;
}
public void setAdminMessage(String adminMessage) {
this.adminMessage = adminMessage;
}
public String getMajorChanges() {
return majorChanges;
}
public void setMajorChanges(String majorChanges) {
this.majorChanges = majorChanges;
}
public String getLatestVersionPathOnWebserver() {
return latestVersionPathOnWebserver;
}
public void setLatestVersionPathOnWebserver(String latestVersionPathOnWebserver) {
this.latestVersionPathOnWebserver = latestVersionPathOnWebserver;
}
public ArrayList<String> getNeedUpdateResourcesSinceLastVersion() {
return needUpdateResourcesSinceLastVersion;
}
public void setNeedUpdateResourcesSinceLastVersion(ArrayList<String> needUpdateResourcesSinceLastVersion) {
this.needUpdateResourcesSinceLastVersion = needUpdateResourcesSinceLastVersion;
}
public ArrayList<String[]> getFeatureRequest() {
return featureRequest;
}
public void setFeatureRequest(ArrayList<String[]> featureRequest) {
this.featureRequest = featureRequest;
}
public ArrayList<String[]> getBugRequests() {
return bugRequests;
}
public void setBugRequests(ArrayList<String[]> bugRequests) {
this.bugRequests = bugRequests;
}
}

View File

@@ -0,0 +1,256 @@
package kst4contest.test;
import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class MockKstServer {
private static final int PORT = 23001;
private static final String CHAT_ID = "2"; // 2 = 144/432 MHz
// Thread-sichere Liste aller verbundenen Clients (OutputStreams)
private final List<PrintWriter> clients = new CopyOnWriteArrayList<>();
// Permanente User (Ihre Test-Callsigns)
private final Map<String, User> onlineUsers = new HashMap<>();
// Historien müssen synchronisiert werden
private final List<String> historyChat = Collections.synchronizedList(new ArrayList<>());
private final List<String> historyDx = Collections.synchronizedList(new ArrayList<>());
private boolean running = false;
private ServerSocket serverSocket;
public MockKstServer() {
// Initiale Permanente User
addUser("DK5EW", "Erwin", "JN47NX");
addUser("DL1TEST", "TestOp", "JO50XX");
addUser("ON4KST", "Alain", "JO20HI");
addUser("PA9R-2", "2", "JO20HI");
addUser("PA9R-70", "70", "JO20HI");
addUser("PA9R", "general", "JO20HI");
}
// Startet den Server im Hintergrund (Non-Blocking)
public void start() {
if (running) return;
running = true;
new Thread(() -> {
try {
serverSocket = new ServerSocket(PORT);
System.out.println("[Server] ON4KST Simulation gestartet auf Port " + PORT);
// Startet den Simulator für Zufallstraffic
new Thread(this::simulationLoop).start();
while (running) {
Socket clientSocket = serverSocket.accept();
System.out.println("[Server] Neuer Client verbunden: " + clientSocket.getInetAddress());
new Thread(new ClientHandler(clientSocket)).start();
}
} catch (IOException e) {
if (running) e.printStackTrace();
}
}).start();
}
public void stop() {
running = false;
try {
if (serverSocket != null) serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private void addUser(String call, String name, String loc) {
onlineUsers.put(call, new User(call, name, loc));
}
private void removeUser(String call) {
onlineUsers.remove(call);
}
// Sendet Nachricht an ALLE verbundenen Clients (inkl. Sender)
private void broadcast(String message) {
if (!message.endsWith("\r\n")) message += "\r\n";
String finalMsg = message;
for (PrintWriter writer : clients) {
try {
writer.print(finalMsg);
writer.flush(); // WICHTIG: Sofort senden!
} catch (Exception e) {
// Client wohl weg, wird beim nächsten Schreibversuch oder im Handler entfernt
}
}
}
// --- Innere Logik: Client Handler ---
private class ClientHandler implements Runnable {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private String myCall = "MYCLIENT"; // Default, wird bei LOGIN überschrieben
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// ISO-8859-1 ist Standard für KST/Telnet Cluster
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ISO-8859-1"));
out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"), true);
clients.add(out);
String line;
boolean loginComplete = false;
while ((line = in.readLine()) != null) {
// System.out.println("[RECV] " + line); // Debugging aktivieren falls nötig
String[] parts = line.split("\\|");
String cmd = parts[0];
if (cmd.equals("LOGIN") || cmd.equals("LOGINC")) {
// Protokoll: LOGIN|callsign|password|... [cite: 21]
if (parts.length > 1) myCall = parts[1];
// 1. Login Bestätigung
// Format: LOGSTAT|100|chat id|client software version|session key|config|dx option|
send("LOGSTAT|100|" + CHAT_ID + "|JavaSim|KEY123|Config|3|");
// Bei LOGIN senden wir die Daten sofort
// Bei LOGINC warten wir eigentlich auf SDONE, senden hier aber vereinfacht direkt
if (cmd.equals("LOGIN")) {
sendInitialData();
loginComplete = true;
}
}
else if (cmd.equals("SDONE")) {
// Abschluss der Settings (bei LOGINC) [cite: 34]
sendInitialData();
loginComplete = true;
}
else if (cmd.equals("MSG")) {
// MSG|chat id|destination|command|0| [cite: 42]
if (parts.length >= 4) {
String text = parts[3];
// Nachricht sofort als CH Frame an alle verteilen (Echo)
handleChatMessage(myCall, "Me", text);
}
}
else if (cmd.equals("CK")) {
// Keepalive [cite: 20]
// Server muss nicht zwingend antworten, aber Connection bleibt offen
}
}
} catch (IOException e) {
// System.out.println("Client getrennt");
} finally {
clients.remove(out);
try { socket.close(); } catch (IOException e) {}
}
}
private void send(String msg) {
if (!msg.endsWith("\r\n")) msg += "\r\n";
out.print(msg);
out.flush();
}
private void sendInitialData() {
// 1. User Liste UA0 [cite: 14]
for (User u : onlineUsers.values()) {
send("UA0|" + CHAT_ID + "|" + u.call + "|" + u.name + "|" + u.loc + "|0|");
}
// 2. Chat History CR [cite: 7]
synchronized(historyChat) {
for (String h : historyChat) send(h);
}
// 3. DX History DL [cite: 10]
synchronized(historyDx) {
for (String d : historyDx) send(d);
}
// 4. Ende User Liste UE [cite: 15]
send("UE|" + CHAT_ID + "|" + onlineUsers.size() + "|");
}
}
// --- Hilfsmethoden für Traffic ---
private void handleChatMessage(String call, String name, String text) {
// CH|chat id|date|callsign|firstname|destination|msg|highlight|
String date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
String frame = String.format("CH|%s|%s|%s|%s|0|%s|0|", CHAT_ID, date, call, name, text);
synchronized(historyChat) {
historyChat.add(frame);
if (historyChat.size() > 50) historyChat.remove(0);
}
broadcast(frame);
}
private void handleDxSpot(String spotter, String dx, String freq) {
// DL|Unix time|dx utc|spotter|qrg|dx|info|spotter locator|dx locator| [cite: 10]
long unixTime = System.currentTimeMillis() / 1000;
String utc = new SimpleDateFormat("HHmm").format(new Date());
// Simple Dummy Locators
String frame = String.format("DL|%d|%s|%s|%s|%s|Simulated|JO00|JO99|",
unixTime, utc, spotter, freq, dx);
synchronized(historyDx) {
historyDx.add(frame);
if (historyDx.size() > 20) historyDx.remove(0);
}
broadcast(frame);
}
private void simulationLoop() {
String[] randomCalls = {"PA0GUS", "F6APE", "OH8K", "OZ2M", "G4CBW"};
String[] msgs = {"CQ 144.300", "Tnx for QSO", "Any sked?", "QRV 432.200"};
Random rand = new Random();
while (running) {
try {
Thread.sleep(3000 + rand.nextInt(5000)); // 3-8 Sek Pause
int action = rand.nextInt(10);
String call = randomCalls[rand.nextInt(randomCalls.length)];
if (action < 4) { // 40% Chat
handleChatMessage(call, "SimOp", msgs[rand.nextInt(msgs.length)]);
} else if (action < 7) { // 30% DX Spot
handleDxSpot(call, randomCalls[rand.nextInt(randomCalls.length)], "144." + rand.nextInt(400));
} else if (action == 8) { // Login Simulation UA5
if (!onlineUsers.containsKey(call)) {
addUser(call, "SimOp", "JO11");
broadcast("UA5|" + CHAT_ID + "|" + call + "|SimOp|JO11|2|");
}
} else if (action == 9) { // Logout Simulation UR6
if (onlineUsers.containsKey(call) && !call.equals("DK5EW")) { // DK5EW nicht kicken
removeUser(call);
broadcast("UR6|" + CHAT_ID + "|" + call + "|");
}
}
// Ping ab und zu
if (rand.nextInt(5) == 0) broadcast("CK|");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// Kleine Datenklasse
private static class User {
String call, name, loc;
User(String c, String n, String l) { this.call=c; this.name=n; this.loc=l; }
}
}

View File

@@ -0,0 +1,131 @@
package kst4contest.test;
import javafx.beans.property.SimpleStringProperty;
import kst4contest.controller.Utils4KST;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PatternMatcherTest {
/**
* Tests if pattern matches with the given String.
*
* @param testString
* @param regExPattern
* @return true if match, else false
*/
private static boolean testPattern(String testString, String regExPattern) {
Pattern pattern = Pattern.compile(regExPattern);
Matcher matcher = pattern.matcher(testString);
return matcher.find();
}
/**
* Normalizes a chatmembers frequency-string for cluster usage<br/>
* <b>returns a frequency String in KHz like = "144300" or "144300.0" to match DXC protocol needs</b>
*
* @param optionalPrefix: if there is a value like ".300", it have to be decided, wich ".300": 144.300, 432.300, 1296.300 .... prefix means for example "144."
*/
private static String normalizeFrequencyString(String qrgString, String optionalPrefix) {
// final String PTRN_QRG_CAT2 = "(([0-9]{3,4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)|(([a-zA-Z][0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)|((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)";
final String PTRN_QRG_CAT2_wholeQRGMHz4Digits = "(([0-9]{4}[\\.|,| ]?[0-9]{3})([\\.|,][\\d]{1,2})?)"; //1296.300.3 etc
final String PTRN_QRG_CAT2_wholeQRGMHz3Digits = "(([0-9]{3}[\\.|,| ]?[0-9]{3})([\\.][\\d]{1,2})?)"; //144.300.3 etc
final String PTRN_QRG_CAT2_QRGwithoutPrefix = "((\\b[0-4]{1}[\\d]{2}\\b)([\\.|,][\\d]{1,2}\\b)?)"; //144.300.3 etc
String predefinedPrefixInMHz = optionalPrefix;
String stringAggregation = "";
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz4Digits)) {
System.out.print("yep: ");
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".","");
stringAggregation = stringAggregation.replace(",","");
if (stringAggregation.length() == 8) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
stringAggregation = stringAggregationNew;
} else if (stringAggregation.length() == 9) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
stringAggregation = stringAggregationNew;
}
} else
if (testPattern(qrgString, PTRN_QRG_CAT2_wholeQRGMHz3Digits)) {
System.out.print("yep: ");
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".","");
stringAggregation = stringAggregation.replace(",","");
if (stringAggregation.length() == 7) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-1) + "." + stringAggregation.substring(stringAggregation.length()-1, stringAggregation.length());
stringAggregation = stringAggregationNew;
} else if (stringAggregation.length() == 8) {
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length()-2) + "." + stringAggregation.substring(stringAggregation.length()-2, stringAggregation.length());
stringAggregation = stringAggregationNew;
}
}
else
if (testPattern(qrgString, PTRN_QRG_CAT2_QRGwithoutPrefix)) { //case ".050 or .300 or something like that"
System.out.print("yep: ");
stringAggregation = qrgString;
stringAggregation = stringAggregation.replace(".", "");
stringAggregation = stringAggregation.replace(",", "");
if (stringAggregation.length() == 3) { // like 050 or 300
String stringAggregationNew = optionalPrefix + stringAggregation;
stringAggregation = stringAggregationNew;
return stringAggregation;
} else if (stringAggregation.length() == 4) { //like 050.2 --> 0502
stringAggregation = optionalPrefix + stringAggregation;
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 1) + "." + stringAggregation.substring(stringAggregation.length() - 1, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
} else if (stringAggregation.length() == 5) { //like 050.20 --> 05020
stringAggregation = optionalPrefix + stringAggregation;
String stringAggregationNew = stringAggregation.substring(0, stringAggregation.length() - 2) + "." + stringAggregation.substring(stringAggregation.length() - 2, stringAggregation.length());
stringAggregation = stringAggregationNew;
return stringAggregation;
}
}
return qrgString;
}
public static void main(String[] args) {
int i = 0;
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.775", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.300.2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144,300.2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144300.2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144300,2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.300", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("144.300.20", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("300", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString(".300", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString(".300.2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString(".300.20", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300.2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300.20", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300,2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300,20", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296.300,2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("1296,300.2", new SimpleStringProperty("144")));
System.out.println(i++ + ": " + Utils4KST.normalizeFrequencyString("q305", new SimpleStringProperty("144")));
}
}

View File

@@ -0,0 +1,484 @@
package kst4contest.utils;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import kst4contest.ApplicationConstants;
import java.io.File;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
/**
* This part of the client drives the sounds. Its a singleton instance. All audio outs are directed to this instance.<br>
* <br>
* */
public class PlayAudioUtils {
/**
* Default constructor initializes the sound files and copies it to the project home folder
*/
public PlayAudioUtils() {
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3", "NOISESTARTUP.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISECQWINDOW.mp3", "NOISECQWINDOW.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISEPMWINDOW.mp3", "NOISEPMWINDOW.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISEERROR.mp3", "NOISEERROR.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/NOISENOTIFY.mp3", "NOISENOTIFY.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/tick.mp3", "tick.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3", "LTTRA.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3", "LTTRB.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3", "LTTRC.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3", "LTTRD.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3", "LTTRE.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3", "LTTRF.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3", "LTTRG.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3", "LTTRH.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3", "LTTRI.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3", "LTTRJ.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3", "LTTRK.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3", "LTTRL.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3", "LTTRM.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3", "LTTRN.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3", "LTTRO.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3", "LTTRP.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3", "LTTRQ.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3", "LTTRR.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3", "LTTRS.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3", "LTTRT.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3", "LTTRU.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3", "LTTRV.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3", "LTTRW.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3", "LTTRX.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3", "LTTRY.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3", "LTTRZ.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3", "LTTR0.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3", "LTTR1.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3", "LTTR2.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3", "LTTR3.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3", "LTTR4.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3", "LTTR5.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3", "LTTR6.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3", "LTTR7.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3", "LTTR8.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3", "LTTR9.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3", "LTTRSTROKE.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/LTTRSPACE.mp3", "LTTRSPACE.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3", "VOICEA.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3", "VOICEB.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3", "VOICEC.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3", "VOICED.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3", "VOICEE.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3", "VOICEF.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3", "VOICEG.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3", "VOICEH.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3", "VOICEI.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3", "VOICEJ.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3", "VOICEK.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3", "VOICEL.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3", "VOICEM.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3", "VOICEN.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3", "VOICEO.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3", "VOICEP.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3", "VOICEQ.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3", "VOICER.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3", "VOICES.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3", "VOICET.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3", "VOICEU.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3", "VOICEV.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3", "VOICEW.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3", "VOICEX.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3", "VOICEY.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3", "VOICEZ.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3", "VOICE0.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3", "VOICE1.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3", "VOICE2.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3", "VOICE3.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3", "VOICE4.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3", "VOICE5.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3", "VOICE6.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3", "VOICE7.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3", "VOICE8.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3", "VOICE9.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKE.mp3", "VOICESTROKE.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3", "VOICEBELL.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3", "VOICEYOUGOTMAIL.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3", "VOICEHELLO.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3", "VOICE73.mp3");
ApplicationFileUtils.copyResourceIfRequired(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3", "VOICESTROKEPORTABLE.mp3");
}
private Queue<Media> musicList = new LinkedList<Media>();
private MediaPlayer mediaPlayer ;
/**
* Plays notification sounds out of the windws 95 box by given action character<br/>
*<br/>
*
* case '!': Startup<br/>
* case '-': tick<br/>
* case 'C': CQ Window new entry<br/>
* case 'P': PM Window new entry<br/>
* case 'E': Error occured<br/>
* case 'N': other notification sounds<br/>
*
* @param actionChar
*/
public void playNoiseLauncher(char actionChar) {
// ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3");
switch (actionChar){
case '-':
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/tick.mp3")).toURI().toString()));
break;
case '!':
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISESTARTUP.mp3")).toURI().toString()));
break;
case 'C':
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISECQWINDOW.mp3")).toURI().toString()));
break;
case 'P':
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEPMWINDOW.mp3")).toURI().toString()));
break;
case 'E':
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISEERROR.mp3")).toURI().toString()));
break;
case 'N':
musicList.add(new Media(new File (ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/NOISENOTIFY.mp3")).toURI().toString()));
break;
// case 'M':
// musicList.add(new Media(new File ("VOICE.mp3").toURI().toString()));
// break;
default:
System.out.println("[KST4ContestApp, warning, letter not defined!]");
}
playMusic();
// mediaPlayer.dispose();
}
/**
* Plays all chars of a given String-parameter as CW Sound out of the speaker.
* As a workaround for delay problems at the beginning of playing, there are added 2x pause chars to the string.
*
* @param playThisChars
*/
public void playCWLauncher(String playThisChars) {
char[] playThisInCW = playThisChars.toUpperCase().toCharArray();
for (char letterToPlay: playThisInCW){
switch (letterToPlay){
case 'A':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRA.mp3")).toURI().toString()));
break;
case 'B':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRB.mp3")).toURI().toString()));
break;
case 'C':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRC.mp3")).toURI().toString()));
break;
case 'D':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRD.mp3")).toURI().toString()));
break;
case 'E':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRE.mp3")).toURI().toString()));
break;
case 'F':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRF.mp3")).toURI().toString()));
break;
case 'G':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRG.mp3")).toURI().toString()));
break;
case 'H':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRH.mp3")).toURI().toString()));
break;
case 'I':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRI.mp3")).toURI().toString()));
break;
case 'J':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRJ.mp3")).toURI().toString()));
break;
case 'K':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRK.mp3")).toURI().toString()));
break;
case 'L':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRL.mp3")).toURI().toString()));
break;
case 'M':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRM.mp3")).toURI().toString()));
break;
case 'N':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRN.mp3")).toURI().toString()));
break;
case 'O':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRO.mp3")).toURI().toString()));
break;
case 'P':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRP.mp3")).toURI().toString()));
break;
case 'Q':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRQ.mp3")).toURI().toString()));
break;
case 'R':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRR.mp3")).toURI().toString()));
break;
case 'S':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRS.mp3")).toURI().toString()));
break;
case 'T':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRT.mp3")).toURI().toString()));
break;
case 'U':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRU.mp3")).toURI().toString()));
break;
case 'V':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRV.mp3")).toURI().toString()));
break;
case 'W':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRW.mp3")).toURI().toString()));
break;
case 'X':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRX.mp3")).toURI().toString()));
break;
case 'Y':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRY.mp3")).toURI().toString()));
break;
case 'Z':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRZ.mp3")).toURI().toString()));
break;
case '1':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR1.mp3")).toURI().toString()));
break;
case '2':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR2.mp3")).toURI().toString()));
break;
case '3':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR3.mp3")).toURI().toString()));
break;
case '4':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR4.mp3")).toURI().toString()));
break;
case '5':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR5.mp3")).toURI().toString()));
break;
case '6':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR6.mp3")).toURI().toString()));
break;
case '7':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR7.mp3")).toURI().toString()));
break;
case '8':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR8.mp3")).toURI().toString()));
break;
case '9':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR9.mp3")).toURI().toString()));
break;
case '0':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTR0.mp3")).toURI().toString()));
break;
case '/':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSTROKE.mp3")).toURI().toString()));
break;
case ' ':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/LTTRSPACE.mp3")).toURI().toString()));
break;
default:
System.out.println("[KST4ContestApp, warning, letter not defined:] cwLetters = " + Arrays.toString(playThisInCW));
}
}
playMusic();
// mediaPlayer.dispose();
}
/**
*
* Plays a voice file for each char in the string (only EN alphabetic and numbers) except some specials: <br/><br/>
* <b>Note that the audio settings (ChatPreferences) must be switched on in order to make the sounds playing.</b><br/><br/>
* case '!': BELL<br/>
* case '?': YOUGOTMAIL<br/>
* case '#': HELLO<br/>
* case '*': 73 bye<br/>
* case '$': STROKEPORTABLE<br/>
* @param playThisChars
*/
public void playVoiceLauncher(String playThisChars) {
char[] spellThisWithVoice = playThisChars.toUpperCase().toCharArray();
for (char letterToPlay: spellThisWithVoice){
switch (letterToPlay){
case '!':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEBELL.mp3")).toURI().toString()));
break;
case '?':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEYOUGOTMAIL.mp3")).toURI().toString()));
break;
case '#':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEHELLO.mp3")).toURI().toString()));
break;
case '*':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE73.mp3")).toURI().toString()));
break;
case '$':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKEPORTABLE.mp3")).toURI().toString()));
break;
case 'A':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEA.mp3")).toURI().toString()));
break;
case 'B':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEB.mp3")).toURI().toString()));
break;
case 'C':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEC.mp3")).toURI().toString()));
break;
case 'D':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICED.mp3")).toURI().toString()));
break;
case 'E':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEE.mp3")).toURI().toString()));
break;
case 'F':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEF.mp3")).toURI().toString()));
break;
case 'G':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEG.mp3")).toURI().toString()));
break;
case 'H':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEH.mp3")).toURI().toString()));
break;
case 'I':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEI.mp3")).toURI().toString()));
break;
case 'J':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEJ.mp3")).toURI().toString()));
break;
case 'K':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEK.mp3")).toURI().toString()));
break;
case 'L':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEL.mp3")).toURI().toString()));
break;
case 'M':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEM.mp3")).toURI().toString()));
break;
case 'N':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEN.mp3")).toURI().toString()));
break;
case 'O':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEO.mp3")).toURI().toString()));
break;
case 'P':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEP.mp3")).toURI().toString()));
break;
case 'Q':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEQ.mp3")).toURI().toString()));
break;
case 'R':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICER.mp3")).toURI().toString()));
break;
case 'S':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICES.mp3")).toURI().toString()));
break;
case 'T':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICET.mp3")).toURI().toString()));
break;
case 'U':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEU.mp3")).toURI().toString()));
break;
case 'V':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEV.mp3")).toURI().toString()));
break;
case 'W':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEW.mp3")).toURI().toString()));
break;
case 'X':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEX.mp3")).toURI().toString()));
break;
case 'Y':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEY.mp3")).toURI().toString()));
break;
case 'Z':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICEZ.mp3")).toURI().toString()));
break;
case '1':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE1.mp3")).toURI().toString()));
break;
case '2':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE2.mp3")).toURI().toString()));
break;
case '3':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE3.mp3")).toURI().toString()));
break;
case '4':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE4.mp3")).toURI().toString()));
break;
case '5':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE5.mp3")).toURI().toString()));
break;
case '6':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE6.mp3")).toURI().toString()));
break;
case '7':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE7.mp3")).toURI().toString()));
break;
case '8':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE8.mp3")).toURI().toString()));
break;
case '9':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE9.mp3")).toURI().toString()));
break;
case '0':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICE0.mp3")).toURI().toString()));
break;
case '/':
musicList.add(new Media(new File(ApplicationFileUtils.getFilePath(ApplicationConstants.APPLICATION_NAME, "/VOICESTROKE.mp3")).toURI().toString()));
break;
// case ' ':
// musicList.add(new Media(new File ("VOICESPACE.mp3").toURI().toString()));
// break;
default:
System.out.println("[KST4ContestApp, warning, letter not defined:] cwLetters = " + Arrays.toString(spellThisWithVoice));
}
}
playMusic();
// mediaPlayer.dispose();
}
private void playMusic() {
// System.out.println("Kst4ContestApplication.playMusic");
if(musicList.peek() == null)
{
return;
}
mediaPlayer = new MediaPlayer(musicList.poll());
mediaPlayer.setRate(1.0);
mediaPlayer.setOnReady(() -> {
mediaPlayer.play();
mediaPlayer.setOnEndOfMedia(() -> {
// mediaPlayer.dispose();
playMusic();
if (musicList.isEmpty()) {
// mediaPlayer.dispose();
}
});
});
}
}

View File

@@ -0,0 +1,19 @@
package kst4contest.utils;
import javafx.application.Application;
import javafx.stage.Stage;
import kst4contest.utils.PlayAudioUtils;
public class TestAudioPlayerUtils extends Application {
public static void main(String[] args) {
}
@Override
public void start(Stage stage) throws Exception {
PlayAudioUtils testAudio = new PlayAudioUtils();
testAudio.playCWLauncher("DO5AMF");
}
}

View File

@@ -1,7 +1,15 @@
package kst4contest.view;
import kst4contest.controller.ChatController;
import kst4contest.model.ChatMember;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GuiUtils {
private static final String PTRN_CALLSIGNSYNTAX = "^(?:[A-Z]{1,2}[0-9]|[0-9][A-Z])[0-9A-Z]{1,3}$";
/**
* Checks wheter the input value of the String is numeric or not, true if yes
* TODO: Move to a utils class for checking input values by user...
@@ -11,5 +19,58 @@ public class GuiUtils {
static boolean isNumeric(String str){
return str != null && str.matches("[0-9.]+");
}
/**
* Checks wheter the given String has a HAM radio callsign syntax or not
* @param maybeCallSignValue
* @return true if yes
*/
static boolean isCallSignSyntax(String maybeCallSignValue) {
Pattern pattern = Pattern.compile(PTRN_CALLSIGNSYNTAX, Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(maybeCallSignValue);
try {
if (matcher.find()) {
return true;
}
else return false;
} catch (Exception exc) {
return false;
}
}
public static void triggerGUIFilteredChatMemberListChange(ChatController chatController) {
if (javafx.application.Platform.isFxApplicationThread()) {
triggerUpdate(chatController);
} else{
javafx.application.Platform.runLater(() -> triggerUpdate(chatController));
}
}
private static void triggerUpdate(ChatController chatController) {
{
//trick to trigger gui changes on property changes of obects
Predicate<ChatMember> dummyPredicate = new Predicate<ChatMember>() {
@Override
public boolean test(ChatMember chatMember) {
return true;
}
};
/**
* //TODO: following 2 lines are a quick fix to making disappear worked chatmembers of the list
* Thats uncomfortable due to this also causes selection changes,
* Better way is to change all worked and qrv values to observables and then trigger the underlying
* list to fire an invalidationevent. Really Todo!
*/
chatController.getLst_chatMemberListFilterPredicates().add(dummyPredicate);
chatController.getLst_chatMemberListFilterPredicates().remove(dummyPredicate);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,369 @@
package kst4contest.view;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.scene.shape.Polygon;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.paint.Color;
import javafx.scene.effect.DropShadow;
import javafx.scene.Node;
import javafx.scene.Group;
import javafx.scene.text.Font;
import kst4contest.model.ContestSked;
import kst4contest.model.ChatCategory;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* A custom UI Component that visualizes future events (Skeds/AP).
* It changes opacity based on the current antenna direction.
*
* Extended:
* - Can also render "priority candidates" (ScoreService top list) with time base = next AirScout airplane minute.
* - Clicking a candidate triggers a callback (selection + /cq preparation happens in Kst4ContestApplication).
*/
public class TimelineView extends Pane {
private double currentAntennaAzimuth = 0;
private double beamWidth = 50.0; // TODO: from Prefs (later)
private final long PREVIEW_TIME_MS = 30 * 60 * 1000; // 30 Minutes Preview
double margin = 30; // enough space for the callsign label
private Function<ContestSked, String> skedTooltipExtraTextProvider; //used for further info in sked tooltip
private Consumer<CandidateEvent> onCandidateClicked;
public TimelineView() {
this.setPrefHeight(40);
this.setStyle("-fx-background-color: #2b2b2b;");
// weitere init defaults, falls du welche hattest
}
/**
* Backward compatibility: if some code still calls the old ctor.
* Potential/tooltip are not view-wide properties; they belong to markers/events.
*/
@Deprecated
public TimelineView(int opportunityPotentialPercent, String tooltipText) {
this(); // ignore args on purpose
}
//
// public int getOpportunityPotentialPercent() {
// return opportunityPotentialPercent;
// }
public void setCurrentAntennaAzimuth(double az) {
this.currentAntennaAzimuth = az;
}
public long getPreviewTimeMs() {
return PREVIEW_TIME_MS;
}
public void setOnCandidateClicked(Consumer<CandidateEvent> handler) {
this.onCandidateClicked = handler;
}
/** Backward compatible call (Skeds only) */
public void updateVisuals(List<ContestSked> skeds) {
updateVisuals(skeds, Collections.emptyList());
}
/**
* Redraws the timeline based on the list of active skeds AND priority candidates.
*/
public void updateVisuals(List<ContestSked> skeds, List<CandidateEvent> candidates) {
this.getChildren().clear();
double width = this.getWidth();
if (width <= 5) {
// Layout not ready yet; will be updated by caller later (uiPulse/list change)
return;
}
// Draw Axis
double axisY = 30;
Line axis = new Line(0, axisY, width, axisY);
axis.setStroke(Color.GRAY);
this.getChildren().add(axis);
long now = System.currentTimeMillis();
// 1) Draw Priority Candidates (upper lanes)
for (CandidateEvent ev : candidates) {
long timeDiff = ev.getTimeUntilMs();
if (timeDiff < 0 || timeDiff > PREVIEW_TIME_MS) continue;
double percent = (double) timeDiff / PREVIEW_TIME_MS;
double xPos = percent * width;
xPos = Math.max(margin, Math.min(this.getWidth() - margin, xPos)); //starting point of the diagram
Node marker = createCandidateMarker(ev);
applyAntennaEffect(marker, ev.getTargetAzimuth());
// Upper lanes so they don't overlap skeds
double laneBaseY = 2;
double laneOffsetY = 14.0 * ev.getLaneIndex();
marker.setLayoutX(xPos);
marker.setLayoutY(laneBaseY + laneOffsetY);
this.getChildren().add(marker);
}
// 2) Draw Skeds (lower lane)
for (ContestSked sked : skeds) {
long timeDiff = sked.getSkedTimeEpoch() - now;
// Only draw if within the next 30 mins
if (timeDiff >= 0 && timeDiff <= PREVIEW_TIME_MS) {
double percent = (double) timeDiff / PREVIEW_TIME_MS;
double xPos = percent * width;
xPos = clamp(xPos, 10, width - 10);
Node marker = createSkedMarker(sked);
applyAntennaEffect(marker, sked.getTargetAzimuth());
marker.setLayoutX(xPos);
marker.setLayoutY(axisY - 18); // below candidate lanes, near axis
this.getChildren().add(marker);
}
}
}
private double clamp(double v, double min, double max) {
return Math.max(min, Math.min(max, v));
}
/**
* Logic:
* If Antenna is ON TARGET -> Bright & Glowing.
* If Antenna is OFF TARGET -> Transparent (Ghost).
*/
private void applyAntennaEffect(Node marker, double targetAz) {
// invalid azimuth -> keep readable
if (!Double.isFinite(targetAz) || targetAz < 0) {
return;
}
double delta = Math.abs(currentAntennaAzimuth - targetAz);
if (delta > 180) delta = 360 - delta;
final boolean onTarget = delta <= (beamWidth / 2.0);
final boolean inBeam = delta <= beamWidth;
// Rule: only fade when we are clearly NOT pointing there
final double iconOpacity = inBeam ? 1.0 : 0.30;
if (marker instanceof Group g) {
// Never fade the whole group -> text stays readable
g.setOpacity(1.0);
for (Node child : g.getChildren()) {
if (child instanceof Label) {
child.setOpacity(1.0);
} else {
child.setOpacity(iconOpacity);
}
}
// Add glow only if well centered (optional)
if (onTarget) {
g.setEffect(new DropShadow(10, Color.LIMEGREEN));
g.setScaleX(1.10);
g.setScaleY(1.10);
} else {
g.setEffect(null);
g.setScaleX(1.0);
g.setScaleY(1.0);
}
return;
}
// fallback
marker.setOpacity(iconOpacity);
marker.setEffect(onTarget ? new DropShadow(10, Color.LIMEGREEN) : null);
}
public void setSkedTooltipExtraTextProvider(Function<ContestSked, String> provider) {
this.skedTooltipExtraTextProvider = provider;
}
/** Existing marker for Skeds (diamond + label) */
private Node createSkedMarker(ContestSked sked) {
Polygon diamond = new Polygon(0.0, 0.0, 6.0, 6.0, 0.0, 12.0, -6.0, 6.0);
diamond.setFill(colorForPotential(sked.getOpportunityPotentialPercent()));
String baseToolTipFallBack = sked.getTargetCallsign() + " (" + sked.getBand() + ")\nAz: " + sked.getTargetAzimuth();
if (skedTooltipExtraTextProvider != null) {
String extra = skedTooltipExtraTextProvider.apply(sked);
if (extra != null && !extra.isBlank()) {
baseToolTipFallBack += "\n" + extra;
}
}
Tooltip t = new Tooltip(baseToolTipFallBack);
Tooltip.install(diamond, t);
Label lbl = new Label("SKED: " + sked.getTargetCallsign());
// lbl.setFont(new Font(9));
// lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(14);
lbl.setLayoutX(-10);
lbl.setStyle(
"-fx-text-fill: white;" +
"-fx-font-weight: bold;" +
"-fx-background-color: rgba(0,0,0,0.65);" +
"-fx-background-radius: 6;" +
"-fx-padding: 1 4 1 4;"
);
lbl.setEffect(new DropShadow(2, Color.BLACK));
return new Group(diamond, lbl);
}
/**
* Give me a color for a given potencial of a AP reflection
*
* @param p AS potencial
* @return
*/
private Color colorForPotential(int p) {
if (p >= 95) return Color.MAGENTA; // ~100%
if (p >= 75) return Color.RED;
if (p >= 50) return Color.YELLOW;
return Color.DEEPSKYBLUE; // low potential
}
/** New marker for Priority Candidates (triangle + label) */
private Node createCandidateMarker(CandidateEvent ev) {
// Color derived from airplane potential (not urgency)
Color markerColor = colorForPotential(ev.getOpportunityPotentialPercent());
// small triangle marker (points downwards)
Polygon tri = new Polygon(-6.0, 0.0, 6.0, 0.0, 0.0, 10.0);
tri.setFill(markerColor);
// Optional: small dot behind triangle (makes it easier to see)
Circle dot = new Circle(4, markerColor);
dot.setLayoutY(5); // center behind triangle
Label lbl = new Label(ev.getDisplayCallSign());
lbl.setFont(new Font(9));
lbl.setTextFill(Color.WHITE);
lbl.setLayoutY(10);
lbl.setLayoutX(-12);
lbl.setStyle(
"-fx-text-fill: white;" +
"-fx-font-weight: bold;" +
"-fx-background-color: rgba(0,0,0,0.65);" +
"-fx-background-radius: 6;" +
"-fx-padding: 1 4 1 4;"
);
lbl.setEffect(new DropShadow(8, Color.BLACK));
// IMPORTANT: include dot + triangle + label in the Group
Group g = new Group(dot, tri, lbl);
if (ev.getTooltipText() != null && !ev.getTooltipText().isBlank()) {
Tooltip.install(g, new Tooltip(ev.getTooltipText()));
}
g.setOnMouseClicked(e -> {
if (onCandidateClicked != null) {
onCandidateClicked.accept(ev);
}
});
return g;
}
/**
* Data object rendered by the Timeline ("priority candidate").
* Created in Kst4ContestApplication from ScoreService TopCandidates + AirScout next-AP minute.
*/
public static class CandidateEvent {
private final String callSignRaw;
private final String displayCallSign;
private final ChatCategory preferredChatCategory;
private final long timeUntilMs;
private final int minuteBucket;
private final int laneIndex;
private final double targetAzimuth;
private final double score;
private final String tooltipText;
private final int opportunityPotentialPercent;
public CandidateEvent(
String callSignRaw,
String displayCallSign,
ChatCategory preferredChatCategory,
long timeUntilMs,
int minuteBucket,
int laneIndex,
double targetAzimuth,
double score,
int opportunityPotentialPercent,
String tooltipText
) {
this.callSignRaw = callSignRaw;
this.displayCallSign = displayCallSign;
this.preferredChatCategory = preferredChatCategory;
this.timeUntilMs = timeUntilMs;
this.minuteBucket = minuteBucket;
this.laneIndex = laneIndex;
this.targetAzimuth = targetAzimuth;
this.score = score;
this.opportunityPotentialPercent = opportunityPotentialPercent;
this.tooltipText = tooltipText;
}
public String getCallSignRaw() { return callSignRaw; }
public String getDisplayCallSign() { return displayCallSign; }
public ChatCategory getPreferredChatCategory() { return preferredChatCategory; }
public long getTimeUntilMs() { return timeUntilMs; }
public int getMinuteBucket() { return minuteBucket; }
public int getLaneIndex() { return laneIndex; }
public double getTargetAzimuth() { return targetAzimuth; }
public double getScore() { return score; }
public String getTooltipText() { return tooltipText; }
public int getOpportunityPotentialPercent() { return opportunityPotentialPercent; }
}
public void setBeamWidthDeg(double beamWidthDeg) {
if (beamWidthDeg > 0 && Double.isFinite(beamWidthDeg)) {
this.beamWidth = beamWidthDeg;
}
}
}

View File

@@ -0,0 +1,13 @@
module praktiKST {
requires javafx.controls;
requires javafx.fxml;
requires javafx.web;
requires jdk.xml.dom;
requires java.sql;
requires javafx.media;
exports kst4contest.controller.interfaces;
exports kst4contest.controller;
exports kst4contest.locatorUtils;
exports kst4contest.model;
exports kst4contest.view;
}

View File

@@ -0,0 +1,201 @@
.button:pressed {
-fx-border-color: #ff0000;
}
.button:hover {
-fx-border-color: #ff7777;
}
.btn-showstate-enabled-default {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: black;
}
.btn-showstate-enabled-default:hover {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/*radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: black;
}
.btn-showstate-enabled-furtherInfo {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: green;
}
.btn-showstate-enabled-furtherInfo:hover {
/*-fx-background-color:linear-gradient(#f0ff35, #b8ee36),*/
/* radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);*/
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: green;
}
.btn-showstate-disabled {
-fx-background-color:linear-gradient(#f0ff35, #111111),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: red;
}
.toggle-button:selected {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: #395306;
}
.text-field {
-fx-prompt-text-fill: black;
}
.text-field .text {
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-stroke: green;
-fx-stroke-width: 0.2;
-fx-font-size: 25px;
}
.text-input-MYQRG1 {
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, orange 0%, red 100%);
-fx-font-weight: 300;
-fx-padding: 1,1,1,1;
}
.button{
-fx-focus-traversable: false;
}
.button:hover{
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, blue 0%, red 100%);
}
.button:hover {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
radial-gradient(center 50% -40%, radius 200%, lightblue 45%, orange 50%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: #395306;
}
.buttonMyQrg1 {
-fx-background-color: linear-gradient(from 0% 0% to 100% 200%, #00ffff 0%, #ff99ff 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: #395306;
}
.toggle-button:selected {
-fx-background-color: linear-gradient(from 0% 0% to 100% 200%, #00ffff 0%, #ff99ff 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: #395306;
}
.table-view .column-header .text {
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, repeat, black 0%, red 50%);
-fx-stroke: black;
-fx-stroke-width: 0.3;
}
.table-view .column-header .label{
-fx-alignment: CENTER_LEFT;
-fx-font-weight: none;
}
.table-row-cell > .defaultText-column {
-fx-text-fill: black;
-fx-background-insets: 0 0 1 0px;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.table-row-cell > .messageToMe-column {
-fx-text-fill: green;
-fx-background-insets: 0 0 1 0px;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlightOwn-column { /*PM own message*/
-fx-background-color: #00ffff;
-fx-background-insets: 0 0 1 0px;
-fx-text-fill: black;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight30-column { /*PM for 30 sec: works*/
-fx-text-fill: black;
-fx-background-color: #33cc33;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight60-column { /*PM for 30 sec: works*/
-fx-text-fill: black;
-fx-background-color: #40bf40;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight90-column { /*PM for 30 sec: works*/
-fx-text-fill: black;
-fx-background-color: #4db34d;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight120-column { /*PM for 30 sec: works*/
-fx-text-fill: black;
-fx-background-color: #59a659;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight180-column { /*PM for 30 sec: works*/
-fx-text-fill: black;
-fx-background-color: #669966;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight300-column { /*PM for 30 sec: works*/
-fx-text-fill: black;
-fx-background-color: #738c73;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.table-cell-bold {
-fx-font-weight: bold;
}
.table-cell-inAngleAndRange {
-fx-text-fill: green;
-fx-font-weight: bold;
}
.table-cell-100PercentAP { /*GEHT*/
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/;
-fx-font-weight: bold;
}
.table-cell-75PercentAP { /*GEHT*/
-fx-text-fill: #fa6666;
-fx-font-weight: bold;
}
.table-cell-50PercentAP {
-fx-text-fill: #fa9f66;
-fx-font-weight: bold;
}

View File

@@ -0,0 +1,243 @@
.root {
-fx-accent: #1e74c6;
-fx-focus-color: -fx-accent;
-fx-base: #373e43;
-fx-control-inner-background: derive(-fx-base, 35%);
-fx-control-inner-background-alt: -fx-control-inner-background ;
}
.label{
-fx-text-fill: lightgray;
}
.label-callSignChatCatDescriptor {
-fx-font-family: "Arial";
-fx-font-size: 18px;
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-alignment: center;
}
.text-field {
-fx-prompt-text-fill: gray;
}
.text-field .text {
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-stroke: green;
-fx-stroke-width: 0.2;
-fx-font-size: 25px;
}
.text-input-MYQRG1 {
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/
-fx-font-weight: 300;
-fx-padding: 1,1,1,1;
}
.titulo{
-fx-font-weight: bold;
-fx-font-size: 18px;
}
.button{
-fx-focus-traversable: false;
}
.button:hover{
-fx-text-fill: white;
-fx-border-color: #ff7777;
}
.separator *.line {
-fx-background-color: #3C3C3C;
-fx-border-style: solid;
-fx-border-width: 1px;
}
.scroll-bar{
-fx-background-color: derive(-fx-base,45%)
}
.button:default {
-fx-base: -fx-accent ;
-fx-font-weight: bold;
}
.button:pressed {
-fx-border-color: #ff0000;
}
.button:hover {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: #395306;
}
.buttonMyQrg1 {
-fx-background-color: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: #395306;
}
.btn-showstate-enabled-default {
-fx-base: #373e43;
-fx-text-fill: lightgray;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-default:hover {
-fx-base: #373e43;
-fx-text-fill: black;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-furtherInfo {
-fx-base: #373e43;
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-enabled-furtherInfo:hover {
-fx-base: #373e43;
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, green 0%, lightgreen 100%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.btn-showstate-disabled {
-fx-base: #373e43 ;
-fx-font-weight: bold;
-fx-text-fill: red;
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
}
.toggle-button:selected {
-fx-background-color:linear-gradient(#f0ff35, #a9ff00),
radial-gradient(center 50% -40%, radius 200%, #b8ee36 45%, #80c800 50%);
-fx-background-radius: 6, 5;
-fx-background-insets: 0, 1;
-fx-effect: dropshadow( three-pass-box , rgba(0,0,0,0.4) , 5, 0.0 , 0 , 1 );
-fx-text-fill: #395306;
}
.table-view .column-header {
-fx-background-color: linear-gradient(to right, #373838, #373838);
}
.table-view .column-header .text {
-fx-fill: linear-gradient(from 0% 0% to 100% 200%, repeat, green 0%, lightgreen 50%);
-fx-stroke: green;
-fx-stroke-width: 0.2;
}
.table-view{
/*-fx-background-color: derive(-fx-base, 10%);*/
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.table-view .column-header .label{
-fx-alignment: CENTER_LEFT;
-fx-font-weight: none;
}
.table-row-cell > .defaultText-column {
-fx-text-fill: white;
-fx-background-insets: 0 0 1 0px;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.table-row-cell > .messageToMe-column {
-fx-text-fill: lightgreen;
-fx-background-insets: 0 0 1 0px;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlightOwn-column { /*PM own message*/
-fx-background-color: #4674b9;
-fx-background-insets: 0 0 1 0px;
-fx-text-fill: white;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight30-column { /*PM for 30 sec: works*/
-fx-text-fill: white;
-fx-background-color: #33cc33;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight60-column { /*PM for 30 sec: works*/
-fx-text-fill: white;
-fx-background-color: #40bf40;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight90-column { /*PM for 30 sec: works*/
-fx-text-fill: white;
-fx-background-color: #4db34d;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight120-column { /*PM for 30 sec: works*/
-fx-text-fill: white;
-fx-background-color: #59a659;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight180-column { /*PM for 30 sec: works*/
-fx-text-fill: white;
-fx-background-color: #669966;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.messageHighlight300-column { /*PM for 30 sec: works*/
-fx-text-fill: white;
-fx-background-color: #738c73;
-fx-selection-bar-non-focused: derive(-fx-base, 50%);
}
.table-cell-bold {
-fx-font-weight: bold;
}
.table-cell-inAngleAndRange {
-fx-text-fill: lightgreen;
-fx-font-weight: bold;
}
.table-cell-100PercentAP { /*GEHT*/
-fx-text-fill: linear-gradient(from 0% 0% to 100% 200%, #f98aff 0%, #f98aff 100%); /*purple*/;
-fx-font-weight: bold;
}
.table-cell-75PercentAP { /*GEHT*/
-fx-text-fill: #fa6666;
-fx-font-weight: bold;
}
.table-cell-50PercentAP {
-fx-text-fill: #fa9f66;
-fx-font-weight: bold;
}

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More