perf(rotator): one adaptive heading poll for every controller type
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.
This commit is contained in:
+11
-20
@@ -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() {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => RotatorStop().catch((err) => setError(String(err?.message ?? err)))}
|
||||
onClick={() => { pokeRotorHeading(); RotatorStop().catch((err) => setError(String(err?.message ?? err))); }}
|
||||
title="Stop rotation"
|
||||
className="px-1.5 py-0.5 border-l border-info-border text-danger hover:bg-danger-muted hover:text-danger-muted-foreground cursor-pointer transition-colors"
|
||||
>
|
||||
@@ -6286,8 +6277,8 @@ export default function App() {
|
||||
rotorEnabled={rotatorHeading.enabled && rotatorHeading.ok}
|
||||
rotors={(rotatorHeading as any).rotors}
|
||||
activeRotor={(rotatorHeading as any).active}
|
||||
onSelectRotor={(i) => { SetActiveRotor(i).then(() => GetRotatorHeading()).then((h: any) => setRotatorHeading(h)).catch((err) => setError(String(err?.message ?? err))); }}
|
||||
onGoto={(az) => RotatorGoTo(Math.round(az), -1).catch((err) => setError(String(err?.message ?? err)))}
|
||||
onSelectRotor={(i) => { SetActiveRotor(i).then(pokeRotorHeading).catch((err) => setError(String(err?.message ?? err))); }}
|
||||
onGoto={(az) => { RotatorGoTo(Math.round(az), -1).then(pokeRotorHeading).catch((err) => setError(String(err?.message ?? err))); }}
|
||||
onClose={() => { setShowRotor(false); writeUiPref('opslog.showRotor', '0'); }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user