diff --git a/app.go b/app.go index d99dbfc..9e8cc4a 100644 --- a/app.go +++ b/app.go @@ -57,6 +57,7 @@ import ( "hamlog/internal/relaydev" "hamlog/internal/rigctld" "hamlog/internal/rotator/dcu1" + "hamlog/internal/rotator/spid" "hamlog/internal/rotator/gs232" "hamlog/internal/rotator/pst" "hamlog/internal/rotgenius" @@ -14186,6 +14187,10 @@ type RotatorDevice struct { Transport string `json:"transport"` // ARCO: "tcp" (LAN) | "serial" (USB COM) ComPort string `json:"com_port"` // GS-232 serial transport Baud int `json:"baud"` // GS-232 serial baud (an ERC needs it; an ARCO ignores it) + // SpidModel picks the SPID dialect: "rot2prog" (RAS/BIG-RAS/MD-01/MD-02, + // azimuth + elevation) or "rot1prog" (the older azimuth-only controller). + // They differ in reply length and baud rate, so guessing is not an option. + SpidModel string `json:"spid_model,omitempty"` } // logicalRotor is one addressable rotor. Flattening the device list expands a @@ -14198,7 +14203,7 @@ type logicalRotor struct { // normRotorType clamps a rotor type to a known backend. func normRotorType(t string) string { - if t == "rotgenius" || t == "arco" || t == "dcu1" { + if t == "rotgenius" || t == "arco" || t == "dcu1" || t == "spid" { return t } return "pst" @@ -14224,6 +14229,7 @@ func deviceLink(d RotatorDevice, sub int) rotorLink { l := rotorLink{ Type: normRotorType(d.Type), Host: d.Host, Port: d.Port, Transport: d.Transport, ComPort: d.ComPort, Baud: d.Baud, HasElevation: d.HasElevation, + SpidModel: d.SpidModel, } if l.Host == "" { l.Host = "127.0.0.1" @@ -14232,7 +14238,12 @@ func deviceLink(d RotatorDevice, sub int) rotorLink { l.Port = rotatorDefaultPort(l.Type) } if l.Baud <= 0 { - l.Baud = 9600 + // A SPID runs at 600 or 1200 baud depending on the dialect; 0 lets its + // driver pick, and forcing 9600 here would have made every controller + // mute for a reason nobody would guess. + if l.Type != "spid" { + l.Baud = 9600 + } } if l.Transport != "serial" { l.Transport = "tcp" @@ -14368,6 +14379,7 @@ type rotorLink struct { ComPort string Baud int HasElevation bool + SpidModel string // SPID: "rot2prog" (default) | "rot1prog" } // activeRotorIndex returns the compass-selected rotor index, clamped to the @@ -14415,6 +14427,22 @@ func dcu1Client(l rotorLink) *dcu1.Client { return dcu1.New(l.Host, l.Port) } +// spidClient builds the SPID (AlfaSpid) client for a rotor. +// +// Serial only, and that is the point: these controllers have a COM port and +// nothing else. The request this answers was to drive them WITHOUT PstRotator +// sitting in between, so there is no network transport to offer. +// +// Baud 0 lets the driver take the dialect's documented default — 600 baud for +// Rot2Prog, 1200 for Rot1Prog. Those numbers look wrong and are not. +func spidClient(l rotorLink) *spid.Client { + m := spid.Rot2Prog + if l.SpidModel == string(spid.Rot1Prog) { + m = spid.Rot1Prog + } + return spid.New(l.ComPort, l.Baud, m) +} + // RotatorHeading is the live antenna heading for the status bar and compass. type RotatorHeading struct { Enabled bool `json:"enabled"` @@ -14481,6 +14509,16 @@ func (a *App) GetRotatorHeading() RotatorHeading { base.Azimuth = az base.Raw = raw return base + case "spid": + az, _, herr := spidClient(link).Heading() + if herr != nil { + base.Raw = herr.Error() + return base + } + base.OK = true + base.Azimuth = az + base.Raw = fmt.Sprintf("%d°", az) + return base case "dcu1": az, raw, herr := dcu1Client(link).Heading() if herr != nil { @@ -14531,6 +14569,8 @@ func (a *App) RotatorGoToPath(az int, el int, path string) error { return rotgenius.New(link.Host, link.Port).GoTo(link.Num, az) case "arco": return arcoClient(link).GoTo(az) + case "spid": + return spidClient(link).GoTo(az, el) case "dcu1": return dcu1Client(link).GoTo(az) default: @@ -14550,6 +14590,8 @@ func (a *App) RotatorStop() error { return rotgenius.New(link.Host, link.Port).Stop() case "arco": return arcoClient(link).Stop() + case "spid": + return spidClient(link).Stop() case "dcu1": return dcu1Client(link).Stop() default: @@ -14570,6 +14612,8 @@ func (a *App) RotatorPark() error { return fmt.Errorf("park is a PstRotator feature; not available on the Rotator Genius") case "arco": return fmt.Errorf("park is a PstRotator feature; not available over the ARCO GS-232 link") + case "spid": + return fmt.Errorf("park is a PstRotator feature; a SPID controller has no park command") case "dcu1": return fmt.Errorf("park is a PstRotator feature; not available over the DCU-1 link") default: @@ -14610,6 +14654,15 @@ func testRotorLink(l rotorLink) error { // GS-232 — without moving the antenna. _, _, err := arcoClient(l).Heading() return err + case "spid": + if strings.TrimSpace(l.ComPort) == "" { + return fmt.Errorf("select the SPID controller's COM port first") + } + // A status read proves the port, the baud rate and the dialect at once, + // without moving anything — and a wrong dialect shows up here as a reply + // of the wrong length rather than as an antenna that turns oddly later. + _, _, err := spidClient(l).Heading() + return err case "dcu1": if l.Transport == "serial" && strings.TrimSpace(l.ComPort) == "" { return fmt.Errorf("select the DCU-1 controller's COM port first") diff --git a/changelog.json b/changelog.json index c489428..769ca2d 100644 --- a/changelog.json +++ b/changelog.json @@ -3,10 +3,12 @@ "version": "0.25.3", "date": "", "en": [ - "Confirmations: a LoTW contact marked V (verified) counted for the awards but not for the band/mode matrix, the slot statistics or the row colours — they only accepted Y. One entity could read “validated” in Awards and “worked” beside it." + "Confirmations: a LoTW contact marked V (verified) counted for the awards but not for the band/mode matrix, the slot statistics or the row colours — they only accepted Y. One entity could read “validated” in Awards and “worked” beside it.", + "Rotators: SPID / AlfaSpid controllers are driven natively over their own COM port — Rot2Prog and Rot1Prog — so PstRotator is no longer needed in between. Two towers means two rotors, as before." ], "fr": [ - "Confirmations : un contact LoTW marqué V (vérifié) comptait pour les diplômes mais pas pour la matrice bande/mode, les statistiques de créneaux ni la coloration des lignes — elles n’acceptaient que Y. Une même entité pouvait être « validée » dans Diplômes et « travaillée » juste à côté." + "Confirmations : un contact LoTW marqué V (vérifié) comptait pour les diplômes mais pas pour la matrice bande/mode, les statistiques de créneaux ni la coloration des lignes — elles n’acceptaient que Y. Une même entité pouvait être « validée » dans Diplômes et « travaillée » juste à côté.", + "Rotors : les contrôleurs SPID / AlfaSpid sont pilotés nativement par leur propre port COM — Rot2Prog et Rot1Prog — sans passer par PstRotator. Deux pylônes restent deux rotors, comme avant." ] }, { diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 58185d1..75b4cfd 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -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 (
@@ -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. */}
@@ -3716,8 +3720,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan )} + {/* 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 && ( +
+ + +
+ )} {/* ARCO and DCU-1 controllers reach over the LAN (TCP) or a serial COM. */} - {isSerialCap && ( + {isSerialCap && !isSPID && (
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. */} +