mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-08-24 19:17:07 +02:00
fix(wintest): add band-aware sked QRG resolution, preserve portable callsigns, strip KST suffixes and correct ADDSKED timestamps
This commit is contained in:
@@ -318,6 +318,85 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses a useful initial band for a new sked.
|
||||||
|
*
|
||||||
|
* <p>Recent frequency evidence has priority over station-name information.
|
||||||
|
* Manual NOT-QRV exclusions are respected. The operator can still select
|
||||||
|
* another locally enabled band from the dropdown.</p>
|
||||||
|
*/
|
||||||
|
private Band resolveDefaultSkedBand(ChatMember selectedMember,
|
||||||
|
EnumSet<Band> enabledBands) {
|
||||||
|
|
||||||
|
if (selectedMember == null || enabledBands == null || enabledBands.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChatMember> variants =
|
||||||
|
chatcontroller.findActiveChatMembersByRawCall(
|
||||||
|
selectedMember.getCallSignRaw()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (variants.isEmpty()) {
|
||||||
|
variants = List.of(selectedMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
BandOpportunityResolver.Resolution resolution =
|
||||||
|
BandOpportunityResolver.resolve(
|
||||||
|
variants,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
|
||||||
|
EnumSet<Band> availableBands = resolution.getAvailableBands();
|
||||||
|
availableBands.retainAll(enabledBands);
|
||||||
|
|
||||||
|
Band newestFrequencyBand = null;
|
||||||
|
long newestTimestamp = Long.MIN_VALUE;
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
for (ChatMember member : variants) {
|
||||||
|
if (member == null || member.getKnownActiveBands() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<Band, ChatMember.ActiveFrequencyInfo> entry
|
||||||
|
: member.getKnownActiveBands().entrySet()) {
|
||||||
|
|
||||||
|
Band band = entry.getKey();
|
||||||
|
ChatMember.ActiveFrequencyInfo info = entry.getValue();
|
||||||
|
|
||||||
|
if (band == null
|
||||||
|
|| info == null
|
||||||
|
|| !availableBands.contains(band)
|
||||||
|
|| !band.isPlausible(info.frequency)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long ageMs = now - info.timestampEpoch;
|
||||||
|
if (ageMs < 0L
|
||||||
|
|| ageMs > BandOpportunityResolver.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.timestampEpoch > newestTimestamp) {
|
||||||
|
newestTimestamp = info.timestampEpoch;
|
||||||
|
newestFrequencyBand = band;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newestFrequencyBand != null) {
|
||||||
|
return newestFrequencyBand;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!availableBands.isEmpty()) {
|
||||||
|
return availableBands.iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledBands.iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public void stopRotator() {
|
public void stopRotator() {
|
||||||
if (rotatorClient != null) {
|
if (rotatorClient != null) {
|
||||||
@@ -773,24 +852,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
|
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
|
||||||
new java.util.concurrent.ConcurrentHashMap<>();
|
new java.util.concurrent.ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Tracks the last time WE sent a message containing a QRG to a specific callsign (UPPERCASE).
|
|
||||||
* Compared against knownActiveBands.timestampEpoch to decide whose QRG to use in a SKED. */
|
|
||||||
private final Map<String, Long> lastSentQRGToCallsign =
|
|
||||||
new java.util.concurrent.ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/** Call this whenever we send a PM to {@code receiverCallsign} that contains our QRG. */
|
|
||||||
public void recordOutboundQRG(String receiverCallsign) {
|
|
||||||
if (receiverCallsign == null) return;
|
|
||||||
lastSentQRGToCallsign.put(receiverCallsign.trim().toUpperCase(), System.currentTimeMillis());
|
|
||||||
System.out.println("[ChatController] Recorded outbound QRG to: " + receiverCallsign);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns epoch-ms of when we last sent our QRG to this callsign, or 0 if never. */
|
|
||||||
public long getLastSentQRGTimestamp(String callsign) {
|
|
||||||
if (callsign == null) return 0L;
|
|
||||||
return lastSentQRGToCallsign.getOrDefault(callsign.trim().toUpperCase(), 0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
|
private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
|
||||||
private ScheduledExecutorService scoreScheduler;
|
private ScheduledExecutorService scoreScheduler;
|
||||||
private final StationMetricsService stationMetricsService = new StationMetricsService();
|
private final StationMetricsService stationMetricsService = new StationMetricsService();
|
||||||
@@ -814,158 +875,414 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pushes a sked to Win-Test via UDP broadcast (LOCKSKED / ADDSKED / UNLOCKSKED).
|
* Pushes a sked to Win-Test via UDP broadcast.
|
||||||
* Runs on a background thread to avoid blocking the UI.
|
*
|
||||||
|
* <p>The internal sked is independent from this handover. If no frequency can
|
||||||
|
* be resolved safely for the selected band, the internal sked remains active,
|
||||||
|
* but no misleading Win-Test entry is created.</p>
|
||||||
*/
|
*/
|
||||||
private void pushSkedToWinTest(ContestSked sked) {
|
private void pushSkedToWinTest(ContestSked sked) {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
InetAddress broadcastAddr = InetAddress.getByName(
|
Double frequencyKHz = resolveSkedFrequencyKHz(sked);
|
||||||
chatPreferences.getLogsynch_wintestNetworkBroadcastAddress());
|
|
||||||
int port = chatPreferences.getLogsynch_wintestNetworkPort();
|
|
||||||
String stationName = chatPreferences.getLogsynch_wintestNetworkStationNameOfKST();
|
|
||||||
|
|
||||||
WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this);
|
if (frequencyKHz == null) {
|
||||||
|
reportSkippedWinTestSked(
|
||||||
// Frequency resolution:
|
sked,
|
||||||
// Compare WHO sent a QRG most recently in the PM conversation:
|
"no recent or configured QRG matches "
|
||||||
// - OM sent their QRG last → use OM's Last Known QRG (ChatMember.frequency)
|
+ sked.getBand().getDisplayLabel()
|
||||||
// - WE sent our QRG last → use our own Win-Test QRG (MYQRG)
|
);
|
||||||
// Fallback chain if no timestamps exist: OM's Last Known QRG → hardcoded default
|
return;
|
||||||
double freqKHz = -1.0;
|
|
||||||
final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes
|
|
||||||
|
|
||||||
ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign());
|
|
||||||
|
|
||||||
// Collect timestamps: when did the OM last mention their QRG? When did WE last send ours?
|
|
||||||
long omLastQRGTimestamp = 0L;
|
|
||||||
double omLastQRGMhz = 0.0;
|
|
||||||
if (targetMember != null && sked.getBand() != null) {
|
|
||||||
ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand());
|
|
||||||
if (fi != null && fi.frequency > 0
|
|
||||||
&& (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) {
|
|
||||||
omLastQRGTimestamp = fi.timestampEpoch;
|
|
||||||
omLastQRGMhz = fi.frequency;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
long ourLastQRGTimestamp = getLastSentQRGTimestamp(sked.getTargetCallsign());
|
|
||||||
|
|
||||||
// Decision: who was more recent?
|
|
||||||
if (omLastQRGTimestamp > 0 && omLastQRGTimestamp >= ourLastQRGTimestamp) {
|
|
||||||
// OM mentioned their QRG MORE RECENTLY (or at same time) → use their QRG
|
|
||||||
freqKHz = omLastQRGMhz * 1000.0;
|
|
||||||
System.out.println("[ChatController] SKED freq: OM sent last → "
|
|
||||||
+ omLastQRGMhz + " MHz → " + freqKHz + " kHz");
|
|
||||||
|
|
||||||
} else if (ourLastQRGTimestamp > 0) {
|
|
||||||
// WE sent our QRG more recently → use our Win-Test QRG
|
|
||||||
try {
|
|
||||||
String qrgStr = chatPreferences.getMYQRGFirstCat().get();
|
|
||||||
if (qrgStr != null && !qrgStr.isBlank()) {
|
|
||||||
String cleaned = qrgStr.trim().replace(".", "");
|
|
||||||
double parsed = Double.parseDouble(cleaned) / 100.0;
|
|
||||||
if (parsed > 50000) {
|
|
||||||
freqKHz = parsed;
|
|
||||||
System.out.println("[ChatController] SKED freq: WE sent last → "
|
|
||||||
+ freqKHz + " kHz (raw: " + qrgStr + ")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException ignored) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback A: OM's Last Known QRG from KST field (if no PM QRG exchange found at all)
|
String winTestCallsign =
|
||||||
if (freqKHz < 0 && targetMember != null) {
|
toWinTestSkedCallsign(
|
||||||
try {
|
sked.getTargetChatCallsign()
|
||||||
String memberQrg = targetMember.getFrequency().get();
|
);
|
||||||
if (memberQrg != null && !memberQrg.isBlank()) {
|
|
||||||
double mhz = Double.parseDouble(memberQrg.trim());
|
if (winTestCallsign == null || winTestCallsign.isBlank()) {
|
||||||
freqKHz = mhz * 1000.0;
|
reportSkippedWinTestSked(
|
||||||
System.out.println("[ChatController] SKED freq: fallback Last Known QRG → "
|
sked,
|
||||||
+ mhz + " MHz → " + freqKHz + " kHz");
|
"the target callsign could not be converted"
|
||||||
}
|
);
|
||||||
} catch (NumberFormatException ignored) { }
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback B: hardcoded default
|
InetAddress broadcastAddress = InetAddress.getByName(
|
||||||
if (freqKHz < 0) {
|
chatPreferences
|
||||||
freqKHz = 144300.0;
|
.getLogsynch_wintestNetworkBroadcastAddress()
|
||||||
}
|
);
|
||||||
|
|
||||||
|
int port =
|
||||||
|
chatPreferences.getLogsynch_wintestNetworkPort();
|
||||||
|
|
||||||
|
String stationName =
|
||||||
|
chatPreferences
|
||||||
|
.getLogsynch_wintestNetworkStationNameOfKST();
|
||||||
|
|
||||||
|
WinTestSkedSender sender = new WinTestSkedSender(
|
||||||
|
stationName,
|
||||||
|
broadcastAddress,
|
||||||
|
port,
|
||||||
|
this
|
||||||
|
);
|
||||||
|
|
||||||
|
String targetLocator =
|
||||||
|
resolveSkedTargetLocator(
|
||||||
|
sked.getTargetCallsign()
|
||||||
|
);
|
||||||
|
|
||||||
// Build notes string with target locator/azimuth info like reference: [JO02OB - 279°]
|
|
||||||
String targetLocator = resolveSkedTargetLocator(sked.getTargetCallsign());
|
|
||||||
String notes = "sked via KST4Contest";
|
String notes = "sked via KST4Contest";
|
||||||
if (targetLocator != null && !targetLocator.isBlank() && sked.getTargetAzimuth() > 0) {
|
|
||||||
notes = String.format("[%s - %.0f°] %s", targetLocator, sked.getTargetAzimuth(), notes);
|
if (targetLocator != null
|
||||||
} else if (targetLocator != null && !targetLocator.isBlank()) {
|
&& !targetLocator.isBlank()
|
||||||
notes = String.format("[%s] %s", targetLocator, notes);
|
&& sked.getTargetAzimuth() > 0) {
|
||||||
|
|
||||||
|
notes = String.format(
|
||||||
|
"[%s - %.0f°] %s",
|
||||||
|
targetLocator,
|
||||||
|
sked.getTargetAzimuth(),
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if (targetLocator != null
|
||||||
|
&& !targetLocator.isBlank()) {
|
||||||
|
|
||||||
|
notes = String.format(
|
||||||
|
"[%s] %s",
|
||||||
|
targetLocator,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
} else if (sked.getTargetAzimuth() > 0) {
|
} else if (sked.getTargetAzimuth() > 0) {
|
||||||
notes = String.format("[%.0f°] %s", sked.getTargetAzimuth(), notes);
|
|
||||||
|
notes = String.format(
|
||||||
|
"[%.0f°] %s",
|
||||||
|
sked.getTargetAzimuth(),
|
||||||
|
notes
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine mode: -1 = auto-detect, 0 = CW, 1 = SSB
|
String modeText =
|
||||||
String modeStr = chatPreferences.getLogsynch_wintestSkedMode();
|
chatPreferences.getLogsynch_wintestSkedMode();
|
||||||
int modeOverride = -1; // AUTO
|
|
||||||
if ("CW".equalsIgnoreCase(modeStr)) modeOverride = 0;
|
|
||||||
else if ("SSB".equalsIgnoreCase(modeStr)) modeOverride = 1;
|
|
||||||
|
|
||||||
sender.pushSkedToWinTest(sked, freqKHz, notes, modeOverride);
|
int modeOverride = -1;
|
||||||
} catch (Exception e) {
|
|
||||||
System.out.println("[ChatController] Error pushing sked to Win-Test: " + e.getMessage());
|
if ("CW".equalsIgnoreCase(modeText)) {
|
||||||
e.printStackTrace();
|
modeOverride = 0;
|
||||||
|
} else if ("SSB".equalsIgnoreCase(modeText)) {
|
||||||
|
modeOverride = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
sender.pushSkedToWinTest(
|
||||||
|
sked,
|
||||||
|
winTestCallsign,
|
||||||
|
frequencyKHz,
|
||||||
|
notes,
|
||||||
|
modeOverride
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (Exception exception) {
|
||||||
|
String message =
|
||||||
|
"Error pushing sked to Win-Test: "
|
||||||
|
+ exception.getMessage();
|
||||||
|
|
||||||
|
System.out.println(
|
||||||
|
"[ChatController] " + message
|
||||||
|
);
|
||||||
|
|
||||||
|
onThreadStatus(
|
||||||
|
"WT-SkedSend",
|
||||||
|
new ThreadStateMessage(
|
||||||
|
"WT-SkedSend",
|
||||||
|
false,
|
||||||
|
message,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
exception.printStackTrace();
|
||||||
}
|
}
|
||||||
}, "WinTestSkedPush").start();
|
}, "WinTestSkedPush").start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
/**
|
||||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
* Resolves the frequency used for the Win-Test sked.
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li>Newest QRG detected for the target station on the selected band</li>
|
||||||
|
* <li>Own QRG belonging to the target's chat category</li>
|
||||||
|
* <li>No result: do not send the Win-Test sked</li>
|
||||||
|
* </ol>
|
||||||
|
*/
|
||||||
|
private Double resolveSkedFrequencyKHz(ContestSked sked) {
|
||||||
|
if (sked == null || sked.getBand() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
Double targetFrequencyKHz =
|
||||||
return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
resolveRecentTargetFrequencyKHz(sked);
|
||||||
}
|
|
||||||
|
|
||||||
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
if (targetFrequencyKHz != null) {
|
||||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
System.out.println(
|
||||||
return null;
|
"[ChatController] SKED frequency from target: "
|
||||||
|
+ targetFrequencyKHz
|
||||||
|
+ " kHz on "
|
||||||
|
+ sked.getBand()
|
||||||
|
);
|
||||||
|
return targetFrequencyKHz;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
String ownQrg =
|
||||||
String locator = member.getQra();
|
resolveOwnQrgForSkedCategory(
|
||||||
if (locator != null && !locator.isBlank()) {
|
sked.getTargetChatCategory()
|
||||||
return locator.trim().toUpperCase(Locale.ROOT);
|
);
|
||||||
}
|
|
||||||
|
Double ownFrequencyKHz =
|
||||||
|
parseSkedFrequencyKHz(
|
||||||
|
ownQrg,
|
||||||
|
sked.getBand()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (ownFrequencyKHz != null) {
|
||||||
|
System.out.println(
|
||||||
|
"[ChatController] SKED frequency from own category QRG: "
|
||||||
|
+ ownFrequencyKHz
|
||||||
|
+ " kHz on "
|
||||||
|
+ sked.getBand()
|
||||||
|
);
|
||||||
|
return ownFrequencyKHz;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
/**
|
||||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
* Returns the newest QRG detected on the selected band across all active
|
||||||
// return null;
|
* suffix and category variants of the base callsign.
|
||||||
// }
|
*/
|
||||||
//
|
private Double resolveRecentTargetFrequencyKHz(ContestSked sked) {
|
||||||
// List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
List<ChatMember> variants =
|
||||||
// return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
findActiveChatMembersByRawCall(
|
||||||
//
|
sked.getTargetCallsign()
|
||||||
// }
|
);
|
||||||
//
|
|
||||||
// private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
long now = System.currentTimeMillis();
|
||||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
long newestTimestamp = Long.MIN_VALUE;
|
||||||
// return null;
|
Double newestFrequencyKHz = null;
|
||||||
// }
|
|
||||||
//
|
for (ChatMember member : variants) {
|
||||||
// String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
if (member == null || member.getKnownActiveBands() == null) {
|
||||||
//
|
continue;
|
||||||
// for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
}
|
||||||
// String locator = member.getQra();
|
|
||||||
// if (locator != null && !locator.isBlank()) {
|
ChatMember.ActiveFrequencyInfo frequencyInfo =
|
||||||
// return locator.trim().toUpperCase(Locale.ROOT);
|
member.getKnownActiveBands().get(
|
||||||
// }
|
sked.getBand()
|
||||||
// }
|
);
|
||||||
//
|
|
||||||
// return null;
|
if (frequencyInfo == null
|
||||||
// }
|
|| frequencyInfo.frequency <= 0.0
|
||||||
|
|| !sked.getBand().isPlausible(
|
||||||
|
frequencyInfo.frequency
|
||||||
|
)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
long ageMs = now - frequencyInfo.timestampEpoch;
|
||||||
|
|
||||||
|
if (ageMs < 0L
|
||||||
|
|| ageMs > BandOpportunityResolver
|
||||||
|
.RECENT_DYNAMIC_EVIDENCE_MAX_AGE_MS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frequencyInfo.timestampEpoch > newestTimestamp) {
|
||||||
|
newestTimestamp = frequencyInfo.timestampEpoch;
|
||||||
|
newestFrequencyKHz =
|
||||||
|
frequencyInfo.frequency * 1000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newestFrequencyKHz;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the own QRG belonging to the chat category in which the target
|
||||||
|
* station was selected.
|
||||||
|
*/
|
||||||
|
private String resolveOwnQrgForSkedCategory(
|
||||||
|
ChatCategory targetCategory) {
|
||||||
|
|
||||||
|
if (sameChatCategory(
|
||||||
|
targetCategory,
|
||||||
|
getChatCategorySecondChat())) {
|
||||||
|
|
||||||
|
return chatPreferences
|
||||||
|
.getMYQRGSecondCat()
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
return chatPreferences
|
||||||
|
.getMYQRGFirstCat()
|
||||||
|
.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameChatCategory(ChatCategory first,
|
||||||
|
ChatCategory second) {
|
||||||
|
|
||||||
|
return first != null
|
||||||
|
&& second != null
|
||||||
|
&& first.getCategoryNumber()
|
||||||
|
== second.getCategoryNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the QRG formats used by KST4Contest and Win-Test.
|
||||||
|
*
|
||||||
|
* <p>Examples: 144.300, 144.300.03, 144300 and 144300.0.</p>
|
||||||
|
*/
|
||||||
|
private Double parseSkedFrequencyKHz(String value,
|
||||||
|
Band expectedBand) {
|
||||||
|
|
||||||
|
if (value == null
|
||||||
|
|| value.isBlank()
|
||||||
|
|| expectedBand == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized = value
|
||||||
|
.trim()
|
||||||
|
.replace(',', '.')
|
||||||
|
.replaceAll("\\s+", "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.util.regex.Matcher groupedFrequency =
|
||||||
|
java.util.regex.Pattern.compile(
|
||||||
|
"^(\\d{2,5})\\.(\\d{3})(?:\\.(\\d{1,2}))?$"
|
||||||
|
).matcher(normalized);
|
||||||
|
|
||||||
|
if (groupedFrequency.matches()) {
|
||||||
|
double frequencyKHz =
|
||||||
|
Integer.parseInt(groupedFrequency.group(1))
|
||||||
|
* 1000.0
|
||||||
|
+ Integer.parseInt(
|
||||||
|
groupedFrequency.group(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
String subKHzPart =
|
||||||
|
groupedFrequency.group(3);
|
||||||
|
|
||||||
|
if (subKHzPart != null) {
|
||||||
|
double subKHz =
|
||||||
|
Integer.parseInt(subKHzPart);
|
||||||
|
|
||||||
|
frequencyKHz +=
|
||||||
|
subKHzPart.length() == 1
|
||||||
|
? subKHz / 10.0
|
||||||
|
: subKHz / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expectedBand.isPlausible(
|
||||||
|
frequencyKHz / 1000.0
|
||||||
|
) ? frequencyKHz : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
double numericValue =
|
||||||
|
Double.parseDouble(normalized);
|
||||||
|
|
||||||
|
if (expectedBand.isPlausible(numericValue)) {
|
||||||
|
return numericValue * 1000.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedBand.isPlausible(
|
||||||
|
numericValue / 1000.0)) {
|
||||||
|
return numericValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// Invalid or unsupported QRG format.
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes KST dash suffixes while retaining ordinary amateur-radio slash
|
||||||
|
* notation such as /P, /M or a country prefix.
|
||||||
|
*
|
||||||
|
* <p>Examples:
|
||||||
|
* DN9APW-2 -> DN9APW
|
||||||
|
* EA5/G8MBI/P-70 -> EA5/G8MBI/P
|
||||||
|
* DN9APW-2/P -> DN9APW/P</p>
|
||||||
|
*/
|
||||||
|
private String toWinTestSkedCallsign(String chatCallsign) {
|
||||||
|
if (chatCallsign == null || chatCallsign.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalized =
|
||||||
|
chatCallsign.trim().toUpperCase(Locale.ROOT);
|
||||||
|
|
||||||
|
return normalized.replaceAll(
|
||||||
|
"-[^/]*(?=/|$)",
|
||||||
|
""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reportSkippedWinTestSked(ContestSked sked,
|
||||||
|
String reason) {
|
||||||
|
|
||||||
|
String target =
|
||||||
|
sked == null
|
||||||
|
? "unknown station"
|
||||||
|
: sked.getTargetChatCallsign();
|
||||||
|
|
||||||
|
String message =
|
||||||
|
"Win-Test sked not sent for "
|
||||||
|
+ target
|
||||||
|
+ ": "
|
||||||
|
+ reason
|
||||||
|
+ ". The internal KST4Contest sked remains active.";
|
||||||
|
|
||||||
|
System.out.println(
|
||||||
|
"[ChatController] " + message
|
||||||
|
);
|
||||||
|
|
||||||
|
onThreadStatus(
|
||||||
|
"WT-SkedSend",
|
||||||
|
new ThreadStateMessage(
|
||||||
|
"WT-SkedSend",
|
||||||
|
false,
|
||||||
|
message,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveSkedTargetLocator(
|
||||||
|
String targetCallsignRaw) {
|
||||||
|
|
||||||
|
if (targetCallsignRaw == null
|
||||||
|
|| targetCallsignRaw.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ChatMember member
|
||||||
|
: findActiveChatMembersByRawCall(
|
||||||
|
targetCallsignRaw)) {
|
||||||
|
|
||||||
|
String locator = member.getQra();
|
||||||
|
|
||||||
|
if (locator != null && !locator.isBlank()) {
|
||||||
|
return locator
|
||||||
|
.trim()
|
||||||
|
.toUpperCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public StationMetricsService getStationMetricsService() {
|
public StationMetricsService getStationMetricsService() {
|
||||||
return stationMetricsService;
|
return stationMetricsService;
|
||||||
|
|||||||
@@ -39,26 +39,61 @@ public class WinTestSkedSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
|
* Pushes a ContestSked into Win-Test by sending the
|
||||||
* sequence via UDP broadcast.
|
* LOCKSKED / ADDSKED / UNLOCKSKED sequence.
|
||||||
*
|
*
|
||||||
* @param sked the sked to push
|
* @param sked sked to push
|
||||||
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0)
|
* @param targetCallsign callsign prepared for Win-Test
|
||||||
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST")
|
* @param frequencyKHz operating frequency in kHz
|
||||||
|
* @param notes optional notes
|
||||||
|
* @param modeOverride -1 for AUTO, 0 for CW, 1 for SSB
|
||||||
*/
|
*/
|
||||||
public void pushSkedToWinTest(ContestSked sked, double frequencyKHz, String notes, int modeOverride) {
|
public void pushSkedToWinTest(ContestSked sked,
|
||||||
|
String targetCallsign,
|
||||||
|
double frequencyKHz,
|
||||||
|
String notes,
|
||||||
|
int modeOverride) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sendLockSked();
|
sendLockSked();
|
||||||
sendAddSked(sked, frequencyKHz, notes, modeOverride);
|
|
||||||
|
sendAddSked(
|
||||||
|
sked,
|
||||||
|
targetCallsign,
|
||||||
|
frequencyKHz,
|
||||||
|
notes,
|
||||||
|
modeOverride
|
||||||
|
);
|
||||||
|
|
||||||
sendUnlockSked();
|
sendUnlockSked();
|
||||||
|
|
||||||
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false);
|
reportStatus(
|
||||||
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign()
|
"Sked pushed to WT: " + targetCallsign,
|
||||||
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand());
|
false
|
||||||
} catch (Exception e) {
|
);
|
||||||
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
|
|
||||||
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage());
|
System.out.println(
|
||||||
e.printStackTrace();
|
"[WinTestSkedSender] Sked pushed: "
|
||||||
|
+ targetCallsign
|
||||||
|
+ " at "
|
||||||
|
+ frequencyKHz
|
||||||
|
+ " kHz, band="
|
||||||
|
+ sked.getBand()
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (Exception exception) {
|
||||||
|
reportStatus(
|
||||||
|
"ERROR pushing sked: "
|
||||||
|
+ exception.getMessage(),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
System.out.println(
|
||||||
|
"[WinTestSkedSender] Error pushing sked: "
|
||||||
|
+ exception.getMessage()
|
||||||
|
);
|
||||||
|
|
||||||
|
exception.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,46 +121,54 @@ public class WinTestSkedSender {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an ADDSKED message with the sked details.
|
* Sends an ADDSKED message with the sked details.
|
||||||
* <p>
|
*
|
||||||
* Win-Test ADDSKED data format (from wtKST):
|
* <p>The wtKST implementation subtracts a reference time of
|
||||||
* <pre>
|
* 1970-01-01 00:01:00 UTC and subsequently adds 60 seconds. Both
|
||||||
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
|
* operations cancel each other out. The transmitted value is therefore
|
||||||
* </pre>
|
* an ordinary Unix timestamp and must not receive another offset here.</p>
|
||||||
* <p>
|
|
||||||
* Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch).
|
|
||||||
* The C# code adds 60 seconds to compensate.
|
|
||||||
*/
|
*/
|
||||||
private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception {
|
private void sendAddSked(ContestSked sked,
|
||||||
// Win-Test timestamp: epoch seconds with 60s offset
|
String targetCallsign,
|
||||||
long epochSeconds = sked.getSkedTimeEpoch() / 1000;
|
double frequencyKHz,
|
||||||
long wtTimestamp = epochSeconds + 60;
|
String notes,
|
||||||
|
int modeOverride) throws Exception {
|
||||||
|
|
||||||
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
|
long wtTimestamp =
|
||||||
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
|
sked.getSkedTimeEpoch() / 1000L;
|
||||||
|
|
||||||
// Win-Test band ID
|
// Frequency in 0.1 kHz units.
|
||||||
int bandId = toWinTestBandId(sked.getBand());
|
long frequencyTenthKHz =
|
||||||
|
Math.round(frequencyKHz * 10.0);
|
||||||
|
|
||||||
|
int bandId =
|
||||||
|
toWinTestBandId(sked.getBand());
|
||||||
|
|
||||||
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
|
|
||||||
int mode;
|
int mode;
|
||||||
|
|
||||||
if (modeOverride >= 0) {
|
if (modeOverride >= 0) {
|
||||||
mode = modeOverride;
|
mode = modeOverride;
|
||||||
} else {
|
} else {
|
||||||
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
|
mode = isInSsbSegment(frequencyKHz)
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
String data = wtTimestamp
|
String data =
|
||||||
+ " " + freqTenthKHz
|
wtTimestamp
|
||||||
+ " " + bandId
|
+ " " + frequencyTenthKHz
|
||||||
+ " " + mode
|
+ " " + bandId
|
||||||
+ " \"" + sked.getTargetCallsign() + "\""
|
+ " " + mode
|
||||||
+ " \"" + (notes != null ? notes : "") + "\"";
|
+ " \"" + targetCallsign + "\""
|
||||||
|
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||||
|
|
||||||
WinTestMessage msg = new WinTestMessage(
|
WinTestMessage message = new WinTestMessage(
|
||||||
WinTestMessage.MessageType.ADDSKED,
|
WinTestMessage.MessageType.ADDSKED,
|
||||||
stationName, "",
|
stationName,
|
||||||
data);
|
"",
|
||||||
sendUdp(msg);
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
sendUdp(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,25 +3,58 @@ package kst4contest.model;
|
|||||||
/**
|
/**
|
||||||
* Represents a scheduled event or an AirScout opportunity in the future.
|
* Represents a scheduled event or an AirScout opportunity in the future.
|
||||||
* Used for the Timeline View and Priority Calculation.
|
* Used for the Timeline View and Priority Calculation.
|
||||||
|
*
|
||||||
|
* <p>The base callsign remains the grouping key for scoring and worked-state
|
||||||
|
* handling. The exact KST login and its chat category are stored separately
|
||||||
|
* because reminders and external logger handover refer to the selected
|
||||||
|
* ChatMember entity.</p>
|
||||||
*/
|
*/
|
||||||
public class ContestSked {
|
public class ContestSked {
|
||||||
|
|
||||||
private String targetCallsign;
|
private String targetCallsign;
|
||||||
private double targetAzimuth; // Required for Antenna-Visuals
|
private String targetChatCallsign;
|
||||||
private long skedTimeEpoch; // The peak time (e.g., AP)
|
private ChatCategory targetChatCategory;
|
||||||
|
private double targetAzimuth;
|
||||||
|
private long skedTimeEpoch;
|
||||||
private Band band;
|
private Band band;
|
||||||
|
|
||||||
// Opportunity potential (0..100). -1 means "unknown".
|
// Opportunity potential (0..100). -1 means "unknown".
|
||||||
int opportunityPotentialPercent = -1;
|
int opportunityPotentialPercent = -1;
|
||||||
|
|
||||||
// Status flags to prevent spamming alarms
|
// Status flags to prevent spamming alarms.
|
||||||
private boolean warning3MinSent = false;
|
private boolean warning3MinSent = false;
|
||||||
private boolean warningNowSent = false;
|
private boolean warningNowSent = false;
|
||||||
|
|
||||||
public ContestSked(String call, double azimuth, long time, Band b) {
|
/**
|
||||||
this.targetCallsign = call;
|
* Backward-compatible constructor.
|
||||||
|
*/
|
||||||
|
public ContestSked(String call, double azimuth, long time, Band band) {
|
||||||
|
this(call, call, null, azimuth, time, band);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a sked for one exact KST login.
|
||||||
|
*
|
||||||
|
* @param callRaw base callsign used for scoring and worked states
|
||||||
|
* @param chatCallsign exact KST login, including an optional dash suffix
|
||||||
|
* @param chatCategory category in which the selected login is active
|
||||||
|
* @param azimuth target azimuth
|
||||||
|
* @param time sked time in epoch milliseconds
|
||||||
|
* @param band selected amateur-radio band
|
||||||
|
*/
|
||||||
|
public ContestSked(String callRaw,
|
||||||
|
String chatCallsign,
|
||||||
|
ChatCategory chatCategory,
|
||||||
|
double azimuth,
|
||||||
|
long time,
|
||||||
|
Band band) {
|
||||||
|
|
||||||
|
this.targetCallsign = callRaw;
|
||||||
|
this.targetChatCallsign = chatCallsign;
|
||||||
|
this.targetChatCategory = chatCategory;
|
||||||
this.targetAzimuth = azimuth;
|
this.targetAzimuth = azimuth;
|
||||||
this.skedTimeEpoch = time;
|
this.skedTimeEpoch = time;
|
||||||
this.band = b;
|
this.band = band;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,15 +65,54 @@ public class ContestSked {
|
|||||||
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
|
return (skedTimeEpoch - System.currentTimeMillis()) / 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters and Setters...
|
/**
|
||||||
public String getTargetCallsign() { return targetCallsign; }
|
* Returns the base callsign used for scoring and worked-state grouping.
|
||||||
public double getTargetAzimuth() { return targetAzimuth; }
|
*/
|
||||||
public long getSkedTimeEpoch() { return skedTimeEpoch; }
|
public String getTargetCallsign() {
|
||||||
public Band getBand() { return band; }
|
return targetCallsign;
|
||||||
public boolean isWarning3MinSent() { return warning3MinSent; }
|
}
|
||||||
public void setWarning3MinSent(boolean b) { this.warning3MinSent = b; }
|
|
||||||
public boolean isWarningNowSent() { return warningNowSent; }
|
/**
|
||||||
public void setWarningNowSent(boolean b) { this.warningNowSent = b; }
|
* Returns the exact KST login selected when the sked was created.
|
||||||
|
*/
|
||||||
|
public String getTargetChatCallsign() {
|
||||||
|
if (targetChatCallsign == null || targetChatCallsign.isBlank()) {
|
||||||
|
return targetCallsign;
|
||||||
|
}
|
||||||
|
return targetChatCallsign;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChatCategory getTargetChatCategory() {
|
||||||
|
return targetChatCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getTargetAzimuth() {
|
||||||
|
return targetAzimuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSkedTimeEpoch() {
|
||||||
|
return skedTimeEpoch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Band getBand() {
|
||||||
|
return band;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWarning3MinSent() {
|
||||||
|
return warning3MinSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWarning3MinSent(boolean warning3MinSent) {
|
||||||
|
this.warning3MinSent = warning3MinSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isWarningNowSent() {
|
||||||
|
return warningNowSent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWarningNowSent(boolean warningNowSent) {
|
||||||
|
this.warningNowSent = warningNowSent;
|
||||||
|
}
|
||||||
|
|
||||||
public int getOpportunityPotentialPercent() {
|
public int getOpportunityPotentialPercent() {
|
||||||
return opportunityPotentialPercent;
|
return opportunityPotentialPercent;
|
||||||
|
|||||||
@@ -667,56 +667,155 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
selectedCallSignDownerSiteGridPane.add(priorityRow, 0, 5, 1, 1);
|
selectedCallSignDownerSiteGridPane.add(priorityRow, 0, 5, 1, 1);
|
||||||
|
|
||||||
ChoiceBox<Integer> cbSkedMinutes = new ChoiceBox<>(FXCollections.observableArrayList(2, 3, 4, 5, 6,7,8,9, 10,11,12,13,14, 15, 20));
|
ChoiceBox<Integer> cbSkedMinutes = new ChoiceBox<>(
|
||||||
|
FXCollections.observableArrayList(
|
||||||
|
2, 3, 4, 5, 6, 7, 8, 9, 10,
|
||||||
|
11, 12, 13, 14, 15, 20
|
||||||
|
)
|
||||||
|
);
|
||||||
cbSkedMinutes.getSelectionModel().select(Integer.valueOf(5));
|
cbSkedMinutes.getSelectionModel().select(Integer.valueOf(5));
|
||||||
|
|
||||||
ChoiceBox<String> cbSkedMode = new ChoiceBox<>(FXCollections.observableArrayList("AUTO", "SSB", "CW"));
|
EnumSet<Band> enabledSkedBands =
|
||||||
String configuredSkedMode = this.chatcontroller.getChatPreferences().getLogsynch_wintestSkedMode();
|
BandOpportunityResolver.getEnabledStationBands(
|
||||||
|
chatcontroller.getChatPreferences()
|
||||||
|
);
|
||||||
|
|
||||||
|
ChoiceBox<Band> cbSkedBand = new ChoiceBox<>(
|
||||||
|
FXCollections.observableArrayList(enabledSkedBands)
|
||||||
|
);
|
||||||
|
|
||||||
|
cbSkedBand.setConverter(new StringConverter<>() {
|
||||||
|
@Override
|
||||||
|
public String toString(Band band) {
|
||||||
|
return band == null ? "" : bandToHumanLabel(band);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Band fromString(String value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cbSkedBand.setPrefWidth(75);
|
||||||
|
cbSkedBand.setTooltip(new Tooltip(
|
||||||
|
"Band for this sked. The initial value is derived from recent "
|
||||||
|
+ "QRG information or the station name. Only bands enabled "
|
||||||
|
+ "for your own station are offered."
|
||||||
|
));
|
||||||
|
|
||||||
|
Band defaultSkedBand =
|
||||||
|
resolveDefaultSkedBand(
|
||||||
|
selectedCallSignInfoStageChatMember,
|
||||||
|
enabledSkedBands
|
||||||
|
);
|
||||||
|
|
||||||
|
if (defaultSkedBand != null) {
|
||||||
|
cbSkedBand.setValue(defaultSkedBand);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChoiceBox<String> cbSkedMode = new ChoiceBox<>(
|
||||||
|
FXCollections.observableArrayList("AUTO", "SSB", "CW")
|
||||||
|
);
|
||||||
|
|
||||||
|
String configuredSkedMode =
|
||||||
|
chatcontroller.getChatPreferences().getLogsynch_wintestSkedMode();
|
||||||
|
|
||||||
if (configuredSkedMode == null || configuredSkedMode.isBlank()) {
|
if (configuredSkedMode == null || configuredSkedMode.isBlank()) {
|
||||||
configuredSkedMode = "AUTO";
|
configuredSkedMode = "AUTO";
|
||||||
}
|
}
|
||||||
String configuredSkedModeUpper = configuredSkedMode.trim().toUpperCase(java.util.Locale.ROOT);
|
|
||||||
|
String configuredSkedModeUpper =
|
||||||
|
configuredSkedMode.trim().toUpperCase(Locale.ROOT);
|
||||||
|
|
||||||
if (!"AUTO".equals(configuredSkedModeUpper)
|
if (!"AUTO".equals(configuredSkedModeUpper)
|
||||||
&& !"SSB".equals(configuredSkedModeUpper)
|
&& !"SSB".equals(configuredSkedModeUpper)
|
||||||
&& !"CW".equals(configuredSkedModeUpper)) {
|
&& !"CW".equals(configuredSkedModeUpper)) {
|
||||||
configuredSkedModeUpper = "AUTO";
|
configuredSkedModeUpper = "AUTO";
|
||||||
}
|
}
|
||||||
|
|
||||||
cbSkedMode.setValue(configuredSkedModeUpper);
|
cbSkedMode.setValue(configuredSkedModeUpper);
|
||||||
cbSkedMode.setTooltip(new Tooltip("Mode for Win-Test ADDSKED packets"));
|
cbSkedMode.setTooltip(new Tooltip("Mode for Win-Test ADDSKED packets"));
|
||||||
cbSkedMode.setOnAction(e ->
|
cbSkedMode.setOnAction(e ->
|
||||||
chatcontroller.getChatPreferences().setLogsynch_wintestSkedMode(cbSkedMode.getValue()));
|
chatcontroller.getChatPreferences()
|
||||||
|
.setLogsynch_wintestSkedMode(cbSkedMode.getValue())
|
||||||
|
);
|
||||||
|
|
||||||
ChoiceBox<String> cbReminderOffsets = new ChoiceBox<>(FXCollections.observableArrayList("2+1", "5+2+1", "10+5+2+1"));
|
ChoiceBox<String> cbReminderOffsets = new ChoiceBox<>(
|
||||||
|
FXCollections.observableArrayList(
|
||||||
|
"2+1",
|
||||||
|
"5+2+1",
|
||||||
|
"10+5+2+1"
|
||||||
|
)
|
||||||
|
);
|
||||||
cbReminderOffsets.getSelectionModel().select("2+1");
|
cbReminderOffsets.getSelectionModel().select("2+1");
|
||||||
|
|
||||||
CheckBox chkPmReminders = new CheckBox("Remind-PM in ");
|
CheckBox chkPmReminders = new CheckBox("Remind-PM in ");
|
||||||
|
|
||||||
Button btnCreateSked = new Button("Create sked");
|
Button btnCreateSked = new Button("Create sked");
|
||||||
btnCreateSked.setTooltip(new Tooltip("Creates a sked entry and boosts priority (ramp-up)."));
|
btnCreateSked.setTooltip(new Tooltip(
|
||||||
|
"Creates a sked entry and boosts priority during the approach."
|
||||||
|
));
|
||||||
|
|
||||||
btnCreateSked.setOnAction(e -> {
|
btnCreateSked.setOnAction(e -> {
|
||||||
ChatMember sel = chatcontroller.getScoreService().selectedChatMemberProperty().get();
|
ChatMember selectedMember =
|
||||||
if (sel == null) return;
|
chatcontroller.getScoreService()
|
||||||
|
.selectedChatMemberProperty()
|
||||||
|
.get();
|
||||||
|
|
||||||
if (cbSkedMode.getValue() != null) {
|
if (selectedMember == null) {
|
||||||
chatcontroller.getChatPreferences().setLogsynch_wintestSkedMode(cbSkedMode.getValue());
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int minutes = cbSkedMinutes.getValue() == null ? 5 : cbSkedMinutes.getValue();
|
Band selectedBand = cbSkedBand.getValue();
|
||||||
long skedTime = System.currentTimeMillis() + minutes * 60_000L;
|
if (selectedBand == null) {
|
||||||
|
showUserInputErrorWindow(
|
||||||
|
"No sked band is available. Enable at least one band "
|
||||||
|
+ "under \"My station uses ...\" before creating a sked."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
double az = sel.getQTFdirection() != null ? sel.getQTFdirection() : 0.0;
|
if (cbSkedMode.getValue() != null) {
|
||||||
|
chatcontroller.getChatPreferences()
|
||||||
|
.setLogsynch_wintestSkedMode(cbSkedMode.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
// band is not strictly required for scoring; keep current category context
|
int minutes =
|
||||||
Band band = Band.B_144; // if you want, replace with a real dropdown later
|
cbSkedMinutes.getValue() == null
|
||||||
ContestSked sked = new ContestSked(sel.getCallSignRaw(), az, skedTime, band);
|
? 5
|
||||||
|
: cbSkedMinutes.getValue();
|
||||||
|
|
||||||
|
long skedTime =
|
||||||
|
System.currentTimeMillis() + minutes * 60_000L;
|
||||||
|
|
||||||
|
double azimuth =
|
||||||
|
selectedMember.getQTFdirection() != null
|
||||||
|
? selectedMember.getQTFdirection()
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
ContestSked sked = new ContestSked(
|
||||||
|
selectedMember.getCallSignRaw(),
|
||||||
|
selectedMember.getCallSign(),
|
||||||
|
selectedMember.getChatCategory(),
|
||||||
|
azimuth,
|
||||||
|
skedTime,
|
||||||
|
selectedBand
|
||||||
|
);
|
||||||
|
|
||||||
chatcontroller.addSked(sked);
|
chatcontroller.addSked(sked);
|
||||||
chatcontroller.getScoreService().requestRecompute("sked-created");
|
chatcontroller.getScoreService()
|
||||||
|
.requestRecompute("sked-created");
|
||||||
|
|
||||||
if (chkPmReminders.isSelected()) {
|
if (chkPmReminders.isSelected()) {
|
||||||
List<Integer> offsets = parseMinuteOffsets(cbReminderOffsets.getValue());
|
List<Integer> offsets =
|
||||||
chatcontroller.getSkedReminderService().armReminders(sel.getCallSignRaw(), sel.getChatCategory(), skedTime, offsets);
|
parseMinuteOffsets(cbReminderOffsets.getValue());
|
||||||
|
|
||||||
|
chatcontroller.getSkedReminderService().armReminders(
|
||||||
|
sked.getTargetChatCallsign(),
|
||||||
|
sked.getTargetChatCategory(),
|
||||||
|
skedTime,
|
||||||
|
offsets
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -728,6 +827,13 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
);
|
);
|
||||||
skedTimeGroup.setAlignment(Pos.CENTER_LEFT);
|
skedTimeGroup.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
|
HBox skedBandGroup = new HBox(
|
||||||
|
4,
|
||||||
|
new Label("Band"),
|
||||||
|
cbSkedBand
|
||||||
|
);
|
||||||
|
skedBandGroup.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
HBox skedModeGroup = new HBox(
|
HBox skedModeGroup = new HBox(
|
||||||
4,
|
4,
|
||||||
new Label("Mode"),
|
new Label("Mode"),
|
||||||
@@ -750,12 +856,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
skedRow.setAlignment(Pos.CENTER_LEFT);
|
skedRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
skedRow.getChildren().addAll(
|
skedRow.getChildren().addAll(
|
||||||
skedTimeGroup,
|
skedTimeGroup,
|
||||||
|
skedBandGroup,
|
||||||
skedModeGroup,
|
skedModeGroup,
|
||||||
btnCreateSked,
|
btnCreateSked,
|
||||||
skedReminderGroup
|
skedReminderGroup
|
||||||
);
|
);
|
||||||
|
|
||||||
selectedCallSignDownerSiteGridPane.add(skedRow, 0, 6, 2, 1);
|
selectedCallSignDownerSiteGridPane.add(
|
||||||
|
skedRow,
|
||||||
|
0,
|
||||||
|
6,
|
||||||
|
2,
|
||||||
|
1
|
||||||
|
);
|
||||||
GridPane.setHgrow(skedRow, Priority.ALWAYS);
|
GridPane.setHgrow(skedRow, Priority.ALWAYS);
|
||||||
|
|
||||||
|
|
||||||
@@ -5453,23 +5566,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
btnSkedWarnIndicator.setTooltip(tipSkedWarnIndicator);
|
btnSkedWarnIndicator.setTooltip(tipSkedWarnIndicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeShowSkedWarnIndicator(String key, ThreadStateMessage msg) {
|
|
||||||
if (msg == null) return;
|
|
||||||
|
|
||||||
String text = msg.getRunningInformationTextDescription();
|
|
||||||
if (text == null || text.isBlank()) text = msg.getRunningInformation();
|
|
||||||
if (text == null || text.isBlank()) return;
|
|
||||||
|
|
||||||
String nick = msg.getThreadNickName() == null ? "" : msg.getThreadNickName().toLowerCase(Locale.ROOT);
|
|
||||||
String k = key == null ? "" : key.toLowerCase(Locale.ROOT);
|
|
||||||
String t = text.toLowerCase(Locale.ROOT);
|
|
||||||
|
|
||||||
boolean isSkedRelated = k.contains("sked") || nick.contains("sked") || t.contains("reminder");
|
|
||||||
if (!isSkedRelated) return;
|
|
||||||
|
|
||||||
final String finalText = text;
|
|
||||||
Platform.runLater(() -> showBlinkingSkedWarnIndicator(finalText + " SKED!"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showBlinkingSkedWarnIndicator(String text) {
|
private void showBlinkingSkedWarnIndicator(String text) {
|
||||||
// short text for the button; full text in tooltip
|
// short text for the button; full text in tooltip
|
||||||
@@ -6417,8 +6514,28 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
bPaneChatWindow.setTop(flwpne_StatusBar);
|
bPaneChatWindow.setTop(flwpne_StatusBar);
|
||||||
|
|
||||||
initSkedWarnIndicatorButton();
|
initSkedWarnIndicatorButton();
|
||||||
|
|
||||||
|
chatcontroller.lastUiReminderEventProperty().addListener(
|
||||||
|
(observable, oldValue, reminderEvent) -> {
|
||||||
|
if (reminderEvent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = "REMINDER: "
|
||||||
|
+ reminderEvent.getCallSignRaw()
|
||||||
|
+ " T-"
|
||||||
|
+ reminderEvent.getMinutesBefore()
|
||||||
|
+ "m";
|
||||||
|
|
||||||
|
Platform.runLater(
|
||||||
|
() -> showBlinkingSkedWarnIndicator(text)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
flwpne_StatusBar.getChildren().add(btnSkedWarnIndicator);
|
flwpne_StatusBar.getChildren().add(btnSkedWarnIndicator);
|
||||||
|
|
||||||
|
|
||||||
initBandUpgradeIndicatorButton();
|
initBandUpgradeIndicatorButton();
|
||||||
flwpne_StatusBar.getChildren().add(btnBandUpgradeIndicator);
|
flwpne_StatusBar.getChildren().add(btnBandUpgradeIndicator);
|
||||||
|
|
||||||
@@ -11384,13 +11501,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
|
|
||||||
settingsStage.show();
|
settingsStage.show();
|
||||||
|
|
||||||
chatcontroller.lastUiReminderEventProperty().addListener((obs, oldVal, ev) -> {
|
|
||||||
if (ev == null) return;
|
|
||||||
|
|
||||||
String text = "REMINDER: " + ev.getCallSignRaw() + " T-" + ev.getMinutesBefore() + "m";
|
|
||||||
Platform.runLater(() -> showBlinkingSkedWarnIndicator(text));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
//initialize the timeline
|
//initialize the timeline
|
||||||
Platform.runLater(this::updateTimelineVisuals);
|
Platform.runLater(this::updateTimelineVisuals);
|
||||||
@@ -11651,10 +11762,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
|||||||
updateStatusButton(key, threadStateMessage);
|
updateStatusButton(key, threadStateMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
maybeShowSkedWarnIndicator(key, threadStateMessage);
|
|
||||||
maybeShowBandUpgradeIndicator(key, threadStateMessage);
|
maybeShowBandUpgradeIndicator(key, threadStateMessage);
|
||||||
|
|
||||||
|
|
||||||
//if we receive a threadstatemessage for sked warning, enable the sked warning
|
//if we receive a threadstatemessage for sked warning, enable the sked warning
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user