feat(rotator): native SPID / AlfaSpid, so PstRotator can go
An operator with a tower at each end and an AlfaSpid on both wanted OpsLog to talk to them directly. Multiple rotors were already there; the missing half was the protocol. Rot2Prog and Rot1Prog, over the controller's own COM port. The byte layout is in the package doc and pinned by table tests against Hamlib's spid.c, the reference implementation — including the one trap this protocol has: digits go out as ASCII and come back as raw bytes. Send raw and the controller ignores you; read as ASCII and every heading is wrong by a constant nobody would recognise as such. Two things the UI has to get right because they cannot be detected: the dialect (different reply length AND baud rate) and the baud list, which for a SPID is 600 or 1200 — offering the usual 4800-and-up would have left the controller permanently mute. Both are handled: picking SPID sets serial transport, 600 baud and Rot2Prog, and the baud dropdown changes to the rates these use. The connection test reads a status rather than moving anything, so a wrong dialect shows up there as a reply of the wrong length instead of as an antenna that behaves oddly an hour later. Untested against real hardware — I have none. The frames are pinned; the controller is the only thing that can confirm the rest.
This commit is contained in:
@@ -3674,7 +3674,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const isRG = dev.type === 'rotgenius';
|
||||
const isARCO = dev.type === 'arco';
|
||||
const isDCU1 = dev.type === 'dcu1';
|
||||
const isSerialCap = isARCO || isDCU1; // COM-port or serial-over-IP controllers
|
||||
// A SPID has a COM port and nothing else — no network transport to
|
||||
// offer, which is the whole point of driving it without PstRotator.
|
||||
const isSPID = dev.type === 'spid';
|
||||
const isSerialCap = isARCO || isDCU1 || isSPID; // COM-port or serial-over-IP controllers
|
||||
const transport = dev.transport ?? 'tcp';
|
||||
return (
|
||||
<div key={dev.id || i} className="rounded-xl border border-border bg-card/40 p-3 space-y-3">
|
||||
@@ -3693,13 +3696,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{/* Each backend gets its default port: Rotator Genius 9006, ARCO 4001
|
||||
(placeholder — must match the ARCO's LAN menu), PstRotator 12000. */}
|
||||
<Select value={dev.type ?? 'pst'}
|
||||
onValueChange={(v) => patch(i, { type: v as any, port: v === 'rotgenius' ? 9006 : (v === 'arco' || v === 'dcu1') ? 4001 : 12000, ...(v === 'dcu1' ? { transport: 'serial' } : {}) })}>
|
||||
onValueChange={(v) => patch(i, { type: v as any, port: v === 'rotgenius' ? 9006 : (v === 'arco' || v === 'dcu1') ? 4001 : 12000, ...(v === 'dcu1' || v === 'spid' ? { transport: 'serial' } : {}), ...(v === 'spid' ? { baud: 600, spid_model: 'rot2prog' } : {}) })}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="pst">PstRotator (UDP)</SelectItem>
|
||||
<SelectItem value="rotgenius">Rotator Genius (4O3A, native)</SelectItem>
|
||||
<SelectItem value="arco">GS-232A controller (microHAM ARCO, ERC…)</SelectItem>
|
||||
<SelectItem value="dcu1">Hy-Gain DCU-1 (RotorCard DXA, Rotor-EZ, Green Heron)</SelectItem>
|
||||
<SelectItem value="spid">SPID / AlfaSpid (RAS, BIG-RAS, MD-01, MD-02)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
@@ -3716,8 +3720,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{/* SPID: pick the dialect. They differ in reply length AND baud
|
||||
rate, so this cannot be detected — a wrong choice is a
|
||||
controller that never answers. */}
|
||||
{isSPID && (
|
||||
<div className="space-y-1">
|
||||
<Label>{t('rot.spidModel')}</Label>
|
||||
<Select value={dev.spid_model || 'rot2prog'}
|
||||
onValueChange={(v) => patch(i, { spid_model: v as any, baud: v === 'rot1prog' ? 1200 : 600 })}>
|
||||
<SelectTrigger className="h-9"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="rot2prog">Rot2Prog (RAS, BIG-RAS/HR, MD-01, MD-02)</SelectItem>
|
||||
<SelectItem value="rot1prog">Rot1Prog (azimuth only)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
{/* ARCO and DCU-1 controllers reach over the LAN (TCP) or a serial COM. */}
|
||||
{isSerialCap && (
|
||||
{isSerialCap && !isSPID && (
|
||||
<div className="space-y-1">
|
||||
<Label>Connection</Label>
|
||||
<Select value={transport} onValueChange={(v) => patch(i, { transport: v as any })}>
|
||||
@@ -3751,10 +3771,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
<Button size="sm" variant="outline" className="h-9" onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
|
||||
<ArrowDown className="size-3.5 rotate-90" />
|
||||
</Button>
|
||||
<Select value={String(dev.baud || 9600)} onValueChange={(v) => patch(i, { baud: Number(v) })}>
|
||||
{/* A SPID runs at 600 or 1200 baud — not a typo, a pulse
|
||||
controller has nothing to say quickly. Offering only the
|
||||
usual rates would have left it permanently mute. */}
|
||||
<Select value={String(dev.baud || (isSPID ? 600 : 9600))} onValueChange={(v) => patch(i, { baud: Number(v) })}>
|
||||
<SelectTrigger className="h-9 w-28"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{[4800, 9600, 19200, 38400, 57600].map((b) => (
|
||||
{(isSPID ? [600, 1200, 2400, 4800, 9600] : [4800, 9600, 19200, 38400, 57600]).map((b) => (
|
||||
<SelectItem key={b} value={String(b)}>{b} baud</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -3784,6 +3807,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
{isRG && <p className="text-xs text-muted-foreground">{t('rot.rgHint')}</p>}
|
||||
{isARCO && <p className="text-xs text-muted-foreground">{t('rot.arcoHint')}</p>}
|
||||
{isDCU1 && <p className="text-xs text-muted-foreground">{t('rot.dcu1Hint')}</p>}
|
||||
{isSPID && <p className="text-xs text-muted-foreground">{t('rot.spidHint')}</p>}
|
||||
{/* Which antenna this rotor carries — only relevant with >1 rotor. */}
|
||||
{multi && (
|
||||
<div className="space-y-1 max-w-xs">
|
||||
|
||||
Reference in New Issue
Block a user