6 Commits

Author SHA1 Message Date
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
58 changed files with 29571 additions and 2567 deletions

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

@@ -0,0 +1,145 @@
name: Nightly Runtime Artifacts
on:
push:
branches:
- main
schedule:
- cron: "20 2 * * *"
workflow_dispatch:
jobs:
build-windows-zip:
name: Build Windows ZIP Artifact
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract version from pom.xml
shell: pwsh
run: |
$xml = [xml](Get-Content pom.xml)
$version = $xml.project.version
Add-Content -Path $env:GITHUB_ENV -Value "VERSION=$version"
- name: Set up Java 17
uses: actions/setup-java@v4
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-${{ env.VERSION }}-windows-x64.zip -Force
- name: Upload Windows ZIP artifact
uses: actions/upload-artifact@v4
with:
name: nightly-windows-zip
path: dist/praktiKST-${{ env.VERSION }}-windows-x64.zip
retention-days: 14
build-linux-appimage:
name: Build Linux AppImage Artifact
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Extract version from pom.xml
run: |
VERSION=$(grep -m1 '<version>' pom.xml | sed 's/.*<version>\(.*\)<\/version>.*/\1/')
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Set up Java 17
uses: actions/setup-java@v4
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-${{ env.VERSION }}-linux-x86_64.AppImage
- name: Upload Linux AppImage artifact
uses: actions/upload-artifact@v4
with:
name: nightly-linux-appimage
path: dist/praktiKST-${{ env.VERSION }}-linux-x86_64.AppImage
retention-days: 14

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

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

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

@@ -0,0 +1,164 @@
name: Tagged Release Build
on:
push:
tags:
- "*"
workflow_dispatch:
permissions:
contents: write
jobs:
build-windows-zip:
name: Build Windows ZIP
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
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
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
- name: Set up Java 17
uses: actions/setup-java@v4
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
with:
name: linux-appimage
path: dist/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage
release-tag:
name: Publish Tagged Release
runs-on: ubuntu-latest
needs:
- build-windows-zip
- build-linux-appimage
steps:
- name: Download Windows artifact
uses: actions/download-artifact@v4
with:
name: windows-zip
path: release-assets/windows
- name: Download Linux artifact
uses: actions/download-artifact@v4
with:
name: linux-appimage
path: release-assets/linux
- name: Create tagged release
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
name: Release ${{ github.ref_name }}
allowUpdates: false
replacesArtifacts: false
makeLatest: true
generateReleaseNotes: true
artifacts: release-assets/windows/praktiKST-${{ github.ref_name }}-windows-x64.zip,release-assets/linux/praktiKST-${{ github.ref_name }}-linux-x86_64.AppImage

16
.gitignore vendored
View File

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

View File

@@ -1,4 +1,2 @@
do5sa
##12390780900ß9'++++2e0NEY#####
on8tt
dl2rmc
dr2x
oe3cin

15832
bugsept24.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
<groupId>de.x08</groupId>
<artifactId>praktiKST</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.40.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,6 +1,17 @@
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.
*/
@@ -9,15 +20,35 @@ public class ApplicationConstants {
/**
* Name of file to store preferences in.
*/
public static final double APPLICATION_CURRENTVERSIONNUMBER = 1.17;
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"; //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 = "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,24 +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 = client.getChatPreferences().getAirScout_asCommunicationPort();
// System.out.println("<<<<<<<<<<<<<<<<<<<<ASPERI: " + port);
int port = client.getChatPreferences().getAirScout_asCommunicationPort();
// byte[] message = "ASSETPATH: \"KST\" \"AS\" 1440000,DO5AMF,JN49GL,OK1MZM,JN89IW ".getBytes(); Original, ging
InetAddress address;
@@ -62,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>
@@ -185,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());

View File

@@ -0,0 +1,388 @@
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";
}
String formattedQRG = String.format(Locale.US, "%.1f", freqFloat);
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,6 +2,7 @@ package kst4contest.controller;
import java.io.*;
import java.net.*;
import java.util.ArrayList;
import java.util.Comparator;
import javafx.collections.FXCollections;
@@ -10,6 +11,7 @@ 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
@@ -24,15 +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;
@@ -54,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");
@@ -128,26 +137,30 @@ public class ReadUDPbyAirScoutMessageThread extends Thread {
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
@@ -158,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) {
@@ -167,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);
}
}

