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

@@ -321,6 +321,13 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
rotatorClient.start();
}
/**
* How close the reported position must be to count as "reached". The target carries a
* decimal, while many rotor controllers report whole degrees: with 0.5 a target of 145.7
* and a report of 145 would count as missed and could trigger the retry.
*/
static final double ROTATOR_REACHED_TOLERANCE_DEGREES = 1.0;
/**
* Sends a new azimuth to PSTRotator without blocking the JavaFX thread.
*
@@ -399,7 +406,7 @@ public class ChatController implements ThreadStatusCallback, PstRotatorEventList
double currentAzimuth = lastReportedRotatorAzimuth;
if (Double.isFinite(currentAzimuth)
&& angularDistance(currentAzimuth, targetAzimuth) < 0.5) {
&& angularDistance(currentAzimuth, targetAzimuth) < ROTATOR_REACHED_TOLERANCE_DEGREES) {
LOGGER.log(
Level.FINE,
@@ -6,6 +6,7 @@ import kst4contest.model.ThreadStateMessage;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;
@@ -192,8 +193,24 @@ public class PstRotatorClient implements Runnable {
// Öffentliche Steuermethoden
public void setAzimuth(double degrees) {
// Formatierung ohne unnötige Nachkommastellen, falls nötig
sendCommand("AZIMUTH", String.valueOf((int) degrees));
sendCommand("AZIMUTH", formatAzimuth(degrees));
}
/**
* Formats an azimuth with one decimal and a dot as separator, independent of the
* default locale, wrapped into [0, 360): 360.0 is sent as 0.0, -10 as 350.0. The
* azimuth used to be truncated to whole degrees, so 145.7 turned the antenna to 145.
*
* @throws IllegalArgumentException for NaN or an infinite value, which has no heading
*/
static String formatAzimuth(double degrees) {
if (!Double.isFinite(degrees)) {
throw new IllegalArgumentException("Azimuth is not a finite number: " + degrees);
}
double rounded = Math.round(degrees * 10.0) / 10.0;
double wrapped = ((rounded % 360.0) + 360.0) % 360.0;
// Rounding can leave a tiny negative remainder that would print as -0.0.
return String.format(Locale.ROOT, "%.1f", wrapped == 0.0 ? 0.0 : wrapped);
}
public void setElevation(double degrees) {
@@ -0,0 +1,58 @@
package kst4contest.controller;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.util.Locale;
import org.junit.jupiter.api.Test;
/** The azimuth sent to PSTRotator: one decimal, dot separator, within [0, 360). */
class PstRotatorClientAzimuthFormatTest {
@Test
void keepsOneDecimalInsteadOfTruncating() {
assertEquals("145.7", PstRotatorClient.formatAzimuth(145.7));
assertEquals("145.0", PstRotatorClient.formatAzimuth(145.0));
assertEquals("0.0", PstRotatorClient.formatAzimuth(0.0));
}
@Test
void roundsToTheNearestTenth() {
assertEquals("145.7", PstRotatorClient.formatAzimuth(145.66));
assertEquals("145.7", PstRotatorClient.formatAzimuth(145.70000000000002));
assertEquals("12.3", PstRotatorClient.formatAzimuth(12.34));
}
@Test
void aValueRoundingUpTo360IsSentAsZero() {
assertEquals("0.0", PstRotatorClient.formatAzimuth(359.96));
assertEquals("359.9", PstRotatorClient.formatAzimuth(359.94));
}
@Test
void wrapsAnyFiniteValueIntoOneTurn() {
assertEquals("0.0", PstRotatorClient.formatAzimuth(720.0));
assertEquals("10.0", PstRotatorClient.formatAzimuth(370.0));
assertEquals("350.0", PstRotatorClient.formatAzimuth(-10.0));
assertEquals("0.0", PstRotatorClient.formatAzimuth(-0.0));
}
@Test
void refusesAValueWithoutAHeading() {
assertThrows(IllegalArgumentException.class, () -> PstRotatorClient.formatAzimuth(Double.NaN));
assertThrows(IllegalArgumentException.class,
() -> PstRotatorClient.formatAzimuth(Double.POSITIVE_INFINITY));
}
@Test
void usesADotEvenUnderAGermanDefaultLocale() {
Locale previous = Locale.getDefault();
try {
Locale.setDefault(Locale.GERMANY);
assertEquals("145.7", PstRotatorClient.formatAzimuth(145.7));
} finally {
Locale.setDefault(previous);
}
}
}
@@ -108,8 +108,8 @@ class PstRotatorClientLoopbackTest {
client.setTrackingMode(true);
client.setTrackingMode(false);
// The azimuth is truncated to whole degrees; the elevation is not.
assertEquals("<PST><AZIMUTH>145</AZIMUTH></PST>", receiveCommand());
// The azimuth keeps one decimal; it used to be truncated to whole degrees.
assertEquals("<PST><AZIMUTH>145.7</AZIMUTH></PST>", receiveCommand());
assertEquals("<PST><ELEVATION>10.5</ELEVATION></PST>", receiveCommand());
assertEquals("<PST><STOP>1</STOP></PST>", receiveCommand());
assertEquals("<PST><PARK>1</PARK></PST>", receiveCommand());