Skip to content

Den Rotor vom Telefon drehen — mit der Sicherung davor - #200

Merged
oe5sos merged 3 commits into
mainfrom
feat/handfunke-rotor
Oct 5, 2026
Merged

oe5sos merged 3 commits into
mainfrom
feat/handfunke-rotor

Conversation

@oe5sos

@oe5sos oe5sos commented Oct 5, 2026

Copy link
Copy Markdown
Owner

Stapelt auf #199 — Basiszweig ist feat/handfunke-aufwachen, weil beide dieselben Seiten-Dateien anfassen.

Punkt 2 der App-Zeitachse. Am anderen Ende der Leitung steht ein Mast mit
einer Antenne darauf, der sich auf Zuruf minutenlang dreht, und niemand steht
daneben. Danach richtet sich alles hier.

Die Nahtstelle

Der RotctldClient wird von RotorLogbookPanel angelegt — einem Fenster.
Dasselbe Muster wie beim Logbuch, wo WorkedBefore dort hängt und der
TCI-Server ihn deshalb nicht anfassen durfte: ein Netzdienst, der an einem
Fenster hängt, stirbt mit dem Fenster.

RadioModel bekommt darum eine Registrierung, keinen Besitz — das Panel
meldet seinen Client an, das Modell hält einen QPointer. Der sauberere Weg
wäre, den Besitz hierher zu holen (wie bei m_spotModel, das 2026-05-12 aus
SpotHubDialog hierher kam); das ist eine Architekturänderung an einem Gerät,
das am Mast hängt, und CLAUDE.md verlangt dafür den Betreiber. Bis dahin der
kleine, umkehrbare Schritt.

Die Sperren

rotor:;           -> rotor_ist:<grad>,<zustand>,<frisch 0|1>;
rotor_to:<grad>;  -> rotor_ok:<grad>;   oder rotor_err:<grund>;
rotor_stop:;      -> rotor_ok:stop;

Abfragen darf jeder angemeldete Client — hinsehen bewegt nichts.
Drehen nur mit TciAllowRemoteRotor=True, ab Werk False: dieselbe
Strenge wie beim Senden, aus demselben Grund. Abgelehnt wird mit Grund; ein
stummes Nein ist von einem Defekt nicht zu unterscheiden, und beim Rotor sieht
man zehn Sekunden lang ohnehin nichts.

frisch ist kein Beiwerk: eine Nadel, die eine veraltete Stellung zeigt, ohne
das zu sagen, ist schlimmer als eine, die nichts zeigt — so steht es schon im
Kopf von RotorController.h.

Was toDouble() angerichtet hätte

  • aus "" und "abc" macht toDouble() eine 0 — und null Grad ist Nord.
    Eine leere Zeile hätte die Antenne nach Norden gedreht.
  • NaN besteht jeden Bereichsvergleich, weil jeder Vergleich mit NaN falsch
    ist. Ein naiver Bereichstest lässt NaN durch.
  • 360 ist dieselbe Richtung wie 0 und gehört angenommen; 361 ist ein
    Tippfehler. Außerhalb wird abgelehnt statt umgerechnet: wer −90 schickt,
    hat sich vertan, und 270 Grad sind eine andere Antwort als „ich habe mich
    vertan".

Auf der Seite

Das Feld bleibt versteckt, bis Longpath einen Rotor meldet. An der QRP gibt
es keinen, und ein leeres Feld, das nichts kann, ist auf einem Telefon reiner
Platzverbrauch.

Die Scheibe wählt nur; gedreht wird mit dem Knopf darunter — dieselbe
Entscheidung wie bei der Sendetaste: ein Tipp, der sofort einen Mast dreht, ist
in einer Hosentasche eine schlechte Idee. Eine Wahl verfällt nach 15 s; sie
gehört sonst dem Finger von vorhin.

Nord ist oben und gezählt wird im Uhrzeigersinn — atan2(x, -y), nicht
atan2(y, x). Letzteres wäre Ost = 0 und gegen den Uhrzeigersinn und sieht auf
einer runden Scheibe genauso plausibel aus; der Prüfstand hält beide
Verwechslungen ausdrücklich fest.

Messing für die gemessene Stellung, Blau gestrichelt für das gewählte Ziel —
die beiden dürfen nie zu verwechseln sein.

Geprüft

48 Prüfpunkte: 8 (tst_rotor_peilung) + 9 (tst_tci_rotor, über den
echten WebSocket) + 31 (pruefe-rotor.mjs, ohne Browser).