View File

@@ -3,6 +3,8 @@ 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;
@@ -10,6 +12,8 @@ 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;
@@ -30,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
@@ -46,6 +56,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
if (this.socket != null) {
System.out.println(">>>>>>>>>>>>>>ReadUdpbyUCS: closing socket");
terminateConnection();
// callBackToController.onThreadStatus("UDPReceiver", new String[]);
}
} catch (Exception e) {
// TODO Auto-generated catch block
@@ -55,17 +66,22 @@ public class ReadUDPbyUCXMessageThread extends Thread {
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 = new DatagramSocket(12060);
socket = new DatagramSocket(udpPortNr);
socket.setSoTimeout(2000); //TODO try for end properly
}
@@ -97,8 +113,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
nE.printStackTrace();
System.out.println("ReadUdpByUCXTH: Socket not ready");
try {
socket = new DatagramSocket(client.getChatPreferences().getLogsynch_ucxUDPWkdCallListenerPort());
socket.setSoTimeout(2000);
@@ -134,6 +148,12 @@ public class ReadUDPbyUCXMessageThread extends Thread {
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;
}
@@ -161,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 {
@@ -194,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);
@@ -234,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);
}
@@ -255,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");
@@ -336,7 +475,6 @@ public class ReadUDPbyUCXMessageThread extends Thread {
fileWriterPersistUDPToFile = new FileWriter(logUDPMessageToThisFile, true);
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
@@ -412,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);
}
}
@@ -424,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());
@@ -436,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

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

@@ -178,7 +178,7 @@ public class UpdateChecker {
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).getTextContent() + " <<<<<<<<<<<<<<<<<< " + i + " / " + childNodeCounter);
// System.out.println(element.getChildNodes().item(i).getNodeName());
aChangeLogEntry[childNodeCounter] = aChangeLogEntry[childNodeCounter] + element.getChildNodes().item(i).getTextContent();
childNodeCounter++;

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);
@@ -71,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

@@ -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,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

