mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-26 16:15:51 +01:00
* Spec: SO2R with two radios in one instance (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Plan: SO2R implementation (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Move the rotator command rules into RotatorControl (#46) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Store the SO2R switch, the second Win-Test station and rotator 2 (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Route frequency reports to radio 1 or 2 (#46) Add the RadioSlot enum (FIRST, SECOND) with its QRG and QTF targets, and the pure RadioSlotRouter that decides per RadioInfo packet and per Win-Test station which radio a report belongs to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Send RadioInfo of radio 2 to SECONDQRG when SO2R is on (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Send Win-Test STATUS of the second station to SECONDQRG when SO2R is on (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run a second PSTRotator for radio 2 with SO2R (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Start rotator 2 only while an ON4KST session runs (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Settings for SO2R, the second Win-Test station and rotator 2 (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Turn antenna 2 and show QTF2 with SO2R (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Make the SO2R switches observable so Apply shows them (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document SO2R (#46) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Refuse clashing PSTRotator ports in both directions (#46) Writing either port now checks same-or-adjacent against the drafted value of the other; rotor 1 is only checked while SO2R and rotor 2 are on. Also fixes a misplaced Javadoc, stale WinTest parseStatus Javadoc, docs wording and removes an unused import and dead comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Remove the executed SO2R plan (#46) The plan's content lives on in the commits, the spec and PROJECT_CONTEXT. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
feb4b4e2c9
commit
5b7c774f5f
37 files changed
+1587
-266
No files matched your search
@@ -0,0 +1,69 @@
|
||||
package kst4contest.view.compose
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.runComposeUiTest
|
||||
import kst4contest.model.ChatCategory
|
||||
import kst4contest.model.ChatMember
|
||||
import kst4contest.model.ChatMessage
|
||||
import kst4contest.model.ChatPreferences
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import java.nio.file.Path
|
||||
|
||||
/** The send line with SO2R: the display-only QTF2 field appears only while SO2R is on. */
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
class ChatInputRowSo2rScreenTest {
|
||||
|
||||
@TempDir
|
||||
lateinit var tempDir: Path
|
||||
|
||||
private fun prefs() = ChatPreferences(tempDir.resolve("preferences.xml")).apply {
|
||||
getActualQTF().set(145.0)
|
||||
getActualQTF2().set(270.0)
|
||||
}
|
||||
|
||||
private fun chatInputStateFor(prefs: ChatPreferences) = ChatInputState(object : ChatInputActions {
|
||||
override fun panelSelection(): ChatMember? = null
|
||||
override fun tableSelection(): ChatMember? = null
|
||||
override fun scoreSelection(): ChatMember? = null
|
||||
override fun resolveVariables(template: String, member: ChatMember?): String? = template
|
||||
override fun isAddressedToOwnCallsign(text: String): Boolean = false
|
||||
override fun resolveCategory(text: String, member: ChatMember?): ChatCategory? = null
|
||||
override fun mainCategory(): ChatCategory? = null
|
||||
override fun recordOutboundCq(text: String) {}
|
||||
override fun requestRecompute(reason: String) {}
|
||||
override fun queue(message: ChatMessage) {}
|
||||
override fun ownQrgMain(): String = prefs.getMYQRGFirstCat().get().orEmpty()
|
||||
override fun ownQrgSecond(): String = prefs.getMYQRGSecondCat().get().orEmpty()
|
||||
})
|
||||
|
||||
@Test
|
||||
fun `QTF2 is shown only with SO2R and cannot be edited`() = runComposeUiTest {
|
||||
val prefs = prefs()
|
||||
var so2r by mutableStateOf(false)
|
||||
setContent {
|
||||
ChatInputRow(
|
||||
state = chatInputStateFor(prefs),
|
||||
prefs = prefs,
|
||||
ownQrgMain = ObservedValue(prefs.getMYQRGFirstCat()),
|
||||
ownQrgSecond = ObservedValue(prefs.getMYQRGSecondCat()),
|
||||
antennaQtf = ObservedValue(prefs.getActualQTF()),
|
||||
antennaQtf2 = ObservedValue(prefs.getActualQTF2()),
|
||||
so2rEnabled = so2r,
|
||||
)
|
||||
}
|
||||
|
||||
onAllNodesWithText("270.0").assertCountEquals(0)
|
||||
so2r = true
|
||||
waitForIdle()
|
||||
onNodeWithText("270.0").assertExists()
|
||||
onNodeWithText("270.0").assertIsNotEnabled()
|
||||
}
|
||||
}
|
||||
@@ -259,4 +259,57 @@ class SelectedStationStateTest {
|
||||
|
||||
assertTrue(session.failMarked.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `antenna 2 is offered only with SO2R and turns rotator 2`() {
|
||||
val headings2 = mutableListOf<Double>()
|
||||
var offered = false
|
||||
val state = SelectedStationState(
|
||||
propagateNotQrv = { },
|
||||
createSked = { _, _, _, _ -> true },
|
||||
turnAntennaTo = { },
|
||||
turnSecondAntennaTo = { headings2.add(it) },
|
||||
secondAntennaOffered = { offered },
|
||||
)
|
||||
state.select(member("DL1ABC").apply { setQTFdirection(145.7) })
|
||||
|
||||
assertFalse(state.showsSecondAntenna)
|
||||
offered = true
|
||||
assertTrue(state.showsSecondAntenna)
|
||||
assertTrue(state.canTurnSecondAntenna)
|
||||
|
||||
state.turnSecondAntenna()
|
||||
|
||||
assertEquals(listOf(145.7), headings2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `antenna 2 is greyed out without a heading`() {
|
||||
val state = SelectedStationState(
|
||||
propagateNotQrv = { },
|
||||
createSked = { _, _, _, _ -> true },
|
||||
turnSecondAntennaTo = { },
|
||||
secondAntennaOffered = { true },
|
||||
)
|
||||
state.select(member("DL1ABC"))
|
||||
|
||||
assertTrue(state.showsSecondAntenna)
|
||||
assertFalse(state.canTurnSecondAntenna)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `antenna 2 offer follows the surroundings flag without a new selection`() {
|
||||
val surroundings = MainWindowSurroundings()
|
||||
val state = SelectedStationState(
|
||||
propagateNotQrv = { },
|
||||
createSked = { _, _, _, _ -> true },
|
||||
turnSecondAntennaTo = { },
|
||||
secondAntennaOffered = { surroundings.secondAntennaOffered },
|
||||
)
|
||||
state.select(member("DL1ABC"))
|
||||
|
||||
assertFalse(state.showsSecondAntenna)
|
||||
surroundings.secondAntennaOffered = true
|
||||
assertTrue(state.showsSecondAntenna)
|
||||
}
|
||||
}
|
||||
@@ -19,15 +19,106 @@ class StationTabStateTest(@TempDir private val tempDir: Path) {
|
||||
var mainApplied: ChatCategory? = null
|
||||
var secondApplied: ChatCategory? = null
|
||||
var secondWasApplied = false
|
||||
val secondRotatorCalls = mutableListOf<Unit>()
|
||||
val draft = SettingsDraft()
|
||||
val state = StationTabState(
|
||||
prefs,
|
||||
draft,
|
||||
{ mainApplied = it },
|
||||
{ secondApplied = it; secondWasApplied = true },
|
||||
applySecondRotator = { secondRotatorCalls.add(Unit) },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotator 2 settings restart it once per apply however many changed`() {
|
||||
val s = Session()
|
||||
|
||||
s.state.stn_pstRotator2Enabled = true
|
||||
s.state.stn_pstRotator2Host = "10.0.0.7"
|
||||
s.state.stn_pstRotator2Port = 12020
|
||||
val outcome = s.draft.apply()
|
||||
|
||||
assertTrue(outcome.isClean, outcome.failures.toString())
|
||||
assertEquals(12020, s.prefs.getStn_pstRotator2Port())
|
||||
assertEquals(1, s.secondRotatorCalls.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotator 2 on the port of rotator 1 is refused and stays in the draft`() {
|
||||
val s = Session()
|
||||
s.state.stn_pstRotatorPort = 12040
|
||||
|
||||
s.state.stn_pstRotator2Port = 12040
|
||||
val outcome = s.draft.apply()
|
||||
|
||||
assertEquals(1, outcome.failedWrites.size)
|
||||
assertTrue(outcome.failedWrites.single().contains("12040"), outcome.failedWrites.toString())
|
||||
assertEquals(12010, s.prefs.getStn_pstRotator2Port())
|
||||
assertEquals(12040, s.state.stn_pstRotator2Port, "the refused value is still shown")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotator 1 onto the port of rotator 2 is refused while SO2R and rotator 2 are on`() {
|
||||
val s = Session()
|
||||
s.prefs.setTrxSynch_so2rEnabled(true)
|
||||
s.prefs.setStn_pstRotator2Enabled(true)
|
||||
|
||||
s.state.stn_pstRotatorPort = 12010
|
||||
val outcome = s.draft.apply()
|
||||
|
||||
assertEquals(1, outcome.failedWrites.size, outcome.failedWrites.toString())
|
||||
assertTrue(outcome.failedWrites.single().contains("12010"), outcome.failedWrites.toString())
|
||||
assertNotEquals(12010, s.prefs.getStn_pstRotatorPort())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotator 1 onto the port of rotator 2 is accepted without SO2R`() {
|
||||
val s = Session()
|
||||
s.prefs.setStn_pstRotator2Enabled(true)
|
||||
|
||||
s.state.stn_pstRotatorPort = 12010
|
||||
val outcome = s.draft.apply()
|
||||
|
||||
assertTrue(outcome.isClean, outcome.failures.toString())
|
||||
assertEquals(12010, s.prefs.getStn_pstRotatorPort())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `rotator 1 onto the port of rotator 2 is accepted while rotator 2 is off`() {
|
||||
val s = Session()
|
||||
s.prefs.setTrxSynch_so2rEnabled(true)
|
||||
|
||||
s.state.stn_pstRotatorPort = 12010
|
||||
val outcome = s.draft.apply()
|
||||
|
||||
assertTrue(outcome.isClean, outcome.failures.toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `adjacent rotator ports are refused in both directions and distant ones accepted`() {
|
||||
for (p2 in intArrayOf(12041, 12039)) {
|
||||
val s = Session()
|
||||
s.state.stn_pstRotatorPort = 12040
|
||||
s.state.stn_pstRotator2Port = p2
|
||||
val outcome = s.draft.apply()
|
||||
assertEquals(1, outcome.failedWrites.size, "rotor 2 on $p2: ${outcome.failedWrites}")
|
||||
}
|
||||
for (p1 in intArrayOf(12011, 12009)) {
|
||||
val s = Session()
|
||||
s.prefs.setTrxSynch_so2rEnabled(true)
|
||||
s.prefs.setStn_pstRotator2Enabled(true)
|
||||
s.state.stn_pstRotatorPort = p1
|
||||
val outcome = s.draft.apply()
|
||||
assertEquals(1, outcome.failedWrites.size, "rotor 1 on $p1: ${outcome.failedWrites}")
|
||||
}
|
||||
val s = Session()
|
||||
s.prefs.setTrxSynch_so2rEnabled(true)
|
||||
s.prefs.setStn_pstRotator2Enabled(true)
|
||||
s.state.stn_pstRotatorPort = 12030
|
||||
assertTrue(s.draft.apply().isClean)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reading comes straight from the preferences`() {
|
||||
val s = Session()
|
||||
|
||||
@@ -15,7 +15,25 @@ class TrxSynchTabStateTest(@TempDir private val tempDir: Path) {
|
||||
val prefs = ChatPreferences(tempDir.resolve("preferences.xml"))
|
||||
val followerCalls = mutableListOf<Boolean>()
|
||||
val draft = SettingsDraft()
|
||||
val state by lazy { TrxSynchTabState(prefs, draft) { followerCalls.add(it) } }
|
||||
val secondRotatorCalls = mutableListOf<Unit>()
|
||||
val state by lazy {
|
||||
TrxSynchTabState(prefs, draft, applySecondRotator = { secondRotatorCalls.add(Unit) }) { followerCalls.add(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the SO2R switch and the second Win-Test station wait for apply and restart rotator 2 once`() {
|
||||
val s = Session()
|
||||
|
||||
s.state.trxSynch_so2rEnabled = true
|
||||
s.state.logsynch_wintestNetworkStationNameOfWintestClient2 = " STN2 "
|
||||
|
||||
assertFalse(s.prefs.isTrxSynch_so2rEnabled())
|
||||
s.draft.apply()
|
||||
|
||||
assertTrue(s.prefs.isTrxSynch_so2rEnabled())
|
||||
assertEquals("STN2", s.prefs.getLogsynch_wintestNetworkStationNameOfWintestClient2())
|
||||
assertEquals(1, s.secondRotatorCalls.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user