Live gegen die Attrappe: Osten angetippt → „AUF 90° DREHEN", Knopf gedrückt →
der Rotor nimmt an, meldet „DREHT …", und die Nadel wandert (143° → 140° → …).
Danach steht der Knopf wieder auf „RICHTUNG WÄHLEN" — die Sicherung ist zu.

🤖 Generated with Claude Code

oe5sos and others added 2 commits October 5, 2026 07:26
Die Seite behandelte das Entsperren bis hierher GAR NICHT.
`visibilitychange` gab es genau einmal, und zwar um das Mikrofon
loszulassen. Was dabei wirklich passiert:

  * Der WebSocket ist tot, aber `onclose` kommt erst, wenn das System
    die Seite auftaut -- und der Wiederholungsabstand ist inzwischen auf
    10 s gewachsen. Man sieht sekundenlang nichts, obwohl das Funkgeraet
    bereit ist. LIVE GEMESSEN, gleiche Ausfalldauer (55 s):

        ohne Wecken:  Server zurueck 05:23:37, Seite da 05:23:45  -> 8 s
        mit Wecken:   Server zurueck 05:25:10, Seite da 05:25:13  -> 0 s
                      (dieselbe Sekunde wie das Wecken)

  * Der AudioContext ist von iOS angehalten. Ein angehaltener Kontext
    ruft seinen Rueckruf nie wieder auf -- der Ton bliebe stumm, bis
    jemand TON AUS und wieder EIN drueckt.

  * Wasserfall und S-Meter zeigen den Stand von VOR dem Sperren, ohne
    das zu sagen. Dieselbe Gattung Luege wie ein stehender Wasserfall:
    er behauptet ein leeres Band, und danach wird nicht gerufen. Jetzt
    zieht das Aufwachen denselben Strich, den ein Bandwechsel zieht --
    oben das Neue, unten das Alte.

Entschieden wird in `aufwachen.js`, und NICHT am `readyState` allein:
iOS friert die Seite ein, die Gegenseite raeumt auf, und der Socket
meldet noch OFFEN. Wer darauf wartet, dass `onclose` kommt, wartet unter
Umstaenden ewig. Darum zaehlt der Zustand UND die Stille -- kommt seit
drei Sekunden nichts an, wird neu verbunden, ganz gleich was der Socket
behauptet. Eine ueberfluessige Neuverbindung kostet eine halbe Sekunde,
eine ausgelassene den Moment, in dem man das Telefon in die Hand nimmt.

Die Grenze fuer "das Bild ist alt" liegt mit 1,5 s UNTER der fuer "der
Socket ist tot" (3 s): lieber einmal zu frueh sagen, dass das Bild von
vorhin ist, als einmal zu spaet. Ein falsch beschriftetes altes Bild ist
harmlos, ein unbeschriftetes nicht.

Die Abonnements muessen nicht erneuert werden -- sie haengen am
`open`-Ereignis und kommen mit der neuen Verbindung von selbst. Die
Spots schon, die laufen waehrend des Schlafens ab.

Drei Wege ins Aufwachen, weil nicht jedes System `visibilitychange`
zuverlaessig meldet: `visibilitychange`, `pageshow` mit `persisted`,
`focus`. Mehrfaches Aufwachen schadet nicht -- die Entscheidung wirft
eine lebendige Verbindung nicht weg, und genau das ist geprueft.

17 Pruefpunkte (pruefe-aufwachen.mjs), dazu die Live-Messung oben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Punkt 2 der App-Zeitachse. Am anderen Ende der Leitung steht ein Mast
mit einer Antenne darauf, der sich auf Zuruf minutenlang dreht, und
niemand steht daneben. Danach richtet sich alles hier.

── DIE NAHTSTELLE ─────────────────────────────────────────────────────

Der RotctldClient wird von `RotorLogbookPanel` angelegt -- einem
FENSTER. Dasselbe Muster wie beim Logbuch, wo `WorkedBefore` am
`RotorLogbookPanel` haengt und der TCI-Server ihn deshalb nicht
anfassen durfte: ein Netzdienst, der an einem Fenster haengt, stirbt
mit dem Fenster.

`RadioModel` bekommt darum eine REGISTRIERUNG, keinen Besitz: das
Panel meldet seinen Client an, das Modell haelt einen QPointer. Der
sauberere Weg waere, den Besitz hierher zu holen (wie bei
`m_spotModel`, das 2026-05-12 aus `SpotHubDialog` hierher kam) -- das
ist eine Architekturaenderung an einem Geraet, das an Martins Mast
haengt, und CLAUDE.md verlangt dafuer den Betreiber. Bis dahin der
kleine, umkehrbare Schritt.

