mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-11-15 00:52:31 +01:00
Harden beacon sessions and mark automatic messages
This commit is contained in:
1 parent
19bcdb428d
commit
f13fe54fde
63 files changed
+1904
-251
No files matched your search
@@ -473,7 +473,17 @@ profiles.changeAfterSwitch=Die Änderung wirkt erst nach einem erneuten Wechsel
|
||||
profiles.alreadyActive=Dieses Profil ist bereits aktiv.
|
||||
profiles.selectFirst=Zuerst ein Profil in der Tabelle wählen.
|
||||
beacon.invalidMessage=Der Bakentext ist ungültig: {0}
|
||||
beacon.intervalTooShort=Einen ganzzahligen Bakenabstand von mindestens {0} Minute eingeben.
|
||||
beacon.intervalTooShort=Einen ganzzahligen Bakenabstand von mindestens {0} Minuten eingeben.
|
||||
beacon.status.main=Hauptkategorie: {0}
|
||||
beacon.status.second=Zweite Kategorie: {0}
|
||||
beacon.status.enabled=aktiv
|
||||
beacon.status.failed=Versand fehlgeschlagen
|
||||
beacon.status.unavailable=nicht verfügbar
|
||||
beacon.reason.session=in dieser Sitzung noch nicht aktiviert
|
||||
beacon.reason.user=durch den Benutzer ausgeschaltet
|
||||
beacon.reason.primaryQrg=Haupt-QRG fehlt oder ist ungültig
|
||||
beacon.reason.secondaryQrg=zweite QRG fehlt oder ist ungültig
|
||||
beacon.reason.automaticQrg=automatische QRG-Änderung um mehr als 5 kHz
|
||||
notification.invalidPort=„{0}" ist kein gültiger TCP-Port. Einen Wert von 1 bis 65535 eingeben.
|
||||
notification.invalidSpotter=„{0}" ist kein gültiges Spotter-Rufzeichen.
|
||||
notification.invalidCallsign=Bitte ein gültiges Rufzeichen eingeben.
|
||||
|
||||
@@ -482,7 +482,17 @@ profiles.changeAfterSwitch=The change takes effect after switching to this profi
|
||||
profiles.alreadyActive=This profile is already active.
|
||||
profiles.selectFirst=Select a profile in the table first.
|
||||
beacon.invalidMessage=The beacon message is invalid: {0}
|
||||
beacon.intervalTooShort=Enter a whole beacon interval of at least {0} minute.
|
||||
beacon.intervalTooShort=Enter a whole beacon interval of at least {0} minutes.
|
||||
beacon.status.main=Main category: {0}
|
||||
beacon.status.second=Second category: {0}
|
||||
beacon.status.enabled=active
|
||||
beacon.status.failed=send failed
|
||||
beacon.status.unavailable=unavailable
|
||||
beacon.reason.session=not activated in this session
|
||||
beacon.reason.user=switched off by the operator
|
||||
beacon.reason.primaryQrg=primary QRG missing or invalid
|
||||
beacon.reason.secondaryQrg=second QRG missing or invalid
|
||||
beacon.reason.automaticQrg=automatic QRG change greater than 5 kHz
|
||||
notification.invalidPort="{0}" is not a valid TCP port. Enter a value from 1 to 65535.
|
||||
notification.invalidSpotter="{0}" is not a valid spotter callsign.
|
||||
notification.invalidCallsign=Please enter a valid callsign.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kst4contest.view;
|
||||
|
||||
import kst4contest.controller.RadioSlot;
|
||||
import kst4contest.model.ChatCategory;
|
||||
import kst4contest.model.ChatMember;
|
||||
import kst4contest.model.ChatMessage;
|
||||
@@ -97,4 +98,9 @@ public final class ComposeChatInputActions implements ChatInputActions {
|
||||
String value = application.chatcontroller.getChatPreferences().getMYQRGSecondCat().get();
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyManualQrg(final RadioSlot slot, final String value) {
|
||||
application.chatcontroller.applyManualQrg(slot, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package kst4contest.view.compose
|
||||
|
||||
import kst4contest.model.BeaconStatusSnapshot
|
||||
import kst4contest.model.BeaconStatusSnapshot.AggregateState
|
||||
import kst4contest.model.BeaconStatusSnapshot.CategoryStatus
|
||||
import kst4contest.model.BeaconStatusSnapshot.InactiveReason
|
||||
import kst4contest.view.i18n.CurrentStrings
|
||||
import kst4contest.view.i18n.Strings
|
||||
|
||||
enum class ThreadStatusVisualStyle {
|
||||
DEFAULT,
|
||||
BEACON_OFF,
|
||||
BEACON_ACTIVE,
|
||||
BEACON_FAILED,
|
||||
}
|
||||
|
||||
data class BeaconStatusPresentation(
|
||||
val label: String,
|
||||
val tooltip: String,
|
||||
val style: ThreadStatusVisualStyle,
|
||||
) {
|
||||
companion object {
|
||||
fun from(
|
||||
snapshot: BeaconStatusSnapshot,
|
||||
strings: Strings = CurrentStrings.get(),
|
||||
): BeaconStatusPresentation {
|
||||
val label = when (snapshot.aggregateState) {
|
||||
AggregateState.OFF -> "off"
|
||||
AggregateState.PARTIAL -> "1/2"
|
||||
AggregateState.ON -> "on"
|
||||
AggregateState.FAILED -> "FAILED"
|
||||
}
|
||||
val style = when (snapshot.aggregateState) {
|
||||
AggregateState.OFF -> ThreadStatusVisualStyle.BEACON_OFF
|
||||
AggregateState.PARTIAL, AggregateState.ON -> ThreadStatusVisualStyle.BEACON_ACTIVE
|
||||
AggregateState.FAILED -> ThreadStatusVisualStyle.BEACON_FAILED
|
||||
}
|
||||
val tooltip = listOf(
|
||||
strings.beaconStatusMain(describe(snapshot.main, strings)),
|
||||
strings.beaconStatusSecond(describe(snapshot.second, strings)),
|
||||
).joinToString("\n")
|
||||
return BeaconStatusPresentation(label, tooltip, style)
|
||||
}
|
||||
|
||||
private fun describe(category: CategoryStatus, strings: Strings): String {
|
||||
if (!category.isAvailable()) return strings.beaconStatusUnavailable
|
||||
if (category.isFailed()) return strings.beaconStatusFailed
|
||||
if (category.isEnabled()) return strings.beaconStatusEnabled
|
||||
return when (category.inactiveReason) {
|
||||
InactiveReason.NOT_ACTIVATED_THIS_SESSION -> strings.beaconReasonSession
|
||||
InactiveReason.USER_DISABLED -> strings.beaconReasonUser
|
||||
InactiveReason.PRIMARY_QRG_UNAVAILABLE -> strings.beaconReasonPrimaryQrg
|
||||
InactiveReason.SECONDARY_QRG_UNAVAILABLE -> strings.beaconReasonSecondaryQrg
|
||||
InactiveReason.AUTOMATIC_QRG_CHANGE -> strings.beaconReasonAutomaticQrg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,12 +163,12 @@ fun ChatInputRow(
|
||||
// profile on every keystroke.
|
||||
InlineCommittedField(
|
||||
stored = ownQrgMain.value.orEmpty(),
|
||||
onCommit = { prefs.getMYQRGFirstCat().set(it) },
|
||||
onCommit = { state.commitOwnQrg(mainCategory = true, it) },
|
||||
tooltip = kst4contest.view.i18n.CurrentStrings.get().chatOwnQrgTooltip,
|
||||
)
|
||||
InlineCommittedField(
|
||||
stored = ownQrgSecond.value.orEmpty(),
|
||||
onCommit = { prefs.getMYQRGSecondCat().set(it) },
|
||||
onCommit = { state.commitOwnQrg(mainCategory = false, it) },
|
||||
tooltip = if (so2rEnabled) {
|
||||
kst4contest.view.i18n.CurrentStrings.get().chatSecondQrgTooltipSo2r
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import kst4contest.controller.RadioSlot
|
||||
import kst4contest.model.ChatCategory
|
||||
import kst4contest.model.ChatMember
|
||||
import kst4contest.model.ChatMessage
|
||||
@@ -46,6 +47,8 @@ interface ChatInputActions {
|
||||
fun ownQrgMain(): String
|
||||
|
||||
fun ownQrgSecond(): String
|
||||
|
||||
fun applyManualQrg(slot: RadioSlot, value: String) {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,6 +153,10 @@ class ChatInputState(private val actions: ChatInputActions) {
|
||||
appendAndFocus("$qrg ")
|
||||
}
|
||||
|
||||
fun commitOwnQrg(mainCategory: Boolean, value: String) {
|
||||
actions.applyManualQrg(if (mainCategory) RadioSlot.FIRST else RadioSlot.SECOND, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the field with "/cq CALL <snippet>", resolved.
|
||||
*
|
||||
|
||||
@@ -194,6 +194,10 @@ fun buildSettingsTabs(
|
||||
setSecondBeaconEnabled = controller::setSecondBeaconEnabled,
|
||||
mainCategoryName = categoryName(controller.chatCategoryMain, kst4contest.view.i18n.CurrentStrings.get().settingsCategoryMainFallback),
|
||||
secondCategoryName = categoryName(controller.chatCategorySecondChat, kst4contest.view.i18n.CurrentStrings.get().settingsCategorySecondFallback),
|
||||
onBeaconTemplatesApplied = {
|
||||
controller.onMainBeaconTemplateApplied()
|
||||
controller.onSecondBeaconTemplateApplied()
|
||||
},
|
||||
)
|
||||
|
||||
val logSynchState = LogSynchTabState(
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -202,8 +203,20 @@ private fun ThreadStatusButtonView(button: ThreadStatusButton) {
|
||||
}
|
||||
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
val background = if (button.highlighted) scheme.primary else scheme.surfaceVariant
|
||||
val foreground = if (button.highlighted) scheme.onPrimary else scheme.onSurfaceVariant
|
||||
val colors = if (button.visualStyle == ThreadStatusVisualStyle.DEFAULT) {
|
||||
ThreadButtonColors(
|
||||
background = if (button.highlighted) scheme.primary else scheme.surfaceVariant,
|
||||
border = null,
|
||||
text = if (button.highlighted) scheme.onPrimary else scheme.onSurfaceVariant,
|
||||
)
|
||||
} else {
|
||||
beaconThreadButtonColors(
|
||||
style = button.visualStyle,
|
||||
highlighted = button.highlighted,
|
||||
darkTheme = scheme.surface.luminance() < 0.5f,
|
||||
neutralBackground = scheme.surfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
TooltipArea(tooltip = { TooltipCard(button.tooltip) }) {
|
||||
val actionModifier = if (button.hasAction) {
|
||||
@@ -211,17 +224,20 @@ private fun ThreadStatusButtonView(button: ThreadStatusButton) {
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
var boxModifier = Modifier
|
||||
.defaultMinSize(minHeight = 22.dp)
|
||||
.background(background, NOTICE_SHAPE)
|
||||
.background(colors.background, NOTICE_SHAPE)
|
||||
.then(actionModifier)
|
||||
.padding(NOTICE_PADDING),
|
||||
colors.border?.let { border ->
|
||||
boxModifier = boxModifier.border(1.dp, border, NOTICE_SHAPE)
|
||||
}
|
||||
Box(
|
||||
modifier = boxModifier.padding(NOTICE_PADDING),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = button.label,
|
||||
color = foreground,
|
||||
color = colors.text,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
maxLines = 1,
|
||||
)
|
||||
@@ -229,6 +245,33 @@ private fun ThreadStatusButtonView(button: ThreadStatusButton) {
|
||||
}
|
||||
}
|
||||
|
||||
internal data class ThreadButtonColors(
|
||||
val background: Color,
|
||||
val border: Color?,
|
||||
val text: Color,
|
||||
)
|
||||
|
||||
internal fun beaconThreadButtonColors(
|
||||
style: ThreadStatusVisualStyle,
|
||||
highlighted: Boolean,
|
||||
darkTheme: Boolean,
|
||||
neutralBackground: Color,
|
||||
): ThreadButtonColors = when (style) {
|
||||
ThreadStatusVisualStyle.BEACON_ACTIVE -> if (highlighted) {
|
||||
ThreadButtonColors(Color(0xFFFFB300), Color(0xFFFFE082), Color(0xFF1B1B1B))
|
||||
} else {
|
||||
ThreadButtonColors(Color(0xFF238636), Color(0xFF56D364), Color.White)
|
||||
}
|
||||
ThreadStatusVisualStyle.BEACON_FAILED ->
|
||||
ThreadButtonColors(Color(0xFFD50000), Color(0xFFFF6B6B), Color.White)
|
||||
ThreadStatusVisualStyle.BEACON_OFF -> ThreadButtonColors(
|
||||
neutralBackground,
|
||||
null,
|
||||
if (darkTheme) Color(0xFFFF6B6B) else Color(0xFFD50000),
|
||||
)
|
||||
ThreadStatusVisualStyle.DEFAULT -> error("Default worker colours come from the theme")
|
||||
}
|
||||
|
||||
/** The tooltips in this bar carry the long text the badges had to cut. */
|
||||
@Composable
|
||||
internal fun TooltipCard(text: String) {
|
||||
|
||||
@@ -187,9 +187,11 @@ class ThreadStatusButtons {
|
||||
val button = bySource.getOrPut(sourceName) {
|
||||
ThreadStatusButton(sourceName, actions[sourceName]).also { ordered.add(it) }
|
||||
}
|
||||
val beacon = effectiveMessage.beaconStatusSnapshot?.let(BeaconStatusPresentation::from)
|
||||
button.report(
|
||||
label = "$sourceName: ${effectiveMessage.runningInformationTextDescription}",
|
||||
tooltip = effectiveMessage.runningInformation.orEmpty(),
|
||||
label = "$sourceName: ${beacon?.label ?: effectiveMessage.runningInformationTextDescription}",
|
||||
tooltip = beacon?.tooltip ?: effectiveMessage.runningInformation.orEmpty(),
|
||||
style = beacon?.style ?: ThreadStatusVisualStyle.DEFAULT,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -226,6 +228,9 @@ class ThreadStatusButton(
|
||||
var highlighted: Boolean by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var visualStyle: ThreadStatusVisualStyle by mutableStateOf(ThreadStatusVisualStyle.DEFAULT)
|
||||
private set
|
||||
|
||||
var revision: Int by mutableStateOf(0)
|
||||
private set
|
||||
|
||||
@@ -235,10 +240,16 @@ class ThreadStatusButton(
|
||||
val hasAction: Boolean
|
||||
get() = action != null
|
||||
|
||||
internal fun report(label: String, tooltip: String) {
|
||||
internal fun report(
|
||||
label: String,
|
||||
tooltip: String,
|
||||
style: ThreadStatusVisualStyle = ThreadStatusVisualStyle.DEFAULT,
|
||||
) {
|
||||
this.label = label
|
||||
this.tooltip = tooltip
|
||||
highlighted = true
|
||||
visualStyle = style
|
||||
highlighted = style == ThreadStatusVisualStyle.DEFAULT ||
|
||||
style == ThreadStatusVisualStyle.BEACON_ACTIVE
|
||||
revision++
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ class BeaconTabState(
|
||||
val secondCategoryName: String,
|
||||
private val setMainBeaconEnabled: (Boolean) -> Unit = prefs::setBcn_beaconsEnabledMainCat,
|
||||
private val setSecondBeaconEnabled: (Boolean) -> Unit = prefs::setBcn_beaconsEnabledSecondCat,
|
||||
private val onBeaconTemplatesApplied: () -> Unit = {},
|
||||
) {
|
||||
|
||||
val mainBeaconEnabled = prefs.bcnBeaconsEnabledMainCatProperty()
|
||||
@@ -45,10 +46,14 @@ class BeaconTabState(
|
||||
|
||||
private var textMainCat: String by draft.setting(
|
||||
{ prefs.getBcn_beaconTextMainCat() },
|
||||
effectKey = BEACON_TEMPLATE_EFFECT,
|
||||
effect = onBeaconTemplatesApplied,
|
||||
) { prefs.setBcn_beaconTextMainCat(it) }
|
||||
|
||||
private var textSecondCat: String by draft.setting(
|
||||
{ prefs.getBcn_beaconTextSecondCat() },
|
||||
effectKey = BEACON_TEMPLATE_EFFECT,
|
||||
effect = onBeaconTemplatesApplied,
|
||||
) { prefs.setBcn_beaconTextSecondCat(it) }
|
||||
|
||||
/*
|
||||
@@ -123,4 +128,8 @@ class BeaconTabState(
|
||||
interval = minutes
|
||||
return null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val BEACON_TEMPLATE_EFFECT = "beaconTemplates"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package kst4contest.view.compose
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class BeaconStatusColorsTest {
|
||||
private val neutral = Color(0xFF123456)
|
||||
|
||||
@Test
|
||||
fun `active and activity colours are the approved traffic colours`() {
|
||||
assertEquals(
|
||||
ThreadButtonColors(Color(0xFF238636), Color(0xFF56D364), Color.White),
|
||||
beaconThreadButtonColors(ThreadStatusVisualStyle.BEACON_ACTIVE, false, false, neutral),
|
||||
)
|
||||
assertEquals(
|
||||
ThreadButtonColors(Color(0xFFFFB300), Color(0xFFFFE082), Color(0xFF1B1B1B)),
|
||||
beaconThreadButtonColors(ThreadStatusVisualStyle.BEACON_ACTIVE, true, false, neutral),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `off uses neutral background and theme specific red text`() {
|
||||
assertEquals(Color(0xFFD50000),
|
||||
beaconThreadButtonColors(ThreadStatusVisualStyle.BEACON_OFF, false, false, neutral).text)
|
||||
assertEquals(Color(0xFFFF6B6B),
|
||||
beaconThreadButtonColors(ThreadStatusVisualStyle.BEACON_OFF, false, true, neutral).text)
|
||||
assertEquals(neutral,
|
||||
beaconThreadButtonColors(ThreadStatusVisualStyle.BEACON_OFF, false, true, neutral).background)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed stays red even when a highlight is requested`() {
|
||||
val colors = beaconThreadButtonColors(
|
||||
ThreadStatusVisualStyle.BEACON_FAILED,
|
||||
true,
|
||||
false,
|
||||
neutral,
|
||||
)
|
||||
assertEquals(ThreadButtonColors(Color(0xFFD50000), Color(0xFFFF6B6B), Color.White), colors)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed suppresses the flash while active status allows it`() {
|
||||
val button = ThreadStatusButton("MyBeacon")
|
||||
button.report("MyBeacon: on", "active", ThreadStatusVisualStyle.BEACON_ACTIVE)
|
||||
assertTrue(button.highlighted)
|
||||
|
||||
button.report("MyBeacon: FAILED", "failed", ThreadStatusVisualStyle.BEACON_FAILED)
|
||||
assertFalse(button.highlighted)
|
||||
assertEquals(
|
||||
ThreadButtonColors(Color(0xFFD50000), Color(0xFFFF6B6B), Color.White),
|
||||
beaconThreadButtonColors(ThreadStatusVisualStyle.BEACON_FAILED, true, false, neutral),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package kst4contest.view.compose
|
||||
|
||||
import kst4contest.model.BeaconStatusSnapshot
|
||||
import kst4contest.model.BeaconStatusSnapshot.AggregateState
|
||||
import kst4contest.model.BeaconStatusSnapshot.CategoryStatus
|
||||
import kst4contest.model.BeaconStatusSnapshot.InactiveReason
|
||||
import kst4contest.view.i18n.LanguageStore
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.Locale
|
||||
|
||||
class BeaconStatusPresentationTest {
|
||||
@Test
|
||||
fun `partial status keeps the inactive category reason in English`() {
|
||||
val presentation = BeaconStatusPresentation.from(snapshot(AggregateState.PARTIAL))
|
||||
|
||||
assertEquals("1/2", presentation.label)
|
||||
assertTrue(presentation.tooltip.contains("Main category: active"))
|
||||
assertTrue(presentation.tooltip.contains("primary QRG missing or invalid"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the same category reasons are available in German`() {
|
||||
val language = LanguageStore({ "de" }, {}, Locale.UK)
|
||||
try {
|
||||
val presentation = BeaconStatusPresentation.from(
|
||||
snapshot(AggregateState.PARTIAL),
|
||||
language.strings,
|
||||
)
|
||||
|
||||
assertTrue(presentation.tooltip.contains("Hauptkategorie: aktiv"))
|
||||
assertTrue(presentation.tooltip.contains("Haupt-QRG fehlt oder ist ungültig"))
|
||||
assertFalse(presentation.tooltip.contains("144."))
|
||||
} finally {
|
||||
language.use("en")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failed style overrides an active category`() {
|
||||
val main = CategoryStatus(true, true, true, InactiveReason.USER_DISABLED)
|
||||
val second = CategoryStatus(false, false, false, InactiveReason.NOT_ACTIVATED_THIS_SESSION)
|
||||
val presentation = BeaconStatusPresentation.from(
|
||||
BeaconStatusSnapshot(AggregateState.FAILED, main, second),
|
||||
)
|
||||
|
||||
assertEquals("FAILED", presentation.label)
|
||||
assertEquals(ThreadStatusVisualStyle.BEACON_FAILED, presentation.style)
|
||||
assertTrue(presentation.tooltip.contains("send failed"))
|
||||
}
|
||||
|
||||
private fun snapshot(state: AggregateState): BeaconStatusSnapshot = BeaconStatusSnapshot(
|
||||
state,
|
||||
CategoryStatus(true, true, false, InactiveReason.USER_DISABLED),
|
||||
CategoryStatus(true, false, false, InactiveReason.PRIMARY_QRG_UNAVAILABLE),
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import kst4contest.model.ChatCategory
|
||||
import kst4contest.model.ChatMember
|
||||
import kst4contest.model.ChatMessage
|
||||
import kst4contest.controller.RadioSlot
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertSame
|
||||
@@ -233,6 +234,20 @@ class ChatInputStateTest {
|
||||
assertEquals("432.200 ", state.value.text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `manual QRG commits keep their radio source explicit`() {
|
||||
val actions = FakeActions()
|
||||
val state = ChatInputState(actions)
|
||||
|
||||
state.commitOwnQrg(mainCategory = true, "144.300")
|
||||
state.commitOwnQrg(mainCategory = false, "1296.100")
|
||||
|
||||
assertEquals(
|
||||
listOf(RadioSlot.FIRST to "144.300", RadioSlot.SECOND to "1296.100"),
|
||||
actions.manualQrgs,
|
||||
)
|
||||
}
|
||||
|
||||
// ---- the snippets ------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -407,6 +422,7 @@ class ChatInputStateTest {
|
||||
val resolvedTemplates = mutableListOf<String>()
|
||||
val recordedCqs = mutableListOf<String>()
|
||||
val recomputeReasons = mutableListOf<String>()
|
||||
val manualQrgs = mutableListOf<Pair<RadioSlot, String>>()
|
||||
|
||||
override fun panelSelection(): ChatMember? = panelSelection
|
||||
override fun tableSelection(): ChatMember? = tableSelection
|
||||
@@ -431,5 +447,6 @@ class ChatInputStateTest {
|
||||
override fun queue(message: ChatMessage) { queued += message }
|
||||
override fun ownQrgMain(): String = ownQrgMain
|
||||
override fun ownQrgSecond(): String = ownQrgSecond
|
||||
override fun applyManualQrg(slot: RadioSlot, value: String) { manualQrgs += slot to value }
|
||||
}
|
||||
}
|
||||
@@ -173,13 +173,24 @@ class BeaconTabStateTest(@TempDir private val tempDir: Path) {
|
||||
s.prefs.setBcn_beaconIntervalInMinutesMainCat(5)
|
||||
val state = s.state()
|
||||
|
||||
assertNotNull(state.commitBeaconInterval("0"))
|
||||
assertNotNull(state.commitBeaconInterval("4"))
|
||||
|
||||
assertEquals(5, s.prefs.getBcn_beaconIntervalInMinutesMainCat())
|
||||
assertEquals(0, s.timerRestarts,
|
||||
"restarting the timer per rejected keystroke would disturb the cooldown")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exactly five minutes is accepted`() {
|
||||
val s = Session()
|
||||
val state = s.state()
|
||||
|
||||
assertNull(state.commitBeaconInterval("5"))
|
||||
|
||||
assertEquals(5, s.prefs.getBcn_beaconIntervalInMinutesMainCat())
|
||||
assertEquals(5, s.prefs.getBcn_beaconIntervalInMinutesSecondCat())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an interval that is not a whole number is refused`() {
|
||||
val s = Session()
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ class RemainingTextsFollowTheLanguageTest {
|
||||
|
||||
assertEquals("Wertung", score.title(), "the column title stayed in the old language")
|
||||
assertEquals(
|
||||
"Einen ganzzahligen Bakenabstand von mindestens 1 Minute eingeben.",
|
||||
"Einen ganzzahligen Bakenabstand von mindestens 5 Minuten eingeben.",
|
||||
beacon.commitBeaconInterval("0"),
|
||||
"the refusal from the tab state stayed in the old language",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
/** Adds the fixed marker used by every user-visible automatic chat message. */
|
||||
final class AutomaticChatMessageFormatter {
|
||||
|
||||
/** Exact marker appended to every user-visible automatic chat message. */
|
||||
/* default */ static final String SUFFIX = " *a*";
|
||||
|
||||
private AutomaticChatMessageFormatter() {
|
||||
}
|
||||
|
||||
/* default */ static String appendSuffix(final String messageText) {
|
||||
return messageText + SUFFIX;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.util.OptionalLong;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import kst4contest.model.Band;
|
||||
|
||||
/** Parses complete beacon-reference frequencies without floating-point comparisons. */
|
||||
final class BeaconFrequencyParser {
|
||||
private static final Pattern GROUPED = Pattern.compile(
|
||||
"^(\\d{2,5})[.,](\\d{1,3})(?:[.,](\\d{1,3}))?$"
|
||||
);
|
||||
private static final Pattern COMPACT = Pattern.compile("^\\d{5,8}$");
|
||||
|
||||
private BeaconFrequencyParser() {
|
||||
}
|
||||
|
||||
static OptionalLong parseHz(String value) {
|
||||
if (value == null) {
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
|
||||
String normalized = value.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
long frequencyHz;
|
||||
Matcher grouped = GROUPED.matcher(normalized);
|
||||
if (grouped.matches()) {
|
||||
long megahertz = Long.parseLong(grouped.group(1));
|
||||
long kilohertz = rightPadToThree(grouped.group(2));
|
||||
long hertz = grouped.group(3) == null
|
||||
? 0L
|
||||
: rightPadToThree(grouped.group(3));
|
||||
frequencyHz = Math.addExact(
|
||||
Math.multiplyExact(megahertz, 1_000_000L),
|
||||
Math.addExact(Math.multiplyExact(kilohertz, 1_000L), hertz)
|
||||
);
|
||||
} else if (COMPACT.matcher(normalized).matches()) {
|
||||
long compact = Long.parseLong(normalized);
|
||||
frequencyHz = Math.multiplyExact(compact, 1_000L);
|
||||
} else {
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
|
||||
if (frequencyHz <= 0L
|
||||
|| Band.fromFrequency(frequencyHz / 1_000_000.0) == null) {
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
return OptionalLong.of(frequencyHz);
|
||||
} catch (ArithmeticException | NumberFormatException ignored) {
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private static long rightPadToThree(String digits) {
|
||||
long value = Long.parseLong(digits);
|
||||
for (int index = digits.length(); index < 3; index++) {
|
||||
value *= 10L;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import kst4contest.model.BeaconStatusSnapshot;
|
||||
import kst4contest.model.BeaconStatusSnapshot.AggregateState;
|
||||
import kst4contest.model.BeaconStatusSnapshot.CategoryStatus;
|
||||
import kst4contest.model.BeaconStatusSnapshot.InactiveReason;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
/** Owns session-only beacon activation, QRG references, reasons, and send failures. */
|
||||
final class BeaconSessionState {
|
||||
static final long MAX_AUTOMATIC_QRG_DELTA_HZ = 5_000L;
|
||||
|
||||
enum Channel {
|
||||
MAIN,
|
||||
SECOND
|
||||
}
|
||||
|
||||
private final ChannelState main = new ChannelState();
|
||||
private final ChannelState second = new ChannelState();
|
||||
|
||||
void resetSession(ChatPreferences preferences) {
|
||||
synchronized (this) {
|
||||
main.reset();
|
||||
second.reset();
|
||||
}
|
||||
project(preferences);
|
||||
}
|
||||
|
||||
boolean setEnabled(Channel channel, boolean enabled, ChatPreferences preferences) {
|
||||
boolean accepted;
|
||||
synchronized (this) {
|
||||
ChannelState target = state(channel);
|
||||
if (!enabled) {
|
||||
target.disable(InactiveReason.USER_DISABLED);
|
||||
accepted = true;
|
||||
} else {
|
||||
InactiveReason invalidReason = missingRequiredQrg(channel, preferences);
|
||||
if (invalidReason != null) {
|
||||
target.disable(invalidReason);
|
||||
accepted = false;
|
||||
} else {
|
||||
target.enabled = true;
|
||||
target.failed = false;
|
||||
OptionalLong reference = ownQrg(channel, preferences);
|
||||
target.referenceHz = reference.isPresent() ? reference.getAsLong() : null;
|
||||
accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
project(preferences);
|
||||
return accepted;
|
||||
}
|
||||
|
||||
void onManualQrgChanged(Channel channel, String value, ChatPreferences preferences) {
|
||||
if (channel == Channel.MAIN) {
|
||||
preferences.getMYQRGFirstCat().set(value);
|
||||
} else {
|
||||
preferences.getMYQRGSecondCat().set(value);
|
||||
}
|
||||
OptionalLong parsed = BeaconFrequencyParser.parseHz(value);
|
||||
synchronized (this) {
|
||||
ChannelState target = state(channel);
|
||||
if (target.enabled) {
|
||||
target.referenceHz = parsed.isPresent() ? parsed.getAsLong() : null;
|
||||
}
|
||||
revalidateEnabled(preferences);
|
||||
}
|
||||
project(preferences);
|
||||
}
|
||||
|
||||
void onExternalQrgChanged(Channel channel, long frequencyHz, ChatPreferences preferences) {
|
||||
boolean valid = frequencyHz > 0L && ownQrg(channel, preferences).isPresent();
|
||||
synchronized (this) {
|
||||
ChannelState target = state(channel);
|
||||
if (target.enabled) {
|
||||
if (!valid) {
|
||||
target.referenceHz = null;
|
||||
} else if (target.referenceHz == null) {
|
||||
target.referenceHz = frequencyHz;
|
||||
} else if (absoluteDifference(target.referenceHz, frequencyHz)
|
||||
> MAX_AUTOMATIC_QRG_DELTA_HZ) {
|
||||
target.disable(InactiveReason.AUTOMATIC_QRG_CHANGE);
|
||||
}
|
||||
}
|
||||
revalidateEnabled(preferences);
|
||||
}
|
||||
project(preferences);
|
||||
}
|
||||
|
||||
void onTemplateChanged(Channel channel, ChatPreferences preferences) {
|
||||
synchronized (this) {
|
||||
ChannelState target = state(channel);
|
||||
if (target.enabled) {
|
||||
InactiveReason reason = missingRequiredQrg(channel, preferences);
|
||||
if (reason != null) {
|
||||
target.disable(reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
project(preferences);
|
||||
}
|
||||
|
||||
synchronized boolean isEnabled(Channel channel) {
|
||||
return state(channel).enabled;
|
||||
}
|
||||
|
||||
synchronized void recordSendFailure(Channel channel, boolean failed) {
|
||||
ChannelState target = state(channel);
|
||||
target.failed = target.enabled && failed;
|
||||
}
|
||||
|
||||
synchronized BeaconStatusSnapshot snapshot(ChatPreferences preferences) {
|
||||
boolean secondAvailable = preferences.isLoginToSecondChatEnabled();
|
||||
boolean mainEnabled = main.enabled;
|
||||
boolean secondEnabled = secondAvailable && second.enabled;
|
||||
boolean failed = (mainEnabled && main.failed) || (secondEnabled && second.failed);
|
||||
int available = secondAvailable ? 2 : 1;
|
||||
int enabled = (mainEnabled ? 1 : 0) + (secondEnabled ? 1 : 0);
|
||||
|
||||
AggregateState aggregate;
|
||||
if (failed) {
|
||||
aggregate = AggregateState.FAILED;
|
||||
} else if (enabled == 0) {
|
||||
aggregate = AggregateState.OFF;
|
||||
} else if (enabled == available) {
|
||||
aggregate = AggregateState.ON;
|
||||
} else {
|
||||
aggregate = AggregateState.PARTIAL;
|
||||
}
|
||||
|
||||
return new BeaconStatusSnapshot(
|
||||
aggregate,
|
||||
new CategoryStatus(true, mainEnabled, main.failed, main.reason),
|
||||
new CategoryStatus(secondAvailable, secondEnabled, second.failed, second.reason)
|
||||
);
|
||||
}
|
||||
|
||||
private void revalidateEnabled(ChatPreferences preferences) {
|
||||
revalidate(Channel.MAIN, preferences);
|
||||
revalidate(Channel.SECOND, preferences);
|
||||
}
|
||||
|
||||
private void revalidate(Channel channel, ChatPreferences preferences) {
|
||||
ChannelState target = state(channel);
|
||||
if (!target.enabled) {
|
||||
return;
|
||||
}
|
||||
InactiveReason reason = missingRequiredQrg(channel, preferences);
|
||||
if (reason != null) {
|
||||
target.disable(reason);
|
||||
}
|
||||
}
|
||||
|
||||
private static InactiveReason missingRequiredQrg(
|
||||
Channel channel,
|
||||
ChatPreferences preferences
|
||||
) {
|
||||
String template = channel == Channel.MAIN
|
||||
? preferences.getBcn_beaconTextMainCat()
|
||||
: preferences.getBcn_beaconTextSecondCat();
|
||||
String safeTemplate = template == null ? "" : template;
|
||||
if ((safeTemplate.contains("MYQRG") || safeTemplate.contains("MYQRGSHORT"))
|
||||
&& BeaconFrequencyParser.parseHz(preferences.getMYQRGFirstCat().get()).isEmpty()) {
|
||||
return InactiveReason.PRIMARY_QRG_UNAVAILABLE;
|
||||
}
|
||||
if (safeTemplate.contains("SECONDQRG")
|
||||
&& BeaconFrequencyParser.parseHz(preferences.getMYQRGSecondCat().get()).isEmpty()) {
|
||||
return InactiveReason.SECONDARY_QRG_UNAVAILABLE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static OptionalLong ownQrg(Channel channel, ChatPreferences preferences) {
|
||||
String value = channel == Channel.MAIN
|
||||
? preferences.getMYQRGFirstCat().get()
|
||||
: preferences.getMYQRGSecondCat().get();
|
||||
return BeaconFrequencyParser.parseHz(value);
|
||||
}
|
||||
|
||||
private static long absoluteDifference(long first, long second) {
|
||||
return first >= second ? first - second : second - first;
|
||||
}
|
||||
|
||||
private void project(ChatPreferences preferences) {
|
||||
boolean mainEnabled;
|
||||
boolean secondEnabled;
|
||||
synchronized (this) {
|
||||
mainEnabled = main.enabled;
|
||||
secondEnabled = second.enabled;
|
||||
}
|
||||
if (preferences.isBcn_beaconsEnabledMainCat() != mainEnabled) {
|
||||
preferences.setBcn_beaconsEnabledMainCat(mainEnabled);
|
||||
}
|
||||
if (preferences.isBcn_beaconsEnabledSecondCat() != secondEnabled) {
|
||||
preferences.setBcn_beaconsEnabledSecondCat(secondEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private ChannelState state(Channel channel) {
|
||||
return channel == Channel.MAIN ? main : second;
|
||||
}
|
||||
|
||||
private static final class ChannelState {
|
||||
private boolean enabled;
|
||||
private boolean failed;
|
||||
private Long referenceHz;
|
||||
private InactiveReason reason = InactiveReason.NOT_ACTIVATED_THIS_SESSION;
|
||||
|
||||
private void reset() {
|
||||
enabled = false;
|
||||
failed = false;
|
||||
referenceHz = null;
|
||||
reason = InactiveReason.NOT_ACTIVATED_THIS_SESSION;
|
||||
}
|
||||
|
||||
private void disable(InactiveReason inactiveReason) {
|
||||
enabled = false;
|
||||
failed = false;
|
||||
referenceHz = null;
|
||||
reason = inactiveReason;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,18 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import kst4contest.model.BeaconStatusSnapshot;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
import kst4contest.model.ThreadStateMessage;
|
||||
|
||||
/** Keeps category failures and builds the single status shown for both beacons. */
|
||||
/** Builds the single status shown for both beacons from their session state. */
|
||||
final class BeaconStatus {
|
||||
|
||||
static final String SOURCE_NAME = ChatController.BEACON_STATUS_SOURCE;
|
||||
|
||||
private boolean mainInvalid;
|
||||
private boolean secondInvalid;
|
||||
private String activity = "Beacon configuration loaded.";
|
||||
|
||||
/** Records the result of one complete timer run. */
|
||||
synchronized void recordRun(
|
||||
boolean mainInvalid,
|
||||
boolean secondInvalid,
|
||||
String activity
|
||||
) {
|
||||
this.mainInvalid = mainInvalid;
|
||||
this.secondInvalid = secondInvalid;
|
||||
synchronized void recordRun(String activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@@ -29,47 +22,33 @@ final class BeaconStatus {
|
||||
}
|
||||
|
||||
/** Builds a fresh aggregate from the current preferences and remembered failures. */
|
||||
synchronized ThreadStateMessage message(ChatPreferences preferences) {
|
||||
boolean secondAvailable = preferences.isLoginToSecondChatEnabled();
|
||||
boolean mainEnabled = preferences.isBcn_beaconsEnabledMainCat();
|
||||
boolean secondEnabled = secondAvailable
|
||||
&& preferences.isBcn_beaconsEnabledSecondCat();
|
||||
int available = secondAvailable ? 2 : 1;
|
||||
int enabled = (mainEnabled ? 1 : 0) + (secondEnabled ? 1 : 0);
|
||||
boolean critical = (mainInvalid && mainEnabled)
|
||||
|| (secondInvalid && secondEnabled);
|
||||
synchronized ThreadStateMessage message(
|
||||
ChatPreferences preferences,
|
||||
BeaconSessionState sessionState
|
||||
) {
|
||||
BeaconStatusSnapshot snapshot = sessionState.snapshot(preferences);
|
||||
boolean critical = snapshot.getAggregateState()
|
||||
== BeaconStatusSnapshot.AggregateState.FAILED;
|
||||
boolean running = snapshot.getAggregateState()
|
||||
!= BeaconStatusSnapshot.AggregateState.OFF;
|
||||
|
||||
String description;
|
||||
if (critical) {
|
||||
description = "FAILED";
|
||||
} else if (enabled == 0) {
|
||||
description = "off";
|
||||
} else if (enabled == available) {
|
||||
description = "on";
|
||||
} else {
|
||||
description = enabled + "/" + available;
|
||||
switch (snapshot.getAggregateState()) {
|
||||
case FAILED -> description = "FAILED";
|
||||
case OFF -> description = "off";
|
||||
case ON -> description = "on";
|
||||
case PARTIAL -> description = "1/2";
|
||||
default -> throw new IllegalStateException("Unknown beacon status");
|
||||
}
|
||||
|
||||
String details = "Main category beacon: " + onOff(mainEnabled)
|
||||
+ "; second category beacon: "
|
||||
+ (secondAvailable
|
||||
? onOff(secondEnabled)
|
||||
: "unavailable");
|
||||
String information = activity == null || activity.isBlank()
|
||||
? details
|
||||
: activity + "\n" + details;
|
||||
|
||||
ThreadStateMessage message = new ThreadStateMessage(
|
||||
SOURCE_NAME,
|
||||
enabled > 0,
|
||||
information,
|
||||
running,
|
||||
activity,
|
||||
critical
|
||||
);
|
||||
message.setRunningInformationTextDescription(description);
|
||||
message.setBeaconStatusSnapshot(snapshot);
|
||||
return message;
|
||||
}
|
||||
|
||||
private static String onOff(boolean enabled) {
|
||||
return enabled ? "on" : "off";
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public class BeaconTask extends TimerTask {
|
||||
private final ChatController chatController;
|
||||
private final ThreadStatusCallback callbackToController;
|
||||
private final BeaconStatus beaconStatus;
|
||||
private final BeaconSessionState beaconSessionState;
|
||||
|
||||
/**
|
||||
* Creates one execution of the shared beacon timer.
|
||||
@@ -36,6 +37,7 @@ public class BeaconTask extends TimerTask {
|
||||
this.chatController = chatController;
|
||||
this.callbackToController = callbackToController;
|
||||
this.beaconStatus = chatController.beaconStatusState();
|
||||
this.beaconSessionState = chatController.beaconSessionState();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,23 +47,34 @@ public class BeaconTask extends TimerTask {
|
||||
Attempt second = sendSecondCategoryBeacon();
|
||||
String activity = "Main category: " + main.description
|
||||
+ "; second category: " + second.description;
|
||||
beaconStatus.recordRun(
|
||||
main == Attempt.INVALID,
|
||||
second == Attempt.INVALID,
|
||||
activity
|
||||
);
|
||||
recordSendResult(BeaconSessionState.Channel.MAIN, main);
|
||||
recordSendResult(BeaconSessionState.Channel.SECOND, second);
|
||||
beaconStatus.recordRun(activity);
|
||||
callbackToController.onThreadStatus(
|
||||
BeaconStatus.SOURCE_NAME,
|
||||
beaconStatus.message(chatController.getChatPreferences())
|
||||
beaconStatus.message(
|
||||
chatController.getChatPreferences(),
|
||||
beaconSessionState
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void recordSendResult(
|
||||
BeaconSessionState.Channel channel,
|
||||
Attempt attempt
|
||||
) {
|
||||
if (attempt == Attempt.INVALID) {
|
||||
beaconSessionState.recordSendFailure(channel, true);
|
||||
} else if (attempt == Attempt.SENT) {
|
||||
beaconSessionState.recordSendFailure(channel, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the main-category beacon if it is currently enabled.
|
||||
*/
|
||||
private Attempt sendMainCategoryBeacon() {
|
||||
if (!chatController.getChatPreferences()
|
||||
.isBcn_beaconsEnabledMainCat()) {
|
||||
if (!beaconSessionState.isEnabled(BeaconSessionState.Channel.MAIN)) {
|
||||
return Attempt.OFF;
|
||||
}
|
||||
|
||||
@@ -95,7 +108,7 @@ public class BeaconTask extends TimerTask {
|
||||
if (!chatController.getChatPreferences().isLoginToSecondChatEnabled()) {
|
||||
return Attempt.UNAVAILABLE;
|
||||
}
|
||||
if (!chatController.getChatPreferences().isBcn_beaconsEnabledSecondCat()) {
|
||||
if (!beaconSessionState.isEnabled(BeaconSessionState.Channel.SECOND)) {
|
||||
return Attempt.OFF;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,9 +65,13 @@ public class ChatController implements ThreadStatusCallback {
|
||||
|
||||
|
||||
public static final String BEACON_STATUS_SOURCE = "MyBeacon";
|
||||
public static final int MIN_BEACON_INTERVAL_MINUTES = 1;
|
||||
public static final int MAX_BEACON_TEXT_LENGTH = 120;
|
||||
public static final int MIN_BEACON_INTERVAL_MINUTES = 5;
|
||||
public static final int MAX_BEACON_PAYLOAD_LENGTH = 120;
|
||||
public static final int MAX_BEACON_TEXT_LENGTH =
|
||||
MAX_BEACON_PAYLOAD_LENGTH
|
||||
+ AutomaticChatMessageFormatter.SUFFIX.length();
|
||||
private static final long INITIAL_BEACON_DELAY_MILLIS = 10_000L;
|
||||
private final BeaconSessionState beaconSessionState = new BeaconSessionState();
|
||||
private final BeaconStatus beaconStatus = new BeaconStatus();
|
||||
|
||||
private Consumer<Double> viewRotorCallback;
|
||||
@@ -3036,6 +3040,7 @@ private SimpleRoster<String>
|
||||
|
||||
chatPreferences = new ChatPreferences(preferencesRelativeFileName);
|
||||
chatPreferences.readPreferencesFromXmlFile();
|
||||
beaconSessionState.resetSession(chatPreferences);
|
||||
Path historyFile = Path.of(ApplicationFileUtils.getDebugFilePath(
|
||||
ApplicationConstants.APPLICATION_NAME,
|
||||
MessageHistoryRecorder.CURRENT_FILE_NAME));
|
||||
@@ -3288,6 +3293,7 @@ private SimpleRoster<String>
|
||||
|
||||
public void setChatPreferences(ChatPreferences chatPreferences) {
|
||||
this.chatPreferences = chatPreferences;
|
||||
beaconSessionState.resetSession(chatPreferences);
|
||||
}
|
||||
|
||||
public ChatMember getownChatMemberObject() {
|
||||
@@ -3385,13 +3391,21 @@ private SimpleRoster<String>
|
||||
|
||||
/** Applies the main-category beacon switch to the running session immediately. */
|
||||
public void setMainBeaconEnabled(boolean enabled) {
|
||||
getChatPreferences().setBcn_beaconsEnabledMainCat(enabled);
|
||||
beaconSessionState.setEnabled(
|
||||
BeaconSessionState.Channel.MAIN,
|
||||
enabled,
|
||||
getChatPreferences()
|
||||
);
|
||||
publishBeaconStatus("Beacon configuration changed.");
|
||||
}
|
||||
|
||||
/** Applies the second-category beacon switch to the running session immediately. */
|
||||
public void setSecondBeaconEnabled(boolean enabled) {
|
||||
getChatPreferences().setBcn_beaconsEnabledSecondCat(enabled);
|
||||
beaconSessionState.setEnabled(
|
||||
BeaconSessionState.Channel.SECOND,
|
||||
enabled,
|
||||
getChatPreferences()
|
||||
);
|
||||
publishBeaconStatus("Beacon configuration changed.");
|
||||
}
|
||||
|
||||
@@ -3405,18 +3419,66 @@ private SimpleRoster<String>
|
||||
public void toggleUsableBeacons() {
|
||||
ChatPreferences preferences = getChatPreferences();
|
||||
boolean secondAvailable = preferences.isLoginToSecondChatEnabled();
|
||||
boolean anyEnabled = preferences.isBcn_beaconsEnabledMainCat()
|
||||
boolean anyEnabled = beaconSessionState.isEnabled(BeaconSessionState.Channel.MAIN)
|
||||
|| (secondAvailable
|
||||
&& preferences.isBcn_beaconsEnabledSecondCat());
|
||||
&& beaconSessionState.isEnabled(BeaconSessionState.Channel.SECOND));
|
||||
boolean enable = !anyEnabled;
|
||||
|
||||
preferences.setBcn_beaconsEnabledMainCat(enable);
|
||||
beaconSessionState.setEnabled(
|
||||
BeaconSessionState.Channel.MAIN,
|
||||
enable,
|
||||
preferences
|
||||
);
|
||||
if (secondAvailable) {
|
||||
preferences.setBcn_beaconsEnabledSecondCat(enable);
|
||||
beaconSessionState.setEnabled(
|
||||
BeaconSessionState.Channel.SECOND,
|
||||
enable,
|
||||
preferences
|
||||
);
|
||||
}
|
||||
publishBeaconStatus("Beacon status toggled from the main window.");
|
||||
}
|
||||
|
||||
/** Applies a deliberate operator QRG entry and rebases the affected beacon. */
|
||||
public void applyManualQrg(RadioSlot slot, String value) {
|
||||
BeaconSessionState.Channel channel = slot == RadioSlot.FIRST
|
||||
? BeaconSessionState.Channel.MAIN
|
||||
: BeaconSessionState.Channel.SECOND;
|
||||
beaconSessionState.onManualQrgChanged(channel, value, getChatPreferences());
|
||||
publishBeaconStatus("QRG changed by the operator.");
|
||||
}
|
||||
|
||||
/** Applies a logger QRG report and enforces the automatic-QSY safety boundary. */
|
||||
public void applyExternalQrg(RadioSlot slot, String displayValue, long frequencyHz) {
|
||||
if (frequencyHz <= 0L) {
|
||||
return;
|
||||
}
|
||||
slot.qrgIn(getChatPreferences()).set(displayValue);
|
||||
BeaconSessionState.Channel channel = slot == RadioSlot.FIRST
|
||||
? BeaconSessionState.Channel.MAIN
|
||||
: BeaconSessionState.Channel.SECOND;
|
||||
beaconSessionState.onExternalQrgChanged(channel, frequencyHz, getChatPreferences());
|
||||
publishBeaconStatus("QRG updated by logging software.");
|
||||
}
|
||||
|
||||
/** Revalidates the applied main-category template. */
|
||||
public void onMainBeaconTemplateApplied() {
|
||||
beaconSessionState.onTemplateChanged(
|
||||
BeaconSessionState.Channel.MAIN,
|
||||
getChatPreferences()
|
||||
);
|
||||
publishBeaconStatus("Beacon template changed.");
|
||||
}
|
||||
|
||||
/** Revalidates the applied second-category template. */
|
||||
public void onSecondBeaconTemplateApplied() {
|
||||
beaconSessionState.onTemplateChanged(
|
||||
BeaconSessionState.Channel.SECOND,
|
||||
getChatPreferences()
|
||||
);
|
||||
publishBeaconStatus("Beacon template changed.");
|
||||
}
|
||||
|
||||
/** Publishes the loaded state without changing either category switch. */
|
||||
public void refreshBeaconStatus() {
|
||||
publishBeaconStatus("Beacon configuration loaded.");
|
||||
@@ -3427,9 +3489,13 @@ private SimpleRoster<String>
|
||||
return beaconStatus;
|
||||
}
|
||||
|
||||
BeaconSessionState beaconSessionState() {
|
||||
return beaconSessionState;
|
||||
}
|
||||
|
||||
/** Builds the current aggregate when the UI is ready to display it. */
|
||||
public ThreadStateMessage currentBeaconStatus() {
|
||||
return beaconStatus.message(getChatPreferences());
|
||||
return beaconStatus.message(getChatPreferences(), beaconSessionState);
|
||||
}
|
||||
|
||||
/** Publishes the aggregate state used by the single beacon status chip. */
|
||||
@@ -3454,7 +3520,7 @@ private SimpleRoster<String>
|
||||
* @param configuredText configured beacon template
|
||||
* @throws IllegalArgumentException if the template contains invalid protocol
|
||||
* content or currently resolves to more than
|
||||
* {@value MAX_BEACON_TEXT_LENGTH} characters
|
||||
* {@value MAX_BEACON_PAYLOAD_LENGTH} characters
|
||||
*/
|
||||
public void validateBeaconTemplate(String configuredText) {
|
||||
String normalizedTemplate =
|
||||
@@ -3473,7 +3539,10 @@ private SimpleRoster<String>
|
||||
return;
|
||||
}
|
||||
|
||||
validateResolvedBeaconText(resolvedText);
|
||||
validateBeaconText(
|
||||
resolvedText,
|
||||
MAX_BEACON_PAYLOAD_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3494,22 +3563,29 @@ private SimpleRoster<String>
|
||||
new MessageVariableResolver(chatPreferences)
|
||||
.resolveGlobalVariables(configuredText);
|
||||
|
||||
return validateResolvedBeaconText(resolvedText);
|
||||
String normalizedPayload = validateBeaconText(
|
||||
resolvedText,
|
||||
MAX_BEACON_PAYLOAD_LENGTH
|
||||
);
|
||||
return validateBeaconText(
|
||||
AutomaticChatMessageFormatter.appendSuffix(normalizedPayload),
|
||||
MAX_BEACON_TEXT_LENGTH
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the message-text and length rules to a fully resolved beacon.
|
||||
* Applies the message-text and length rules to a beacon processing stage.
|
||||
*/
|
||||
private String validateResolvedBeaconText(String resolvedText) {
|
||||
private String validateBeaconText(String resolvedText, int maximumLength) {
|
||||
String normalizedText =
|
||||
On4KstProtocol.messageText(resolvedText);
|
||||
|
||||
if (normalizedText.length() > MAX_BEACON_TEXT_LENGTH) {
|
||||
if (normalizedText.length() > maximumLength) {
|
||||
throw new IllegalArgumentException(
|
||||
"The resolved beacon message contains "
|
||||
+ normalizedText.length()
|
||||
+ " characters; maximum is "
|
||||
+ MAX_BEACON_TEXT_LENGTH
|
||||
+ maximumLength
|
||||
+ "."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -872,10 +872,14 @@ public class MessageBusManagementThread extends Thread {
|
||||
versionInfo.setSender(itsMe);
|
||||
versionInfo.setReceiver(newMessageArrived.getSender());
|
||||
versionInfo.setChatCategory(newMessageArrived.getChatCategory());
|
||||
versionInfo.setMessageText("/CQ " + newMessageArrived.getSender().getCallSign()
|
||||
+ " " + AUTOANSWER_PREFIX
|
||||
+ " KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION
|
||||
+ " by DO5AMF");
|
||||
versionInfo.setMessageText(
|
||||
AutomaticChatMessageFormatter.appendSuffix(
|
||||
"/CQ " + newMessageArrived.getSender().getCallSign()
|
||||
+ " " + AUTOANSWER_PREFIX
|
||||
+ " KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION
|
||||
+ " by DO5AMF"
|
||||
)
|
||||
);
|
||||
|
||||
this.client.getMessageTXBus().add(versionInfo);
|
||||
}
|
||||
@@ -2070,7 +2074,9 @@ public class MessageBusManagementThread extends Thread {
|
||||
+ payload;
|
||||
|
||||
try {
|
||||
return On4KstProtocol.messageText(messageText);
|
||||
return On4KstProtocol.messageText(
|
||||
AutomaticChatMessageFormatter.appendSuffix(messageText)
|
||||
);
|
||||
|
||||
} catch (IllegalArgumentException invalidMessage) {
|
||||
LOGGER.warning(
|
||||
|
||||
@@ -535,6 +535,7 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
* interpreted as frequencies.
|
||||
*/
|
||||
String formattedPassQrg = null;
|
||||
Long passFrequencyRawValue = null;
|
||||
|
||||
if (parts.size() > 11) {
|
||||
try {
|
||||
@@ -544,6 +545,7 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
passFrequencyRaw / 10.0;
|
||||
|
||||
if (passFrequencyKHz > 100.0) {
|
||||
passFrequencyRawValue = passFrequencyRaw;
|
||||
formattedPassQrg =
|
||||
helper_formatWinTestFrequency(
|
||||
passFrequencyRaw
|
||||
@@ -566,10 +568,16 @@ public class ReadUDPByWintestThread extends Thread {
|
||||
usePassQrg && formattedPassQrg != null
|
||||
? formattedPassQrg
|
||||
: formattedMainQrg;
|
||||
final long selectedFrequencyRaw =
|
||||
usePassQrg && passFrequencyRawValue != null
|
||||
? passFrequencyRawValue
|
||||
: mainFrequencyRaw;
|
||||
final long selectedFrequencyHz = Math.multiplyExact(selectedFrequencyRaw, 100L);
|
||||
|
||||
if (prefs.isLogsynch_wintestQrgSyncEnabled()) {
|
||||
RadioSlot target = slot.get();
|
||||
client.uiDispatcher().runOnUi(() -> target.qrgIn(prefs).set(qrgToSet));
|
||||
client.uiDispatcher().runOnUi(() ->
|
||||
client.applyExternalQrg(target, qrgToSet, selectedFrequencyHz));
|
||||
}
|
||||
|
||||
System.out.println(
|
||||
|
||||
@@ -417,7 +417,19 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
}
|
||||
|
||||
String qrg = freqNode.getTextContent();
|
||||
qrg = qrg == null ? "" : qrg.trim();
|
||||
String mode = modeNode.getTextContent();
|
||||
final long frequencyHz;
|
||||
try {
|
||||
frequencyHz = Math.multiplyExact(Long.parseLong(qrg), 10L);
|
||||
} catch (NumberFormatException | ArithmeticException invalidFrequency) {
|
||||
System.out.println("[ReadUDPFromUCX, warning]: RadioInfo with invalid Freq ignored");
|
||||
continue;
|
||||
}
|
||||
if (frequencyHz <= 0L) {
|
||||
System.out.println("[ReadUDPFromUCX, warning]: RadioInfo with non-positive Freq ignored");
|
||||
continue;
|
||||
}
|
||||
|
||||
// System.out.println("QRG Length: " + qrg.length() + " // " + qrg);
|
||||
|
||||
@@ -464,7 +476,7 @@ public class ReadUDPbyUCXMessageThread extends Thread {
|
||||
final String finalFormattedQRG = formattedQRG;
|
||||
final RadioSlot target = slot.get();
|
||||
helper_runOnFxThread(() ->
|
||||
target.qrgIn(this.client.getChatPreferences()).set(finalFormattedQRG)
|
||||
this.client.applyExternalQrg(target, finalFormattedQRG, frequencyHz)
|
||||
);
|
||||
|
||||
// System.out.println("[ReadUDPbyUCXTh: ] Radioinfo processed: " + formattedQRG);
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package kst4contest.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/** Immutable, display-neutral status of both beacon categories. */
|
||||
public final class BeaconStatusSnapshot {
|
||||
public enum AggregateState {
|
||||
OFF,
|
||||
PARTIAL,
|
||||
ON,
|
||||
FAILED
|
||||
}
|
||||
|
||||
public enum InactiveReason {
|
||||
NOT_ACTIVATED_THIS_SESSION,
|
||||
USER_DISABLED,
|
||||
PRIMARY_QRG_UNAVAILABLE,
|
||||
SECONDARY_QRG_UNAVAILABLE,
|
||||
AUTOMATIC_QRG_CHANGE
|
||||
}
|
||||
|
||||
/** Immutable status of one beacon category. */
|
||||
public static final class CategoryStatus {
|
||||
private final boolean available;
|
||||
private final boolean enabled;
|
||||
private final boolean failed;
|
||||
private final InactiveReason inactiveReason;
|
||||
|
||||
public CategoryStatus(
|
||||
boolean available,
|
||||
boolean enabled,
|
||||
boolean failed,
|
||||
InactiveReason inactiveReason
|
||||
) {
|
||||
this.available = available;
|
||||
this.enabled = enabled;
|
||||
this.failed = failed;
|
||||
this.inactiveReason = Objects.requireNonNull(inactiveReason);
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return available;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public boolean isFailed() {
|
||||
return failed;
|
||||
}
|
||||
|
||||
public InactiveReason getInactiveReason() {
|
||||
return inactiveReason;
|
||||
}
|
||||
}
|
||||
|
||||
private final AggregateState aggregateState;
|
||||
private final CategoryStatus main;
|
||||
private final CategoryStatus second;
|
||||
|
||||
public BeaconStatusSnapshot(
|
||||
AggregateState aggregateState,
|
||||
CategoryStatus main,
|
||||
CategoryStatus second
|
||||
) {
|
||||
this.aggregateState = Objects.requireNonNull(aggregateState);
|
||||
this.main = Objects.requireNonNull(main);
|
||||
this.second = Objects.requireNonNull(second);
|
||||
}
|
||||
|
||||
public AggregateState getAggregateState() {
|
||||
return aggregateState;
|
||||
}
|
||||
|
||||
public CategoryStatus getMain() {
|
||||
return main;
|
||||
}
|
||||
|
||||
public CategoryStatus getSecond() {
|
||||
return second;
|
||||
}
|
||||
}
|
||||
@@ -366,7 +366,8 @@ public class ChatPreferences {
|
||||
* Beacon prefs
|
||||
*/
|
||||
|
||||
MutableValue<Boolean> bcn_beaconsEnabledMainCat = new SimpleValue<>(true);
|
||||
private static final int MIN_BEACON_INTERVAL_MINUTES = 5;
|
||||
MutableValue<Boolean> bcn_beaconsEnabledMainCat = new SimpleValue<>(false);
|
||||
MutableValue<Boolean> bcn_beaconsEnabledSecondCat = new SimpleValue<>(false);
|
||||
int bcn_beaconIntervalInMinutesMainCat = 20;
|
||||
int bcn_beaconIntervalInMinutesSecondCat = 20;
|
||||
@@ -2198,11 +2199,11 @@ public class ChatPreferences {
|
||||
beaconCQ.appendChild(bcn_beaconIntervalInMinutesMainCat);
|
||||
|
||||
Element beaconCQEnabled = doc.createElement("beaconCQEnabled");
|
||||
beaconCQEnabled.setTextContent(this.isBcn_beaconsEnabledMainCat()+"");
|
||||
beaconCQEnabled.setTextContent("false");
|
||||
beaconCQ.appendChild(beaconCQEnabled);
|
||||
|
||||
Element bcn_beaconsEnabledMainCat = doc.createElement("bcn_beaconsEnabledMainCat");
|
||||
bcn_beaconsEnabledMainCat.setTextContent(this.isBcn_beaconsEnabledMainCat()+"");
|
||||
bcn_beaconsEnabledMainCat.setTextContent("false");
|
||||
beaconCQ.appendChild(bcn_beaconsEnabledMainCat);
|
||||
|
||||
|
||||
@@ -2223,11 +2224,11 @@ public class ChatPreferences {
|
||||
beaconCQ.appendChild(bcn_beaconIntervalInMinutesSecondCat);
|
||||
|
||||
Element beaconCQEnabledSecondCat = doc.createElement("beaconCQEnabledSecondCat");
|
||||
beaconCQEnabledSecondCat.setTextContent(this.isBcn_beaconsEnabledSecondCat()+"");
|
||||
beaconCQEnabledSecondCat.setTextContent("false");
|
||||
beaconCQ.appendChild(beaconCQEnabledSecondCat);
|
||||
|
||||
Element bcn_beaconsEnabledSecondCat = doc.createElement("bcn_beaconsEnabledSecondCat");
|
||||
bcn_beaconsEnabledSecondCat.setTextContent(this.isBcn_beaconsEnabledSecondCat()+"");
|
||||
bcn_beaconsEnabledSecondCat.setTextContent("false");
|
||||
beaconCQ.appendChild(bcn_beaconsEnabledSecondCat);
|
||||
|
||||
/**
|
||||
@@ -3178,12 +3179,18 @@ public class ChatPreferences {
|
||||
|
||||
Element beaconCQEl = getFirstElement(doc, "beaconCQ");
|
||||
if (beaconCQEl != null) {
|
||||
setBcn_beaconsEnabledMainCat(getBoolean(beaconCQEl, isBcn_beaconsEnabledMainCat(), "bcn_beaconsEnabledMainCat", "beaconCQEnabled"));
|
||||
bcn_beaconIntervalInMinutesMainCat = getInt(beaconCQEl, bcn_beaconIntervalInMinutesMainCat, "bcn_beaconIntervalInMinutesMainCat", "beaconCQIntervalMinutes");
|
||||
setBcn_beaconsEnabledMainCat(false);
|
||||
bcn_beaconIntervalInMinutesMainCat = Math.max(
|
||||
MIN_BEACON_INTERVAL_MINUTES,
|
||||
getInt(beaconCQEl, bcn_beaconIntervalInMinutesMainCat, "bcn_beaconIntervalInMinutesMainCat", "beaconCQIntervalMinutes")
|
||||
);
|
||||
bcn_beaconTextMainCat = getText(beaconCQEl, bcn_beaconTextMainCat, "bcn_beaconTextMainCat", "beaconCQText");
|
||||
|
||||
setBcn_beaconsEnabledSecondCat(getBoolean(beaconCQEl, isBcn_beaconsEnabledSecondCat(), "bcn_beaconsEnabledSecondCat", "beaconCQEnabledSecondCat"));
|
||||
bcn_beaconIntervalInMinutesSecondCat = getInt(beaconCQEl, bcn_beaconIntervalInMinutesSecondCat, "bcn_beaconIntervalInMinutesSecondCat", "beaconCQIntervalMinutesSecondCat");
|
||||
setBcn_beaconsEnabledSecondCat(false);
|
||||
bcn_beaconIntervalInMinutesSecondCat = Math.max(
|
||||
MIN_BEACON_INTERVAL_MINUTES,
|
||||
getInt(beaconCQEl, bcn_beaconIntervalInMinutesSecondCat, "bcn_beaconIntervalInMinutesSecondCat", "beaconCQIntervalMinutesSecondCat")
|
||||
);
|
||||
bcn_beaconTextSecondCat = getText(beaconCQEl, bcn_beaconTextSecondCat, "bcn_beaconTextSecondCat", "beaconCQTextSecondText");
|
||||
|
||||
System.out.println("[ChatPreferences, info]: beaconCQ main='" + bcn_beaconTextMainCat + "', second='" + bcn_beaconTextSecondCat + "'");
|
||||
|
||||
@@ -14,6 +14,7 @@ public class ThreadStateMessage {
|
||||
|
||||
boolean criticalState;
|
||||
String criticalStateFurtherInfo;
|
||||
BeaconStatusSnapshot beaconStatusSnapshot;
|
||||
|
||||
|
||||
|
||||
@@ -99,5 +100,13 @@ public class ThreadStateMessage {
|
||||
public void setCriticalStateFurtherInfo(String criticalStateFurtherInfo) {
|
||||
this.criticalStateFurtherInfo = criticalStateFurtherInfo;
|
||||
}
|
||||
|
||||
public BeaconStatusSnapshot getBeaconStatusSnapshot() {
|
||||
return beaconStatusSnapshot;
|
||||
}
|
||||
|
||||
public void setBeaconStatusSnapshot(BeaconStatusSnapshot beaconStatusSnapshot) {
|
||||
this.beaconStatusSnapshot = beaconStatusSnapshot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,10 +112,10 @@
|
||||
</textSnippets>
|
||||
<beaconCQ>
|
||||
<beaconCQText>_.~'"´"'~.__.~'´ CQ CQ CQ NAC de MYCALL, pse try 144.317 for JO54 `'~.__.~'"`"'~._</beaconCQText>
|
||||
<beaconCQIntervalMinutes>1</beaconCQIntervalMinutes>
|
||||
<beaconCQIntervalMinutes>5</beaconCQIntervalMinutes>
|
||||
<beaconCQEnabled>false</beaconCQEnabled>
|
||||
<beaconCQTextSecondText>_.~'"´"'~.__.~'´ CQ CQ CQ de MYCALL, pse sked or SECONDQRG `'~.__.~'"`"'~._</beaconCQTextSecondText>
|
||||
<beaconCQIntervalMinutesSecondCat>3</beaconCQIntervalMinutesSecondCat>
|
||||
<beaconCQIntervalMinutesSecondCat>5</beaconCQIntervalMinutesSecondCat>
|
||||
<beaconCQEnabledSecondCat>false</beaconCQEnabledSecondCat>
|
||||
</beaconCQ>
|
||||
<beaconUnworkedstations>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Verifies the fixed suffix contract for automatic chat messages. */
|
||||
class AutomaticChatMessageFormatterTest {
|
||||
|
||||
@Test
|
||||
void appendsTheExactAutomaticChatSuffix() {
|
||||
assertEquals(
|
||||
"CQ test *a*",
|
||||
AutomaticChatMessageFormatter.appendSuffix("CQ test"),
|
||||
"the suffix must include its leading space"
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendsTheSuffixEvenWhenTheTextAlreadyEndsWithIt() {
|
||||
assertEquals(
|
||||
"CQ test *a* *a*",
|
||||
AutomaticChatMessageFormatter.appendSuffix("CQ test *a*"),
|
||||
"an existing suffix must not be deduplicated"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,7 @@ class BeaconControlTest {
|
||||
@Test
|
||||
void toggleWithSecondChatDisabledChangesOnlyTheMainBeacon() {
|
||||
preferences.setLoginToSecondChatEnabled(false);
|
||||
preferences.setBcn_beaconsEnabledMainCat(false);
|
||||
preferences.setBcn_beaconsEnabledSecondCat(true);
|
||||
controller.setSecondBeaconEnabled(true);
|
||||
|
||||
controller.toggleUsableBeacons();
|
||||
|
||||
@@ -56,8 +55,6 @@ class BeaconControlTest {
|
||||
@Test
|
||||
void toggleWithSecondChatEnabledSwitchesBothUsableBeacons() {
|
||||
preferences.setLoginToSecondChatEnabled(true);
|
||||
preferences.setBcn_beaconsEnabledMainCat(false);
|
||||
preferences.setBcn_beaconsEnabledSecondCat(false);
|
||||
|
||||
controller.toggleUsableBeacons();
|
||||
|
||||
@@ -65,7 +62,7 @@ class BeaconControlTest {
|
||||
assertTrue(preferences.isBcn_beaconsEnabledSecondCat());
|
||||
assertLastDescription("on");
|
||||
|
||||
preferences.setBcn_beaconsEnabledSecondCat(false);
|
||||
controller.setSecondBeaconEnabled(false);
|
||||
controller.toggleUsableBeacons();
|
||||
|
||||
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
|
||||
@@ -76,8 +73,6 @@ class BeaconControlTest {
|
||||
@Test
|
||||
void anIndividualCheckboxPublishesThePartialStateImmediately() {
|
||||
preferences.setLoginToSecondChatEnabled(true);
|
||||
preferences.setBcn_beaconsEnabledMainCat(false);
|
||||
preferences.setBcn_beaconsEnabledSecondCat(false);
|
||||
|
||||
controller.setMainBeaconEnabled(true);
|
||||
|
||||
@@ -86,6 +81,47 @@ class BeaconControlTest {
|
||||
assertLastDescription("1/2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void activationRejectsOnlyTheTemplateWhoseRequiredQrgIsMissing() {
|
||||
preferences.setLoginToSecondChatEnabled(true);
|
||||
preferences.setMYQRGSecondCat("1296.200");
|
||||
preferences.setBcn_beaconTextMainCat("CQ SECONDQRG");
|
||||
preferences.setBcn_beaconTextSecondCat("CQ MYQRG");
|
||||
|
||||
controller.toggleUsableBeacons();
|
||||
|
||||
assertTrue(preferences.isBcn_beaconsEnabledMainCat());
|
||||
assertFalse(preferences.isBcn_beaconsEnabledSecondCat());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualQrgRebasesBeforeAnAutomaticMoveIsCompared() {
|
||||
preferences.setBcn_beaconTextMainCat("CQ");
|
||||
controller.setMainBeaconEnabled(true);
|
||||
controller.applyManualQrg(RadioSlot.FIRST, "144.300");
|
||||
controller.applyExternalQrg(RadioSlot.FIRST, "144.305", 144_305_000L);
|
||||
assertTrue(preferences.isBcn_beaconsEnabledMainCat());
|
||||
|
||||
controller.applyManualQrg(RadioSlot.FIRST, "144.310");
|
||||
controller.applyExternalQrg(RadioSlot.FIRST, "144.315.01", 144_315_010L);
|
||||
|
||||
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonPositiveExternalQrgCannotOverwriteTheDisplayOrClearTheReference() {
|
||||
preferences.setBcn_beaconTextMainCat("CQ");
|
||||
controller.applyManualQrg(RadioSlot.FIRST, "144.300");
|
||||
controller.setMainBeaconEnabled(true);
|
||||
|
||||
controller.applyExternalQrg(RadioSlot.FIRST, "0", 0L);
|
||||
|
||||
assertEquals("144.300", preferences.getMYQRGFirstCat().get());
|
||||
controller.applyExternalQrg(RadioSlot.FIRST, "144.306", 144_306_000L);
|
||||
assertFalse(preferences.isBcn_beaconsEnabledMainCat(),
|
||||
"the rejected packet must not clear the 144.300 reference");
|
||||
}
|
||||
|
||||
private void assertLastDescription(String expected) {
|
||||
ArgumentCaptor<String> key = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<ThreadStateMessage> message = ArgumentCaptor.forClass(ThreadStateMessage.class);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class BeaconFrequencyParserTest {
|
||||
|
||||
@Test
|
||||
void parsesEveryCompleteQrgFormWithoutLosingHertz() {
|
||||
assertHz("144.300", 144_300_000L);
|
||||
assertHz("144,300", 144_300_000L);
|
||||
assertHz("144.300.03", 144_300_030L);
|
||||
assertHz("1296.123.000", 1_296_123_000L);
|
||||
assertHz("144300", 144_300_000L);
|
||||
assertHz("10368100", 10_368_100_000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsIncompleteMalformedAndUnsupportedValues() {
|
||||
for (String value : new String[] {null, "", " ", "210", "144 MHz", "0", "-144.300",
|
||||
"999999999999999999999999999999"}) {
|
||||
assertTrue(BeaconFrequencyParser.parseHz(value).isEmpty(), value);
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertHz(String value, long expected) {
|
||||
assertEquals(expected, BeaconFrequencyParser.parseHz(value).orElseThrow());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package kst4contest.controller;
|
||||
|
||||
import static kst4contest.controller.BeaconSessionState.Channel.MAIN;
|
||||
import static kst4contest.controller.BeaconSessionState.Channel.SECOND;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import kst4contest.model.BeaconStatusSnapshot;
|
||||
import kst4contest.model.BeaconStatusSnapshot.AggregateState;
|
||||
import kst4contest.model.BeaconStatusSnapshot.InactiveReason;
|
||||
import kst4contest.model.ChatPreferences;
|
||||
|
||||
class BeaconSessionStateTest {
|
||||
@TempDir Path tempDir;
|
||||
private ChatPreferences preferences;
|
||||
private BeaconSessionState state;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
preferences = new ChatPreferences(tempDir.resolve("preferences.xml"));
|
||||
preferences.setLoginToSecondChatEnabled(true);
|
||||
preferences.setBcn_beaconTextMainCat("CQ MAIN");
|
||||
preferences.setBcn_beaconTextSecondCat("CQ SECOND");
|
||||
preferences.setMYQRGFirstCat("");
|
||||
preferences.setMYQRGSecondCat("");
|
||||
state = new BeaconSessionState();
|
||||
state.resetSession(preferences);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetStartsBothCategoriesOffWithSessionReason() {
|
||||
BeaconStatusSnapshot snapshot = state.snapshot(preferences);
|
||||
assertEquals(AggregateState.OFF, snapshot.getAggregateState());
|
||||
assertEquals(InactiveReason.NOT_ACTIVATED_THIS_SESSION, snapshot.getMain().getInactiveReason());
|
||||
assertEquals(InactiveReason.NOT_ACTIVATED_THIS_SESSION, snapshot.getSecond().getInactiveReason());
|
||||
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
|
||||
assertFalse(preferences.isBcn_beaconsEnabledSecondCat());
|
||||
}
|
||||
|
||||
@Test
|
||||
void enablesValidCategoryAndRejectsOtherCategoryWithMissingCrossTemplateQrg() {
|
||||
preferences.setMYQRGSecondCat("1296.123");
|
||||
preferences.setBcn_beaconTextMainCat("CQ SECONDQRG");
|
||||
preferences.setBcn_beaconTextSecondCat("CQ MYQRG");
|
||||
|
||||
assertTrue(state.setEnabled(MAIN, true, preferences));
|
||||
assertFalse(state.setEnabled(SECOND, true, preferences));
|
||||
|
||||
BeaconStatusSnapshot snapshot = state.snapshot(preferences);
|
||||
assertEquals(AggregateState.PARTIAL, snapshot.getAggregateState());
|
||||
assertTrue(snapshot.getMain().isEnabled());
|
||||
assertEquals(InactiveReason.PRIMARY_QRG_UNAVAILABLE, snapshot.getSecond().getInactiveReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualQrgRebasesAndInvalidValueClearsReferenceWithoutPlaceholder() {
|
||||
assertTrue(state.setEnabled(MAIN, true, preferences));
|
||||
state.onManualQrgChanged(MAIN, "144.300", preferences);
|
||||
state.onExternalQrgChanged(MAIN, 144_306_000L, preferences);
|
||||
assertFalse(state.isEnabled(MAIN));
|
||||
|
||||
assertTrue(state.setEnabled(MAIN, true, preferences));
|
||||
state.onManualQrgChanged(MAIN, "invalid", preferences);
|
||||
assertTrue(state.isEnabled(MAIN));
|
||||
state.onExternalQrgChanged(MAIN, 144_310_000L, preferences);
|
||||
assertTrue(state.isEnabled(MAIN), "first valid report establishes the cleared reference");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidManualQrgDisablesEveryActiveTemplateThatNeedsIt() {
|
||||
preferences.setMYQRGFirstCat("144.300");
|
||||
preferences.setBcn_beaconTextMainCat("CQ MYQRG");
|
||||
preferences.setBcn_beaconTextSecondCat("CQ MYQRGSHORT");
|
||||
assertTrue(state.setEnabled(MAIN, true, preferences));
|
||||
assertTrue(state.setEnabled(SECOND, true, preferences));
|
||||
|
||||
state.onManualQrgChanged(MAIN, "", preferences);
|
||||
|
||||
assertFalse(state.isEnabled(MAIN));
|
||||
assertFalse(state.isEnabled(SECOND));
|
||||
assertEquals(InactiveReason.PRIMARY_QRG_UNAVAILABLE,
|
||||
state.snapshot(preferences).getSecond().getInactiveReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void externalMoveUsesStrictFiveKhzBoundaryAndOnlyItsCategory() {
|
||||
preferences.setMYQRGFirstCat("144.300");
|
||||
preferences.setMYQRGSecondCat("1296.100");
|
||||
assertTrue(state.setEnabled(MAIN, true, preferences));
|
||||
assertTrue(state.setEnabled(SECOND, true, preferences));
|
||||
|
||||
state.onExternalQrgChanged(MAIN, 144_305_000L, preferences);
|
||||
assertTrue(state.isEnabled(MAIN));
|
||||
state.onExternalQrgChanged(MAIN, 144_305_001L, preferences);
|
||||
|
||||
assertFalse(state.isEnabled(MAIN));
|
||||
assertTrue(state.isEnabled(SECOND));
|
||||
assertEquals(InactiveReason.AUTOMATIC_QRG_CHANGE,
|
||||
state.snapshot(preferences).getMain().getInactiveReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidExternalQrgClearsTheReferenceWithoutAPlaceholder() {
|
||||
assertTrue(state.setEnabled(MAIN, true, preferences));
|
||||
preferences.setMYQRGFirstCat("7.100.00");
|
||||
state.onExternalQrgChanged(MAIN, 7_100_000L, preferences);
|
||||
assertTrue(state.isEnabled(MAIN));
|
||||
|
||||
preferences.setMYQRGFirstCat("144.300");
|
||||
state.onExternalQrgChanged(MAIN, 144_300_000L, preferences);
|
||||
|
||||
assertTrue(state.isEnabled(MAIN), "the first valid report must establish the reference");
|
||||
}
|
||||
|
||||
@Test
|
||||
void failureOverridesAggregateAndA_successClearsOnlyThatCategory() {
|
||||
assertTrue(state.setEnabled(MAIN, true, preferences));
|
||||
assertTrue(state.setEnabled(SECOND, true, preferences));
|
||||
state.recordSendFailure(MAIN, true);
|
||||
state.recordSendFailure(SECOND, true);
|
||||
assertEquals(AggregateState.FAILED, state.snapshot(preferences).getAggregateState());
|
||||
|
||||
state.recordSendFailure(MAIN, false);
|
||||
BeaconStatusSnapshot snapshot = state.snapshot(preferences);
|
||||
assertEquals(AggregateState.FAILED, snapshot.getAggregateState());
|
||||
assertFalse(snapshot.getMain().isFailed());
|
||||
assertTrue(snapshot.getSecond().isFailed());
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,9 @@ class BeaconTaskTest {
|
||||
|
||||
@Test
|
||||
void invalidEnabledBeaconIsNotQueuedAndReportsOneCriticalAggregateState() {
|
||||
preferences.setBcn_beaconsEnabledMainCat(true);
|
||||
controller.setMainBeaconEnabled(true);
|
||||
preferences.setBcn_beaconTextMainCat("invalid|template");
|
||||
statuses.clear();
|
||||
|
||||
task.run();
|
||||
|
||||
@@ -80,8 +81,9 @@ class BeaconTaskTest {
|
||||
|
||||
@Test
|
||||
void changingTheOtherCheckboxDoesNotHideAnActiveCategoryFailure() {
|
||||
preferences.setBcn_beaconsEnabledMainCat(true);
|
||||
controller.setMainBeaconEnabled(true);
|
||||
preferences.setBcn_beaconTextMainCat("invalid|template");
|
||||
statuses.clear();
|
||||
task.run();
|
||||
statuses.clear();
|
||||
|
||||
@@ -93,6 +95,29 @@ class BeaconTaskTest {
|
||||
assertTrue(status.isCriticalState());
|
||||
}
|
||||
|
||||
@Test
|
||||
void unavailableCategoryRetainsFailureUntilItsNextSuccessfulRun() {
|
||||
controller.setSecondBeaconEnabled(true);
|
||||
preferences.setBcn_beaconTextSecondCat("invalid|template");
|
||||
task.run();
|
||||
assertTrue(controller.currentBeaconStatus()
|
||||
.getBeaconStatusSnapshot().getSecond().isFailed());
|
||||
|
||||
preferences.setLoginToSecondChatEnabled(false);
|
||||
task.run();
|
||||
assertTrue(controller.currentBeaconStatus()
|
||||
.getBeaconStatusSnapshot().getSecond().isFailed(),
|
||||
"an unavailable run did not succeed and must not clear the failure");
|
||||
|
||||
preferences.setLoginToSecondChatEnabled(true);
|
||||
assertEquals("FAILED", controller.currentBeaconStatus()
|
||||
.getRunningInformationTextDescription());
|
||||
preferences.setBcn_beaconTextSecondCat("CQ SECOND");
|
||||
task.run();
|
||||
assertFalse(controller.currentBeaconStatus()
|
||||
.getBeaconStatusSnapshot().getSecond().isFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void everyRunUsesTheCurrentEnableFlagsAndReportsOneAggregateState() {
|
||||
task.run();
|
||||
@@ -100,16 +125,22 @@ class BeaconTaskTest {
|
||||
assertAggregateStatus("off", false);
|
||||
|
||||
statuses.clear();
|
||||
preferences.setBcn_beaconsEnabledMainCat(true);
|
||||
controller.setMainBeaconEnabled(true);
|
||||
statuses.clear();
|
||||
task.run();
|
||||
assertEquals(1, txBus.size());
|
||||
assertEquals(ChatCategory.VUHF, txBus.peek().getChatCategory().getCategoryNumber());
|
||||
assertEquals("CQ MAIN *a*", txBus.peek().getMessageText());
|
||||
assertAggregateStatus("1/2", true);
|
||||
|
||||
statuses.clear();
|
||||
preferences.setBcn_beaconsEnabledSecondCat(true);
|
||||
controller.setSecondBeaconEnabled(true);
|
||||
statuses.clear();
|
||||
task.run();
|
||||
assertEquals(3, txBus.size());
|
||||
List<ChatMessage> sentMessages = new ArrayList<>(txBus);
|
||||
assertEquals("CQ MAIN *a*", sentMessages.get(1).getMessageText());
|
||||
assertEquals("CQ SECOND *a*", sentMessages.get(2).getMessageText());
|
||||
assertAggregateStatus("on", true);
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -116,7 +116,7 @@ class MessageBusManagementThreadAutoAnswerDecisionTest {
|
||||
@Test
|
||||
void buildsTheQrgReply() {
|
||||
assertEquals(
|
||||
"/CQ DL1ABC " + MARKER + " QRG is: 144.300",
|
||||
"/CQ DL1ABC " + MARKER + " QRG is: 144.300 *a*",
|
||||
MessageBusManagementThread.buildAutoAnswerMessageText(
|
||||
"DL1ABC", VUHF, true, " 144.300 ", false, null));
|
||||
}
|
||||
@@ -124,7 +124,7 @@ class MessageBusManagementThreadAutoAnswerDecisionTest {
|
||||
@Test
|
||||
void qrgRequestWinsOverTheGenericReply() {
|
||||
assertEquals(
|
||||
"/CQ DL1ABC " + MARKER + " QRG is: 144.300",
|
||||
"/CQ DL1ABC " + MARKER + " QRG is: 144.300 *a*",
|
||||
MessageBusManagementThread.buildAutoAnswerMessageText(
|
||||
"DL1ABC", VUHF, true, "144.300", true, "not qrv"));
|
||||
}
|
||||
@@ -143,7 +143,7 @@ class MessageBusManagementThreadAutoAnswerDecisionTest {
|
||||
@Test
|
||||
void buildsTheGenericReplyTrimmed() {
|
||||
assertEquals(
|
||||
"/CQ DL1ABC " + MARKER + " not qrv, back at 18z",
|
||||
"/CQ DL1ABC " + MARKER + " not qrv, back at 18z *a*",
|
||||
MessageBusManagementThread.buildAutoAnswerMessageText(
|
||||
"DL1ABC", VUHF, false, "144.300", true, " not qrv, back at 18z "));
|
||||
}
|
||||
@@ -169,7 +169,7 @@ class MessageBusManagementThreadAutoAnswerDecisionTest {
|
||||
@Test
|
||||
void senderCallSignIsTrimmedButNotUpperCased() {
|
||||
assertEquals(
|
||||
"/CQ dl1abc " + MARKER + " QRG is: 144.300",
|
||||
"/CQ dl1abc " + MARKER + " QRG is: 144.300 *a*",
|
||||
MessageBusManagementThread.buildAutoAnswerMessageText(
|
||||
" dl1abc ", VUHF, true, "144.300", false, null));
|
||||
}
|
||||
|
||||
+7
-7
@@ -104,7 +104,7 @@ class MessageBusManagementThreadAutoAnswerTest {
|
||||
ChatMessage reply = replies.get(0);
|
||||
assertEquals(
|
||||
"/CQ " + REMOTE_CALL + " " + ApplicationConstants.AUTOANSWER_PREFIX
|
||||
+ " QRG is: " + FIRST_QRG,
|
||||
+ " QRG is: " + FIRST_QRG + " *a*",
|
||||
reply.getMessageText());
|
||||
assertEquals(MY_CALL, reply.getSender().getCallSign());
|
||||
assertEquals(REMOTE_CALL, reply.getReceiver().getCallSign());
|
||||
@@ -152,7 +152,7 @@ class MessageBusManagementThreadAutoAnswerTest {
|
||||
"another station and another category each get their own reply");
|
||||
assertEquals(
|
||||
"/CQ " + REMOTE_CALL + " " + ApplicationConstants.AUTOANSWER_PREFIX
|
||||
+ " QRG is: " + SECOND_QRG,
|
||||
+ " QRG is: " + SECOND_QRG + " *a*",
|
||||
replies.get(2).getMessageText(),
|
||||
"the second-category question is answered with the second QRG");
|
||||
assertSame(secondCategory, replies.get(2).getChatCategory());
|
||||
@@ -232,7 +232,7 @@ class MessageBusManagementThreadAutoAnswerTest {
|
||||
"the rejected reply must not have started the cooldown");
|
||||
assertEquals(
|
||||
"/CQ " + REMOTE_CALL + " " + ApplicationConstants.AUTOANSWER_PREFIX
|
||||
+ " QRG is: " + FIRST_QRG,
|
||||
+ " QRG is: " + FIRST_QRG + " *a*",
|
||||
txBus.peek().getMessageText());
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ class MessageBusManagementThreadAutoAnswerTest {
|
||||
assertEquals(1, txBus.size());
|
||||
assertEquals(
|
||||
"/CQ " + REMOTE_CALL + " " + ApplicationConstants.AUTOANSWER_PREFIX
|
||||
+ " not qrv, back at 18z",
|
||||
+ " not qrv, back at 18z *a*",
|
||||
txBus.peek().getMessageText(),
|
||||
"the configured text is trimmed");
|
||||
}
|
||||
@@ -265,7 +265,7 @@ class MessageBusManagementThreadAutoAnswerTest {
|
||||
assertEquals(1, txBus.size());
|
||||
assertEquals(
|
||||
"/CQ " + REMOTE_CALL + " " + ApplicationConstants.AUTOANSWER_PREFIX
|
||||
+ " QRG is: " + FIRST_QRG,
|
||||
+ " QRG is: " + FIRST_QRG + " *a*",
|
||||
txBus.peek().getMessageText());
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ class MessageBusManagementThreadAutoAnswerTest {
|
||||
|
||||
assertEquals(1, txBus.size(),
|
||||
"the blank generic reply is dropped; the QRG reply still goes out");
|
||||
assertTrue(txBus.peek().getMessageText().endsWith("QRG is: " + FIRST_QRG));
|
||||
assertTrue(txBus.peek().getMessageText().endsWith("QRG is: " + FIRST_QRG + " *a*"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -295,7 +295,7 @@ class MessageBusManagementThreadAutoAnswerTest {
|
||||
assertEquals(
|
||||
"/CQ " + REMOTE_CALL + " " + ApplicationConstants.AUTOANSWER_PREFIX
|
||||
+ " KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION
|
||||
+ " by DO5AMF",
|
||||
+ " by DO5AMF *a*",
|
||||
txBus.peek().getMessageText());
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ class MessageBusManagementThreadChatFrameTest {
|
||||
assertEquals(
|
||||
"/CQ " + REMOTE_CALL + " " + ApplicationConstants.AUTOANSWER_PREFIX
|
||||
+ " KST4Contest v" + ApplicationConstants.APPLICATION_CURRENT_VERSION
|
||||
+ " by DO5AMF",
|
||||
+ " by DO5AMF *a*",
|
||||
txBus.peek().getMessageText());
|
||||
verify(controller, times(1)).publishChatMessage(any(ChatMessage.class));
|
||||
}
|
||||
|
||||
@@ -89,13 +89,32 @@ class On4KstProtocolTest {
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
"calling cq at 144.300",
|
||||
"calling cq at 144.300 *a*",
|
||||
controller.resolveAndValidateBeaconText(
|
||||
"calling cq at MYQRG"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsThe120CharacterPayloadAllowanceWithA124CharacterFinalBeacon() {
|
||||
ChatPreferences preferences = new ChatPreferences(tempDir.resolve("preferences.xml"));
|
||||
ChatController controller = new ChatController();
|
||||
controller.setChatPreferences(preferences);
|
||||
String payload = "x".repeat(120);
|
||||
|
||||
assertDoesNotThrow(() -> controller.validateBeaconTemplate(payload));
|
||||
|
||||
String finalMessage = controller.resolveAndValidateBeaconText(payload);
|
||||
assertEquals(124, finalMessage.length());
|
||||
assertEquals(payload + " *a*", finalMessage);
|
||||
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate("x".repeat(121))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsTemporarilyUnresolvedVariableOnlyBeaconTemplate() {
|
||||
ChatPreferences preferences = new ChatPreferences(tempDir.resolve("preferences.xml"));
|
||||
@@ -136,10 +155,7 @@ class On4KstProtocolTest {
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> controller.validateBeaconTemplate(
|
||||
"x".repeat(
|
||||
ChatController.MAX_BEACON_TEXT_LENGTH
|
||||
+ 1
|
||||
)
|
||||
"x".repeat(121)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
@@ -105,6 +108,7 @@ class ReadUDPbyUCXMessageThreadLoopbackTest {
|
||||
when(preferences.isTrxSynch_ucxLogUDPListenerEnabled()).thenReturn(true);
|
||||
SimpleValue<String> qrg = new SimpleValue<>("");
|
||||
when(preferences.getMYQRGFirstCat()).thenReturn(qrg);
|
||||
stubExternalQrgWrites(qrg);
|
||||
CountDownLatch firstQrg = new CountDownLatch(1);
|
||||
CountDownLatch secondQrg = new CountDownLatch(1);
|
||||
qrg.addListener(value -> {
|
||||
@@ -230,6 +234,29 @@ class ReadUDPbyUCXMessageThreadLoopbackTest {
|
||||
assertEquals("1296.200.00", markerQrg.get());
|
||||
}
|
||||
|
||||
/** A malformed Freq is expected external input and must be ignored without the safety net. */
|
||||
@Test
|
||||
void malformedRadioInfoFrequencyIsSkippedWithoutAnUnexpectedError(@TempDir Path tempDir) throws Exception {
|
||||
givenQsoProcessingEnabled(tempDir.resolve("udp-backup.txt"));
|
||||
|
||||
sendThenAwaitMarker("<RadioInfo><Freq>not-a-number</Freq><Mode>CW</Mode></RadioInfo>");
|
||||
|
||||
assertEquals("1296.200.00", markerQrg.get());
|
||||
assertFalse(statusMessages.stream()
|
||||
.anyMatch(message -> message.getRunningInformation().startsWith("packet dropped after error")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonPositiveRadioInfoFrequencyIsIgnoredBeforeItReachesTheController(@TempDir Path tempDir)
|
||||
throws Exception {
|
||||
givenQsoProcessingEnabled(tempDir.resolve("udp-backup.txt"));
|
||||
|
||||
sendThenAwaitMarker("<RadioInfo><Freq>0</Freq><Mode>CW</Mode></RadioInfo>");
|
||||
|
||||
verify(controller, never()).applyExternalQrg(any(RadioSlot.class), eq("0"), eq(0L));
|
||||
assertEquals("1296.200.00", markerQrg.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Any runtime exception while processing one packet drops that packet,
|
||||
* reports a non-critical status and leaves the receive loop running.
|
||||
@@ -275,6 +302,16 @@ class ReadUDPbyUCXMessageThreadLoopbackTest {
|
||||
when(controller.uiDispatcher()).thenReturn(new DirectUiDispatcher());
|
||||
when(preferences.isTrxSynch_ucxLogUDPListenerEnabled()).thenReturn(true);
|
||||
when(preferences.getMYQRGFirstCat()).thenReturn(markerQrg);
|
||||
stubExternalQrgWrites(markerQrg);
|
||||
}
|
||||
|
||||
private void stubExternalQrgWrites(SimpleValue<String> firstQrg) {
|
||||
doAnswer(invocation -> {
|
||||
RadioSlot slot = invocation.getArgument(0);
|
||||
String value = invocation.getArgument(1);
|
||||
(slot == RadioSlot.FIRST ? firstQrg : secondQrg).set(value);
|
||||
return null;
|
||||
}).when(controller).applyExternalQrg(any(RadioSlot.class), anyString(), anyLong());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,12 +5,10 @@ import kst4contest.observe.DirectUiDispatcher;
|
||||
import kst4contest.observe.SimpleValue;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/** A Win-Test STATUS of the second station writes SECONDQRG when SO2R is on. */
|
||||
@@ -18,18 +16,10 @@ class WinTestStatusSlotTest {
|
||||
|
||||
/* STATUS layout as parseStatus reads it: token 1 station, 5 mode, 7 frequency in 100 Hz. */
|
||||
private static final String STATUS_STN2 = "STATUS: \"STN2\" \"\" 0 12 0 0 4322000 0";
|
||||
private static final String STATUS_STN1 = "STATUS: \"STN1\" \"\" 0 12 0 0 1443000 0";
|
||||
|
||||
private final ChatController controller = mock(ChatController.class);
|
||||
private final ChatPreferences prefs = mock(ChatPreferences.class);
|
||||
private final List<String> first = new ArrayList<>();
|
||||
private final List<String> second = new ArrayList<>();
|
||||
|
||||
private static SimpleValue<String> recording(List<String> writes) {
|
||||
SimpleValue<String> value = new SimpleValue<>("");
|
||||
value.addListener(writes::add);
|
||||
return value;
|
||||
}
|
||||
|
||||
private ReadUDPByWintestThread thread(boolean so2r) {
|
||||
when(controller.getChatPreferences()).thenReturn(prefs);
|
||||
when(controller.uiDispatcher()).thenReturn(new DirectUiDispatcher());
|
||||
@@ -37,8 +27,6 @@ class WinTestStatusSlotTest {
|
||||
when(prefs.isTrxSynch_so2rEnabled()).thenReturn(so2r);
|
||||
when(prefs.getLogsynch_wintestNetworkStationNameOfWintestClient1()).thenReturn("STN1");
|
||||
when(prefs.getLogsynch_wintestNetworkStationNameOfWintestClient2()).thenReturn("STN2");
|
||||
when(prefs.getMYQRGFirstCat()).thenReturn(recording(first));
|
||||
when(prefs.getMYQRGSecondCat()).thenReturn(recording(second));
|
||||
return new ReadUDPByWintestThread(controller, mock(ThreadStatusCallback.class));
|
||||
}
|
||||
|
||||
@@ -46,15 +34,21 @@ class WinTestStatusSlotTest {
|
||||
void withSo2rTheSecondStationWritesTheSecondQrg() {
|
||||
thread(true).parseStatus(STATUS_STN2);
|
||||
|
||||
assertEquals(1, second.size());
|
||||
assertTrue(first.isEmpty());
|
||||
verify(controller).applyExternalQrg(RadioSlot.SECOND, "432.200.00", 432_200_000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutSo2rTheSecondStationIsFilteredOutAsBefore() {
|
||||
thread(false).parseStatus(STATUS_STN2);
|
||||
|
||||
assertTrue(first.isEmpty());
|
||||
assertTrue(second.isEmpty());
|
||||
org.mockito.Mockito.verify(controller, org.mockito.Mockito.never())
|
||||
.applyExternalQrg(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutSo2rTheFirstStationUpdatesOnlyTheMainSlotWithExactHz() {
|
||||
thread(false).parseStatus(STATUS_STN1);
|
||||
|
||||
verify(controller).applyExternalQrg(RadioSlot.FIRST, "144.300.00", 144_300_000L);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,31 +25,60 @@ class ChatPreferencesBeaconStateTest {
|
||||
preferences.bcnBeaconsEnabledMainCatProperty().addListener(mainChanges::add);
|
||||
preferences.bcnBeaconsEnabledSecondCatProperty().addListener(secondChanges::add);
|
||||
|
||||
preferences.setBcn_beaconsEnabledMainCat(true);
|
||||
preferences.setBcn_beaconsEnabledMainCat(false);
|
||||
preferences.setBcn_beaconsEnabledSecondCat(true);
|
||||
|
||||
assertFalse(preferences.isBcn_beaconsEnabledMainCat());
|
||||
assertTrue(preferences.isBcn_beaconsEnabledSecondCat());
|
||||
assertEquals(List.of(false), mainChanges);
|
||||
assertEquals(List.of(true, false), mainChanges);
|
||||
assertEquals(List.of(true), secondChanges);
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitWritePersistsBeaconActivationButAnUnsavedChangeDoesNot() {
|
||||
void activationIsSessionOnlyAndEveryFullWriteStoresFalse() throws Exception {
|
||||
ChatPreferences preferences = preferences("persistence.xml");
|
||||
preferences.setBcn_beaconsEnabledMainCat(false);
|
||||
preferences.setBcn_beaconsEnabledMainCat(true);
|
||||
preferences.setBcn_beaconsEnabledSecondCat(true);
|
||||
assertTrue(preferences.writePreferencesToXmlFile());
|
||||
|
||||
String xml = Files.readString(tempDir.resolve("persistence.xml"));
|
||||
assertTrue(xml.contains("<beaconCQEnabled>false</beaconCQEnabled>"));
|
||||
assertTrue(xml.contains("<bcn_beaconsEnabledMainCat>false</bcn_beaconsEnabledMainCat>"));
|
||||
assertTrue(xml.contains("<beaconCQEnabledSecondCat>false</beaconCQEnabledSecondCat>"));
|
||||
assertTrue(xml.contains("<bcn_beaconsEnabledSecondCat>false</bcn_beaconsEnabledSecondCat>"));
|
||||
|
||||
preferences.setBcn_beaconsEnabledMainCat(true);
|
||||
preferences.setBcn_beaconsEnabledSecondCat(false);
|
||||
preferences.setBcn_beaconsEnabledSecondCat(true);
|
||||
assertTrue(preferences.writeLayoutPreferencesToXmlFile());
|
||||
|
||||
ChatPreferences restored = preferences("persistence.xml");
|
||||
restored.readPreferencesFromXmlFile();
|
||||
|
||||
assertFalse(restored.isBcn_beaconsEnabledMainCat());
|
||||
assertTrue(restored.isBcn_beaconsEnabledSecondCat());
|
||||
assertFalse(restored.isBcn_beaconsEnabledSecondCat());
|
||||
}
|
||||
|
||||
@Test
|
||||
void storedActivationIsIgnoredAndIntervalsBelowFiveAreNormalized() throws Exception {
|
||||
Path file = tempDir.resolve("legacy.xml");
|
||||
Files.writeString(file, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<praktiKST><beaconCQ>
|
||||
<beaconCQEnabled>true</beaconCQEnabled>
|
||||
<beaconCQEnabledSecondCat>true</beaconCQEnabledSecondCat>
|
||||
<beaconCQIntervalMinutes>1</beaconCQIntervalMinutes>
|
||||
<beaconCQIntervalMinutesSecondCat>9</beaconCQIntervalMinutesSecondCat>
|
||||
</beaconCQ></praktiKST>
|
||||
""");
|
||||
|
||||
ChatPreferences restored = new ChatPreferences(file);
|
||||
assertTrue(restored.readPreferencesFromXmlFile());
|
||||
|
||||
assertFalse(restored.isBcn_beaconsEnabledMainCat());
|
||||
assertFalse(restored.isBcn_beaconsEnabledSecondCat());
|
||||
assertEquals(5, restored.getBcn_beaconIntervalInMinutesMainCat());
|
||||
assertEquals(9, restored.getBcn_beaconIntervalInMinutesSecondCat());
|
||||
}
|
||||
|
||||
private ChatPreferences preferences(String fileName) {
|
||||
|
||||
@@ -64,6 +64,10 @@ class ChatPreferencesRoundTripTest {
|
||||
Map.entry("setMessageHandling_autoAnswerEnabledSecondCat",
|
||||
"Same deliberate mirroring as the second-category auto-answer text; the "
|
||||
+ "main-category flag wins after every read"),
|
||||
Map.entry("setBcn_beaconsEnabledMainCat",
|
||||
"Session-only safety switch; every XML write stores false and every read starts false"),
|
||||
Map.entry("setBcn_beaconsEnabledSecondCat",
|
||||
"Session-only safety switch; every XML write stores false and every read starts false"),
|
||||
Map.entry("setBcn_contestScoreSum",
|
||||
"DEAD FIELD: declared with accessors only, no caller in core or app-desktop, "
|
||||
+ "never written nor read")
|
||||
|
||||
+16
-3
@@ -215,10 +215,12 @@ CR/LF framing, XML framing, ports/transports, callsign normalization and frequen
|
||||
## Autoanswer / Beacon
|
||||
|
||||
- Automated-message loops must be prevented.
|
||||
- Cooldown/minimum-interval rules must be preserved.
|
||||
- Every user-visible automatic chat message generated by KST4Contest ends with the exact suffix ` *a*`: generic and QRG-specific automatic replies, `//VER` replies and both category beacons. Automatic replies retain their `[KST4C Automsg]` prefix as the loop guard. The suffix is appended unconditionally after message construction or beacon variable resolution; manual messages and internal login, keepalive and control frames are unchanged. Beacon content remains limited to 120 characters before the suffix, making 124 characters the final transmitted maximum.
|
||||
- Cooldown/minimum-interval rules must be preserved. The shared beacon interval has a hard minimum of five minutes; older stored values below five are normalized while loading.
|
||||
- A reply rejected before a complete valid TX item is queued must not consume cooldown.
|
||||
- Both category beacons share one timer, while their enable flags and templates remain profile-scoped and independent. The enable flags are observable runtime preferences: Settings changes and the `MyBeacon` status-chip action update them immediately, and every timer run reads their current values without restarting the timer or causing an immediate transmission. Only the explicit full settings write persists those flags; layout autosave deliberately leaves the on-disk functional state untouched.
|
||||
- `MyBeacon` owns one aggregate thread-status entry for both categories (`off`, a partial `enabled/available` count, `on`, or `FAILED`). Its action disables all currently usable beacons if any is enabled, otherwise it enables all currently usable beacons. A disabled second chat is not usable and its dormant enable flag must not be changed by the aggregate action. Category-specific timer failures remain authoritative while the affected beacon stays active; independent switch changes cannot hide them. The status bar resolves the aggregate from current controller state when a queued UI update is applied, so a delayed timer snapshot cannot overwrite a newer manual switch. Open Settings views observe the same preference values, so changes from the status chip remain synchronized without a parallel UI state.
|
||||
- Both category beacons share one timer, while their message templates remain profile-scoped and independent. `BeaconSessionState` is the authoritative owner of activation, per-category QRG references, deactivation reasons and send failures. Activations are never restored from XML: both start off after program startup and every profile switch, remain unchanged during an ON4KST reconnect in the same application session, and are projected into the legacy observable preference booleans for Settings/UI compatibility. Full preference writes emit both legacy/current enable tags as `false`.
|
||||
- Enabling validates QRG placeholders across either template: `MYQRG` and `MYQRGSHORT` require a valid primary QRG; `SECONDQRG` requires a valid secondary QRG. A template without a QRG placeholder may be enabled without a current QRG, and the first later valid own-category QRG establishes its reference. A valid manual QRG change always rebases the corresponding active beacon. A UCXLog or Win-Test update more than 5,000 Hz away from the reference disables only that category; exactly 5,000 Hz remains allowed. Without SO2R, logger updates reach the primary category only; with SO2R each radio/category is handled independently. A missing/invalid required QRG disables an active beacon, while an invalid own QRG without a QRG placeholder only clears its reference.
|
||||
- `MyBeacon` owns one structured aggregate thread-status snapshot for both categories (`off`, `1/2`, `on`, or `FAILED`). Its action disables all currently usable beacons if any is enabled, otherwise it attempts to enable each usable beacon independently, so one invalid category can remain off. The tooltip derives the state or last general deactivation reason for each category from the same snapshot. Active states use the dedicated green treatment with a 200 ms orange activity flash; `off` keeps a neutral surface with red text; `FAILED` uses a persistent red surface with white text and suppresses the activity flash. Other thread-status chips keep their existing styling.
|
||||
- Current implementation/tests define the exact message markers and timer details.
|
||||
|
||||
## Build / Verification
|
||||
@@ -417,10 +419,21 @@ Before implementing planned items, re-check current decisions and obtain a fresh
|
||||
- Historical project context is useful but may be stale; current code/tests win.
|
||||
- External service/API behaviour must be verified against current upstream documentation when uncertain.
|
||||
- Screenshots in manuals/website may need targeted replacement after visible UI changes; never fabricate them.
|
||||
- `client_settings_window_beacon.png` was recaptured from the current Compose UI in a live application session on 2026-11-13. It shows both beacons disabled after a profile switch and the five-minute minimum interval.
|
||||
- `station_map_path_analysis.png` and `station_map_reset.png` predate the current header controls, callsign-only markers, shared station context menu and Worked-square overlay. The former remains useful only where the visible analysis panel itself is the subject. `station_map_spatial_filters.png` is the current general map screenshot shared by the DE/EN interface manuals and the website through `/manual/assets/`.
|
||||
|
||||
## Recent Significant Changes
|
||||
|
||||
### 2026-11-13 – Automatic chat messages marked with a suffix
|
||||
|
||||
- Generic and QRG-specific automatic replies, `//VER` replies and both beacons now carry the exact ` *a*` suffix. Existing auto-reply loop protection remains based on `[KST4C Automsg]`; manual chat and ON4KST control traffic are unchanged.
|
||||
- Beacon templates and resolved payloads retain the 120-character limit. The fixed suffix is added afterwards, so a transmitted beacon may contain up to 124 characters.
|
||||
|
||||
### 2026-11-12 – Beacon activation and QRG safety made session-scoped
|
||||
|
||||
- Beacon activation is now explicit per application session, while ordinary ON4KST reconnects preserve it. Per-category references distinguish deliberate manual QRG changes from UCXLog/Win-Test moves and disable only the affected beacon after a change greater than 5 kHz.
|
||||
- The structured aggregate beacon status keeps per-category deactivation reasons and drives the dedicated green/orange/red `MyBeacon` chip without changing other thread-status chips.
|
||||
|
||||
### 2026-10-17 – Repository moved to the HamRadioOnline organisation
|
||||
|
||||
- The GitHub repository moved from `praktimarc/kst4contest` to `HamRadioOnline/kst4contest`. GitHub redirects the old repository, Pages, API and git URLs, so published links and release assets from before the move keep working.
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
# Beacon Session Safety Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make beacon activation session-only, enforce a five-minute minimum, disable the correct category after an external QRG move beyond 5 kHz, and render a reason-aware traffic-colour `MyBeacon` chip.
|
||||
|
||||
**Architecture:** A controller-owned `BeaconSessionState` coordinates the existing observable runtime flags, category references, inactive reasons and send failures. Manual QRG commits and UCXLog/Win-Test reports enter through distinct controller methods; a structured model snapshot crosses the existing thread-status boundary and `app-desktop` localises and styles it.
|
||||
|
||||
**Tech Stack:** Java 21, Kotlin 2.2, Compose Multiplatform 1.8.x, JUnit 5, Mockito, Gradle wrapper
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-11-11-beacon-session-safety-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Work directly on `main`, as explicitly authorised.
|
||||
- Do not stage, commit, push, merge, tag, release or deploy; commit steps are deliberately omitted because Marc authorised none of them.
|
||||
- Keep source comments and Javadoc in English; user-facing DE/EN text follows the project writing style.
|
||||
- Use exactly 5 minutes and 5,000 Hz; exactly 5,000 Hz must not disable a beacon.
|
||||
- Do not add a production dependency or change ON4KST/UCXLog/Win-Test framing.
|
||||
- Keep the shared timer, initial ten-second check, outbound queue and second-chat eligibility unchanged.
|
||||
- Do not execute UI callbacks while holding an internal beacon-state lock.
|
||||
- Preserve unrelated local changes and inspect the diff before and after every task.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- A secondary beacon using `MYQRG` must be refused or disabled when the primary QRG is invalid, even though its own reference remains the secondary QRG; Task 1 pins this cross-template case.
|
||||
- An external report with no reference must establish the reference without disabling; Task 1 pins this first-report case.
|
||||
- Without SO2R a Win-Test/UCXLog report must affect only the main beacon; Task 3 pins both integrations.
|
||||
- An old XML file with enabled flags and a one-minute interval must load as disabled with a five-minute interval and later write `false`; Task 2 pins the migration.
|
||||
- `FAILED` must stay red without an orange flash while ordinary worker chips retain current theme colours; Task 4 pins both sides.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Pure beacon session policy and structured status
|
||||
|
||||
**Files:**
|
||||
- Create: `core/src/main/java/kst4contest/controller/BeaconFrequencyParser.java`
|
||||
- Create: `core/src/main/java/kst4contest/controller/BeaconSessionState.java`
|
||||
- Create: `core/src/main/java/kst4contest/model/BeaconStatusSnapshot.java`
|
||||
- Create: `core/src/test/java/kst4contest/controller/BeaconFrequencyParserTest.java`
|
||||
- Create: `core/src/test/java/kst4contest/controller/BeaconSessionStateTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `BeaconFrequencyParser.parseHz(String): OptionalLong`.
|
||||
- Produces: `BeaconSessionState.Channel { MAIN, SECOND }` and synchronized policy methods `resetSession(ChatPreferences)`, `setEnabled(Channel, boolean, ChatPreferences)`, `onManualQrgChanged(Channel, String, ChatPreferences)`, `onExternalQrgChanged(Channel, long, ChatPreferences)`, `onTemplateChanged(Channel, ChatPreferences)`, `recordSendFailure(Channel, boolean)` and `snapshot(ChatPreferences): BeaconStatusSnapshot`.
|
||||
- Produces: explicit `BeaconStatusSnapshot` DTO with aggregate state `OFF`, `PARTIAL`, `ON`, `FAILED`, plus main/second category DTOs carrying availability, enablement, failure and `InactiveReason`.
|
||||
|
||||
- [ ] **Step 1: Write parser tests** for `144.300`, `144,300`, `144.300.03`, `1296.123.000`, compact `144300`, compact `10368100`, blanks, malformed text, non-positive values and overflow.
|
||||
- [ ] **Step 2: Run the parser test and verify RED.**
|
||||
|
||||
Run: `.\gradlew.bat :core:test --tests kst4contest.controller.BeaconFrequencyParserTest --console=plain`
|
||||
|
||||
Expected: compilation failure because `BeaconFrequencyParser` does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement `BeaconFrequencyParser`** with integer arithmetic and the QRG forms already used by the client; accept only a complete positive frequency that maps to a supported `Band`.
|
||||
- [ ] **Step 4: Re-run the parser test and verify GREEN.**
|
||||
- [ ] **Step 5: Write session-policy tests** for initial reasons, independent enablement, placeholder requirements in either template, partial aggregate activation, manual reference creation/rebase, invalid manual clearing, cross-template invalidation, first external reference, 5,000/5,001-Hz boundaries, per-category external changes, reason retention and send-failure clearing.
|
||||
- [ ] **Step 6: Run `BeaconSessionStateTest` and verify RED** because the policy and DTO are absent.
|
||||
- [ ] **Step 7: Implement the DTO and policy** so the existing observable preference booleans are projection only and no listener/callback leaves a synchronized method.
|
||||
- [ ] **Step 8: Re-run both focused tests and verify GREEN.**
|
||||
- [ ] **Step 9: Inspect the task diff** for duplicated state, floating-point frequency comparisons or display strings in `core`.
|
||||
|
||||
### Task 2: Session-only persistence and five-minute migration
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/src/main/java/kst4contest/model/ChatPreferences.java`
|
||||
- Modify: `core/src/main/java/kst4contest/controller/ChatController.java`
|
||||
- Modify: `core/src/main/resources/praktiKSTpreferences.xml`
|
||||
- Modify: `core/src/test/java/kst4contest/model/ChatPreferencesBeaconStateTest.java`
|
||||
- Modify: `core/src/test/java/kst4contest/model/ChatPreferencesRoundTripTest.java`
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/BeaconTabStateTest.kt`
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/i18n/RemainingTextsFollowTheLanguageTest.kt`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 `BeaconSessionState`.
|
||||
- Produces: `ChatController.MIN_BEACON_INTERVAL_MINUTES == 5` and session reset during controller/profile construction without reset on reconnect.
|
||||
|
||||
- [ ] **Step 1: Replace persistence expectations with failing tests** proving defaults are off, stored `true` is ignored, every full write stores both legacy/current enable tags as `false`, layout-only writes do not revive them, and intervals below five normalise in memory.
|
||||
- [ ] **Step 2: Add interval tests** proving four is refused, five accepted, and an old stored one becomes five without changing a larger value.
|
||||
- [ ] **Step 3: Run the focused core and desktop tests and verify RED.**
|
||||
|
||||
Run: `.\gradlew.bat :core:test --tests kst4contest.model.ChatPreferencesBeaconStateTest --tests kst4contest.model.ChatPreferencesRoundTripTest :app-desktop:test --tests kst4contest.view.compose.tabs.BeaconTabStateTest --tests kst4contest.view.i18n.RemainingTextsFollowTheLanguageTest --console=plain`
|
||||
|
||||
- [ ] **Step 4: Make enable flags session-only**: initialise both false, ignore XML enable values, always serialise `false`, retain existing element names, and reset the policy only when a new controller/profile preferences object is installed.
|
||||
- [ ] **Step 5: Set and enforce the five-minute minimum** in controller, XML defaults and preference-load normalisation; retain timer start/restart semantics.
|
||||
- [ ] **Step 6: Re-run the focused tests and inspect their XML summaries.**
|
||||
- [ ] **Step 7: Inspect the task diff** to confirm no reconnect path resets the session and no default was silently raised to ten minutes.
|
||||
|
||||
### Task 3: Explicit manual and external QRG paths
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/src/main/java/kst4contest/controller/ChatController.java`
|
||||
- Modify: `core/src/main/java/kst4contest/controller/ReadUDPbyUCXMessageThread.java`
|
||||
- Modify: `core/src/main/java/kst4contest/controller/ReadUDPByWintestThread.java`
|
||||
- Modify: `app-desktop/src/main/kotlin/kst4contest/view/compose/ChatInputState.kt`
|
||||
- Modify: `app-desktop/src/main/kotlin/kst4contest/view/compose/ChatInputRow.kt`
|
||||
- Modify: `app-desktop/src/main/java/kst4contest/view/ComposeChatInputActions.java`
|
||||
- Modify: `app-desktop/src/main/kotlin/kst4contest/view/compose/tabs/BeaconTabState.kt`
|
||||
- Modify: `app-desktop/src/main/kotlin/kst4contest/view/compose/SettingsTabs.kt`
|
||||
- Modify: focused fake `ChatInputActions` implementations in affected tests
|
||||
- Modify: `core/src/test/java/kst4contest/controller/BeaconControlTest.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/ReadUDPbyUCXMessageThreadLoopbackTest.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/WinTestStatusSlotTest.java`
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/compose/ChatInputStateTest.kt`
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/compose/tabs/BeaconTabStateTest.kt`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: controller methods `applyManualQrg(RadioSlot, String)`, `applyExternalQrg(RadioSlot, String, long)`, `onMainBeaconTemplateApplied()` and `onSecondBeaconTemplateApplied()`.
|
||||
- Produces: a `ChatInputActions`/`ChatInputState` manual-QRG commit method used by `ChatInputRow` instead of writing preferences directly.
|
||||
- Consumes: Task 1 policy and Task 2 session lifecycle.
|
||||
|
||||
- [ ] **Step 1: Add failing controller tests** for individual activation refusal, valid partial chip activation, user disable reason, manual rebase, invalid manual cross-template disabling and template-apply revalidation.
|
||||
- [ ] **Step 2: Add failing chat-input tests** proving both manual fields call the explicit action with the correct `RadioSlot`/category and never write preferences directly.
|
||||
- [ ] **Step 3: Add failing UCXLog tests** for exact Hz conversion, first-reference behaviour, >5-kHz main-only shutdown without SO2R and second-only shutdown for RadioNr 2 with SO2R.
|
||||
- [ ] **Step 4: Add failing Win-Test tests** for main/pass frequency selection, 100-Hz units, non-SO2R main-only and SO2R slot routing.
|
||||
- [ ] **Step 5: Run the four focused test groups and verify RED.**
|
||||
- [ ] **Step 6: Implement the controller entry points** so the preference value and policy update occur together on the existing UI-dispatch boundary and publish a fresh beacon status only after state is consistent.
|
||||
- [ ] **Step 7: Route manual Compose commits through `ComposeChatInputActions`** while preserving focus/commit behaviour and existing displayed `ObservedValue` mirrors.
|
||||
- [ ] **Step 8: Give both template draft properties a shared post-apply effect key** that revalidates active beacons after all writes, without changing their validation-before-stage behaviour.
|
||||
- [ ] **Step 9: Route UCXLog and Win-Test through `applyExternalQrg`** only when their current QRG-sync setting already permits the update; preserve packet parsing and diagnostics.
|
||||
- [ ] **Step 10: Re-run focused tests and inspect XML summaries.**
|
||||
- [ ] **Step 11: Run related integration tests** for SO2R routing, settings apply and chat-input rendering.
|
||||
- [ ] **Step 12: Inspect the task diff** for protocol/framing changes, toolkit calls in `core`, or source inference through listeners.
|
||||
|
||||
### Task 4: Beacon task status, translated tooltips and traffic colours
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/src/main/java/kst4contest/controller/BeaconTask.java`
|
||||
- Modify: `core/src/main/java/kst4contest/controller/BeaconStatus.java`
|
||||
- Modify: `core/src/main/java/kst4contest/model/ThreadStateMessage.java`
|
||||
- Create: `app-desktop/src/main/kotlin/kst4contest/view/compose/BeaconStatusPresentation.kt`
|
||||
- Modify: `app-desktop/src/main/kotlin/kst4contest/view/compose/StatusIndicatorState.kt`
|
||||
- Modify: `app-desktop/src/main/kotlin/kst4contest/view/compose/StatusBar.kt`
|
||||
- Modify: `app-desktop/src/main/i18n/strings_de.properties`
|
||||
- Modify: `app-desktop/src/main/i18n/strings_en.properties`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/BeaconTaskTest.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/BeaconControlTest.java`
|
||||
- Create: `app-desktop/src/test/kotlin/kst4contest/view/compose/BeaconStatusPresentationTest.kt`
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/compose/StatusIndicatorStateTest.kt`
|
||||
- Modify: `app-desktop/src/test/kotlin/kst4contest/view/compose/StatusBarScreenTest.kt`
|
||||
- Modify: relevant i18n boundary tests
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 `BeaconStatusSnapshot` and policy send-failure methods.
|
||||
- Produces: optional structured beacon snapshot on `ThreadStateMessage`; other thread messages remain unchanged.
|
||||
- Produces: `BeaconStatusPresentation` mapping snapshot plus current strings to label, tooltip and visual style.
|
||||
|
||||
- [ ] **Step 1: Add failing core tests** proving a failed active category yields `FAILED`, a later successful run clears only its failure, disabled categories are not failed, and ordinary timer runs preserve inactive reasons.
|
||||
- [ ] **Step 2: Add failing presentation tests** for bilingual per-category tooltips, initial/user/QRG/QRG-invalid reasons, `1/2`, and no concrete frequency in the QRG reason.
|
||||
- [ ] **Step 3: Add failing colour-model tests** for the exact light/dark OFF colours, ON/PARTIAL green, 200-ms orange highlight, FAILED red/white with highlight suppression and unchanged default-worker colours.
|
||||
- [ ] **Step 4: Run focused core and desktop tests and verify RED.**
|
||||
- [ ] **Step 5: Attach the structured snapshot to the existing status message** without adding translated display prose to `core`.
|
||||
- [ ] **Step 6: Make `BeaconTask` record per-category outcomes** while retaining text resolution, category validation and regular TX queue insertion.
|
||||
- [ ] **Step 7: Implement translated presentation and visual style** only for messages carrying a beacon snapshot; keep action/provider ordering and stale-update protection intact.
|
||||
- [ ] **Step 8: Implement the selected colours in `ThreadStatusButtonView`** and suppress the highlight when style is FAILED.
|
||||
- [ ] **Step 9: Add aligned DE/EN i18n keys** and re-run translation-boundary tests.
|
||||
- [ ] **Step 10: Re-run focused tests and inspect XML summaries.**
|
||||
- [ ] **Step 11: Run the affected Compose screen tests** and inspect a rendered light/dark result if the test harness emits one.
|
||||
- [ ] **Step 12: Inspect the task diff** to ensure no non-beacon chip acquired special colours or click behaviour.
|
||||
|
||||
### Task 5: Targeted documentation, context and full verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `github_docs/de-Konfiguration.md`
|
||||
- Modify: `github_docs/en-Configuration.md`
|
||||
- Modify: `github_docs/de-Contest-Workflow.md`
|
||||
- Modify: `github_docs/en-Contest-Workflow.md`
|
||||
- Modify: `github_docs/de-Makros-und-Variablen.md`
|
||||
- Modify: `github_docs/en-Macros-and-Variables.md`
|
||||
- Review/modify: `github_docs/de-Funktionen.md`, `github_docs/en-Features.md`
|
||||
- Review/modify: `github_docs/de-Benutzeroberflaeche.md`, `github_docs/en-User-Interface.md`
|
||||
- Modify: `github_docs/de-Changelog.md`
|
||||
- Modify: `github_docs/en-Changelog.md`
|
||||
- Modify: `website/src/features/macros.md`
|
||||
- Review/modify: `website/src/features/trx-qrg-synchronisation.md`
|
||||
- Modify: `docs/PROJECT_CONTEXT.md`
|
||||
- Modify: `docs/superpowers/specs/2026-11-04-live-beacon-control-design.md`
|
||||
- Review: `github_docs/client_settings_window_beacon.png`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: completed behaviour from Tasks 1-4.
|
||||
- Produces: semantically aligned DE/EN user documentation and current durable project context.
|
||||
|
||||
- [ ] **Step 1: Update the targeted manual sections** with session-only activation, five-minute minimum, activation prerequisites, source-specific reference/QSY rules, per-category tooltip and traffic colours.
|
||||
- [ ] **Step 2: Update both changelogs compactly** without inventing a version or release state.
|
||||
- [ ] **Step 3: Update only website sections that already describe interval or TRX-QRG behaviour**; do not duplicate the manual.
|
||||
- [ ] **Step 4: Reconcile the older live-beacon design and `PROJECT_CONTEXT`** so they no longer claim activation persists or the one-minute minimum remains unchanged.
|
||||
- [ ] **Step 5: Inspect the existing Beacon settings screenshot** and record replacement only if a visible value is now false; do not fabricate an image.
|
||||
- [ ] **Step 6: Run all focused test classes from Tasks 1-4.**
|
||||
- [ ] **Step 7: Run the full build.**
|
||||
|
||||
Run: `.\gradlew.bat build --console=plain`
|
||||
|
||||
Expected: `BUILD SUCCESSFUL` with no failed tests.
|
||||
|
||||
- [ ] **Step 8: Read every generated `*/build/test-results/**/TEST-*.xml` summary** and report test/failure/error/skipped totals rather than relying only on exit code.
|
||||
- [ ] **Step 9: Count PMD and SpotBugs XML findings per module** from `<violation` and `<BugInstance>` and identify findings touching changed production files.
|
||||
- [ ] **Step 10: Run `git diff --check` and inspect `git status --short`.**
|
||||
- [ ] **Step 11: Review the complete diff against the approved spec** for unrelated cleanup, stale documentation, accidental protocol changes and user-authored changes.
|
||||
- [ ] **Step 12: Perform a manual runtime check if safely possible**: initial off state, valid/invalid category activation, manual rebase, external >5-kHz shutdown, `1/2`, OFF tooltip, orange flash and persistent FAILED red.
|
||||
|
||||
## Self-Review Result
|
||||
|
||||
- Every approved behaviour maps to Tasks 1-4; documentation and durable context map to Task 5.
|
||||
- Exact types and controller entry points are defined once in the Interfaces blocks.
|
||||
- Cross-template QRG dependencies, first external reference, non-SO2R routing, XML migration and FAILED colour priority are explicit tests.
|
||||
- Plan size reflects five independently reviewable deliverables; no implementation bodies or unauthorised Git actions are included.
|
||||
@@ -0,0 +1,96 @@
|
||||
# Automatic Chat Suffix Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Append the fixed suffix ` *a*` to every active KST4Contest autoanswer and beacon while leaving manual and protocol traffic unchanged.
|
||||
|
||||
**Architecture:** A package-private `AutomaticChatMessageFormatter` owns the suffix and performs unconditional appending. The active autoanswer and beacon producers call it explicitly; beacon validation keeps a 120-character payload allowance and validates the resulting 124-character maximum.
|
||||
|
||||
**Tech Stack:** Java 21, Kotlin 2.2, Gradle wrapper, JUnit 5, Mockito
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-11-13-automatic-chat-suffix-design.md`
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- The exact suffix is ` *a*`, including its leading space.
|
||||
- `[KST4C Automsg]` remains on autoanswers and remains the loop-prevention marker.
|
||||
- Suffix insertion is unconditional; duplicate `*a*` markers are allowed.
|
||||
- Beacon payloads remain limited to 120 characters; complete suffixed beacons are limited to 124 characters.
|
||||
- Manual messages and internal ON4KST frames must remain byte-for-byte unaffected by this feature.
|
||||
- No new dependency, setting, persistence field, version bump, staging, commit or publication action.
|
||||
|
||||
## Review Focus
|
||||
|
||||
- A 120-character beacon payload must be sent as exactly 124 characters after suffix insertion.
|
||||
- A 121-character beacon payload must still be rejected rather than producing a 125-character frame.
|
||||
- Text already ending in `*a*` must receive a second suffix without deduplication.
|
||||
- Autoanswer validation and cooldown accounting must still reject invalid text before consuming cooldown.
|
||||
- The existing prefix-based ping-pong guard and all manual send paths must remain unchanged.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Shared automatic-message formatter and autoanswers
|
||||
|
||||
**Files:**
|
||||
- Create: `core/src/main/java/kst4contest/controller/AutomaticChatMessageFormatter.java`
|
||||
- Create: `core/src/test/java/kst4contest/controller/AutomaticChatMessageFormatterTest.java`
|
||||
- Modify: `core/src/main/java/kst4contest/controller/MessageBusManagementThread.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/MessageBusManagementThreadAutoAnswerDecisionTest.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/MessageBusManagementThreadAutoAnswerTest.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/MessageBusManagementThreadChatFrameTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `AutomaticChatMessageFormatter.appendSuffix(String messageText): String`
|
||||
- Consumes: existing `ApplicationConstants.AUTOANSWER_PREFIX` and `On4KstProtocol.messageText(String)`
|
||||
|
||||
- [x] Add failing formatter tests for exact suffix insertion and intentional duplicate suffixes.
|
||||
- [x] Run `\.\gradlew.bat :core:test --tests "kst4contest.controller.AutomaticChatMessageFormatterTest"` and confirm the tests fail because the formatter is absent.
|
||||
- [x] Implement the package-private formatter with `SUFFIX = " *a*"` and unconditional appending.
|
||||
- [x] Update failing autoanswer and version-reply expectations so generic, QRG and `//VER` replies end in ` *a*` while retaining `[KST4C Automsg]`.
|
||||
- [x] Append before final autoanswer protocol validation and in the `//VER` construction path.
|
||||
- [x] Run the formatter and three focused autoanswer test classes and confirm zero failures.
|
||||
|
||||
### Task 2: Beacon suffix and length boundaries
|
||||
|
||||
**Files:**
|
||||
- Modify: `core/src/main/java/kst4contest/controller/ChatController.java`
|
||||
- Modify: `core/src/main/java/kst4contest/controller/BeaconTask.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/On4KstProtocolTest.java`
|
||||
- Modify: `core/src/test/java/kst4contest/controller/BeaconTaskTest.java`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `AutomaticChatMessageFormatter.appendSuffix(String)` from Task 1
|
||||
- Produces: beacon payload validation at 120 characters and final-message validation at 124 characters
|
||||
|
||||
- [x] Add failing tests asserting that each queued beacon ends in ` *a*`, a 120-character payload produces a 124-character message, and a 121-character payload is rejected.
|
||||
- [x] Run `\.\gradlew.bat :core:test --tests "kst4contest.controller.On4KstProtocolTest" --tests "kst4contest.controller.BeaconTaskTest"` and confirm the new assertions fail.
|
||||
- [x] Separate the 120-character payload limit from the 124-character final beacon limit in `ChatController`.
|
||||
- [x] Append the suffix only after macro resolution and payload validation, then validate the final message before queueing.
|
||||
- [x] Run the focused beacon tests and confirm zero failures.
|
||||
|
||||
### Task 3: Targeted documentation and verification
|
||||
|
||||
**Files:**
|
||||
- Modify: `github_docs/de-Konfiguration.md`
|
||||
- Modify: `github_docs/en-Configuration.md`
|
||||
- Modify: `github_docs/de-Makros-und-Variablen.md`
|
||||
- Modify: `github_docs/en-Macros-and-Variables.md`
|
||||
- Modify: `github_docs/de-Funktionen.md`
|
||||
- Modify: `github_docs/en-Features.md`
|
||||
- Modify: `github_docs/de-Changelog.md`
|
||||
- Modify: `github_docs/en-Changelog.md`
|
||||
- Modify: `website/src/features/automatic-replies.md`
|
||||
- Modify: `website/src/features/macros.md`
|
||||
- Modify: `docs/PROJECT_CONTEXT.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: implemented suffix and length behaviour from Tasks 1 and 2
|
||||
- Produces: aligned German/English user documentation and durable current-state context
|
||||
|
||||
- [x] Update examples and behavioural text so autoanswers retain `[KST4C Automsg]` and all automated chat messages end in ` *a*`.
|
||||
- [x] Document the 120-character beacon payload and 124-character transmitted-message limits in German and English.
|
||||
- [x] Record the fixed suffix rule in the changelogs and `docs/PROJECT_CONTEXT.md` without changing unrelated documentation.
|
||||
- [x] Run the focused core tests from Tasks 1 and 2, then `\.\gradlew.bat build`.
|
||||
- [x] Read every Gradle test-result XML summary and report failures, errors and skipped tests; inspect PMD and SpotBugs XML counts produced by the build.
|
||||
- [x] Run the website test/build commands defined by the current `website/package.json` if the changed website pages participate in that build.
|
||||
- [x] Run `git diff --check` and review the complete scoped diff for accidental changes; perform no Git publication action.
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
Freigegeben von Marc am 2026-11-04.
|
||||
|
||||
> Durch das freigegebene [Beacon Session Safety Design](2026-11-11-beacon-session-safety-design.md)
|
||||
> abgelöst, soweit Aktivierungspersistenz, Mindestintervall, QRG-Sicherung und Chip-Darstellung
|
||||
> betroffen sind.
|
||||
|
||||
## Ziel
|
||||
|
||||
Die beiden Beacon-Aktivierungsschalter wirken in der laufenden Sitzung sofort. Der gemeinsame
|
||||
@@ -11,19 +15,19 @@ gemeinsam ein- oder ausschalten.
|
||||
|
||||
## Verbindliches Verhalten
|
||||
|
||||
- Die gespeicherten Beacon-Zustände werden beim Profilstart geladen.
|
||||
- Nach einem Programmstart und Profilwechsel sind beide Beacon-Zustände ausgeschaltet.
|
||||
- Ein normaler ON4KST-Reconnect innerhalb derselben Programmsitzung verändert sie nicht.
|
||||
- Ein Klick auf eine Beacon-Checkbox wirkt sofort im Speicher und damit beim nächsten
|
||||
regulären Timerlauf.
|
||||
- Aktivieren sendet nicht sofort und startet den Timer nicht neu.
|
||||
- Erst **Einstellungen speichern** schreibt die Zustände in die Profil-XML. Ohne explizites
|
||||
Speichern gilt die Änderung nur für die laufende Sitzung.
|
||||
- Aktivierungszustände werden nicht in die Profil-XML übernommen; ein vollständiger
|
||||
Einstellungsspeichervorgang schreibt die kompatiblen Aktivierungs-Tags als `false`.
|
||||
- Beacon-Text und Intervall bleiben normale Settings-Entwurfswerte. Das Intervall startet den
|
||||
Timer erst beim Anwenden neu.
|
||||
- Der Status-Chip zeigt `off`, `1/2` oder `on` für die aktuell nutzbaren Kategorien.
|
||||
- Der Status-Chip zeigt `off`, `1/2`, `on` oder `FAILED` für die aktuell nutzbaren Kategorien.
|
||||
- Sind alle nutzbaren Beacons aus, aktiviert ein Chip-Klick alle nutzbaren Beacons. Ist
|
||||
mindestens eine nutzbare Beacon an, deaktiviert der Klick alle nutzbaren Beacons.
|
||||
- Bei deaktiviertem zweiten Chat schaltet der Chip nur die Hauptkategorie und verändert das
|
||||
gespeicherte Flag der zweiten Kategorie nicht.
|
||||
- Bei deaktiviertem zweiten Chat schaltet der Chip nur die Hauptkategorie.
|
||||
- Chip und geöffnetes Settings-Fenster zeigen denselben beobachtbaren Zustand.
|
||||
- Nur der Beacon-Chip wird anklickbar. Andere Thread-Status-Chips bleiben reine Anzeigen.
|
||||
|
||||
@@ -32,15 +36,15 @@ gemeinsam ein- oder ausschalten.
|
||||
- Beide Kategorien behalten eigene Aktivierungsflags und Texte.
|
||||
- Der zweite Beacon-Versand setzt weiterhin einen aktivierten zweiten Chat voraus.
|
||||
- Nachrichtenvalidierung, TX-Warteschlange und ON4KST-Protokoll bleiben unverändert.
|
||||
- Mindestintervall, gemeinsamer Timer und Intervall-Persistenz bleiben unverändert.
|
||||
- Gemeinsamer Timer und Intervall-Persistenz bleiben unverändert; das Mindestintervall beträgt
|
||||
fünf Minuten.
|
||||
- Profilzustände werden nicht miteinander vermischt.
|
||||
|
||||
## Technischer Ansatz
|
||||
|
||||
Die vorhandenen `ChatPreferences`-Getter und -Setter bleiben die kompatible Schnittstelle,
|
||||
werden für die beiden Aktivierungsflags aber auf thread-sichere beobachtbare Werte gestellt.
|
||||
Der Settings-Tab liest diese Werte als Compose-State und schreibt Aktivierungen direkt durch
|
||||
den Controller. Der Controller veröffentlicht nach jeder manuellen Umschaltung einen
|
||||
zusammengefassten `MyBeacon`-Status. `BeaconTask` meldet nach einem Timerlauf ebenfalls genau
|
||||
diesen zusammengefassten Status. `ThreadStatusButtons` erhält eine optionale Aktion pro
|
||||
Quelle; die Anwendung registriert sie nur für `MyBeacon`.
|
||||
`BeaconSessionState` ist die maßgebliche sitzungsbezogene Quelle für Aktivierung,
|
||||
QRG-Referenzen, Abschaltgründe und Fehler. Die vorhandenen beobachtbaren
|
||||
`ChatPreferences`-Werte bleiben die kompatible Projektion für Settings und UI. Controller und
|
||||
`BeaconTask` veröffentlichen einen strukturierten `BeaconStatusSnapshot`, aus dem Chip-Text,
|
||||
Tooltip und die Beacon-spezifische Farbgebung entstehen. Details der QRG-Prüfung und
|
||||
Darstellung stehen im ablösenden Design vom 2026-11-11.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Beacon Session Safety Design
|
||||
|
||||
Freigegeben von Marc am 2026-11-11.
|
||||
|
||||
## Ziel
|
||||
|
||||
Die Beacon-Funktion darf nach einem Programmstart oder Profilwechsel nicht aus einer
|
||||
gespeicherten Aktivierung weiterlaufen. Jeder Beacon wird in der laufenden Sitzung bewusst
|
||||
eingeschaltet, hält seine eigene Kategorie-QRG als Referenz fest und schaltet nur bei einer
|
||||
automatisch von UCXLog oder Win-Test gemeldeten Abweichung von mehr als 5 kHz ab.
|
||||
|
||||
Der zusammengefasste `MyBeacon`-Chip macht Aktivierung, Teilaktivierung, automatische
|
||||
Abschaltung und Versandfehler ohne Umweg über das Einstellungsfenster erkennbar.
|
||||
|
||||
## Verbindliches Verhalten
|
||||
|
||||
### Intervall und Sitzung
|
||||
|
||||
- Das kleinste zulässige gemeinsame Beacon-Intervall beträgt fünf Minuten.
|
||||
- Genau fünf Minuten sind zulässig. Kleinere geladene Werte werden im Speicher auf fünf
|
||||
Minuten normalisiert; die mitgelieferte Konfiguration verwendet für beide Kategorien fünf.
|
||||
- Nach jedem Programmstart und Profilwechsel sind beide Beacons ausgeschaltet.
|
||||
- Ein Disconnect oder Reconnect innerhalb derselben Programmsitzung verändert ihre
|
||||
Aktivierung nicht.
|
||||
- Die Aktivierungsflags werden nicht aus XML gelesen. Die vorhandenen XML-Elemente bleiben
|
||||
für die Dateikompatibilität erhalten, werden aber immer mit `false` geschrieben.
|
||||
|
||||
### Aktivierung und benötigte QRG-Werte
|
||||
|
||||
- `MYQRG` und `MYQRGSHORT` benötigen eine gültige Haupt-QRG.
|
||||
- `SECONDQRG` benötigt eine gültige zweite QRG.
|
||||
- Diese Voraussetzung gilt unabhängig davon, in welchem der beiden Bakentexte die Variable
|
||||
vorkommt.
|
||||
- Fehlt ein benötigter Wert oder ist er ungültig, bleibt der betroffene Beacon aus. Sein
|
||||
kategorieweiser Status nennt die fehlende oder ungültige QRG als Grund.
|
||||
- Der gemeinsame Chip aktiviert beide nutzbaren Kategorien unabhängig. Ein gültiger Beacon
|
||||
wird eingeschaltet, auch wenn der andere abgelehnt wird; der Chip zeigt dann `1/2`.
|
||||
- Ein Text ohne QRG-Platzhalter darf ohne bekannte QRG aktiviert werden.
|
||||
- Geprüft wird der bereits angewendete Text, nicht ein noch offener Settings-Entwurf.
|
||||
|
||||
### Kategoriebezogene Referenz-QRG
|
||||
|
||||
- Beim Einschalten übernimmt jeder Beacon die aktuell gültige QRG seiner eigenen Kategorie
|
||||
als Referenz.
|
||||
- Ist keine gültige Kategorie-QRG vorhanden und benötigt der Text sie nicht, wird die erste
|
||||
später gültige manuelle oder externe QRG zur Referenz.
|
||||
- Eine gültige manuelle QRG-Eingabe ist eine bewusste Operatorentscheidung. Sie deaktiviert
|
||||
keinen Beacon und verschiebt die Referenz der zugehörigen Kategorie.
|
||||
- Eine ungültige manuelle Eingabe deaktiviert jeden aktiven Beacon, dessen angewendeter Text
|
||||
den betroffenen QRG-Wert benötigt. Benötigt sein Text ihn nicht, bleibt der Beacon aktiv;
|
||||
nur die Referenz seiner Kategorie wird gelöscht.
|
||||
- Ausschließlich QRG-Übernahmen von UCXLog `RadioInfo` und Win-Test `STATUS`, für die der
|
||||
bestehende QRG-Abgleich aktiv ist, dürfen automatisch abschalten.
|
||||
- Der Vergleich erfolgt in ganzzahligen Hz gegen die aktuelle Referenz. Genau 5.000 Hz sind
|
||||
zulässig; erst eine größere Abweichung schaltet ab.
|
||||
- Ohne Referenz setzt die erste externe QRG nur die Referenz und schaltet nicht ab.
|
||||
- Ohne SO2R betrifft eine externe Änderung von `MYQRG` ausschließlich die Hauptbake. Mit
|
||||
SO2R folgt Radio 1 `MYQRG` und Radio 2 `SECONDQRG`.
|
||||
|
||||
### Status und Tooltip
|
||||
|
||||
- Der Status wird als strukturierter Snapshot aus `core` geliefert. Übersetzte Anzeigetexte
|
||||
entstehen ausschließlich in `app-desktop`.
|
||||
- Der Snapshot kennt `OFF`, `PARTIAL`, `ON` und `FAILED` sowie den Zustand beider Kategorien.
|
||||
- Der Tooltip zeigt je Kategorie den aktuellen Zustand oder letzten Abschalt-/Ablehnungsgrund:
|
||||
noch nicht in dieser Sitzung aktiviert, durch den Benutzer ausgeschaltet, fehlende oder
|
||||
ungültige Haupt-QRG, fehlende oder ungültige zweite QRG oder automatische QRG-Abweichung
|
||||
von mehr als 5 kHz.
|
||||
- Der QRG-Grund nennt keine konkreten Frequenzen.
|
||||
- Timeraktivität überschreibt den kategorieweisen Grund nicht.
|
||||
- Ein erfolgreicher späterer Lauf löscht einen früheren Versandfehler der Kategorie.
|
||||
|
||||
### Verkehrsfarben des `MyBeacon`-Chips
|
||||
|
||||
- `on` und `1/2`: Hintergrund `#238636`, Rand `#56D364`, weiße Schrift.
|
||||
- Der bestehende 200-ms-Aktivitätsblitz verwendet Hintergrund `#FFB300`, Rand `#FFE082`
|
||||
und Schrift `#1B1B1B`.
|
||||
- `off`: neutraler Theme-Hintergrund mit `#D50000` im hellen beziehungsweise `#FF6B6B` im
|
||||
dunklen Design.
|
||||
- `FAILED`: dauerhaft `#D50000` mit Rand `#FF6B6B` und weißer Schrift.
|
||||
- `FAILED` unterdrückt den orangefarbenen Aktivitätsblitz.
|
||||
- Andere Status-Chips behalten ihr heutiges Theme-Verhalten.
|
||||
|
||||
## Technischer Ansatz
|
||||
|
||||
Eine controllerseitige `BeaconSessionState`-Komponente koordiniert den Zustand beider
|
||||
Kategorien. Die beobachtbaren Flags in `ChatPreferences` bleiben kompatible UI-Projektion,
|
||||
aber kein persistenter Produktzustand. Die Komponente verwaltet Referenzen, kategorieweise
|
||||
Gründe und Versandfehler und ist der einzige Produktionsschreibpfad der Aktivierungsflags.
|
||||
|
||||
Manuelle QRG-Eingaben und externe Logger-Meldungen verwenden getrennte Controller-Einstiege.
|
||||
UCXLog und Win-Test übergeben die bereits ermittelte Kategorie, den formatierten Anzeigewert
|
||||
und die genaue Frequenz in Hz innerhalb der bestehenden `UiDispatcher`-Grenze. Damit ist die
|
||||
Herkunft explizit; Listener, Thread-lokale Markierungen oder erratene Ursprünge sind unnötig.
|
||||
|
||||
Der vorhandene `BeaconTask` bleibt für Auflösung, Validierung und Einreihung in die reguläre
|
||||
TX-Warteschlange verantwortlich. Er meldet je Kategorie Erfolg oder Fehler an den
|
||||
Sitzungszustand zurück. Ein DTO im gemeinsamen Model transportiert den strukturierten Status
|
||||
über den bestehenden Threadstatus-Kanal; Compose übersetzt Gründe und wählt die Chipfarben.
|
||||
|
||||
## Unveränderte Grenzen
|
||||
|
||||
- Beide Kategorien teilen einen Timer und besitzen getrennte Aktivierung und Texte.
|
||||
- Der zweite Versand setzt weiterhin eine verfügbare zweite Chatkategorie voraus.
|
||||
- Aktivieren sendet nicht sofort und startet den Timer nicht neu.
|
||||
- Der erste Timerlauf nach dem Verbindungsaufbau bleibt bei ungefähr zehn Sekunden.
|
||||
- Nachrichtenauflösung, Längenlimit, TX-Warteschlange und ON4KST-Framing bleiben unverändert.
|
||||
- Die Chip-Texte `off`, `1/2`, `on` und `FAILED` und seine gemeinsame Klickfunktion bleiben.
|
||||
- Es gibt keine neue Produktionsabhängigkeit, kein neues persistentes Schema und keine
|
||||
Änderung eines externen Protokolls.
|
||||
|
||||
## Berücksichtigte Ansätze
|
||||
|
||||
1. **Gewählt:** controllerseitiger Sitzungszustand mit expliziten manuellen und externen
|
||||
Ereignissen. Das hält Policy, Referenzen und Gründe zusammen und bleibt testbar.
|
||||
2. **Verworfen:** Flags in Preferences, Referenzen in einem Guard und Gründe in `BeaconStatus`.
|
||||
Der zusammengehörige Zustand wäre auf mehrere Besitzer verteilt.
|
||||
3. **Verworfen:** Beobachtung der QRG-Werte mit temporärer Herkunftsmarkierung. `MutableValue`
|
||||
trägt keine Quelle; die asynchrone UI-Weitergabe machte diesen Ansatz fragil.
|
||||
|
||||
## Dokumentationswirkung
|
||||
|
||||
Die Änderung ist benutzerseitig sichtbar, betrifft Laufzeit/Persistenz sowie die bestehende
|
||||
UCXLog-/Win-Test-Integration. Geprüft und bei Bedarf angepasst werden die relevanten
|
||||
deutschen und englischen Konfigurations-, Workflow-, Variablen-, Funktions- und
|
||||
Benutzeroberflächenabschnitte, Changelog, Website-Texte, `docs/PROJECT_CONTEXT.md` und der
|
||||
ältere Live-Beacon-Entwurf. README und verbundene Projekte sind voraussichtlich nicht
|
||||
betroffen.
|
||||
@@ -0,0 +1,27 @@
|
||||
# Automatic Chat Suffix Design
|
||||
|
||||
## Intent
|
||||
|
||||
Every user-visible chat message generated and sent automatically by KST4Contest must end with the fixed suffix ` *a*`. The marker makes automatic traffic recognisable without changing manual messages or ON4KST control traffic.
|
||||
|
||||
## Behaviour
|
||||
|
||||
- Append ` *a*` to generic private autoanswers, QRG autoanswers, `//VER` replies and both beacon categories.
|
||||
- Keep the existing `[KST4C Automsg]` prefix on every autoanswer and continue using it for the reply-loop guard.
|
||||
- Append the suffix unconditionally after text preparation. A configured text which already ends in `*a*` therefore receives another ` *a*`.
|
||||
- Preserve the existing 120-character beacon payload allowance. The complete transmitted beacon, including the four-character suffix, may contain up to 124 characters.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Do not mark manually sent messages, snippets or manually triggered CQ messages.
|
||||
- Do not alter login, keepalive, ON4KST command or other internal protocol frames.
|
||||
- Do not change cooldowns, category routing, beacon activation, QRG safeguards, timer behaviour, persistence or settings UI.
|
||||
- Do not revive or refactor the unused legacy `ScoreboardUpdateTask`.
|
||||
|
||||
## Selected Approach
|
||||
|
||||
Use one package-private formatter in the controller package to append the fixed suffix. Call it explicitly from the active autoanswer and beacon construction paths. This keeps the scope visible and prevents a broad writer-layer rule from modifying manual messages or raw protocol frames.
|
||||
|
||||
## Validation and Documentation
|
||||
|
||||
Validate autoanswers after suffix insertion. Validate beacon payloads against 120 characters, append the suffix, then validate the final text against 124 characters. Update focused tests, the German and English manuals, relevant website feature pages, changelogs and durable project context.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 53 KiB |
@@ -565,7 +565,7 @@ Bei einer ungünstigen Aufteilung sollten zuerst die Divider und Spaltenbreiten
|
||||
|
||||
## Tipps zur Bedienung
|
||||
|
||||
- **Beacon schnell schalten**: Die Aktivierung ist im geöffneten Einstellungsfenster je Kategorie möglich. Ein Klick auf den Status-Chip **MyBeacon** schaltet alle derzeit nutzbaren Beacons gemeinsam ein oder aus.
|
||||
- **Beacon schnell schalten**: Die Aktivierung ist im geöffneten Einstellungsfenster je Kategorie möglich. Ein Klick auf den Status-Chip **MyBeacon** schaltet alle derzeit nutzbaren Beacons gemeinsam ein oder aus. **off** hat rote Schrift, **1/2** und **on** sind grün, bei gemeldeter Aktivität blitzt der Chip orange. **FAILED** bleibt rot mit weißer Schrift. Der Tooltip nennt den Zustand oder letzten Abschaltgrund je Kategorie.
|
||||
- **Rechtsklick in der Benutzerliste**: Öffnet das Snippet-Menü und weitere Aktionen (QRZ.com-Profil, NOT-QRV-Tags setzen).
|
||||
- **Enter aus dem Chat heraus**: Wenn im Sendfeld Text steht, sendet Enter direkt – auch wenn der Fokus woanders liegt.
|
||||
- **Beacon stoppen**: Beim Scannen von Frequenzen den Beacon ausschalten, damit der Chat nicht mit Meldungen überflutet wird.
|
||||
- **Beacon nach dem Start prüfen**: Nach einem Programmstart oder Profilwechsel sind beide Beacons ausgeschaltet. Ein normaler ON4KST-Reconnect innerhalb derselben Programmsitzung verändert ihren Zustand nicht.
|
||||
@@ -40,6 +40,10 @@ Mehrere Operateure an einem Rechner können jetzt eigene Rufzeichen, Locators un
|
||||
|
||||
### Geändert
|
||||
|
||||
- **Automatische Chatmeldungen eindeutig markiert:** Allgemeine und QRG-bezogene Autoantworten, `//VER`-Antworten sowie beide Beacons enden jetzt mit ` *a*`. Das bestehende Präfix `[KST4C Automsg]` der Autoantworten bleibt erhalten. Der aufgelöste Beacon-Inhalt bleibt auf 120 Zeichen begrenzt; mit dem festen Suffix kann die gesendete Nachricht 124 Zeichen lang sein.
|
||||
|
||||
- **Beacon mit Sitzungs- und QRG-Sicherung:** Beacons starten nach jedem Programmstart und Profilwechsel ausgeschaltet, bleiben bei einem normalen ON4KST-Reconnect aber aktiv. Das Mindestintervall beträgt fünf Minuten. Jeder aktive Beacon überwacht die QRG seiner eigenen Kategorie: Manuelle Änderungen verschieben die Referenz, ein automatischer Sprung durch UCXLog oder Win-Test von mehr als 5 kHz schaltet nur den betroffenen Beacon aus. Der **MyBeacon**-Chip zeigt `off`, `1/2`, `on` und `FAILED` mit eindeutigen Verkehrsfarben; sein Tooltip nennt den Zustand oder letzten Abschaltgrund je Kategorie.
|
||||
|
||||
- **Neues Zuhause auf GitHub:** Das Projekt ist vom Konto `praktimarc` in die GitHub-Organisation [HamRadioOnline](https://github.com/HamRadioOnline) umgezogen und liegt jetzt unter [HamRadioOnline/kst4contest](https://github.com/HamRadioOnline/kst4contest). Alte Links auf das Repository, die Releases, die Issues und das Wiki leiten auf die neue Adresse weiter. Auch das Flatpak-Repository ist umgezogen und liegt jetzt unter `https://hamradioonline.github.io/kst4contest/`; bestehende Flatpak-Installationen aktualisieren über die Weiterleitung der alten Adresse weiter, neue Einrichtungen verwenden die neue Adresse aus der Installationsanleitung. Die AUR-Pakete zeigen ebenfalls auf die neue Adresse.
|
||||
|
||||
- **Zwischenspeicher der Geländeprofile getrennt:** Berechnete Geländeprofile liegen jetzt in der eigenen, gemeinsam genutzten Datei `terrainprofilecache.db` und werden nach Besitzer getrennt gespeichert. Bisher wurde der gesamte Zwischenspeicher gelöscht, sobald sich Rufzeichen oder Locator änderten; bei einem Profilwechsel wäre damit jedes berechnete Profil verloren gegangen.
|
||||
|
||||
@@ -93,9 +93,9 @@ KST4Contest hält die benötigten Informationen zwischen diesen Schritten zusamm
|
||||
|
||||
Bei einer weitgehend festen CQ-QRG sollten `MYQRG` beziehungsweise `SECONDQRG` den tatsächlich verwendeten Frequenzen entsprechen. Eine aktivierte und tatsächlich liefernde TRX-Synchronisation kann `MYQRG` automatisch aktualisieren. Die Aktivierung allein reicht nicht: Vor dem Contest sollte mit einer realen Frequenzänderung geprüft werden, ob gültige Pakete ankommen. Ohne funktionierende automatische Quelle muss der Wert von Hand gepflegt werden.
|
||||
|
||||
Der Beacon kann die aktuelle QRG, den Locator und die Antennenrichtung regelmäßig im Chat veröffentlichen. Seine Variablen werden bei jedem Sendedurchlauf erneut ausgewertet.
|
||||
Der Beacon kann die aktuelle QRG, den Locator und die Antennenrichtung regelmäßig im Chat veröffentlichen. Er muss nach jedem Programmstart oder Profilwechsel bewusst aktiviert werden. Eine manuell geänderte QRG wird zur neuen Referenz; meldet UCXLog oder Win-Test danach einen Sprung von mehr als 5 kHz, wird der betroffene Beacon automatisch ausgeschaltet.
|
||||
|
||||
Wird während des CQ-Betriebs über mehrere Frequenzen gescannt, sollte der Beacon deaktiviert werden. Eine automatisch veröffentlichte QRG ist nur hilfreich, solange sie noch stimmt.
|
||||
Beim Scannen über mehrere Frequenzen verhindert diese Sicherung eine unbeabsichtigte Aussendung mit einer neuen Logger-QRG. Der Status-Tooltip zeigt, welche Kategorie abgeschaltet wurde und warum.
|
||||
|
||||
Shortcuts und Snippets sollten die regelmäßig benötigten Nachrichten abdecken, beispielsweise:
|
||||
|
||||
|
||||
@@ -301,7 +301,7 @@ Bei zwei gleichzeitig geöffneten Chat-Kategorien bleibt der Zusammenhang erhalt
|
||||
|
||||
Fehlt die QRG der betreffenden Kategorie, sendet KST4Contest keine inhaltslose Auskunft. Auch ein leerer oder protokollwidriger allgemeiner Antworttext wird nicht in die Sendequeue übernommen.
|
||||
|
||||
Automatische Antworten benötigen Grenzen. KST4Contest versieht sie daher mit `[KST4C Automsg]`, ignoriert entsprechend gekennzeichnete Nachrichten und begrenzt weitere Antworten an dasselbe vollständige Rufzeichen in derselben Kategorie auf eine Nachricht innerhalb von zwei Minuten. Der Schutz gilt gemeinsam für die allgemeine und die QRG-bezogene Antwort.
|
||||
Automatische Antworten benötigen Grenzen. KST4Contest versieht sie daher mit `[KST4C Automsg]`, ignoriert entsprechend gekennzeichnete Nachrichten und begrenzt weitere Antworten an dasselbe vollständige Rufzeichen in derselben Kategorie auf eine Nachricht innerhalb von zwei Minuten. Der Schutz gilt gemeinsam für die allgemeine und die QRG-bezogene Antwort. Zusätzlich endet jede automatisch erzeugte Chatmeldung einschließlich `//VER`-Antworten und Beacons mit ` *a*`.
|
||||
|
||||
Die Sperrzeit wird nur nach einer vollständigen und lokal gültigen Antwort gestartet. Eine fehlende QRG oder ein verworfener Antworttext verhindert daher keine spätere gültige Antwort.
|
||||
|
||||
@@ -798,13 +798,13 @@ Die Timeline ist eine Vorschau. AirScout-Daten können sich ändern, und ein ein
|
||||
|
||||
KST4Contest kann wiederkehrende CQ-Nachrichten in den öffentlichen Chat senden. Der Beacon ist für längeres CQ-Rufen auf einer festen Frequenz gedacht: Die eigene QRG wird regelmäßig veröffentlicht, ohne dass derselbe Text von Hand wiederholt werden muss.
|
||||
|
||||
Beide Chat-Kategorien verwenden ein gemeinsames Intervall, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Der zweite Beacon wird nur gesendet, wenn die zweite Kategorie in derselben ON4KST-Sitzung aktiviert und vollständig synchronisiert ist.
|
||||
Beide Chat-Kategorien verwenden ein gemeinsames Intervall von mindestens fünf Minuten, besitzen aber jeweils einen eigenen Aktivierungsschalter und Nachrichtentext. Die Aktivierung gilt nur für die laufende Programmsitzung und muss nach einem Programmstart oder Profilwechsel erneut erfolgen. Ein ON4KST-Reconnect ändert sie nicht. Der zweite Beacon wird nur gesendet, wenn die zweite Kategorie in derselben ON4KST-Sitzung aktiviert und vollständig synchronisiert ist.
|
||||
|
||||
Globale Variablen wie `MYQRG`, `SECONDQRG`, `MYLOCATOR` oder `MYQTF` werden unmittelbar vor jeder Aussendung neu aufgelöst. Eine zwischenzeitlich vom Logprogramm aktualisierte QRG kann dadurch bereits im nächsten Beacon erscheinen.
|
||||
Globale Variablen wie `MYQRG`, `SECONDQRG`, `MYLOCATOR` oder `MYQTF` werden unmittelbar vor jeder Aussendung neu aufgelöst. Jeder aktive Beacon überwacht dabei die QRG seiner eigenen Kategorie. Eine manuelle Änderung verschiebt die Referenz; ein automatischer Sprung durch UCXLog oder Win-Test von mehr als 5 kHz schaltet den betroffenen Beacon aus.
|
||||
|
||||
Vor dem Versand prüft KST4Contest den vollständig aufgelösten Nachrichtentext. Leere Nachrichten, Zeilenumbrüche, das Protokoll-Trennzeichen `|` und Texte mit mehr als 120 Zeichen werden nicht gesendet.
|
||||
Vor dem Versand prüft KST4Contest den vollständig aufgelösten Beacon-Inhalt. Leere Inhalte, Zeilenumbrüche, das Protokoll-Trennzeichen `|` und Inhalte mit mehr als 120 Zeichen werden nicht gesendet. Danach wird ` *a*` angehängt; die gesendete Nachricht kann damit bis zu 124 Zeichen lang sein.
|
||||
|
||||
Beim Absuchen oder häufigen Wechseln der QRG sollte der Beacon ausgeschaltet werden. Eine automatisch verbreitete Frequenz ist nur dann hilfreich, wenn dort tatsächlich jemand hört und ruft.
|
||||
Der farbige **MyBeacon**-Chip zeigt aktive, teilweise aktive, ausgeschaltete und fehlgeschlagene Zustände. Sein Tooltip nennt den Zustand oder letzten Abschaltgrund je Kategorie.
|
||||
|
||||
Konfiguration, Timer-Verhalten und verfügbare Variablen: [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon).
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ Dedizierter Netzwerk-Erkenner für Win-Test. KST4Contest empfängt und verarbeit
|
||||
|
||||
## TRX-Sync-Einstellungen
|
||||
|
||||
Die TRX-Synchronisation übernimmt die aktuelle Frequenz aus dem Logprogramm und stellt sie in KST4Contest als eigene QRG der ersten Chat-Kategorie bereit. QSO- und Frequenzsynchronisation verwenden teilweise denselben UDP-Empfänger, sind funktional aber voneinander getrennt: Ein empfangenes `RadioInfo`-Paket markiert keine Station als gearbeitet, und ein QSO-Paket ändert nicht automatisch die eigene QRG.
|
||||
Die TRX-Synchronisation übernimmt die aktuelle Frequenz aus dem Logprogramm und stellt sie in KST4Contest als eigene QRG bereit. Ohne SO2R ist das `MYQRG` der ersten Chat-Kategorie; mit SO2R kann Gerät 2 getrennt `SECONDQRG` aktualisieren. QSO- und Frequenzsynchronisation verwenden teilweise denselben UDP-Empfänger, sind funktional aber voneinander getrennt: Ein empfangenes `RadioInfo`-Paket markiert keine Station als gearbeitet, und ein QSO-Paket ändert nicht automatisch die eigene QRG.
|
||||
|
||||

|
||||
|
||||
@@ -153,11 +153,11 @@ Wird der gemeinsame UDP-Port geändert, muss KST4Contest neu gestartet werden. E
|
||||
|
||||
### Welche QRG wird aktualisiert?
|
||||
|
||||
Beide automatischen Quellen aktualisieren ausschließlich `MYQRG`. Das ist die eigene QRG der ersten beziehungsweise primären Chat-Kategorie.
|
||||
Ohne SO2R aktualisieren beide automatischen Quellen ausschließlich `MYQRG`. Das ist die eigene QRG der ersten beziehungsweise primären Chat-Kategorie; `SECONDQRG` bleibt unabhängig und kann von Hand eingetragen werden.
|
||||
|
||||
Bei aktiviertem zweiten Chat bleibt dessen QRG davon unabhängig. Sie wird nicht aus den empfangenen TRX-Paketen abgeleitet und steht als `SECONDQRG` zur Verfügung. Dadurch kann beispielsweise die erste Kategorie automatisch der Frequenz des Logprogramms folgen, während für die zweite Kategorie eine eigene QRG von Hand eingetragen wird.
|
||||
Mit SO2R ordnet KST4Contest die Pakete einem Gerät zu: Gerät 1 aktualisiert `MYQRG`, Gerät 2 `SECONDQRG`. Die genaue Zuordnung steht unter [SO2R: zwei Funkgeräte getrennt führen](#so2r-zwei-funkgeräte-getrennt-führen).
|
||||
|
||||
Sobald mindestens eine automatische QRG-Quelle aktiviert ist und einen gültigen Wert liefert, folgt das QRG-Feld der ersten Kategorie im Hauptfenster der empfangenen Frequenz. Bleiben die erwarteten Pakete aus, sollte die Schnittstelle vor dem Contest geprüft werden. Eine manuelle Eingabe in dieses Feld ist möglich, wenn sowohl der allgemeine RadioInfo-Listener als auch die Win-Test-STATUS-Synchronisation deaktiviert sind.
|
||||
Sobald mindestens eine automatische QRG-Quelle aktiviert ist und einen gültigen Wert liefert, folgt das zugeordnete QRG-Feld im Hauptfenster der empfangenen Frequenz. Bleiben die erwarteten Pakete aus, sollte die Schnittstelle vor dem Contest geprüft werden. Eine manuelle Eingabe bleibt möglich und gilt für die Beacon-Sicherung als bewusste neue Referenz; ein späteres Logger-Paket wird gegen diesen Wert geprüft.
|
||||
|
||||
### Haupt- oder Pass-Frequenz aus Win-Test
|
||||
|
||||
@@ -176,7 +176,7 @@ Die Frequenzen werden einheitlich im KST4Contest-Format dargestellt, beispielswe
|
||||
10368.100.00
|
||||
```
|
||||
|
||||
Die Werte werden erst beim Auflösen des Nachrichtentextes eingesetzt. Ändert das Logprogramm zwischen zwei Beacon-Läufen die Frequenz, verwendet die nächste Nachricht bereits den aktualisierten Wert.
|
||||
Die Werte werden erst beim Auflösen des Nachrichtentextes eingesetzt. Bei normalen Nachrichten steht eine Logger-Änderung sofort zur Verfügung. Ein aktiver Beacon verwendet dagegen seine QRG-Referenz: Ein automatischer Sprung um mehr als 5 kHz schaltet den betroffenen Beacon aus. Einzelheiten stehen unter [Beacon Settings](#beacon-settings-automatischer-beacon).
|
||||
|
||||
Die eigene QRG kann außerdem als Fallback für die Übergabe eines Skeds an Win-Test verwendet werden. Das geschieht nur, wenn die QRG auswertbar ist und zum ausdrücklich ausgewählten Sked-Band gehört. Einzelheiten stehen unter [Log-Synchronisation](de-Log-Synchronisation#skeds-an-win-test-übergeben).
|
||||
|
||||
@@ -603,13 +603,13 @@ KST4Contest verwendet einen gemeinsamen Timer für beide Chat-Kategorien. Aktivi
|
||||
|
||||
Sind beide Beacons aktiviert, werden sie beim selben Timer-Lauf nacheinander in ihren jeweiligen Kategorien gesendet. Der zweite Beacon wird nur berücksichtigt, wenn die zweite Kategorie in derselben ON4KST-Sitzung aktiviert und vollständig synchronisiert ist.
|
||||
|
||||
Die beiden Aktivierungsschalter wirken sofort auf den laufenden Timer. Sie starten weder den Countdown neu noch senden sie unmittelbar eine Nachricht; der nächste reguläre Timer-Lauf prüft den aktuellen Zustand. Erst **Save Settings** speichert die Aktivierung für den nächsten Programmstart. Nachrichtentexte und Intervall folgen weiterhin der normalen Apply-/Save-Logik.
|
||||
Die beiden Aktivierungsschalter wirken sofort auf den laufenden Timer. Sie starten weder den Countdown neu noch senden sie unmittelbar eine Nachricht; der nächste reguläre Timer-Lauf prüft den aktuellen Zustand. Die Aktivierung gilt bewusst nur für die laufende Programmsitzung: Nach jedem Programmstart und Profilwechsel sind beide Beacons ausgeschaltet. Ein normaler ON4KST-Reconnect innerhalb derselben Programmsitzung lässt den Zustand dagegen unverändert. Nachrichtentexte und Intervall folgen weiterhin der normalen Apply-/Save-Logik.
|
||||
|
||||
Der Status-Chip **MyBeacon** im Hauptfenster fasst beide nutzbaren Kategorien zusammen: **off** bedeutet kein aktiver Beacon, **1/2** genau eine von zwei aktiven Kategorien und **on** alle derzeit nutzbaren Kategorien. Ein Klick auf den Chip schaltet alle nutzbaren Beacons gemeinsam ein oder aus. Ist der zweite Chat deaktiviert, ändert der Chip nur den Beacon der Hauptkategorie und lässt die gespeicherte Einstellung des zweiten Beacons unverändert.
|
||||
Der Status-Chip **MyBeacon** im Hauptfenster fasst beide nutzbaren Kategorien zusammen: **off** bedeutet kein aktiver Beacon, **1/2** genau eine von zwei aktiven Kategorien und **on** alle derzeit nutzbaren Kategorien. Ein Klick auf den Chip schaltet alle nutzbaren Beacons gemeinsam ein oder aus. Ist der zweite Chat deaktiviert, ändert der Chip nur den Beacon der Hauptkategorie. **off** erscheint mit roter Schrift, **1/2** und **on** dauerhaft grün. Bei gemeldeter Aktivität blitzt der aktive Chip kurz orange auf. **FAILED** überschreibt diese Anzeige dauerhaft mit rotem Hintergrund und weißer Schrift; der orange Aktivitätsblitz wird dann unterdrückt. Der Tooltip nennt für jede Kategorie den aktuellen Zustand oder den letzten allgemeinen Abschaltgrund.
|
||||
|
||||
### Intervall und Timer-Verhalten
|
||||
|
||||
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert ist eine Minute.
|
||||
Das Intervall wird in ganzen Minuten angegeben. Der kleinste zulässige Wert beträgt fünf Minuten. Kleinere Werte aus einer älteren Konfiguration werden beim Laden auf fünf Minuten angehoben.
|
||||
|
||||
Nach dem Aufbau der Chat-Verbindung prüft KST4Contest die Beacons erstmals nach ungefähr zehn Sekunden. Anschließend gilt das eingestellte Intervall.
|
||||
|
||||
@@ -635,15 +635,19 @@ Eine mögliche Nachricht für die erste Chat-Kategorie ist:
|
||||
calling cq at MYQRGSHORT, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
KST4Contest hängt an jede gesendete Beacon-Nachricht automatisch das feste Suffix ` *a*` an. Es gehört deshalb nicht in das konfigurierte Template.
|
||||
|
||||
Für den zweiten Chat muss `SECONDQRG` verwendet werden, wenn dessen Frequenz von der ersten Kategorie abweicht:
|
||||
|
||||
```text
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Die Variablen werden bei jedem Timer-Lauf neu aufgelöst. Ändert die Logsoftware zwischenzeitlich die in `MYQRG` gespeicherte Frequenz, verwendet bereits der nächste Beacon den aktualisierten Wert. Das Template selbst muss dafür nicht geändert werden.
|
||||
Die Variablen werden bei jedem Timer-Lauf neu aufgelöst. `MYQRG` und `MYQRGSHORT` benötigen immer eine gültige QRG der ersten Chat-Kategorie, `SECONDQRG` immer eine gültige QRG der zweiten Kategorie. Diese Zuordnung gilt unabhängig davon, in welchem der beiden Beacon-Texte die Variable steht.
|
||||
|
||||
`MYQRG` und `MYQRGSHORT` beziehen sich immer auf die erste Chat-Kategorie. Die Auswahl oder Aktivierung des zweiten Chats ändert diese Zuordnung nicht.
|
||||
Enthält ein Beacon-Text eine solche QRG-Variable, kann der betreffende Beacon nur mit gültigem zugeordnetem QRG-Wert aktiviert bleiben. Fehlt der Wert oder ist er ungültig, bleibt nur dieser Beacon ausgeschaltet beziehungsweise wird ausgeschaltet; der Tooltip nennt den Grund. Ein Beacon-Text ohne QRG-Variable darf dagegen auch ohne vorhandene QRG aktiviert werden.
|
||||
|
||||
Beim Einschalten merkt sich jeder Beacon die gültige QRG seiner eigenen Kategorie als Referenz. Ist noch keine gültige eigene QRG vorhanden und enthält der Text keine QRG-Variable, wird die erste später manuell oder vom Logger gemeldete gültige Frequenz zur Referenz. Eine manuelle QRG-Änderung gilt als bewusste Bedienung und verschiebt die Referenz auf den neuen Wert. Ändert UCXLog oder Win-Test die zugeordnete QRG später um mehr als 5 kHz gegenüber dieser Referenz, schaltet KST4Contest den betreffenden Beacon automatisch aus. Genau 5 kHz sind noch zulässig. Ohne SO2R betrifft eine automatische Logger-QRG nur den Beacon der Hauptkategorie; mit SO2R wird die zweite QRG und damit der zweite Beacon getrennt behandelt.
|
||||
|
||||
Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` benötigen eine ausgewählte Gegenstation. Da ein öffentlicher Beacon keine bestimmte Station adressiert, werden diese Variablen im Beacon nicht aufgelöst.
|
||||
|
||||
@@ -651,16 +655,16 @@ Stationsbezogene Variablen wie `QRZNAME`, `FIRSTAP` oder `SECONDAP` benötigen e
|
||||
|
||||
KST4Contest prüft sowohl das eingetragene Template als auch den nach der Variablenauflösung tatsächlich zu sendenden Text.
|
||||
|
||||
Für Beacon-Nachrichten gelten folgende Bedingungen:
|
||||
Für den aufgelösten Beacon-Inhalt vor dem festen Suffix gelten folgende Bedingungen:
|
||||
|
||||
- Der endgültige Nachrichtentext darf nicht leer sein.
|
||||
- Er darf nicht leer sein.
|
||||
- Er darf höchstens 120 Zeichen enthalten.
|
||||
- Das Protokoll-Trennzeichen `|` ist nicht zulässig.
|
||||
- Zeilenumbrüche sind nicht zulässig.
|
||||
|
||||
Eine ungültige Eingabe wird nicht als neue Beacon-Konfiguration übernommen. Wird ein Template erst durch eine spätere Variablenauflösung ungültig, beispielsweise weil der aufgelöste Text länger als 120 Zeichen ist, wird dieser Beacon-Lauf ausgelassen.
|
||||
KST4Contest hängt anschließend ` *a*` an. Die tatsächlich gesendete Beacon-Nachricht darf damit höchstens 124 Zeichen lang sein. Eine ungültige Eingabe wird nicht als neue Beacon-Konfiguration übernommen. Wird ein Template erst durch eine spätere Variablenauflösung ungültig, beispielsweise weil der aufgelöste Inhalt länger als 120 Zeichen ist, wird dieser Beacon-Lauf ausgelassen.
|
||||
|
||||
Ein Template, das ausschließlich aus einer momentan noch leeren globalen Variable besteht, kann gespeichert werden. Das ist beispielsweise beim Start möglich, bevor die erste QRG vom Logprogramm empfangen wurde. Solange die Variable keinen verwendbaren Inhalt liefert, sendet KST4Contest jedoch keine leere Nachricht.
|
||||
Ein Template mit einer QRG-Variable kann gespeichert werden, bevor eine passende QRG vorhanden ist; der betroffene Beacon lässt sich dann aber nicht aktivieren. Wird eine benötigte QRG später ungültig, wird ein bereits aktiver Beacon ausgeschaltet. Enthält das Template keine QRG-Variable, löscht eine ungültige eigene QRG lediglich die Referenz und der Beacon bleibt aktiv.
|
||||
|
||||
### Wann sollte der Beacon ausgeschaltet werden?
|
||||
|
||||
@@ -668,7 +672,7 @@ Der Beacon ist nur dann hilfreich, wenn seine QRG-Angabe zum tatsächlichen Betr
|
||||
|
||||
Im Klartext: Solange auf einer festen QRG CQ gerufen wird, spart der Beacon Arbeit. Beim „Schleichen“ über das Band sollte er ausgeschaltet werden.
|
||||
|
||||
Änderungen wirken während der laufenden Verbindung. Damit Aktivierung, Texte und Intervall auch nach dem nächsten Programmstart erhalten bleiben, anschließend **Save Settings** verwenden.
|
||||
Änderungen wirken während der laufenden Verbindung. **Save Settings** speichert Texte und Intervall für den nächsten Programmstart. Die Aktivierung wird absichtlich nicht gespeichert und muss in jeder Programmsitzung erneut erfolgen.
|
||||
|
||||
---
|
||||
|
||||
@@ -690,12 +694,12 @@ Eine zweckmäßige Nachricht ist beispielsweise:
|
||||
Sri, I am not taking part in this contest. No skeds.
|
||||
```
|
||||
|
||||
Das Präfix `[KST4C Automsg]` muss nicht in das Eingabefeld geschrieben werden. KST4Contest ergänzt es automatisch.
|
||||
Das Präfix `[KST4C Automsg]` und das feste Suffix ` *a*` müssen nicht in das Eingabefeld geschrieben werden. KST4Contest ergänzt beides automatisch.
|
||||
|
||||
Die beim Empfänger sichtbare Nachricht lautet daher beispielsweise:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] Sri, I am not taking part in this contest. No skeds.
|
||||
[KST4C Automsg] Sri, I am not taking part in this contest. No skeds. *a*
|
||||
```
|
||||
|
||||
Die eingegangene Privatnachricht bleibt sichtbar. Die Funktion blockiert oder verwirft keine Anfrage, sondern erspart lediglich die wiederholte manuelle Antwort.
|
||||
@@ -726,7 +730,7 @@ Die Antwort enthält ausschließlich die QRG der Kategorie, in der die Anfrage e
|
||||
Eine mögliche Antwort lautet:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] QRG is: 144.300.00
|
||||
[KST4C Automsg] QRG is: 144.300.00 *a*
|
||||
```
|
||||
|
||||
Die Werte stammen aus denselben QRG-Feldern, die auch von `MYQRG` und `SECONDQRG` verwendet werden. Die Haupt-QRG kann manuell eingetragen oder durch die [TRX-Synchronisation](#trx-sync-einstellungen) aktualisiert werden. Für die zweite Kategorie wird der dort konfigurierte beziehungsweise manuell eingetragene Wert verwendet; mit SO2R schreibt Funkgerät 2 dieses Feld (`SECONDQRG`).
|
||||
@@ -762,7 +766,7 @@ Das vorhandene Bild `client_settings_window_messagehandling.png` zeigt die neue
|
||||
|
||||
### Schutz vor wiederholten Antworten
|
||||
|
||||
Jede automatisch erzeugte Nachricht trägt das feste Präfix:
|
||||
Jede automatische Antwort trägt das feste Präfix:
|
||||
|
||||
```text
|
||||
[KST4C Automsg]
|
||||
@@ -770,6 +774,8 @@ Jede automatisch erzeugte Nachricht trägt das feste Präfix:
|
||||
|
||||
Die allgemeine und die QRG-bezogene Antwort reagieren nicht auf Nachrichten, die dieses Präfix bereits enthalten. Dadurch beantworten sich zwei entsprechend arbeitende Clients nicht gegenseitig in einer Schleife.
|
||||
|
||||
Zusätzlich endet jede von KST4Contest automatisch erzeugte Chatmeldung mit ` *a*`. Das gilt für allgemeine und QRG-bezogene Antworten, `//VER`-Antworten sowie beide Beacons. Das Suffix wird immer angehängt, auch wenn der konfigurierte Text bereits damit endet. Manuell gesendete Nachrichten und interne ON4KST-Steuerframes bleiben unverändert.
|
||||
|
||||
Zusätzlich gilt eine gemeinsame Sperrzeit von zwei Minuten für beide Antwortarten. Die Sperre wird getrennt nach vollständigem Rufzeichen und Chat-Kategorie geführt.
|
||||
|
||||
Daraus folgt:
|
||||
|
||||
@@ -305,16 +305,20 @@ Verwendet die zweite Chat-Kategorie eine andere QRG, muss deren Vorlage `SECONDQ
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Die globalen Variablen werden bei jedem Timer-Lauf erneut ausgewertet. Eine inzwischen vom Logprogramm aktualisierte QRG kann dadurch bereits in der nächsten Beacon-Nachricht erscheinen.
|
||||
Die globalen Variablen werden bei jedem Timer-Lauf erneut ausgewertet. `MYQRG` und `MYQRGSHORT` benötigen immer eine gültige QRG der Hauptkategorie, `SECONDQRG` immer eine gültige QRG der zweiten Kategorie – unabhängig davon, in welcher Beacon-Vorlage sie stehen. Fehlt eine benötigte QRG, lässt sich nur der betroffene Beacon nicht aktivieren beziehungsweise wird ausgeschaltet. Eine Vorlage ohne QRG-Variable darf auch ohne vorhandene QRG aktiviert werden.
|
||||
|
||||
Der vollständig aufgelöste Beacon-Text:
|
||||
Beim Einschalten merkt sich jeder Beacon die QRG seiner eigenen Kategorie. Eine manuelle QRG-Änderung verschiebt diese Referenz. Ändert UCXLog oder Win-Test die zugeordnete QRG später um mehr als 5 kHz, schaltet KST4Contest den betreffenden Beacon automatisch aus, statt die neue Frequenz unbemerkt in die nächste Aussendung zu übernehmen. Ist beim Einschalten noch keine eigene QRG vorhanden, wird die erste später gültig eingegebene oder empfangene Frequenz zur Referenz.
|
||||
|
||||
Der vollständig aufgelöste Beacon-Inhalt vor dem festen Suffix ` *a*`:
|
||||
|
||||
- muss mindestens ein gültiges Zeichen enthalten,
|
||||
- darf höchstens 120 Zeichen lang sein,
|
||||
- darf das Protokoll-Trennzeichen `|` nicht enthalten und
|
||||
- darf keine Zeilenumbrüche enthalten.
|
||||
|
||||
Ist der Text beim vorgesehenen Versand leer oder ungültig, wird der betreffende Beacon-Lauf ausgelassen.
|
||||
KST4Contest hängt das Suffix anschließend automatisch an. Die tatsächlich gesendete Beacon-Nachricht kann deshalb bis zu 124 Zeichen lang sein.
|
||||
|
||||
Ist der aufgelöste Text beim vorgesehenen Versand aus einem anderen Grund leer oder ungültig, wird der betreffende Beacon-Lauf ausgelassen und der Status-Chip zeigt **FAILED**.
|
||||
|
||||
Intervall, Aktivierung und Verhalten beider Kategorien sind unter [Konfiguration – Beacon Settings](de-Konfiguration#beacon-settings-automatischer-beacon) beschrieben.
|
||||
|
||||
|
||||
@@ -40,6 +40,10 @@ Several operators sharing one computer can now use their own callsigns, locators
|
||||
|
||||
### Changed
|
||||
|
||||
- **Automatic chat messages marked consistently:** general and QRG-specific automatic replies, `//VER` replies and both beacons now end with ` *a*`. Automatic replies keep their existing `[KST4C Automsg]` prefix. Resolved beacon content remains limited to 120 characters; including the fixed suffix, the transmitted message may contain 124 characters.
|
||||
|
||||
- **Beacon session and QRG safeguards:** beacons start off after every program start and profile switch, but remain active across a normal ON4KST reconnect. The minimum interval is five minutes. Each active beacon monitors the QRG of its own category: manual changes move the reference, while an automatic UCXLog or Win-Test jump of more than 5 kHz disables only the affected beacon. The **MyBeacon** chip shows `off`, `1/2`, `on` and `FAILED` with clear traffic-light colours; its tooltip lists the state or last deactivation reason for each category.
|
||||
|
||||
- **New home on GitHub:** the project moved from the `praktimarc` account to the [HamRadioOnline](https://github.com/HamRadioOnline) GitHub organisation and now lives at [HamRadioOnline/kst4contest](https://github.com/HamRadioOnline/kst4contest). Old links to the repository, the releases, the issues and the wiki redirect to the new address. The Flatpak repository moved as well and now lives at `https://hamradioonline.github.io/kst4contest/`; existing Flatpak installations keep updating through the redirect of the old address, new setups use the new address from the installation manual. The AUR packages point to the new address too.
|
||||
|
||||
- **The terrain profile cache is separate:** computed terrain profiles now live in their own shared file `terrainprofilecache.db` and are stored per owner. Previously the whole cache was dropped whenever the callsign or locator changed, which would have discarded every computed profile on each profile switch.
|
||||
|
||||
@@ -154,7 +154,7 @@ Configuration of the individual logging applications, band and locator handling,
|
||||
|
||||
## TRX Sync Settings
|
||||
|
||||
TRX synchronisation imports the current frequency from the logging application and makes it available in KST4Contest as the local QRG of the first chat category. QSO and frequency synchronisation may use the same UDP receiver, but they remain separate functions: receiving a `RadioInfo` packet does not mark a station as worked, and receiving a QSO packet does not automatically change the local QRG.
|
||||
TRX synchronisation imports the current frequency from the logging application and makes it available as a local QRG in KST4Contest. Without SO2R this is `MYQRG` for the first chat category; with SO2R, radio 2 can update `SECONDQRG` separately. QSO and frequency synchronisation may use the same UDP receiver, but they remain separate functions: receiving a `RadioInfo` packet does not mark a station as worked, and receiving a QSO packet does not automatically change the local QRG.
|
||||
|
||||

|
||||
|
||||
@@ -172,11 +172,11 @@ Restart KST4Contest after changing the shared UDP port. Changes to the two QRG-s
|
||||
|
||||
### Which QRG Is Updated?
|
||||
|
||||
Both automatic sources update `MYQRG` only. This is the local QRG of the first or primary chat category.
|
||||
Without SO2R, both automatic sources update `MYQRG` only. This is the local QRG of the first or primary chat category; `SECONDQRG` remains independent and can be entered manually.
|
||||
|
||||
If a second chat is enabled, its QRG remains independent. It is not derived from incoming TRX packets and is available through `SECONDQRG`. The first category can therefore follow the logging application's frequency automatically while a separate QRG is entered manually for the second category.
|
||||
With SO2R, KST4Contest assigns packets to a radio: radio 1 updates `MYQRG`, while radio 2 updates `SECONDQRG`. The exact assignment is described under [SO2R: Keeping Two Radios Apart](#so2r-keeping-two-radios-apart).
|
||||
|
||||
When at least one automatic QRG source is enabled and supplies a valid value, the first category's QRG field in the main window follows the received frequency. If the expected packets do not arrive, verify the interface before the contest. Manual entry is available when both the general RadioInfo listener and Win-Test STATUS synchronisation are disabled.
|
||||
When at least one automatic QRG source is enabled and supplies a valid value, the assigned QRG field in the main window follows the received frequency. If the expected packets do not arrive, verify the interface before the contest. Manual entry remains possible and is treated as the deliberate new reference for beacon safety; a later logger packet is compared with that value.
|
||||
|
||||
### Main or Pass Frequency from Win-Test
|
||||
|
||||
@@ -220,7 +220,7 @@ The synchronised QRG can be used in every text processed by the common KST4Conte
|
||||
- snippets, and
|
||||
- automatic beacons.
|
||||
|
||||
`MYQRG` contains the complete QRG of the first chat category. `MYQRGSHORT` uses only its first seven characters. `SECONDQRG` contains the separately entered QRG of the second chat category.
|
||||
`MYQRG` contains the complete QRG of the first chat category. `MYQRGSHORT` uses only its first seven characters. `SECONDQRG` contains the QRG of the second chat category, whether it was entered manually or supplied by radio 2 with SO2R enabled.
|
||||
|
||||
Examples:
|
||||
|
||||
@@ -603,13 +603,13 @@ KST4Contest uses one shared timer for both chat categories. Each category nevert
|
||||
|
||||
When both beacons are enabled, they are sent one after the other in their respective categories during the same timer run. The second beacon is only considered while the second category is enabled and fully synchronised in the same ON4KST session.
|
||||
|
||||
Both enable settings affect the running timer immediately. They neither restart the countdown nor send a message straight away; the next regular timer run checks the current state. Only **Save Settings** stores the enable state for the next program start. Message templates and the interval continue to follow the normal Apply/Save workflow.
|
||||
Both enable settings affect the running timer immediately. They neither restart the countdown nor send a message straight away; the next regular timer run checks the current state. Activation deliberately lasts only for the current program session: both beacons are off after every program start and profile switch. A normal ON4KST reconnect within the same program session preserves their state. Message templates and the interval continue to follow the normal Apply/Save workflow.
|
||||
|
||||
The **MyBeacon** status chip in the main window combines the currently usable categories: **off** means that no beacon is enabled, **1/2** that one of two categories is enabled, and **on** that every usable category is enabled. Clicking the chip enables or disables all usable beacons together. If the second chat is disabled, the chip changes only the primary-category beacon and leaves the stored secondary-beacon setting untouched.
|
||||
The **MyBeacon** status chip in the main window combines the currently usable categories: **off** means that no beacon is enabled, **1/2** that one of two categories is enabled, and **on** that every usable category is enabled. Clicking the chip enables or disables all usable beacons together. If the second chat is disabled, the chip changes only the primary-category beacon. **off** uses red text, while **1/2** and **on** remain green. The active chip briefly flashes orange when activity is reported. **FAILED** overrides this with a persistent red background and white text; no orange activity flash is shown then. The tooltip lists the current state or last general deactivation reason for each category.
|
||||
|
||||
### Interval and timer behaviour
|
||||
|
||||
The interval is entered in whole minutes. The minimum permitted value is one minute.
|
||||
The interval is entered in whole minutes. The minimum permitted value is five minutes. Smaller values from an older configuration are raised to five minutes when loaded.
|
||||
|
||||
After the chat connection has been established, KST4Contest performs the first beacon check after approximately ten seconds. The configured interval applies after that initial check.
|
||||
|
||||
@@ -635,15 +635,19 @@ A suitable message for the primary chat category is:
|
||||
calling cq at MYQRGSHORT, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
KST4Contest automatically appends the fixed suffix ` *a*` to every transmitted beacon message. It therefore does not belong in the configured template.
|
||||
|
||||
Use `SECONDQRG` for the second chat if it operates on a different frequency:
|
||||
|
||||
```text
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Variables are resolved again on every timer run. If the logging software changes the QRG stored in `MYQRG`, the next beacon can already contain the updated value. The message template does not have to be edited.
|
||||
Variables are resolved again on every timer run. `MYQRG` and `MYQRGSHORT` always require a valid primary-category QRG, while `SECONDQRG` always requires a valid secondary-category QRG. This assignment is independent of which beacon template contains the variable.
|
||||
|
||||
`MYQRG` and `MYQRGSHORT` always refer to the primary chat category. Enabling or selecting the second chat does not change this assignment.
|
||||
If a beacon template contains one of these QRG variables, that beacon can remain enabled only while the assigned QRG is valid. If the value is missing or invalid, only that beacon remains off or is disabled, and its tooltip states the reason. A template without a QRG variable may still be enabled when no QRG is available.
|
||||
|
||||
When enabled, each beacon remembers the valid QRG of its own category as its reference. If no valid own QRG exists yet and the template has no QRG variable, the first valid frequency reported later by the user or logger becomes the reference. A manual QRG change is treated as intentional and moves the reference to the new value. If UCXLog or Win-Test later changes the assigned QRG by more than 5 kHz from that reference, KST4Contest disables the affected beacon automatically. Exactly 5 kHz is still permitted. Without SO2R, an automatic logger QRG affects only the primary beacon; with SO2R, the secondary QRG and beacon are handled separately.
|
||||
|
||||
Station-specific variables such as `QRZNAME`, `FIRSTAP` and `SECONDAP` require a selected remote station. A public beacon has no such destination, so these variables are not resolved in beacon messages.
|
||||
|
||||
@@ -651,16 +655,16 @@ Station-specific variables such as `QRZNAME`, `FIRSTAP` and `SECONDAP` require a
|
||||
|
||||
KST4Contest validates both the configured template and the message which remains after all variables have been resolved.
|
||||
|
||||
The following restrictions apply:
|
||||
The following restrictions apply to the resolved beacon content before the fixed suffix:
|
||||
|
||||
- The final message must not be empty.
|
||||
- It must not be empty.
|
||||
- It must not exceed 120 characters.
|
||||
- The protocol separator `|` is not permitted.
|
||||
- Line breaks are not permitted.
|
||||
|
||||
An invalid entry is not accepted as the new beacon configuration. If a template becomes invalid only after resolving its variables, for example because the resulting text exceeds 120 characters, that beacon run is skipped.
|
||||
KST4Contest then appends ` *a*`, so the transmitted beacon message may contain up to 124 characters. An invalid entry is not accepted as the new beacon configuration. If a template becomes invalid only after resolving its variables, for example because the resolved content exceeds 120 characters, that beacon run is skipped.
|
||||
|
||||
A template which consists only of a temporarily empty global variable may still be stored. This can happen during startup before the first QRG has been received from the logger. KST4Contest does not send an empty message while the variable has no usable value.
|
||||
A template containing a QRG variable may be stored before a matching QRG is available, but the affected beacon cannot then be enabled. If a required QRG becomes invalid later, an active beacon is disabled. If the template contains no QRG variable, an invalid own QRG only clears the reference and the beacon remains enabled.
|
||||
|
||||
### When should the beacon be disabled?
|
||||
|
||||
@@ -668,7 +672,7 @@ The beacon is useful only while its QRG matches the actual operation. Leaving it
|
||||
|
||||
In plain terms: the beacon saves work while calling CQ on a fixed QRG. It should be disabled while moving around the band.
|
||||
|
||||
Changes take effect during the current connection. Use **Save Settings** afterwards to retain the enable settings, message templates and interval for the next program start.
|
||||
Changes take effect during the current connection. **Save Settings** stores the message templates and interval for the next program start. Activation is deliberately not stored and must be performed again in every program session.
|
||||
|
||||
---
|
||||
|
||||
@@ -690,12 +694,12 @@ A suitable message is:
|
||||
Sri, I am not taking part in this contest. No skeds.
|
||||
```
|
||||
|
||||
Do not add the `[KST4C Automsg]` prefix to the configured text. KST4Contest inserts it automatically.
|
||||
Do not add the `[KST4C Automsg]` prefix or the fixed ` *a*` suffix to the configured text. KST4Contest inserts both automatically.
|
||||
|
||||
The message received by the remote station may therefore be:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] Sri, I am not taking part in this contest. No skeds.
|
||||
[KST4C Automsg] Sri, I am not taking part in this contest. No skeds. *a*
|
||||
```
|
||||
|
||||
The incoming private message remains visible. The function neither blocks nor discards the request; it merely avoids entering the same answer repeatedly.
|
||||
@@ -726,7 +730,7 @@ The answer contains only the QRG belonging to the category in which the request
|
||||
A possible reply is:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] QRG is: 144.300.00
|
||||
[KST4C Automsg] QRG is: 144.300.00 *a*
|
||||
```
|
||||
|
||||
The values come from the same QRG fields used by `MYQRG` and `SECONDQRG`. The primary QRG may be entered manually or updated through [TRX synchronisation](#trx-sync-settings). The second category uses the value configured or entered for that category; with SO2R, radio 2 writes this field (`SECONDQRG`).
|
||||
@@ -762,7 +766,7 @@ The existing `client_settings_window_messagehandling.png` screenshot does not ye
|
||||
|
||||
### Protection against repeated replies
|
||||
|
||||
Every automatically generated reply contains the fixed prefix:
|
||||
Every automatic reply contains the fixed prefix:
|
||||
|
||||
```text
|
||||
[KST4C Automsg]
|
||||
@@ -770,6 +774,8 @@ Every automatically generated reply contains the fixed prefix:
|
||||
|
||||
The general and QRG-specific functions ignore messages which already contain this prefix. This prevents two clients with automatic replies enabled from answering each other indefinitely.
|
||||
|
||||
In addition, every chat message generated automatically by KST4Contest ends with ` *a*`. This applies to general and QRG-specific replies, `//VER` replies and both beacons. The suffix is always appended, even if the configured text already ends with it. Manually sent messages and internal ON4KST control frames remain unchanged.
|
||||
|
||||
A common two-minute cooldown additionally applies to both reply types. The cooldown is tracked separately for each complete callsign and chat category.
|
||||
|
||||
This means:
|
||||
|
||||
@@ -93,9 +93,9 @@ KST4Contest keeps the required information together between these steps. Changin
|
||||
|
||||
During operation on a mainly fixed CQ frequency, `MYQRG` and `SECONDQRG` should match the frequencies actually in use. TRX synchronisation can update `MYQRG` automatically only when it is enabled and actually supplies valid packets. Before the contest, verify this with a real frequency change. Without a working automatic source, the value must be maintained manually.
|
||||
|
||||
The beacon can publish the current QRG, locator and antenna direction in the chat at regular intervals. Its variables are evaluated again for every transmission.
|
||||
The beacon can publish the current QRG, locator and antenna direction in the chat at regular intervals. It must be enabled deliberately after every program start or profile switch. A manually changed QRG becomes the new reference; if UCXLog or Win-Test later reports a jump of more than 5 kHz, the affected beacon is disabled automatically.
|
||||
|
||||
Disable the beacon while scanning across several frequencies. An automatically published QRG is useful only while it is still correct.
|
||||
While scanning across several frequencies, this safeguard prevents an unintended transmission with a new logger QRG. The status tooltip shows which category was disabled and why.
|
||||
|
||||
Shortcuts and snippets should cover the messages required regularly, for example:
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ When two chat categories are connected, the original context is retained. The re
|
||||
|
||||
If the required QRG is missing, KST4Contest sends no incomplete answer. Empty or locally invalid general reply texts are rejected as well.
|
||||
|
||||
Automatic replies need limits. KST4Contest adds the fixed prefix `[KST4C Automsg]`, ignores incoming messages which already contain that prefix and permits only one automatic reply to the same complete callsign in the same category within two minutes. The cooldown is shared by the general and QRG-specific reply functions.
|
||||
Automatic replies need limits. KST4Contest adds the fixed prefix `[KST4C Automsg]`, ignores incoming messages which already contain that prefix and permits only one automatic reply to the same complete callsign in the same category within two minutes. The cooldown is shared by the general and QRG-specific reply functions. In addition, every automatically generated chat message, including `//VER` replies and beacons, ends with ` *a*`.
|
||||
|
||||
A rejected reply does not start the cooldown. After entering the missing QRG or correcting the configured text, KST4Contest can therefore answer the next request immediately.
|
||||
|
||||
@@ -796,13 +796,13 @@ The timeline is a preview. AirScout data can change, and a stored sked guarantee
|
||||
|
||||
KST4Contest can send recurring CQ messages to the public chat. The beacon is intended for longer periods of calling CQ on a fixed frequency: it publishes the local QRG regularly without requiring the operator to enter the same text again.
|
||||
|
||||
Both chat categories use one shared interval, but each category has its own enable setting and message template. The second beacon is only sent while the second category is enabled and fully synchronised in the shared ON4KST session.
|
||||
Both chat categories use one shared interval of at least five minutes, but each category has its own enable setting and message template. Activation lasts only for the current program session and must be repeated after a program start or profile switch. An ON4KST reconnect does not change it. The second beacon is only sent while the second category is enabled and fully synchronised in the shared ON4KST session.
|
||||
|
||||
Global variables such as `MYQRG`, `SECONDQRG`, `MYLOCATOR` and `MYQTF` are resolved immediately before every transmission. A QRG updated by the logging software can therefore appear in the next beacon.
|
||||
Global variables such as `MYQRG`, `SECONDQRG`, `MYLOCATOR` and `MYQTF` are resolved immediately before every transmission. Each active beacon also monitors the QRG of its own category. A manual change moves the reference; an automatic jump of more than 5 kHz from UCXLog or Win-Test disables the affected beacon.
|
||||
|
||||
Before transmission, KST4Contest validates the completely resolved message. Empty messages, line breaks, the protocol separator `|` and messages exceeding 120 characters are not sent.
|
||||
Before transmission, KST4Contest validates the completely resolved beacon content. Empty content, line breaks, the protocol separator `|` and content exceeding 120 characters are not sent. It then appends ` *a*`, so the transmitted message may contain up to 124 characters.
|
||||
|
||||
Disable the beacon while searching the band or changing QRG frequently. An automatically published frequency is useful only while somebody is actually listening and calling there.
|
||||
The coloured **MyBeacon** chip shows active, partially active, off and failed states. Its tooltip lists the state or last deactivation reason for each category.
|
||||
|
||||
Configuration, timer behaviour and available variables: [Configuration – Beacon Settings](en-Configuration#beacon-settings).
|
||||
|
||||
|
||||
@@ -307,16 +307,20 @@ If the second chat category uses a different QRG, its template must contain `SEC
|
||||
calling cq at SECONDQRG, ant MYQTF deg, loc MYLOCATOR
|
||||
```
|
||||
|
||||
Global variables are evaluated again on every timer run. A QRG updated by the logging software can therefore already appear in the next beacon message.
|
||||
Global variables are evaluated again on every timer run. `MYQRG` and `MYQRGSHORT` always require a valid primary-category QRG, while `SECONDQRG` always requires a valid secondary-category QRG, regardless of which beacon template contains them. If a required QRG is unavailable, only the affected beacon cannot be enabled or is disabled. A template without a QRG variable may still be enabled without an available QRG.
|
||||
|
||||
The fully resolved beacon text:
|
||||
When enabled, each beacon remembers the QRG of its own category. A manual QRG change moves this reference. If UCXLog or Win-Test later changes the assigned QRG by more than 5 kHz, KST4Contest disables the affected beacon instead of silently including the new frequency in its next transmission. If no own QRG exists when the beacon is enabled, the first valid frequency entered or received later becomes the reference.
|
||||
|
||||
The fully resolved beacon content before the fixed ` *a*` suffix:
|
||||
|
||||
- must contain at least one valid character,
|
||||
- must not exceed 120 characters,
|
||||
- must not contain the protocol separator `|`, and
|
||||
- must not contain line breaks.
|
||||
|
||||
If the text is empty or invalid when transmission is due, that beacon run is skipped.
|
||||
KST4Contest appends the suffix automatically afterwards. The transmitted beacon message may therefore contain up to 124 characters.
|
||||
|
||||
If the resolved text is empty or invalid for another reason when transmission is due, that beacon run is skipped and the status chip shows **FAILED**.
|
||||
|
||||
The interval, activation and behaviour of both categories are described under [Configuration – Beacon Settings](en-Configuration#beacon-settings).
|
||||
|
||||
|
||||
@@ -570,7 +570,7 @@ If the layout has become inconvenient, first move the dividers and column widths
|
||||
|
||||
## Operating Tips
|
||||
|
||||
- **Toggle beacons quickly**: Keep the settings window open to control each category separately. Click the **MyBeacon** status chip to enable or disable all currently usable beacons together.
|
||||
- **Toggle beacons quickly**: Keep the settings window open to control each category separately. Click the **MyBeacon** status chip to enable or disable all currently usable beacons together. **off** uses red text, **1/2** and **on** are green, and the chip flashes orange when activity is reported. **FAILED** stays red with white text. The tooltip lists the state or last deactivation reason for each category.
|
||||
- **Right-click in the user list**: Opens the snippet menu and additional actions, including QRZ.com profiles and NOT-QRV marks.
|
||||
- **Press Enter while working in the chat**: If the send field contains text, Enter sends it directly even when another control has focus.
|
||||
- **Stop the beacon while scanning**: Disable the beacon while moving through frequencies to avoid flooding the chat with unnecessary messages.
|
||||
- **Check the beacon after startup**: Both beacons are off after a program start or profile switch. A normal ON4KST reconnect within the same program session does not change their state.
|
||||
@@ -47,7 +47,7 @@ pse qrg
|
||||
and answers with the QRG belonging to the chat category in which the request was received:
|
||||
|
||||
```text
|
||||
[KST4C Automsg] QRG is: 144.300.00
|
||||
[KST4C Automsg] QRG is: 144.300.00 *a*
|
||||
```
|
||||
|
||||
When both functions are enabled, a recognised QRG request receives the QRG reply only. It does not additionally trigger the general answer.
|
||||
@@ -82,6 +82,8 @@ Every automatic reply contains:
|
||||
|
||||
Messages which already contain this prefix are not answered automatically. Two KST4Contest clients therefore do not continue replying to each other.
|
||||
|
||||
Every chat message generated automatically by KST4Contest also ends with ` *a*`. This includes general and QRG-specific replies, `//VER` replies and both beacons. The suffix is added even when the configured text already ends with it; manual messages are unchanged.
|
||||
|
||||
A shared two-minute cooldown additionally applies to the general and QRG-specific functions. It is tracked separately for each complete callsign and chat category.
|
||||
|
||||
A rejected reply does not start the cooldown. If a QRG was missing and is entered afterwards, the next request can be answered immediately.
|
||||
|
||||
@@ -170,11 +170,13 @@ A public beacon has no selected remote station. It can therefore use only global
|
||||
|
||||

|
||||
|
||||
Each chat category has its own enable setting and message template. Both categories use the same timer interval. The second beacon is sent only when the second category and its beacon are enabled in the shared ON4KST session.
|
||||
Each chat category has its own enable setting and message template. Both categories use the same timer interval. Beacon activation is session-only: both are off after a program start or profile switch, but an ON4KST reconnect within the same program session preserves their state. The second beacon is sent only when the second category and its beacon are enabled in the shared ON4KST session.
|
||||
|
||||
Global variables are evaluated again on every timer run. A QRG changed by the logger can therefore be included in the next beacon without editing its template.
|
||||
Global variables are evaluated again on every timer run. `MYQRG` and `MYQRGSHORT` require a valid primary QRG, while `SECONDQRG` requires a valid secondary QRG, regardless of which beacon template contains them. A template without a QRG variable may be enabled before any QRG is known.
|
||||
|
||||
The completely resolved beacon text must not exceed 120 characters. The configured minimum interval is one minute.
|
||||
When enabled, each beacon remembers the QRG of its own category. A manual QRG change moves that reference. A later UCXLog or Win-Test change of more than 5 kHz disables the affected beacon automatically; exactly 5 kHz remains allowed. If no reference exists yet, the first valid manual or logger QRG establishes it.
|
||||
|
||||
The completely resolved beacon content before the fixed suffix must not exceed 120 characters. KST4Contest then appends ` *a*`, so the transmitted message may contain up to 124 characters. The configured minimum interval is five minutes.
|
||||
|
||||
A suitable template is:
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ title: TRX/QRG Synchronisation
|
||||
icon: 🎛️
|
||||
category: Station Control
|
||||
since: "1.0"
|
||||
summary: Keep the primary operating frequency in MYQRG current from RadioInfo or Win-Test STATUS packets while leaving logging and SECONDQRG independent.
|
||||
description: KST4Contest can take the local primary QRG from compatible logger packets and make it available immediately to messages, beacons, QRG replies and Win-Test sked handover.
|
||||
summary: Keep MYQRG and, with SO2R, SECONDQRG current from RadioInfo or Win-Test STATUS packets while leaving QSO logging independent.
|
||||
description: KST4Contest can take local QRGs from compatible logger packets and make them available immediately to messages, beacons, QRG replies and Win-Test sked handover.
|
||||
tagsList:
|
||||
- TRX synchronisation
|
||||
- QRG
|
||||
@@ -25,34 +25,34 @@ related:
|
||||
|
||||
Changing frequency in the logger and then copying the same value into the chat client is easy to forget. The result may be a perfectly valid beacon or QRG reply which points to yesterday's frequency.
|
||||
|
||||
KST4Contest can therefore update the primary local QRG, `MYQRG`, from the logger. The current value is then available immediately wherever the application's own QRG is required.
|
||||
KST4Contest can therefore update the primary local QRG, `MYQRG`, from the logger. With SO2R enabled, radio 2 can update `SECONDQRG` independently. The current values are then available immediately wherever the application's own QRG is required.
|
||||
|
||||
TRX/QRG Synchronisation has existed since version 1.0. Support for native Win-Test `STATUS` packets is included from version 1.31 onwards.
|
||||
|
||||

|
||||
|
||||
## Which sources can update MYQRG?
|
||||
## Which sources can update the local QRGs?
|
||||
|
||||
Two automatic sources are available:
|
||||
|
||||
- compatible `RadioInfo` packets received through the shared log-sync UDP listener; and
|
||||
- native Win-Test `STATUS` packets received through the Win-Test network listener.
|
||||
|
||||
If neither source is enabled, `MYQRG` can be entered manually in the main window. Enabling an automatic source makes the received value the primary QRG instead.
|
||||
If neither source is enabled, `MYQRG` and `SECONDQRG` can be entered manually in the main window. Without SO2R, automatic packets update `MYQRG` only. With SO2R, the configured radio or station mapping assigns radio 1 to `MYQRG` and radio 2 to `SECONDQRG`.
|
||||
|
||||
## Frequency and QSO synchronisation are separate
|
||||
|
||||
The general UDP listener may receive QSO data and `RadioInfo` packets on the same port, but KST4Contest treats them as different functions.
|
||||
|
||||
A `RadioInfo` packet can update `MYQRG`; it does not mark a station as worked. Conversely, QSO or Worked synchronisation updates the log-derived station state without automatically changing the local QRG.
|
||||
A `RadioInfo` packet can update a local QRG; it does not mark a station as worked. Conversely, QSO or Worked synchronisation updates the log-derived station state without automatically changing a local QRG.
|
||||
|
||||
This separation is deliberate. A radio frequency says where the local station is operating. It says nothing about which remote station has just been worked.
|
||||
|
||||
## MYQRG does not mean SECONDQRG
|
||||
## MYQRG and SECONDQRG remain distinct
|
||||
|
||||
Both automatic sources update `MYQRG` only. This is the local QRG of the first or primary chat category.
|
||||
Without SO2R, both automatic sources update `MYQRG` only. This is the local QRG of the first or primary chat category; the secondary value remains independent.
|
||||
|
||||
`SECONDQRG` remains independent and is not derived from incoming TRX packets. A dual-chat setup can therefore follow the logger automatically for the primary category while retaining a separately entered QRG for the second category.
|
||||
With SO2R enabled, `RadioInfo` packets use `RadioNr` to update radio 1 or radio 2. Win-Test uses the configured station name for each radio. Radio 2 updates `SECONDQRG`, without changing the meaning of either variable.
|
||||
|
||||
## Main or pass frequency from Win-Test
|
||||
|
||||
@@ -64,27 +64,29 @@ Several Win-Test operating positions may transmit `STATUS` packets on the same n
|
||||
|
||||
## What happens with several frequency sources?
|
||||
|
||||
All enabled sources write to the same `MYQRG` value. KST4Contest does not assign a fixed priority to `RadioInfo` and Win-Test `STATUS` packets.
|
||||
All enabled sources assigned to the same radio write to the same QRG value. KST4Contest does not assign a fixed priority to `RadioInfo` and Win-Test `STATUS` packets.
|
||||
|
||||
If several sources are active, the most recently processed packet determines the current primary QRG. That is useful when the sources describe the same radio. Independent radios should not compete for the same value unless that last-packet behaviour is actually intended.
|
||||
If several sources are active, the most recently processed packet for that radio determines its current QRG. That is useful when the sources describe the same radio. Independent radios should use the SO2R assignments instead of competing for the same value.
|
||||
|
||||
## Where is the synchronised QRG used?
|
||||
|
||||
The current primary QRG is immediately available for:
|
||||
The current assigned QRG is immediately available for:
|
||||
|
||||
- [macros and variables](/features/macros/), including `MYQRG` and `MYQRGSHORT`;
|
||||
- [macros and variables](/features/macros/), including `MYQRG`, `MYQRGSHORT` and `SECONDQRG`;
|
||||
- automatic beacons;
|
||||
- [automatic QRG replies](/features/automatic-replies/) in the primary chat category; and
|
||||
- a matching primary local-frequency fallback when a [sked is handed to Win-Test](/features/sked-reminder/).
|
||||
|
||||
The sked handover still checks whether the local QRG belongs to the selected band. Synchronisation supplies the value; it does not bypass that validation.
|
||||
|
||||
An active beacon treats a manual QRG edit as intentional and moves its category reference to the new value. If UCXLog or Win-Test later changes that category QRG by more than 5 kHz, KST4Contest disables the affected beacon. Without SO2R only the primary beacon can be affected by an automatic QRG update; with SO2R both category references are monitored independently.
|
||||
|
||||
## What the function does not do
|
||||
|
||||
TRX/QRG Synchronisation tracks the local station's primary frequency. It does not detect the QRG of a remote station from chat text; that is the separate [Automatic QRG Detection](/features/qrg-detection/) function.
|
||||
TRX/QRG Synchronisation tracks the local station's assigned frequency or frequencies. It does not detect the QRG of a remote station from chat text; that is the separate [Automatic QRG Detection](/features/qrg-detection/) function.
|
||||
|
||||
It also does not turn a `RadioInfo` packet into a logged QSO. Worked information remains the responsibility of [Log Synchronisation](/features/log-sync/).
|
||||
|
||||
In practical terms: one current local QRG can feed several operating workflows, but it remains one value. `SECONDQRG`, remote-station frequencies and Worked state keep their own meanings.
|
||||
In practical terms, each current local QRG can feed several operating workflows, while primary QRG, secondary QRG, remote-station frequencies and Worked state keep their own meanings.
|
||||
|
||||
[Read the complete TRX Sync configuration in the manual.](/manual/en/configuration/#trx-sync-settings)
|
||||
Reference in new issue
Block a user