SO2R: two radios in one instance (#46) (#137)

* 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:
Rsclub2_2andClaude Opus 5.5 authored and GitHub committed 2026-10-16 19:14:27 +02:00
1 parent feb4b4e2c9
commit 5b7c774f5f
37 files changed
+1587 -266

No files matched your search

@@ -0,0 +1,197 @@
# Entwurf — SO2R: zwei Funkgeräte, eine Instanz (#46)
Freigegeben vom Maintainer am 2026-10-16 (Gespräch). Erster Schritt zu #46; MO2R und
Multi-Operator über mehrere Rechner sind eigene Vorhaben mit eigenem Entwurf.
## Warum
KST4Contest kennt heute genau ein Funkgerät:
- Jede RadioInfo-Meldung aus dem Logprogramm (N1MM, DXLog, UCXLog) überschreibt MYQRG. Das
Feld `RadioNr` wird nicht gelesen. Wer mit zwei Geräten arbeitet, sieht MYQRG bei jedem
Gerätewechsel springen, und die Makros tragen die QRG des zuletzt gemeldeten Geräts.
- Win-Test kennt einen einzigen Stationsnamen-Filter, also ebenfalls ein Gerät.
- Es gibt einen PSTRotator-Client, also nur „Antenne 1 drehen".
Ein SO2R-Platz (ein Operator, ein Rechner, eine ON4KST-Anmeldung, zwei Geräte mit je eigener
Antenne) braucht pro Gerät eine eigene QRG und einen eigenen Rotor.
## Umfang
Drin:
- Pro Gerät eine eigene QRG: Gerät 1 schreibt MYQRG, Gerät 2 schreibt SECONDQRG.
- Zuordnung über `RadioNr` (N1MM, DXLog, UCXLog) und über je einen Win-Test-Stationsnamen.
- Ein zweiter PSTRotator mit Button „Antenne 2 drehen" und eigenem QTF-Feld.
- Ein Schalter „SO2R", standardmäßig aus. Aus heißt: alles wie heute.
Bewusst nicht drin:
- Ein eigenes Rechenband pro Gerät (Pfadanalyse, Tropo-Spalte, Karte bleiben wie heute).
- Ein „aktiver Platz", auf den sich Gearbeitet, Skeds und Makros beziehen.
- Strahlrichtung beider Antennen auf der Karte.
- Mehr als zwei Geräte.
- MO2R und Multi-Operator. Das Modell schließt sie nicht aus (siehe unten), wird aber nicht
dafür ausgebaut.
## Modell
### `RadioSlot` (core)
Genau zwei Instanzen, Platz 1 und Platz 2. Ein Platz bündelt:
| | Platz 1 | Platz 2 |
|---|---|---|
| QRG-Ziel | `getMYQRGFirstCat()` (MYQRG) | `getMYQRGSecondCat()` (SECONDQRG) |
| Rotor | `RotatorControl` über `stn_pstRotator*` | `RotatorControl` über `stn_pstRotator2*` |
| gemeldete Richtung | `getActualQTF()` | neu `getActualQTF2()` |
| Win-Test-Stationsname | vorhandener Filter `logsynch_wintestNetworkStationNameOfWintestClient1` | neu `logsynch_wintestNetworkStationNameOfWintestClient2` |
Platz 2 existiert nur bei eingeschaltetem SO2R. Ohne SO2R gibt es keinen Rotor 2 und kein
QTF2.
`RadioSlot` ist die Einheit, an die ein späteres MO2R einen Operator oder eine Arbeitsfläche
hängen kann. Für SO2R wird nichts davon gebaut.
### `RotatorControl` (core, herausgezogen)
Drehbefehl, Wiederholung und die Toleranz „erreicht" (1°, `ROTATOR_REACHED_TOLERANCE_DEGREES`)
liegen heute im `ChatController`. Sie ziehen unverändert in eine eigene Klasse, die einmal pro
Platz instanziert wird: eigener `PstRotatorClient`, eigener Wiederholungs-Scheduler, eigene
zuletzt gemeldete Richtung. Das Verhalten bleibt bitgenau gleich, einschließlich der
vorhandenen Kompatibilitäts-Wiederholung; der `ChatController` delegiert nur noch.
PSTRotator antwortet auf Port+1. Eine zweite PSTRotator-Instanz braucht also eigene Ports; die
Vorgabe für Rotor 2 ist 12010 (Antwort auf 12011).
### `RadioSlotRouter` (core, rein)
Entscheidet, zu welchem Platz eine Meldung gehört. Rein und ohne Abhängigkeiten, damit die
Regel ohne Netz testbar ist.
RadioInfo:
| SO2R | `RadioNr` | Platz |
|---|---|---|
| aus | beliebig, auch fehlend | 1 |
| an | 1 | 1 |
| an | 2 | 2 |
| an | fehlend | 1 |
| an | anderer Wert oder nicht lesbar | verworfen, geloggt |
Win-Test STATUS:
| SO2R | Stationsname | Platz |
|---|---|---|
| aus | wie heute: Filter leer oder passt | 1, sonst verworfen |
| an | passt auf Name von Platz 2 | 2 |
| an | Name von Platz 1 leer oder passt | 1 |
| an | sonst | verworfen |
Namensvergleich wie heute beim Filter (`equalsIgnoreCase`). Die Filterung der ADDQSO-Pakete nach Stationsname bleibt unberührt.
## Datenfluss und Fäden
- `ReadUDPbyUCXMessageThread` liest zusätzlich `RadioNr`, fragt den Router und schreibt die QRG
über `uiDispatcher.runOnUi` in das QRG-Ziel des Platzes. Der Weg für eine RadioInfo ohne
Freq oder Mode bleibt unverändert (Paket verwerfen, weiter empfangen).
- `ReadUDPByWintestThread` fragt den Router mit dem Stationsnamen; Passfrequenz-Option und
Formatierung bleiben, nur das Ziel hängt vom Platz ab.
- Rotor-Rückmeldungen laufen im Empfangsfaden des jeweiligen Rotors und schreiben die Richtung
des Platzes, wie heute bei Rotor 1. Nichts wartet auf dem AWT-Ereignisfaden.
## Einstellungen
### Reiter TRX-Abgleich
- Neuer Schalter „SO2R: zwei Geräte getrennt führen" (`trxSynch_so2rEnabled`, Vorgabe aus).
- Bei SO2R an wird der vorhandene Win-Test-Stationsnamen-Filter zum Namen für Platz 1; ein neues
Feld nimmt den Namen für Platz 2 auf. Bei SO2R aus ist das neue Feld ausgegraut.
- Hinweistext: N1MM, DXLog und UCXLog werden über `RadioNr` zugeordnet, dafür ist nichts
einzustellen.
### Reiter Station, Abschnitt PSTRotator
- Zweiter Block „PSTRotator 2 (SO2R)": Schalter, Host, Port (Vorgaben aus, 127.0.0.1, 12010).
- Nur aktiv bei SO2R an. Der Hinweis „antwortet auf Port+1" gilt auch hier.
- Gleicher Port wie Rotor 1 wird beim Übernehmen abgelehnt, mit Meldung, wie andere ungültige
Eingaben.
### Übernehmen
Wie seit dem Entwurf vom 2026-10-16 (Einstellungen mit „Übernehmen"): alles wirkt erst mit
Übernehmen/Schließen oder Speichern.
- Ändern sich SO2R oder ein Wert von Rotor 2, wird Rotor 2 einmal neu gestartet oder gestoppt —
ein eigener Effekt-Schlüssel neben dem von Rotor 1.
- SO2R aus stoppt Rotor 2 und leert QTF2.
### preferences.xml
Neue Elemente: `trxSynch_so2rEnabled`, `logsynch_wintestNetworkStationNameOfWintestClient2`,
`stn_pstRotator2Enabled`, `stn_pstRotator2Host`, `stn_pstRotator2Port`. Fehlen sie in einer
älteren Datei, gelten die Vorgaben; keine Migration. Alles gilt pro Profil.
## Oberfläche
### Stationspanel
- Neben „Antenne 1 Richtung {0} drehen" steht „Antenne 2 Richtung {0} drehen"
(`selected.turnAntenna2`, de/en).
- Sichtbar nur bei SO2R an und aktiviertem Rotor 2. Ausgegraut nach derselben Regel wie
`canTurnAntenna` bei Rotor 1 (Rotor nicht verbunden, Station ohne Locator).
- Überall, wo heute „Antenne 1 drehen" angeboten wird, kommt „Antenne 2 drehen" dazu.
### Sendezeile
- Reihenfolge MYQRG | SECONDQRG | QTF | QTF2. QTF2 zeigt die Richtung von Rotor 2, nur zur
Anzeige, und ist nur bei SO2R sichtbar.
- Bei SO2R an lautet der Tooltip von SECONDQRG „QRG Gerät 2 (aus RadioNr 2 bzw.
Win-Test-Station 2)".
- Bei SO2R aus sieht die Zeile aus wie heute.
### Statusleiste
Rotor 2 meldet seinen Zustand wie Rotor 1 über `ThreadStatusCallback`, aber unter eigenem Thread-Namen, und bekommt so einen eigenen Knopf in der Statusleiste. Ein
Ausfall von Rotor 2 berührt Rotor 1 nicht.
### Unverändert
QTF-Filter der Stationsliste (Rotor 1), Karte, Pfadanalyse, Gearbeitet-Logik. Die Makros
MYQRG und SECONDQRG tragen bei SO2R die Frequenzen der beiden Geräte.
## Dokumentation
- Changelog-Eintrag de/en.
- Handbuch: kurzer Abschnitt „SO2R" — N1MM/DXLog/UCXLog brauchen nichts, Win-Test braucht zwei
Stationsnamen, die zweite PSTRotator-Instanz braucht einen anderen Port.
## Tests
Core:
- `RadioSlotRouterTest`: alle Zeilen der beiden Tabellen oben.
- `RotatorControlTest`: Toleranz „erreicht", keine Wiederholung bei Bewegung, die vorhandene
Wiederholung bei Stillstand — mit Fake-Client und Fake-Scheduler. Die bisherigen Rotor-Tests
bleiben unverändert grün; sie sichern das Herausziehen ab.
- Empfänger: ein echtes RadioInfo-Paket mit `RadioNr` 2 bzw. ein Win-Test-STATUS mit
Stationsname 2 setzt SECONDQRG, MYQRG bleibt unverändert. Bei SO2R aus setzt dasselbe Paket
MYQRG.
- `ChatPreferences`: Roundtrip der fünf neuen Elemente; eine alte Datei ohne sie ergibt die
Vorgaben (SO2R aus, Port 12010).
App:
- Einstellungen (`DraftedSettingsTableTest`-Gerüst): Übernehmen startet bzw. stoppt Rotor 2
genau einmal; gleicher Port wie Rotor 1 wird abgelehnt; SO2R aus stoppt Rotor 2 und leert
QTF2.
- Oberfläche (Compose-UI-Tests wie beim Stationspanel): Button Antenne 2 und QTF2 nur bei SO2R
sichtbar; Button ohne Rotor 2 ausgegraut; ohne SO2R ist die Sendezeile wie heute.
- i18n: neue Schlüssel de/en, vom Erzeuger beim Bau geprüft.
Live, durch den Maintainer:
- N1MM oder DXLog mit zwei Geräten: Gerätewechsel bewegt MYQRG und SECONDQRG getrennt.
- Zwei PSTRotator-Instanzen auf verschiedenen Ports: beide Buttons drehen die richtige Antenne,
QTF und QTF2 folgen.
- Win-Test mit zwei Stationen, falls verfügbar.