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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user