import { GetRotatorHeading } from '../../wailsjs/go/main/App'; // One poll loop for the antenna heading, shared by everything that shows it. // // TWO PROBLEMS THIS FIXES, both of them invisible until you count. // // The status bar polled, and the Station Control compass polled, and they polled // the SAME binding — so with that tab open the rotator was asked twice as often // as either component believed. 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 a fixed interval has no good value. Three seconds moved the needle in // steps of about thirteen degrees on a turning antenna — a compass that jumps // rather than sweeps. Seven hundred milliseconds sweeps beautifully and opens // the controller's port about ten thousand times an hour to watch an antenna // that has not moved since breakfast. // // So: fast while it is turning, slow while it is not. "Turning" is not something // these controllers report — a SPID answers a position and nothing else — so it // is inferred from the position changing, and held for a few seconds after the // last change so the tail of a movement stays smooth. export type RotorHeading = { enabled: boolean; ok: boolean; azimuth: number; rotors?: string[]; active?: number; motorized?: boolean; raw?: string; }; const MOVING_MS = 500; // while the antenna is turning const IDLE_MS = 3000; // while it is parked — the rate everything used before const SETTLE_MS = 6000; // stay fast this long after the last movement const subs = new Set<(h: RotorHeading) => void>(); let timer: number | undefined; let inFlight = false; let lastAz: number | null = null; let lastMoveAt = 0; // The last heading anyone received, replayed to whoever subscribes next. // // Without it, opening Station Control left the compass blank for one or two // seconds while every other panel filled at once. Nothing was slow: the status // bar already had the loop running with a tick pending, so a component mounting // halfway through an idle interval simply waited out the rest of it. The // heading was known the whole time — it just had nowhere to be read from. let last: RotorHeading | null = null; function schedule(delay: number) { if (timer !== undefined) window.clearTimeout(timer); timer = window.setTimeout(tick, delay); } async function tick() { // A slow controller must not stack requests behind itself: at 600 baud a SPID // reply takes a fifth of a second on the wire alone, and a port that is still // open from the last poll cannot be opened again. if (inFlight) { schedule(MOVING_MS); return; } inFlight = true; try { const h = (await GetRotatorHeading()) as unknown as RotorHeading; if (h?.ok && typeof h.azimuth === 'number') { if (lastAz !== null && h.azimuth !== lastAz) lastMoveAt = Date.now(); lastAz = h.azimuth; } last = h; subs.forEach((fn) => { try { fn(h); } catch { /* a subscriber must not stop the loop */ } }); } catch { // Leave the last heading alone: a single failed poll on a shared serial port // is not news, and blanking the compass on one would make it flicker. } finally { inFlight = false; } if (subs.size > 0) schedule(Date.now() - lastMoveAt < SETTLE_MS ? MOVING_MS : IDLE_MS); else timer = undefined; } // subscribeRotorHeading starts the loop if it is not running and returns the // unsubscribe. The loop stops when the last subscriber leaves. export function subscribeRotorHeading(fn: (h: RotorHeading) => void): () => void { subs.add(fn); // Hand over what is already known, at once. An Alpha SPID poll is an open, // a read at 600 baud and a close, so even an immediate one takes a moment — // the cached heading is what makes the compass appear with the panel rather // than after it. In a microtask, so a subscriber is never called back before // subscribeRotorHeading has returned to it. if (last) { const h = last; queueMicrotask(() => { if (subs.has(fn)) fn(h); }); } if (timer === undefined && !inFlight) void tick(); return () => { subs.delete(fn); if (subs.size === 0 && timer !== undefined) { window.clearTimeout(timer); timer = undefined; } }; } // pokeRotorHeading polls at once and switches to the fast rate — call it after // commanding a move, so the needle starts sweeping on the click rather than on // whatever was left of a three-second tick. export function pokeRotorHeading(): void { lastMoveAt = Date.now(); if (timer !== undefined) window.clearTimeout(timer); timer = undefined; void tick(); }