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() {
|
||||
if (rotatorClient != null) {
|
||||
@@ -773,24 +852,6 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
private final Map<String, ChatCategory> lastInboundCategoryByCallSignRaw =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** Tracks the last time WE sent a message containing a QRG to a specific callsign (UPPERCASE).
|
||||
* Compared against knownActiveBands.timestampEpoch to decide whose QRG to use in a SKED. */
|
||||
private final Map<String, Long> lastSentQRGToCallsign =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/** Call this whenever we send a PM to {@code receiverCallsign} that contains our QRG. */
|
||||
public void recordOutboundQRG(String receiverCallsign) {
|
||||
if (receiverCallsign == null) return;
|
||||
lastSentQRGToCallsign.put(receiverCallsign.trim().toUpperCase(), System.currentTimeMillis());
|
||||
System.out.println("[ChatController] Recorded outbound QRG to: " + receiverCallsign);
|
||||
}
|
||||
|
||||
/** Returns epoch-ms of when we last sent our QRG to this callsign, or 0 if never. */
|
||||
public long getLastSentQRGTimestamp(String callsign) {
|
||||
if (callsign == null) return 0L;
|
||||
return lastSentQRGToCallsign.getOrDefault(callsign.trim().toUpperCase(), 0L);
|
||||
}
|
||||
|
||||
private final ScoreService scoreService = new ScoreService(this, new PriorityCalculator(), 15);
|
||||
private ScheduledExecutorService scoreScheduler;
|
||||
private final StationMetricsService stationMetricsService = new StationMetricsService();
|
||||
@@ -814,158 +875,414 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a sked to Win-Test via UDP broadcast (LOCKSKED / ADDSKED / UNLOCKSKED).
|
||||
* Runs on a background thread to avoid blocking the UI.
|
||||
* Pushes a sked to Win-Test via UDP broadcast.
|
||||
*
|
||||
* <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) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
InetAddress broadcastAddr = InetAddress.getByName(
|
||||
chatPreferences.getLogsynch_wintestNetworkBroadcastAddress());
|
||||
int port = chatPreferences.getLogsynch_wintestNetworkPort();
|
||||
String stationName = chatPreferences.getLogsynch_wintestNetworkStationNameOfKST();
|
||||
Double frequencyKHz = resolveSkedFrequencyKHz(sked);
|
||||
|
||||
WinTestSkedSender sender = new WinTestSkedSender(stationName, broadcastAddr, port, this);
|
||||
|
||||
// Frequency resolution:
|
||||
// Compare WHO sent a QRG most recently in the PM conversation:
|
||||
// - OM sent their QRG last → use OM's Last Known QRG (ChatMember.frequency)
|
||||
// - WE sent our QRG last → use our own Win-Test QRG (MYQRG)
|
||||
// Fallback chain if no timestamps exist: OM's Last Known QRG → hardcoded default
|
||||
double freqKHz = -1.0;
|
||||
final long SKED_FREQ_MAX_AGE_MS = 60 * 60 * 1000L; // 60 minutes
|
||||
|
||||
ChatMember targetMember = resolveSkedTargetMember(sked.getTargetCallsign());
|
||||
|
||||
// Collect timestamps: when did the OM last mention their QRG? When did WE last send ours?
|
||||
long omLastQRGTimestamp = 0L;
|
||||
double omLastQRGMhz = 0.0;
|
||||
if (targetMember != null && sked.getBand() != null) {
|
||||
ChatMember.ActiveFrequencyInfo fi = targetMember.getKnownActiveBands().get(sked.getBand());
|
||||
if (fi != null && fi.frequency > 0
|
||||
&& (System.currentTimeMillis() - fi.timestampEpoch) <= SKED_FREQ_MAX_AGE_MS) {
|
||||
omLastQRGTimestamp = fi.timestampEpoch;
|
||||
omLastQRGMhz = fi.frequency;
|
||||
}
|
||||
}
|
||||
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) { }
|
||||
if (frequencyKHz == null) {
|
||||
reportSkippedWinTestSked(
|
||||
sked,
|
||||
"no recent or configured QRG matches "
|
||||
+ sked.getBand().getDisplayLabel()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback A: OM's Last Known QRG from KST field (if no PM QRG exchange found at all)
|
||||
if (freqKHz < 0 && targetMember != null) {
|
||||
try {
|
||||
String memberQrg = targetMember.getFrequency().get();
|
||||
if (memberQrg != null && !memberQrg.isBlank()) {
|
||||
double mhz = Double.parseDouble(memberQrg.trim());
|
||||
freqKHz = mhz * 1000.0;
|
||||
System.out.println("[ChatController] SKED freq: fallback Last Known QRG → "
|
||||
+ mhz + " MHz → " + freqKHz + " kHz");
|
||||
}
|
||||
} catch (NumberFormatException ignored) { }
|
||||
String winTestCallsign =
|
||||
toWinTestSkedCallsign(
|
||||
sked.getTargetChatCallsign()
|
||||
);
|
||||
|
||||
if (winTestCallsign == null || winTestCallsign.isBlank()) {
|
||||
reportSkippedWinTestSked(
|
||||
sked,
|
||||
"the target callsign could not be converted"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback B: hardcoded default
|
||||
if (freqKHz < 0) {
|
||||
freqKHz = 144300.0;
|
||||
}
|
||||
InetAddress broadcastAddress = InetAddress.getByName(
|
||||
chatPreferences
|
||||
.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";
|
||||
if (targetLocator != null && !targetLocator.isBlank() && 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);
|
||||
|
||||
if (targetLocator != null
|
||||
&& !targetLocator.isBlank()
|
||||
&& 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) {
|
||||
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 modeStr = chatPreferences.getLogsynch_wintestSkedMode();
|
||||
int modeOverride = -1; // AUTO
|
||||
if ("CW".equalsIgnoreCase(modeStr)) modeOverride = 0;
|
||||
else if ("SSB".equalsIgnoreCase(modeStr)) modeOverride = 1;
|
||||
String modeText =
|
||||
chatPreferences.getLogsynch_wintestSkedMode();
|
||||
|
||||
sender.pushSkedToWinTest(sked, freqKHz, notes, modeOverride);
|
||||
} catch (Exception e) {
|
||||
System.out.println("[ChatController] Error pushing sked to Win-Test: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
int modeOverride = -1;
|
||||
|
||||
if ("CW".equalsIgnoreCase(modeText)) {
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
||||
return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
||||
}
|
||||
Double targetFrequencyKHz =
|
||||
resolveRecentTargetFrequencyKHz(sked);
|
||||
|
||||
private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
||||
if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
return null;
|
||||
if (targetFrequencyKHz != null) {
|
||||
System.out.println(
|
||||
"[ChatController] SKED frequency from target: "
|
||||
+ targetFrequencyKHz
|
||||
+ " kHz on "
|
||||
+ sked.getBand()
|
||||
);
|
||||
return targetFrequencyKHz;
|
||||
}
|
||||
|
||||
for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
||||
String locator = member.getQra();
|
||||
if (locator != null && !locator.isBlank()) {
|
||||
return locator.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
String ownQrg =
|
||||
resolveOwnQrgForSkedCategory(
|
||||
sked.getTargetChatCategory()
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// private ChatMember resolveSkedTargetMember(String targetCallsignRaw) {
|
||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// List<ChatMember> matchingMembers = findActiveChatMembersByRawCall(targetCallsignRaw);
|
||||
// return matchingMembers.isEmpty() ? null : matchingMembers.get(0);
|
||||
//
|
||||
// }
|
||||
//
|
||||
// private String resolveSkedTargetLocator(String targetCallsignRaw) {
|
||||
// if (targetCallsignRaw == null || targetCallsignRaw.isBlank()) {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// String normalizedTargetCall = normalizeCallRaw(targetCallsignRaw);
|
||||
//
|
||||
// for (ChatMember member : findActiveChatMembersByRawCall(targetCallsignRaw)) {
|
||||
// String locator = member.getQra();
|
||||
// if (locator != null && !locator.isBlank()) {
|
||||
// return locator.trim().toUpperCase(Locale.ROOT);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
/**
|
||||
* Returns the newest QRG detected on the selected band across all active
|
||||
* suffix and category variants of the base callsign.
|
||||
*/
|
||||
private Double resolveRecentTargetFrequencyKHz(ContestSked sked) {
|
||||
List<ChatMember> variants =
|
||||
findActiveChatMembersByRawCall(
|
||||
sked.getTargetCallsign()
|
||||
);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long newestTimestamp = Long.MIN_VALUE;
|
||||
Double newestFrequencyKHz = null;
|
||||
|
||||
for (ChatMember member : variants) {
|
||||
if (member == null || member.getKnownActiveBands() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ChatMember.ActiveFrequencyInfo frequencyInfo =
|
||||
member.getKnownActiveBands().get(
|
||||
sked.getBand()
|
||||
);
|
||||
|
||||
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() {
|
||||
return stationMetricsService;
|
||||
|
||||
@@ -39,26 +39,61 @@ public class WinTestSkedSender {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a ContestSked into Win-Test by sending the LOCKSKED / ADDSKED / UNLOCKSKED
|
||||
* sequence via UDP broadcast.
|
||||
* Pushes a ContestSked into Win-Test by sending the
|
||||
* LOCKSKED / ADDSKED / UNLOCKSKED sequence.
|
||||
*
|
||||
* @param sked the sked to push
|
||||
* @param frequencyKHz current operating frequency in kHz (e.g. 144321.0)
|
||||
* @param notes free-text notes (e.g. "[JO62QM - 123°] sked via KST")
|
||||
* @param sked sked to push
|
||||
* @param targetCallsign callsign prepared for Win-Test
|
||||
* @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 {
|
||||
sendLockSked();
|
||||
sendAddSked(sked, frequencyKHz, notes, modeOverride);
|
||||
|
||||
sendAddSked(
|
||||
sked,
|
||||
targetCallsign,
|
||||
frequencyKHz,
|
||||
notes,
|
||||
modeOverride
|
||||
);
|
||||
|
||||
sendUnlockSked();
|
||||
|
||||
reportStatus("Sked pushed to WT: " + sked.getTargetCallsign(), false);
|
||||
System.out.println("[WinTestSkedSender] Sked pushed: " + sked.getTargetCallsign()
|
||||
+ " at " + frequencyKHz + " kHz, band=" + sked.getBand());
|
||||
} catch (Exception e) {
|
||||
reportStatus("ERROR pushing sked: " + e.getMessage(), true);
|
||||
System.out.println("[WinTestSkedSender] Error pushing sked: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
reportStatus(
|
||||
"Sked pushed to WT: " + targetCallsign,
|
||||
false
|
||||
);
|
||||
|
||||
System.out.println(
|
||||
"[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.
|
||||
* <p>
|
||||
* Win-Test ADDSKED data format (from wtKST):
|
||||
* <pre>
|
||||
* {epoch_seconds} {freq_in_0.1kHz} {bandId} {mode} "{callsign}" "{notes}"
|
||||
* </pre>
|
||||
* <p>
|
||||
* Win-Test uses a timestamp reference of 1970-01-01 00:01:00 UTC (60s offset from Unix epoch).
|
||||
* The C# code adds 60 seconds to compensate.
|
||||
*
|
||||
* <p>The wtKST implementation subtracts a reference time of
|
||||
* 1970-01-01 00:01:00 UTC and subsequently adds 60 seconds. Both
|
||||
* operations cancel each other out. The transmitted value is therefore
|
||||
* an ordinary Unix timestamp and must not receive another offset here.</p>
|
||||
*/
|
||||
private void sendAddSked(ContestSked sked, double frequencyKHz, String notes, int modeOverride) throws Exception {
|
||||
// Win-Test timestamp: epoch seconds with 60s offset
|
||||
long epochSeconds = sked.getSkedTimeEpoch() / 1000;
|
||||
long wtTimestamp = epochSeconds + 60;
|
||||
private void sendAddSked(ContestSked sked,
|
||||
String targetCallsign,
|
||||
double frequencyKHz,
|
||||
String notes,
|
||||
int modeOverride) throws Exception {
|
||||
|
||||
// Frequency in 0.1 kHz units (Win-Test convention): multiply kHz by 10
|
||||
long freqTenthKHz = Math.round(frequencyKHz * 10.0);
|
||||
long wtTimestamp =
|
||||
sked.getSkedTimeEpoch() / 1000L;
|
||||
|
||||
// Win-Test band ID
|
||||
int bandId = toWinTestBandId(sked.getBand());
|
||||
// Frequency in 0.1 kHz units.
|
||||
long frequencyTenthKHz =
|
||||
Math.round(frequencyKHz * 10.0);
|
||||
|
||||
int bandId =
|
||||
toWinTestBandId(sked.getBand());
|
||||
|
||||
// Mode: -1 = auto-detect from frequency, 0 = CW, 1 = SSB
|
||||
int mode;
|
||||
|
||||
if (modeOverride >= 0) {
|
||||
mode = modeOverride;
|
||||
} else {
|
||||
mode = isInSsbSegment(frequencyKHz) ? 1 : 0;
|
||||
mode = isInSsbSegment(frequencyKHz)
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
String data = wtTimestamp
|
||||
+ " " + freqTenthKHz
|
||||
+ " " + bandId
|
||||
+ " " + mode
|
||||
+ " \"" + sked.getTargetCallsign() + "\""
|
||||
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||
String data =
|
||||
wtTimestamp
|
||||
+ " " + frequencyTenthKHz
|
||||
+ " " + bandId
|
||||
+ " " + mode
|
||||
+ " \"" + targetCallsign + "\""
|
||||
+ " \"" + (notes != null ? notes : "") + "\"";
|
||||
|
||||
WinTestMessage msg = new WinTestMessage(
|
||||
WinTestMessage message = new WinTestMessage(
|
||||
WinTestMessage.MessageType.ADDSKED,
|
||||
stationName, "",
|
||||
data);
|
||||
sendUdp(msg);
|
||||
stationName,
|
||||
"",
|
||||
data
|
||||
);
|
||||
|
||||
sendUdp(message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,25 +3,58 @@ package kst4contest.model;
|
||||
/**
|
||||
* Represents a scheduled event or an AirScout opportunity in the future.
|
||||
* Used for the Timeline View and Priority Calculation.
|
||||
*
|
||||
* <p>The base callsign remains the grouping key for scoring and worked-state
|
||||
* handling. The exact KST login and its chat category are stored separately
|
||||
* because reminders and external logger handover refer to the selected
|
||||
* ChatMember entity.</p>
|
||||
*/
|
||||
public class ContestSked {
|
||||
|
||||
private String targetCallsign;
|
||||
private double targetAzimuth; // Required for Antenna-Visuals
|
||||
private long skedTimeEpoch; // The peak time (e.g., AP)
|
||||
private String targetChatCallsign;
|
||||
private ChatCategory targetChatCategory;
|
||||
private double targetAzimuth;
|
||||
private long skedTimeEpoch;
|
||||
private Band band;
|
||||
|
||||
// Opportunity potential (0..100). -1 means "unknown".
|
||||
int opportunityPotentialPercent = -1;
|
||||
|
||||
// Status flags to prevent spamming alarms
|
||||
// Status flags to prevent spamming alarms.
|
||||
private boolean warning3MinSent = false;
|
||||
private boolean warningNowSent = false;
|
||||
|
||||
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.skedTimeEpoch = time;
|
||||
this.band = b;
|
||||
this.band = band;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,15 +65,54 @@ public class ContestSked {
|
||||
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; }
|
||||
/**
|
||||
* Returns the base callsign used for scoring and worked-state grouping.
|
||||
*/
|
||||
public String getTargetCallsign() {
|
||||
return targetCallsign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the exact KST login selected when the sked was created.
|
||||
*/
|
||||
public String getTargetChatCallsign() {
|
||||
if (targetChatCallsign == null || targetChatCallsign.isBlank()) {
|
||||
return targetCallsign;
|
||||
}
|
||||
return targetChatCallsign;
|
||||
}
|
||||
|
||||
public ChatCategory getTargetChatCategory() {
|
||||
return targetChatCategory;
|
||||
}
|
||||
|
||||
public double getTargetAzimuth() {
|
||||
return targetAzimuth;
|
||||
}
|
||||
|
||||
public long getSkedTimeEpoch() {
|
||||
return skedTimeEpoch;
|
||||
}
|
||||
|
||||
public Band getBand() {
|
||||
return band;
|
||||
}
|
||||
|
||||
public boolean isWarning3MinSent() {
|
||||
return warning3MinSent;
|
||||
}
|
||||
|
||||
public void setWarning3MinSent(boolean warning3MinSent) {
|
||||
this.warning3MinSent = warning3MinSent;
|
||||
}
|
||||
|
||||
public boolean isWarningNowSent() {
|
||||
return warningNowSent;
|
||||
}
|
||||
|
||||
public void setWarningNowSent(boolean warningNowSent) {
|
||||
this.warningNowSent = warningNowSent;
|
||||
}
|
||||
|
||||
public int getOpportunityPotentialPercent() {
|
||||
return opportunityPotentialPercent;
|
||||
|
||||
@@ -667,56 +667,155 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
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));
|
||||
|
||||
ChoiceBox<String> cbSkedMode = new ChoiceBox<>(FXCollections.observableArrayList("AUTO", "SSB", "CW"));
|
||||
String configuredSkedMode = this.chatcontroller.getChatPreferences().getLogsynch_wintestSkedMode();
|
||||
EnumSet<Band> enabledSkedBands =
|
||||
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()) {
|
||||
configuredSkedMode = "AUTO";
|
||||
}
|
||||
String configuredSkedModeUpper = configuredSkedMode.trim().toUpperCase(java.util.Locale.ROOT);
|
||||
|
||||
String configuredSkedModeUpper =
|
||||
configuredSkedMode.trim().toUpperCase(Locale.ROOT);
|
||||
|
||||
if (!"AUTO".equals(configuredSkedModeUpper)
|
||||
&& !"SSB".equals(configuredSkedModeUpper)
|
||||
&& !"CW".equals(configuredSkedModeUpper)) {
|
||||
configuredSkedModeUpper = "AUTO";
|
||||
}
|
||||
|
||||
cbSkedMode.setValue(configuredSkedModeUpper);
|
||||
cbSkedMode.setTooltip(new Tooltip("Mode for Win-Test ADDSKED packets"));
|
||||
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");
|
||||
|
||||
CheckBox chkPmReminders = new CheckBox("Remind-PM in ");
|
||||
|
||||
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 -> {
|
||||
ChatMember sel = chatcontroller.getScoreService().selectedChatMemberProperty().get();
|
||||
if (sel == null) return;
|
||||
ChatMember selectedMember =
|
||||
chatcontroller.getScoreService()
|
||||
.selectedChatMemberProperty()
|
||||
.get();
|
||||
|
||||
if (cbSkedMode.getValue() != null) {
|
||||
chatcontroller.getChatPreferences().setLogsynch_wintestSkedMode(cbSkedMode.getValue());
|
||||
if (selectedMember == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int minutes = cbSkedMinutes.getValue() == null ? 5 : cbSkedMinutes.getValue();
|
||||
long skedTime = System.currentTimeMillis() + minutes * 60_000L;
|
||||
Band selectedBand = cbSkedBand.getValue();
|
||||
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
|
||||
Band band = Band.B_144; // if you want, replace with a real dropdown later
|
||||
ContestSked sked = new ContestSked(sel.getCallSignRaw(), az, skedTime, band);
|
||||
int minutes =
|
||||
cbSkedMinutes.getValue() == null
|
||||
? 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.getScoreService().requestRecompute("sked-created");
|
||||
chatcontroller.getScoreService()
|
||||
.requestRecompute("sked-created");
|
||||
|
||||
if (chkPmReminders.isSelected()) {
|
||||
List<Integer> offsets = parseMinuteOffsets(cbReminderOffsets.getValue());
|
||||
chatcontroller.getSkedReminderService().armReminders(sel.getCallSignRaw(), sel.getChatCategory(), skedTime, offsets);
|
||||
List<Integer> 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);
|
||||
|
||||
HBox skedBandGroup = new HBox(
|
||||
4,
|
||||
new Label("Band"),
|
||||
cbSkedBand
|
||||
);
|
||||
skedBandGroup.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
HBox skedModeGroup = new HBox(
|
||||
4,
|
||||
new Label("Mode"),
|
||||
@@ -750,12 +856,19 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
skedRow.setAlignment(Pos.CENTER_LEFT);
|
||||
skedRow.getChildren().addAll(
|
||||
skedTimeGroup,
|
||||
skedBandGroup,
|
||||
skedModeGroup,
|
||||
btnCreateSked,
|
||||
skedReminderGroup
|
||||
);
|
||||
|
||||
selectedCallSignDownerSiteGridPane.add(skedRow, 0, 6, 2, 1);
|
||||
selectedCallSignDownerSiteGridPane.add(
|
||||
skedRow,
|
||||
0,
|
||||
6,
|
||||
2,
|
||||
1
|
||||
);
|
||||
GridPane.setHgrow(skedRow, Priority.ALWAYS);
|
||||
|
||||
|
||||
@@ -5453,23 +5566,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
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) {
|
||||
// short text for the button; full text in tooltip
|
||||
@@ -6417,8 +6514,28 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
bPaneChatWindow.setTop(flwpne_StatusBar);
|
||||
|
||||
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);
|
||||
|
||||
|
||||
initBandUpgradeIndicatorButton();
|
||||
flwpne_StatusBar.getChildren().add(btnBandUpgradeIndicator);
|
||||
|
||||
@@ -11384,13 +11501,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
|
||||
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
|
||||
Platform.runLater(this::updateTimelineVisuals);
|
||||
@@ -11651,10 +11762,7 @@ public class Kst4ContestApplication extends Application implements StatusUpdateL
|
||||
updateStatusButton(key, threadStateMessage);
|
||||
});
|
||||
|
||||
maybeShowSkedWarnIndicator(key, threadStateMessage);
|
||||
maybeShowBandUpgradeIndicator(key, threadStateMessage);
|
||||
|
||||
|
||||
//if we receive a threadstatemessage for sked warning, enable the sked warning
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user