Restore the turn-antenna button and send the PSTRotator azimuth with one decimal (#126)

* Send the PSTRotator azimuth with one decimal instead of truncating it

setAzimuth cast the azimuth to int, so 145.7 degrees turned the antenna
to 145. It is now rounded to one decimal and formatted with Locale.ROOT,
so a German default locale cannot turn the dot into a comma; a value that
rounds up to 360.0 is sent as 0.0. The loopback test pinned the old
truncation and now expects the decimal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bring back the turn-antenna button lost in the Compose migration

The JavaFX station panel had "Turn ant1 to <call>", the only caller of
ChatController.rotateTo; removing JavaFX (fe4215bc) dropped it, so the
antenna could no longer be turned from KST4Contest at all. The Compose
panel gets the button again, enabled when a rotator callback is wired and
the selected station has a finite heading.

Review follow-ups for the decimal azimuth: formatAzimuth refuses NaN and
infinite values and wraps any finite value into [0, 360). The "reached"
check before the SPID retry uses a 1.0 degree tolerance, because the
target now carries a decimal while many controllers report whole degrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep rotateTo's Javadoc on the method and word the German label as a bearing

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:
Rsclub2_2andClaude Opus 5.5 authored and GitHub committed 2026-10-16 10:47:53 +02:00
1 parent e1e6fe7297
commit 2898c530d9
12 files changed
+150 -5

No files matched your search

@@ -184,6 +184,10 @@ fun SelectedStationPanel(
FlowRow(horizontalArrangement = Arrangement.spacedBy(Density.BUTTON_GAP)) {
Form.button(strings.selectedShowPathInAs, onClick = onShowPathInAirScout)
Form.button("🗺 Show on map", onClick = onShowOnMap)
Form.button(
strings.selectedTurnAntenna(state.selected?.callSignRaw.orEmpty()),
enabled = state.canTurnAntenna,
) { state.turnAntenna() }
Form.button(strings.selectedLookupQrz, enabled = state.canLookUp) {
openInBrowser.accept(state.qrzComUrl())
}
@@ -54,6 +54,8 @@ enum class SelectedMessageFilter(val label: String) {
* `propagateNotQrvStateToActiveMembers` does
* @param createSked takes the station, the minutes, the band and the mode; returns
* whether it was accepted
* @param turnAntennaTo turns the antenna to an azimuth in degrees, which is what
* `ChatController.rotateTo` does; null where no rotator can be driven
*/
class SelectedStationState(
private val propagateNotQrv: (ChatMember) -> Unit,
@@ -63,6 +65,7 @@ class SelectedStationState(
private val onMessageFilterChange: (SelectedMessageFilter?) -> Unit = { },
private val detectBands: (ChatMember) -> String = { "" },
private val resolveDefaultSkedBand: (ChatMember) -> Band? = { null },
private val turnAntennaTo: ((Double) -> Unit)? = null,
) {
var selected: ChatMember? by mutableStateOf(null)
@@ -103,6 +106,16 @@ class SelectedStationState(
val canLookUp: Boolean
get() = !selected?.callSign.isNullOrBlank()
/** Needs a rotator to drive and a finite heading to the selected station. */
val canTurnAntenna: Boolean
get() = turnAntennaTo != null && selected?.getQTFdirection()?.isFinite() == true
/** Turns the antenna to the selected station's heading; does nothing when it cannot. */
fun turnAntenna() {
val heading = selected?.getQTFdirection() ?: return
if (heading.isFinite()) turnAntennaTo?.invoke(heading)
}
fun select(member: ChatMember?) {
if (selected?.callSignRaw != member?.callSignRaw) {
skedBand = member?.let { resolveDefaultSkedBand(it) }