From 191e92ad62b2a8da43393a49b94d1b5a271fab7a Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Sun, 16 Aug 2026 13:39:23 +0200 Subject: [PATCH] perf(rotator): one adaptive heading poll for every controller type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asked whether the faster poll applies to all the rotator backends. It does — GetRotatorHeading is one binding over PstRotator, Rotator Genius, GS-232/ARCO, DCU-1 and SPID alike, and it reads only the ACTIVE rotor, so two towers do not double it. But counting what that costs turned up two things worth fixing. Every backend builds a fresh client per call — spid.New, gs232.NewSerial, dcu1.NewSerial, rotgenius.New — so one poll is one OPEN and CLOSE of a serial port or a TCP connection, not a read on a link already up. And the status bar and the Station Control compass each ran their own interval against that same binding, so with the tab open the controller was asked twice over. At the 700 ms I had just set, that was nearly three port opens a second on a 600-baud line. Both now share one loop, and it adapts: 500 ms while the position is changing, 3 s once it has been still for six seconds. These controllers do not report "moving" — a SPID answers a position and nothing else — so it is inferred from the position itself, and held briefly after the last change so the tail of a movement stays smooth. Commanding a move, a stop, or switching rotor polls at once, so the needle starts sweeping on the click. A slow controller cannot stack requests behind itself either: at 600 baud a SPID reply takes a fifth of a second on the wire alone, and a port still open from the last poll cannot be opened again. --- changelog.json | 6 +- frontend/src/App.tsx | 31 +++---- .../src/components/StationControlPanel.tsx | 27 +++--- frontend/src/lib/rotorHeading.ts | 87 +++++++++++++++++++ 4 files changed, 113 insertions(+), 38 deletions(-) create mode 100644 frontend/src/lib/rotorHeading.ts diff --git a/changelog.json b/changelog.json index fa43548..82a5cf4 100644 --- a/changelog.json +++ b/changelog.json @@ -10,7 +10,8 @@ "Fixed OpsLog re-tuning its own rig from its own radio broadcasts, which dropped the CAT link on every JTDX or WSJT-X “Fake It” transmission.", "New device: a bench power supply on Modbus RTU (BSIDE, Wanptek and kin) — its output switched from Station Control, with volts, amps and watts.", "SPID rotator: a Rot1Prog controller turned nearly a full circle the wrong way for every heading — its commands take three digits, not four.", - "The compass now follows a turning antenna smoothly, and a rotator test that only reads the heading says so instead of naming PstRotator." + "The compass sweeps with a turning antenna instead of jumping, and the controller is polled once for the whole app rather than by each panel.", + "A rotator test that only reads the heading now says so, instead of naming PstRotator for a move it never commanded." ], "fr": [ "Clic droit : mettre à jour le comté US des contacts sélectionnés depuis la base ULS, pour remplacer un comté renommé ou supprimé.", @@ -20,7 +21,8 @@ "Corrigé : OpsLog réaccordait sa propre radio depuis ses propres diffusions, ce qui coupait le lien CAT à chaque émission JTDX ou WSJT-X en « Fake It ».", "Nouvel appareil : alimentation de laboratoire en Modbus RTU (BSIDE, Wanptek et similaires) — sortie commutée depuis Contrôle station, avec V, A et W.", "Rotator SPID : un contrôleur Rot1Prog tournait presque un tour complet à l’envers pour chaque azimut — ses commandes tiennent trois chiffres, pas quatre.", - "Le compas suit désormais une antenne en rotation sans à-coups, et un test de rotator qui ne fait que lire l’azimut le dit au lieu de citer PstRotator." + "Le compas suit l’antenne en rotation au lieu de sauter, et le contrôleur est interrogé une fois pour toute l’application, plus par chaque panneau.", + "Un test de rotator qui ne fait que lire l’azimut le dit, au lieu de citer PstRotator pour un mouvement jamais commandé." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8179595..18144be 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -21,7 +21,7 @@ import { GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, EntryBandChanged, FlexApplyBandAntenna, FlexApplyBandPower, GetSecretStatus, UnlockSecrets, RefreshCtyDat, DownloadAllReferenceLists, - RotatorGoTo, RotatorStop, GetRotatorHeading, SetActiveRotor, + RotatorGoTo, RotatorStop, SetActiveRotor, GetDBConnectionInfo, GetLogbookRevision, GetUltrabeamStatus, SetUltrabeamDirection, UILog, GetAntGeniusStatus, GetAntGeniusSettings, AntGeniusActivate, @@ -107,6 +107,7 @@ import { DetailsPanel, type DetailsState } from '@/components/DetailsPanel'; import { SendSpotModal, type RecentSpotQSO } from '@/components/SendSpotModal'; import { WinkeyerPanel, type WKStatus, type WKMacro } from '@/components/WinkeyerPanel'; import { RotorCompass } from '@/components/RotorCompass'; +import { subscribeRotorHeading, pokeRotorHeading } from '@/lib/rotorHeading'; import { writeUiPref } from '@/lib/uiPref'; import { formatDateTimeUTC } from '@/lib/dateFormat'; import { titleCase, sentenceCase } from '@/lib/textCase'; @@ -2390,22 +2391,12 @@ export default function App() { }; }, []); - // Poll the rotator for the live antenna heading (status bar and compass). - // Cheap when it is disabled — the backend just reads settings and returns. - // - // 700 ms, not three seconds: a turning antenna moved the needle in steps of - // about thirteen degrees, which reads as a compass that jumps rather than one - // that sweeps. A heading query is a few bytes on a serial controller or one - // short exchange over TCP. - useEffect(() => { - let alive = true; - const tick = async () => { - try { const h: any = await GetRotatorHeading(); if (alive) setRotatorHeading(h); } catch {} - }; - tick(); - const id = window.setInterval(tick, 700); - return () => { alive = false; window.clearInterval(id); }; - }, []); + // The live antenna heading, from the shared poller in lib/rotorHeading — fast + // while the antenna turns, slow while it is parked, and ONE loop however many + // panels are watching. This used to be its own interval alongside Station + // Control's, so the controller was polled twice over whenever that tab was + // open, and every poll opens and closes the port. + useEffect(() => subscribeRotorHeading((h) => setRotatorHeading(h as any)), []); // Poll the Ultrabeam antenna for its connection + pattern direction. useEffect(() => { @@ -5496,7 +5487,7 @@ export default function App() {