@@ -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,16 +1,29 @@
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
@@ -19,7 +32,7 @@ public class ChatMember {
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 activityTimeLastInEpoch; // time of last activity in epochtimesec
@@ -39,6 +52,12 @@ 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
@@ -52,8 +71,36 @@ public class ChatMember {
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;
}
@@ -262,8 +309,129 @@ public class ChatMember {
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() {
@@ -305,10 +473,71 @@ public class ChatMember {
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
@@ -317,13 +546,20 @@ 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
@@ -345,9 +581,9 @@ public class ChatMember {
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;
}
@@ -366,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

@@ -3,7 +3,7 @@ package kst4contest.model;
import java.util.ArrayList;
public class UpdateInformation {
double latestVersionNumberOnServer = 100; //dummy value to prevent nullpointerexc
double latestVersionNumberOnServer = 1.26; //dummy value to prevent nullpointerexc
String adminMessage ="";
String majorChanges ="";
String latestVersionPathOnWebserver="";

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

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

@@ -3,6 +3,7 @@ module praktiKST {
requires jdk.xml.dom;
requires java.sql;
requires javafx.media;
exports kst4contest.controller.interfaces;
exports kst4contest.controller;
exports kst4contest.locatorUtils;
exports kst4contest.model;

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.

View File

@@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<praktiKST>
<station>
<LoginCallSign>DO5AMF</LoginCallSign>
<LoginPassword>changeMe</LoginPassword>
<LoginDisplayedName>KST4Contest</LoginDisplayedName>
<LoginLocator>JO51IJ</LoginLocator>
<ChatCategory>2</ChatCategory>
<stn_antennaBeamWidthDeg>50.0</stn_antennaBeamWidthDeg>
<stn_maxQRBDefault>900.0</stn_maxQRBDefault>
<stn_qtfDefault>135.0</stn_qtfDefault>
<stn_bandActive144>true</stn_bandActive144>
<stn_bandActive432>true</stn_bandActive432>
<stn_bandActive1240>false</stn_bandActive1240>
<stn_bandActive2300>false</stn_bandActive2300>
<stn_bandActive3400>false</stn_bandActive3400>
<stn_bandActive5600>false</stn_bandActive5600>
<stn_bandActive10G>false</stn_bandActive10G>
</station>
<logsynch>
<logsynch_fileBasedWkdCallInterpreterFileNameReadOnly>SimpleLogFile.txt</logsynch_fileBasedWkdCallInterpreterFileNameReadOnly>
<logsynch_storeWorkedCallSignsFileNameUDPMessageBackup>udpReaderBackup.txt</logsynch_storeWorkedCallSignsFileNameUDPMessageBackup>
<logsynch_fileBasedWkdCallInterpreterEnabled>true</logsynch_fileBasedWkdCallInterpreterEnabled>
<logsynch_ucxUDPWkdCallListenerPort>12060</logsynch_ucxUDPWkdCallListenerPort>
<logsynch_ucxUDPWkdCallListenerEnabled>true</logsynch_ucxUDPWkdCallListenerEnabled>
</logsynch>
<trxSynchUCX>
<trxSynch_ucxLogUDPListenerEnabled>true</trxSynch_ucxLogUDPListenerEnabled>
<trxSynch_defaultMYQRGValue>144.374.50</trxSynch_defaultMYQRGValue>
</trxSynchUCX>
<AirScoutQuerier>
<asQry_airScoutCommunicationEnabled>true</asQry_airScoutCommunicationEnabled>
<asQry_airScoutServerName>KST</asQry_airScoutServerName>
<asQry_airScoutClientName>KST</asQry_airScoutClientName>
<asQry_airScoutUDPPort>9872</asQry_airScoutUDPPort>
<asQry_airScoutBandValue>1440000</asQry_airScoutBandValue>
</AirScoutQuerier>
<notifications>
<notify_SimpleAudioNotificationsEnabled>true</notify_SimpleAudioNotificationsEnabled>
<notify_CWCallsignAudioNotificationsEnabled>true</notify_CWCallsignAudioNotificationsEnabled>
<notify_VoiceCallsignAudioNotificationsEnabled>true</notify_VoiceCallsignAudioNotificationsEnabled>
</notifications>
<shortCuts>
<t>Hi OM,</t>
<t>pse</t>
<t>turn</t>
<t>ant</t>
<t>my</t>
<t>dir</t>
<t>sked</t>
<t>ssb</t>
<t>cw</t>
<t>try</t>
<t>agn</t>
<t>nw</t>
<t>qrg</t>
<t>beaming</t>
<t>calling</t>
<t>lsn to</t>
<t>qsb</t>
<t>rpt</t>
<t>nr</t>
<t>ur</t>
<t>I</t>
<t>hear</t>
<t>you</t>
<t>weak</t>
<t>nil, sry</t>
<t>maybe</t>
<t>later</t>
<t>tmw</t>
<t>rrr</t>
<t>tnx</t>
<t>qso</t>
<t>73</t>
<t>?</t>
<t>!</t>
<t>,</t>
<t>/SETNAME MYQRG</t>
<t>MYQRG</t>
</shortCuts>
<textSnippets>
<t>Hi QRZNAME, try sked 2m? Ur QRG?</t>
<t>try? FIRSTAP, pse lsn MYQRGSHORT</t>
<t>maybe we need AP. FIRSTAP SECONDAP</t>
<t>I am calling cq to ur dir, pse lsn to MYLOCATOR at MYQRG</t>
<t>pse ur qrg?</t>
<t>rrr, I move to your qrg nw, pse ant dir MYLOCATOR</t>
<t>Hrd you but many qrm here, pse agn</t>
<t>I turn my ant to you now</t>
<t>Sry, strong qrm by local station, may try MYQRG</t>
<t>Sry, in qso nw, pse qrx, I will meep you</t>
<t>Ur ant my dir MYLOCATOR nw?</t>
<t>QRZNAME pse ant dir MYLOCATORSHORT</t>
<t>No cw op here, pse can we use ssb?</t>
<t>No chance in ssb, can we use cw?</t>
<t>Nil till now, are you calling?</t>
<t>Nil here, tnx try, maybe later!</t>
<t>Nil, I will look for an ap and meep you then</t>
<t>There will be an AP in </t>
<t>Tnx fb qso, all ok, 73 es gl!</t>
</textSnippets>
<beaconCQ>
<beaconCQText>_.~'"´"'~.__.~'´ CQ CQ CQ de MYCALL, pse call us at MYQRG `'~.__.~'"`"'~._</beaconCQText>
<beaconCQIntervalMinutes>5</beaconCQIntervalMinutes>
<beaconCQEnabled>false</beaconCQEnabled>
</beaconCQ>
<beaconUnworkedstations>
<beaconUnworkedstationsText>Hi OM, pse Sked at MYQRG or your QRG</beaconUnworkedstationsText>
<beaconUnworkedstationsIntervalMinutes>20</beaconUnworkedstationsIntervalMinutes>
<beaconUnworkedstationsEnabled>false</beaconUnworkedstationsEnabled>
<beaconUnworkedstationsPrefix>OE</beaconUnworkedstationsPrefix>
</beaconUnworkedstations>
<guiOptions>
<GUIscn_ChatwindowMainSceneSizeHW>748.0;1544.800048828125</GUIscn_ChatwindowMainSceneSizeHW>
<GUIclusterAndQSOMonStage_SceneSizeHW>765.5999755859375;467.20001220703125</GUIclusterAndQSOMonStage_SceneSizeHW>
<GUIstage_updateStage_SceneSizeHW>640.0;480.0</GUIstage_updateStage_SceneSizeHW>
<GUIsettingsStageSceneSizeHW>720.0;768.0</GUIsettingsStageSceneSizeHW>
<GUIselectedCallSignSplitPane_dividerposition>0.5120192307692308</GUIselectedCallSignSplitPane_dividerposition>
<GUImainWindowLeftSplitPane_dividerposition>0.5046632124352332</GUImainWindowLeftSplitPane_dividerposition>
<GUImessageSectionSplitpane_dividerposition>0.5102549889135255;0.5934035476718403;0.6377494456762749</GUImessageSectionSplitpane_dividerposition>
<GUImainWindowRightSplitPane_dividerposition>0.74029933481153</GUImainWindowRightSplitPane_dividerposition>
<GUIpnl_directedMSGWin_dividerpositionDefault>0.09707903525548366</GUIpnl_directedMSGWin_dividerpositionDefault>
</guiOptions>
</praktiKST>

View File

@@ -2,13 +2,23 @@
<praktiKST>
<station>
<LoginCallSign>DO5AMF</LoginCallSign>
<LoginPassword>kst4contest.test</LoginPassword>
<LoginDisplayedName>Marc</LoginDisplayedName>
<LoginLocator>JN49GL</LoginLocator>
<LoginPassword>changeMe</LoginPassword>
<LoginDisplayedName>KST4Contest1263</LoginDisplayedName>
<stn_loginNameSecondCat>KST4Contest1263</stn_loginNameSecondCat>
<LoginLocator>JO54ME</LoginLocator>
<ChatCategory>2</ChatCategory>
<stn_antennaBeamWidthDeg>50</stn_antennaBeamWidthDeg>
<stn_maxQRBDefault>900</stn_maxQRBDefault>
<stn_qtfDefault>135</stn_qtfDefault>
<ChatCategorySecond>3</ChatCategorySecond>
<stn_secondCatEnabled>true</stn_secondCatEnabled>
<stn_antennaBeamWidthDeg>60.0</stn_antennaBeamWidthDeg>
<stn_maxQRBDefault>1333.0</stn_maxQRBDefault>
<stn_qtfDefault>135.0</stn_qtfDefault>
<stn_bandActive144>true</stn_bandActive144>
<stn_bandActive432>true</stn_bandActive432>
<stn_bandActive1240>true</stn_bandActive1240>
<stn_bandActive2300>false</stn_bandActive2300>
<stn_bandActive3400>false</stn_bandActive3400>
<stn_bandActive5600>false</stn_bandActive5600>
<stn_bandActive10G>false</stn_bandActive10G>
</station>
<logsynch>
<logsynch_fileBasedWkdCallInterpreterFileNameReadOnly>SimpleLogFile.txt</logsynch_fileBasedWkdCallInterpreterFileNameReadOnly>
@@ -19,21 +29,30 @@
</logsynch>
<trxSynchUCX>
<trxSynch_ucxLogUDPListenerEnabled>true</trxSynch_ucxLogUDPListenerEnabled>
<trxSynch_defaultMYQRGValue>144.010.00</trxSynch_defaultMYQRGValue>
<trxSynch_defaultMYQRGValue>144.123.00</trxSynch_defaultMYQRGValue>
</trxSynchUCX>
<AirScoutQuerier>
<asQry_airScoutCommunicationEnabled>true</asQry_airScoutCommunicationEnabled>
<asQry_airScoutServerName>KST</asQry_airScoutServerName>
<asQry_airScoutServerName>AS</asQry_airScoutServerName>
<asQry_airScoutClientName>KST</asQry_airScoutClientName>
<asQry_airScoutUDPPort>9872</asQry_airScoutUDPPort>
<asQry_airScoutBandValue>1440000</asQry_airScoutBandValue>
</AirScoutQuerier>
<notifications>
<notify_SimpleAudioNotificationsEnabled>true</notify_SimpleAudioNotificationsEnabled>
<notify_CWCallsignAudioNotificationsEnabled>true</notify_CWCallsignAudioNotificationsEnabled>
<notify_VoiceCallsignAudioNotificationsEnabled>true</notify_VoiceCallsignAudioNotificationsEnabled>
<notify_dxClusterServerEnabled>true</notify_dxClusterServerEnabled>
<notify_DXClusterServerTriggerBearing>false</notify_DXClusterServerTriggerBearing>
<notify_DXClusterServerTriggerOnQRGDetect>false</notify_DXClusterServerTriggerOnQRGDetect>
<notify_dxclusterServerPort>8000</notify_dxclusterServerPort>
<notify_optionalFrequencyPrefix>432</notify_optionalFrequencyPrefix>
<notify_DXCSrv_SpottersCallSign>DO5AMF</notify_DXCSrv_SpottersCallSign>
</notifications>
<shortCuts>
<t>Hi OM,</t>
<t>pse</t>
<t>Hi OM, try Sked? FIRSTAP</t>
<t>TNX qso, 73!</t>
<t>turn</t>
<t>ant</t>
<t>my</t>
<t>dir</t>
<t>sked</t>
<t>ssb</t>
@@ -66,11 +85,12 @@
<t>,</t>
<t>/SETNAME MYQRG</t>
<t>MYQRG</t>
<t>SECONDQRG</t>
</shortCuts>
<textSnippets>
<t>Hi OM, try sked 2m? Ur QRG?</t>
<t>Hi OM, try sked 70cms? Ur QRG?</t>
<t>Hi OM, try sked 23cms? Ur QRG?</t>
<t>Hi QRZNAME, try sked 2m? Ur QRG?</t>
<t>try? FIRSTAP, pse lsn MYQRGSHORT</t>
<t>maybe we need AP. FIRSTAP SECONDAP</t>
<t>I am calling cq to ur dir, pse lsn to MYLOCATOR at MYQRG</t>
<t>pse ur qrg?</t>
<t>rrr, I move to your qrg nw, pse ant dir MYLOCATOR</t>
@@ -79,7 +99,7 @@
<t>Sry, strong qrm by local station, may try MYQRG</t>
<t>Sry, in qso nw, pse qrx, I will meep you</t>
<t>Ur ant my dir MYLOCATOR nw?</t>
<t>nil?</t>
<t>QRZNAME pse ant dir MYLOCATORSHORT</t>
<t>No cw op here, pse can we use ssb?</t>
<t>No chance in ssb, can we use cw?</t>
<t>Nil till now, are you calling?</t>
@@ -89,9 +109,12 @@
<t>Tnx fb qso, all ok, 73 es gl!</t>
</textSnippets>
<beaconCQ>
<beaconCQText>_.~'"´"'~.__.~'´ CQ CQ CQ de MYCALL, pse call us at MYQRG `'~.__.~'"`"'~._</beaconCQText>
<beaconCQIntervalMinutes>5</beaconCQIntervalMinutes>
<beaconCQText>_.~'"´"'~.__.~'´ CQ CQ CQ NAC de MYCALL, pse try 144.317 for JO54 `'~.__.~'"`"'~._</beaconCQText>
<beaconCQIntervalMinutes>1</beaconCQIntervalMinutes>
<beaconCQEnabled>false</beaconCQEnabled>
<beaconCQTextSecondText>_.~'"´"'~.__.~'´ CQ CQ CQ de MYCALL, pse sked or SECONDQRG `'~.__.~'"`"'~._</beaconCQTextSecondText>
<beaconCQIntervalMinutesSecondCat>3</beaconCQIntervalMinutesSecondCat>
<beaconCQEnabledSecondCat>false</beaconCQEnabledSecondCat>
</beaconCQ>
<beaconUnworkedstations>
<beaconUnworkedstationsText>Hi OM, pse Sked at MYQRG or your QRG</beaconUnworkedstationsText>
@@ -99,4 +122,28 @@
<beaconUnworkedstationsEnabled>false</beaconUnworkedstationsEnabled>
<beaconUnworkedstationsPrefix>OE</beaconUnworkedstationsPrefix>
</beaconUnworkedstations>
</praktiKST>
<messageHandling>
<autoAnswerText>Hi, sry I am not qrv, just testing new features of KST4Contest 1.25</autoAnswerText>
<autoAnswerEnabled>false</autoAnswerEnabled>
<autoAnswerTextSecondCat>Hi, sry I am not qrv, just testing new features of KST4Contest 1.251</autoAnswerTextSecondCat>
<autoAnswerEnabledSecondCat>false</autoAnswerEnabledSecondCat>
<autoAnswerToQrgRequestEnabled>true</autoAnswerToQrgRequestEnabled>
</messageHandling>
<guiSaveableOptions>
<guiOptions_defaultFilterNothing>false</guiOptions_defaultFilterNothing>
<guiOptions_defaultFilterPmToMe>true</guiOptions_defaultFilterPmToMe>
<guiOptions_defaultFilterPmToOther>false</guiOptions_defaultFilterPmToOther>
<guiOptions_defaultFilterPublicMsgs>false</guiOptions_defaultFilterPublicMsgs>
</guiSaveableOptions>
<guiOptions>
<GUIscn_ChatwindowMainSceneSizeHW>1080.800048828125;2048.0</GUIscn_ChatwindowMainSceneSizeHW>
<GUIclusterAndQSOMonStage_SceneSizeHW>1196.800048828125;259.20001220703125</GUIclusterAndQSOMonStage_SceneSizeHW>
<GUIstage_updateStage_SceneSizeHW>640.0;480.0</GUIstage_updateStage_SceneSizeHW>
<GUIsettingsStageSceneSizeHW>720.0;768.0</GUIsettingsStageSceneSizeHW>
<GUIselectedCallSignSplitPane_dividerposition>0.5154494382022472</GUIselectedCallSignSplitPane_dividerposition>
<GUImainWindowLeftSplitPane_dividerposition>0.45846364347146207</GUImainWindowLeftSplitPane_dividerposition>
<GUImessageSectionSplitpane_dividerposition>0.5284522003034902;0.5853566009104704;0.6157056145675266</GUImessageSectionSplitpane_dividerposition>
<GUImainWindowRightSplitPane_dividerposition>0.5749241274658573</GUImainWindowRightSplitPane_dividerposition>
<GUIpnl_directedMSGWin_dividerpositionDefault>0.1754658301944221</GUIpnl_directedMSGWin_dividerpositionDefault>
</guiOptions>
</praktiKST>

View File

@@ -8,8 +8,8 @@ public class TestReadUDPASListenerThread {
@Test
public static void main(String[] args) {
ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
asUDPReader.start();
// ReadUDPbyAirScoutMessageThread asUDPReader = new ReadUDPbyAirScoutMessageThread(9872, null, "AS", "KST");
// asUDPReader.start();
String testThis;

View File

@@ -8,8 +8,8 @@ public class TestReadUDPUCXListenerThread {
@Test
public static void main(String[] args) {
ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
ucxUDPReader.start();
// ReadUDPbyUCXMessageThread ucxUDPReader = new ReadUDPbyUCXMessageThread(12060);
// ucxUDPReader.start();
String testThis;

File diff suppressed because it is too large Load Diff