fix(spid): a Rot1Prog takes three digits, so every target went the wrong way

Field report from a tower: on a RAK/RAU in Rot1Prog, every commanded heading
made the antenna want to turn nearly a full circle ANTICLOCKWISE — 0°, 90°, any
of them — while the heading readout, the stop button and everything else worked.

BuildSet framed the four-digit Rot2Prog azimuth for both dialects. A Rot1Prog
reads three: its replies are three digits in a five-byte frame, and its command
field matches. So 90° went out as "0450" and was read as 045 — 45 − 360 = −315°.
Every target landed 360° low, which is why it was always anticlockwise and
always nearly a full turn. The operator's own guess, that OpsLog was in 720°
mode, was the right instinct in the wrong place: the fault is a decimal shift,
not a range.

The round-trip test added here is the one that would have caught it without a
tower — every degree of the circle through the command builder and back through
the reply parser, which must return the degree that went in. The frame tests
pinned the Rot2Prog form against the reference and said nothing about the other
dialect.

Two more from the same report. A rotator test that only READS the heading — SPID,
ARCO, DCU-1 — said "Packet sent, the antenna should swing to north, check
PstRotator's UDP listener", naming a program not in the path for a move never
commanded; it now says the controller answered and nothing was moved. And the
compass polled every three seconds, so a turning antenna moved the needle in
steps of about thirteen degrees; the heading now has its own 700 ms tick while
the relay boards and the antenna controller stay at three seconds.
This commit is contained in:
2026-08-16 13:31:00 +02:00
parent 0924062ced
commit f405e71d7d
7 changed files with 133 additions and 16 deletions
+8 -3
View File
@@ -2390,15 +2390,20 @@ export default function App() {
};
}, []);
// Poll PstRotator for the live antenna heading (status bar). Cheap when the
// rotator is disabled (the backend just reads settings and returns).
// 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, 3000);
const id = window.setInterval(tick, 700);
return () => { alive = false; window.clearInterval(id); };
}, []);
+13 -1
View File
@@ -3086,7 +3086,19 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
setRotatorTest(null);
try {
await TestRotatorDevice(dev as any, sub);
setRotatorTest({ ok: true, msg: (dev as any).type === 'rotgenius' ? t('rot.testOkRG') : t('cat.rotatorOk') });
// Say what the test ACTUALLY did, which is not the same on every backend.
//
// PstRotator and a Rotator Genius are sent a move to 0°; a SPID, an ARCO
// and a DCU-1 are asked their heading and nothing turns. They all used to
// report "Packet sent — the antenna should swing to north. If it didn't,
// check PstRotator's UDP listener" — which named a program that is not in
// the path, for a move that was never commanded. An operator on a tower
// read that as the test having failed.
const type = (dev as any).type;
const msg = type === 'rotgenius' ? t('rot.testOkRG')
: (type === 'spid' || type === 'arco' || type === 'dcu1') ? t('rot.testOkRead')
: t('cat.rotatorOk');
setRotatorTest({ ok: true, msg });
} catch (e: any) {
setRotatorTest({ ok: false, msg: String(e?.message ?? e) });
} finally {
@@ -557,8 +557,16 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
useEffect(() => { loadDevices(); }, [loadDevices]);
useEffect(() => {
poll(); pollRot(); pollAnt();
const id = window.setInterval(() => { poll(); pollRot(); pollAnt(); }, 3000);
return () => window.clearInterval(id);
const id = window.setInterval(() => { poll(); pollAnt(); }, 3000);
// The rotor gets its own, faster tick. On three seconds the compass moved in
// steps of about thirteen degrees while the antenna was turning, which reads
// as a needle that jumps rather than one that sweeps — and an operator
// watching a tower wants to see it move. One heading query is a few bytes on
// a slow serial line or one short TCP exchange; the relay boards and the
// antenna controller are the expensive polls, and they stay at three
// seconds.
const rotId = window.setInterval(() => { pollRot(); }, 700);
return () => { window.clearInterval(id); window.clearInterval(rotId); };
}, [poll, pollRot, pollAnt, devices.length]);
const persistOrder = (next: string[]) => { setOrder(next); writeUiPref('opslog.stationOrder', JSON.stringify(next)); };
File diff suppressed because one or more lines are too long