── DIE SPERREN ────────────────────────────────────────────────────────

    rotor:;           -> rotor_ist:<grad>,<zustand>,<frisch 0|1>;
    rotor_to:<grad>;  -> rotor_ok:<grad>;   oder rotor_err:<grund>;
    rotor_stop:;      -> rotor_ok:stop;

ABFRAGEN darf jeder angemeldete Client -- hinsehen bewegt nichts.
DREHEN nur mit `TciAllowRemoteRotor=True`, ab Werk FALSE: dieselbe
Strenge wie beim Senden, aus demselben Grund. Abgelehnt wird MIT
Grund; ein stummes Nein ist von einem Defekt nicht zu unterscheiden,
und beim Rotor sieht man zehn Sekunden lang ohnehin nichts.

`frisch` ist kein Beiwerk: eine Nadel, die eine veraltete Stellung
zeigt, ohne das zu sagen, ist schlimmer als eine, die nichts zeigt --
so steht es schon im Kopf von RotorController.h.

── WAS toDouble() ANGERICHTET HAETTE ──────────────────────────────────

`RotorPeilung::lies()` nimmt NICHT einfach `toDouble()`:

  * aus "" und "abc" macht `toDouble()` eine 0 -- und NULL GRAD IST
    NORD. Eine leere Zeile haette die Antenne nach Norden gedreht.
  * NaN besteht jeden Bereichsvergleich, weil jeder Vergleich mit NaN
    falsch ist. Ein naiver Bereichstest laesst NaN durch.
  * 360 ist dieselbe Richtung wie 0 und gehoert angenommen; 361 ist
    ein Tippfehler. Ausserhalb wird ABGELEHNT statt umgerechnet: wer
    -90 schickt, hat sich vertan, und 270 Grad sind eine andere
    Antwort als "ich habe mich vertan".

── AUF DER SEITE ──────────────────────────────────────────────────────

Das Feld BLEIBT VERSTECKT, bis Longpath einen Rotor meldet. An der QRP
gibt es keinen, und ein leeres Feld, das nichts kann, ist auf einem
Telefon reiner Platzverbrauch.

Die Scheibe WAEHLT nur; gedreht wird mit dem Knopf darunter --
dieselbe Entscheidung wie bei der Sendetaste (Entwurf 1, 2026-10-04):
ein Tipp, der sofort einen Mast dreht, ist in einer Hosentasche eine
schlechte Idee. Eine Wahl verfaellt nach 15 s; sie gehoert sonst dem
Finger von vorhin.

Nord ist OBEN und gezaehlt wird im Uhrzeigersinn -- `atan2(x, -y)`,
nicht `atan2(y, x)`. Letzteres waere Ost = 0 und gegen den
Uhrzeigersinn und sieht auf einer runden Scheibe genauso plausibel
aus; der Pruefstand haelt beide Verwechslungen ausdruecklich fest.

Messing fuer die gemessene Stellung, Blau gestrichelt fuer das
gewaehlte Ziel -- die beiden duerfen nie zu verwechseln sein.

── GEPRUEFT ───────────────────────────────────────────────────────────

48 Pruefpunkte: 8 (RotorPeilung) + 9 (TCI, ueber den echten
WebSocket) + 31 (Seite, ohne Browser).

Live gegen die Attrappe: Osten angetippt -> "AUF 90° DREHEN", Knopf
gedrueckt -> der Rotor nimmt an, meldet "DREHT …", und die Nadel
wandert (143° -> 140° -> …). Danach steht der Knopf wieder auf
"RICHTUNG WÄHLEN" -- die Sicherung ist zu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oe5sos
oe5sos changed the base branch from feat/handfunke-aufwachen to main October 5, 2026 06:10
Zwei Konflikte, zwei verschiedene Antworten -- darum nachgesehen statt
pauschal aufgeloest:

  * handfunke/app.js: beide Stellen sind reine Ergaenzungen, mains Seite
    ist leer -> unsere behalten.
  * tests/CMakeLists.txt: main hat dort den Eintrag fuer
    tst_audio_rate_matcher (#198) ergaenzt, wir die beiden Rotor-Staende
    -> BEIDE behalten. Hier haette "unsere behalten" den Driftausgleich
    aus der Pruefliste geworfen, und niemand haette es gemerkt, weil die
    Datei trotzdem baut.

Danach neu konfiguriert und gegen frische Binaerdateien gemessen:

    tst_rotor_peilung        8
    tst_tci_rotor            9
    tst_audio_rate_matcher   9
    pruefe-rotor            31
    pruefe-spots            43
    pruefe-aufwachen        17

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@oe5sos
oe5sos merged commit 136d6d9 into main Oct 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant